From fed672acbfa5ad243ea3511b73f9ef391e0f298c Mon Sep 17 00:00:00 2001 From: m6z1 Date: Mon, 20 Apr 2026 19:43:57 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=94=8C=EB=A0=88=EC=9D=B4=EC=8A=A4=20=ED=99=80?= =?UTF-8?q?=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/layout/activity_normal_explore.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/activity_normal_explore.xml b/app/src/main/res/layout/activity_normal_explore.xml index 26f1d334a..ee8a0670c 100644 --- a/app/src/main/res/layout/activity_normal_explore.xml +++ b/app/src/main/res/layout/activity_normal_explore.xml @@ -75,6 +75,7 @@ android:autofillHints="@null" android:background="@null" android:cursorVisible="true" + android:hint="@string/explore_normal_search_hint" android:imeOptions="actionSearch" android:inputType="text" android:maxLines="1" @@ -83,6 +84,7 @@ android:textAppearance="@style/label1" android:textColor="@color/black" android:textColorHighlight="@color/primary_50_F1EFFF" + android:textColorHint="@color/gray_200_949399" android:theme="@style/Theme.Websoso.EditText" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/iv_normal_explore_cancel_button" From 0bda6f49029d9d01c6789389fcff38c5449d0bc5 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Tue, 7 Apr 2026 09:48:55 +0900 Subject: [PATCH 02/13] =?UTF-8?q?fix:=2016kb=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 6 ++++++ gradle/libs.versions.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 199855442..0db570a04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,12 @@ android { } } +configurations.all { + resolutionStrategy { + force("pl.droidsonroids.gif:android-gif-drawable:1.2.29") + } +} + // 룸 디비 제거 // 좋아요 기능 좀더 생각해보기 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25217f871..2bf76af97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ lottie = "6.7.1" pull-to-refresh = "1.5.2" # Social Login Libraries -kakao = "2.23.1" +kakao = "2.23.2" # Ktlint ktlint = "14.0.1" From 524fc18038ec68bc8cbbccbb3066d6e8deb218dd Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Thu, 9 Apr 2026 12:32:49 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20kakao=20version=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 5 ++--- gradle/libs.versions.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0db570a04..d1fdcdde3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,13 +76,12 @@ android { configurations.all { resolutionStrategy { + // SSPullToRefresh가 android-gif-drawable 1.2.28을 끌고 옴 + // 1.2.28은 16KB 페이지 미지원, 1.2.29에서 지원 추가됨 force("pl.droidsonroids.gif:android-gif-drawable:1.2.29") } } -// 룸 디비 제거 -// 좋아요 기능 좀더 생각해보기 - dependencies { // 프로젝트 의존성 implementation(projects.core.resource) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2bf76af97..25217f871 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ lottie = "6.7.1" pull-to-refresh = "1.5.2" # Social Login Libraries -kakao = "2.23.2" +kakao = "2.23.1" # Ktlint ktlint = "14.0.1" From 552801fb1eec9569258368763ffb6f6699d4a3ca Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Tue, 14 Apr 2026 10:29:49 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix:=20swiperefreshlayout=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 10 +--------- .../websoso/ui/novelFeed/NovelFeedFragment.kt | 16 +++------------- app/src/main/res/layout/fragment_novel_feed.xml | 4 ++-- gradle/libs.versions.toml | 4 ++-- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d1fdcdde3..f87f0f070 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,14 +74,6 @@ android { } } -configurations.all { - resolutionStrategy { - // SSPullToRefresh가 android-gif-drawable 1.2.28을 끌고 옴 - // 1.2.28은 16KB 페이지 미지원, 1.2.29에서 지원 추가됨 - force("pl.droidsonroids.gif:android-gif-drawable:1.2.29") - } -} - dependencies { // 프로젝트 의존성 implementation(projects.core.resource) @@ -136,7 +128,7 @@ dependencies { // UI 관련 유틸 라이브러리 implementation(libs.dots.indicator) // ViewPager2 indicator implementation(libs.lottie) // Lottie 애니메이션 - implementation(libs.pull.to.refresh) // Pull 새로고침 + implementation(libs.swipe.refresh.layout) // Pull 새로고침 // Third-party SDK implementation(libs.kakao) // 카카오 로그인 API diff --git a/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt b/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt index b095262f1..35b7577cf 100644 --- a/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams import android.view.WindowManager.LayoutParams.WRAP_CONTENT import android.widget.PopupWindow import android.widget.TextView @@ -341,17 +340,8 @@ class NovelFeedFragment : BaseFragment(R.layout.fragme } private fun setupRefreshView() { - binding.sptrNovelFeedRefresh.apply { - setRefreshViewParams( - params = LayoutParams( - 30.toIntPxFromDp(), - 30.toIntPxFromDp(), - ), - ) - setLottieAnimation("lottie_websoso_loading.json") - setOnRefreshListener { - novelFeedViewModel.updateRefreshedFeeds(novelId) - } + binding.sptrNovelFeedRefresh.setOnRefreshListener { + novelFeedViewModel.updateRefreshedFeeds(novelId) } } @@ -375,7 +365,7 @@ class NovelFeedFragment : BaseFragment(R.layout.fragme !novelFeedUiState.loading -> { binding.wllNovelFeed.setWebsosoLoadingVisibility(false) - binding.sptrNovelFeedRefresh.setRefreshing(false) + binding.sptrNovelFeedRefresh.isRefreshing = false updateFeeds(novelFeedUiState) } } diff --git a/app/src/main/res/layout/fragment_novel_feed.xml b/app/src/main/res/layout/fragment_novel_feed.xml index d101380a8..2f688463a 100644 --- a/app/src/main/res/layout/fragment_novel_feed.xml +++ b/app/src/main/res/layout/fragment_novel_feed.xml @@ -21,7 +21,7 @@ android:background="@color/white" app:layout_constraintTop_toTopOf="parent" /> - - + Date: Thu, 30 Apr 2026 19:33:15 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20sosopick=EC=9D=84=20ExploreFragme?= =?UTF-8?q?nt=EC=97=90=EC=84=9C=20NormalExploreActivity=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EB=B0=94=20=EC=95=84=EB=9E=98=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검색 실행 전에는 표시되고, 검색 후에는 숨김 처리. 취소 버튼으로 검색어 지우면 다시 표시. --- .../ui/main/explore/ExploreFragment.kt | 42 ------------ .../ui/normalExplore/NormalExploreActivity.kt | 15 +++++ .../normalExplore/NormalExploreViewModel.kt | 24 +++++++ .../res/layout/activity_normal_explore.xml | 65 ++++++++++++++++++- app/src/main/res/layout/fragment_explore.xml | 58 ----------------- 5 files changed, 103 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt b/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt index 83359a1c4..3206a25c5 100644 --- a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt @@ -2,16 +2,13 @@ package com.into.websoso.ui.main.explore import android.os.Bundle import android.view.View -import androidx.fragment.app.viewModels import com.into.websoso.R import com.into.websoso.core.common.ui.base.BaseFragment import com.into.websoso.core.common.util.SingleEventHandler import com.into.websoso.core.common.util.tracker.Tracker import com.into.websoso.databinding.FragmentExploreBinding import com.into.websoso.ui.detailExplore.DetailExploreDialogBottomSheet -import com.into.websoso.ui.main.explore.adapter.SosoPickAdapter import com.into.websoso.ui.normalExplore.NormalExploreActivity -import com.into.websoso.ui.novelDetail.NovelDetailActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -20,9 +17,7 @@ class ExploreFragment : BaseFragment(R.layout.fragment_e @Inject lateinit var tracker: Tracker - private val sosoPickAdapter: SosoPickAdapter by lazy { SosoPickAdapter(::navigateToNovelDetail) } private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - private val exploreViewModel: ExploreViewModel by viewModels() override fun onViewCreated( view: View, @@ -30,28 +25,11 @@ class ExploreFragment : BaseFragment(R.layout.fragment_e ) { super.onViewCreated(view, savedInstanceState) - initSosoPickAdapter() onNormalSearchButtonClick() onDetailExploreButtonClick() - onReloadPageButtonClick() - setupObserver() tracker.trackEvent("search") } - private fun initSosoPickAdapter() { - binding.rvExploreSosoPick.adapter = sosoPickAdapter - binding.rvExploreSosoPick.setHasFixedSize(true) - } - - private fun navigateToNovelDetail(novelId: Long) { - tracker.trackEvent( - eventName = "soso_pick", - properties = mapOf("novelId" to novelId), - ) - val intent = NovelDetailActivity.getIntent(requireContext(), novelId) - startActivity(intent) - } - private fun onNormalSearchButtonClick() { binding.clExploreNormalSearch.setOnClickListener { tracker.trackEvent("general_search") @@ -73,26 +51,6 @@ class ExploreFragment : BaseFragment(R.layout.fragment_e detailExploreBottomSheet.show(childFragmentManager, DETAIL_BOTTOM_SHEET_TAG) } - private fun onReloadPageButtonClick() { - binding.wllExplore.setReloadButtonClickListener { - binding.wllExplore.setErrorLayoutVisibility(false) - exploreViewModel.updateSosoPicks() - } - } - - private fun setupObserver() { - exploreViewModel.uiState.observe(viewLifecycleOwner) { uiState -> - when { - uiState.loading -> binding.wllExplore.setWebsosoLoadingVisibility(true) - uiState.error -> binding.wllExplore.setLoadingLayoutVisibility(false) - !uiState.loading -> { - binding.wllExplore.setWebsosoLoadingVisibility(false) - sosoPickAdapter.submitList(uiState.sosoPicks) - } - } - } - } - companion object { private const val DETAIL_BOTTOM_SHEET_TAG = "DetailExploreDialogBottomSheet" const val TAG = "ExploreFragment" diff --git a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt index a4c77c655..a55c9b2aa 100644 --- a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt +++ b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt @@ -17,6 +17,7 @@ import com.into.websoso.core.common.util.SingleEventHandler import com.into.websoso.core.common.util.tracker.Tracker import com.into.websoso.core.resource.R.string.novel_inquire_link import com.into.websoso.databinding.ActivityNormalExploreBinding +import com.into.websoso.ui.main.explore.adapter.SosoPickAdapter import com.into.websoso.ui.normalExplore.adapter.NormalExploreAdapter import com.into.websoso.ui.normalExplore.adapter.NormalExploreItemType.Header import com.into.websoso.ui.normalExplore.adapter.NormalExploreItemType.Loading @@ -37,6 +38,7 @@ class NormalExploreActivity : BaseActivity(activit ::navigateToInquire, ) } + private val sosoPickAdapter: SosoPickAdapter by lazy { SosoPickAdapter(::navigateToNovelDetailFromSosoPick) } private val normalExploreViewModel: NormalExploreViewModel by viewModels() private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } @@ -67,6 +69,7 @@ class NormalExploreActivity : BaseActivity(activit ), ) } + rvNormalExploreSosoPick.adapter = sosoPickAdapter onClick = onNormalExploreButtonClick() } } @@ -153,6 +156,14 @@ class NormalExploreActivity : BaseActivity(activit } } + private fun navigateToNovelDetailFromSosoPick(novelId: Long) { + singleEventHandler.throttleFirst { + tracker.trackEvent("soso_pick", mapOf("novelId" to novelId)) + val intent = NovelDetailActivity.getIntent(this, novelId) + startActivity(intent) + } + } + private fun navigateToInquire() { val inquireUrl = getString(novel_inquire_link) val intent = Intent(ACTION_VIEW, Uri.parse(inquireUrl)) @@ -183,6 +194,10 @@ class NormalExploreActivity : BaseActivity(activit normalExploreViewModel.searchWord.observe(this) { normalExploreViewModel.validateSearchWordClearButton() } + + normalExploreViewModel.sosoPicks.observe(this) { sosoPicks -> + sosoPickAdapter.submitList(sosoPicks) + } } private fun updateView(uiState: NormalExploreUiState) { diff --git a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt index 47972292c..5ee3a74a8 100644 --- a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.into.websoso.data.model.SosoPickEntity +import com.into.websoso.data.repository.NovelRepository import com.into.websoso.domain.usecase.GetNormalExploreResultUseCase import com.into.websoso.ui.mapper.toUi import com.into.websoso.ui.normalExplore.NormalExploreActivity.Companion.SEARCH_AUTHOR @@ -18,6 +20,7 @@ class NormalExploreViewModel @Inject constructor( private val getNormalExploreResultUseCase: GetNormalExploreResultUseCase, + private val novelRepository: NovelRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { private val _uiState: MutableLiveData = @@ -36,12 +39,29 @@ class NormalExploreViewModel private val _isNovelResultEmptyBoxVisibility: MutableLiveData = MutableLiveData(false) val isNovelResultEmptyBoxVisibility: LiveData get() = _isNovelResultEmptyBoxVisibility + private val _sosoPicks: MutableLiveData> = MutableLiveData(emptyList()) + val sosoPicks: LiveData> get() = _sosoPicks + + private val _isSosoPickVisible: MutableLiveData = MutableLiveData(initialSearchWord.isBlank()) + val isSosoPickVisible: LiveData get() = _isSosoPickVisible + init { + fetchSosoPicks() if (initialSearchWord.isNotBlank()) { updateSearchResult(isSearchButtonClick = true) } } + private fun fetchSosoPicks() { + viewModelScope.launch { + runCatching { + novelRepository.fetchSosoPicks() + }.onSuccess { result -> + _sosoPicks.value = result.novels + } + } + } + fun updateSearchWord(searchWord: String) { _searchWord.value = searchWord savedStateHandle[SEARCH_AUTHOR] = searchWord @@ -51,6 +71,9 @@ class NormalExploreViewModel if ((_searchWord.value.isNullOrBlank() || _uiState.value?.isLoadable == false) && !isSearchButtonClick) { return } + if (isSearchButtonClick) { + _isSosoPickVisible.value = false + } viewModelScope.launch { _uiState.value = _uiState.value?.copy(loading = isSearchButtonClick) runCatching { @@ -90,5 +113,6 @@ class NormalExploreViewModel fun updateSearchWordEmpty() { _searchWord.value = "" savedStateHandle[SEARCH_AUTHOR] = "" + _isSosoPickVisible.value = true } } diff --git a/app/src/main/res/layout/activity_normal_explore.xml b/app/src/main/res/layout/activity_normal_explore.xml index ee8a0670c..50c13ecb5 100644 --- a/app/src/main/res/layout/activity_normal_explore.xml +++ b/app/src/main/res/layout/activity_normal_explore.xml @@ -125,6 +125,69 @@ + + + + + + + + + + + + - - - - - - - - - From f93a973ce45a87e4d25138b53bae52ec3604c170 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 1 May 2026 13:42:19 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20DetailExplore=20=ED=83=90?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=ED=9A=8D=20=EB=B3=80=EA=B2=BD=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 홈 화면 검색바 아래 상세 탐색 진입 카드 추가 - DetailExploreDialogBottomSheet → DetailExploreActivity (Compose)로 전환 - 별점 기준을 단일값 → 0.0~5.0 범위 슬라이더로 변경 - 정보/키워드 탭, 커스텀 RangeSlider, S3 카테고리 이미지 연결 --- app/src/main/AndroidManifest.xml | 4 + .../ui/detailExplore/DetailExploreActivity.kt | 72 +++ .../DetailExploreDialogBottomSheet.kt | 154 ------- .../ui/detailExplore/DetailExploreScreen.kt | 75 +++ .../detailExplore/DetailExploreViewModel.kt | 38 +- .../component/DetailExploreAppBar.kt | 154 +++++++ .../component/DetailExploreCtaButton.kt | 52 +++ .../component/DetailExploreInfoTab.kt | 240 ++++++++++ .../component/DetailExploreKeywordTab.kt | 431 ++++++++++++++++++ .../component/RatingRangeSlider.kt | 158 +++++++ .../detailExplore/component/SelectableChip.kt | 90 ++++ .../info/DetailExploreInfoFragment.kt | 192 -------- .../keyword/DetailExploreKeywordFragment.kt | 322 ------------- .../ui/main/explore/ExploreFragment.kt | 10 +- .../into/websoso/ui/main/home/HomeFragment.kt | 13 + .../layout/fragment_detail_explore_info.xml | 184 -------- .../fragment_detail_explore_keyword.xml | 256 ----------- app/src/main/res/layout/fragment_home.xml | 61 ++- .../img_home_detail_search.png | Bin 0 -> 86349 bytes core/resource/src/main/res/values/strings.xml | 2 + 20 files changed, 1381 insertions(+), 1127 deletions(-) create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreDialogBottomSheet.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt create mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt delete mode 100644 app/src/main/res/layout/fragment_detail_explore_info.xml delete mode 100644 app/src/main/res/layout/fragment_detail_explore_keyword.xml create mode 100644 core/resource/src/main/res/drawable-xxhdpi/img_home_detail_search.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f87ca51cd..a67f2cedf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -96,6 +96,10 @@ android:name=".ui.novelRating.NovelRatingActivity" android:exported="false" android:screenOrientation="portrait" /> + (R.layout.dialog_detail_explore) { - private val detailExploreInfoFragment: DetailExploreInfoFragment by lazy { DetailExploreInfoFragment() } - private val detailExploreKeywordFragment: DetailExploreKeywordFragment by lazy { DetailExploreKeywordFragment() } - private val detailExploreViewModel: DetailExploreViewModel by activityViewModels() - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - setupBottomSheet() - initDetailExploreFragment() - onReplaceFragmentButtonClick() - onBottomSheetExitButtonClick() - setupObserver() - } - - private fun setupBottomSheet() { - (dialog as BottomSheetDialog).apply { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.isDraggable = false - behavior.isHideable = false - setCancelable(false) - } - } - - private fun initDetailExploreFragment() { - childFragmentManager.commit { - add( - R.id.fcv_detail_explore, - detailExploreInfoFragment, - ) - } - } - - private fun onReplaceFragmentButtonClick() { - binding.tvDetailExploreInfoButton.setOnClickListener { - singleEventHandler.throttleFirst { - switchFragment(INFO) - detailExploreViewModel.updateIsSearchKeywordProceeding(false) - } - } - - binding.tvDetailExploreKeywordButton.setOnClickListener { - singleEventHandler.throttleFirst { - switchFragment(KEYWORD) - } - } - } - - private fun switchFragment(selectedFragmentTitle: SelectedFragmentTitle) { - val fragmentToShow = when (selectedFragmentTitle) { - INFO -> detailExploreInfoFragment - KEYWORD -> { - if (childFragmentManager.findFragmentById(R.id.fcv_detail_explore) !is DetailExploreKeywordFragment) { - childFragmentManager.commit { - add(R.id.fcv_detail_explore, detailExploreKeywordFragment) - } - } - detailExploreKeywordFragment - } - } - - val fragmentToHide = when (selectedFragmentTitle) { - INFO -> detailExploreKeywordFragment - KEYWORD -> detailExploreInfoFragment - } - - childFragmentManager.commit { - setReorderingAllowed(true) - show(fragmentToShow) - hide(fragmentToHide) - } - - updateButtonColors(selectedFragmentTitle) - } - - private fun updateButtonColors(selectedFragmentTitle: SelectedFragmentTitle) { - val selectedColor = requireContext().getColor(R.color.primary_100_6A5DFD) - val defaultColor = requireContext().getColor(R.color.gray_200_949399) - - when (selectedFragmentTitle) { - INFO -> { - binding.apply { - tvDetailExploreInfoButton.setTextColor(selectedColor) - tvDetailExploreKeywordButton.setTextColor(defaultColor) - viewDetailExploreSelectedInfoTab.isVisible = true - viewDetailExploreSelectedKeywordTab.isVisible = false - } - } - - KEYWORD -> { - binding.apply { - tvDetailExploreKeywordButton.setTextColor(selectedColor) - tvDetailExploreInfoButton.setTextColor(defaultColor) - viewDetailExploreSelectedInfoTab.isVisible = false - viewDetailExploreSelectedKeywordTab.isVisible = true - } - } - } - } - - private fun onBottomSheetExitButtonClick() { - binding.ivDetailExploreExitButton.setOnClickListener { - detailExploreViewModel.updateSelectedInfoValueClear() - detailExploreViewModel.updateSelectedKeywordValueClear() - dismiss() - } - } - - private fun setupObserver() { - detailExploreViewModel.isInfoChipSelected.observe(viewLifecycleOwner) { isVisible -> - binding.ivDetailExploreInfoActiveDot.isVisible = isVisible - } - - detailExploreViewModel.isKeywordChipSelected.observe(viewLifecycleOwner) { isVisible -> - binding.ivDetailExploreKeywordActiveDot.isVisible = isVisible - } - } - - override fun onPause() { - detailExploreViewModel.updateSelectedInfoValueClear() - detailExploreViewModel.updateSelectedKeywordValueClear() - dismiss() - super.onPause() - } - - companion object { - fun newInstance(): DetailExploreDialogBottomSheet = DetailExploreDialogBottomSheet() - } -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt new file mode 100644 index 000000000..528f37091 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt @@ -0,0 +1,75 @@ +package com.into.websoso.ui.detailExplore + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.into.websoso.core.designsystem.theme.White +import com.into.websoso.ui.detailExplore.component.DetailExploreAppBar +import com.into.websoso.ui.detailExplore.component.DetailExploreCtaButton +import com.into.websoso.ui.detailExplore.component.DetailExploreInfoTab +import com.into.websoso.ui.detailExplore.component.DetailExploreKeywordTab +import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.INFO +import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.KEYWORD + +@Composable +fun DetailExploreScreen( + viewModel: DetailExploreViewModel, + onBackClick: () -> Unit, + onSearchClick: () -> Unit, + onKeywordInquireClick: () -> Unit, +) { + var selectedTab by rememberSaveable { mutableStateOf(INFO) } + + val isInfoChipSelected by viewModel.isInfoChipSelected.observeAsState(false) + val isKeywordChipSelected by viewModel.isKeywordChipSelected.observeAsState(false) + + BackHandler(onBack = onBackClick) + + Box( + modifier = Modifier + .fillMaxSize() + .background(White) + .windowInsetsPadding(WindowInsets.systemBars) + .padding(top = 10.dp), + ) { + Column(modifier = Modifier.fillMaxSize()) { + DetailExploreAppBar( + selectedTab = selectedTab, + isInfoChipActive = isInfoChipSelected, + isKeywordChipActive = isKeywordChipSelected, + onTabSelected = { selectedTab = it }, + onBackClick = onBackClick, + onResetClick = { + when (selectedTab) { + INFO -> viewModel.updateSelectedInfoValueClear() + KEYWORD -> viewModel.updateSelectedKeywordValueClear() + } + }, + ) + Box(modifier = Modifier.weight(1f)) { + when (selectedTab) { + INFO -> DetailExploreInfoTab(viewModel = viewModel) + KEYWORD -> DetailExploreKeywordTab( + viewModel = viewModel, + onKeywordInquireClick = onKeywordInquireClick, + ) + } + } + DetailExploreCtaButton(onClick = onSearchClick) + } + } +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt index 66472eabe..3f46954dc 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt @@ -29,10 +29,15 @@ class DetailExploreViewModel private val _selectedStatus: MutableLiveData = MutableLiveData() val selectedStatus: LiveData get() = _selectedStatus - private val _selectedRating: MutableLiveData = MutableLiveData() - val selectedRating: LiveData get() = _selectedRating + private val _selectedRatingMin: MutableLiveData = MutableLiveData(RATING_MIN) + val selectedRatingMin: LiveData get() = _selectedRatingMin - val ratings: List = listOf(3.5f, 4.0f, 4.5f, 4.8f) + private val _selectedRatingMax: MutableLiveData = MutableLiveData(RATING_MAX) + val selectedRatingMax: LiveData get() = _selectedRatingMax + + val selectedRating: LiveData = _selectedRatingMin.map { min -> + if (min > RATING_MIN) min else null + } private val _isInfoChipSelected: MediatorLiveData = MediatorLiveData(false) val isInfoChipSelected: LiveData get() = _isInfoChipSelected @@ -51,7 +56,10 @@ class DetailExploreViewModel _isInfoChipSelected.addSource(_selectedStatus) { _isInfoChipSelected.value = isInfoChipSelectedEnabled() } - _isInfoChipSelected.addSource(_selectedRating) { + _isInfoChipSelected.addSource(_selectedRatingMin) { + _isInfoChipSelected.value = isInfoChipSelectedEnabled() + } + _isInfoChipSelected.addSource(_selectedRatingMax) { _isInfoChipSelected.value = isInfoChipSelectedEnabled() } @@ -61,15 +69,18 @@ class DetailExploreViewModel private fun isInfoChipSelectedEnabled(): Boolean { val isGenreChipSelected: Boolean = _selectedGenres.value?.isNotEmpty() == true val isStatusChipSelected: Boolean = _selectedStatus.value != null - val isRatingChipSelected: Boolean = _selectedRating.value != null + val isRatingRangeNarrowed: Boolean = + (_selectedRatingMin.value ?: RATING_MIN) > RATING_MIN || + (_selectedRatingMax.value ?: RATING_MAX) < RATING_MAX - return isGenreChipSelected || isStatusChipSelected || isRatingChipSelected + return isGenreChipSelected || isStatusChipSelected || isRatingRangeNarrowed } fun updateSelectedInfoValueClear() { _selectedGenres.value = mutableListOf() _selectedStatus.value = null - _selectedRating.value = null + _selectedRatingMin.value = RATING_MIN + _selectedRatingMax.value = RATING_MAX } fun updateSelectedGenres(genre: Genre) { @@ -92,8 +103,9 @@ class DetailExploreViewModel _selectedStatus.value = status } - fun updateSelectedRating(rating: Float?) { - _selectedRating.value = rating + fun updateSelectedRatingRange(min: Float, max: Float) { + _selectedRatingMin.value = min + _selectedRatingMax.value = max } fun updateKeyword(searchWord: String?) { @@ -188,4 +200,10 @@ class DetailExploreViewModel ) } } - } + + companion object { + const val RATING_MIN = 0.0f + const val RATING_MAX = 5.0f + const val RATING_STEP = 0.5f + } + } \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt new file mode 100644 index 000000000..68b8e33f4 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt @@ -0,0 +1,154 @@ +package com.into.websoso.ui.detailExplore.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.graphics.Color +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.into.websoso.core.common.util.clickableWithoutRipple +import com.into.websoso.core.designsystem.theme.Gray200 +import com.into.websoso.core.designsystem.theme.Gray300 +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.core.designsystem.theme.White +import com.into.websoso.core.resource.R.drawable.ic_detail_explore_reset +import com.into.websoso.core.resource.R.drawable.ic_navigate_left +import com.into.websoso.core.resource.R.string.detail_explore_info +import com.into.websoso.core.resource.R.string.detail_explore_keyword +import com.into.websoso.core.resource.R.string.detail_explore_reset +import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle +import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.INFO +import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.KEYWORD + +@Composable +fun DetailExploreAppBar( + selectedTab: SelectedFragmentTitle, + isInfoChipActive: Boolean, + isKeywordChipActive: Boolean, + onTabSelected: (SelectedFragmentTitle) -> Unit, + onBackClick: () -> Unit, + onResetClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .background(White) + .fillMaxWidth() + .height(44.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(6.dp)) + Box( + modifier = Modifier + .size(44.dp) + .clickableWithoutRipple(onClick = onBackClick), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = ic_navigate_left), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } + TabLabel( + label = stringResource(detail_explore_info), + isSelected = selectedTab == INFO, + isActive = isInfoChipActive, + slotWidth = 68.dp, + onClick = { onTabSelected(INFO) }, + ) + TabLabel( + label = stringResource(detail_explore_keyword), + isSelected = selectedTab == KEYWORD, + isActive = isKeywordChipActive, + slotWidth = 69.dp, + onClick = { onTabSelected(KEYWORD) }, + ) + Spacer(modifier = Modifier.weight(1f)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + .clickableWithoutRipple(onClick = onResetClick), + ) { + Image( + painter = painterResource(id = ic_detail_explore_reset), + contentDescription = null, + modifier = Modifier.size(14.dp), + ) + Text( + text = stringResource(detail_explore_reset), + style = WebsosoTheme.typography.title2, + color = Gray300, + ) + } + } +} + +@Composable +private fun TabLabel( + label: String, + isSelected: Boolean, + isActive: Boolean, + slotWidth: Dp, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .width(slotWidth) + .height(30.dp) + .clickableWithoutRipple(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Row(verticalAlignment = Alignment.Top) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.width(IntrinsicSize.Max), + ) { + Text( + text = label, + style = WebsosoTheme.typography.title1, + color = if (isSelected) Primary100 else Gray200, + ) + Spacer(modifier = Modifier.weight(1f)) + if (isSelected) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background(Primary100), + ) + } + } + Spacer(modifier = Modifier.width(3.dp)) + Box( + modifier = Modifier + .size(4.dp) + .background( + color = if (isActive) Primary100 else Color.Transparent, + shape = CircleShape, + ), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt new file mode 100644 index 000000000..48ac7a9d0 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt @@ -0,0 +1,52 @@ +package com.into.websoso.ui.detailExplore.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.into.websoso.core.common.util.SingleEventHandler +import com.into.websoso.core.common.util.clickableWithoutRipple +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.core.designsystem.theme.White +import com.into.websoso.core.resource.R.string.detail_explore_search_novel + +@Composable +fun DetailExploreCtaButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val singleEventHandler = remember { SingleEventHandler.from() } + Column( + modifier = modifier + .fillMaxWidth() + .background(White) + .padding(horizontal = 8.dp, vertical = 10.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(Primary100) + .clickableWithoutRipple { singleEventHandler.throttleFirst(event = onClick) } + .padding(vertical = 18.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(detail_explore_search_novel), + style = WebsosoTheme.typography.title1, + color = White, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt new file mode 100644 index 000000000..7606c3996 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt @@ -0,0 +1,240 @@ +package com.into.websoso.ui.detailExplore.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.into.websoso.core.designsystem.theme.Black +import com.into.websoso.core.designsystem.theme.Gray50 +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.core.resource.R.string.detail_explore_info_genre +import com.into.websoso.core.resource.R.string.detail_explore_info_rating +import com.into.websoso.core.resource.R.string.detail_explore_info_rating_range +import com.into.websoso.core.resource.R.string.detail_explore_info_rating_value +import com.into.websoso.core.resource.R.string.detail_explore_info_status +import com.into.websoso.core.resource.R.string.detail_explore_info_status_complete +import com.into.websoso.core.resource.R.string.detail_explore_info_status_in_series +import com.into.websoso.ui.detailExplore.DetailExploreViewModel +import com.into.websoso.ui.detailExplore.DetailExploreViewModel.Companion.RATING_MAX +import com.into.websoso.ui.detailExplore.DetailExploreViewModel.Companion.RATING_MIN +import com.into.websoso.ui.detailExplore.DetailExploreViewModel.Companion.RATING_STEP +import com.into.websoso.ui.detailExplore.info.model.Genre +import com.into.websoso.ui.detailExplore.info.model.SeriesStatus + +@Composable +fun DetailExploreInfoTab( + viewModel: DetailExploreViewModel, + modifier: Modifier = Modifier, +) { + val selectedGenres by viewModel.selectedGenres.observeAsState(emptyList()) + val selectedStatus by viewModel.selectedStatus.observeAsState(null) + val ratingMin by viewModel.selectedRatingMin.observeAsState(RATING_MIN) + val ratingMax by viewModel.selectedRatingMax.observeAsState(RATING_MAX) + + Column( + modifier = modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + GenreSection( + selectedGenres = selectedGenres, + onGenreClick = viewModel::updateSelectedGenres, + ) + StatusSection( + + selectedStatus = selectedStatus, + onStatusClick = { status -> + viewModel.updateSelectedSeriesStatus( + if (selectedStatus == status) null else status, + ) + }, + ) + RatingSection( + min = ratingMin, + max = ratingMax, + onRangeChange = viewModel::updateSelectedRatingRange, + ) + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Composable +private fun SectionTitle(text: String) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 10.dp), + ) { + Text( + text = text, + style = WebsosoTheme.typography.title2, + color = Black, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun GenreSection( + selectedGenres: List, + onGenreClick: (Genre) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + SectionTitle(text = stringResource(detail_explore_info_genre)) + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Genre.entries.forEach { genre -> + SelectableTagChip( + label = genre.titleKr, + isSelected = selectedGenres.contains(genre), + onClick = { onGenreClick(genre) }, + ) + } + } + } +} + +@Composable +private fun StatusSection( + selectedStatus: SeriesStatus?, + onStatusClick: (SeriesStatus) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + SectionTitle(text = stringResource(detail_explore_info_status)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + StatusChipCell( + label = stringResource(detail_explore_info_status_in_series), + status = SeriesStatus.IN_SERIES, + selectedStatus = selectedStatus, + onClick = onStatusClick, + modifier = Modifier.weight(1f), + ) + StatusChipCell( + label = stringResource(detail_explore_info_status_complete), + status = SeriesStatus.COMPLETED, + selectedStatus = selectedStatus, + onClick = onStatusClick, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun StatusChipCell( + label: String, + status: SeriesStatus, + selectedStatus: SeriesStatus?, + onClick: (SeriesStatus) -> Unit, + modifier: Modifier = Modifier, +) { + SelectableStatusChip( + label = label, + isSelected = selectedStatus == status, + onClick = { onClick(status) }, + modifier = modifier.wrapContentHeight(), + ) +} + +@Composable +private fun RatingSection( + min: Float, + max: Float, + onRangeChange: (Float, Float) -> Unit, +) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(detail_explore_info_rating), + style = WebsosoTheme.typography.title2, + color = Black, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = stringResource(detail_explore_info_rating_range, min, max), + style = WebsosoTheme.typography.body2, + color = Primary100, + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .height(38.dp) + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + RatingValueBox(value = min) + Box(modifier = Modifier.weight(1f)) { + RatingRangeSlider( + min = min, + max = max, + valueRange = RATING_MIN..RATING_MAX, + stepSize = RATING_STEP, + onValueChange = onRangeChange, + ) + } + RatingValueBox(value = max) + } + Spacer(modifier = Modifier.height(38.dp)) + } +} + +@Composable +private fun RatingValueBox(value: Float) { + Box( + modifier = Modifier + .size(width = 50.dp, height = 38.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Gray50), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(detail_explore_info_rating_value, value), + style = WebsosoTheme.typography.body2, + color = Primary100, + ) + } +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt new file mode 100644 index 000000000..3fbd832a6 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt @@ -0,0 +1,431 @@ +package com.into.websoso.ui.detailExplore.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel +import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel +import com.into.websoso.core.common.util.clickableWithoutRipple +import com.into.websoso.core.common.util.getS3ImageUrl +import com.into.websoso.core.designsystem.component.NetworkImage +import com.into.websoso.core.designsystem.theme.Black +import com.into.websoso.core.designsystem.theme.Gray100 +import com.into.websoso.core.designsystem.theme.Gray200 +import com.into.websoso.core.designsystem.theme.Gray300 +import com.into.websoso.core.designsystem.theme.Gray50 +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.core.designsystem.theme.White +import com.into.websoso.core.resource.R.drawable.ic_common_search +import com.into.websoso.core.resource.R.drawable.ic_common_search_clear +import com.into.websoso.core.resource.R.drawable.ic_detail_explore_keyword_not_exist_result +import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keword_remove +import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keyword_toggle_selected +import com.into.websoso.core.resource.R.string.detail_explore_keyword_inquire_keyword +import com.into.websoso.core.resource.R.string.detail_explore_keyword_not_exist_result +import com.into.websoso.core.resource.R.string.detail_explore_keyword_search_result +import com.into.websoso.core.resource.R.string.detail_explore_search_hint +import com.into.websoso.ui.detailExplore.DetailExploreViewModel +import com.into.websoso.ui.detailExplore.keyword.model.DetailExploreKeywordUiState +import androidx.compose.ui.platform.LocalContext + +@Composable +fun DetailExploreKeywordTab( + viewModel: DetailExploreViewModel, + onKeywordInquireClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val uiState by viewModel.uiState.observeAsState(DetailExploreKeywordUiState()) + var searchValue by remember { mutableStateOf(TextFieldValue("")) } + + val selectedKeywords = remember(uiState.categories) { + uiState.categories.flatMap { it.keywords }.filter { it.isSelected } + } + + Column(modifier = modifier.fillMaxWidth()) { + KeywordSearchField( + value = searchValue, + onValueChange = { value -> + searchValue = value + if (value.text.isEmpty()) { + viewModel.initSearchKeyword() + } else { + viewModel.updateIsSearchKeywordProceeding(true) + } + }, + onSearch = { + if (searchValue.text.isEmpty()) { + viewModel.initSearchKeyword() + } else { + viewModel.updateKeyword(searchValue.text) + } + }, + onClear = { + searchValue = TextFieldValue("") + viewModel.initSearchKeyword() + }, + ) + + if (selectedKeywords.isNotEmpty()) { + SelectedKeywordsRow( + keywords = selectedKeywords, + onRemove = viewModel::updateClickedChipState, + ) + } + + when { + uiState.isSearchKeywordProceeding && uiState.isSearchResultKeywordsEmpty -> { + KeywordEmptyResult(onInquireClick = onKeywordInquireClick) + } + + uiState.isSearchKeywordProceeding -> { + Divider() + SearchResultList( + keywords = uiState.searchResultKeywords, + isInitial = uiState.isInitialSearchKeyword, + onKeywordClick = viewModel::updateClickedChipState, + ) + } + + else -> { + CategoryList( + categories = uiState.categories, + onKeywordClick = viewModel::updateClickedChipState, + ) + } + } + } +} + +@Composable +private fun KeywordSearchField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + onSearch: () -> Unit, + onClear: () -> Unit, +) { + Box( + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 12.dp) + .fillMaxWidth() + .height(44.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Gray50) + .padding(horizontal = 14.dp), + contentAlignment = Alignment.CenterStart, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.weight(1f)) { + BasicTextField( + value = value, + onValueChange = onValueChange, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onSearch() }), + cursorBrush = SolidColor(Primary100), + textStyle = WebsosoTheme.typography.body4.copy(color = Black), + ) + if (value.text.isEmpty()) { + Text( + text = stringResource(detail_explore_search_hint), + style = WebsosoTheme.typography.body4, + color = Gray200, + ) + } + } + if (value.text.isNotEmpty()) { + androidx.compose.foundation.Image( + painter = painterResource(id = ic_common_search_clear), + contentDescription = null, + modifier = Modifier + .size(18.dp) + .clickableWithoutRipple(onClick = onClear), + ) + Spacer(modifier = Modifier.width(8.dp)) + } + androidx.compose.foundation.Image( + painter = painterResource(id = ic_common_search), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickableWithoutRipple(onClick = onSearch), + ) + } + } +} + +@Composable +private fun SelectedKeywordsRow( + keywords: List, + onRemove: (Int) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 20.dp) + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + keywords.forEach { keyword -> + SelectedKeywordChip( + label = keyword.keywordName, + onRemove = { onRemove(keyword.keywordId) }, + ) + } + } +} + +@Composable +private fun SelectedKeywordChip( + label: String, + onRemove: () -> Unit, +) { + val shape = RoundedCornerShape(20.dp) + Row( + modifier = Modifier + .clip(shape) + .border(width = 1.dp, color = Primary100, shape = shape) + .background(color = White, shape = shape) + .clickableWithoutRipple(onClick = onRemove) + .padding(start = 13.dp, end = 11.dp, top = 7.dp, bottom = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = label, + style = WebsosoTheme.typography.body2, + color = Primary100, + ) + androidx.compose.foundation.Image( + painter = painterResource(id = ic_novel_rating_keword_remove), + contentDescription = null, + modifier = Modifier.size(10.dp), + ) + } +} + +@Composable +private fun Divider() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(Gray50), + ) +} + +@Composable +private fun CategoryList( + categories: List, + onKeywordClick: (Int) -> Unit, +) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .background(Gray50), + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(items = categories, key = { it.categoryName }) { category -> + CategoryCard(category = category, onKeywordClick = onKeywordClick) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun CategoryCard( + category: CategoryModel, + onKeywordClick: (Int) -> Unit, +) { + var isExpanded by remember { mutableStateOf(false) } + val context = LocalContext.current + val imageUrl = remember(category.categoryImage) { + context.getS3ImageUrl(category.categoryImage) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(White), + ) { + Row( + modifier = Modifier + .padding(start = 20.dp, top = 20.dp, end = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + NetworkImage( + imageUrl = imageUrl, + contentDescription = null, + modifier = Modifier + .size(28.dp) + .clip(RoundedCornerShape(10.dp)), + ) + Text( + text = category.categoryName, + style = WebsosoTheme.typography.title2, + color = Gray300, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + FlowRow( + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .heightIn(max = if (isExpanded) Int.MAX_VALUE.dp else 92.dp) + .clip(RoundedCornerShape(0.dp)), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(0.dp), + ) { + category.keywords.forEach { keyword -> + Box(modifier = Modifier.padding(vertical = 4.dp)) { + SelectableTagChip( + label = keyword.keywordName, + isSelected = keyword.isSelected, + onClick = { onKeywordClick(keyword.keywordId) }, + ) + } + } + } + Spacer(modifier = Modifier.height(18.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(Gray50), + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .clickableWithoutRipple { isExpanded = !isExpanded }, + contentAlignment = Alignment.Center, + ) { + androidx.compose.foundation.Image( + painter = painterResource(id = ic_novel_rating_keyword_toggle_selected), + contentDescription = null, + modifier = Modifier + .size(16.dp) + .rotate(if (isExpanded) 0f else 180f), + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SearchResultList( + keywords: List, + isInitial: Boolean, + onKeywordClick: (Int) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp), + ) { + if (!isInitial) { + Spacer(modifier = Modifier.height(28.dp)) + Text( + text = stringResource(detail_explore_keyword_search_result), + style = WebsosoTheme.typography.title3, + color = Gray300, + ) + Spacer(modifier = Modifier.height(20.dp)) + } + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + keywords.forEach { keyword -> + SelectableTagChip( + label = keyword.keywordName, + isSelected = keyword.isSelected, + onClick = { onKeywordClick(keyword.keywordId) }, + ) + } + } + } +} + +@Composable +private fun KeywordEmptyResult(onInquireClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 80.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + androidx.compose.foundation.Image( + painter = painterResource(id = ic_detail_explore_keyword_not_exist_result), + contentDescription = null, + modifier = Modifier.size(80.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringResource(detail_explore_keyword_not_exist_result), + style = WebsosoTheme.typography.body1, + color = Gray200, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(36.dp)) + Box( + modifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .background(Gray50) + .clickableWithoutRipple(onClick = onInquireClick) + .padding(horizontal = 26.dp, vertical = 20.dp), + ) { + Text( + text = stringResource(detail_explore_keyword_inquire_keyword), + style = WebsosoTheme.typography.title2, + color = Primary100, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt new file mode 100644 index 000000000..0a07aeadf --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt @@ -0,0 +1,158 @@ +package com.into.websoso.ui.detailExplore.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.Primary50 +import com.into.websoso.core.designsystem.theme.White +import kotlin.math.absoluteValue +import kotlin.math.round + +@Composable +fun RatingRangeSlider( + min: Float, + max: Float, + valueRange: ClosedFloatingPointRange, + stepSize: Float, + onValueChange: (Float, Float) -> Unit, + modifier: Modifier = Modifier, +) { + val totalRange = valueRange.endInclusive - valueRange.start + val tickCount = ((totalRange / stepSize).toInt() + 1).coerceAtLeast(2) + + val density = LocalDensity.current + val thumbRadiusPx = with(density) { 8.dp.toPx() } + + var sliderWidthPx by remember { mutableFloatStateOf(0f) } + var activeThumb by remember { mutableIntStateOf(0) } + + val trackWidthPx = (sliderWidthPx - thumbRadiusPx * 2).coerceAtLeast(0f) + val startFraction = ((min - valueRange.start) / totalRange).coerceIn(0f, 1f) + val endFraction = ((max - valueRange.start) / totalRange).coerceIn(0f, 1f) + val startCenterPx = thumbRadiusPx + trackWidthPx * startFraction + val endCenterPx = thumbRadiusPx + trackWidthPx * endFraction + + val latestMin by rememberUpdatedState(min) + val latestMax by rememberUpdatedState(max) + val latestStartCenter by rememberUpdatedState(startCenterPx) + val latestEndCenter by rememberUpdatedState(endCenterPx) + + fun snap(rawValue: Float): Float { + val stepped = round(rawValue / stepSize) * stepSize + return stepped.coerceIn(valueRange.start, valueRange.endInclusive) + } + + fun valueAtX(x: Float): Float { + if (trackWidthPx <= 0f) return valueRange.start + val fraction = ((x - thumbRadiusPx) / trackWidthPx).coerceIn(0f, 1f) + return snap(valueRange.start + fraction * totalRange) + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(16.dp) + .onSizeChanged { sliderWidthPx = it.width.toFloat() } + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + val touchX = offset.x + activeThumb = if ((touchX - latestStartCenter).absoluteValue <= + (touchX - latestEndCenter).absoluteValue + ) 1 else 2 + val newValue = valueAtX(touchX) + if (activeThumb == 1) { + onValueChange(newValue.coerceAtMost(latestMax), latestMax) + } else { + onValueChange(latestMin, newValue.coerceAtLeast(latestMin)) + } + }, + onDrag = { change, _ -> + change.consume() + val newValue = valueAtX(change.position.x) + if (activeThumb == 1) { + onValueChange(newValue.coerceAtMost(latestMax), latestMax) + } else { + onValueChange(latestMin, newValue.coerceAtLeast(latestMin)) + } + }, + ) + }, + contentAlignment = Alignment.CenterStart, + ) { + Box( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth() + .height(4.dp) + .clip(RoundedCornerShape(6.dp)) + .background(Primary50), + ) + Row( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth() + .height(4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(tickCount) { + Box( + modifier = Modifier + .size(width = 1.dp, height = 2.dp) + .background(Primary100, RoundedCornerShape(1.dp)), + ) + } + } + val activeStartDp = with(density) { startCenterPx.toDp() } + val activeWidthDp = with(density) { (endCenterPx - startCenterPx).coerceAtLeast(0f).toDp() } + Box( + modifier = Modifier + .offset(x = activeStartDp) + .width(activeWidthDp) + .height(4.dp) + .clip(RoundedCornerShape(6.dp)) + .background(Primary100), + ) + ThumbCircle(offsetXDp = with(density) { (startCenterPx - thumbRadiusPx).toDp() }) + ThumbCircle(offsetXDp = with(density) { (endCenterPx - thumbRadiusPx).toDp() }) + } +} + +@Composable +private fun ThumbCircle(offsetXDp: androidx.compose.ui.unit.Dp) { + Box( + modifier = Modifier + .offset(x = offsetXDp) + .shadow(elevation = 2.dp, shape = CircleShape, clip = false) + .size(16.dp) + .background(White, CircleShape), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt new file mode 100644 index 000000000..cae6692ff --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt @@ -0,0 +1,90 @@ +package com.into.websoso.ui.detailExplore.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.into.websoso.core.common.util.clickableWithoutRipple +import com.into.websoso.core.designsystem.theme.Gray300 +import com.into.websoso.core.designsystem.theme.Gray50 +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.Primary50 +import com.into.websoso.core.designsystem.theme.Transparent +import com.into.websoso.core.designsystem.theme.WebsosoTheme + +@Composable +fun SelectableTagChip( + label: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + SelectableChipBase( + label = label, + isSelected = isSelected, + onClick = onClick, + cornerRadius = 20.dp, + horizontalPadding = 13.dp, + verticalPadding = 7.dp, + modifier = modifier, + ) +} + +@Composable +fun SelectableStatusChip( + label: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + SelectableChipBase( + label = label, + isSelected = isSelected, + onClick = onClick, + cornerRadius = 8.dp, + horizontalPadding = 0.dp, + verticalPadding = 10.dp, + modifier = modifier, + ) +} + +@Composable +private fun SelectableChipBase( + label: String, + isSelected: Boolean, + onClick: () -> Unit, + cornerRadius: Dp, + horizontalPadding: Dp, + verticalPadding: Dp, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(cornerRadius) + val background = if (isSelected) Primary50 else Gray50 + val borderColor = if (isSelected) Primary100 else Transparent + val textColor = if (isSelected) Primary100 else Gray300 + + Box( + modifier = modifier + .clip(shape) + .clickableWithoutRipple(onClick = onClick) + .background(color = background, shape = shape) + .border(width = 1.dp, color = borderColor, shape = shape) + .padding(horizontal = horizontalPadding, vertical = verticalPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + style = WebsosoTheme.typography.body2, + color = textColor, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt deleted file mode 100644 index a2c8d886b..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.into.websoso.ui.detailExplore.info - -import android.os.Bundle -import android.view.View -import androidx.core.view.forEach -import androidx.fragment.app.activityViewModels -import com.google.android.material.chip.Chip -import com.into.websoso.R -import com.into.websoso.core.common.ui.base.BaseFragment -import com.into.websoso.core.common.ui.custom.WebsosoChip -import com.into.websoso.core.common.util.SingleEventHandler -import com.into.websoso.core.common.util.toFloatPxFromDp -import com.into.websoso.databinding.FragmentDetailExploreInfoBinding -import com.into.websoso.ui.detailExplore.DetailExploreViewModel -import com.into.websoso.ui.detailExplore.info.model.Genre -import com.into.websoso.ui.detailExplore.info.model.Rating -import com.into.websoso.ui.detailExplore.info.model.SeriesStatus -import com.into.websoso.ui.detailExploreResult.DetailExploreResultActivity -import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class DetailExploreInfoFragment : BaseFragment(R.layout.fragment_detail_explore_info) { - private val detailExploreViewModel: DetailExploreViewModel by activityViewModels() - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - bindViewModel() - onResetButtonClick() - onDetailSearchNovelButtonClick() - setupGenreChips() - setupSeriesStatusChips() - setupRatingChips() - setupObserver() - } - - private fun bindViewModel() { - binding.detailExploreViewModel = detailExploreViewModel - binding.lifecycleOwner = this - } - - private fun onResetButtonClick() { - binding.clDetailExploreInfoResetButton.setOnClickListener { - detailExploreViewModel.updateSelectedInfoValueClear() - } - } - - private fun onDetailSearchNovelButtonClick() { - binding.tvDetailExploreSearchButton.setOnClickListener { - singleEventHandler.throttleFirst { - val selectedGenres = detailExploreViewModel.selectedGenres.value ?: emptyList() - val isCompleted = detailExploreViewModel.selectedStatus.value?.isCompleted - val novelRating = detailExploreViewModel.selectedRating.value - - val keywordIds = detailExploreViewModel.uiState.value - ?.categories - ?.flatMap { it.keywords.asSequence() } - ?.filter { it.isSelected } - ?.map { it.keywordId } - ?.toList() ?: emptyList() - - val intent = DetailExploreResultActivity.getIntent( - context = requireContext(), - DetailExploreFilteredModel( - genres = selectedGenres, - isCompleted = isCompleted, - novelRating = novelRating, - keywordIds = keywordIds, - ), - ) - - startActivity(intent) - } - } - } - - private fun setupGenreChips() { - val genres = Genre.entries - genres.forEach { genre -> - WebsosoChip(requireContext()) - .apply { - setWebsosoChipText(genre.titleKr) - setWebsosoChipTextAppearance(R.style.body2) - setWebsosoChipTextColor(R.color.bg_detail_explore_chip_text_selector) - setWebsosoChipStrokeColor(R.color.bg_detail_explore_chip_stroke_selector) - setWebsosoChipBackgroundColor(R.color.bg_detail_explore_chip_background_selector) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnWebsosoChipClick { detailExploreViewModel.updateSelectedGenres(genre) } - }.also { websosoChip -> binding.wcgDetailExploreInfoGenre.addChip(websosoChip) } - } - } - - private fun setupSeriesStatusChips() { - val seriesStatusChips = listOf( - binding.chipDetailExploreInfoStatusInSeries, - binding.chipDetailExploreInfoStatusComplete, - ) - - seriesStatusChips.forEach { chip -> - setupChipCheckListener(chip) { isChecked -> - when (isChecked) { - true -> { - seriesStatusChips.filter { it != chip }.forEach { it.isChecked = false } - - val status = SeriesStatus.from(chip.text.toString()) - detailExploreViewModel.updateSelectedSeriesStatus(status) - } - - false -> { - detailExploreViewModel.updateSelectedSeriesStatus(null) - } - } - } - } - } - - private fun setupRatingChips() { - val ratingChips = listOf( - binding.chipDetailExploreInfoRatingLowest, - binding.chipDetailExploreInfoRatingLower, - binding.chipDetailExploreInfoRatingHigher, - binding.chipDetailExploreInfoRatingHighest, - ) - - ratingChips.forEach { chip -> - setupChipCheckListener(chip) { isChecked -> - when (isChecked) { - true -> { - ratingChips.filter { it != chip }.forEach { it.isChecked = false } - val ratingValue = - Rating.entries.find { - chip.text.toString().contains(it.value.toString()) - } - detailExploreViewModel.updateSelectedRating(ratingValue?.value) - } - - false -> detailExploreViewModel.updateSelectedRating(null) - } - } - } - } - - private fun setupChipCheckListener( - chip: Chip, - onCheckedChange: (Boolean) -> Unit, - ) { - chip.setOnCheckedChangeListener(null) - chip.isChecked = false - chip.setOnCheckedChangeListener { _, isChecked -> onCheckedChange(isChecked) } - } - - private fun setupObserver() { - detailExploreViewModel.selectedGenres.observe(viewLifecycleOwner) { genres -> - if (genres.isNullOrEmpty()) { - binding.wcgDetailExploreInfoGenre.forEach { - it.isSelected = false - } - } - } - - detailExploreViewModel.selectedStatus.observe(viewLifecycleOwner) { selectedStatus -> - val selectedChip = selectedStatus?.title - - listOf( - binding.chipDetailExploreInfoStatusInSeries, - binding.chipDetailExploreInfoStatusComplete, - ).forEach { chip -> - chip.isChecked = chip.text.toString() == selectedChip - } - } - - detailExploreViewModel.selectedRating.observe(viewLifecycleOwner) { selectedRating -> - listOf( - binding.chipDetailExploreInfoRatingLowest, - binding.chipDetailExploreInfoRatingLower, - binding.chipDetailExploreInfoRatingHigher, - binding.chipDetailExploreInfoRatingHighest, - ).forEach { chip -> - val ratingValue = - Rating.entries.find { chip.text.toString().contains(it.value.toString()) } - chip.isChecked = selectedRating == ratingValue?.value - } - } - } -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt deleted file mode 100644 index b91c62b0a..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt +++ /dev/null @@ -1,322 +0,0 @@ -package com.into.websoso.ui.detailExplore.keyword - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.core.view.children -import androidx.core.view.forEach -import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels -import com.into.websoso.R.color.bg_novel_rating_chip_background_selector -import com.into.websoso.R.color.bg_novel_rating_chip_stroke_selector -import com.into.websoso.R.color.bg_novel_rating_chip_text_selector -import com.into.websoso.R.color.primary_100_6A5DFD -import com.into.websoso.R.color.white -import com.into.websoso.R.layout.fragment_detail_explore_keyword -import com.into.websoso.R.style.body2 -import com.into.websoso.core.common.ui.base.BaseFragment -import com.into.websoso.core.common.ui.custom.WebsosoChip -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel -import com.into.websoso.core.common.ui.model.CategoriesModel.Companion.findKeywordByName -import com.into.websoso.core.common.util.SingleEventHandler -import com.into.websoso.core.common.util.toFloatPxFromDp -import com.into.websoso.core.common.util.tracker.Tracker -import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keword_remove -import com.into.websoso.core.resource.R.string.detail_explore_search_hint -import com.into.websoso.core.resource.R.string.inquire_link -import com.into.websoso.databinding.FragmentDetailExploreKeywordBinding -import com.into.websoso.ui.detailExplore.DetailExploreViewModel -import com.into.websoso.ui.detailExplore.keyword.adapter.DetailExploreKeywordAdapter -import com.into.websoso.ui.detailExplore.keyword.model.DetailExploreKeywordUiState -import com.into.websoso.ui.detailExploreResult.DetailExploreResultActivity -import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class DetailExploreKeywordFragment : BaseFragment(fragment_detail_explore_keyword) { - @Inject - lateinit var tracker: Tracker - - private val detailExploreViewModel: DetailExploreViewModel by activityViewModels() - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - private val detailExploreKeywordAdapter: DetailExploreKeywordAdapter by lazy { - DetailExploreKeywordAdapter(detailExploreViewModel::updateClickedChipState) - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - bindViewModel() - setupAdapter() - setupObserver() - setupSearchKeyword() - setupWebsosoSearchEditListener() - setupBackButtonListener() - } - - private fun bindViewModel() { - binding.detailExploreViewModel = detailExploreViewModel - binding.onClick = onDetailExploreKeywordButtonClick() - binding.lifecycleOwner = viewLifecycleOwner - } - - private fun onDetailExploreKeywordButtonClick() = - object : DetailExploreClickListener { - override fun onNovelInquireButtonClick() { - tracker.trackEvent("contact_keyword") - val inquireUrl = getString(inquire_link) - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(inquireUrl)) - startActivity(intent) - } - - override fun onDetailSearchNovelButtonClick() { - singleEventHandler.throttleFirst { - navigateToDetailSearchResult() - } - } - - override fun onKeywordResetButtonClick() { - detailExploreViewModel.updateSelectedKeywordValueClear() - } - } - - private fun navigateToDetailSearchResult() { - val selectedGenres = detailExploreViewModel.selectedGenres.value ?: emptyList() - val isCompleted = detailExploreViewModel.selectedStatus.value?.isCompleted - val novelRating = detailExploreViewModel.selectedRating.value - - val keywordIds = detailExploreViewModel.uiState.value - ?.categories - ?.asSequence() - ?.flatMap { it.keywords.asSequence() } - ?.filter { it.isSelected } - ?.map { it.keywordId } - ?.toList() ?: emptyList() - - val intent = DetailExploreResultActivity.getIntent( - context = requireContext(), - DetailExploreFilteredModel( - genres = selectedGenres, - isCompleted = isCompleted, - novelRating = novelRating, - keywordIds = keywordIds, - ), - ) - - startActivity(intent) - } - - private fun setupAdapter() { - binding.rvDetailExploreKeywordList.apply { - adapter = detailExploreKeywordAdapter - itemAnimator = null - } - } - - private fun setupObserver() { - detailExploreViewModel.uiState.observe(viewLifecycleOwner) { uiState -> - detailExploreKeywordAdapter.submitList(uiState.categories) - setupSelectedScrollViewVisibility(uiState.categories) - setupSelectedChips(uiState.categories) - updateSearchKeywordResult(uiState) - } - } - - private fun setupSelectedScrollViewVisibility(categories: List) { - val hasSelectedKeywords = categories.flatMap { it.keywords }.any { it.isSelected } - - binding.hsvRatingKeywordSelectedKeyword.isVisible = hasSelectedKeywords - } - - private fun setupSelectedChips(categories: List) { - val currentChipKeywords = - binding.wcgDetailExploreKeywordSelectedKeyword.children - .filterIsInstance() - .map { it.text.toString() } - .toList() - - val selectedKeywords = - categories - .asSequence() - .flatMap { it.keywords.asSequence() } - .filter { it.isSelected } - .map { it.keywordName } - .toList() - - val chipsToRemove = currentChipKeywords - selectedKeywords.toSet() - val chipsToAdd = selectedKeywords - currentChipKeywords.toSet() - - chipsToRemove.forEach { keywordName -> - removeSelectedChip(keywordName) - } - - chipsToAdd.forEach { keywordName -> - createSelectedChip( - categories.findKeywordByName(keywordName) - ?: throw IllegalArgumentException("Keyword not found: $keywordName"), - ) - } - } - - private fun removeSelectedChip(keywordName: String) { - val chipToRemove = binding.wcgDetailExploreKeywordSelectedKeyword.children.find { - (it as WebsosoChip).text == keywordName - } - chipToRemove?.let { - binding.wcgDetailExploreKeywordSelectedKeyword.removeView(it) - } - } - - private fun createSelectedChip(selectedKeyword: KeywordModel) { - WebsosoChip(requireContext()) - .apply { - setWebsosoChipText(selectedKeyword.keywordName) - setWebsosoChipTextAppearance(body2) - setWebsosoChipTextColor(primary_100_6A5DFD) - setWebsosoChipStrokeColor(primary_100_6A5DFD) - setWebsosoChipBackgroundColor(white) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnCloseIconClickListener { - detailExploreViewModel.updateClickedChipState( - selectedKeyword.keywordId, - ) - } - setWebsosoChipCloseIconVisibility(true) - setWebsosoChipCloseIconDrawable(ic_novel_rating_keword_remove) - setWebsosoChipCloseIconSize(10f.toFloatPxFromDp()) - setWebsosoChipCloseIconEndPadding(12f.toFloatPxFromDp()) - setCloseIconTintResource(primary_100_6A5DFD) - }.also { websosoChip -> - binding.wcgDetailExploreKeywordSelectedKeyword.addChip(websosoChip) - } - } - - private fun updateSearchKeywordResult(uiState: DetailExploreKeywordUiState) { - val previousSearchResultKeywords = - binding.wcgDetailExploreKeywordResult.children - .toList() - .map { it as WebsosoChip } - - when { - !uiState.isSearchKeywordProceeding -> return - - uiState.isSearchResultKeywordsEmpty -> return - - uiState.searchResultKeywords.map { it.keywordName } == previousSearchResultKeywords.map { it.text.toString() } -> { - updateSearchKeywordResultIsSelected(uiState) - return - } - - else -> { - binding.wcgDetailExploreKeywordResult.removeAllViews() - updateSearchKeywordResultWebsosoChips(uiState) - } - } - } - - private fun updateSearchKeywordResultIsSelected(uiState: DetailExploreKeywordUiState) { - binding.wcgDetailExploreKeywordResult.forEach { view -> - val chip = view as? WebsosoChip ?: return@forEach - - val isSelected = - uiState.categories - .asSequence() - .flatMap { it.keywords } - .filter { it.isSelected } - .any { it.keywordName == chip.text.toString() } - - chip.isSelected = isSelected - } - } - - private fun updateSearchKeywordResultWebsosoChips(uiState: DetailExploreKeywordUiState) { - uiState.searchResultKeywords.forEach { keyword -> - val isSelected = uiState.categories - .asSequence() - .flatMap { it.keywords } - .any { it.keywordId == keyword.keywordId && it.isSelected } - - WebsosoChip(binding.root.context) - .apply { - setWebsosoChipText(keyword.keywordName) - setWebsosoChipTextAppearance(body2) - setWebsosoChipTextColor(bg_novel_rating_chip_text_selector) - setWebsosoChipStrokeColor(bg_novel_rating_chip_stroke_selector) - setWebsosoChipBackgroundColor(bg_novel_rating_chip_background_selector) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnWebsosoChipClick { - detailExploreViewModel.updateClickedChipState(keyword.keywordId) - } - this.isSelected = isSelected - }.also { websosoChip -> - binding.wcgDetailExploreKeywordResult.addChip(websosoChip) - } - } - } - - private fun setupSearchKeyword() { - binding.wsetDetailExploreKeywordSearch.apply { - setWebsosoSearchHint(getString(detail_explore_search_hint)) - } - } - - private fun setupWebsosoSearchEditListener() { - binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchActionListener { _, _, _ -> - performSearch() - true - } - - binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchFocusChangeListener { _, isFocused -> - if (isFocused) detailExploreViewModel.updateIsSearchKeywordProceeding(true) - } - - binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchClearClickListener { - initSearchKeyword() - } - } - - private fun performSearch() { - val input = binding.wsetDetailExploreKeywordSearch.getWebsosoSearchText() - if (input.isEmpty()) { - initSearchKeyword() - return - } - detailExploreViewModel.updateKeyword(input) - } - - private fun initSearchKeyword() { - binding.wsetDetailExploreKeywordSearch.clearWebsosoSearchFocus() - detailExploreViewModel.initSearchKeyword() - } - - private fun setupBackButtonListener() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (binding.wsetDetailExploreKeywordSearch.hasFocus()) { - initSearchKeyword() - } else { - requireActivity().onBackPressedDispatcher.onBackPressed() - } - } - }, - ) - } - - override fun onDestroyView() { - initSearchKeyword() - super.onDestroyView() - } -} diff --git a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt b/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt index 3206a25c5..3e3d7c51e 100644 --- a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt @@ -7,7 +7,7 @@ import com.into.websoso.core.common.ui.base.BaseFragment import com.into.websoso.core.common.util.SingleEventHandler import com.into.websoso.core.common.util.tracker.Tracker import com.into.websoso.databinding.FragmentExploreBinding -import com.into.websoso.ui.detailExplore.DetailExploreDialogBottomSheet +import com.into.websoso.ui.detailExplore.DetailExploreActivity import com.into.websoso.ui.normalExplore.NormalExploreActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -41,18 +41,12 @@ class ExploreFragment : BaseFragment(R.layout.fragment_e private fun onDetailExploreButtonClick() { binding.clExploreDetailSearch.setOnClickListener { singleEventHandler.throttleFirst { - showDetailExploreDialog() + startActivity(DetailExploreActivity.getIntent(requireContext())) } } } - private fun showDetailExploreDialog() { - val detailExploreBottomSheet = DetailExploreDialogBottomSheet.newInstance() - detailExploreBottomSheet.show(childFragmentManager, DETAIL_BOTTOM_SHEET_TAG) - } - companion object { - private const val DETAIL_BOTTOM_SHEET_TAG = "DetailExploreDialogBottomSheet" const val TAG = "ExploreFragment" } } diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt index e400af729..f8e39dd2c 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt @@ -19,10 +19,12 @@ import com.into.websoso.core.common.ui.model.ResultFrom.NormalExploreBack import com.into.websoso.core.common.ui.model.ResultFrom.Notification import com.into.websoso.core.common.ui.model.ResultFrom.NovelDetailBack import com.into.websoso.core.common.ui.model.ResultFrom.ProfileEditSuccess +import com.into.websoso.core.common.util.SingleEventHandler import com.into.websoso.core.common.util.collectWithLifecycle import com.into.websoso.core.common.util.tracker.Tracker import com.into.websoso.core.resource.R.string.home_nickname_interest_feed import com.into.websoso.databinding.FragmentHomeBinding +import com.into.websoso.ui.detailExplore.DetailExploreActivity import com.into.websoso.ui.feedDetail.FeedDetailActivity import com.into.websoso.ui.main.MainViewModel import com.into.websoso.ui.main.home.adpater.PopularFeedsAdapter @@ -45,6 +47,8 @@ class HomeFragment : BaseFragment(fragment_home) { private val homeViewModel: HomeViewModel by viewModels() private val mainViewModel: MainViewModel by activityViewModels() + private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } + private val homeResultLauncher: ActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { result -> when (result.resultCode) { @@ -109,6 +113,7 @@ class HomeFragment : BaseFragment(fragment_home) { onSettingPreferenceGenreClick() onNotificationButtonClick() onNormalSearchButtonClick() + onDetailSearchButtonClick() tracker.trackEvent("home") } @@ -120,6 +125,14 @@ class HomeFragment : BaseFragment(fragment_home) { } } + private fun onDetailSearchButtonClick() { + binding.clHomeDetailSearch.setOnClickListener { + singleEventHandler.throttleFirst { + startActivity(DetailExploreActivity.getIntent(requireContext())) + } + } + } + private fun bindViewModel() { binding.viewModel = homeViewModel binding.lifecycleOwner = this diff --git a/app/src/main/res/layout/fragment_detail_explore_info.xml b/app/src/main/res/layout/fragment_detail_explore_info.xml deleted file mode 100644 index f902b4aa6..000000000 --- a/app/src/main/res/layout/fragment_detail_explore_info.xml +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_detail_explore_keyword.xml b/app/src/main/res/layout/fragment_detail_explore_keyword.xml deleted file mode 100644 index 6e43cc3c6..000000000 --- a/app/src/main/res/layout/fragment_detail_explore_keyword.xml +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b467f12bf..a900d63d9 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -87,6 +87,65 @@ + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/cl_home_detail_search" /> mgL7p~_~yNFa$|ck@x-=mXJSljTN86)+qTV#ZQHgpNjBf^Z}*?r)78~|y56oj z=dJGZym+bDxVm$sRW1Q#d;B$Svoz;5nQAw(@X-;U4<#&(^uJD8k=>HULRLq!f zW?eGkl(tZ-D~vp9&^JyPAI@CaQBPwmj~VEn&KH6&cMC<1v$YX2BgtHdrgMq(&gYZP zI;L}7RCj%~J;|@MKAhBEty`ze3-1T_@MRyYm?B zZ`_vBYuu&Tvp+Sp)zhpR%so~NaCzGvk6$q+FwW#Dw9K)xAj|%?Tt`V-D>n9sn))?2 zmp-AnHYHo*<85_xGIp{>u2Ro7{4TJbV=a|cE0SETye9&mWMM5;^;m^ZLwomWlO}6^ z__;cT@c78@y)8N+oS^3<<_2VG;rfHsBUq(Vf$2tv>DGn=!5oq(kKbz3Hq%kGaq8W; z{a$1;>v`g|N@%{iDMiw6Iyob!j4LnEpdfb#)+vVmJqAB`Z^mhs?fqV$e)?lYzx8Ee z^P}>K(Boh=f2#08^>JM@hwG1{%1&7#Hp zK|q(j2;VQJMYhzU6BS*d%e+jcFop0;I?HYR?YVCJz*6#3fO#+-2Q;y6Q4+~&Nn_vgyE&WGb!P0trl^2yDXg;s^B{{Z` ztr?r*PYW&^UB2@ttfD^0jK2^^`u)kY``WBm+ z1A2)bpnpWB&G7#1Jo#Mq*z^8;JUl$Sh^c%#`U`e2%=^uGiK`qkD-n|FnxEQ{m->*| z+Vj5Y;ct9?#MWk(jBR?&c62H(jMs%4aE)-50Pd?k9Bt9vSI@;VB3eG;fw7ztCXMLl&<=E6iu__O* z=k#Xjf;4DuY-wt$$9!>NOFjXl4L--}q5&BtoD-x!&Z509JeHbpa5Q#MWU6yzM0GNI zf4)AkUqL)rxZ#3${@q+8HxEI7TGwxMG_qUUoR8#?OprEo6rcl;>P&l6=*Anql+t8} zErp5d6z<@jYLo(xq%|Z!Y(hP5WG9qBOM@`y#_(bE8Gf?Wd6SOC4^l%oS^``6m7(Xk zw3?L*Imxrf>wYvcmX0P}ZQNm433?8nVn_Tyv)eQG zq%vx7oV?`>BORsT=|axY9r#qi^H0zoE(&E&9oaqkZdz{5siL5CPkHn})Q0b;N12oc z{Mi#`nxd`4)Trv*Rrd`oC|f}h;b;WxCY?h*alxVtGOHAF@oKfz*sbGf&%FNSQt-6$ zj|+sDOWe?4Ez#frxNG=4HPUoK;7N_i+R|xgVLdI_hGv1uu8*?jj)|pDN zgL9c^6GCl`{4&22l8UcdU&@pD7PL1rJc_KlreBOwnZzZcKSVwM9?y)(%Pq|z(G+}K z*8bs2oUyCO64lQ-r#8}Ytz2J$=mf1inv9&-0c%4hYwcwMa}#&41?GN{I;0VF&^gG2 z8zvhrjE!Pbz`pxsy`Dwe=(W1M60mohpU|nyUne&A{LB3uUHWNRg89sZx&84$;<|B< z1U0mleJP&~o*!wB8_jWMy-yiP2bNJP$I2Uz9EJTa7s%jFjR;}q?_+|)j7o?ANR`5| zCxK?dzV|krbmQ8@p?kDBqm|vN@2Gyim|$OjIypK+BuF}%WLeXDGI~h$GCjPqz0#Ar z;=dQ3Slk|hE^ReoGI2?wKhX!@Sd%iYLdHYc7j#mtI0qLbyZQDw2&^&2SXQa|2FFmA z;(U&tZh>=`LjUdC{sd#hSSKm%i7l`lKfb zvLpR^zZN1IJNY#_9rv~S?8oM`>}lSQ2{SFq{@HlqfNp`lCEXi5p?_dhh3oEa<5-L= z#->q%IGkC-)Cid1e4>NP1q7yZe}C~KIJwH5%CoKQ+~@yoJZWEil>L2HW~4kfK z(KdL!cpuekc%pyRrhVgl(su7lx1ya{wfK(`fzZ>++nzyPXPL8hL2tQ}B{f6+8$pMS z^PS&)&m_kQ%K3fT!b5bm*ZU;+iTIoKr&cPNU8KM1H#>k%L4zr#po7LZP{o%KR%G*b zg?|!^#I%jXB@mx`md+qUd^m23xu*|z2t(27aN8(pXywf{Czy@~an9`5eYNfjc=xrT z?A;@07_r8lqq*NdWzJ-(@+u2IQ(IuBD3Xg!_QyZF&X?4a5^IjTgNnuna_H|hOyodZ ztI>pIX1L6WT@Ltv39zc<<0AbK-*Wn4ZSCQEv6$u=i?khl6=%+L%>u7Q>N1D&^lg3} z`332mY(%uL>3QDLe(uQIn)|rjA9!e)!01{0yzYGr2uLSvG3QubZEt#+xBET!SUv5W zj_2g!lSsEsfA#`xW2uuQ8{DQ_pYuUa!K+vaRb4hB)x7FNCN@bzPZinU%!t!Sv5zj+ zuI3Svo_SapF`>^ei{>&;djBVbfP`p!MOX}6+zk~KJ3ycxNR-fE5Wsa`w*#ZUy%n4g zSHZ4wP*)+=%2BmHvcEhQPMk2JN*BIIL0A@WXj~`AjC;Q2zBblvd%z!|@>gvd>_x!` z(z2xN3y}OEo33zw+kUFweM?u=9Mk~p=72E@Afx?~V<$u3%29k^_sN`*LmH+5Cp#Jx2j7j-)E zaeo8*ZRC6#t&wF5t`6kLp52TLO2n>o;;>>RwDVJnHha`+8^M-xc)5ZB1pD3E{gjEN zHr!CPflC-@&NB@3>^~~oC{0e_JcH`(n4HSS56XIsYjP3WPWMjXPAT`?Run0(QX!ZQ zi|{!KD2y_HetU4*GWzB8_JKv)agw%l#;d~WH9(W|_or}U&z+@Td~`!HJulD8MQ_39 z>P;}66*wPMcjyHS69+^Igq<%TG4wIz#IAW?mcR(*cUX z2%sT>x)gbm+!u~vp{U+AOF&l_r=WM1l$NGBTVr|QSQ2`eE7gkHIRa4!_xIEmmh8eW3W8gc`O*Mj7t#?T?- z&IMfxjYjwvB^hWxp4yYiUbH1FB8gMWU6zstX6XyM@#?yWrQEc>fXnvpB@_76C{8TLNxTfcsrsi;iO`55-jZ&jT5tH%=M;3ca)i}T`vRU zX`kg@T|hH&PhERXrn1)eSaI1nNFU)6tIs4c>x_38;p9y(MIzmgWAq^<<)#wCb9=`7jBSmYK7ql zwauX^HacpFuoh>KxGi=2vl#fwAQV>hNfrKgNF$i(6!(}Zs_(9nSdjHAl=^TYG<5^2 z(T1lZ9{5KcHUSFEc?iP~PW3|EX3^F7XF@F=XAuet%W|TR}t30V6 ze5P~S+lTN?KpF{fbStn59EmW6hw$Ul}0oWPv@6FNJAiEhLd&w}b~t zB$a_X>}17^-``^c4#-K8Mn_W~DNB&`>KYzCPWI_1(se7{fJcR3RPXhn$u@dR()a6y zsMQK)8x%|rss4vH{wju37Ii~t=m(JE1z1C3!aI3enouY7V?jeiY-Q8Ks{=jpl9^ zjJJZPZ_??qh%Ky zZe|VGGxxR=a{{FM%$0`{BPtG0Ixa04X)zO`AnGg@s6K$W>z0kZ1_cH3`R_qG)94x@ zuu^M+p=Er|h{WHbcWcgbNCV5vlp9crje>rb3E>IgFuKFapxQ{!!v!c&HDh}TD*Ayl zx0p4kLdcQ|Mdb-dWiHti6mxpYUUA#LRbH@1Ts)0yyB^K|XY49F{xqH%1WQM1Tw+6# z|G*^Lzw3opsr~o;9~i94pz0|Y9F#f+c8iFUZVrT7W~)YtFdGmg1XVT-BFA9ZgU=vK zW9u%{!msC2?Z*eILS%^2Xh|Az7K(bm15J)21+cpQWDm|LQK%w^npvs=MOH=cjzJo) z!z(;at~5s4MVSLTH{(K_6Lg4#$Ax700shIE`|r~~llsi!t#N5)pis{Lskn}5rY6hIPu&?`R*70c?>%{3br>y0S`Kg0Vn5l% zhKnpC{YEPo;5LuN8{FFB-SJYZW!w3Hr2BE3Z0T1Y{Dsb6wsIkBd6lWL-)4Ng?X)y_ zb*2r-c@X!N0}2xrLOn+U92;j^0P`K14!ZzO0n!&37-uXcU}fJEhnxeBu?_wVy3fxh z^FkqpDY@5G2|(U1`vj0CYPp&iA3$tF>ZvW_@}5)h2~3VU;7y;9Y9;`$r-diE$qvEZ zeetFTk3NP>r)EK4Z zDorFV%c;(*G8NpbI97)VMZ+bj=vb+W<1!pKX5wHUQ!S-gi06p{cmtGc&r=t~{4-!V zBLL9Der>zzhDau5olT#E_XB5}1-n}lGOS$Um?emN_&9ZC?n#)_99O@oyyKIQ?@60u z!kINPf_f!uB{uEApy>TXfwX|K>sTn6Vd|8DXW}*yKc+ctbwDuy8Qucs7EWJ*M)APG z9V$G0l8(d|H-cmOqRg_b<-G74kQlCFH1vE*WFm^c9yOS*R$o)`?lF;<1Y^C(H@;q|l@3$i!olH(4ZDUWc(Y z0g6uGajVhZmg+C4<1F57lxfK7W5j)F+%-j5Ca1uhu4Qvcl#1_iSIpIof0IFl0cWD1 zfE~LtqdVc*CqCJmXR-ymC>jNVK~5$k)w^?=oL}9RTv3XqBciiU|Wl_DTr1<0^jfY_zMHQ65^vBPr+(X#_lQjLTgbi@clRoq6F zL($_(Fb(d#-c$w}Ga;ER`b}Rh0v0Bd5AV!?-WBIkfh${V;wH4{n{t;lMCfjqG^gyj zPGZyu98Q|rUHmyaJkrDwaT0YdN7IhwodBG*$;JccD0diXh20cH?j&h>8x4!=1TM4^ zNIf6uT(=>txO0(wR5Tme*Z8mn77DIXC8?(gbF-G|o+;`w@=h69*kMPCaUpivc3$wN zH-}!=%2ds8)_euEj(S2>2Q<_LrYMFvwbyPlSfcv0;6K#KQFH-D)4i}g!s(g9a^OTe zlp-NX8VF1UoWhNiFldDpK>IJ{tY^qpmKml>`YaUX!<4E8p>%TW1At#s*NB}$;FKF5GY}WtTt<~ z9@+(Haw(mRWJTmGLiSYky4sRGKwihgzL0Li-?w?%ju=Gl2K0AAfziy`H!)BUqLa)~ zD>C3#VW72N&^gWPzrZt={Yqev=RkIq0R(OEDmV?J!oL)gC;L^mf&+&v+t4JDPEIP1 zfq}ClzXfl0Y2XHP*+*5HnUO`}y67Zq8w#$~bMY2Nmczn81goLP;L@ft4s6YyCzs;I zC!_B_E*2t0p*->v2nywv+_|sRqx_9a!jf}I|J>=1F>~B|QKEeiFOQ5O?-hN5Brd17IsZC%(k|Lj+YL0uP!U!rzI^3HL!H^`l(r^k zwk|UL%d44jq2_5YkFE!0`%uLQx0ys!Iw(FfWF227<{C%4cx07r0vWo+O(wv1;?)YR zFiNBka2kPMw!Qk&lj9O7-T|HLin`XDVjG>K=+!5Wtuf2`=#_efYZ1Wd@``M}=MQ(mqT zHPwV=i0C$vT#gN1%hb=oXqBQ`jJ?Md-2OaC$+Rsb&u#PSgbZZTnwJeWQJ~1R47p8B zz_9o^(5{#Ev6S(Ao~st|kdIl?eq{7zNo2*plwGf=Z%3NT%lj6nIZAU)kz+r11;_S| zA_^YRr~&c!#}1pv!NjaqYqbjsfRT>g{PBW>c?$h)1Ha(WN1Wk?>yUO5)Omn*9ixWg z)~iL@SWO{gV4B~n>~Rtna6CJ7!589Yvg))G@8+T1aC(^PT50DqL7C#m;|kcSV!!jz zawCWX*wX4N&U|I@PP&Tt-$lvxImFT=&&_Zw;!`27QsM~S>ae8MBTk3>(Gkg1@v{rg zeZ3bnj59l}%^J#wIB=-YBD>mkV_SR_t>%+6gFR+mztcwA>+FuuZg9!+mJEB>Z6t{D zfdxS*mIN5ucFy{3G_DZVgbj$J1?|KBOkf@_Ybc^@by>!%l3_2}qK-OcuqrufDo5OF zU8$DJ4uZ6iVt*K%8xcx74zdTxyFyVLgC#h)s)Svz0FZA6{|NEy8j^4GFm1!@lkNk! z0cv)|5aBtF*8YI!=X33CFl!pfz6BQDX`$exOiVlUjK;s0bGH&9@VL`zR77I!LUM=N z({f{>e*er_j5k~c^6F6z8^S5=ehQYxGdZPTeK;&uu~-N9g9mZMlp17JPj3|ICe4EU z=6=`|FE~y|8dTvhzF|vA`G{46mf(RgldrYZ7kBW1KXdW-+O%s?=gqa%I&xlZ+-f&l z+$3;lgD$waq+GMH3ka%=q{Yaa|FmbUU9bsHMHXkfh*Q!6Z)hyz=1~3OO`N_nHKfKz z&Y0X4iSx}>>E5VLcnm3_ec7E~7U-yz>M^HgPW768gCt0RrF#dv0~WzyRa%itxXBSY zb$L?gV4QB*4%;{EgSjLW%Ek=b#2ba1XjrxxEzb8^2`Lq1XNRomvWp8T5Rt(yP*xDj zvKj`k<_it;D9O$Pu%X7XM-rs3a(`RD6e?yy;c|y=R|!uW0@JSHz7U)d#s#byQq*?A`$Nkfx>#&e~x1>)G(PHJg2g%Oe*u|Jasd_DlwX~t}U#_5@O$UQV3xv;~6-@ogAhKo= zoNIq|gAlq)1P6m*+Ut6t->-kNj*5_}iyBT7bwow=?B^BhKqv(^*^D$j~X z%?uU6>3d19>cD1F6h&Ya<*;^?ki1gM`;o`#+6Zp(>0MnFZCJq!bZ4SSrkBj-;*vI- z)LzX^wa@}&CxqD0v*&-ss$HD*RKORTTWeq~Jk;#YDnwD_^CM;mWJc6IHHwSqmog#- zQk@hg4w$D&6{qfx*fThBsUN%1ovq^5WBQ@|#aK!O47`;6bC>1v_%PGFHs zb~WTar|RiZ*{ItQ@G<@H;5&dU2fT{}Q7fLHgxd{J=05qaUumbi4|y5=bQ{EDtl?cf z(WX#NudD6(Z9s8Iz(W#_Zxq^Jog;GfY%$ z6Kr)NB@+k70oz6Gw*a}Rd4Jwf1!3OZ0meStJECepCdFEEARL-^luO0c`^7Qay6lEt zL*t5ku@mj?NbxS4+2yKE?8Z;5eF3M5K(%;yh~j|!_xfo5M*qZgaV97T=&-s+I}l?r9UQ^3fD%V9!GgtFQGpY_~jvfH`cW|A6tcFQ}qq%j&Hjf-x&#GDIqy*iy zwW37NHK(n2EgvZbEfCA!Tc$SDP*Oah|tq%)D*HfewtpUKY<-Y;_fDC8?psL^035k zJwceS+S#L(Xm!U;mWwMZ*nY$TF|X%zF^ggl4D|^^XsaAFCZxig0T`X@cH&Wx}#k)=w>Sz5cyY9|ubR9EG` zoiJN(WAFHyQFDj)x#wicqGQtv|AJUk6T?68B$J){J&^yK%_mXnqa|VKdF8)7Xlh$e zb%Rfv69zVRsBO3?U~`SK$+!)Kmm?h;45K=M%+iMn1gKBIJ#G;BP8Z*GJqJmyqL)25 zi>)I%^=bl!2)Nh9+Ua^}-}|7`Br9&6*p?{l(ANpaQmcB3*Z%Mh@S0`nE@0Q>9tkMP zra~GsMPp4bV{9)k$m4x&h z^&uhg*BQgOnIIH=g;bp{NY@@O=4-~-D|flI3E}xekh&TxI<7Wa%gEeouoflRw_~-`ot3Uwd!aMfmIcnfmg=cm`Udld2Rb3LSL@`Up%qb|=5P^q!f$JAr(=e>g z|1~RrRX|bitHW^pICod%-zW$h#p36dJHu;JPA(*yz-B7Y= zchPR&PJ?z;J{c=9rwpmdU;3ln#3T#tGA3wN8FOkz!zItNQY4;zUJTh;5qls^*RdAG zGN>KZPrK$s5QOQ@zd@hbmGp}#GNB^RAcY~wEO)jp2!n4*onj)r@bfdl@mFkpaD5G+ z+Yh%4a_w<+z(Qm;NQ{SwSYO*ouu7Yw?#8<_mU_x{HWiqpyVtU8rw~h?glOMyD{zn&=RiQ3 z1+KVPK=BkM0u%bBWTR=u;64%4Z)qo93z@%TqC^BYvLhj%t>+bJ?KWwyA0IC|o7ru@ zHFq8gs^RW&`Vtexqg1a1!PU(HAevYM+>QwW?u3XcL`Qbph3`%e3w!q|m!~*-i}Gr^ zYw~Zb_3DeP5s;h_ev8}Q&SlT>1VST7?lu$2cKE;x!=k1;Ui0 zrm85JCaKzq5IdmYcb(fpU8&<{UaI#g$fyQtcdEnHfCFxU#s=#$u#nlJ8nRad^68m? z%K89;3M2cvUqk02`wT5zwWOxCMM#p?r?jEbgpqYwQ3PFk`-buNE1qv$2_47^L*hdV zV}Gdr3NA~=OgiIUi&D@Xbd?IicZFVU1>!2>!%L26BKpNmatY~GAK-@iKtjv$H2$TY zvgD8kF+f<9U~<@EKDXXG7FMm@(}-3Gq@a@UKy|^4f}v+5I{bu1=KE0K=e^C@RuC{w zKu41IIQ#DBbUpAx$K`^Bpw(x)y%h7|WIja|rX@u~Vz3_07wM>H@HIR$)WDxuV9tKW zZ;*0?CJH2%0HK8}IRvlvLSW3B=lH5L`kMQ)yYqcT?NUL9L>Gl zyB+-oRxJ9Qyc=2m3B2?1g_3Oh#y^BnOAc-j!~K7%p~swdkUILFws}t%xrZ?G6XeUd zD(N{VV9MDijT#Pw6S(r@etivov(@4V&`nBpPJ){tJTb#?Ue#s571F@>RC z>@qL?sq@)UH{NqG7?a8g?4ZmGjjS*>E*7dKObkv)Y#IKyDvHn6%!Lmv=%26?uZ?An zvW;UF>@0)T^EsA%mWrq|?6%utf?w=(=3tvaH^&-U3m=4b4V}nAH<~}zR~0DpGwDs3 zw@M(>V9_9&7}IIHh2t@obZBwz2igoA3-?HOvAyFM@6&p{leafcuGQVS@Cy!)=LnS| ztsN$2k2w*W4h~Nd^AZA*3#IWNYN&Cr={&F_1Q#x?dC0|fK3<7pxzo~LeuzsffUyaJ zP7ee7n56r~MaT(CC{5F+Ae+fRppVO`;1{ZrN8_UO@b=A>nZO*ogrY}nGO#bS%(hDaUL9#PkAKyDOL zzWjfJlnl(OghxA_5)G+^l2>;Msa8qwZh!s2?HfbXDAGLIrg@qp>|||TfQ)g@Dvp(# z!cEm-Fr2PEt1_n4(M3UHM!dDjGt}FQ!N=;6FP;M}$SR0rXHM9UkTB0EZ)q-iEF9{D zbdUUVeyX6&`K$Al?0ISKX=rv9GX2oX!6rJGb3(esK7n^iN>}$hJg@Nu3N$vZ6ln3U zN}tICH9MNL##Fh5yH^brv3|Lip9?IpbwH4<-v-$XjB}hD!8v6qw*1%Bf>fDZm|v%jDKKM)r}mOo`OHYb0r zE+E!t6LESec{$zw)AXDB{{F$G@&+$8$Zq+IpTKd2sm|5nq}1cpK|yLb4laU%wG9At zeBcLc(W2J?)HT`l^sWk{y(Oad7TI_k_+$(T2elT){auAlh$~F7IABcBcU@_U6@9my zji;F@sbyujJ6gMzf%=(q0!7yeGs5Z13G8lQ8P`U-r$W$pb%)M0d~;wM8RR3`%;<*# zd?1%wbVc#1;!vjpV)P+ZtFu&?`|lx*$TRkWd?wtF$GuS~tBQWxU> zN$4?Se0=@lg-XYPtKyJ>M*6yx$G;y0@*~Xk(}92Su5QDVC78f-WuC7%GD7dlCTsD* z5lzaP0s|B?UM^Jnk2;=P%>7p)0-p1`JJ>DbT;vcpua}E4f8R~=Z+aOr%)c`?0{G+q zL+x}(Cvq4MmTu35%wm?mEz!(A)uo|k_YmWgW(d9ukYvDlG1DeI4~J(JO&PS#U@t*e zkq778_G0EW!X^59B0XUm)18PlFt+X_?QXXK%5JYy1L}N$e(pM?qOMz;x)Hr&0#xQE z3-uqejA0I)ENX z`Ar1p<1}&|=X`*r!{-KF zN5{JrK``0ph!xyUXOx7*Sf?U<3>B@Of3{vD{#i7_kXa4ueFQJuZkpGc2PX<~^MiZ7 zku0f<(B}^l-U!Yyc2Z%XgEz4WB+mK?-Vx?F=Dq@c#D1!nJ;Usdv9vo*hBQasxs1wT z&hQVXSd+tC_8=?UKdUn%CeCeq&!vWX+U{E$-&S51kxSm_je!J*Un?ey0tD~bEHA*o z|60!GtNo7{!3$?x0$ePn(QrWIisZ?)5vf~ZjF4xbvM`#d{@RlN3zIWOS8VOEf5tsv zUstlg9A_axO;>4?i4hFsDMCFX<74n0NV?=2ZHJV!l!r8M2@?)YL!ykwn|E~)^+rL8 z>S##pC}{(oY-d`OD6}1MX;jpui{@bo2nNj2p@G+i^>#$DVkSQc&G8}MPs(d}?uRM@ z&ZMN%fHo(ZgEJIC&v1iItZ^=NvGm@x0YvolzG#n-j#cj0fNzX)B;0Y^epnO*vj6bE zncyGOrdK-?VIcw8xV1cD4{q$!tR8ebU;4MZ*Kgj|Y>yy=P)m2JYrovf-;uwlUZ_oh zenf`qC9`}5y%W|iT<`O8L}=yAM6siHVhQrivPKB+*JBOvh1_;#NC# z;>pITD8@StX!EjRdlh}DdFnRnqHxIAnUL9NVv@)8db1h)OjIY=*B@gqlex3v1bA6x zrcEYq(>}0_@pkDKGAXiwRdqB$@jJobU7UO|=l&Ukb@=~g4CkC2=IH6dH2hS~`nQI% zns((oxE)HJ@84_&7f*J^8xsEL8RW&Z+xm3nP3{T_?aY>lGq94?gnKn5@j9kSS(g zmei|&5N?)%Yu(88q%%chHror|z^JR#%zl*p3jSpZ%z-R%-adgyJpGC9FnFRIUnLx&V* zIR_PNkgKH~9L?(Jklw-%_Z|0sH!2q3Ne>zywZpvQ8D~-tH^4i^zDY~U3Pt+1a2C#r zM_KRdkc!OALOPW;teC1xz^TXzxwvO||7x*@zERN_2E`{$!0Hekcu+&GEQ;o?VJcUtnyV^3$ z^;AO{w9@SX%D(!OSbTLG&TveHz8to0=KI5|=%X!?scKjp#v(cRgs-^cE--w$_R`k& z`28I4Cg@ql9Mu}Z5D&z^^ybv3WejE6e^{MvC`F$X2AV-{8~%_qR~eAwMvc-wZ{}O^fK48 zM2yalWM{t4^_hu86JiYXwd|ZQKl)4GQhLrBexe^xwiQy&a*>ZyxtV`=mEI8v4%bN2 zoTqKJEcf1Gb(X3|zF~zvdD2m|pqCxwT@j42&$=Do(+G6Ct?2VXAjFS$-~J10{y> zp*U)hrGr}-@SMhzgW8>fQ^wCSQcUv*ASV1`r9c6@F(b~gAp)F!9L0B3n6WucWWJ7s zbU@CWBbxXdzxq$+Pn7C7-@ZVC?kuDBbbB%342@yBG5G?m_LQ7p?d;@H(yB&@feGn> zW-z@^DQ#9^XcVWHkpq99pvdn`6O%lq%Zff24i-YveK0O*opH8j^(8lPX%%j3PpKug zBCUYW!{^S3Zu_fm7haKR_x?!uAOGPKJLp&=EIPU<9!<9y5c(`SY3+jf$A&YslOB`d z#2v)_=bXli5p()0H~(E=xV8YE2nuBe4#^8K)r7AQSPCFO{fXt}Zp)a05ts?30$`8l zY9#osZt8S!L6ajrY`WNrV4~D^JesHDDOYUji^OVDXkGZ*I0-7(4t1^Di0=tf5@e!L zx2&6p;w;FM>4S2S!Id6T-$%srU_F$(yhO*-4x|6*3{SwU`;!A*!I9T6G1@?K#qhOd z7Mo$pS5B|f{7B1&K%DEg_K)hb_qrL)MM18kpY+)ZzsMg0MhE{tD@VZfp2I zGZ!@&_Bh|)%ey~Y6prK;+agW+<)}eSVG&E-=L;cmO0CXGWXSIhtv`2|y?g=2xI=*c z6G2~P1w)$@ESchxnD!P@g#wldqMWpX`qe})ML;&Yo5WF5N_GQVabm^~Vqv^6$K#ZQ z5V@{tIK3=7bf&mMz9nHDs)BW@Wp~FaYH&aW$=%=+iwHV~#}8K;7V=BUSb`v?j;SdKU-x z9cB0ptUio6;qg6nbBfysgyQds;PiZ_*H(}ry{rh<<80w`%Q%Ibhled2y7J}u@$bKH zUY=|||9zc2X_I^o!gAvH@|4_sXnLIgxUyYk6a+oS^Jwj;xgBGf)1bF(BV!L!FP-_H z@Cit4lWMvnBMG3+Lv5FWAZyms!SLxYNP--J4E6%hqQzLyZ#WG!zc%Fv4JJ8eCu>(P zh;MPGWrn#(`eqVUZ1U{?wGWo0^#n`Ve{WJl6e zppaVo*z4*2TWKHhcXwMt|I|~ST)Kr_G7%Ws*y*#4IK$5h)3VZ?MX~EDJ@5bET`n&C zergnzHfPLLvEZK11_bB z%4QlCs4du3i$&%SP?>Z7idN_N0f5;3OWf&zVJq}rG2QkSMwJE`aYPgFFy?xK+nU_7 z>Oi3}j||!dPLu3TF%n@7wIZ&a?0BF=CZw4lCm0KgPLlgbyitCn&M<;8-*)|JpI%DL zoY{BBVoFY}Vm7WVXpZ6)3fw8Vv^;ea z?K)Wl?lpT>NBPYytRRXXF@Kzi%-VfkJ(~E8sJv?$QH{RShm*LD>P|9fcKBw|@U{aG z>tdETJ}KQi$F)2-Og&^PCVQO@>9tENp7$srq#eFCvOUEA#J%QCaF+cOuReI%@y8Ws z0;>v$ry3Jmr`5e(?9iK+52mtyvcOiut-!*liV$Ul-clc%nqd}$r$rK%3`h}aEFi@q z;cHDUii>C_c=>o;PT7RttF|x7McWg;Sz=Z-45=T6rqkO|CHM2TpfeHTWxb1-=PaLc zc>JbMs+s_OMe<t!C#d)y}2Tv(^KSv7mGRNe7d?O7M1D9Asobf43 zz#$DaWC``6BpBYmRSxn@$6iIr$>w#c2_>AUG>vT;cK_F8zOJ>MLb}@*#Io4SoK+>f zAv_TOYpZMY$5PWfME5F|$QQ#7(msX$T<(&u<1MU-{{^ipZpon-!PHlXU=yj)Lj)Gd z1HmEAYE*m}Q3z*_Nrwgzins7N_6}JKD}lxs5&)V`m-Yya5czQilbIKHjO&z^fft4P zy2po6iDHV6gmXG>0l!(0NXc>+N3V)X5`WjVOQb+8)|Z-+WH?zNZI@lCCo;0^k`(k8 zGOo~OR#ANZ%Ib)PxY#>XM62C{w7P*kBM5z*8<8)43NYj6Fs3rrw&1d4Q#uFl zXb9Z{eQb|zWl=U;^n>>I_CY?_A(Yol$3OAx+h8>P)KU(J+GWNZB;2WxXX!me)j zy!yuO0TgL<$wleRkza>j!XCedu=32o%N4}W#cnLe-cHs;_ADy+@cnIw98n9~$l)A33BWw+-%}*k$q5ldvnK5tsSG8%pvz({tI&T)`49l&ZWcN(FuXy?2 zNPaqnU40#w>_@sDnC;xec#oESyU6$E$F1-yW1cSj%jg>^zB$kn$;X(sa$g-Q8}}#K zWZbfkh90WDm>Ar%x2Z&6HdpUCCNqkKN~bA`f6OmnI)yi(4y>kxpSedDY?9dD;8nnp zAfMqiUNVNniN}CdL9DUxd3@QIUA$mWiP{XiNFgJvkRKCe;=!On4hA>X3tyt_>hiN9 zvA4QzS7E)`fP}gk?k;hKw0Vop%-li{(a_yEEvXT{l_VirE%(d~u!x#3f0^vUfiOi> zN4x&zIjMW~Gg8B*K9gEaSI8g+@I}yGIb@tsx|;O%*p53Nad|6h%Nd%QrirDS=d>65 z+$_4N>y;5385@lH(t1oDcq}5I z7$g97&l0Qz!+t+m<_=>xF-VJ$HuK}fzz97C!F@n597=N7f_bV!AuvzD9>@@3;=w`x z46tcLYU4UECVE(T5Ex5$Ru>!=VVd;cfo<~o$us1Sk7Y-z=LD>2t<`xGz0q3?@}@<{ zFvcSi?XB42#ACq{ZG|-%cNcBSM-GWtx3{FLy|oDJ22GODa3YxCQny`CWGdezX|7JC zcBLf%xqG2L!4zAKY zsqVnw<-(ixZ5ii~Yn|yjy#9|n=-`i_Qi*X)12oKbg!1{yy03!>%z)x}v2d1930ycr zu#{%id@;u=eVZTR5fOReh^yRRhonCa2Z}}yWaNf`0Q#P>@lm1Fl|Zh%tQ?HsX~|Az zU?J@62Im?je9(NpL6cd9mQlDGnySJo49L$*TPC^juD3=w2k=q%18HM2pZ(!hWxf|R zkFuufu>7?hW|boPa{4FMu>cV{`M)l&~ z3AkDodl8U99?rw7AEEBAZTBQScmSld%-{#{ji!j(nDEG5O_A8#*_PnmfIwkh>{Cl( z4f4~}_pFMLI%f7$)4c#W4--Ra`hinfXnM{Ug{$PN7bj|0*OQ~I)G9t&R;es%SM#A6!O@~1kmZ4nl6d}X@Ihn1m}>nbdSQ=87E^AJXt2^E8q za-@jQgv8ADEpau|`oioeFtWFFxJyJEMZ2xrVjYcdSBQT5GE8Qx&Nk# ztt*CrSdOWnocFsf^6;y?%a^q2q7oM#_)%s85+P^%d&ZFSAcf^{8u?H8XqanKadCTS ziC_xFvhitspcwhmPo=|$K%_`QlIiuQX-Q6F&mlEZc`r|?s9enEIC{nrP3W5AGk?)K z2f?wx;S||CV?iI+OL}rnt{EAaaxfK|O3N+16qgx^9SgeQfvzR0?d^%~qH2ElrSJL9 zOYS0gbr|KBQR5530>i7?KW~@YhxPog2jJ;vl;v!X!K>k|0SeXwcJV^KJaFj=bOqxP zJN|`81_kYj4RFx&1~KTf*h)@AS1IN{>v#Fn?c6XGz?XST?XXi!T^&%*@j)&XWKj!5 zI$$|;M#Ir_rW3Ue>*-QmI|{`hr%VAmgX^rZPIY&HiLaz%2BLNVlqf5WR7@XB*D1mo z7n`~O9`nJ=rt&!+(Cw#(YzDIv7s22fujR z2e)5gWtYgVI*d!?Kwj+K&|vm39bdWl)O460(#Xw&oq;+Vlpe4iv<%z;1Q;05X3V_B z9_0+v&;%a^s>!#t+M6ivQ`=0ZoM4_vlBPY@0f>-P2oQ<5Yot+xLg?C|$K10Wz+_pq zpM-%4rp(ys%_Q>kA@VcDt|#-(^PQ$ob#;15Jz_4P7jm==#vZXzpekt!96loz)sn4;?QGil- zvr=NvD$`siY|%pTg$?HG>3ESb?@Ycc6UkMH0bmUI?1%%JgCRuq9T9GJE&5P8(E~Xu5k2KDrJlMzA`_Mbk)8PBB z604pL_bWSCV)E}HmV%fy1U-&D?WfQ4OG%N>wvbw6^cwTDq-Ck0UV_MPt?L&yBTI}P zI|tIymnfqRxgN>V9{*nDG`6>N_t&`pZBKq`{152Xr4XvaxD<}!CDezVqoc#^I2~qf z*)PS*gVh6;v&lR-4M<9Ra5G5F7X!Bpc$)>5q9fahfj$f5PNx_QGXprT|89e9?s+jP z5H&}zgrgDJye4Ru+meyjg{P?zgLME}rB6nBu@=UxGe8!4)Bx0)C6~KOr3)~_)@`|- zEDEb+RdM&E$ii#=3_aj_r4OQikf*4pWI zdYR5GcL4|*?;g-GQ;4%kfFTAhBbETrF}SsrLCioF#b@Rk#SCQ2;!mMbe+U^Y0)y3#R-cN{O7Gp^IPJ$D3r1i<1l=2UiFRj z-@AD-{le`ZIqKJFhSbHmuMXqlJZ_h4t93SZV^7G?ojGdo`HYPLszJ+ujl4#BPIrSi z4{EcK26_x~A&b0kuPh@Q!MOlGBx6VhlEUFQXTn*aGnm}2tf%09oQZ}$%FT(q`jJ&D zLHk@~ZmY`4!1kr<9SCbp#w1m3$h(MbD zpptY4p)l013&pJXW5*05qw!6Jb%tqUE&3%~wn8yN-g*7OdI0h$>2olLGr ztbu1F48j5{H6OmR)?J)OL!z`#?5@K*l&DDQlAU)#rA`-$ukcvNJI;hvCuMVmC0V{_ z-RUp=LzZ;T7bhBgxexTkTh=X+a>rVUQA1sp!{%h9ldd`_?>=NY%+V+5hF^lV6=?~? z#$G{spo9|_KY2S0u5D-&`0qQI(&3vle*qBm%LqsxxUjygx8=G&%DX=_gFr^VLn@%{Fh$?8z2NU?!HyoB zmN&hyaI)F|vAaLA^Rr*Hj6o=?1%a|-Fc`rLi&cky!Ex6FCyG8|bd@}3#eg1p?*c?RnamgEg^yT( znHxo!%MptI07>jXJ1ER`Diu7L&-@U@V<+o4crvS^=u>8cY#ouvAXzdl6K9BYd~RQ< zlQ!{ZIGu^eVe=)ub8JEH zoYI?Py{u$A!7n`np&PF+=Zk|8a;)RdOvbYn8pFRfmbG|@@2N-S{Am+M$<4Lw4yl_{0O(txTtr z1N4wvCXMGFj10^Gjp9fGpaCrfjZP2R`N6D$kcq(y8w6-<;F8Z2nH|i@!4_I$@asK! zAyWjAhelqZI2Gri0Ynr4faWvNIwGItGnl|ifD0X#Wd{%=Ukb@`%4&ilk~wQqNsHK( zWu8u^=yF}~(Z?c4VWS^FYALNegOjpyMo->pmaCvE4*(Dm z07P#}tGEm1(@17;GuXR;Y~IjvM$udz#NbRc+T()EOjP868fS9bitcEaifA8|7g@yx z$cqjalPNr@Yy-JN!P7adqE}@5kWN;8%c1DAe$h^g z#b4}DY#rrx14Tt{e;kw2R}M}AXWA1rYR|t4%7yU0okM^$XZu90Pc%YTXURZdv%?ME!rvVO=`Fw0i1|LaZnI6jm8}!~pVD#H=b|pY zP#wm_cg!x`7LR+^YQP=jN!hsTB^-xuhs@wy2Bh!b7kcret^t-bqm~#{E3uM{do~9r z0;oEg27e6rLQ%O0H%TH#tb5K&xZE!sz_>OUo< zKlfYW?xQ+`oxI6uEbDHmSsS8G+Ii8prIT+?g%!!medhLN3y90NdBKns*9X`BW!fF`sqWI!;=K+mYe zS+29*Dcxm+0GWo4phL>wR%oIW_#y7vVIi3GueJ~q(9`i)e<`3)Tj7NmD4bbtqJh~ddjZ-qht*E|dIV_3W+jTtbS9JBj;`FmYOcAP#wlab+|6^#!lPo z00L$R0g`#3$sw>TRlc#zAPffqs0OflVCDg8E0Hch)6q#tk0N6UIsqzgV$NwUz&3jD zikAb%U{`yrLpvn`ECL*;w`?NQk$m=P0(D9^&Pxp}Xa1I@!PY0b`s_ZbEdglM zQ}{EVNaj)Grn@eYLbUkDP$`;ih76(bDv4|-GglTos*~!>6mp%cb!caDj*DJm=(HI- zx~~wT5A@yvKIw)xc4x#cFlXKs0)o<-v*;@&EI&C1!8{x@_=D`$DE(L3pmy;51b0#K zRCyeqtO|LYsw+ehA?JfN@}`GoS1HymW{Fk5D_%~R#{Jhl@#(GaG5bZds}AELI#`!< zQ#JN=zx)#5=uie4V4^{+r@ynqV9<%9D<`3UZ3YA)(!>SioY9WFL&$W=!dG-;`9Nyp zgs=k8$;hCXTV-R@`6BXJhrv(nGf0Tm#iRjFsxDiW>+;aer-WlgIS-vQ_VEUNPLG%2 z0I)(Uv6`l2-c&B((|Dw(*iEV6LqI7nO_3`7o_BCFr4|Z@5dA}%U^OzC;d}g3#g{VXc<;?D#^VBsfS6U2b%0zo~@}XnLb&R2|Fjy|D2`6Mx z>z5V%7mCoI85P?}QjLOjIH5GhPrqZ9TeTz8 zs_TJ`?w*fY=yKO8=(mgur&jHnp6*tEomF*yVl$$%2Ucan<;G`*Av6X^cK`@HA4&b zxK_7OvTo*F?0Tpxx5W{NMW9VNE|Kj@M!|iii*9@L75XeiE_C1&f-h)gAH3NnBF3$z zQJO_UUug?GG|D^p=$l{{yUq#tbtT=Jhc4pH~EY`_&lc)B^Ua4{-d2E+@rA9i5sWnn02prhKAN7;;Qm zI1Z9d`NGLYOx z(MGyssy`fj?USFHe5=U6q3wbX)nQ!lhv~_Qr?kgsHitnH z8iNr&Hi8GJb^@~k_FVr+at>E~RZ4rVQCer`oZ%VoB*)php980cLc)SL61sfJ_A<&;sVU1^tjgA{@yFmq5>)0MAi)0Hom)9@=Pa3TUM zwXM{JEKtSlWA*YTtsTyGScx-d|*S4`>} zAm^veqoPhePShb1!Umx;%jX8-q)G)L6MzV>ORU1~#S(F`Uqt@xU1od+T=R!U&oFZB zwAp|kqBN=tT|G*0JaW^bJ_(`Hza^!d&Mc-unA^N8sDsTn#raKeryljrx6OHIsw>Li zF^BuCBI)!VLH`zs!)Zi3mDk`nFD&$jJKN*Gr~dtpbnr{PphI;S7xdwIej2AAZ~nD= zATXT1WPo4c%kq6GC2vS?NDrrp7Gh#Rl|hWw0_>*6khd!HYCwB1mf96Fh}l76X3^i> zro*q*{q_5qNr=j9Bl8D@l)9NoC(&$tM$VnJ=X{&lTp6>^ed;-f&g%VKyRfl@~&8ujrx(JNokOZ%=( z>Fz%rr-whMVa#l!oDm~O8x#{dompQ{#pAn|%q(7UMKWT9O1=nmih`ixSslo8uoT<5 zN|SvDKyGvR|6BGiYy=<<>!a(Oov5!(FibOcZX6)1|9HpG{790S+Ijr*2f+` zWE8=}=K7T281k;Fp?;0+M(~*dW_85PC#^sovqqW{bnVfZ*|MFY?RFgc-Ezow^n2|0 zd*6KLheo%E{i7DXfOpklT)+qCE8R4Jo;uD1`!nRZwW;+qa`aPESuHfW+VHc306#t^7FCR)x|)Qj|9@@9RV3Bftwnj+HTlQzvBEjyQfH0o0IZ@?Fwoa_~p{LXP9e@xnHKMTTOQ zprBKidFeMc6O9=KHe$)f_T>0!7Ur~di%&EH;2-*x$qDxWUHLac^@Qj{=y8!{sW9wi z9%X4mq)Ff~1n9bcq$L0DA8m6$Tz$zRQiY9jnG)!Veu%FKD~3M5O|?F`*z|XAZBE`M z=?mUr=orbo=VHKTTh-{ow*beSR?Z*ycf6zRzx9nzT#FAW9Xk2h#rL<{gI8{D>3yS`1puxFE=HwCCZkP*k+y4R0Pd7CNGaDp zV5js%+aWoNy*FlOp~5m)5$g~7TBp_wLNA7!=s}NxKJMJtBf?|d_eh=~8>P`^lOFu2 zE;=6-LI?$@ziXG0MWIXnZW5mvxcCcEss38{JY=uS*X38<; zFa@h)D7G(T@fLn_Wtth4Hm&RdRP=BrpcHMg^Dxk=%!Twtbjs$oa#mliDNrvAg~v9u zyvU_;|HM%uLT+aOJQ?yboIV zu~p{O+WZ9@(ppwgG-;cW?vLG^934FQO}&MC$DjGN+nV9(7pKk2XSOf9>Gpqq>vfO6 z{aa>hwWTfn$?9Ti#u`dFkYMmJ#xl`6-O=&w5 zl>^)WEaBE$|1y32z|MJnaxw)Cv)|S)FHy=r_{koT@>#sX3pdPDFawPi7+o7-N zrTafD;9l3@R->XG8t6tuM3MrBnd5*3PC)<2E2n9f+Qr*d0XmvKTo|Tf$4;ite)g_( z+imxxC!TmpYNl!L-qrM?7hRiP^{N-ALx=X+v2jM))k!_ljbpsxW%A&dP)}BX+bvQ z`1Wze{}oDsx|uQJrC2kcZ{m zHOh`+ZU)&tv2k_UIsOgPwDIjtyY;&1c>RX{a57A-zCttJlwa7^paso@{x}JpZMVc+ z4A5w9pvzIT(DgF4MqYxL?*uT)8eJu1fO{}*J%SfbJcOyEgA)GmkL-Ki95AugD!0EpQe|6 z`$Qk^QUBCN+Je(~-}^q6e*M=!lpeVM5w$_~MOGb|D9r`!%P!li_PjQI-}ilMx@`X* z0gR3n4WM#`NGc=n{QL(}dhpLRQ{g+uiX8+_h5QSt#=woj(J{qQP z3%F#De8+B&QwSmFCd^dP)C{oUG;tE$Ch+dkC^av9B+#q0_C1cdKl<>MuOEKo$J8w?S z&b7@z_i2t!X)S!+4#JMW4gzN(XCSAcN3RtFPB;u991a6O^A--H6%ZyI2P26=2@t~S zJTz@`CJfy^qr@X}F^${hoo2Y}{fYKKrI)_+y7Uu2@wRl;;r(f6gi?X=$9w6-7d7&Yc+p?z>arU}J=RG+x0A+>WrOQp zH%^CMqPkF}GH$E<<$v?*>Amm$h}-5{fhCOX%7#+z=o*c7()HI}nf~EF{O{7MUU75U z#zs78Hm4aCt9O^N5)q_jKj z>fKN59?oT^({*u3we{sJ2HJ+Yq6RMx!}XLd&C7>cx7B)`S9^-XgI97o*N9`0+sXM` zUk*LmVu#i0j;uyEGi)vGNpE@iw+%m>RbOyXbr=`iAvrey?H_E0 zW1XHJmya}@5=ZGZ@n!kKxnhw8SPJq0b_|n=Sz3W{cPVe~teqX?#Gdd~u zH3rwAvwl|uKhCO^(-@~4Z@en~lYjJgb!NZI*EP1+n{@97v{=W|RBnT+b{7#Z%_%Wu zw5h+M*~Q-+dwC1Kfv%tYyZ`5R)SmZi=|k5Z=${I3UT%}ps4p5R8_8+y+O?Yg@z4B^ z>6TkwW~bpM1f#io0-}d*)lB2P1GUNUX}TLGKH5c~4J9TGuyL43=OXuywi9&Jf?foJ zn3+{3`g~jF*$}jUg@HDdd}LK9(9CDtJsiedz5p?z$a=C&V{_h>W?8E*jId-jN8YJW zmHQ)2=t3^Xu2;6hSqv28F#5Wc`SJT>NwZXEC!AH6s)z&p;ZiUm^s9>Kdrm}sxxdt{ zjlTC(*UGYv*WTKSV+JL+iJ+?dV-;IQsD>o!m3o zd?Zb_bro--C%`7hn%?w;0Qr;vT(^Q~7NH5UmNItf!t52PS-v?nYp+br?$@N=p07>K zzOUEzT5VsQ((ad~cI}4LE?ucpv3;sb9}t-6j8?aXCOs{k^whLB0XMg>@@Y%Jo^Oot zLO)Ikr{dS@R~oHvr_sq{X?pz8)SSGdx3cm3&4Hc&lrEpVJ1w{O8pz~LB!isv3cw6T z#5e+gHgBW>3Cb7u;-FLvpL)hwcvm;+Ohch74;o7ws|R(vuC9e3Gtg4qXFhXR`kixF(^@GFn8~wp1fzNPLhc$KlC1=}F#_^S07ogDwMKqJ+VKquezsct| z$(AmM#~Vyp5c{amP91)e-TP=C%0-H&42yC=Ud&-ePITT@)|A`3*anp z6x{Ljz2CU=*0$OF-u1nEI_AQ!(H0s7?wHp8tR8H=fbaEOC!zBN!_phL~2eQRTYoxJr=sXM2}6k6D^y7 zmH;^e3a25>f(6wx8(`FJ7VoqamYz(@y<=%<@CEswo6^bYYtnjirFd{0AW051mrYq> z25#ha2FDW}6tI&>PiOIf^b<#q>1-~}=*SGX3Ab{Eqa$y#0Uj(uN%;%<#&F-0gGlrY0SGND3v} ziCe#T%oz=&?Jxw5GX5gCjACsiZFY*4V4XgnpO{eOEYW3P^nczd!Vb`+n6}wT$ws6p z!lsl%=f#fn-z{XCqK4p;988Blc%dOf4>&<6brfFmR3ep8uVSqH|Fid|v6>#)eP7+Z z@9mXMvf131;Vhg@qZvsa%aUcujx2eLWe0M;C4phXhJgS90yrC>5ex*$hX8@U1o;r; zQ-VZ+oy4(&7_lwImh2hLNHY>g6v^Sv?k3s$di(Bge!u^zdhgp5DGO25-HC7A`@B_k z&Z%=w)l+rqspYB1(Q$?W;sb+24?9i8c(cRfg>MSQXmpt>RCJIlKJrbfp2krwd6ug4 zH@&iaha?|Znwx+A%G;Y?h2kGbcE7oU4iBfk%Q>?|hdC#T!w_P;-A*Z=z5{Q0$= z&G&h#UuWrJ9WWEz>pa%8sN9&#pZGDCbR1Mf>0&XQ}uk^wc&wk9OmOHo3`&X5%uW3*^Tn(?V?FG>}YV z;iv8NxUG4DL5;bb-)Jifztxu88*QyU*RIchsokD?nvR3C@-i(tmeIoRtyq4j8NQ(* z z8A!BnozA4gr3`_`L34=yR|Y2-8+65zMp8;N^Rsz~7{=)kS{Jzi|~ zRT2-(d*QU?(%i!0?)v%9~moRjtkJ5~9j`B|N!K_zLy> zZG0LKQ!ERMD?G)o(s7(@b4U3s_mPL&+{rKU5(M}SMjW@8a^jk7USb|<1H;1jTdSc= z8&O&ezLuh&k>pgC8@3q2wAHy++RFS}jZIYT+QJXEjkyQV2Hrg>-W_h9M)_()nSv4BB)d}CY1jYBa(CPop1mA>%Y_PEVyBkt4dyq0>IA8Gpv+70&^Qfv7?31!bo}4{ zk6&&-_G4eky96~t=Sg~v^+=*_cDSNb zGTylQFQo#neD)w@#dEM6^mp38_#Fm4 zgh7LYE~Ml^IIdD!dP6p{B+CV~k7{IGcfv%b0#J!%Qwj}0Xw}23hzN$hr!-csvr)SF z@Du>*(vSc8KfUqrm;dNdp1STYcQC@ZzwXEXLv1E=JC`o}+mp#V|LNT7wbOfB?Bi!7 zF}ZxJ?OwFA*ls)P-0H0rMiL85iE#1UKJhi$zKUmqp_x{^YoR?)E$2~ho~fl!q_Mcj zZS(ZWmKshi-(^?P{2uS`O&C$ISTV=c$^6>!Hh=UH)9~moK&5_pZ0y^S9gXH4XVP9mEly z+So#hahveS#y%%r$zvYhi!-$MFowH4lJ9YOO8+iXPkW3S962~I;7d(BbGHSI@H|Ww z7#%FoNzAiqy|A;vwMD~X(VXGHybI`Db94mrSFUoM2lo<-=*Q?hPPL_Dj}d-~>v7%> zywvt?zQ>veqmC`!hj2>FhM385{WL>h`m{nj0ra11x7zXDUu~;9Z?!A5tLt;mfud1j zRbuL)L#;H_OhH+!wod+SuxYIFL{`KBut^&hskHnOfK{9e*h^HScW5$#Gy7tObXc^8tCskO_R>ikow-{9g^RE z`OWtE&wqmVVO)1APX5aK5Phlo$LNA9u0<41C?Q=*1s)E@p_G_wuZRFX>U(;ww6t=qi?C z4bHf3;H4O_4`IBwsVfScp)p9@{0o*-K$Sed>EMKSWTb^y%EXGAV2Tl0>c29-u(-3e z_a{IebQo1a2fx3c5}=ay&f@*UdpF-;3Tp0ev|H$S6O=j@HPb8VL$NbW>h;Yk+b zj{)Ans_iiSw8b{oE!H5m@i$q_zP+&8c6rly{?Jle=2}`??-^|&Nx3#mr4x#ga_MEhE$&@v z4{!g&wzl*_yRi6&SRg+FUdzlzVP@krHlxGffVXyt{JD@#j^I@d)`nW9%U__h?h9avddGjF(Q@H z-$;)UDrQBm?6_`Yin2iRdiULP;S+J4G+4qpJWm;feTgjSlp_>G66aUHw1q}uB84L^ zf&x*$Awv?K(2=$%hKwQz-luS=d{Q4$64G$Wdt3s$lYeA{dFiAck_r#2^e(`pjRcSo zNSBvJ)8iCF(@B^-geRfM4@Yp7&uAawN|$8v?l8RsRRCbZ`H^XN#I2*HWYGuPNffKb zkx6FMHH+@RlV^OE@c5bg(Kp|moOt%xIfmi)k2@G)+&}l?_oT^wXE9mb+dTWXXcvED z_uaE?`z=Nm7dg6OYl)Y{mfAMcJ3G9`u*1k=n@(YqHHKUBYwgDT;kL!8YRgAf+R>9M z?exjzcJ$a{TU(oND}2k?vvq6_HYkmU_1uc=<jKkhtdB6_@Dr#uOm(p7`BzVS`i^;kKyzgGMHa{3Zx>enNV_)wH1=(`G4s_s zp0vW z%1s&q@+lgC7Qi%~;GJ>PNg}D39-fsUxOnTMw>k*phv0;_{W%0+?uDB#Z+r>pFNwNe z+(CzNzubx6qaHN0$)(AqV>`Fr`HvQEtpB?^Z@$|$&hf@JFOp4mRxloJfZ}vjMiU!! z6t|a-wcGQn?G~eoxz*Kn?4d*Lk;hjtT1&iV(4UU2+^O9(Ml7Zc)W+%=Me<9>0KUT2 zFf-CH9r*F|@9V0QqZq@(T)YRs#5$von{DCp_zn}q`40r zCMn3FtA4^oI1S4uhPm^Pu3cn!Temtc7CykP4mjVDNco0&FEww7nVQm)^WX(Z4;}GJ zhmQ6fxa4~_Kf;I?>-}<!R}yQNll zZ|(j#)W5`a-?)PgcM9c`!SC=MUClPKe_(H{*f(uoSv0C74-rc&s8rk#fM z+0H{lQ}U_-CJxRvWTzsXtyDwK+1nh`ak4Eu`gB{lb)_v{{GjcfKi3v+vA)45VxG=p zck5c4yL6V($a!A(c&IHLb&>p3n=BurQ(=*uPRGti0rIv@c@9%K+m|vqTY(jr2e-c7 z4lQ44@8A3*eAaZOtv8KGohmbB-y$ePa~K z%A0ZKH#saT6?H@#6?g17FIq~WY!Ug9fc};j`n_MwEatwBG=l~WLpc1|h{VauVk znaZy`b`D*(x+q^)AT%TpuzI6Nvcr$%JzPTk#CSt33-4X&ZTU-C_d7V!R%cLUxL*eQ zJO}SmsKeDkPW~hiZbRjgCrUc-V*N=uC(?lliv&kl+VoWeE=*ie~wz}#zS`D4mb+}Qp_d5m8Jo0!b z=iP@vY38z^;BDY^5J&qk*C|*mG|1x2h6`; zyV@4Ej&J*B7SJZBkXVr39P>En zh-0CxZNJtYqZ3+x|A*V&Bd17Hr_Mh7@QL<)U-%^NFMOMk2%{l&t>#PEwGZ>qVfxC@ zb<5#Pb|;S0i2-+4unX&2ot`Y|W9Fh>Pb&$X!WN%=y?Sn!7wR}cmv=dS=&N6BZ~YJN zwnbhTDGrz;3OBrBePnIk#P<9WE2q&c+>=k94rr)4#lz?pU*i+*J#(u-*g-I8HOx`E z5)~X|X---8(9{DVuFB1{xUhgXm7uHJ;f2p_F%i+JTslzej=9j~$vTV@4V|fDd<8`) zK0s9-DU+HXX)6q78UbclECKPR3yAV?iZLkDs+a^*frTHuA`_h@sUHJ40Wwqs;Tkaz ziI9#PG(?4q5;V#}@SqQ@3-!QEmfF5_^}7#z5{lmp-F@(L&|%yMH{o|z!D{brs7uFo z&z}AHy|cIe()PQz7q)m>-D8cyX%a^RHx^gh_0<&*8Ox!i-al!(5B+f4dE`lWvbC6k`jbET)%Hye9^7E6tH@19 zSw_bw-}6%8FyzO>2~R!pV0-xCli{<&3vs)2%F0gONv})Z!;46)L+Rvv`v@=yF=`zQbS>+Kpl$u!&=%{;AlE_H+oj7V<2I+qwkMOmDO4lTDAKK)$ufvC$X z-(y#ik4Em4= z10ksiGO~am9a-+@U_a|oLMW@IC7vA!WSOHwqNT=$aV4XZG z6n)d*%m9-62<}APlXYC@gVcx%lR++>a$N zH+ggN=AoT)@BfFBv$y~9&Y8`HZC=>+f!HUPLeX#w& zk3W*m!tVxKFXKk#?NN>7Huz1fxSxvLW^r~CTdX~H`hSy4e_YNAZA*u2UlO;Yn!VD= zAgrdVs)NZo2y!6L=p!Q>J2_-r*?>pjjX zCL3+*{M&8&gEv^~xRx`g=Z_v^ifffEt?qQvkty?)23JwT6SfG?vukVdSN>U>d*c-h zCew8&{tVMWKb^Ou86Ko}qkK?Ky>b!JNLB|TTr$dtLV4k{pI}OFRiSO`I_n2)sU5~^ zYHv_Veo>Bemc5rxwAaqgw~KF!;wylsA8J4I=YJyQsSzFqEk-g%)3M4Dytw==Ui|D{rpo)8!1< z;2YmW^3fao2pIP#hBmzR^_|kC&}Y)7{7h-b#br9eArss$MV0_O^C}(n)Haf^bUjh6 z6YO^ub>EulfQ5oyN`;Mj~two;%F3BwX{XC*(;otC>-%TrZ(fUvS^dD(I^n;)0gF`%qMRS5g$PrR$&(=T8}PB9zE!Oy2pPrLn@KmV_>cVom2bi5 z&@CNA_s0}zl>vN7m-L-Jx>v%KlkwTk$8;sS3nU|v6-1cHFdRzJNkAo#?6Wxp&ZE-1A%TyDRV)&VS+0&z;};xxM$c zS9aIgwrXd=wTbb$ym+Kt<{T%V;x||$I5U5|edVhUa6X{M!&-{NOQ=^!L3S1yDcD)i zN!S6T)1U+Kh2bsZb_~#Y*&$R1f$>ZnqlNF~2xkB3oH)XAi6=bNh$>M;eM*_M!yw>~ z?DMw{wKo=?Z5N;aYTNnL7uwE=(`}oMW3tSyDHPqhe6DR>I>Y-PtY`RH%cFdxk0YV; z!2vrBYcm~&?^5K=FLu$mlWBoR&6V%|dRu(?HyL?&%=tq5tAFJ$wy%8U^Su1FNghzP zx9aFb0msM_aVly9nm_la{}@m6Ph^+W#tlBE^w$1#kN3l!QY*+^@fiDuKvD^K0{J@W*5dI+P7zA!uM+&1bu#Eu{b-ecWCB|cTo zj7yWNaN+YIzRFX49JqAClxdYQfR0RBhys^~;Yv02^Y9`)_0l>yTP~8KN*`$qwo}PL z?w0Upue`PK`Of;DdkbIt+SlfO{^x)Go-1;o!0#-9$@Morz5D*n|7Py|-ea5BI1>uP z=Ea7ec9m_iSC1Uan!wK7QhRy*MElB@jUN$p^>7M`%6FjQz;jX@cX~9$T?EJ z)Toh~Yfr6ix6eMX(VjZ8*&bWlX^$V-X-}+ewkKA%+M_ETpTdGSN3!H;6^8DZn$W`> zJIw(tETsS74}F1A#!ma*_ugqc$n+&c)W_wg$_EqZ@gH_>f9B8pc>8z$t$#iEP9ZU_ zjQz|1XsO-&fL&wgI*jxfI9Se>jb~fc%L;a?+(_SRh|u!ol>16C=U#?;gmir<82;t= zkxqC>s_P?&t8f@3UOc4pN>%9~(SrBuk7XO5T(XT-fmwKLmHuP#2cy z-VsGj zEtO%I9ZAIKOG}y4o zUKMYr&+)#+9BDjlH%?(F?Ua>9UE}G2b}KLcMhxKQ^Pl5T!TI)Y{_LM@U&Ubm;xB%) z{pO3Ww98lE4Uc7BZhYqHC)!W{^pCb5{DIFQ556$C@&01F^b+rVz|XSraPnu4Znw2X zj5&hm-GFbVkiX{$N1DC$_V$gV%wVYt<$*r$e4V3Mkn{9cCX5Id+h6_5f1!QriEMIHtSPkDuL~HU;e4d`#1mR-4AvT?cCgE zN`|eW7~&hekauD2NW0BPhbLI%clI7?=eCcwzxbm|?U~1TtD7p7sz${djl%k9hu{LY z9Yu8(`yEC`8{jqOd8#xFmwL0?bi_&PRM!K?TCO8&ypyox8@H+ZWT9%hzZz0y+}>n* zgjXmoGlO)CwHNhB15Zyx3f4Pfu)6H*Y$dT%JTj5i-9m>shU4_o#dhQZFZsRyPFuQt zksgVUKzV)@?^P^5$a@}3>;~JUds^o*v!f9r)6OWRq7Pr$Z@lo8w)M;>Gh$odadMug zdcXC33GHrlgXWT%`! z<>D>Sw9Z~#YTvkoo^-CNrK@|g+aCGcPJ5OwtS>BE;)|5q9`9#-&|ZJ-op$ETd&GC! z1ILfF=RWZii`>t)W5?E}(@@@Er_SsYy$@%*07`~rP z^(%Y%>fW;a*wM#1nWK&tPxTGgM126z)vl}&CXUR=l@>m6rAqi3PamOPm5okcaz`4E zk>LoMHy(U;`;R~ShgaVUpL_UoFv7Tph5vD)CyXw3zPs~RI7{%*<|Y!~gk7T@};p~(9c@YunsC5%BA?gP0U z26pE|9fl(h)<*b(-jUV0_RLfB?c@nQUCEQR_1jD4@+Wzh8>kpMlbSK&oWAx1M}c2s z#^>C{y>|6FG(HtdL}N+7MrEtPFXJIervZLrW1+2YvFq!hqwN$Y>P_B#nMSw7`x|r` z8hGan9^#uE?qH%u&(T3m{Q6&sigH=k5af$;zz==zMZOq#q-~ylJn45C60I=*_2}sb z)htijewSmL3_6;)`5tRTzs&htSLb6?!W%w5DZYA>qum)cb)qYqx>3I6;}d*-XV?f7FHTSA#U_sk>hQ+ygW3LMTjRcT z*bNqP97*^f@#Z&H+FQRyp+hh41oXO7CEX^YFsIyfbj_3neUzk^8Qnh!$|_3>QTVq5>=epc?Kt5FoW^vpo4)89i0@>^4Vm(850d48{{f=;t|SoiQKg_dAQ~9MVa^#06_{Hd3b{ zb_h6SWjaSAb{L;{V!l1`2u4z)Auq08^`RO~WL9e7t>^O5*%_Rq<2Z4c(b2WN_7?B! zv169+L4cG{Ctq8$Q3dq&Lx`}tuy(+d)XkO8wWmIDxUHSR;Pb>kx5W`4*ICz?TxkoZ z7zrGs(_rK?$JtF2KNqWumFRw~7kCavTz=&{?drt`T6^RO-;eObUBx?zhC{vvXzS`i zyZpvNyYw<^5bQcy;Dp*3qGS+05PR_Zi&s|LI-S&~j{7#B$@#z%?-_pU{WXqeS#ydzht^cANEvm;U}jJMrvJd+hUc9H%klbBrn}@alkiCqx=8qIx>T=E?Gx-(764 z|BIz|72nQ7r)tWHvm6ZwBYf_+IaG3Op*{L3jHY}n8;Qxtc>1=3bbZD;@mv37sol6t z8rD2Cvf(2+!D0AKK4cIqe)AQ717Ic1m&N6z$0v+%0q?J0!$PiZarRn z029deS(sRU(>!*16O_5{yc8NIL3g11HpS?4gmy6r4Vl7I36LdE%cSTAZ(;=F^aTqF ztaJ~!!%ZAKq~^oO0>^}G+@5@f*gxR9r*4r9n!f{gACL-6&e3RR1@x@Oqwg--qMj9DTosrb$Kn%^gJ4idi&PMmhiTTD!CGFKSd+qX7j2{oh zSyTk?@ZN@$h9c8`4Swo$(?a5Tzj{FaHQzcn`Ie6P)S0LSaDTQ~PZU<<=Fp zr}C1XYa}`FPNmXhR1V&d>x_Mmqe$E-_3UxioF?t1D=Y2lJO@3dV=S6MYo1g~0}+G9 z1Hq+Nmg$@(ZSB-vJMqL`JN5{>yN6eM`1(S-evY*eV0JvC zoGL)f&LD+}Z+bv>GOzv0VteE33+eoGU&J|j(3v;lF8ca~-3{J1StRdy0v>sl-f+@T zHm1XkVku({K>5;jo-$6J0hP9-Ovbz;Kol*l|6DrDH|3K^K;4i^+a*g2-N@=RF^q<8 zNf|nRdbvbYj~3_7zeil-DaFc*UQu+0x(l#Y>5PsNQB2&RDUy-p&Q#Cx9mM#Cdm0fA z+;+DnUm-a9qZ#768|=>{cUS0vw2vgp`d7#?(-HX4E8q6$ch z=Jl=9?e@t-tZjUU!wui%iJ0^obQJ#3t}GGu5W0ZJ)zex5N`X$#J;9byLseOIv4k18e^+2 zMkOg^%1Y`OG|~l+h^W)!)*a(+ZqOAEwC){?Q1eik?KFWIF6{6^*Fh<#j#YY5RS;^H1jC)u1 zA1(Ry>yPf;+WN$f-)Y9e`^Ks}jy9+`+fGr_lCSS_{D-^!*@F4R!|d3>u%%L^>J?k( zK81PJ=z=;&*l7kk7Y*R=xWM1$d*3fSztD~z@o4mqR&lh2fgbTeJ=(&E@B60EDDn%@ zrH>q(k#X|KOYOhj-csH&}b9Aw<%|p2#Dan_&XBj0Sv28>7dl!x56-Hc5xq z$$jE<+GyW(ZNe8QdPYrt;ser8*tF`)y^c=@px(J5(xad{`o4}Lr4*VjBJoY$0!C?mpto3l%c_3qI%t#-ttO1nFJ@{K2aV9 z_vSuv;jPu@K-|*~!+Lq(?xPaey?o=#^E;D=cA0jXxQ@V7l8fEj%NjfsrBYqp;oI5N zDJCP@!w+(NfGudoSqalQ=#yUC=Vj-S(Znpi<7D9xAt#wKI(@39D`r?G2JnbjB+^yLdK^x7U~X#`nojvXXtA zA;SYqyYMawBaI!t;l0P`t3Ak06Sk8UKYZUi;B&7z`kW4Bck^0%?9p$whgn0=K*l)w zYoXv`6cQ-8%1%Qkps$+VoFgkc?Lpqj&|tZR_Q4~2?fFA|Dp#5r10bvE<1xXWwK6)L zUt~ux1|8q7)u|qJlVSNKJ4P)sWULXBrZetAG0KT7@OFsSh3J7796$PYB!hwXB3B56 zcV`-5_)}TLYlNUb=%Oi+Fb!~u>PZKSAiEsXCpePzxU!PHSw9I&4Zu^C44cI%J@Ki| zVcw~&YxxH#0sro3cB(Y_kSz50x=vHT@mr|E+qD(XFa{VM^m|x#!{vVUNJ2sDD1`t4O0R~UsCluE&>_oC2(c!6#;Pt(nvU>9I{YO_oGtInY2-A%U zZ`<(+RfAT}CqRu>kK)Y)_ko1e&BgTRA7jy&ZMWO(D4It>AL{djX+o}al!k@(#-!W= zO*oV4Fb5NUxqadR-V3?%M#;X-`xNi+0Vk*ypE#a2zQ9N$TX{7QT#f?#5R#qM{LOdT zGoN{l9ZR2S8*EIIdt@jC7o(h>q2f_Scp+lUKY47EyTfQk#yDoD+9ywJ@`gIwaksc< z>TepZuqC6zSlAgLC;R8k2r z^&b+$gT49FRaahNK}n0QVQyNkexnkpC;L(@yh&skhb&CJ$NOD*%rrFOUB9u@p6abY z>kD4x)zI{=2;B!K#?QDhkS1M-if85BMA8+nkzq%|sX@UNU+){E&F`n^MiY?RU3Ui^ z#$7M+j~7So+Xmd7%IKeRkyQG2c( zJ477(oxDEGsXDMDbbOCKIB9R6!+5y1;8Q+{lP{S@AT1?5N%58U$hTe8wVk#0+QOIH zbI0b|>f}{$uGsR`zjv9QVid9X1PkHN&0nERG%_}R%9B`i8gx`E?Yr%%?|Zzx{L4qu zU?UdNXw6T2fL)zZ%s6HI3|neXuWnL~+zh>(%GFm5TXyZi)p=@!Vc1zJF){IKNaDg(FbZbGWEO#0F}mtd@ADZun_@qUX*gGj zMa5}48HJa0-POn_;zC!Q0Rkg4VpWZ-C>_Zo!C-)Iy)zGX7&?>Y*Mzb%y~%Y%QtBh# zebW@{lmr947i3I^h`D7McXgE_ue_S0X|m863FK+yMqqnE*JRk zCT{DuKllW1Dt~&%^D$_(o+V?W-gAp1b;hUhb&r^^qJmg*L6q87rlL68pnY;oI@5Y- z6lTIUv^%dlL+aRJY%2!NQOK+_I@-j5Tgfl;PQ~}m@^wE9OB#|!m}?QEf0B;pxraG9 zmrkhoeTZpiBvku-iqXM)m-pxpfhf$@V$11*M; z*IpIDJR6^deV6ZS(6KH&dX$01W}D~WTgBwv49d_cDv!t5Vb%NL$G_J;c=}81R$>~9 zvWT3nie)M`qeF=4G|`}+O-tDN zlzS@|>893l!~u~ZHx`K2DJ!HEO&zx^WIYhl4?tK(4rtTLucs}VqE!!1G??XMv8Im8 zy4SZMWXgpi&3iswMsBK1itWBXD|4zq=N6&+1ksTvx(r%b$GHCD1CM}xb5PuEpWAxi z?w1nSy}k80>dS+9Sx4hzB}KX2-eFjmZtrjmhP4+A#}M^!q9G23A3KPer`GQm6MAjcf`Q$cR2a#;-iQMy}tU6odu1ZTBOnO-2gib2Lk7BPTGrfChPT{8R@EV zn|}5RPXKl-yu@6M3La_a{ES8}V#uELEtBCiyVv&)x7V4T+B*6ypL*r{8Y_p}?)pyK zew%NeU*W8ahnD#;6C({e3U$q{pL|bKBT8m&|8{%s`#)f367sbEar?5>BTs_8vv}mt zPWwEa!90cRh63O0KEs>c|Ln|R-oaR9#90)4~fZ9134V(!)3Rv`FjzR#+o`<%kCcdzWWiqCXN%D)RI zKh`CuHKUIaoaLT)V*Ll;^ggS%;K0gHG2T19C4ZM%;m+*Y~G zbd)0vZCstd(~X%4a8_*Vr$)o6rmECcDO2giefW}AL7j)8-_0RDqlduN*+DN`oYq6`?ZTl+6PQ8*4^tSWMnEC9I~RG5CQ!3``z^$iN9Y`Q5z!a^IO48+I}kcczVaLQ_KS2fXQ^{+$_q2)S_Ve# z%?YTMK}&x)@w%>UsUDy-=wccPNSvvm;53tAIZY>tx1du8KcYi!F&Z>7;?fJ@kfT4i zOWR$Sn^+2wIzSnv;aUGgkSuvMoHiAxu{-4eRzis-FTH#cH%a)IsJjl6Nd@aSRZ`lJ z39GPUyoEj9Ty5}C2MAM%ZlYH5w#WRdfCa|5gI!nzV&?v6pXRmTdHFIGLT z(D-g{)bS$eup+?R?7P9&!)N0th_tn-RRKy8=}Z~P<41AAh!@V^y>?-8vaKHaQhQ>T zH?LW=nB&#*$>rN^_svUf{?iY(d8WYHWxw}LS(zla045ab&eb>DlP^5pF1-F}z6)*> zq3f0?GE0~p#2Qmhs~A`hakR5IcX7FW`|^>t$MlzX(HH%$Y&qIkY%e@8-<~_c)H7_3 zu#@VAV>jCeADrmhSBv$?pnasG>gNuzQ(*KVrs^rIkr=ap3NK2~anV z+Hz>GwBO!_)sQKO+(viEf=;NG?l?GDDWX7Yl6O`Vbob5_k}+jXK^m>dmHO*VduN;(a&Fr z!GvS+?Y_p^b;yf+r~7r@=Du`$vAxIHJ2XBUj<4^Co@s7~w6>jqJCC$;vNN2kcJ}wD~PI(xNo8c3R5J4t97l33QhQ!(S{pt6A<9${e z?~OYcVceT?pGqC0i+qOhSLc}Is1`PwUFwfN);ynEQ^EKlu*r8KoNsmhIdw}g)pBY; z0+iH1lJIK<>@X^#C_5n9Zd>PJe4A;SMGQw6;tiJp1Q9!|E38kPVRmN@qoR?}099+~ z)FnbqxIERi*ctmcC&o9phg{=k*=aeMCr-#Iq~wvXK*wH{yNV0$*-blpYw44GE$?bu z+aq(Hx6aY%yXS7Txuc71kwtL7^*x~+i>u--5J<0{Yp0)ltDQS}vaMgD+?87mR}nZm zY%>4t3#(b{aIIp=%|wMF5Kf`Ysj8IKxvfL(y$?8u5UaDpr*-obuks}_w1%SvzPZl* z8JQm$ix9~Oboxr=Z-Jbut5Pdis?UHZ5B&;e0)=I)NTgDw7Q!B~DJ+$wNChVSP>E!v zN-&i!NkLc=hLu;aPBgq3pfq+E`?{I12)nn>W0tP-DppcXYf0TVK+|eUSi02dJm?f> z*WvscVuTlq6pgGa7d@egD@k}vl?5g^x|bpyAO~Dem!yM-1?<0Rx%#aLA4_fa9`4Zi z82vqU<@$-o;r42H-sK<5@W9;%CGcbI$2{S$=AKgHQmtyrqJnwS&U674%*wvO6w>-_ zmQT|XI*}D|##OY8s^RhUHb|_X!cDC`UFUdkXNKgDjdMq;R=eF`>+D-+TKkPxSj>KH z(ylwZA`#BYUL4AcOd5hui*Bzyd`QC=sDvNu@(m-%WH-$1w82;=GLm);sybuiNXPKI$n84L3Tpth7;btA%L~6)U!LegFLy>a7^mE3a1j~a6|Se) z{q)L~pLtLI6EBTte`g;QN?%HGhHuo0F7U&bCcKFzZYlHx@^ziee8#4RRs&YF$zSK8 zzNI7BX&AcP5EIB(f8XWa3HyjEG6JtM4R82PX$|*yscSwV)7ay=#)W zJPF9U_`uHf#e25!eK5k9y<7J%e6$^%uKlIbO=}^67z!)5^O!v4Td}y@`gh~zZhP>g zGw9T&BCWb;g}cL>T~|jY zAO;ao3on@pb@C{!mxgdEhOn^VEUnP_)7cH&ARJ{CG<37+r?e)QfIM2hy810@*V}eH zWRi|SRb_GqUx{ZJOEc)gNnFZrhDZ?ULPvO3|D+c-b)<`e0OVa>CFv|hv;AOoXLHw+ zYybW<>0JyQbQpKBus=!^4K?-2PdMz&y}fIMdWTIcgJRW5<>6^w2=9DP4V#Vp%5^@# z!?EYq#$K;X%++hSUdN60JF7={D}#VjN7ru7wQv3=Hik|iA5H>i*ujL2s$^6u`369= zOPjKG>gxH2=G!s8rQt|p(!LTXX42MF z3?@N5e{Nqj>6ztc*tUDV9pm$^^NY9I9>R9d-e&Qem+2lRV~o@qi^1ho0A3Wp($?$k z^d~;mUjI6~s}`%Pt~*gO)T!FJff1fz15mD@;t~$25M$ich=pI$cQ_G4x`fPhMdBsa zLFQX$-bXEPnsFqNQFti;(mX9gAGt#ah~m%K2qfRI3Y;H(0R|E_&h=4F9i_qP zOO}-U#1E!`o@W(V9cJEo(sY8s4S7`V5lledFiaU3PWyq8L^xZW@3W@KFdm5BoMlV(lzxz{_wX=f8bmQr9GWeh@uTzY(c`1DC|!FKF!j{Q`xeU!XD^b{j37IF{J~ARmeAmY8x6>au17{AuPCKK zz|q$Z=cvDX_=UE&dXo90!*qg6eDal%#u;``v5TuQZI*_v4$AhimonJBaiu-__?vBM z6-~0nQd0z7%dQ`>o-DfXBMYyoLl5&Z;K7+l2>wVPKS*>R_>s4sT0IjS+!D8p%hhP1 zvli6p%PSqG#2r1z^y6e^9kdb{PP}~i7`JRQqAF!73^H=WLzZhlQ>LuEO2=g}hb9Qa zmW7ZKnSeAIp@CBmFI=d)4v8n6Dm2A~FbSYe+U_TDVSNzVq7GQ^yXbK5VnSp&#IA=& zrTg%2>E87UvJ1fr4p{1E=Mn6Tka~2KNcyU~>N7~Z@0J(Wr_aAMxhD(XR)+(3AC$n) z^1WW(=;my0s)tsTORt$~Z=?&Lqc%OF&dip!U9p<**2N+d6_mCA^ZeHUz!8?8&iM;VX1|8!(<(D8$ zY_#5~(xsP;FU0CiC}_iJ)CJ61k3lZWaeo48|}eISu96ck3ZkPR8(EaggW(TWqaTP?sSk%-7q1h zM^?(*{8c=uv=IKoe!4y~gBD(wWfeA|H%;ysS<#8Ipkn;R3^<5s=Q1TUV45(gv^`wO z_J37RCnP*O-vId;x9{WP!ox#85Qkmo*+F;m2+&0RB26atiRvPrAMbZa^r;g*XjL77 zfr!q>pq+(!Zu8KhGKH1^Cw~$qZ}wDN;!_+65bnO7dGY>v$uqrMSPl6_A5{O5&b1To zLv}yP5II%%_tTXa9>B7VtUYz%gRQ6GcGn%nb>Qxo5@2L;iStr39X-cta~VB`p)piw ziFH_A!is4nsFdEko458DWw1bNgjzK^j?sWqk@-m@5-*v4E*ry4r3@@Jxf2@hjfBu0 zPLwUD{)4iqtT00PP z$x!!dbOwk_SHAVD;eT-Wxwg4NZ?n8##eU+JUt|%gW$m{nvv;Za3)4u~|iO=Lk1|N2VUO3_0?GB5_&wieZ zCUhb-k{ZsTU}%ZYrZNBF-GQdF9L)D@z5q?j5_rP44{tANBvm&-ZZF=X{`ppaIxk>D zr|6;#sFOxs#{3aJE`r0!vuj!Su2^~IZ{KCtoz$J5o{2J@NT_&~%pwBIO(qWy@7|g` z9rC;UqfQ6zJ}H685;IftbDJ(WTZw#=TSJ!)1LH+|YHN#`7opNr*=V{sLM0V_^fdNp$+rH zZWiPYtPeN{@H}f5V;71IXvi>a=w}#}9wZ!@8<`RWy>~veKo}=qFZkWz^l!VLHV!5cor0{E=iPPZCr;C|?CWTrXr-;%4$A zFN;%p16}8o@n{pBBxu_foy9f_-P`N7oFp}QsJzYlI`(o9Hd_`F_bYWFQ8|?=`nmUT zpXvnKaxLZ%^mZhlktZK?!q)a6JdF4?EkC@#x4^dS=LO$w*KhgV7JDG?qe@`z(84>s zBz1G%H>wyixN9k2ZDWMt=NtTX^x@^59B7wH1QXltJLk6A?c2Kvdqb_$qfMuNThX|z zaPcy#pz8IwwB@Q&PNM;)>GVq8D>bz^6}eD3;nXd;$C%bRb&N$hl^Y6n&Lz1b%Dci- z-MiGBfRk-~Gv$QtF%SWHeC}VDg6Kwf3tKa_K zy}%aR8yJpNN|4?xMoJbMd-Art^Im)4 zP^rc}DG~5h9H?Q)jP>FtR2_%92^#{8Yp~)-oI0RC0Qf1};X^yhIz~-8iF1{rNefXx z{3NbG3VHAnXt*Qq2#4js8X^`(8Z5mt=##V#;+UL?W}_i0U-L4Y&+DxxpTdpBsTz;&~t$IH@bo z>ii)GpxA*EdC*~esDS{?Juv_F+~WNEi@a*fCox!<<$KCl4j$N>1B*R| z56c{4w??Ni8nmO3>s0!)?``GnTq{$ivfyXEGM=l)M@LU=C3vJap})ruM}sdL~lwkvAw*J352$aK(xXD|)@)wdA_)jrwC$J#{0E zG|>S-Myee@Xc7%g<-vqQ=kPaO7o?wjm?&`N5lI?3>WU_G{8&d9*!j1~x6yOdeUU?E z{vy~U;TuWv_3}@Aj~8!#!?K04ROUdjtWhzx0}>@ zk5gD-E#c(Rf_IiP-jGuA&he2+UplnK_yjx*N&)thnTl?#^bl-=Rxc1<<%E664ZInb zc`n19j`)?MPqeLNjtg1gWj6=vD;v1Eu`?;?mgBPrR zyOU?%{KhRNfbXueNgcTRqy&Co?g#jw-qLS5bQpe%!FQq{i?r9_=#k27gXVIF z)8UmmZe5ykCe#hTWZY!0`z2mFG5wf|(tzfY99PYpMvAg`$P5ygt*Nt384ykz4WEkG zVcJuAbjdWs#zD(r5l%+9!|O*mz@9l={f~g)h}x}V^lCsLpFc8dVQGEcmC=eeAreVyg8PmJO&@- zmPl!O_2{bL?K?9jZ@r$k$_o%X3GaDjp0es?A_^FT_H_6gtXssHum=@B9U%ye0I3Mn zKTzN@8iJEp243BX9`VjTSbyX>bv;6ZlVwn>mpcULZTdwA46~T%);-(SGR_ zj=P}I+9+I9cKT$DG^~(!gpH3Ywb+IxfWiQg3S2=z)qqn>qr&TJM7VaDV)CnbCcV?$ z2{Cq&BSyAoo=OLO3Oah&1#di$Qel0^^ZJbpkgZ z(aAWvdGV*A;y+htA*C@D`8gf z>aw#bgJn|X$AM3S!k@xMBceftEORg+gGNH9<$C&&58_GRJ46kNZ}e^*eV}bFv+%vb zS1%X&HU-vias$QKs@%v=xH8I^KU^)O96#OBcg-U^2kw3;0V=}W&maD;F3@58(mdyfy8!E# z-|R4!xKnG)voA7|z@)Vkd{beSmGHdv-CK^|<^IlhZ?U!NU2LnIx%5}x5uyl5ac?n3fg z4ZS4IG}u^*ZUlIR!pG?;-(f{&%AwfN@G0Z<)rXjZVy6^SQ*&%lo3Pug-DI*v_7tG+ zV%S;Cak?x!Q`(WeYupPr9iUCAS79w4x@wFv^6L1|i2`p!AKQ>CQyL!#HAkH=u_=sv zbadgRD{_8kaK?JPf-|@(zZmJRW2|!&B8~L$Oh?euMub1Jt^+F%=4XsJIw6db1)@WJ z>yiAYWzx%LnuqX|pac!|rhtwrvNGD86d9DA3H9-mGs?=B`3w)asY4KK`!+WZ1*3jj zM@HL7dVW%GjD)woE|Tzr=rRXvs!_`Dj(V50#j_LyKs6d{I=Z~dN`;3W$n6r7$K)r4 z+f(k&e`zxL1#A3WbE<#f?xPa;+T7RnmR6Vl``v}T+w&{cQ!Mec>yaJHRF*?77W3G$ z$h6d{qg!b>qtQ5uc>lsqd-=8X=_n&3j2KIkVfb=|1!14Y>#|~w#!^o>RaFD0+D`G7 zl`0fEB`5u+Z$>BfFZqd8C?3}RTt<1EltXv`M?$|02VejPTx(#>9T$t348bt)P$pU7 zM}A`=I=h?_n=(KeX*Q0Y;`6DDFqoE_^F53$)-mWX8gI`}7^!9M*!dHursy!1nU-3a z@J)@bQVbWFsK3&5nlES!XO15AdJi{9L+WzdHhWy~{8;yW1+ z@*>$Xi?dd!IDDV?zw=u++tq7ZcZ@O|ZRE)cKaGZ;#Hp~x6NX8_c)PsA#l=8R8;<~7 zWR-76#jex+#u9@4AJy~HSi6gBYD_d5!*EET8-B%lV;KfTrTCxb!6*r$629<$g;l4m zG!kJX(P69$IW4t)}Q%czJQc?F*PX`ZC(@Q?#A-*KH!#bDCG!yi#;E&sp)y4Cm2`)oe;lS}V6zS(|#dgt1q zhmGGYM@u+x_ge|jacoSM7Jk7e+_t+vqhLn)^pyKNBUNZX{GhKom)HW6*uUr& znVq(n4H2ET@=$E6BXA;q$QN#n7{)WBh@=GWz3q8Qhe4ur5)o%D8s$?7Vr0dE@I8-d zrz0Qbm;e{O4GRNT`RMuVu*mLVhk)L*9bd}CpVnpDfK064){WF3{TgWtSE%)%aF%9w zpbEGTh<>sYrBcaZ{{=bpuhI|eP>ghPDjL(yI<*8QkAUyQ+ZD6$y>|P?-a&^kTWtTT zhBpr{{@vY$$%~7J_>3F#QHy*hI%^qJ=tH-8!HmY`lj}o=H`$@&BgwQ{r-OL?-OcvB zH<>`^8jNI4La$W!`}Z+#duD8IVlbCt=dAFrWG) zly}X|w(q+p^gz(>UIJ92xud5~pPwu({q4OatX-z17*S+u%J%{IP6Iop92PI|=J#VK zJWI-2r0YIL?pMEkGY1peX}AcUPQu31uT*^MWQ;E`je5dxAxO9zRbo?kmm}WvL+TP8 zTF!;)@ECCLei-TemDXN)cdz}{Ta$L%m!+T?>GZ2H5lRCAFoh9-SUE|g*W+pE=n@>d zJJmGDT88IvdFrg21%yeEj!}H45AEOs{Vr!pP53gOJxWZ8Pr95jG}$2$9Z0>8L4vGf zKrvxd2kSU=PLn4S!wAOIC1X(Fm*MSC&l)og+<-$8Jg{`4Jw6PnbQ)kgM{-bd3mAC- z3+8jpizN@kujY*Q=VI*8R_~ax8EsWp&r<7_NO6xeIVbX zj#7PMj4ffy*rN0DKF)S1cZ^Q0b6ybo=YRh(5SOw<&uL-(4jKX03u%;Aq_gP^3zyFF z^@<#T^E}P$Y|no(=)3e|Ssl3htOTfpsUzzTAN@O%#mP69kGPA+4#PLUYb|5-mMvv2 zFY=vO+iEA*crU}Mm84}zZ?WU&m%nk1?{w42U_@OwpN=q~%aa!MFC7TJ4Pe@t7#1eJ z`)Np&ReWK{!;45qF*kS_>aF+q3LsNr=Q&vLRXT|m-`H!fpWADft}{L0gnnAXV7OoK zX&C$r<01DrgvB5mL8p>!YaxzIdpX}%2@XZmz#UO=rQ>j>i9_aGKar)Kg?Su#aHO%j zdYJW5){FQ?hL$D!?zbS~NDzfEHmS~nj>NZ{9c4HWDEpi4VLwbB%*!x_`<)1^aOO|r z>kF8!Lpe%J-oeQ!d+bvVgpCIZ#`{X1%!6V1PyY<3C=rUy#888VA@m&hYsmJ2q!$hQWr;^5BydwtdMQDZhwCQ{Z zk4`A*QvUFy>U)jO%%e|S(01y{5eEAg+6qVPd!HSZFFQ!5(-?Jo)=zUWJ5{i$7se2s zu>P#G-t3nj5&m8KNuc}TX+kgZXs;ZYc{g4REWwnQg7&y9_LMb&DmUWrNPRR}7*@HP zmK=ebd}cEFnich~xVt9kuH@$bXF^V#IB{j}@XFubU2NODt;dWBFYP&6;I6GvqgFD# z!*mI?@~M-zIQks3Fs}$y3mpSGtB3dn`v8oEemcYCSNcxVasK)aGx%@wH(w$N)!26f%E?OzFVq{+X65 zSTa#hzQQ`b8^&oTaz`9JdCs( zMOxXz$9X4cjV_*BmW#6UVTRLoQtx>_sJaZ^x|Paec_cUcKceCW0-8aTyaTtM>B>_+ zkxoBof(14`&&veHrBm)SJnk8O2s*FO^)z@oLGUeaZ||R~mZqGch-d;J+sHN7JD#uMrIPwot`Ub$gX6E^RQ{x`Dj9=lD-&kJCuj#Cp|n=+@)Jo1 zk~Go@g7m}@KB{!gsR7~uZ}YL5tJnA1Ex$tm!!*1plfkuQ*shfsCy20}6u}sNL(0&v zP7OE)A|%n8m;eAk07*naRMSo*t^*a(vDhVz>8qn7z<=#1us5;yVG0T*_b@CYAYfC!|D(Y0ENtRh5dy-q!Lux zVQ7N#o^*2AOJSP8jDR=qDMNwNFoC2k-Gl1)wbT?cWSdTndp-rrP(trtiHT1TSVsxr zA2G!WxY?CH9j`vF-eL}v`yvrQ2qPoRj22#`p^Or`9D9|OL!7bppi@xc7(PDyHS>8$vwQi>KP%hEHK2y2z3x4amw@rAylWGnEUU zx|bv%1Jg1i%6}Z4Gs{%e)4g`$t+yIus^%x4{=nTqhw%qq#J@iQG?b04Gavk|o$d8M zyl{BsF{eaU=ro)gW)`aL@&TZmJVNZf2P{Si)U-i#ve*!Qv^}d-=6q8Ubrjo!bqYMbl)wBVjgb;DgFUL3-hR=XyWIrF@SD&{?Agm?$OVOrc%q&7vYwBpJRbTMcy``w?=fc%4@guo&dkaQ zFHe#kY`NL5C@{7-WCDv;9);MaGfhttF4EXRdw*sP%}5%e@J$GsidZtGP{fU35%KH& zQ>Um-JPY3Xz((FE87c2bn>-4RQ%3$nlR6jqm2%`c`5c6#OY%`LrdJs=q{C0pdz(BD zRMEI&E&RfylB^w`v=ej|%RB^Ww6hVydQj}9z5)A&dP&hh5990fyi(5au}=(a=s_gq z(`hIfLPog6jc^B9rIH|AX7N3p%oRe^`dGXRXC70#jgCutqajf`fyw0X_Tr|85Hg+KY`yyKf5y!Bf7!Mc=jXBP%Z$r4-bZ%!+Rny&+c=ET13hZO(QYURyO#L{Y9dxWpHAX?U zZ}~beUa6`QD{UAdrvpCw+(KJ(8*QhDI4%YeJ`X=I*Nz;XYcIXZXoCX@0a5?u*`4WT zroCIUgP`m3#?O(hk)689*w;32w8~F zdWka3cLX6P$`sA$$C3@;BhOLTjH;0I;*ze%^`dN%$khCW_beai3J#v~N__=Z88xu) zOso9ppig}qc}&@po+CnzZm!RrCe<7JDgWR?2OY*AT-pA=%6aL;%743aardWJjvo5y zO}2$BQsI}aajX#>^(|J`H}=@6w6L(=A&Nd}^ij-pV;TjU=%0d-0P>iYOnreW_Zookd0^p&@-y#^2&C>8BoH$_%@TdLQfT zNI0FNJ^##H`|e9LV4mDF6iAv%SWlUw;n@iYS7%^`w2>ymPvc9cVYpy{lSDqr2k@zH zOB7DeAU;geX%I${tU0h?k`lr?IJ$Fmv9Z7i4hl6HjoB2nmDG^sqbmccj5PcL6+GIP zp6VUz(=CoqxWz@diEscSs{)6&SSvrWCl-Rxp-IrT8aq^u;PX* z>TkJ<8}7&ol`dVLd9iq-fL_m?PZz?6ynbt|D4;6cN~XV~R4m*T)~1dgXEO)u6+X$^ z-&>A8L+H#!rYMt2WXY08%G(-cx4QM{zW>-Ob&*cFg%fhe4tLf5FY2I(2jFt+W5} z&d$ac7S~pu*rr1AfJKFwv{gEgJ>Imwfpy$t3T2g(Y+rcvR{QSRBY8if>Yk0kr>oY! zPN!i_d*(St8U!|}k&_wmkSe^&iC@VhMb9ZmsFF@Z-ODKr z@}j3SAs-y&BeLS=IjZ#@bHCFekqmIAFA4aa-uop?=MCK}sI%|o-Vy3WaEdfMoT3TW1B^iL zM($!EM20E6^~0!p14ri5SxoP1rI*xtmBLY30G3yZjI?Qwz#JX&Dd9uBY4sFTcg3YB z?~0@YA;0euSbpa5Z%uQK%db_C#sLmRlVg~yj( z8er%;V5S+g=|sY_$|1ar%a8BN&Mi3JrU6-eT zAu>KOX*2cIX*5zb8rn}AIplNbGdh=j`5ia(;h`Y3(HWd&1k86Lyv9zb#Gzp(FZ6|j zCVA{U(JOM~J;TqPb?%n($~jPEicSc`|D+TdYh$ZZDur!2JMVy zK(+z|7y~kk4JJ&4z{ZXM6F3~QL&9Ld5d`7Dn81dxC9y-;A(kM75lBK3ATb`zqUq^Y z&rI*@d;OMvuj*CRxB2(`pS<6z9?LTdX@u}}W!3#M^W-@vPv*UOPHs1^i`u?lQO~5e ziueco|&S_FO1F4 zde;p$Aum6Hy5<79UAQN=A<2-q8H3(;_g1Z*XD1Sx5r-K2ygbE zsta}1D_XXn`P3`z@Zhk$^SyV_3fG!bNpa$F+1s@NtDqcVl#+rT9clyMKRk!+qSE?Y zZ^P>b$AlYxts9={|5rjgHNeV zz&#BZ`W*RjfKg>C4|PXgrX0E@k70 zg1C8@f$+#hJh#2>Z*nycMUsyI()DxYr}9m(mnl2}2+L0uNfx;mf{#gi`yO$lyca<~ zPQNMugb41@IO`Y+UzLfmpkv_8luml{w-`Pnk}A1*mn+6e1CTUvk_Tzh2oFJs6PJPf zyL1VOoBWJP%Hxr>kYbZO!)u@#$^oPBy8S~OOp)U%Cxdp*yk}&Ubj6BYsHhl$Yt%iN zdTY^s?cQ@o@1?8uNfLiq-HE~YWv$+?JoE1yv0rDo{O_ipdGdkv(dO?w;$+n^4~~oA zy!3!lquK1Qx9KiDWFEZxFYeR8mhJK9*`UsYR`a?x1V$qmfzLnoN;}#+Xz%*KJ#C9o zi>I{aIB%Y08z5Zs;A%Ad421f#6b}hJ$XTQ`$TRFw8S0NU4n#cUJ%&q{7wxV)IVf?} zC}hS}KL*lMzril4YrNQ}A{ZLb&{)!#L+?+B)1cI1Io<=1xEWbga*Y}~Tu_V%{xE$I z81XT~p%^&U41dds9-P-cJJhUjrBH}b5d}x z2qKKb-;#Rn)924NJRpBQo( zf%-c+2T)@#uAHFFE7|I|_CIkrL8%(GehABT)L6ygl3|Pfz zRJ4@U2v@PFGUr8A-=*Qv^d-F9=RS%Id+_bl_=bPv85qjSFfMrbUCID@wIpLlJ0(Pr zB9n9iEvHh3*T68yMKXYLPe4u{J>Q}&PW7uK-6c!n4t0*X#9*5G5WN^zy%>k-3Iy`P zhB+JGee=<^zxdKW-~VHG|C*hjJjU)T9y~D^U-2sa*vwMV>2HKnN_46vV*BNh~6f{J3kUt3=dqCO-4G%rED<@`uycZzJv=dFY|*W zPETWV?b<6I|t zFV~DD4Y_$Dm|@qH)|o2xg}Y3Hf>#G<29tblm&G#KUota0L?t8Xgo91KRgYChVM&l8 z@(DLk>8nH#2l^hWIJkMz(q+} z&t3Y1?dpxc#nB<};|mRZppO>EC}YHQ$%JDq4))jDjBeRt%y!$mFdF!eJ;i$%taE5U z)}hg0tTO^Rx^}ZY_7k6L_rB{5?asH{g|g&fX1JT-F%PD_I6J5ggB>x%Q1D<63dly| zoNyEom7wC(J^#`&FX>%4H_8@U8?KF;S)g{EQH7`6dNf2v7vv#hn^7kZkIXB>e{eRS zv?1G(mrAFH9YqY`pwTd_`@jFylkH86sdDV{t9=z1p7rwBX)=|m8T!mSJg^AqbDi?Q!j_&iXV8Cm#D!YcGD zzVd!8|J(zRI?M1B$5IrB3PIIf5)<~>lCE5pzj6d<#-@e0&zXT0C?qQs1Kk-M+Q+J= zq$C}uJbsg}e0x?sudqqlz!BekK^)H4y0t%DZZ6l(Pv(>VetK}>+n@bsxBmEjAKm_^ z$0)uc!4re=6{*Hw875oz-uc+k=br!l)8)Zm-#oqZj-$~{j0Te_d<19>qp>{Vp}c|e zJEH=ewfh-qoZ38WpZ&rqj(r|`+DJbA${QR^&!ARVnSAb3PqiDI>2dGF545e*+Z-Oq z?jRmuN4{Cl@L{I$+p|oAMEf}PVI(6Fs-j2ZYj_Sg$nW`=N?F@-R8%g0N)KgvkcMjr zV52SW55dp>)o)pDl4k7()4jL03aj_ z*$R^eM|FP@{8(BTX-KNBrIT3Nz&dU5>-Phg&%?k!1mwLkMx?7GrHT0A*YB<6?Rk1{ zh7p9DEjXct<6d}fwYV~XIy=XV>nuM+7Rz$VQ8}XMg@GqL?1`tI0Qek6nBo!~gXSH`D#LH7rSOZ^>GGYS7I+iY1 zFF$8djpnp_&*s|NnS2Z9WB$G8erWm^PQQ8bXV1K2bop48zoa19p1A+N*TB&eFMd6* zOaH)RI(ysEO?tz0-`&;YI|DQ3;HPZ0U7VS;~GuHEN%KVz=$x+$^KP1L_o& z4O7+ST>kp@X1n_x_qFp6-j$}~;+i9Y7>y{3QXCo*A9y>$`aaD1!_>-cg;KR#X&nbp zXiS18mp{F6)x%kF@ya1%63Wj=2tZ^{BPwO%NF%$Xgh8mj@R~3w{&SCCX+QLjpJ?C2 zyC@%i>lNTnwE3&I+VZ)Z_>2?|cH4{(EA3vq(AJ)!tzG&Ac4V(@?B3B1@A&oY=Fa=u zEvC{A_viW2An!@GDaLn!6`TS);Ebepv(VA8=uTpR-xrzmp z&S*wg0;35jn;4W=g*muE8CBi(1nMhwMl@>~vgk0?QSNlKuIa}0D_q*R!lvb(ny$=2u7@irQlZdZmku5T&j;Q@uDIv;TK9A8rK)`3q znuCCUpQm~jJARK3c?@a#8ZJC8=&Q<#Uor^0yJ;eEYp^`~2yQ3UVSS4WEv6DVP`xderedjOr*QVIco{6;^@xCwxdtV#P%( z3K~nE&fyWYq2h)pei~Pg8?H5tQXYmaL#~dq$;*wO|NQm#fBlnZ+74%L{r2}y_{{C& zFuL9rFEOe2;vq+^OnBju_fyVa;Aj)xw|ML^cAh=jmOD)Oo!e>i!?SHVdJ9`U-F3!7 zIWI>Z9Gq=ersvz0wY%Eh#(8@48z{#a-;iGLQ7H@u({D$th0I(Jp{yF+Qoc$@ee`+& zjDoOW2K=#Nx{?rxq8${Er<)EK;Q?k5c2AzE>*|4r6B#8h387XFv#2!h-EbdECa1}2 ziq14;E`P9A!VJeFqG77CnKyA@wbj-Q7OqG5Au!H+;#%ik)HIX#t+s1tTQ`Qn9Z^GC zUjPoMXQ0ddN88?hE-Z6*0Ii3?8Kgy2ou}RdsATU)M^3d- z4teX0iymWX%zuF$KKR0N1%RC5bUZ=26`f%&vmIy#dht@%{AzU4S<ZLfBcr4G7ADn(RvlyXzA8Vqkte36O9Fo<9zK0%T)vdFyqlIC>Rmr0xYj# zyaY6kDOVv>g@_?u5J?TAiU$_ba404NLwtilH;g>Giko6segq(w!+@VO!P5OamvRq6 znVxb}R*mqfn>=Ke8@Ti}wWXoueXZ^FHd~v1cJ1`?yC3?F%^xJgL2w!Vf}Lzn+*eTz z96kN~*N$%PeIFx@hmSa*X*9jXmQbdr*Z?%AS3Kp#vpJ_0uAMn-Yg;pVbG%!?_p4uc z@^rg-c`FOyzOd&)E(;ua*fYFm(s{J8(RS{6Lpy!{8{33eZOd@bH;Xc{N6CZARo_!k z@s!aB+&Z>#cp2lu8;EG-*8G=-cD0KZ-!uk=Dn5`bFM5 zLGja~-ga@`KKi~}?bODT=&KuTd5OjDm#8Cb?7{w4JKDV1HZR=KMxTGWZ9Mz&HoqT( z^BdpHdJuR|@3;9Y%rh~qhiYqU_rJZ3?|ZO~Z#>f$&s}P>OMI8)y7uGF_L-X>XwS~? zrNP>0G1p87`e8q{qc5^eF&uN=D4#R(m~v@+t%Irum70}gRG9V28i{}2pTMfF{H;*o zSwA%u)OmSdKp5~;CY9drSpa1$ymwh&0a@~FJy}E7_Bow&i=JFkt-B;ZVj_a^6R5&XIe|@qsny>M+Z7u%t z*8S5z@upw%rAHe)e~2vYZKsSU?yIZ@4!`iydq;<_e&70H_WtSKt8ID6{Jchkd8C@6 zVt*8K#cNDat?ll$$tFJ2R99b|w3k1}E~>-znlgxTqX9A!@uBvzH#u70YFl^R-FEMN zV_QFaIw|=Gj>?6{D+VHuXoWQ#rDUpsCT^XJGhF>15RFSCC6WiUKc#_@D$Wp2qanSN z4zdC$rNg;e0AFHZ`=@^5x%TXfd%Ph$ZWq?4?c3isX%{a(je>f%EpG6Z_X|wjU7NQV zyUK1Ju=V)V8#t$HGoy?3vrn|eN8i!f+uzpK_OK|IFJTyNw8hJO<&a+V<{f9-@_`Sx z@rBcEbouAn{MlC+h0w#M@gCk_RCE2k?a}@BwgW0=L66(|D`9K;zyhi&O8Ht}08rZX zrZfdLPP(;ky;|_mpn-r@ZCH;gwT!_GdF>cf zN5HU0qUd8J@EuX;^UGV#)ZKhjD@2Mih39-&t<(v3))sW)Zfvc5@4{V0d{ z#&t=rVBi7t@`)wB$alr-k55>C2pc#a z4_AM9BIETZQ!h=4fZ;Js1zn{8#>({Vp;I1~} z)E>?O<}Jf3ZT8I7HotnKZE*(H`uX>@(S09b^zd>Uz4+NSd-l~fWu$TRD(e_G7VX)q zZ*8Bv`Jr}WzKhhp9O!7m9{Fu$9HncWSWYhMK%-%RJAhKgchTHhP=lm4@Vc)`R;g!H zA}x$kzv>uV!r;o+a10cIoPi~8!;zS;NYk+NG`!FnU+tii zPiShL#GG_xL=0D*3tkyl!G#u2T-a(!_$-ktw1lXo8e ziMM^=^d*vB;|8NK0J(oUB}V;rzuFzjiMZk6${p|l*TpX-XE0X}n0*>Oai=IU z?+tAK)x-A0=dZR$K6|}Gt=;ng3)Sl{FjdD$f$CT?x>)jE zjydZR2e(+uS$_i)O}lOF=YF#7Jn$lCu)l$~z%RCTo^!O%bHXn#LypGmM7wsW&0o4q zyWDGAn-8?no&SCt-L=`)Zhp2cpZ^>NW53O=&f4KsrkfaTT)KKk`{b+N(q38IO*x$3 zM;TN2D!MUB;lK>s(V+1D;g}8+h99$3?r~bt7V) zGx9gD!qih1p9$eqDm0xeylNXT1SEN^xKmE&&#rSZn%GV^J`3b!sKA5AK)=ev>|~Jt z+e(jUaPvs1-2KxBpph92fWACU(@>aZTmzG|?yvAVUzKMWAvt#u!kaKOsRsG-XPqQ2 z9xl&(1(R3O_z9n+4?l$@TpDEvfqcEBkk0(@R|bQ4mnsR;Wpvcb7Fy{w0G(w8ZbzgU zvxc#u!MF(?JEqD=-o{x8Y=D32WXVWvw8c)U#n$2z>vt^v*xP@@`acQpV?P=V4aML~ zdg8u{Yv4zIWI6eU`=0#IC$q!v9xo0rPWP_yz6IygGJ(E8kq&P)FX*~C+F0Xz8sk%C zIDEiwzOdV_J-y8oi&H#2Ys|;58W9bn9%B_9jg1~K3nMHBpJzwW#kRptqsi%0ZH=`L z6_<)CFB;Ke(rAe5tElvSlJ(`E#vAUpAJLT}0Hl#fPYi;?Thn&!@?Lx4nQQHZmu~Vs z5Eom=SxCLLcgPa<&GwP^F53AwlHbOqwzmI#PWN42LP5Sl9k3oT<2aSW`5kSseNS6| z@#(fX{#YB`afXEki~wh_ud&!aI!BzTr=@SuuXCKpK5HU-7!4M>Cl`KmYxiDk~mN#{lc$M{RXGQhi6ro zjI)lS5aFbc18%zc%DVEg>?Y#y#jrdv7=nZiG6%<+E8GO+89MN|N9!1iHICyMvAwtq zMy01H;Wr=`5AO)mSIhM+Uf}9q!Z3pO&NAA#u-`@+jt%DaITvd0 zg^jlV>}H$aU^GEh)vO4LiHELKfsG)KJUv-A>B4WsAwU~!P2D}ilov0^og%!sm9v=E z(k)D@igL4os^oN~))Ihee?;+YOiqnWdCBfz@2K6py4S8;y1`B_J{pD5Skk)Ol`{eH zh@DTjuH9%4oIPwGde4NB49?=_ zt2W}$#>K6Tl-1GFm^t~iQy4?G7xT^8w%}U~qph25e2X^{Ij3uK?jxizZ-4l?w%q?Y z%Hq5C@L;Faf_$bgu;Jj!xb0sZx6i)%q4xOVebxJ?(DvSk>7Zeh5Q9N|ST8DD>n27* zqe0lXAJmDFpsJEiU6nzRrW+Cv%8@c0TrXL%@HWu z7>dmecF@sVaBU#(Qjl3>B46c$#mbezg8%Ba<)$H^X#Vkxh~E`qWDy0Bm+88Cd)}V9 z&y@U-vC~BELhEVOqtDufBMF7~ zw)fm-n_Xw3l+8lE-tBodDvl@*Yt*TRKc`tkvEN{tR%785)XOOIJ&Xn0iD$QVSiEI)G1_h8mw%Q8eT*c^ zbxEFJ!yjddAsRFFGGTPe7w|G} znwQ&GD^7WkH|JY<@2ChBygP|DkfGjvsQT477%w2OHThUd2O z1Li%(iLdf5r;>|`t8~DFvRV-kkRNDCGymwnV&21D$vKULOd@(;MbV%|cZD}U#JN3j z@>=ylI=+PA58j}}MJ74<4={Pk#up&vBZ7ueH{d4He^M^PG7mx}D9)gy6BdF0R1tVp zuf}^hBoACVQH9fLGKv>p7|tIqpJMqM){EbfM&47OhbO@4Q&K?^aaGSoG$cjaxs=gh zSY)%^WNoAE&YwE{hW3Zv{+lNM5X{%OM^m0*!}rKsXmadFWwq8s$V(NGA_GMtA6GQDP=z3p7Y0AcLL%NKe3otGI|_h0g^#rzc%Ec3k!-ieqUas0{A9oW7LDE`m2 zO%`q!yXP2rynsfAhW*I4$-HT7FOKhatofD{J5AP3lQ6o)Sn7Q=a-`e;3}wE~iM|-e zBQDPKTCh&!OxvDwCf4m#C_+XUTBLNTHOx~lA{>wJoRW@a-ZvuFzG5W z{!{Mw)U|aWC-JzEk56o^Bk>UIqyaLmzJv(LyBBZH@ z{~+6g{wOc&-8tW9`|a%3a_Q=C*CsyPDz7}#ezfKcYMS0UUu)v2;u-RU*F%mTxq<-$&gkBS#xcMjO7vpwW=IhQsbnaENPYyavNSMwKQHZjTBt zi^TM(W`~Rh7N+xW%7<$T3k?TvXipFM zcF6QJ#^Ua_y!w3GI&()`yZ9zf?tO{xq_g8`%CRBrtfFGz!&DuOaE#(#^J7mKkH)UD z$?5x^`4W3Um^dd~-x2wMkPaK@*&;cq4QY1=brCalG;vj9C|k3D&z4ay)C4dkgW z`5CZ&2ZP`(795R))l!ZZ*hjSI}Kl;Je!UhXBZ5> zS6;iMK(da)gCD8oAyfD(fzD{2k)EeblSTB=gy(#6`sjau=tEn7^z6fg#p= zd-(Zh-ZPos{I8eOTfc2Qnyt?cn5LRB89Hagp>{!~CEG)axw11s*%uGlyN8(B8)Chrd|fP$Uf2d zp3B4(m2XVX@c9r(+h(WgnLF0nnLBqF9k4J>?|Qk-flwRQ;f%6>^`I>-qolbO$Tq*l zHrXSUxd$trWyyjgPhP#uyC~<{?t6a&_}AOP_0P7WeO~B0e7UVnF*@ulV}}6zFd8WS zHOlLv_4v$gJN4ixdz>mmM9MwAI)R>%7u%O>I*z(hi8xiD(b(avkagBLsAflK z`=*^X=mq7FGrpj4#3gNw z$_ig~lK;S~E@L!cCci;Mgl_@Ef*(L}#&xMq^YM0|V4d<(!TWd#iv;lv zQ*#4@?wi+2h9NCvm6^&|ey{1xlccRGLfVL#9b>Ek82DzrhU6HG&`?i4HsjUfBg^^WN9cFc!$!LfBNE^}rVpG!Zw$>@dCfM;{^ z^WBMPC>uT(qPmM^!ON1%3C7H~ysvW<{pF4Jb2mQNo*8kZ2sP*qCf}Z>@E9I;^fH=w z^Zi@xA|nbL?Tk9{a7pWEwNOsoRU8=Md>9B^JDe(v7z(Qzwq>lT*RBDSP1;-##P9b< zg-254*B{(~6N51TyRTx?nXh>mOL!m>CQz639??pWV9fLmjn)l*5<+$Rhw|hRijdY` z(FZ|%;ao8o0b<6P!f~#D=xtku$b5{yWCLvp((%DjOr0`Sjzoav?QelRt2(P8qYlCZ zb1%fR8d5%hK@Ywo5^0LLI}%MC@MK_EH~0ku4?^EHnli%R@uUsVs>B^=`u&i07?q@K znHB@hrq}t#fE$O0%Y)me*jm3c{rJU)*8fD$UN$H0^zA&e<&8&_L<^5)mHXO>^XSO#$z zR`?>~TQL+5zLA$CnRYsE1YA&eSCrMI0*d$$8W$cOJuSu_EfZtmT8Ku$>gKZS7C*XN z{M=sdNy7XfyXh7|7jM8Pp68&a4G6(EqT+#tD%>8pPJH8W-K&%WnUwSaMSpb>D26Mg zT_e(>JwDGrO|RNVi146j#f!*8KpB!);v6r6Nh>G6F0a0I zu}X(m)w2-lA$gjw)HQ__TxrNWBaEaGmYFa;nOYL$Ngm+1pXBiKbS8Me$X!yIE+aAc zq9-5;JovL%;Xzv1;R_2I{QP2PMon~Zwl-O_+gdP@dg4xMz#2IA3_bq%a`XP>qu(~3 z9efvE=U+Ejn{CaHSmQw3kC=aF6w*f>KCn(%@fgt%ZCtX+)6b%-^Zf4KaOYEcT=r&E zV`;%rXVBP!dn@)a=4n;A<$D6y`xl)o*I(M-ES*Xjz! z;u;J2H<8ojm>KIOa}+oJe7eK;D>!Xd!@;Oyeu{IT?tLqEha(lzh!6z@fAL+PX&Vnd z*7p9{wYLAn7Wj^uZnRC_>fYt!Qk!?*%P@jBy|4TnhT?$pqZnCWR~?berW@qNC?=Ov zMkx6xXk=lX#>M+g%h@0)g429+zKX~%=Sj`mE!Npzy7^FhVeua7Z@2xbcWkzME>H&W zty=46Fc6uhqUbRcRAH8$@MA2Dn+DNJ@}67q$LSp;UrQ3 zU`1m&3Q0s|tf;;V{(<=eew_G2*Z^_w_#ofFYd}+}1|L9pb_BOrhb9$StX;sDPNwA( zG_PKkgh{cNT_X(ARE%^!GCELmN=Qa!;D}Tv@p=;BULT}Ikwa26`bHA%tUAEVd%5eK zX=o{%Ws1Sj@5(%l3F@Q|bx69nsSkbfkljE(p-mh-L8s+1P}bTaPv_qS0oDbOQbOs6 z9|G3iz~5@EaE-_&8??tzocx^Bz|T_ye}@Hy-}30A-@IJh{QWFx|E9@!ac1E=9CP-j zE*W*qr)wj2_hi)JbBK2Fu-o+FY*1>YJfQY=fcxOv!=`bT0nvvr7hxaZq=9G)($HXd zc#95u!AOFUMhpgq!-aL;wBzV;rkrkID6aA%+zpHbhGThz5kp}wmTco)uu{P$i8MmPoqOX`?d&&PVf4VRrWc=J;hTdRnR@eu z#O2yW^5Ccyj{v!{*QP97Pp@Dw*fu+JSCoc{QA!MkJESxi)Jb+rq4k+Gi$cyJe-zmU zYLy*PSJUOxZ!fi5Y#F|}cw2k$;csad`6halgmuyJ^)(W!5rtuxl%c>ta38FC-`5yM z8tMl}IyJ3WGT8bFnG~liJrF$M$6?VgqHx8igHY)Nyl+Vo0jM}~$2X4(m{(618f7aU zf65jP{YNy)5OhJtapFXOAEUP(c-q2mzzZ@z!ejNMlV6-l(J3g>hiM<=chnID3EX@P zrvoFM!~jbSej}{qB<#Nl{OFk&dg3*55N1lCgz)+ABniY$0SG&NDQr9P9ON0&l~V*= z_r+PqVBC6o(GEGFz_-1_D`fyJJY=8@I(_5(_e)5%(}IhYe2mJ6x}xiL;!bMd=dXe9 z$qKI$oReg)ffCU}x2ccR7}G7AMM5#8BA#;}MG@ zu(6G7)oHOz$n((JX!3CS;HnH&I57}}>B;aSflp{X!A^zof*~LEU}_0GrnML<@TN8E z6t|e)LcuPb;zD6Nn#jfBf%AoLnD1g}F1DMjUGOf*V_Og1`GvJt-}Ij8%j0`_+uhj= z>=i$-R3_WgtGtMJ`qB$+a@T{b(VRtgy8Sr6$TzxinKh9ond-XLrq`G=X6DPa8PDg^ zIJ|b6NabC1U{Af)ZIVmMRU-ln4OQcHfb0iYm-+Z?d)veBVIiN>a9Ln?%cKof3FV{4 zbpJsadnBFt^rm29uErzS^34mSq$}kS%%0uM`W|#r*_cL+vB98&N zf!5_4;(=MM=4nKFo~atU)VmE*!Pv%uO zL0iFy4R{F>u|85aRUiJEMGUaA5w194aAOdFCB+0Ue?$b|bjza%N-&gLc%B7h&+`Va z6N6#howzSs0~>F9_s2o{_{}Gt_%rJV`yZtr^Bt@>f79Cd@BwD;QP_L*t~qjKgjVOs z%u#i#dHQ@3qfPol?9;BR#^E!Y1`rT2pHQZNvSoMZcJTzKF`xil_fWesi&I@_dC!@= zjbe6J64N)lVLl&YC^pZxo3qUu2aAm#J=&W5z?t{I{h#eVc;^0V|L62?x+G}BFuU6Z zgT@2k=Kh&>l2}ytVxdQ&9&QRZRINxw~#NvOtcECTMll zTONGilsvtAL{1twR1!MG##!q(E^Hq$B-mqg^u~97u${Z>PLu)%LNfj42XY*RDesv2 zjB+7NATX;*1s7+1r{RMOOhfHNg6Lv69R>!n`m2okghF00u%bK!ik&GmwMoYV0zt=x zKE2N{NJ{V`PK-stN-Vhbe4^qACwKfVPW;MA>5_Z+M@;#JfS;bd1se=rC7IS4gDigP z3#p2Sdco~O<5 z#9&xyC+-(q1E=2nW(T}~!)x!+r{6ePT=^Cj#lM|5+P;PNz#p7!`oWDuHWV@H;OLQ& z8a|8VPF=Ajh^H`T7*z|Yd;{aDWSVK!^He+Pc=*%MNmljwP;<;Cu-dZfwpAA0Nga>I ze6VP`+-m!?jf3fO=hM^i&ObUlwey2-{|yg+qL{G`{+XkBuqsuRGqAxH09bRWD=@vW~SY;FugH?vX>82dB z65k@ID7nx`T^#`I5(S6ZJ>o~aIJtf1-gfu>Z(-_<4==Gkqp@(RWK~_@sq7ixrCu_l zlGaZ>Awx}pC{s)#5avLcHTBxo88US72iAMsbd{L1LCJk zopATN;dxBaS{M-Y6gWPzKA;ATb>F9##cvW(H)>E?pfyjld-X4_*eo;S%aPnYYT zWas!lx-r@Ki5D+^?We!-8+i@;i`?w`a-Sne_+l-sM>&#hZw=;6!?NKVjyBpCo_>G3 z>*`bO+{KU4=&tcz32P9%kaxuSP`-2F#xV^|?Sj$(sodlxjC6eCw$=2~@$&|8;Fi^N z%c)Z3oeX&0cmKPYi$;C2i^}5F1(!aD5<5!OflB*VQJI_O&oCESF!S@~1zr>^MdfhP z$fIpVgel&Ypezg_-LJZy6fxjdyeZy4FdNIYyRoOqQZY553SapQX7RDr44%ZDOut31fukIY>l8^C@a=bt%2BTN#iT6vZfzjLU@jU$> z_4*M`$oEJJSsOId5F>A2~}J3 zz|yj9j?tn%*=Sq8YEKWH3H|jc4V7~_CzseI{=#Cs{j<~Y_{WasyC3^sC+~j#dq0xN zD%k)0jx~@O>+VZFbVqY3C78ycR>!5W(@->EzTf@a)i<{rSI)OPcYlJ^m-r$dYayIh zwPadsHLZh|@vC_;5VW>395tFqBMj5D!SIjbN&_t=uIco>ijAFf7$=TXXaBnfLSx}U za#_QOg0^~qcO|Cg&pf|c7WuP7=(Y~1k%`8o7Yw#h2I_+tXyo$Gs@7LOSS$Glrr#l@ zfV?XmV3ILqiE=_deG#-jp#>084Des zGs}&=#m}B!KRmpPr|wO(;5ROo^V96K+NO1EE$4Gy{X+$+ad~D5Pshvg(PGKO%XrKJ z$=XY!^~vWJN0aAfo2Op7_(KofY~P!{;E=_a^%rZ)`FOIn7^~oI9A=cC-ssX&3YJEO z#<+HPzU{sGk@kk2kG1XfC)y!|Gp3JRACT5|sd`e0)Rlf=HM&S-Ms-zVB=4TDjZj+8 zk=o__^^Hx|V|@2Rg{9(jWTD@cO$Z@Y(Rg)zfRAATV43)@3)!N)dtRm~lr#p@6U8?+ z&={}^ly7j!OQ=Y=x;wr1OFUqc2^4Pe3$FMEaMtI*zjzP_mUMooy~tYzx8zj0SGJ~h z*Yi=K);%g7;#Hrc>U6lTxhLRON5ndTmsT1A`43e)$TqA6m|@9QN;9QOCLygv5yVpv z)2Q&!5G>bYR*ZN6Mf2Bt--EXk06DUV6TcG-G^s;jCdg8c0(igd{l_TxiWW_mtc_p@ zi*+YV!IeWS8R_GU6He4{(oNu~5WN$3QUm`cYGCv*dsxeeXh=T&^X6&m{^R}8{T?3G zuTW=Z=osycmIs^*c$9CP+hAyEHny~jp>af;0kb6;=3Csi##`+RSHG@ZoSbfVZhs2B zz_NeTv z4b>%|fc>qozbhqHoRS92+a3HN?@$#_f5pD-ClXlDDk?%<15l2@hI%%f^T)W+)~o0> z{ltCKaNv}|>Q1r2IbL_B$xC3QHJmtp4<jj|fGQ&~1{cL>i-6Kk@` zxmf377-B3CVDLc;ZGAH~I=CZ!r83D^*;SohZ*2;jm5~L93r*@su5?wUNA}=^_+o5{a+E zgfemgOm&>NlN$Jyt$~%XKn%kxWAQ~|4Tj^3z`vmAK)ZXoP1iSa@m&adOsu7|rcsNw zaI%+K4ZheyN1V&mQ2sqY@cR7z_KC~i+FrhPUpvJKvl}+oU!Y}EDH0kA1alE+j9m1g@5_!a|@F3qHZ{=NSLGI!dUFE7mmrk1DlAT<6 z9�akN0{Q=rq6so#B>bCs#@r9?1{i`ie_=3%@fPyh;zx z{2)v5mWgEu50Y|E27aUjPuYQG$}C~;JY@)MNCP0yOJ`c@Mc=&erS+=h@>e*a3QWGk zv|o52NA7ezw8cvvLmrZLJ|`oLs>hSxU%?s}l(`}dVIJgYpdtPh6OS_o*Iyj%`KI`c zZL4X(v>+QaZ7q$&7T$BOMmHD?dZP~WD;qrEO z8k)T1(_3#8MwEfDKB^E0bv~z+$6#Q|a%6>T7Npt36hng*KE~u1KDX7oxPoo;=BW{| zK>FsZQqa#%s*Wq4pz&jFhL5Kkm*>j$h`frj07VIC082nP(LxFD6`AR{@F!ilRPKS` zr4CojO38;dX}9C0=pYjw<=n%>{Z@#s41)@u`tI}zXpse2ztp9k3+;{!tIDZI;xVMs zfu3DO;MUF~>mFd|iAT&|0|hscP~r5ns;izpBnDqb`7avD$W)$kP4ys{BaL7oM>&`P z6ztBjj7W~*B#R-R)B$BOo*(?+1|iQ?N);GsK4av$;#p`XQ&LpTiTk&(2EO8kVkqO? z?O^=q=qWzs`c3OBv9Hk@?Mb`(z=J-y=zD!4SaufV18B0+x`J&0FEf^xf4 ztTWKhyEcl=m3VlPXYee4)v)(Rix&NA9#&!)ffIh6sq~^MCmHL1!W^RD0T%9mXoWl(ev1>^Ho zSJ!dS5t*C6ETeStQ}Oy8I1QN=nQ4(*c}cP^3Q#g84O0EQrvl&N1Pt7a zI+8B5(v&VrlnS&iT)dV(SBDk9syoBI2r&REQtwUHX7EimuT1F`YLu--lz$9BnDoSz zghO1PduQ2t-6a?RFu?*sSI-=f$Xja_NvvdX5*_fQD_4etWMvoRNC@}njg%M#uMJoqv2I6u>x#H z)k9lDtNBgsObgNQCWMEP!o)M9Xh)txi~aD>T$KE7yL9xy_VTR<+qs*cYWHkB)pj=g zHpe!&8U|?KT?^r{0zF*WJJn@FO@4}!){8VwuYew?r#!>i)bOFAL9DtVm{^p%YI)9M|cU*eIh3#3@hctebv_h%8|BQYHGfH z_~hEH$xD3J^!}XshZ50wxRi~_%f{{-y-sJ?BU5=yJWAbGMGI94Ibw)1P$6)6H1%w9 zN4q}#FsH1)(9YcWRJ*Wox$W|5#mH}(m*$5DjZ_}A(kwpW&pb>DUxj0D*}SzOk%pEV z#g~>{`Ij7o2bicxo<^RqKk|!uAW0@e0x^q?u*CO(UKO$Djx9s5GbP|08q5!kWOOP@bYjIhc()$HfdhdLq7IIfTaP z1knYgM`=K7u?9Er8E^)kgOXP%>5R+M^hnsl4vI>lsj3P$_|g=zzg>n>MgwN@78#fP zgIFcBQX+$82X0dClVM;f3*n43NG*kWdkrIHlt|$Q2Ejv05<^c)7!P;=hmnW$Me1lq zgAZ!%LoXwq#xh05m#0QQbb=k-5ci(QN7USC0_JUe1y5SVO-w%0$P8y(5q&lUNv}Xo zYp!n*YGQnt|88jLyS96?B`7AC+xQ{t4#jm9~iqT+cI7&(N z8=4mQ8^e7596YA z#ne#}ElT;yd59Ihu^50AV{jByH+X}Q=rHriW!Xi;&-z8Li;VoFu>c^7GtKBi@sOft z5yeho3?4wsY1T^V^v>@8kgUtfCzN0QKLr~w+)pr8jsVKb*q zmNFqx`(WF1eOg?VLkx*Z-o^#KhC~_{$@zQ}Uk5zR3w~eUUV8OBpYQnDcIMQK>8<*T z-k>n2N4IF;hi!9r*Ts>pCBhE2WTcaSzXoHGsV!wkm>2?mqR|h)2L&ro_~3-EoQgF{ zvlQ%*lzhXWFol;Sg)zvbLDfY`M;j84JcVstW`_~fL%vmRsd`Tx{NO3GyoO5xm7b`$ z(617Si_cY~ireKjuJbc0uwIP<3tD`XZYYC{GXevah4&tJo3r=R9=qQxF@N#OFnIrY z7Q_LTO%%lK(K{tIj66-V1m-6`2rGMJjawYSBBXr6z$uwbP|nIXKH*G%2@?<({Mj83 zw?DQ2@~@k+bJ` z;+@pM>!t=a-_kzEO7>r0_Xu%Xfl46_#2T>mq$QQHu({VCc>LLWj1nN={IW0pjUm=> zco|kdc=sLcmH7wTrRy7Qk5B7O?XS@y{cgpQPv73!yBhw{0sPn_ zcH*nP*FmO)t2Kj<5-2$VMIq48k-LmWI8@vrjNM4J04^gDc@r%NKq68cS0KkD!Uty_ zA{TDYQ&@*e0cedC2&__xEI-t^E}Z-&cXc)p6p zC3*2CKfX%7kRceftF)9Jk4v6}k|*&lW68h)lO*w-tz0VJ;r?^Wy0N(JTc0XauWWc* z#Wf(ny-X8MAjSs7iO-9i$;$=x@SuR^)rEutXQB1D%xiE31)J{(FQ&_X@U{QR#{aZ^ z_u|_(PR)L3_sm+`Vv1^=Lo9tsmBDR2@5dR)2qwd`qo2(g!l)NJ1j<(O4Y{89CpGXo zssR)}C!tUN($RGKG>hLfA{$&a+B6OuiuKVOiece88*<_{bZPoEOhv?as0dhtn4lHh zwKC?~z-Vloep7pO^nrHe=9mu!E|?YbnD%=A;>IyHf1+ zlspW}6TjqRryR%FyUt|+J6ZhPcS9X+ng+qVD!)p|vjJWV|H?0;Q5}p@;yGTo9$rmhBRTzrw^tF&&lHbzkA~ocfIG?-?DLb_T8JO+s*BrabxE;+rlwO z7>#TvhA~pcI7ssvBThn%v`kQb4Ndt%PTWZiyzXk?><34m<0D0X(HG{@3T=rtP(6Ru zBDg3^6`N76!sv0l(443!B3eT3AnS!t1JE6`O2@$2VIPa zio6?{$_s-7ui+|u6ukVPIb;MEBc!a6IF-1PKcJKYeE1ATDg!cPTN$L3&$7_>cU~=Q zj90mhzvfyo?)jRYX9I!~As8X+H_6d0pQ-OtTBL{6n1_ z>3{egf9{HDj1op~{>YmTKJ;Cie`@RE;y)apnmxU>%O`(*@5s#*8RiW9dih~HyiD42 zFq7ot`$tZ>DaMIAse#vR4UF!Z{AG@={g<2TP7kG>R{N*L#4sH5Ra`;(qS~@?JLjAR zjPF!KH8dI+VW*rNVQ7~$PMb8^&F!~lG5nCv(W;~zWgHw_Z`ZH9Ov86ymO0&a%90U| zGT55qn=$`tNR&?$Dey#(qlEx9(-oBDrSOg^zRvP^FFuO2Sd-eFae|?*>?=R=9eCDO zVd8@<`aQjrbHx)*kG|t}{3?#}4)T?;EP2XMXlSzZ1iePX-Dz`o1|i%O-z!deHBS0m z!zj_(z0Lnv{xGN!1~S4^n!$yKyi%U3&y=f|E%ghs_bp^SPw9sdfr+M783#mYfRCBF zbm}OI*(uU!=RAc;mrrr8P;uqge9U3op8MX>_acq_ZkP1^zjyQRoqO~6cZ^TZ9^Kwq zuh0Iv<`(`S6I31vQ(nS4WztsVytO_u8AuA_7PpYui6yvVt`E zs4yB=ANfZQ_a@T* z^R7I^Ei}^yy!iYma}+c^c{>-wxnc@B;3klKWKf3Cd^3GCUb`;epU=JPH?IH7yWYO> zos(13&u(tgIfB3W-F6y6>y$E5uQ{8F_yogcUyA+?uv%ZaO>+`Dse#vF4V?S%`VTM0 zi$6E%J^R;G0WGx@V{f!wDWt8fer!l2n5Gjxw8n%h9)*BLSi^$Yi=3>V<%so-?4;TT zzQKy*-i;UA)mL6bab(0Jw{ppspqfMF61k8~UyzE3#-2srD8Rx^+HJBZtcW`B8-zCg zMNZx+Oiv51PFH+;-oPr&x*}r!dg8)gZ|I2&OI#SC#D$@xhH0TFU=;Ge#IiYx$bHaA zpwKoS1V<&FhSK9M?*wjB`-=eEUF9{ ze+F-$!Dv8pd-|-GCm9z`MmJq{>t!f>WP(@9Z@$Us_M?;M!zrLBc8!S{_e(aRk*=;} zIa&UH8Vu9l`fa06pL<~K4@|b_mp3MMkKpH}lOGpH=I{|OHfe0vwS9^yRM$SV7(Tm& zee!!!1Fz#6Krh_+fsH?XIGX*S4Jk^bR5F!fFOJ4Us1#^GtF~JWwl{on1_R+LHLYva zIZflF&q=xqj>5-mw_QK??zRoy+4XtbA?pcm?mhR^XV_wUgakHE^Otd5_KTx8OgRQm zdMwonwUk~ZtaKG*D|++;VU=FN)MwFY$X4Z84Y;D%l1`dyE};!FxcGG`TThesF%~HLm+3LWQR*aS z+Df0xyyIge^oL9SSyw@z$b@XV)O%;1vI!$Mal+p)FnjxIgF<*gi^UAB8})b6-< z-0pf{((ZWUxShXy+|FEN&25|K6oWBls?2v$d?HWWNe#U2Yhd(-(cb*t>3?ynP5<>| zyiP+}`UO^$h{mETL24;sd=&zbYP2ehL~Y~*XjOTyfiSv&I6N>C(eR|@Jq>Z7yE%Jv zyL|Z0b_PRnVX|naSs0)1ztWz4;*luJQYeM%g5;ni;`jUqIpRB|fH1kpTA1ZfKw-+g zy5y(wj-o4{WJzvconBbc4GQv@G9^)YR_Q&Tz+xP_+*C#sWo!yZyx}UP zUhy&z7!qh_ES9U>4Ii-S`JS)4okeFyFEXU0qkF-Og}kC@z0X{e8)lPYX|t(tBc zQg7HmV>Iag556tFUI2?8h%$3Jzz-|k+?%$~UU_@_^vl1honw>jU2BK!%myE6dg-zD z%oC5JL@*%kA~J83SrmViS}BVtpi(JOE-H{gkwicR#X-*16&iwxrc((FkzpIBERCb*1qFxT-sOa1BaR{2{#Z14BOXET>ndkWl7+hn;K2c3Cgi zF*F!ce-OqO_UGKpe7b@9-9ef@wWA>v8%3M%ntN{!_)&4};(n zcTxiky zBjb6y`@VOE_X4dQg%RZe|E@eD-=Lik3GQZxL&+HLQQ=O9iK zZ)k%qiJb)Rtp?1pC`YE-5DsX?Eob2Md_-<&U1bKGXC_m4;1wb~dpJJNlg}F4Wgq*( zxBk#of?w7>zWno>m%eZDf!V=i^N?q7#`;e^L*xll-_vlqYKaV~Bf^ew%)-tG_Qvro$kyAHXMc(w;Ei_I zeeZ>igQ%D@k)^uM=@WEj^Cmlb^}u}ZD7 z41CpE$%mihJ>(NU#XtpIhSn@gJ~_%G_1hJziZLS^DrYbhDm2UL6ycPB+mRhH#RjYd zs(>^qXZ~@e_#=6=c*&^}uXN+crPU-~;h{3B*j(l#*q85sggL*d8(-kFj^tM51J6V* z%UWAw**gIs4q$X&LazP`S*Nq#V04-dKF{J4fFM z)|YWq{Pn;4pZ>ZlPt5<|!NFoY)%ZGEb7vS#^Kzbf!qf8K`GN7@Ti;n+v3c7PovmPmPLG@(nGL_1uzDs}SP&Mhf=HWIkt7TaAwT`O z#}vSeFZlk294cLYe#P&z*WT2A@QH6=7uBTQx3%BySYNcG%a68aKl>x?>gDI#5r-4b z&@7q1CleKElq#9jREUa;yuHkK2;(~<%Ctt{3R<*-qN~wM(WnHA9-7W0_~BD&DPvE| z+&|%(x~kr~{7T`ws9dnf(B+WvU<8t1$Iq5o!j(pydyEPS?TBliEwg)E1S4p?3a-km zoP*5PaEuiEgbj6--A!GlVaktN5f>%TJ&y5!re_sjgvcs9S4S|FJoNYvZP*#_d~>e} zyPoC7fpjvw|CU>FF3TMLvLdWu83sni%hfYuSQ!_i+5Yw&Yd_W#zobWP>|FlB^bgOb zYiHele0a#o%1%Ey?Zm?!p9kGhjqywa_Ng}WKN(?s$?82xe7)5G4RCqoW6QIPCuVqqU*3EjOl8<7o`E>^aH5;W>GO&YR40hHHu1fUKV7sYV>+OoayJPMCFmFi78 z_pSm^fv5l$?Q>V&*#3`4zq$Rkcm70s6Gr2OwbSkD;!?Z(^pCciyKiY{?s|8duv(d3 zdFB#|UPWV1HCf_E@tA$WCRA7hp;LKABlX^+&=?I9kwyLrl`;qj9q}NFB)+M_il=#p zEQv)Mbn}uEe#eBS=;E77f`&&wN)$%MeQW!GBWI7gag<&~8V9nYgP|ucuaz^Hs1hO< z_s|UoN{}@qpk1u=tQAbY<{e`ZdT4})Rq;t36R)9!Dcu>Ay`FxAgvK%x4(}2u;f!1$ z82lA2q}E~b?fjBUfO5x~4uUUvj@Rc;zUjT|pQ?l}{ri*OH~Yqe{n0nxI+$^)00!cS zI>uNG^=3Whew#00^YuA3s{AJg<4f1?N$TsY2Cn~$Lr|VDfDToI1xGIc;S* zVgplK4BIwoI4XJcPg;_mS6Y<~xyMzP&IwC*O7Go7?*4S#smJbTunZ z9@(jbALXp{hI`yRhY^JuKpDjsVNevI7u0bfm3sJ3II@PW8-)^HtO%i>e2n8_oQg&G z2C=ki83)p8G=d+a;es|J3XbT$g{4Scsj=d^ywTj2fxOJ0xM4?YSuoZYfHSK|8axbB zflc9FfE0m)3607fUiePIm^|>Quk8UqAX$%?w@I2m0m!4+=|{u_Zo`aLd3r)JM>>S` zIC4j#ab4!HmOa;Py28m@;p>Zka^Jf~uN2Xj_(jyc|8eo*#eA}T#LFv39;&FZ;7aF! z415p62h=UJZnyOr&+}^S5k4^(U!s0bl3rgmaQ!EiU%z;9_E$#p$-}SmK_jO;ZcnZ@v0R+q?RC494ATiM*df z4exAg8ytk24~;DsxAFeVZec87{*dYBde zqy#9u(#r3+WJEDZ>+(<;NeAwt_5l@tz;wycF-3N~uAqwY9P-sTIOnb)RW231a^ee1 zsL}^MrFfmQ=fha!8=Ij}@ToVfx!=UIe$aWBCvWjg4D*!5u=u*_t@ry$JaACM4nVkM?|B17tdGM*TWFI&wg2(O14xbc&>KDxL({l3w1a{nF* z*?DH#P#Wj$25Q5xVcD{nR$}|1`_yMFz&c&x9J_|XUfU7YAzMCyYeXiTRBN|ZMWqtc z*!W481Vd0Hgzj$f?eEa@SepCCM@BJ6;zTFqw`u=XaGO924?bAg(*ng@W z+^XG z!hsh|I!QW}Z`wqA)Eb_VJCsOwk-N6)0+TH+evAcS zX;r8_v1b!Je}=qGmPA0R8d-Ij787Y;AP!93ji-~w4n$_rObS#sg!o_QCDXO-!yiBO zd$vE;es53w65h8y`8|^#oF3lc^O-HPKAYs#Zv)Std0J=Id9OQ!Shr3XWcmWp0M$O0zhCuER#gTLVGbFoqCFa#WEaV zY&8}~QIhuF%{iu`Xa{D|w19|I4|5yzS-J!nK>by;atj$JQW6?O6ww*yeS9B;_#3pN;m|! zy(CCydXnX1y3dYAjd+f{C!@vMe6f9I^!;Bu8ZlP*^6s(6_b-e_lQ-lT6&JWEmu0J} zOfE4}Yh-RLza-`;ckycXzE}vWcrH#?}{+n}#3wAoA2`IGM zpT_H@7e~XfXQ)$Q=e-K>+$!tGX%-j?m6+2AzO&(4MS9lo3>C0yh%f-)h(d*&MoZkR zb;*LTUu+#QFR?eKzxJgD=xl7>414O}7wzKFnR9t(LPJ^detvv%Tjm z5PUg12>j8Aa~>RhtepAfK!U1bHMZo^D`Bu<%pDRJ&<`@sp7lICdz!D}(cS@d;Y@VcVE~?4EDf_=iK< zm-8>2+gYrwG4{0#XT(8}5KGcHPFE7>ufili$~A4WMwgj(&XpXBgPO ze{aUwO-@Bn7ir|toy2VLDt&^16HZT0_yAMO2)7sLG0-$dlWo%~01qqI&-4$d`licN zWvar8-VsSKz5|rxNtkwx@|(AV`BwXDpZfat+url>_V8U#AjfuFJ9IQrhZ(wEH@#tK z;uWCbxY))yex^2lE&_@z&tO=Ku-pVxTl2rx@XQ;v5(LlBtg++H`^STEr6a z_BobvPAQzSvJCa70;-qk?4kR<#1xIBa`pX<+*5^3g_+hoNje*ye^Z;?$yZ5akUD?f7PMtLt8 zoiM}4fF&-5km~fMx4c{kG-ZOfiZw-or>@`%uH%0AfOvCtf(cqTLNeVcn7F#!uPthz z!h@dyhYukww$8P`@bJSUwo&}T+?{tmd2PP9=aLVy&!B_4;od}!UVcXyidZssdCAyO z^vMY07gpCNSYL%TaO-D|zGE_7`|XDweTs5VW3-{rT5O;ikaX|xo!?gHrs1kxR0`s0 zB;t!75N~olt|E*^lx39$PL`$8h(rL_XHb9Pt#qTs0i;~*GM8$K=^IJV zeCt~%<5S>YoG~I9wIkk2SzKk0KBw$HJAHqEzKM4^(;4jTLXnQ`0oV?dT!09j;GqUq^0h{26z5R;LNz4V-z+ikLCNfzLMlF_ELa`rb_=qEA2s+Xin z!*m~s%H}B5=c;ZCahR<6M&IV_({K8~&A(sNzp&rG`=&oOU0?gFd~l`MDNo)7>SeQ@ zmC0vTqoMOwEb?sOlXzk2dX*{+FNg3sN&%YqHfS&7H2ATKN5#OEbBYMsv$Iz#JjTL2xB!)d zCh83Bkm)b+`O{#nP3F*#+dufs2Z*n=ufOk+wvJ5WgH5wwii$A=M&pP%YDv80rRs|c z76}*r6x67B$EXCZx+s`JE}7_F+ne)YAFemhZGV8pdwF^{CyL{@Ql5+z7w$ zk=TOqA7vcbOZh2I3`=sYJffE>!BiYlcA5weM5VdG$n8!}R6V_ulas2htRE@(8H}+s z6YJ0XsTUE%@v{6`BO!ypyodg+aD-N_1y_)QNg$q1KXNC|Jtm)wD!74lwiO1ozw_ZJ z6LF%1|DX2GJ=m|Ry5r~fx{qIO-U*2z1W5o9RI~&ipejm zPN!OJtW~j{DKk~4P+GOqKO7%*>L|7q$08~SiJ)LW1QH`ZxGskuL+S*$EDA21(B_|% zU9)celE;0Wd4XQ|8?rAAzSjKQuBv`89$6^?+881K9zhgso*Tacf#Wwy1;iA;+tOel zq|%)*62Mqc*|Et5uT)!pafd?^C>ALtHxZ>;InxVAp=h{ZBcYb!#YU`wP_1Cqh)@(L zeSCUlC=VK7O^U|bT7?XGYYmE~9F&1P_wFcGmr`^K+KvMtO+H4)3(P8y-ZKg@*ObuL z?>JXBq>{a2%^fO(Mm{Rzq4IRKIK8V~Tz=(+sV5~#IYWoC3}qF&W1>hK6X7^b(^vz& z?`og}hq(k&J4vs?Ua9GfroQ7*+nuf0abNcH-We)CRZ9G#=UgN-(q)+n7+g@fsBS3a zFlK~(Cd7##1k%#^gKYU?jdn!XgBc}I21+fE6yZNz7;!6nrK0@IM1cSYAUZAO0VU;i zLUA*j-1sdZD+itGQKe|jLNP-ll6_-DBQe%$baiDoSWgj4PA?YggLwfdH!328kdBE8 zUep6+Uf?iQ$F985vHU(WawIp2dGwz;n`$GKfq2pYEk-PikoXN-PWlbcgCSv*?gW#7+GX_z*(Lg5QME;vFewo9HSOA4UDaUG?e1XG+O46KQQDu27Ui%T zGYSQTEnf|RGtjUyR=|ctakorpDz8Ty3ZgPuICuF43dIBivL%VG=%C1Ry+)*` zTvu;-fp$sNvsa#en|Z8tG}2JH9Kf1ZS=||#80Dc(g(8A{VVtZe4G2CB7b0cE{J%_< zG}b^nnR+^W2-DRS+JUr0pP7|noY=ch#zaQVyu-c9vf;ybO@q23ptboBk&%-`B z60efI#SdMI3sgM2#K9pP@enXRLd1uMc-02ecX|#o9yJVydk>M{JQlQ6Klq~lF2IFI zCW?wp`z}ZfX`QJ1@UH5vl=X}bo_WnJso5#jS)=#y>n99L0Q7oGjh7N$DECtaS zp=>sIlm=rcp)zoyNMQ1we0e_TUf8AY??xKEDw+$uso|vt!qk*|g*0G=>FF_dj_)3L zf?-f->^34ae(+G-Nrhl6Vl<)#C}r-cD5cRxi_^3!N$oe$L1Bvqi^$exG+^ipMmaH! zq5MMnub-WhO%3YV@1Aw54MnD{v$1A?(NIrUQ&gUVrw&#`@J<+_5k}Q<#V|^|GF@dJ z@WW{2I(41zW06HSpGxFNnK|((L&+s-_E^oFu<4HyY|kr z^x4u{_L>)LwqmT#Xa^MrOJ%?VFPQW9np1vsqcxBM6pKm-Cz@VhRqN zFbv2Gx;7R8Hoib+pfIQr0YOfrUA%(OE@Vg+5^NB7PNp45=9_e2Ag0UZOBPO79>7Ca zLci6e^RolQ<(moy?d=G`V`ptZCeN@VVJ^ ze?q!0X%z&bI_VC?1r>%ZiV}^`K2D=tFl z%GAl7c74NsF~jeIHM;bZb3M56d@NJa1t<;16O(zF>o}Hx6v`qs+A7NzezmoX13vrd z-P=;@311CHEj8xW)m&)AYB=oLu|pZml7p2AjamU83`0SIfYOoYk%${$z-fS-!2nb! z2QV=lg>o2{V7h{f2^uzdC{BDv27u~%mB= z$jc=~VIjX^SSjG@mU~N@LkY<%tunCNCT%uXepRmE8NT#N~`}O>tKfO-h+8b2ga$vvS-(@SNawtU3 zW$TWp{j=4NqBIyGoAQ1FNkE2TTs9%+yaE^Pgq{@Z1^%j@jEARhn7idtT3mt)hX#d0 zpb9xgPA%xmWy7W#8t_ zo8Phd^Ut|OeWnj+wPi(Xs^d-)Wg{}@Z(FWjK%E-PR(xKcfJh?Q&AEN$>s%0N$t6MaD!~Z z2?_ozs01VKE9$v+#YooF32sn^b<{A&Mnk*+G+gOWpsw`Z3)#CI6$2{HIK&l(4^I&( zOq{%eD5S7FVom`>y%Ri$ITp#B$`lju4KImAiVzPTU9Tb;Jh@bYW#SI>({TR>+s2|-oN)d`mEoFRo?HB(`NDPw4x?M zLD}j+*j^rWp(TM*0>_gCWH9#XJX()8b~3u)70>>%7{){4fnd3OF2crH=M7R7mj8t#}8x^I(*0bmL{}Kb>94HHl}bx2jc4LIdkBzCS%6O zqUutL@rrMhEK^ewf;L$Gc;C8g)1LEGe+|!MY5TIG`h2ZNT^@NU?Dt10y{~W|JTt9( zfhSFB14~@i8I#kKWnMgyXDb&b!z6w<&%*AaI{c|y%WFA!<$FU0rN#wJ+f?EE5dYEH z_s9fM3@?mf7&Uz05f8YcsxM_n`S?tQLC1KEEG!3&x)O|cgjq(=63k_hmM%2wFbt05 zV56*jHb_p7dU$h!Hz){)bUAQm{~Bz75=iym`m#&MA2Q;TbbI^9mfZTA?^?KF>8k9V z3s35AZ%)+Rv0$FIug22V+af#dyquDHlH^^IIgTVyZ`O8pTH2VHSI0u(scB02ONWIK z!1uINGA0_YGeyP1Ab5|s)QD-!F+~-VD3E{Vx&MOILrqg zap6*~jfQ9t2Zx&xk|(Vd>j9ytNA#xKZTpsHmtXhd?2j+JDO<5%D%;(ikRs6Zl&+f6 zyE8JDI^c)GN5P>yt^7<9gV)QZwI*`YzH_sCCQl6>p-{=hvQG5i6Hfx0J>AfK z=2>;69#LgRGEoQcPF*&VjYvs{1f{T1s2DC82Il*Rr|naAkG@!Q5fgT^^F-Og6odMGnyu<+|NDB0qz+L$xo-V>zxDRYPk&_cy1_4IHV($Bmvj!+ zCOXqH>z#f@-+)n3ruE>=|sWRvYK>>P`#=iVP*lItJ5Ho`O=xx)@=2 z>H4Js@!`%IRB1~8}rul-fvXqqQS4#U|q+MdwBJG~5tYa-sNQ>o6e2#h;Cabs_Iu z%CggR=Hex1aM(}=Ks4w^+fZnPQRAh#>4Y>!L`asm*Oizu#HiY^M_I_y21R~hSm7J_ zP=3zu-c zXF)1Kj6;Us5BRQ!2_6bp^c)6_?zM|vcJFUra>*G__#B3cIU=`FcJtC~OMUS#&emIL zN`rAk!Yh%V84|d6)6ClTebsMPI*oPhb}x-2P_*bw`8F0(H26$M0ZRtM#s%G~IIp+n zaz5n^KgE?!!!Ha7%AD^)6b8g;x>?B!k5P+_g~OrZz(FCQWL1o50V@oU4F)*+GcNqg zP=v7nHrr@0=Rcjj_53Z_udjPhMns=`)ukQAs)Igeq|eAc`0#jk+s>8Qmi=q;1O4&J zs7?%ecape3QKI?ckKY0D1d%rPv=K1z1CBf?KMX{|l+v}aFu#!#h5ypCn%S8$8Z52y z9xTu4Gpf+Yqoz>?BMiTKUPcSL$2P*gG?Oo?DDgu^^KSKuiuI)tsLM6?(=fP*sXyNGK5 z01z@sL_t)rQjy5Deej07O+Kdid| z%0rq-ldgJ10m|uuf(|gJDavmvaOBJJJ8(LYM992Ad)7rV@w$dwy?XDO)u)YI4eY6Q zsm!LX97#fs$5(jUK(! zrmR62l)_k~p1%};dxg>_^!m{UMZTdgAV$CjMZU=T)VFaJ!wkd-Lw+ikyg0B?9OlO{ zUY-hKM^ci!HvNvLm4^{dxU^=g~4k;?X! zV|$;Yj}N}GQE9AbG#ZPvD14TticVK?i|hKR5`&3BoE{V?bV(J)rDb9zLtt|k9()&$ zAa!ys`Q!RpUQ;jWv%%2!HexiPbZMqaF%>1`LMK~wNnnR)LP^hw0&Fn|5KJ&fbR&Z@ z8xJW(24}&zE0LQW=EX7NE98He5M41(F-*$Y7?J}Piu|TUJb3W)i>!tP-A87D7n2Z% ze+!C7XCe6DGrXEO-9NG*|LY4jR5T8JdK^aM@RCY{akxav_z5EcHHh9_KiIQz>fYw^ zT6gq~J?$3iF^XWCL!$@QEIcBRKRn>aA_2e+Kk}Kk(S7Fpdnnyq8kFBn4 zy*G8ROW%Y3NuGEAvy=C~dELFgvCV|jZA+Hjpdc5na?v^eJFnT9f0_)(*_ymMPuoe) zuUG49)y-dCsWiq>&Qfg}END$aoH80QR}`*tp&>0Rkj#wZf}x8Po~Pib3lDcyK{#JT9CfRk(8Bp(}m;t&3O0b9xoc)kPgMm^|r3>SNL9*gUQAl}sB%pLpk%^e;0hbCv-=JPF zG)!Bu!waQ?#tMVMCKeUobUvaqBObdC_4D2iO)hO!Ip0@>HuWomd%k<`gL*yo2627l z?)1~YF?vR}WO=1AvR2!8Um(SPwtB(OmM&bWxucV08TDKf|29zYa7KnhAqp`WpB^Q)*wDtDwW-x6Yu|%k#dBL-yl-uX{(Y-`~_4 z41TcMf?gul z7fVy>rR7yAzr4S=SzYqxi%zZ08`m~pty%b69{5tqT=d9ey&#k<8aLLFcnli7hXKA>qV*?S2 z3!|ZE7zO2Ve$s_zMW%rBE6ymxMnzhUJccWel^}7GMghu@>HFK=yw~dXd%I;QcF0KF ztX#Kt`klKE4rXq@aoekQ3VjZ_FV-$xwyN2iKhvz&#+KE)*&1GZTIuK06V2MI-|<^# z{mJtABjbDwJr=o@mU{IZgi!URs6#2oNJ3#*#V{NgYvSM~dGRcihaSI2ZOM1s@JJsD z2z!y>P_eUdP`R8v5f3(C$-oplsI2^jA4Y&@#PgwCo)%#U;|nL10d1FM9tBKs2a1~> zrp8&7UahmWwqWM9Z~Ci6n~S7l?Ux4QSU-WX{5dIs&urXt_VmGQ!%VAkaesPnX5Q9R zOJ6UdU?&ibT^1tOu)>WWr6CwSLjWjz;wURCIQbZhSW94ZAh>X%_bWX0TD=y+s6+`G zJ;>MldHfZJe}3^XovJvR2e@k0 z*OqfCZW$6R~n2t^C(J2=86Q=c-^6epDVB2 z+}L&ZNo!iWtIy55z31kg>Z$s2+KPT_utEp^R%%qRNa!_vKcg;XhBnZwg*LEi?Zd`` zeNY7Ro=}XT<8sxu3`4gs2)m8?m0YJF^n#8&-!0HXQk?o2>fiy5ChpYto42&OogF&{ zw@u!C_j{kV!&*=6vp_|odBgqN+aCPr*{hbkzhCLdP%!NycyWPZvC2mgE32oWyn{_~ z8H-|chta^`k~!)9P~_wTAUZbIkcy~#{)#&8G83G#yk;Uc;m16sX)5rjf%B!j7zh`w zjA1h(E>obwgCEk#hk*gCL6VE)7G^|S3Y6Nbx3AaO_FZrPz>-@*olCAX7<1`ql)TI> z2}oC@mi!35HfrYN>@QtE`oND?F4#TQS)^|l&DY_*iEd{w-tW~$w48(8nTmANwUhUS_mkqkLc{*!7uig6GXD@F5vN9jRz;Pcb z)NL4zk6e2Hb1pb-!T08m)t5=#hH@S%bjJf7a8=HxM) zG+iif`9k4eWvB$;L{Q?2lk7ypfJWRn2?hu_YCn91MVKq5#S&bJ#sO~pFfN{a(tcuI zInv4x?rhgum)-Nc9iRT^cled#ke_p|Jclvo9!bg2@hAa|$fNmwBnUn9%kiX}PH+SC zqn*>2>j2*?7B!kL@Af-hH{cZ^7!H(sp^#DhY^U`!6mVUv4-Ikj7~)YJf?+tqNcaIb z9sF>i)O}A2?86iupDYOmrJ&EcN|E)DAE9De%Ig^dDQ6fe6NDcbF+%}pVp69QCwm8H zw29%%{n6=9eBd9~{KTkp`1Z(O^c-fiWUZ9IT$aFN8G^%kKG6-s;qsUrzw!@vpY^P< zkt9UWW07#w~RbVP& zY6z2W0|kO3j5dUun1rN_M~cVrB@stl)zC+;IRxwX&{bFu>bI;?I)i+KJkfWweg0ArfX2r zHWN6e>v-V(Syu!;|ehyHw>4I zmJ*nY5}0i;N+WS}Pw1kH^7`x7J^Y?#R{hX;Wo(JgnXr4xdRn%i;xiX0W+`E>2}prk zY3l}3q3B7B-abknLx6Ucezs*n^$|PdM+j(KLpqAv0Fa+yiQ|-MYKQQh%|=dGb)A*T zvYEkT)~~j=%?w&U=;hrn99-PG;fgEH)hYYq)0GC}_#rTl>gUw+>hiJ5xDJG~#}#9s6DwBd zilX5KWEA4Cw789mEkYOw)fO5o@u zaOIVGW7np8S9hl>&&xaY7wNR)8G8HFYR&g7(*er)wLw*bka|<9exBZ);&~~)TaGPZ zN=pE}-B58E2)5;76k<9`BLmEiw^3F4dX2ltVGgpoj?c*W3}$q=Qg46PRq3^|>R^9X z?{BTt`?q%+o!jfDx1;2=_^dmx zu$N8HKtrqNa=ec?ul71xCu!-WB>Qy2ev;RJR2tb#ZM46)F`n)1R{9SuUfrI|&fePF zcTErj4bCGbI`qXb|FsQ>bt%Aqtb1B(A!6k6G;(07_Jd@k&;WD7E zvthro?lLG}l!aI>wTApI(%%oW$(XcC0-el$$lOz=#f5x>$08nd7?w~T(>t7(BK|A+ zN)*9=JlujF=#Pi@m<&G22?ampl+=?f*pgf+fl>lTFM*+gnsuPDfE`A?FalQehbz~} zQ=lA{Geud}AP8=eUt!H(*^yQdc$6_I@DuLidXK^XFwc?D8fJQ$0*Uv^{{f0Ia=e_L RQfB}F002ovPDHLkV1hfNzmfm| literal 0 HcmV?d00001 diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml index 364c8ca5b..87161e535 100644 --- a/core/resource/src/main/res/values/strings.xml +++ b/core/resource/src/main/res/values/strings.xml @@ -46,6 +46,8 @@ %1$.1f이상 %1$.1f이상 %1$.1f이상 + %1$.1f ~ %2$.1f + %1$.1f 키워드를 검색하세요 From d893423a6a1eb88dee54c52f03eb2ce8b047081e Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 1 May 2026 13:50:22 +0900 Subject: [PATCH 07/13] =?UTF-8?q?build:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=ED=83=90=EC=83=89=20=ED=83=AD=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ExploreFragment, fragment_explore.xml 삭제 - BottomNav 메뉴에서 menu_explore 항목 제거 - ic_main_explore drawable 및 main_ic_explore, explore_title, explore_detail_search_button 문자열 제거 - MainActivity FragmentType.EXPLORE 분기 제거 --- .../com/into/websoso/ui/main/MainActivity.kt | 7 - .../ui/main/explore/ExploreFragment.kt | 52 ----- app/src/main/res/layout/fragment_explore.xml | 184 ------------------ app/src/main/res/menu/menu_main_bnv.xml | 5 - .../src/main/res/drawable/ic_main_explore.xml | 13 -- core/resource/src/main/res/values/strings.xml | 3 - 6 files changed, 264 deletions(-) delete mode 100644 app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt delete mode 100644 app/src/main/res/layout/fragment_explore.xml delete mode 100644 core/resource/src/main/res/drawable/ic_main_explore.xml diff --git a/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt b/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt index 36ce24f74..2c6fad011 100644 --- a/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt +++ b/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt @@ -12,7 +12,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import com.google.firebase.messaging.FirebaseMessaging import com.into.websoso.R.id.fcv_main -import com.into.websoso.R.id.menu_explore import com.into.websoso.R.id.menu_feed import com.into.websoso.R.id.menu_home import com.into.websoso.R.id.menu_library @@ -24,12 +23,10 @@ import com.into.websoso.core.resource.R.drawable.ic_blocked_user_snack_bar import com.into.websoso.core.resource.R.string.main_back_press import com.into.websoso.databinding.ActivityMainBinding import com.into.websoso.ui.common.dialog.LoginRequestDialogFragment -import com.into.websoso.ui.main.MainActivity.FragmentType.EXPLORE import com.into.websoso.ui.main.MainActivity.FragmentType.FEED import com.into.websoso.ui.main.MainActivity.FragmentType.HOME import com.into.websoso.ui.main.MainActivity.FragmentType.LIBRARY import com.into.websoso.ui.main.MainActivity.FragmentType.MY_PAGE -import com.into.websoso.ui.main.explore.ExploreFragment import com.into.websoso.ui.main.feed.FeedFragment import com.into.websoso.ui.main.home.HomeFragment import com.into.websoso.ui.main.library.LibraryFragment @@ -44,7 +41,6 @@ class MainActivity : BaseActivity(activity_main) { private val fragmentTags = mapOf( menu_home to HomeFragment.TAG, - menu_explore to ExploreFragment.TAG, menu_feed to FeedFragment.TAG, menu_library to LibraryFragment.TAG, menu_my_page to MyPageFragment.TAG, @@ -168,7 +164,6 @@ class MainActivity : BaseActivity(activity_main) { private fun findOrCreateFragment(tag: String): Fragment = supportFragmentManager.findFragmentByTag(tag) ?: when (tag) { HomeFragment.TAG -> HomeFragment() - ExploreFragment.TAG -> ExploreFragment() FeedFragment.TAG -> FeedFragment() LibraryFragment.TAG -> LibraryFragment() MyPageFragment.TAG -> MyPageFragment() @@ -203,7 +198,6 @@ class MainActivity : BaseActivity(activity_main) { private fun handleNavigation(destination: FragmentType?) { val menuId = when (destination) { - EXPLORE -> menu_explore MY_PAGE -> menu_my_page FEED -> menu_feed LIBRARY -> menu_library @@ -257,7 +251,6 @@ class MainActivity : BaseActivity(activity_main) { ) { LIBRARY(menu_library), HOME(menu_home), - EXPLORE(menu_explore), FEED(menu_feed), MY_PAGE(menu_my_page), ; diff --git a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt b/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt deleted file mode 100644 index 3e3d7c51e..000000000 --- a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.into.websoso.ui.main.explore - -import android.os.Bundle -import android.view.View -import com.into.websoso.R -import com.into.websoso.core.common.ui.base.BaseFragment -import com.into.websoso.core.common.util.SingleEventHandler -import com.into.websoso.core.common.util.tracker.Tracker -import com.into.websoso.databinding.FragmentExploreBinding -import com.into.websoso.ui.detailExplore.DetailExploreActivity -import com.into.websoso.ui.normalExplore.NormalExploreActivity -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class ExploreFragment : BaseFragment(R.layout.fragment_explore) { - @Inject - lateinit var tracker: Tracker - - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - onNormalSearchButtonClick() - onDetailExploreButtonClick() - tracker.trackEvent("search") - } - - private fun onNormalSearchButtonClick() { - binding.clExploreNormalSearch.setOnClickListener { - tracker.trackEvent("general_search") - val intent = NormalExploreActivity.getIntent(requireContext()) - startActivity(intent) - } - } - - private fun onDetailExploreButtonClick() { - binding.clExploreDetailSearch.setOnClickListener { - singleEventHandler.throttleFirst { - startActivity(DetailExploreActivity.getIntent(requireContext())) - } - } - } - - companion object { - const val TAG = "ExploreFragment" - } -} diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml deleted file mode 100644 index 1666991d5..000000000 --- a/app/src/main/res/layout/fragment_explore.xml +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/menu_main_bnv.xml b/app/src/main/res/menu/menu_main_bnv.xml index 1b19c67c9..825d8a2db 100644 --- a/app/src/main/res/menu/menu_main_bnv.xml +++ b/app/src/main/res/menu/menu_main_bnv.xml @@ -5,11 +5,6 @@ android:icon="@drawable/ic_main_home" android:title="@string/main_ic_home" /> - - - - diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml index 87161e535..aaf29458c 100644 --- a/core/resource/src/main/res/values/strings.xml +++ b/core/resource/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ - 탐색 서재 피드 My @@ -21,11 +20,9 @@ 찾는 작품이 없다면? - 탐색하기 작품 제목, 작가를 검색하세요 뭐 읽을지 고민될 땐? 장르, 연재상태, 별점, 키워드로 작품 찾기 - 내 취향에 맞는 웹소설 찾기 소소 다른 독자들이 최근에 찾아본 웹소설이에요 From 55958033a1b322f305c65362eadcaf591900f370 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 1 May 2026 13:56:15 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=83=90=EC=83=89=20=EA=B2=B0=EA=B3=BC=EC=9D=98=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=ED=8E=B8=EC=A7=91=20BottomSheet=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 필터 버튼 클릭 시 finish() 처리로 변경하여 이전 DetailExploreActivity 상태를 그대로 사용 - DetailExploreResultDialogBottomSheet/InfoFragment/KeywordFragment 및 dialog/fragment xml 삭제 - DetailExploreResultViewModel에서 편집 관련 state 및 메서드 제거 --- .../DetailExploreResultActivity.kt | 13 +- .../DetailExploreResultDialogBottomSheet.kt | 140 -------- .../DetailExploreResultInfoFragment.kt | 173 ---------- .../DetailExploreResultKeywordFragment.kt | 299 ------------------ .../DetailExploreResultViewModel.kt | 253 +-------------- .../model/DetailExploreResultUiState.kt | 6 - .../main/res/layout/dialog_detail_explore.xml | 97 ------ .../fragment_detail_explore_result_info.xml | 182 ----------- ...fragment_detail_explore_result_keyword.xml | 255 --------------- 9 files changed, 13 insertions(+), 1405 deletions(-) delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultDialogBottomSheet.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt delete mode 100644 app/src/main/res/layout/dialog_detail_explore.xml delete mode 100644 app/src/main/res/layout/fragment_detail_explore_result_info.xml delete mode 100644 app/src/main/res/layout/fragment_detail_explore_result_keyword.xml diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt index be6eeaf25..bf306b9d8 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt @@ -145,16 +145,7 @@ class DetailExploreResultActivity : BaseActivity(R.layout.dialog_detail_explore) { - private val detailExploreResultInfoFragment: DetailExploreResultInfoFragment by lazy { DetailExploreResultInfoFragment() } - private val detailExploreResultKeywordFragment: DetailExploreResultKeywordFragment by lazy { DetailExploreResultKeywordFragment() } - private val detailExploreResultViewModel: DetailExploreResultViewModel by activityViewModels() - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - setupBottomSheet() - initDetailExploreFragment() - onReplaceFragmentButtonClick() - onBottomSheetExitButtonClick() - setupObserver() - } - - private fun setupBottomSheet() { - (dialog as BottomSheetDialog).apply { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.isDraggable = false - behavior.isHideable = false - setCancelable(false) - } - } - - private fun initDetailExploreFragment() { - childFragmentManager.commit { - add( - R.id.fcv_detail_explore, - detailExploreResultInfoFragment, - ) - } - } - - private fun onReplaceFragmentButtonClick() { - binding.tvDetailExploreInfoButton.setOnClickListener { - singleEventHandler.throttleFirst { - switchFragment(INFO) - } - } - - binding.tvDetailExploreKeywordButton.setOnClickListener { - singleEventHandler.throttleFirst { - switchFragment(KEYWORD) - } - } - } - - private fun switchFragment(selectedFragmentTitle: SelectedFragmentTitle) { - val fragmentToShow = when (selectedFragmentTitle) { - INFO -> detailExploreResultInfoFragment - KEYWORD -> { - if (childFragmentManager.findFragmentById(R.id.fcv_detail_explore) !is DetailExploreResultKeywordFragment) { - childFragmentManager.commit { - add(R.id.fcv_detail_explore, detailExploreResultKeywordFragment) - } - } - detailExploreResultKeywordFragment - } - } - - val fragmentToHide = when (selectedFragmentTitle) { - INFO -> detailExploreResultKeywordFragment - KEYWORD -> detailExploreResultInfoFragment - } - - childFragmentManager.commit { - setReorderingAllowed(true) - show(fragmentToShow) - hide(fragmentToHide) - } - - updateButtonColors(selectedFragmentTitle) - } - - private fun updateButtonColors(selectedFragmentTitle: SelectedFragmentTitle) { - val selectedColor = requireContext().getColor(R.color.primary_100_6A5DFD) - val defaultColor = requireContext().getColor(R.color.gray_200_949399) - - when (selectedFragmentTitle) { - INFO -> { - binding.apply { - tvDetailExploreInfoButton.setTextColor(selectedColor) - tvDetailExploreKeywordButton.setTextColor(defaultColor) - viewDetailExploreSelectedInfoTab.isVisible = true - viewDetailExploreSelectedKeywordTab.isVisible = false - } - } - - KEYWORD -> { - binding.apply { - tvDetailExploreKeywordButton.setTextColor(selectedColor) - tvDetailExploreInfoButton.setTextColor(defaultColor) - viewDetailExploreSelectedInfoTab.isVisible = false - viewDetailExploreSelectedKeywordTab.isVisible = true - } - } - } - } - - private fun onBottomSheetExitButtonClick() { - binding.ivDetailExploreExitButton.setOnClickListener { - dismiss() - } - } - - private fun setupObserver() { - detailExploreResultViewModel.isInfoChipSelected.observe(viewLifecycleOwner) { isVisible -> - binding.ivDetailExploreInfoActiveDot.isVisible = isVisible - } - - detailExploreResultViewModel.isKeywordChipSelected.observe(viewLifecycleOwner) { isVisible -> - binding.ivDetailExploreKeywordActiveDot.isVisible = isVisible - } - } - - companion object { - fun newInstance(): DetailExploreResultDialogBottomSheet = DetailExploreResultDialogBottomSheet() - } -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt deleted file mode 100644 index 6d6b3ea33..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.into.websoso.ui.detailExploreResult - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.activityViewModels -import com.google.android.material.chip.Chip -import com.into.websoso.R -import com.into.websoso.core.common.ui.base.BaseFragment -import com.into.websoso.core.common.ui.custom.WebsosoChip -import com.into.websoso.core.common.util.SingleEventHandler -import com.into.websoso.core.common.util.toFloatPxFromDp -import com.into.websoso.databinding.FragmentDetailExploreResultInfoBinding -import com.into.websoso.ui.detailExplore.info.model.Genre -import com.into.websoso.ui.detailExplore.info.model.Rating -import com.into.websoso.ui.detailExplore.info.model.SeriesStatus -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class DetailExploreResultInfoFragment : - BaseFragment(R.layout.fragment_detail_explore_result_info) { - private val detailExploreResultViewModel: DetailExploreResultViewModel by activityViewModels() - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - bindViewModel() - onResetButtonClick() - onDetailSearchNovelButtonClick() - setupGenreChips() - setupSeriesStatusChips() - setupRatingChips() - setupObserver() - } - - private fun bindViewModel() { - binding.detailExploreResultViewModel = detailExploreResultViewModel - binding.lifecycleOwner = this - } - - private fun onResetButtonClick() { - binding.clDetailExploreInfoResetButton.setOnClickListener { - detailExploreResultViewModel.updateSelectedInfoValueClear() - } - } - - private fun onDetailSearchNovelButtonClick() { - binding.tvDetailExploreSearchButton.setOnClickListener { - singleEventHandler.throttleFirst { - detailExploreResultViewModel.updateSearchResult(isSearchButtonClick = true) - - (parentFragment as? DetailExploreResultDialogBottomSheet)?.dismiss() - - detailExploreResultViewModel.updateIsBottomSheetOpen(false) - } - } - } - - private fun setupGenreChips() { - val genres = Genre.entries - genres.forEach { genre -> - WebsosoChip(requireContext()) - .apply { - setWebsosoChipText(genre.titleKr) - setWebsosoChipTextAppearance(R.style.body2) - setWebsosoChipTextColor(R.color.bg_detail_explore_chip_text_selector) - setWebsosoChipStrokeColor(R.color.bg_detail_explore_chip_stroke_selector) - setWebsosoChipBackgroundColor(R.color.bg_detail_explore_chip_background_selector) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnWebsosoChipClick { detailExploreResultViewModel.updateSelectedGenres(genre) } - }.also { websosoChip -> binding.wcgDetailExploreInfoGenre.addChip(websosoChip) } - } - } - - private fun setupSeriesStatusChips() { - val seriesStatusChips = listOf( - binding.chipDetailExploreInfoStatusInSeries, - binding.chipDetailExploreInfoStatusComplete, - ) - - seriesStatusChips.forEach { chip -> - setupChipCheckListener(chip) { isChecked -> - when (isChecked) { - true -> { - seriesStatusChips.filter { it != chip }.forEach { it.isChecked = false } - val status = SeriesStatus.from(chip.text.toString()) - detailExploreResultViewModel.updateSelectedSeriesStatus(status) - } - - false -> { - detailExploreResultViewModel.updateSelectedSeriesStatus(null) - } - } - } - } - } - - private fun setupRatingChips() { - val ratingChips = listOf( - binding.chipDetailExploreInfoRatingLowest, - binding.chipDetailExploreInfoRatingLower, - binding.chipDetailExploreInfoRatingHigher, - binding.chipDetailExploreInfoRatingHighest, - ) - - ratingChips.forEach { chip -> - setupChipCheckListener(chip) { isChecked -> - when (isChecked) { - true -> { - ratingChips.filter { it != chip }.forEach { it.isChecked = false } - val ratingValue = - Rating.entries.find { - chip.text.toString().contains(it.value.toString()) - } - detailExploreResultViewModel.updateSelectedRating(ratingValue?.value) - } - - false -> detailExploreResultViewModel.updateSelectedRating(null) - } - } - } - } - - private fun setupChipCheckListener( - chip: Chip, - onCheckedChange: (Boolean) -> Unit, - ) { - chip.setOnCheckedChangeListener(null) - chip.setOnCheckedChangeListener { _, isChecked -> onCheckedChange(isChecked) } - } - - private fun setupObserver() { - detailExploreResultViewModel.selectedGenres.observe(viewLifecycleOwner) { genres -> - (0 until binding.wcgDetailExploreInfoGenre.childCount) - .map { binding.wcgDetailExploreInfoGenre.getChildAt(it) } - .filterIsInstance() - .forEach { chip -> - chip.isSelected = genres?.any { it.titleKr == chip.text } == true - } - } - - detailExploreResultViewModel.isNovelCompleted.observe(viewLifecycleOwner) { selectedStatus -> - val selectedChip = selectedStatus?.let { - SeriesStatus.fromIsCompleted(it).title - } - - listOf( - binding.chipDetailExploreInfoStatusInSeries, - binding.chipDetailExploreInfoStatusComplete, - ).forEach { chip -> - chip.isChecked = chip.text.toString() == selectedChip - } - } - - detailExploreResultViewModel.selectedRating.observe(viewLifecycleOwner) { selectedRating -> - listOf( - binding.chipDetailExploreInfoRatingLowest, - binding.chipDetailExploreInfoRatingLower, - binding.chipDetailExploreInfoRatingHigher, - binding.chipDetailExploreInfoRatingHighest, - ).forEach { chip -> - val ratingValue = - Rating.entries.find { chip.text.toString().contains(it.value.toString()) } - chip.isChecked = selectedRating == ratingValue?.value - } - } - } -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt deleted file mode 100644 index 3399ff386..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt +++ /dev/null @@ -1,299 +0,0 @@ -package com.into.websoso.ui.detailExploreResult - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.core.view.children -import androidx.core.view.forEach -import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels -import com.into.websoso.R -import com.into.websoso.R.color.bg_novel_rating_chip_background_selector -import com.into.websoso.R.color.bg_novel_rating_chip_stroke_selector -import com.into.websoso.R.color.bg_novel_rating_chip_text_selector -import com.into.websoso.R.color.primary_100_6A5DFD -import com.into.websoso.R.color.white -import com.into.websoso.R.style.body2 -import com.into.websoso.core.common.ui.base.BaseFragment -import com.into.websoso.core.common.ui.custom.WebsosoChip -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel -import com.into.websoso.core.common.ui.model.CategoriesModel.Companion.findKeywordByName -import com.into.websoso.core.common.util.SingleEventHandler -import com.into.websoso.core.common.util.toFloatPxFromDp -import com.into.websoso.core.common.util.tracker.Tracker -import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keword_remove -import com.into.websoso.core.resource.R.string.detail_explore_search_hint -import com.into.websoso.core.resource.R.string.inquire_link -import com.into.websoso.databinding.FragmentDetailExploreResultKeywordBinding -import com.into.websoso.ui.detailExplore.keyword.DetailExploreClickListener -import com.into.websoso.ui.detailExplore.keyword.adapter.DetailExploreKeywordAdapter -import com.into.websoso.ui.detailExploreResult.model.DetailExploreResultUiState -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class DetailExploreResultKeywordFragment : - BaseFragment(R.layout.fragment_detail_explore_result_keyword) { - @Inject - lateinit var tracker: Tracker - - private val detailExploreResultViewModel: DetailExploreResultViewModel by activityViewModels() - private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() } - private val detailExploreKeywordAdapter: DetailExploreKeywordAdapter by lazy { - DetailExploreKeywordAdapter(detailExploreResultViewModel::updateClickedChipState) - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - bindViewModel() - setupAdapter() - setupObserver() - setupSearchKeyword() - setupWebsosoSearchEditListener() - setupBackButtonListener() - tracker.trackEvent("seek_result") - } - - private fun bindViewModel() { - binding.detailExploreResultViewModel = detailExploreResultViewModel - binding.onClick = onDetailExploreKeywordButtonClick() - binding.lifecycleOwner = viewLifecycleOwner - } - - private fun onDetailExploreKeywordButtonClick() = - object : DetailExploreClickListener { - override fun onNovelInquireButtonClick() { - val inquireUrl = getString(inquire_link) - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(inquireUrl)) - startActivity(intent) - } - - override fun onDetailSearchNovelButtonClick() { - singleEventHandler.throttleFirst { - detailExploreResultViewModel.updateSearchResult(true) - - (parentFragment as? DetailExploreResultDialogBottomSheet)?.dismiss() - - detailExploreResultViewModel.updateIsBottomSheetOpen(false) - } - } - - override fun onKeywordResetButtonClick() { - detailExploreResultViewModel.updateSelectedKeywordValueClear() - } - } - - private fun setupAdapter() { - binding.rvDetailExploreKeywordList.apply { - adapter = detailExploreKeywordAdapter - itemAnimator = null - } - } - - private fun setupObserver() { - detailExploreResultViewModel.uiState.observe(viewLifecycleOwner) { uiState -> - detailExploreKeywordAdapter.submitList(uiState.categories) - setupSelectedScrollViewVisibility(uiState.categories) - setupSelectedChips(uiState.categories) - updateSearchKeywordResult(uiState) - } - } - - private fun setupSelectedScrollViewVisibility(categories: List) { - val hasSelectedKeywords = categories.flatMap { it.keywords }.any { it.isSelected } - - binding.hsvRatingKeywordSelectedKeyword.isVisible = hasSelectedKeywords - } - - private fun setupSelectedChips(categories: List) { - val currentChipKeywords = - binding.wcgDetailExploreKeywordSelectedKeyword.children - .filterIsInstance() - .map { it.text.toString() } - .toList() - - val selectedKeywords = - categories - .asSequence() - .flatMap { it.keywords.asSequence() } - .filter { it.isSelected } - .map { it.keywordName } - .toList() - - val chipsToRemove = currentChipKeywords - selectedKeywords.toSet() - val chipsToAdd = selectedKeywords - currentChipKeywords.toSet() - - chipsToRemove.forEach { keywordName -> - removeSelectedChip(keywordName) - } - - chipsToAdd.forEach { keywordName -> - createSelectedChip( - categories.findKeywordByName(keywordName) - ?: throw IllegalArgumentException("Keyword not found: $keywordName"), - ) - } - } - - private fun removeSelectedChip(keywordName: String) { - val chipToRemove = binding.wcgDetailExploreKeywordSelectedKeyword.children.find { - (it as WebsosoChip).text == keywordName - } - chipToRemove?.let { - binding.wcgDetailExploreKeywordSelectedKeyword.removeView(it) - } - } - - private fun createSelectedChip(selectedKeyword: KeywordModel) { - WebsosoChip(requireContext()) - .apply { - setWebsosoChipText(selectedKeyword.keywordName) - setWebsosoChipTextAppearance(body2) - setWebsosoChipTextColor(primary_100_6A5DFD) - setWebsosoChipStrokeColor(primary_100_6A5DFD) - setWebsosoChipBackgroundColor(white) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(4f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnCloseIconClickListener { - detailExploreResultViewModel.updateClickedChipState( - selectedKeyword.keywordId, - ) - } - setWebsosoChipCloseIconVisibility(true) - setWebsosoChipCloseIconDrawable(ic_novel_rating_keword_remove) - setWebsosoChipCloseIconSize(20f) - setWebsosoChipCloseIconEndPadding(18f) - setCloseIconTintResource(primary_100_6A5DFD) - }.also { websosoChip -> - binding.wcgDetailExploreKeywordSelectedKeyword.addChip(websosoChip) - } - } - - private fun updateSearchKeywordResult(uiState: DetailExploreResultUiState) { - val previousSearchResultKeywords = - binding.wcgDetailExploreKeywordResult.children - .toList() - .map { it as WebsosoChip } - - when { - !uiState.isSearchKeywordProceeding -> return - - uiState.isSearchResultKeywordsEmpty -> return - - uiState.searchResultKeywords.map { it.keywordName } == previousSearchResultKeywords.map { it.text.toString() } -> { - updateSearchKeywordResultIsSelected(uiState) - return - } - - else -> { - binding.wcgDetailExploreKeywordResult.removeAllViews() - updateSearchKeywordResultWebsosoChips(uiState) - } - } - } - - private fun updateSearchKeywordResultIsSelected(uiState: DetailExploreResultUiState) { - binding.wcgDetailExploreKeywordResult.forEach { view -> - val chip = view as? WebsosoChip ?: return@forEach - - val isSelected = - uiState.categories - .asSequence() - .flatMap { it.keywords } - .filter { it.isSelected } - .any { it.keywordName == chip.text.toString() } - - chip.isSelected = isSelected - } - } - - private fun updateSearchKeywordResultWebsosoChips(uiState: DetailExploreResultUiState) { - uiState.searchResultKeywords.forEach { keyword -> - val isSelected = uiState.categories - .asSequence() - .flatMap { it.keywords } - .any { it.keywordId == keyword.keywordId && it.isSelected } - - WebsosoChip(binding.root.context) - .apply { - setWebsosoChipText(keyword.keywordName) - setWebsosoChipTextAppearance(body2) - setWebsosoChipTextColor(bg_novel_rating_chip_text_selector) - setWebsosoChipStrokeColor(bg_novel_rating_chip_stroke_selector) - setWebsosoChipBackgroundColor(bg_novel_rating_chip_background_selector) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnWebsosoChipClick { - detailExploreResultViewModel.updateClickedChipState(keyword.keywordId) - } - this.isSelected = isSelected - }.also { websosoChip -> - binding.wcgDetailExploreKeywordResult.addChip(websosoChip) - } - } - } - - private fun setupSearchKeyword() { - binding.wsetDetailExploreKeywordSearch.apply { - setWebsosoSearchHint(getString(detail_explore_search_hint)) - } - } - - private fun setupWebsosoSearchEditListener() { - binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchActionListener { _, _, _ -> - performSearch() - true - } - - binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchFocusChangeListener { _, isFocused -> - if (isFocused) detailExploreResultViewModel.updateIsSearchKeywordProceeding(true) - } - - binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchClearClickListener { - initSearchKeyword() - } - } - - private fun performSearch() { - val input = binding.wsetDetailExploreKeywordSearch.getWebsosoSearchText() - if (input.isEmpty()) { - initSearchKeyword() - return - } - detailExploreResultViewModel.updateKeyword(input) - } - - private fun initSearchKeyword() { - binding.wsetDetailExploreKeywordSearch.clearWebsosoSearchFocus() - detailExploreResultViewModel.initSearchKeyword() - } - - private fun setupBackButtonListener() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (binding.wsetDetailExploreKeywordSearch.hasFocus()) { - initSearchKeyword() - } else { - requireActivity().onBackPressedDispatcher.onBackPressed() - } - } - }, - ) - } - - override fun onDestroyView() { - initSearchKeyword() - super.onDestroyView() - } -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt index d25edb937..820e9957a 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt @@ -4,13 +4,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.map import androidx.lifecycle.viewModelScope -import com.into.websoso.core.common.ui.model.CategoriesModel -import com.into.websoso.data.repository.KeywordRepository import com.into.websoso.domain.usecase.GetDetailExploreResultUseCase import com.into.websoso.ui.detailExplore.info.model.Genre -import com.into.websoso.ui.detailExplore.info.model.SeriesStatus import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel import com.into.websoso.ui.detailExploreResult.model.DetailExploreResultUiState import com.into.websoso.ui.mapper.toUi @@ -23,44 +19,22 @@ class DetailExploreResultViewModel @Inject constructor( private val getDetailExploreResultUseCase: GetDetailExploreResultUseCase, - private val keywordRepository: KeywordRepository, ) : ViewModel() { private val _uiState: MutableLiveData = MutableLiveData(DetailExploreResultUiState()) val uiState: LiveData get() = _uiState - private val _selectedGenres: MutableLiveData?> = - MutableLiveData(mutableListOf()) - val selectedGenres: LiveData?> get() = _selectedGenres.map { it?.toList() } - + private val _selectedGenres: MutableLiveData?> = MutableLiveData(emptyList()) private val _isNovelCompleted: MutableLiveData = MutableLiveData() - val isNovelCompleted: LiveData get() = _isNovelCompleted - - private val _selectedSeriesStatus: MutableLiveData = MutableLiveData() - private val _selectedRating: MutableLiveData = MutableLiveData() - val selectedRating: LiveData get() = _selectedRating - - val ratings: List = listOf(3.5f, 4.0f, 4.5f, 4.8f) - - private val _selectedKeywordIds: MutableLiveData?> = - MutableLiveData(mutableListOf()) - val selectedKeywordIds: LiveData?> get() = _selectedKeywordIds.map { it?.toList() } + private val _selectedKeywordIds: MutableLiveData?> = MutableLiveData(emptyList()) private val _appliedFiltersMessage: MediatorLiveData = MediatorLiveData() val appliedFiltersMessage: LiveData get() = _appliedFiltersMessage - private val _isInfoChipSelected: MediatorLiveData = MediatorLiveData(false) - val isInfoChipSelected: LiveData get() = _isInfoChipSelected - - private val _isKeywordChipSelected: MediatorLiveData = MediatorLiveData(false) - val isKeywordChipSelected: LiveData get() = _isKeywordChipSelected - private val _isNovelResultEmptyBoxVisibility: MutableLiveData = MutableLiveData(false) val isNovelResultEmptyBoxVisibility: LiveData get() = _isNovelResultEmptyBoxVisibility - private val isBottomSheetOpen = MutableLiveData(false) - init { _appliedFiltersMessage.apply { addSource(_selectedGenres) { updateMessage() } @@ -68,82 +42,26 @@ class DetailExploreResultViewModel addSource(_selectedRating) { updateMessage() } addSource(_selectedKeywordIds) { updateMessage() } } - - _isInfoChipSelected.apply { - addSource(_selectedGenres) { isInfoChipSelectedEnabled() } - addSource(_isNovelCompleted) { isInfoChipSelectedEnabled() } - addSource(_selectedRating) { isInfoChipSelectedEnabled() } - } - - _isKeywordChipSelected.addSource(_selectedKeywordIds) { isKeywordChipSelectedEnabled() } - updateKeyword(null) } private fun updateMessage() { - if (isBottomSheetOpen.value == true) return - val appliedFilters = mutableListOf() - _selectedGenres.value?.let { genres -> - if (genres.isNotEmpty()) { - appliedFilters.add(GENRES_LABEL) - } - } - - _isNovelCompleted.value?.let { - appliedFilters.add(NOVEL_COMPLETED_LABEL) - } - - _selectedRating.value?.let { - appliedFilters.add(RATING_LABEL) - } - - _selectedKeywordIds.value?.let { keywords -> - if (keywords.isNotEmpty()) { - appliedFilters.add(KEYWORDS_LABEL) - } - } + if (_selectedGenres.value?.isNotEmpty() == true) appliedFilters.add(GENRES_LABEL) + if (_isNovelCompleted.value != null) appliedFilters.add(NOVEL_COMPLETED_LABEL) + if (_selectedRating.value != null) appliedFilters.add(RATING_LABEL) + if (_selectedKeywordIds.value?.isNotEmpty() == true) appliedFilters.add(KEYWORDS_LABEL) _appliedFiltersMessage.value = appliedFilters.joinToString(FILTER_SEPARATOR) } - private fun isInfoChipSelectedEnabled() { - val isGenreChipSelected: Boolean = _selectedGenres.value?.isNotEmpty() == true - val isStatusChipSelected: Boolean = _isNovelCompleted.value != null - val isRatingChipSelected: Boolean = _selectedRating.value != null - - _isInfoChipSelected.value = - isGenreChipSelected || - isStatusChipSelected || - isRatingChipSelected - } - - private fun isKeywordChipSelectedEnabled() { - _isKeywordChipSelected.value = selectedKeywordIds.value?.isNotEmpty() - } - fun updatePreviousSearchFilteredValue(detailExploreFilteredModel: DetailExploreFilteredModel) { - _selectedGenres.value = detailExploreFilteredModel.genres?.toMutableList() + _selectedGenres.value = detailExploreFilteredModel.genres _isNovelCompleted.value = detailExploreFilteredModel.isCompleted _selectedRating.value = detailExploreFilteredModel.novelRating - _selectedKeywordIds.value = detailExploreFilteredModel.keywordIds?.toMutableList() - - val currentUiState = _uiState.value ?: return - val selectedKeywordIds = _selectedKeywordIds.value.orEmpty() - - val updatedCategories = currentUiState.categories.map { category -> - val updatedKeywords = category.keywords.map { existingKeyword -> - if (selectedKeywordIds.contains(existingKeyword.keywordId)) { - existingKeyword.copy(isSelected = !existingKeyword.isSelected) - } else { - existingKeyword - } - } - category.copy(keywords = updatedKeywords) - } + _selectedKeywordIds.value = detailExploreFilteredModel.keywordIds updateSearchResult(true) - _uiState.value = currentUiState.copy(categories = updatedCategories) } fun updateSearchResult(isSearchButtonClick: Boolean) { @@ -152,10 +70,10 @@ class DetailExploreResultViewModel viewModelScope.launch { runCatching { getDetailExploreResultUseCase( - genres = selectedGenres.value?.map { it.titleEn }, - isCompleted = isNovelCompleted.value, - novelRating = selectedRating.value, - keywordIds = selectedKeywordIds.value, + genres = _selectedGenres.value?.map { it.titleEn }, + isCompleted = _isNovelCompleted.value, + novelRating = _selectedRating.value, + keywordIds = _selectedKeywordIds.value, isSearchButtonClick = isSearchButtonClick, ) }.onSuccess { results -> @@ -190,153 +108,6 @@ class DetailExploreResultViewModel } } - fun updateSelectedInfoValueClear() { - _selectedGenres.value = mutableListOf() - _isNovelCompleted.value = null - _selectedRating.value = null - } - - fun updateSelectedGenres(genre: Genre) { - val currentGenres = _selectedGenres.value?.toMutableList() ?: mutableListOf() - - _selectedGenres.value = when (currentGenres.contains(genre)) { - true -> { - currentGenres.remove(genre) - currentGenres - } - - false -> { - currentGenres.add(genre) - currentGenres - } - } - } - - fun updateSelectedSeriesStatus(status: SeriesStatus?) { - _selectedSeriesStatus.value = status - _isNovelCompleted.value = status?.isCompleted - } - - fun updateSelectedRating(rating: Float?) { - _selectedRating.value = rating - } - - fun updateKeyword(searchWord: String?) { - viewModelScope.launch { - runCatching { - keywordRepository.fetchKeywords(searchWord) - }.onSuccess { keywordsList -> - val categoriesModel = - CategoriesModel(categories = keywordsList.categories.map { it.toUi() }) - - val selectedKeywordIds = selectedKeywordIds.value.orEmpty() - - val updatedCategories = categoriesModel.categories.map { category -> - val updatedKeywords = category.keywords.map { keyword -> - if (selectedKeywordIds.contains(keyword.keywordId)) { - keyword.copy(isSelected = true) - } else { - keyword - } - } - category.copy(keywords = updatedKeywords) - } - - _uiState.value = when (searchWord) { - null -> { - uiState.value?.copy( - loading = false, - categories = updatedCategories, - ) - } - - else -> { - val results = updatedCategories.flatMap { it.keywords } - uiState.value?.copy( - loading = false, - searchResultKeywords = results, - isInitialSearchKeyword = false, - isSearchResultKeywordsEmpty = results.isEmpty(), - ) - } - } - }.onFailure { - _uiState.value = uiState.value?.copy( - loading = false, - error = true, - ) - } - } - } - - fun updateClickedChipState(keywordId: Int) { - val currentUiState = _uiState.value ?: return - - val updatedCategories = currentUiState.categories.map { category -> - val updatedKeywords = category.keywords.map { existingKeyword -> - when (existingKeyword.keywordId == keywordId) { - true -> existingKeyword.copy(isSelected = !existingKeyword.isSelected) - false -> existingKeyword - } - } - category.copy(keywords = updatedKeywords) - } - - val selectedKeywordIds = updatedCategories.flatMap { category -> - category.keywords.filter { it.isSelected }.map { it.keywordId } - } - - val isAnyKeywordSelected = updatedCategories.any { category -> - category.keywords.any { it.isSelected } - } - - _selectedKeywordIds.value = selectedKeywordIds.toMutableList() - _isKeywordChipSelected.value = isAnyKeywordSelected - _uiState.value = currentUiState.copy(categories = updatedCategories) - } - - fun updateSelectedKeywordValueClear() { - val currentState = _uiState.value ?: return - val updatedCategories = currentState.categories - - val resetCategories = updatedCategories.map { category -> - category.copy( - keywords = category.keywords.map { keyword -> - keyword.copy(isSelected = false) - }, - ) - } - - _selectedKeywordIds.value = mutableListOf() - _uiState.value = currentState.copy(categories = resetCategories) - _isKeywordChipSelected.value = false - } - - fun updateIsSearchKeywordProceeding(isProceeding: Boolean) { - uiState.value?.let { uiState -> - _uiState.value = uiState.copy( - isSearchKeywordProceeding = isProceeding, - ) - } - } - - fun initSearchKeyword() { - uiState.value?.let { uiState -> - _uiState.value = uiState.copy( - searchResultKeywords = emptyList(), - isSearchKeywordProceeding = false, - isInitialSearchKeyword = true, - isSearchResultKeywordsEmpty = false, - ) - } - } - - fun updateIsBottomSheetOpen(isBottomSheetOpen: Boolean) { - this.isBottomSheetOpen.value = isBottomSheetOpen - - if (!isBottomSheetOpen) updateMessage() - } - companion object { private const val GENRES_LABEL = "장르" private const val NOVEL_COMPLETED_LABEL = "연재상태" diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt index 0be8226de..a5cba5f8d 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt @@ -1,6 +1,5 @@ package com.into.websoso.ui.detailExploreResult.model -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel import com.into.websoso.ui.normalExplore.model.NormalExploreModel data class DetailExploreResultUiState( @@ -9,9 +8,4 @@ data class DetailExploreResultUiState( val isLoadable: Boolean = false, val novels: List = emptyList(), val novelCount: Long = 0, - val categories: List = emptyList(), - val searchResultKeywords: List = emptyList(), - val isSearchKeywordProceeding: Boolean = false, - val isInitialSearchKeyword: Boolean = true, - val isSearchResultKeywordsEmpty: Boolean = false, ) diff --git a/app/src/main/res/layout/dialog_detail_explore.xml b/app/src/main/res/layout/dialog_detail_explore.xml deleted file mode 100644 index 7efe8e058..000000000 --- a/app/src/main/res/layout/dialog_detail_explore.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_detail_explore_result_info.xml b/app/src/main/res/layout/fragment_detail_explore_result_info.xml deleted file mode 100644 index 6f93908cb..000000000 --- a/app/src/main/res/layout/fragment_detail_explore_result_info.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_detail_explore_result_keyword.xml b/app/src/main/res/layout/fragment_detail_explore_result_keyword.xml deleted file mode 100644 index 7ace9a82b..000000000 --- a/app/src/main/res/layout/fragment_detail_explore_result_keyword.xml +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From b8920d4b8dc24cd22eb86ff0b261b15278c432d5 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 1 May 2026 14:06:24 +0900 Subject: [PATCH 09/13] =?UTF-8?q?build:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20Keyword=20=EC=AA=BD=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/detailExplore/info/model/Rating.kt | 10 --- .../keyword/DetailExploreClickListener.kt | 9 -- .../adapter/DetailExploreKeywordAdapter.kt | 52 ----------- .../adapter/DetailExploreKeywordViewHolder.kt | 88 ------------------- 4 files changed, 159 deletions(-) delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt deleted file mode 100644 index b31c640df..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.into.websoso.ui.detailExplore.info.model - -enum class Rating( - val value: Float, -) { - LOWEST(3.5f), - LOWER(4.0f), - HIGHER(4.5f), - HIGHEST(4.8f), -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt deleted file mode 100644 index 2fe604822..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.into.websoso.ui.detailExplore.keyword - -interface DetailExploreClickListener { - fun onNovelInquireButtonClick() - - fun onDetailSearchNovelButtonClick() - - fun onKeywordResetButtonClick() -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt deleted file mode 100644 index 750f35066..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.into.websoso.ui.detailExplore.keyword.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel -import com.into.websoso.databinding.ItemCommonKeywordBinding - -class DetailExploreKeywordAdapter( - private val onKeywordClick: (keywordId: Int) -> (Unit), -) : ListAdapter(diffUtil) { - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int, - ): DetailExploreKeywordViewHolder { - val binding = ItemCommonKeywordBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - return DetailExploreKeywordViewHolder(binding, onKeywordClick) - } - - override fun onBindViewHolder( - holder: DetailExploreKeywordViewHolder, - position: Int, - ) { - val item = getItem(position) - holder.apply { - when (isChipSetting) { - true -> updateChipState(item) - false -> initKeywordView(item) - } - } - } - - companion object { - private val diffUtil = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: CategoryModel, - newItem: CategoryModel, - ): Boolean = oldItem.categoryName == newItem.categoryName - - override fun areContentsTheSame( - oldItem: CategoryModel, - newItem: CategoryModel, - ): Boolean = oldItem == newItem - } - } -} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt deleted file mode 100644 index bb52b5dcb..000000000 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.into.websoso.ui.detailExplore.keyword.adapter - -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.recyclerview.widget.RecyclerView -import com.into.websoso.R -import com.into.websoso.core.common.ui.custom.WebsosoChip -import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel -import com.into.websoso.core.common.util.getS3ImageUrl -import com.into.websoso.core.common.util.toFloatPxFromDp -import com.into.websoso.core.common.util.toIntPxFromDp -import com.into.websoso.databinding.ItemCommonKeywordBinding - -class DetailExploreKeywordViewHolder( - private val binding: ItemCommonKeywordBinding, - private val onKeywordClick: (keywordId: Int) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - var isChipSetting: Boolean = false - private set - - init { - binding.setupExpandToggleBtn() - } - - private fun ItemCommonKeywordBinding.setupExpandToggleBtn() { - ivNovelRatingKeywordToggle.setOnClickListener { - ivNovelRatingKeywordToggle.isSelected = !ivNovelRatingKeywordToggle.isSelected - val layoutParams = - wcgNovelRatingKeyword.layoutParams as ConstraintLayout.LayoutParams - - when (ivNovelRatingKeywordToggle.isSelected) { - true -> - layoutParams.matchConstraintMaxHeight = - ConstraintLayout.LayoutParams.WRAP_CONTENT - - false -> layoutParams.matchConstraintMaxHeight = 92.toIntPxFromDp() - } - wcgNovelRatingKeyword.layoutParams = layoutParams - } - } - - fun initKeywordView(category: CategoryModel) { - val imageUrl: String = itemView.getS3ImageUrl(category.categoryImage) - - val updatedCategory = category.copy( - categoryImage = imageUrl, - ) - - binding.apply { - tvRatingKeyword.text = updatedCategory.categoryName - categoryImageUrl = updatedCategory.categoryImage - setupWebsosoChips(category) - } - isChipSetting = true - } - - private fun ItemCommonKeywordBinding.setupWebsosoChips(category: CategoryModel) { - wcgNovelRatingKeyword.removeAllViews() - category.keywords.forEach { keyword -> - WebsosoChip(binding.root.context) - .apply { - setWebsosoChipText(keyword.keywordName) - setWebsosoChipTextAppearance(R.style.body2) - setWebsosoChipTextColor(R.color.bg_novel_rating_chip_text_selector) - setWebsosoChipStrokeColor(R.color.bg_novel_rating_chip_stroke_selector) - setWebsosoChipBackgroundColor(R.color.bg_novel_rating_chip_background_selector) - setWebsosoChipPaddingVertical(12f.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp()) - setWebsosoChipRadius(20f.toFloatPxFromDp()) - setOnWebsosoChipClick { - onKeywordClick(keyword.keywordId) - } - isSelected = keyword.isSelected - }.also { websosoChip -> wcgNovelRatingKeyword.addChip(websosoChip) } - } - } - - fun updateChipState(category: CategoryModel) { - val keywordSelectionMap = - category.keywords.associateBy({ it.keywordName }, { it.isSelected }) - - (0 until binding.wcgNovelRatingKeyword.childCount) - .map { binding.wcgNovelRatingKeyword.getChildAt(it) } - .filterIsInstance() - .forEach { chip -> - chip.isSelected = keywordSelectionMap[chip.text] ?: false - } - } -} From fd5e4098eb8ea8e7d77bac5926201fbb8908ac75 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 1 May 2026 14:14:45 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore:=20=ED=99=88=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EA=B4=80=EC=8B=AC=EA=B8=80=20=EC=84=B9=EC=85=98=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fragment_home.xml에서 관심글 섹션(빈/등록/추천) 3개 컨테이너 제거 - HomeFragment/HomeViewModel/HomeUiState에서 userInterestFeeds 관련 상태 및 메서드 제거 - UserInterestFeedAdapter/ViewHolder, item_user_interest_feed.xml 삭제 - FeedRepository.fetchUserInterestFeeds, FeedApi.getUserInterestFeeds, mapper, DTO, Entity 제거 (app + data/feed 모듈) - 관심글 관련 string 리소스 정리 --- .../into/websoso/data/mapper/FeedMapper.kt | 19 --- .../data/model/UserInterestFeedsEntity.kt | 29 ---- .../into/websoso/data/remote/api/FeedApi.kt | 4 - .../response/UserInterestFeedsResponseDto.kt | 32 ---- .../websoso/data/repository/FeedRepository.kt | 4 - .../into/websoso/ui/main/home/HomeFragment.kt | 71 +-------- .../websoso/ui/main/home/HomeViewModel.kt | 35 +---- .../home/adpater/UserInterestFeedAdapter.kt | 41 ----- .../adpater/UserInterestFeedViewHolder.kt | 38 ----- .../websoso/ui/main/home/model/HomeUiState.kt | 5 +- app/src/main/res/layout/fragment_home.xml | 113 +------------- .../res/layout/item_user_interest_feed.xml | 141 ------------------ .../response/UserInterestFeedsResponseDto.kt | 32 ---- core/resource/src/main/res/values/strings.xml | 9 -- .../websoso/data/feed/mapper/FeedMapper.kt | 19 --- .../feed/model/UserInterestFeedsEntity.kt | 29 ---- 16 files changed, 11 insertions(+), 610 deletions(-) delete mode 100644 app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt delete mode 100644 app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt delete mode 100644 app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt delete mode 100644 app/src/main/res/layout/item_user_interest_feed.xml delete mode 100644 core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/UserInterestFeedsResponseDto.kt delete mode 100644 data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt diff --git a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt index 2e8c9c4b8..509272e3f 100644 --- a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt +++ b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt @@ -8,14 +8,12 @@ import com.into.websoso.data.model.FeedDetailEntity.UserEntity import com.into.websoso.data.model.FeedEntity import com.into.websoso.data.model.FeedsEntity import com.into.websoso.data.model.PopularFeedsEntity -import com.into.websoso.data.model.UserInterestFeedsEntity import com.into.websoso.data.remote.response.CommentResponseDto import com.into.websoso.data.remote.response.CommentsResponseDto import com.into.websoso.data.remote.response.FeedDetailResponseDto import com.into.websoso.data.remote.response.FeedResponseDto import com.into.websoso.data.remote.response.FeedsResponseDto import com.into.websoso.data.remote.response.PopularFeedsResponseDto -import com.into.websoso.data.remote.response.UserInterestFeedsResponseDto fun FeedsResponseDto.toData(): FeedsEntity = FeedsEntity( @@ -120,20 +118,3 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity = ) }, ) - -fun UserInterestFeedsResponseDto.toData(): UserInterestFeedsEntity = - UserInterestFeedsEntity( - userInterestFeeds = userInterestFeeds.map { feed -> - UserInterestFeedsEntity.UserInterestFeedEntity( - avatarImage = feed.avatarImage, - feedContent = feed.feedContent, - nickname = feed.nickname, - novelId = feed.novelId, - novelImage = feed.novelImage, - novelRating = feed.novelRating, - novelRatingCount = feed.novelRatingCount, - novelTitle = feed.novelTitle, - ) - }, - message = message, - ) diff --git a/app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt b/app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt deleted file mode 100644 index 7d08b5b24..000000000 --- a/app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.into.websoso.data.model - -data class UserInterestFeedsEntity( - val userInterestFeeds: List, - val message: String, -) { - data class UserInterestFeedEntity( - val avatarImage: String, - val feedContent: String, - val nickname: String, - val novelId: Int, - val novelImage: String, - val novelRating: Double, - val novelRatingCount: Int, - val novelTitle: String, - ) -} - -enum class UserInterestFeedMessage( - val message: String, -) { - NO_INTEREST_NOVELS("NO_INTEREST_NOVELS"), - NO_ASSOCIATED_FEEDS("NO_ASSOCIATED_FEEDS"), - ; - - companion object { - fun fromMessage(message: String): UserInterestFeedMessage? = entries.find { it.message == message } - } -} diff --git a/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt b/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt index 4c7a1f6eb..31986305c 100644 --- a/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt +++ b/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt @@ -5,7 +5,6 @@ import com.into.websoso.data.remote.response.CommentsResponseDto import com.into.websoso.data.remote.response.FeedDetailResponseDto import com.into.websoso.data.remote.response.FeedsResponseDto import com.into.websoso.data.remote.response.PopularFeedsResponseDto -import com.into.websoso.data.remote.response.UserInterestFeedsResponseDto import okhttp3.MultipartBody import retrofit2.http.Body import retrofit2.http.DELETE @@ -47,9 +46,6 @@ interface FeedApi { @GET("feeds/popular") suspend fun getPopularFeeds(): PopularFeedsResponseDto - @GET("feeds/interest") - suspend fun getUserInterestFeeds(): UserInterestFeedsResponseDto - @DELETE("feeds/{feedId}") suspend fun deleteFeed( @Path("feedId") feedId: Long, diff --git a/app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt b/app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt deleted file mode 100644 index af2615c75..000000000 --- a/app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.into.websoso.data.remote.response - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class UserInterestFeedsResponseDto( - @SerialName("recommendFeeds") - val userInterestFeeds: List, - @SerialName("message") - val message: String, -) { - @Serializable - data class UserInterestFeedResponseDto( - @SerialName("avatarImage") - val avatarImage: String, - @SerialName("feedContent") - val feedContent: String, - @SerialName("nickname") - val nickname: String, - @SerialName("novelId") - val novelId: Int, - @SerialName("novelImage") - val novelImage: String, - @SerialName("novelRating") - val novelRating: Double, - @SerialName("novelRatingCount") - val novelRatingCount: Int, - @SerialName("novelTitle") - val novelTitle: String, - ) -} diff --git a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt index 54040827c..fefadad96 100644 --- a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt @@ -1,11 +1,9 @@ package com.into.websoso.data.repository -import com.into.websoso.data.mapper.MultiPartMapper import com.into.websoso.data.mapper.toData import com.into.websoso.data.model.FeedEntity import com.into.websoso.data.model.FeedsEntity import com.into.websoso.data.model.PopularFeedsEntity -import com.into.websoso.data.model.UserInterestFeedsEntity import com.into.websoso.data.remote.api.FeedApi import javax.inject.Inject import javax.inject.Singleton @@ -37,8 +35,6 @@ class FeedRepository suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData() - suspend fun fetchUserInterestFeeds(): UserInterestFeedsEntity = feedApi.getUserInterestFeeds().toData() - suspend fun saveRemovedFeed(feedId: Long) { runCatching { feedApi.deleteFeed(feedId) diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt index f8e39dd2c..ca8497b07 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt @@ -22,7 +22,6 @@ import com.into.websoso.core.common.ui.model.ResultFrom.ProfileEditSuccess import com.into.websoso.core.common.util.SingleEventHandler import com.into.websoso.core.common.util.collectWithLifecycle import com.into.websoso.core.common.util.tracker.Tracker -import com.into.websoso.core.resource.R.string.home_nickname_interest_feed import com.into.websoso.databinding.FragmentHomeBinding import com.into.websoso.ui.detailExplore.DetailExploreActivity import com.into.websoso.ui.feedDetail.FeedDetailActivity @@ -30,7 +29,6 @@ import com.into.websoso.ui.main.MainViewModel import com.into.websoso.ui.main.home.adpater.PopularFeedsAdapter import com.into.websoso.ui.main.home.adpater.PopularNovelsAdapter import com.into.websoso.ui.main.home.adpater.RecommendedNovelsByUserTasteAdapter -import com.into.websoso.ui.main.home.adpater.UserInterestFeedAdapter import com.into.websoso.ui.main.home.dialog.TermsAgreementDialogFragment import com.into.websoso.ui.normalExplore.NormalExploreActivity import com.into.websoso.ui.notification.NotificationActivity @@ -66,10 +64,6 @@ class HomeFragment : BaseFragment(fragment_home) { PopularFeedsAdapter(::onPopularFeedClick) } - private val userInterestFeedAdapter: UserInterestFeedAdapter by lazy { - UserInterestFeedAdapter(::onUserInterestNovelFeedClick) - } - private val recommendedNovelsByUserTasteAdapter: RecommendedNovelsByUserTasteAdapter by lazy { RecommendedNovelsByUserTasteAdapter(::onRecommendedNovelClick) } @@ -109,7 +103,6 @@ class HomeFragment : BaseFragment(fragment_home) { setupItemDecoration() setupObserver() setupDotsIndicator() - onPostInterestNovelClick() onSettingPreferenceGenreClick() onNotificationButtonClick() onNormalSearchButtonClick() @@ -142,28 +135,17 @@ class HomeFragment : BaseFragment(fragment_home) { with(binding) { rvHomeTodayPopularNovel.adapter = popularNovelsAdapter vpHomePopularFeed.adapter = popularFeedsAdapter - rvUserInterestFeed.adapter = userInterestFeedAdapter rvRecommendNovelByUserTaste.adapter = recommendedNovelsByUserTasteAdapter } } private fun setupItemDecoration() { - with(binding) { - rvHomeTodayPopularNovel.addItemDecoration( - HomeCustomItemDecoration( - TODAY_POPULAR_NOVEL_MARGIN, - ), - ) - rvUserInterestFeed.addItemDecoration(HomeCustomItemDecoration(USER_INTEREST_MARGIN)) - } + binding.rvHomeTodayPopularNovel.addItemDecoration( + HomeCustomItemDecoration(TODAY_POPULAR_NOVEL_MARGIN), + ) } private fun setupObserver() { - mainViewModel.mainUiState.observe(viewLifecycleOwner) { uiState -> - binding.tvHomeInterestFeed.text = - getString(home_nickname_interest_feed, uiState.nickname) - } - homeViewModel.uiState.observe(viewLifecycleOwner) { uiState -> when { uiState.error -> { @@ -175,9 +157,7 @@ class HomeFragment : BaseFragment(fragment_home) { binding.wllHome.setWebsosoLoadingVisibility(false) popularNovelsAdapter.submitList(uiState.popularNovels) popularFeedsAdapter.submitList(uiState.popularFeeds) - updateUserInterestFeedsVisibility(uiState.userInterestFeeds.isEmpty()) updateRecommendedNovelByUserTasteVisibility(uiState.recommendedNovelsByUserTaste.isEmpty()) - userInterestFeedAdapter.submitList(uiState.userInterestFeeds) recommendedNovelsByUserTasteAdapter.submitList(uiState.recommendedNovelsByUserTaste) updateHasNotificationUnread(uiState.isNotificationUnread) } @@ -206,32 +186,6 @@ class HomeFragment : BaseFragment(fragment_home) { } } - private fun updateUserInterestFeedsVisibility(isUserInterestEmpty: Boolean) { - with(binding) { - if (isUserInterestEmpty) { - when (homeViewModel.uiState.value?.isInterestNovel) { - true -> { - clHomeUserInterestFeed.visibility = View.GONE - clHomeInterestFeed.visibility = View.GONE - clHomeNoAssociatedFeed.visibility = View.VISIBLE - } - - false -> { - clHomeUserInterestFeed.visibility = View.GONE - clHomeInterestFeed.visibility = View.VISIBLE - clHomeNoAssociatedFeed.visibility = View.GONE - } - - else -> Unit - } - } else { - clHomeUserInterestFeed.visibility = View.VISIBLE - clHomeInterestFeed.visibility = View.GONE - clHomeNoAssociatedFeed.visibility = View.GONE - } - } - } - private fun updateRecommendedNovelByUserTasteVisibility(isRecommendNovelByUserTasteEmpty: Boolean) { with(binding) { if (isRecommendNovelByUserTasteEmpty) { @@ -281,11 +235,6 @@ class HomeFragment : BaseFragment(fragment_home) { navigateToNovelDetail(novelId) } - private fun onUserInterestNovelFeedClick(novelId: Long) { - tracker.trackEvent("home_love_feedlist") - navigateToNovelDetail(novelId) - } - private fun onRecommendedNovelClick(novelId: Long) { tracker.trackEvent("home_prefer_novellist") navigateToNovelDetail(novelId) @@ -314,17 +263,6 @@ class HomeFragment : BaseFragment(fragment_home) { ) } - private fun onPostInterestNovelClick() { - binding.clHomeInterestFeed.setOnClickListener { - tracker.trackEvent("home_to_love_btn") - startActivityLauncher.launch( - NormalExploreActivity.getIntent( - requireContext(), - ), - ) - } - } - private fun onSettingPreferenceGenreClick() { binding.clHomeRecommendNovel.setOnClickListener { tracker.trackEvent("home_to_prefer_btn") @@ -354,7 +292,6 @@ class HomeFragment : BaseFragment(fragment_home) { companion object { private const val TODAY_POPULAR_NOVEL_MARGIN = 15 - private const val USER_INTEREST_MARGIN = 14 const val TAG = "HomeFragment" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index 99c2f6ea0..b7ec03296 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -9,9 +9,6 @@ import com.into.websoso.data.model.PopularFeedsEntity import com.into.websoso.data.model.PopularNovelsEntity import com.into.websoso.data.model.RecommendedNovelsByUserTasteEntity import com.into.websoso.data.model.TermsAgreementEntity -import com.into.websoso.data.model.UserInterestFeedMessage -import com.into.websoso.data.model.UserInterestFeedMessage.NO_INTEREST_NOVELS -import com.into.websoso.data.model.UserInterestFeedsEntity import com.into.websoso.data.repository.FeedRepository import com.into.websoso.data.repository.NotificationRepository import com.into.websoso.data.repository.NovelRepository @@ -78,11 +75,9 @@ class HomeViewModel val results = listOf( async { runCatching { novelRepository.fetchPopularNovels() } }, async { runCatching { feedRepository.fetchPopularFeeds() } }, - async { runCatching { feedRepository.fetchUserInterestFeeds() } }, async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } }, ).awaitAll() - // 실패가 하나라도 있다면 상위 onFailure로 예외 전파 val failures = results.filter { it.isFailure } if (failures.isNotEmpty()) { throw failures.first().exceptionOrNull() @@ -93,10 +88,8 @@ class HomeViewModel ?: PopularNovelsEntity(emptyList()) val popularFeeds = results[1].getOrNull() as? PopularFeedsEntity ?: PopularFeedsEntity(emptyList()) - val userInterestFeeds = results[2].getOrNull() as? UserInterestFeedsEntity - ?: UserInterestFeedsEntity(emptyList(), "") val recommendedNovels = - results[3].getOrNull() as? RecommendedNovelsByUserTasteEntity + results[2].getOrNull() as? RecommendedNovelsByUserTasteEntity ?: RecommendedNovelsByUserTasteEntity(emptyList()) _uiState.value = uiState.value?.copy( @@ -104,8 +97,6 @@ class HomeViewModel error = false, popularNovels = popularNovels.popularNovels, popularFeeds = popularFeeds.popularFeeds.chunked(3), - isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message), - userInterestFeeds = userInterestFeeds.userInterestFeeds, recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, ) }.onFailure { @@ -165,23 +156,13 @@ class HomeViewModel fun updateFeed() { viewModelScope.launch { runCatching { - listOf( - async { feedRepository.fetchPopularFeeds() }, - async { feedRepository.fetchUserInterestFeeds() }, - ).awaitAll() - }.onSuccess { responses -> - val popularFeeds = responses[0] as PopularFeedsEntity - val userInterestFeeds = responses[1] as UserInterestFeedsEntity - + feedRepository.fetchPopularFeeds() + }.onSuccess { popularFeeds -> _uiState.value = uiState.value?.copy( popularFeeds = popularFeeds.popularFeeds.chunked(3), - isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message), - userInterestFeeds = userInterestFeeds.userInterestFeeds, ) }.onFailure { - _uiState.value = uiState.value?.copy( - error = true, - ) + _uiState.value = uiState.value?.copy(error = true) } } } @@ -225,12 +206,6 @@ class HomeViewModel } } - private fun isUserInterestedInNovels(userInterestFeedMessage: String): Boolean = - when (UserInterestFeedMessage.fromMessage(userInterestFeedMessage)) { - NO_INTEREST_NOVELS -> false - else -> true - } - fun saveFCMToken(token: String) { viewModelScope.launch { runCatching { @@ -276,4 +251,4 @@ class HomeViewModel } } } - } + } \ No newline at end of file diff --git a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt b/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt deleted file mode 100644 index 6c3716f2c..000000000 --- a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.into.websoso.ui.main.home.adpater - -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.into.websoso.data.model.UserInterestFeedsEntity.UserInterestFeedEntity - -class UserInterestFeedAdapter( - private val onUserInterestFeedClick: (novelId: Long) -> (Unit), -) : ListAdapter(diffUtil) { - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int, - ): UserInterestFeedViewHolder = UserInterestFeedViewHolder.of(parent, onUserInterestFeedClick) - - override fun onBindViewHolder( - holder: UserInterestFeedViewHolder, - position: Int, - ) { - holder.bind(getItem(position)) - - val params = holder.itemView.layoutParams as RecyclerView.LayoutParams - params.width = (holder.itemView.context.resources.displayMetrics.widthPixels * 0.8).toInt() - holder.itemView.layoutParams = params - } - - companion object { - val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: UserInterestFeedEntity, - newItem: UserInterestFeedEntity, - ): Boolean = oldItem.novelId == newItem.novelId - - override fun areContentsTheSame( - oldItem: UserInterestFeedEntity, - newItem: UserInterestFeedEntity, - ): Boolean = oldItem == newItem - } - } -} diff --git a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt b/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt deleted file mode 100644 index b1dfa8aed..000000000 --- a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.into.websoso.ui.main.home.adpater - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.into.websoso.core.common.util.getS3ImageUrl -import com.into.websoso.data.model.UserInterestFeedsEntity.UserInterestFeedEntity -import com.into.websoso.databinding.ItemUserInterestFeedBinding - -class UserInterestFeedViewHolder( - private val binding: ItemUserInterestFeedBinding, - onUserInterestFeedClick: (novelId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - init { - binding.onClick = onUserInterestFeedClick - } - - fun bind(userInterestFeed: UserInterestFeedEntity) { - val updateUserInterestFeed = userInterestFeed.copy( - avatarImage = itemView.getS3ImageUrl(userInterestFeed.avatarImage ?: ""), - ) - binding.userInterestFeed = updateUserInterestFeed - } - - companion object { - fun of( - parent: ViewGroup, - onUserInterestFeedClick: (novelId: Long) -> Unit, - ): UserInterestFeedViewHolder { - val binding = ItemUserInterestFeedBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - return UserInterestFeedViewHolder(binding, onUserInterestFeedClick) - } - } -} diff --git a/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt b/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt index 609a79057..98ff109be 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt @@ -3,15 +3,12 @@ package com.into.websoso.ui.main.home.model import com.into.websoso.data.model.PopularFeedsEntity.PopularFeedEntity import com.into.websoso.data.model.PopularNovelsEntity.PopularNovelEntity import com.into.websoso.data.model.RecommendedNovelsByUserTasteEntity.RecommendedNovelByUserTasteEntity -import com.into.websoso.data.model.UserInterestFeedsEntity.UserInterestFeedEntity data class HomeUiState( val loading: Boolean = true, val error: Boolean = false, - val isInterestNovel: Boolean = false, val isNotificationUnread: Boolean = false, val popularNovels: List = listOf(), val popularFeeds: List> = listOf(), - val userInterestFeeds: List = listOf(), val recommendedNovelsByUserTaste: List = listOf(), -) +) \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index a900d63d9..53ee70e1f 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -217,117 +217,6 @@ app:progressMode="false" app:selectedDotColor="@color/black" /> - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/dotsIndicator_home" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/UserInterestFeedsResponseDto.kt b/core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/UserInterestFeedsResponseDto.kt deleted file mode 100644 index a83c0dc62..000000000 --- a/core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/UserInterestFeedsResponseDto.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.into.websoso.core.network.datasource.feed.model.response - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class UserInterestFeedsResponseDto( - @SerialName("recommendFeeds") - val userInterestFeeds: List, - @SerialName("message") - val message: String, -) { - @Serializable - data class UserInterestFeedResponseDto( - @SerialName("avatarImage") - val avatarImage: String, - @SerialName("feedContent") - val feedContent: String, - @SerialName("nickname") - val nickname: String, - @SerialName("novelId") - val novelId: Int, - @SerialName("novelImage") - val novelImage: String, - @SerialName("novelRating") - val novelRating: Double, - @SerialName("novelRatingCount") - val novelRatingCount: Int, - @SerialName("novelTitle") - val novelTitle: String, - ) -} diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml index aaf29458c..3b156ac51 100644 --- a/core/resource/src/main/res/values/strings.xml +++ b/core/resource/src/main/res/values/strings.xml @@ -233,10 +233,6 @@ + 오늘의 발견 + 지금 뜨는 글 - 관심글 - 관심 등록한 작품의 최신 글이에요 - 관심작품의 최신 소식을 모아서 볼 수 있어요.\n좋아하는 웹소설을 관심 등록 해볼까요? - 관심작품 등록하기 관심 등록한 작품의 최신 글이에요 로맨스, 로판, 판타지, 현판 등\n선호장르를 기반으로 웹소설을 추천해드려요! 선호장르를 기반으로 추천해드려요 @@ -245,12 +241,7 @@ %s님의 한마디 작품 소개 스포일러가 포함된 글 보기 - %1$s님의 한마디 - %1$.1f (%2$d) - ・ :*%1$s님의 관심글*: ・ %1$.1f (%2$d) - 관심 등록한 작품과 관련된 글이 없어요 - 관심글 설정 diff --git a/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt b/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt index 6ffe13643..68ae94e9f 100644 --- a/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt +++ b/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt @@ -6,14 +6,12 @@ import com.into.websoso.core.network.datasource.feed.model.response.FeedDetailRe import com.into.websoso.core.network.datasource.feed.model.response.FeedResponseDto import com.into.websoso.core.network.datasource.feed.model.response.FeedsResponseDto import com.into.websoso.core.network.datasource.feed.model.response.PopularFeedsResponseDto -import com.into.websoso.core.network.datasource.feed.model.response.UserInterestFeedsResponseDto import com.into.websoso.data.feed.model.CommentEntity import com.into.websoso.data.feed.model.CommentsEntity import com.into.websoso.data.feed.model.FeedDetailEntity import com.into.websoso.data.feed.model.FeedEntity import com.into.websoso.data.feed.model.FeedsEntity import com.into.websoso.data.feed.model.PopularFeedsEntity -import com.into.websoso.data.feed.model.UserInterestFeedsEntity fun FeedsResponseDto.toData(): FeedsEntity = FeedsEntity( @@ -122,20 +120,3 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity = ) }, ) - -fun UserInterestFeedsResponseDto.toData(): UserInterestFeedsEntity = - UserInterestFeedsEntity( - userInterestFeeds = userInterestFeeds.map { feed -> - UserInterestFeedsEntity.UserInterestFeedEntity( - avatarImage = feed.avatarImage, - feedContent = feed.feedContent, - nickname = feed.nickname, - novelId = feed.novelId, - novelImage = feed.novelImage, - novelRating = feed.novelRating, - novelRatingCount = feed.novelRatingCount, - novelTitle = feed.novelTitle, - ) - }, - message = message, - ) diff --git a/data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt b/data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt deleted file mode 100644 index f34c9967d..000000000 --- a/data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.into.websoso.data.feed.model - -data class UserInterestFeedsEntity( - val userInterestFeeds: List, - val message: String, -) { - data class UserInterestFeedEntity( - val avatarImage: String, - val feedContent: String, - val nickname: String, - val novelId: Int, - val novelImage: String, - val novelRating: Double, - val novelRatingCount: Int, - val novelTitle: String, - ) -} - -enum class UserInterestFeedMessage( - val message: String, -) { - NO_INTEREST_NOVELS("NO_INTEREST_NOVELS"), - NO_ASSOCIATED_FEEDS("NO_ASSOCIATED_FEEDS"), - ; - - companion object { - fun fromMessage(message: String): UserInterestFeedMessage? = entries.find { it.message == message } - } -} From eb0bee1624fccf492cdb1be44faf8afe568011a2 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 1 May 2026 14:33:50 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20=EC=84=9C=EC=9E=AC=EC=97=90=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_plus_novel.xml | 9 ++++++ .../websoso/feature/library/LibraryScreen.kt | 2 +- .../library/component/LibraryTopBar.kt | 32 +++++++++++-------- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 core/resource/src/main/res/drawable/ic_plus_novel.xml diff --git a/core/resource/src/main/res/drawable/ic_plus_novel.xml b/core/resource/src/main/res/drawable/ic_plus_novel.xml new file mode 100644 index 000000000..31e4fab63 --- /dev/null +++ b/core/resource/src/main/res/drawable/ic_plus_novel.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt b/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt index 81b8eb420..543452cb5 100644 --- a/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt +++ b/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt @@ -101,7 +101,7 @@ fun LibraryScreen( onSortClick = libraryViewModel::updateSortType, onToggleViewType = libraryViewModel::updateViewType, onItemClick = { navigateToNovelDetailActivity(it.novelId) }, - onSearchClick = { /* TODO */ }, + onSearchClick = navigateToNormalExploreActivity, onExploreClick = navigateToNormalExploreActivity, onInterestClick = libraryViewModel::updateInterestedNovels, onAttractivePointClick = libraryViewModel::updateAttractivePoints, diff --git a/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt b/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt index b66081877..40f36fc25 100644 --- a/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt +++ b/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt @@ -1,23 +1,30 @@ package com.into.websoso.feature.library.component +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp +import com.into.websoso.core.common.extensions.debouncedClickable import com.into.websoso.core.designsystem.theme.Black import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.core.resource.R.drawable.ic_plus_novel @Composable internal fun LibraryTopBar(onSearchClick: () -> Unit = {}) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp), + .padding(start = 20.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -26,17 +33,16 @@ internal fun LibraryTopBar(onSearchClick: () -> Unit = {}) { style = WebsosoTheme.typography.headline1, color = Black, ) - /* - TODO: 추후 검색 기능 구현 시 롤백 - IconButton(onClick = onSearchClick) { - Image( - imageVector = ImageVector.vectorResource(id = ic_common_search), - contentDescription = "검색", - modifier = Modifier.size(24.dp), - ) - } - } - - */ + Box( + modifier = Modifier + .debouncedClickable(onClick = onSearchClick) + .padding(horizontal = 20.dp, vertical = 8.dp), + ) { + Image( + imageVector = ImageVector.vectorResource(id = ic_plus_novel), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } } } From 3e48a289af29ee62403855bf7381cc8e7c2064cc Mon Sep 17 00:00:00 2001 From: m6z1 Date: Fri, 8 May 2026 19:10:28 +0900 Subject: [PATCH 12/13] =?UTF-8?q?fix:=20PR=20ktlint=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - final-newline: 10개 파일에 누락된 개행 추가 - blank-line-between-when-conditions: DetailExploreScreen when 조건 사이 빈 줄 추가 - function-signature: updateSelectedRatingRange 파라미터 멀티라인 포맷 - import-ordering: DetailExploreAppBar, DetailExploreKeywordTab import 순서 수정 - no-blank-line-in-list: DetailExploreInfoTab StatusSection 호출 내 불필요한 빈 줄 제거 - if-else-wrapping/multiline-if-else: RatingRangeSlider if-else 중괄호 및 개행 추가 - backing-property-naming: DetailExploreResultViewModel 미노출 backing property 이름 변경 Co-Authored-By: Claude Sonnet 4.6 --- .../ui/detailExplore/DetailExploreActivity.kt | 2 +- .../ui/detailExplore/DetailExploreScreen.kt | 1 + .../detailExplore/DetailExploreViewModel.kt | 7 +++- .../component/DetailExploreAppBar.kt | 4 +- .../component/DetailExploreCtaButton.kt | 2 +- .../component/DetailExploreInfoTab.kt | 1 - .../component/DetailExploreKeywordTab.kt | 4 +- .../component/RatingRangeSlider.kt | 13 ++++-- .../detailExplore/component/SelectableChip.kt | 2 +- .../DetailExploreResultViewModel.kt | 40 +++++++++---------- .../into/websoso/ui/main/home/HomeFragment.kt | 2 +- .../websoso/ui/main/home/HomeViewModel.kt | 2 +- .../websoso/ui/main/home/model/HomeUiState.kt | 2 +- 13 files changed, 45 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt index fcf83343d..1a55a3e19 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt @@ -69,4 +69,4 @@ class DetailExploreActivity : AppCompatActivity() { companion object { fun getIntent(context: Context): Intent = Intent(context, DetailExploreActivity::class.java) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt index 528f37091..cc7e7172b 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt @@ -63,6 +63,7 @@ fun DetailExploreScreen( Box(modifier = Modifier.weight(1f)) { when (selectedTab) { INFO -> DetailExploreInfoTab(viewModel = viewModel) + KEYWORD -> DetailExploreKeywordTab( viewModel = viewModel, onKeywordInquireClick = onKeywordInquireClick, diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt index 3f46954dc..cb2945b2e 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt @@ -103,7 +103,10 @@ class DetailExploreViewModel _selectedStatus.value = status } - fun updateSelectedRatingRange(min: Float, max: Float) { + fun updateSelectedRatingRange( + min: Float, + max: Float, + ) { _selectedRatingMin.value = min _selectedRatingMax.value = max } @@ -206,4 +209,4 @@ class DetailExploreViewModel const val RATING_MAX = 5.0f const val RATING_STEP = 0.5f } - } \ No newline at end of file + } diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt index 68b8e33f4..fd86ffaa4 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt @@ -14,11 +14,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.ui.graphics.Color import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp @@ -151,4 +151,4 @@ private fun TabLabel( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt index 48ac7a9d0..13cbba0ca 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt @@ -49,4 +49,4 @@ fun DetailExploreCtaButton( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt index 7606c3996..a39ed8ad4 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt @@ -66,7 +66,6 @@ fun DetailExploreInfoTab( onGenreClick = viewModel::updateSelectedGenres, ) StatusSection( - selectedStatus = selectedStatus, onStatusClick = { status -> viewModel.updateSelectedSeriesStatus( diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt index 3fbd832a6..1a5d7261a 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -67,7 +68,6 @@ import com.into.websoso.core.resource.R.string.detail_explore_keyword_search_res import com.into.websoso.core.resource.R.string.detail_explore_search_hint import com.into.websoso.ui.detailExplore.DetailExploreViewModel import com.into.websoso.ui.detailExplore.keyword.model.DetailExploreKeywordUiState -import androidx.compose.ui.platform.LocalContext @Composable fun DetailExploreKeywordTab( @@ -428,4 +428,4 @@ private fun KeywordEmptyResult(onInquireClick: () -> Unit) { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt index 0a07aeadf..240115325 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt @@ -84,9 +84,14 @@ fun RatingRangeSlider( detectDragGestures( onDragStart = { offset -> val touchX = offset.x - activeThumb = if ((touchX - latestStartCenter).absoluteValue <= - (touchX - latestEndCenter).absoluteValue - ) 1 else 2 + activeThumb = + if ((touchX - latestStartCenter).absoluteValue <= + (touchX - latestEndCenter).absoluteValue + ) { + 1 + } else { + 2 + } val newValue = valueAtX(touchX) if (activeThumb == 1) { onValueChange(newValue.coerceAtMost(latestMax), latestMax) @@ -155,4 +160,4 @@ private fun ThumbCircle(offsetXDp: androidx.compose.ui.unit.Dp) { .size(16.dp) .background(White, CircleShape), ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt index cae6692ff..6ba028449 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt @@ -87,4 +87,4 @@ private fun SelectableChipBase( color = textColor, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt index 820e9957a..f8ae543a9 100644 --- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt @@ -24,10 +24,10 @@ class DetailExploreResultViewModel MutableLiveData(DetailExploreResultUiState()) val uiState: LiveData get() = _uiState - private val _selectedGenres: MutableLiveData?> = MutableLiveData(emptyList()) - private val _isNovelCompleted: MutableLiveData = MutableLiveData() - private val _selectedRating: MutableLiveData = MutableLiveData() - private val _selectedKeywordIds: MutableLiveData?> = MutableLiveData(emptyList()) + private val filterGenres: MutableLiveData?> = MutableLiveData(emptyList()) + private val filterIsNovelCompleted: MutableLiveData = MutableLiveData() + private val filterRating: MutableLiveData = MutableLiveData() + private val filterKeywordIds: MutableLiveData?> = MutableLiveData(emptyList()) private val _appliedFiltersMessage: MediatorLiveData = MediatorLiveData() val appliedFiltersMessage: LiveData get() = _appliedFiltersMessage @@ -37,29 +37,29 @@ class DetailExploreResultViewModel init { _appliedFiltersMessage.apply { - addSource(_selectedGenres) { updateMessage() } - addSource(_isNovelCompleted) { updateMessage() } - addSource(_selectedRating) { updateMessage() } - addSource(_selectedKeywordIds) { updateMessage() } + addSource(filterGenres) { updateMessage() } + addSource(filterIsNovelCompleted) { updateMessage() } + addSource(filterRating) { updateMessage() } + addSource(filterKeywordIds) { updateMessage() } } } private fun updateMessage() { val appliedFilters = mutableListOf() - if (_selectedGenres.value?.isNotEmpty() == true) appliedFilters.add(GENRES_LABEL) - if (_isNovelCompleted.value != null) appliedFilters.add(NOVEL_COMPLETED_LABEL) - if (_selectedRating.value != null) appliedFilters.add(RATING_LABEL) - if (_selectedKeywordIds.value?.isNotEmpty() == true) appliedFilters.add(KEYWORDS_LABEL) + if (filterGenres.value?.isNotEmpty() == true) appliedFilters.add(GENRES_LABEL) + if (filterIsNovelCompleted.value != null) appliedFilters.add(NOVEL_COMPLETED_LABEL) + if (filterRating.value != null) appliedFilters.add(RATING_LABEL) + if (filterKeywordIds.value?.isNotEmpty() == true) appliedFilters.add(KEYWORDS_LABEL) _appliedFiltersMessage.value = appliedFilters.joinToString(FILTER_SEPARATOR) } fun updatePreviousSearchFilteredValue(detailExploreFilteredModel: DetailExploreFilteredModel) { - _selectedGenres.value = detailExploreFilteredModel.genres - _isNovelCompleted.value = detailExploreFilteredModel.isCompleted - _selectedRating.value = detailExploreFilteredModel.novelRating - _selectedKeywordIds.value = detailExploreFilteredModel.keywordIds + filterGenres.value = detailExploreFilteredModel.genres + filterIsNovelCompleted.value = detailExploreFilteredModel.isCompleted + filterRating.value = detailExploreFilteredModel.novelRating + filterKeywordIds.value = detailExploreFilteredModel.keywordIds updateSearchResult(true) } @@ -70,10 +70,10 @@ class DetailExploreResultViewModel viewModelScope.launch { runCatching { getDetailExploreResultUseCase( - genres = _selectedGenres.value?.map { it.titleEn }, - isCompleted = _isNovelCompleted.value, - novelRating = _selectedRating.value, - keywordIds = _selectedKeywordIds.value, + genres = filterGenres.value?.map { it.titleEn }, + isCompleted = filterIsNovelCompleted.value, + novelRating = filterRating.value, + keywordIds = filterKeywordIds.value, isSearchButtonClick = isSearchButtonClick, ) }.onSuccess { results -> diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt index ca8497b07..84c069b84 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt @@ -294,4 +294,4 @@ class HomeFragment : BaseFragment(fragment_home) { private const val TODAY_POPULAR_NOVEL_MARGIN = 15 const val TAG = "HomeFragment" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index b7ec03296..c17f6ffe0 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -251,4 +251,4 @@ class HomeViewModel } } } - } \ No newline at end of file + } diff --git a/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt b/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt index 98ff109be..e3a5d451f 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt @@ -11,4 +11,4 @@ data class HomeUiState( val popularNovels: List = listOf(), val popularFeeds: List> = listOf(), val recommendedNovelsByUserTaste: List = listOf(), -) \ No newline at end of file +) From 45fc7d7a54d16df2f047b0a410deaf5ea918423e Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Thu, 21 May 2026 10:15:41 +0900 Subject: [PATCH 13/13] =?UTF-8?q?feat:=201.7.0=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26e57e574..d952d5fab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # App Versioning -versionCode = "10050" -versionName = "1.6.3" +versionCode = "10051" +versionName = "1.7.0" # Gradle Plugin & Kotlin android-gradle-plugin = "8.13.2"