From f7d9d375b19e0ad78fde7cb3c54d8ba6af4be1cc Mon Sep 17 00:00:00 2001 From: "Lina.L" Date: Sun, 30 Jun 2024 03:30:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 검색 화면 구현 * refactor: 컴포넌트 분리 * fix: 파일 위치 수정 --- .../DesignSystem/Sources/BasicModal.swift | 75 +++++++++++++ .../Sources/CategoryTagView.swift | 87 +++++++++++++++ .../Sources/HorizontalMimScrollView.swift | 38 +++++++ .../Sources/MimCategoryView.swift | 38 +++++++ .../Sources/Search/HotKeyword.swift | 48 +++++++++ .../Sources/Search/MimCategory.swift | 33 ++++++ Projects/Features/Search/Project.swift | 6 +- .../Search/Sources/View/FakeSearchBar.swift | 40 +++++++ .../Sources/View/HotKeywordImageView.swift | 57 ++++++++++ .../Sources/View/SearchPreparingAlert.swift | 67 ++++++++++++ .../Search/Sources/View/SearchView.swift | 99 +++++++++++++++++- .../Search.imageset/Contents.json | 15 +-- .../Icon.xcassets/Search.imageset/Search.png | Bin 649 -> 0 bytes .../Icon.xcassets/Search.imageset/Search.svg | 4 + .../Search.imageset/Search@2x.png | Bin 1204 -> 0 bytes .../Search.imageset/Search@3x.png | Bin 1654 -> 0 bytes 16 files changed, 588 insertions(+), 19 deletions(-) create mode 100644 Projects/Core/DesignSystem/Sources/BasicModal.swift create mode 100644 Projects/Core/DesignSystem/Sources/CategoryTagView.swift create mode 100644 Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift create mode 100644 Projects/Core/DesignSystem/Sources/MimCategoryView.swift create mode 100644 Projects/Core/PPACModels/Sources/Search/HotKeyword.swift create mode 100644 Projects/Core/PPACModels/Sources/Search/MimCategory.swift create mode 100644 Projects/Features/Search/Sources/View/FakeSearchBar.swift create mode 100644 Projects/Features/Search/Sources/View/HotKeywordImageView.swift create mode 100644 Projects/Features/Search/Sources/View/SearchPreparingAlert.swift delete mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.png create mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.svg delete mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@2x.png delete mode 100644 Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@3x.png diff --git a/Projects/Core/DesignSystem/Sources/BasicModal.swift b/Projects/Core/DesignSystem/Sources/BasicModal.swift new file mode 100644 index 0000000..f4c652e --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/BasicModal.swift @@ -0,0 +1,75 @@ +// +// BasicModal.swift +// DesignSystem +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import ResourceKit + +// Thanks to 균 +public struct BasicModal: View { + @Binding var isPresented: Bool + private let content: () -> Content + private var opacity: Double + + public init( + isPresented: Binding, + opacity: Double, + content: @escaping () -> Content + ) { + self._isPresented = isPresented + self.opacity = opacity + self.content = content + } + + public var body: some View { + GeometryReader { geometry in + ZStack { + if isPresented { + Color.black.opacity(opacity) + .onTapGesture { + self.isPresented.toggle() + } + .transition(.opacity) + + content() + .padding(.horizontal, 40) + } + } + .edgesIgnoringSafeArea(.all) + } + } +} + +extension View { + public func basicModal( + isPresented: Binding, + opacity: Double, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + modifier( + BasicModalModifier( + content: { + BasicModal( + isPresented: isPresented, + opacity: opacity, + content: content + ) + } + ) + ) + } +} + +fileprivate struct BasicModalModifier: ViewModifier where SheetContent: View { + var content: () -> BasicModal + + func body(content: Content) -> some View { + ZStack { + content + self.content() + } + } +} diff --git a/Projects/Core/DesignSystem/Sources/CategoryTagView.swift b/Projects/Core/DesignSystem/Sources/CategoryTagView.swift new file mode 100644 index 0000000..3f75de6 --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/CategoryTagView.swift @@ -0,0 +1,87 @@ +// +// CategoryTagView.swift +// DesignSystem +// +// Created by 리나 on 2024/06/29. +// + +import SwiftUI +import ResourceKit + +// thanks to NamS +public struct CategoryTagView: View { + @State public var categories: [String] + + public init(categories: [String]) { + self.categories = categories + } + + public var body: some View { + ScrollView { + CategoryTagLayout(verticalSpacing: 8, horizontalSpacing: 8) { + ForEach(categories, id: \.self) { category in + Text(category) + .font(Font.Body.Medium.medium) + .foregroundColor(Color.Text.primary) + .padding(.horizontal, 16) + .padding(.vertical, 9.5) + .background( + Capsule().foregroundStyle(Color.Background.assistive) + ) + } + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let cacheValue = categories + categories = [] + categories = cacheValue + } + } + } +} + +struct CategoryTagLayout: Layout { + var verticalSpacing: CGFloat = 0 + var horizontalSpacing: CGFloat = 0 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize { + CGSize(width: proposal.width ?? 0, height: cache.height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) { + var sumX: CGFloat = bounds.minX + var sumY: CGFloat = bounds.minY + + for index in subviews.indices { + let view = subviews[index] + let viewSize = view.sizeThatFits(.unspecified) + guard let proposalWidth = proposal.width else { continue } + + if (sumX + viewSize.width > proposalWidth) { + sumX = bounds.minX + sumY += viewSize.height + sumY += verticalSpacing + } + + let point = CGPoint(x: sumX, y: sumY) + view.place(at: point, anchor: .topLeading, proposal: proposal) + sumX += viewSize.width + sumX += horizontalSpacing + } + + if let firstViewSize = subviews.first?.sizeThatFits(.unspecified) { + cache.height = sumY + firstViewSize.height + } + } + + struct Cache { + var height: CGFloat + } + + func makeCache(subviews: Subviews) -> Cache { + return Cache(height: 0) + } + + func updateCache(_ cache: inout Cache, subviews: Subviews) { } +} diff --git a/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift b/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift new file mode 100644 index 0000000..925d1f9 --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift @@ -0,0 +1,38 @@ +// +// HorizontalMimScrollView.swift +// DesignSystem +// +// Created by 리나 on 2024/06/29. +// + +import SwiftUI +import ResourceKit + +public protocol HorizontalMimItemProtocol: Hashable { } + +public protocol HorizontalMimItemViewProtocol: View { + associatedtype Item: HorizontalMimItemProtocol + init(item: Item) +} + +public struct HorizontalMimScrollView: View where ItemView.Item == Item { + @Binding public var items: [Item] + + public init(items: Binding<[Item]>) { + self._items = items + } + + public var body: some View { + ScrollView(.horizontal) { + LazyHStack(spacing: 10) { + ForEach(items, id: \.self) { item in + ItemView(item: item) + } + .listStyle(.plain) + } + } + .contentMargins(20) + .scrollIndicators(.hidden) + .frame(height: 90) + } +} diff --git a/Projects/Core/DesignSystem/Sources/MimCategoryView.swift b/Projects/Core/DesignSystem/Sources/MimCategoryView.swift new file mode 100644 index 0000000..4dcad2b --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/MimCategoryView.swift @@ -0,0 +1,38 @@ +// +// MimCategoryView.swift +// DesignSystem +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import ResourceKit + +public struct MimCategoryView: View { + public let title: String + public let categories: [String] + + public init(title: String, categories: [String]) { + self.title = title + self.categories = categories + } + + public var body: some View { + VStack(spacing: 0) { + HStack { + Text(title) + .font(Font.Body.Small.semiBold) + .foregroundColor(Color.Text.tertiary) + + Spacer() + } + .padding(.top, 4) + .padding(.bottom, 16) + .padding(.horizontal, 20) + + CategoryTagView(categories: categories) + .padding(.horizontal, 20) + .padding(.bottom, 20) + } + } +} diff --git a/Projects/Core/PPACModels/Sources/Search/HotKeyword.swift b/Projects/Core/PPACModels/Sources/Search/HotKeyword.swift new file mode 100644 index 0000000..65f54bc --- /dev/null +++ b/Projects/Core/PPACModels/Sources/Search/HotKeyword.swift @@ -0,0 +1,48 @@ +// +// HotKeyword.swift +// PPACModels +// +// Created by 리나 on 2024/06/29. +// + +import Foundation + +public struct HotKeyword: Hashable { + + // MARK: - Properties + + public let title: String + public let imageUrlString: String + + // MARK: - Initializers + + public init( + title: String, + imageUrlString: String + ) { + self.title = title + self.imageUrlString = imageUrlString + } +} + +public extension HotKeyword { + static let mock1 = HotKeyword( + title: "띠용오오우어어우어엉?", + imageUrlString: "https://www.blockmedia.co.kr/wp-content/uploads/2024/05/%EB%8F%84%EC%A7%80%EC%BD%94%EC%9D%B8Doge-%EC%B9%B4%EB%B6%80%EC%88%98.png" + ) + + static let mock2 = HotKeyword( + title: "짜란다짜란다", + imageUrlString: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR_xO_sgszdvyVBp8xz4B82kHmyyO0SNZO-4A&s" + ) + + static let mock3 = HotKeyword( + title: "후회", + imageUrlString: "https://s3.ap-northeast-2.amazonaws.com/univ-careet/FileData/Picture/202310/3e7e0445-3812-4c06-bc75-a1bed40a3332_770x426.png" + ) + + static let mock4 = HotKeyword( + title: "무한도전", + imageUrlString: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQBnKb96Uc-894Fhgt_5xLsCvY0pSkCl0B4TA&s" + ) +} diff --git a/Projects/Core/PPACModels/Sources/Search/MimCategory.swift b/Projects/Core/PPACModels/Sources/Search/MimCategory.swift new file mode 100644 index 0000000..91ca7a6 --- /dev/null +++ b/Projects/Core/PPACModels/Sources/Search/MimCategory.swift @@ -0,0 +1,33 @@ +// +// MimCategory.swift +// PPACModels +// +// Created by 리나 on 2024/06/29. +// + +import Foundation + +public struct MimCategory: Hashable { + public let title: String + public let categories: [String] + + public init(title: String, categories: [String]) { + self.title = title + self.categories = categories + } +} + +public extension MimCategory { + static let mock1 = MimCategory( + title: "감정", + categories: ["행복", "슬픈", "무념무상", "분노", "웃긴", "피곤", "절망", "현타", "당황"] + ) + static let mock2 = MimCategory( + title: "상황", + categories: ["회사", "대학", "공부", "연애", "긁", "다이어트", "주식", "고민", "축하", "칭찬"] + ) + static let mock3 = MimCategory( + title: "컨텐츠", + categories: ["동물", "무한도전", "캐릭터", "짱구", "문상훈", "검정고무신", "그 외"] + ) +} diff --git a/Projects/Features/Search/Project.swift b/Projects/Features/Search/Project.swift index 4007555..17f30fb 100644 --- a/Projects/Features/Search/Project.swift +++ b/Projects/Features/Search/Project.swift @@ -18,7 +18,11 @@ let project = Project( sources: "Sources/**", resources: "Resources/**", dependencies: [ - + .ResourceKit, + .Core.DesignSystem, + .Core.PPACModels, + .ThirdParty.Dependency, + .ThirdParty.Kingfisher, ] ) ] diff --git a/Projects/Features/Search/Sources/View/FakeSearchBar.swift b/Projects/Features/Search/Sources/View/FakeSearchBar.swift new file mode 100644 index 0000000..cb88305 --- /dev/null +++ b/Projects/Features/Search/Sources/View/FakeSearchBar.swift @@ -0,0 +1,40 @@ +// +// FakeSearchBar.swift +// DesignSystem +// +// Created by 리나 on 2024/06/29. +// + +import SwiftUI +import ResourceKit + +struct FakeSearchBar: View { + let placeHolder: String + + init(placeHolder: String) { + self.placeHolder = placeHolder + } + + var body: some View { + fakeTextField + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(Color.Background.assistive) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.horizontal, 20) + .padding(.vertical, 16) + } + + private var fakeTextField: some View { + HStack(spacing: 12) { + ResourceKitAsset.Icon.search.swiftUIImage + + Text(placeHolder) + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.tertiary) + + Spacer() + } + .padding(.horizontal, 16) + } +} diff --git a/Projects/Features/Search/Sources/View/HotKeywordImageView.swift b/Projects/Features/Search/Sources/View/HotKeywordImageView.swift new file mode 100644 index 0000000..e2442e4 --- /dev/null +++ b/Projects/Features/Search/Sources/View/HotKeywordImageView.swift @@ -0,0 +1,57 @@ +// +// HotKeywordImageView.swift +// Search +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import Kingfisher +import PPACModels +import DesignSystem +import ResourceKit + +struct HotKeywordImageView: View, HorizontalMimItemViewProtocol { + typealias Item = HotKeyword + + let hotKeyword: HotKeyword + + init(item hotKeyword: HotKeyword) { + self.hotKeyword = hotKeyword + } + + var body: some View { + ZStack { + imageView + + Color.black.opacity(0.4) + + keywordView + } + .overlay( + RoundedRectangle(cornerRadius: 10) + .inset(by: 1) + .stroke(Color.Border.primary, lineWidth: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + private var imageView: some View { + KFImage(URL(string: hotKeyword.imageUrlString)) + .resizable() + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .frame(width: 90, height: 90) + .aspectRatio(contentMode: .fit) + } + + private var keywordView: some View { + Text(hotKeyword.title) + .font(Font.Body.Large.semiBold) + .foregroundColor(Color.Text.inverse) + .lineLimit(2) + .multilineTextAlignment(.center) + .padding(.horizontal, 16) + .frame(width: 90, height: 90) + } +} diff --git a/Projects/Features/Search/Sources/View/SearchPreparingAlert.swift b/Projects/Features/Search/Sources/View/SearchPreparingAlert.swift new file mode 100644 index 0000000..016e0d0 --- /dev/null +++ b/Projects/Features/Search/Sources/View/SearchPreparingAlert.swift @@ -0,0 +1,67 @@ +// +// SearchPreparingAlert.swift +// Search +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import ResourceKit + +struct SearchPreparingAlert: View { + private var dismiss: (() -> Void) + + init(dismiss: @escaping (() -> Void)) { + self.dismiss = dismiss + } + + var body: some View { + VStack(spacing: 0) { + title + + description + .padding(.top, 8) + + confirmButton + .padding(.top, 14) + } + .padding(.horizontal, 30) + .padding(.vertical, 20) + .background(Color.Background.white) + .cornerRadius(20) + } + + + private var title: some View { + HStack { + Text("조금만 기다려주세요!") + .font(Font.Heading.Medium.semiBold) + .foregroundColor(Color.Text.primary) + .foregroundColor(.black) + Spacer() + } + } + + private var description: some View { + HStack { + Text("검색은 준비 중이에요.") + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.secondary) + .multilineTextAlignment(.leading) + Spacer() + } + } + + private var confirmButton: some View { + HStack{ + Spacer() + Button { + dismiss() + } label: { + Text("확인") + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.brand) + } + } + } +} diff --git a/Projects/Features/Search/Sources/View/SearchView.swift b/Projects/Features/Search/Sources/View/SearchView.swift index 86c3eee..44f8c09 100644 --- a/Projects/Features/Search/Sources/View/SearchView.swift +++ b/Projects/Features/Search/Sources/View/SearchView.swift @@ -6,17 +6,106 @@ // import SwiftUI +import PPACModels +import ResourceKit +import DesignSystem public struct SearchView: View { - public init() { } + @State public var hotKeywords: [HotKeyword] + @State public var mimCategories: [MimCategory] + @State private var isPresenting: Bool = false + + public init(hotKeywords: [HotKeyword] = [], mimCategories: [MimCategory] = []) { + self.hotKeywords = hotKeywords + self.mimCategories = mimCategories + } public var body: some View { - Text("검색 화면") - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(.pink) + VStack(spacing: 0) { + fakeSearchBar + + ScrollView { + VStack(spacing: 0) { + currentHotKeywords + mimCategoriesViews + } + } + .scrollIndicators(.hidden) + } + .padding(.bottom, 40) + .basicModal( + isPresented: $isPresenting, + opacity: 0.5, + content: { + SearchPreparingAlert(dismiss: { isPresenting = false }) + } + ) + } + + private var fakeSearchBar: some View { + Button { + isPresenting = true + } label: { + FakeSearchBar(placeHolder: "🚧 검색은 오픈 준비 중!") + } + .buttonStyle(PlainButtonStyle()) + } + + private var currentHotKeywords: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 8) { + ResourceKitAsset.Icon.special.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + + Text("두둥! 요즘 핫한 #키워드") + .font(Font.Heading.Small.semiBold) + .foregroundColor(Color.Text.primary) + } + .padding(.horizontal, 20) + .padding(.vertical, 18) + + HorizontalMimScrollView(items: $hotKeywords) + } + .padding(.bottom, 40) + } + + private var mimCategoriesViews: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 8) { + ResourceKitAsset.Icon.category.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + + Text("무슨 밈 찾아?") + .font(Font.Heading.Small.semiBold) + .foregroundColor(Color.Text.primary) + } + .padding(.horizontal, 20) + .padding(.vertical, 18) + + ForEach(mimCategories, id: \.self) { mimCategory in + MimCategoryView(title: mimCategory.title, categories: mimCategory.categories) + } + } } } +extension HotKeyword: HorizontalMimItemProtocol {} + #Preview { - SearchView() + SearchView( + hotKeywords: [ + .mock1, + .mock2, + .mock3, + .mock4 + ], mimCategories: [ + .mock1, + .mock2, + .mock3 + ] + ) } diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json index 4e324d2..668818a 100644 --- a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json @@ -1,19 +1,8 @@ { "images" : [ { - "filename" : "Search.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Search@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "Search@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "Search.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.png b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.png deleted file mode 100644 index 2effee2a424f6892b03ed64ac5ed2cdeac5522fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 649 zcmV;40(Sk0P)}BE)@=K#IgAyg_^8NS}c61T9Yh9J#?8q)!ld0^kjDLE09GhMbU4+wHBD z+Hvifo$WTAWOw`LOJ>*K`WxGy9e_JFT;?>!^kAhsbPk6A+!J_C#1s2F`wllD(7=u! zZi#za9(xH$4#;2V0e!GpW}yv%dG>th&Q8aAtblTs+e;Q^4sNNlvSEMyoGgeBlnDN< z430b^!u7w?-TrKvJ(*2+QJ;ZvCw`)M{SN9fFlJyJu@9~{M#7C{wKh)&YwUQ;p@f#) z$qQ?3zAytv@GjoGF1VWd6<20L-R%^PE>huF;lQJ&D)G3oT)gE~mm=`#7HT1+9cW?H zfl+bvl1>tU*LmeuEzFlnWvgKvrwN=NqJW2MSdg$9rb|R6AURY`C`|FYKt(eCkwE(a zEv$-{8JVOBPLC&L9ofBx7Ump8e4gr`C(r?rI)3(V7&n_{5pcZeA7nSBFy$POJN|@4 zlSK{VIui9O&d1hCmc^ICU0Qa!Q&@IOoC&{+WG@T**j#A(O7d=;uWxEvk!~Q@1e77QV>%awfF|O>CY-|1)CW2yVfw%`7@xk17 + + + diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@2x.png b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@2x.png deleted file mode 100644 index e42a2d4deb42c6d235db8dd2e6da7311b0f309c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1204 zcmV;l1WWsgP)1@WJVC+}02M<;`~<{L0C@sZ(&2P;n9eW|E}=C< z=|YCwvMbquPABVROEY06zZuU+r@cSF+r7QrHNXi@FcA!rD zzYl0NgUjQdkC6o8#nT(eFd6O~62OL1xCuWGy7o?r=3x5tqB(+9L|US3(;J%vW-mT6 zn|H3o#|y`{G6(Cv>W?=AR#_|0+!EAP=5V>FIb{;Kyl~?=IBcFfXq-UhLWEeW8QNIi z-Tv?x3cyJ~+$2)uZ7gBx;uyPRL$pikl#ocBD>lbv{i)aS914`Hkr$s*bF}j%EM6dE zp!u;ZW7^*aZrU3u07Gyj z1OY;`loCL-0Tm;<2yGzU1P>$$ki$3g4GUGnHAftt=ORG>)80py>3Omc&w<)%uKDy$ zYQXB`aB&ra?74~)_!nWDP|}@w?m{_0doC~STPB35TQLfWD@Hs=GIwx=87?GEh+r!v z!SOs)4aXep5qH1?2?7c+<<2;`^14tafiaK{9S}O;fusfuZ8~IDsx+fGe4(b@&KcUK zPyYk~39c~BoHMi2Whe8$!TGlAe(-l2+ke+Ld`U;6fR50C9{L7XMG)^kuG#He3~PH1 z;m*jeEZq3wNcl9EKyHKEsB z!hYYgtP<4DoW1abFiMW*9U>u{=>0GZ51;^Y3tJ;fAOg*Cdnd{nMisUU-R0L+$PG?(Ac##d z&;7k`TRzgc1O#H2JmBm4**S?%7Gc6DH7XP+yibHOUeh7rO~Q-75aC|G%Y(KDL;`Rh z#!Rq_fJ`7w!yIBz01hQ6 zQ-tqf#9vI87aD6LioPSy#6$$j#JD61nNXF;H>dOaY))_OFUN*}U^EG6$oaDyk6lu~ zyWLxpTCClkRUtW68W_T*3HDx3#S7Z9$L0{bAYSfh3Yt^MxmJNcJx*|f0{8`TJl)tH SX48cL00005B#;BQ zIEc(-Vh_IC?4G0vAlvk|RoPAru;XV;DD&snXJ+T&>hk>| zP4%!v2TXh#1d@g5q@R+kwdSO zovMBn z%9i>bM!?nOMRR6$0b}l|OnC1hhyR}-BU*t%v*zdkT11@oHjI$2NKa?^eh&3hm1rD6y_vA^{C&6VVoT@j1ly4vzK@d zMI5>W7|1}RKnbj8EaiurcwU=xEzpK^j=nX>Kx7o-r0@@jh+JYlXQxL?P!K-nd)4Gf z9gb*ArV(wZ1wC6?)m8w-dZ0jr8GAHgWfKe;DiD&|&|K5;PTCD95`3J|`Jyb_EW!s- zwBkr(tq~7Hwhs{=XqM>y;93R*3XAOkYta`tx*HiAq_iR5?R~G*3|dg!el2mwp@2BP zJ|_Zr?1BLU0{Il^lv`5hk`H_CecYGs9Js^-njvKX@qgNrVrw>V`G6E#Bv&8~1 zd!r)2M-NS|gO+^Ilv_*%ia`zj0He*6{=eIgZ{E&P!^h}1N>Mx?52VT8Pe}Tr@O$v&T%*)13vfDvmrJs_Hwlv<)&rXKgSS@`G7y)E{R5Q*m}->JKBeg#11da&V2SW;JvRwJMD$l zQHj*GSkw+mNaRxY)n@17BJ*^I<@q3JVhJZ|J}A+YYj>-~j+_WC`A~Z@w{Z2><%PBK zth@aDRe{hokFU6C&!t6 zPoMNtu1l2$JHT&JT+)$dqdb`6lvGPQ<;r=LR13lbF`xOsa4guSNT~%9Qd-hoI_#@% zZ9fVtmFR-Sp>$g!83`mLoE01^HbgRh6hX$|WW)`+QKgWzY67gy&dqzB%lE&4A(^@w z;c+Cub@mt5oMJ-uRDerT2#Z@75H{SE)igfy7RFobF5$Q^!gfjro{De|8)2uP<4B|< z_24aOhAsM-ZZ~TJ_2~`%+yPmi*-{l68S@!lqs8J(%)kk}vQdQWK|x zu;h~9A>TYj%2lA7^dIsCnI0j>y*UFQ%}VLVIm&6l_1~UJfjuvgMkdfG$U~%w5=gKu z(!>cQq!1}~M0g^nJLC&v@Nqpso>T_sQB}8KV#i-nh@@qr=Y!%zlFDP;Q0l}FQ0YWU z^1R7JI_W6n+1G%&L-OiA?iO`ocgmD0Q>G;3UsjC0O5=E<*#H0l07*qoM6N<$g3S&U A!vFvP