diff --git a/HappyAnding/HappyAnding.xcodeproj/project.pbxproj b/HappyAnding/HappyAnding.xcodeproj/project.pbxproj index d0d284bb..dd6e74e5 100644 --- a/HappyAnding/HappyAnding.xcodeproj/project.pbxproj +++ b/HappyAnding/HappyAnding.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ F96D45BB29804057000C2441 /* EnvironmentValues+Alerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96D45BA29804057000C2441 /* EnvironmentValues+Alerter.swift */; }; F96D45BD29816578000C2441 /* StickyHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96D45BC29816578000C2441 /* StickyHeader.swift */; }; F9724BBF292755E400860F8A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9724BBE292755E400860F8A /* Comment.swift */; }; + F975C2172BD96994006CC401 /* Int+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F975C2162BD96994006CC401 /* Int+Extension.swift */; }; F976E82C29368E0D0088BBA1 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = F976E82B29368E0D0088BBA1 /* Version.swift */; }; F976E85129395B350088BBA1 /* ShareExtensionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F976E85029395B350088BBA1 /* ShareExtensionViewModel.swift */; }; F98017182BBC29A7004F2EA7 /* SCZ+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98017172BBC29A7004F2EA7 /* SCZ+Color.swift */; }; @@ -342,6 +343,11 @@ F96D45BA29804057000C2441 /* EnvironmentValues+Alerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+Alerter.swift"; sourceTree = ""; }; F96D45BC29816578000C2441 /* StickyHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyHeader.swift; sourceTree = ""; }; F9724BBE292755E400860F8A /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + F975C2122BD91321006CC401 /* SF-Compact-Rounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Semibold.otf"; sourceTree = ""; }; + F975C2132BD91321006CC401 /* SF-Compact-Rounded-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Medium.otf"; sourceTree = ""; }; + F975C2142BD91322006CC401 /* SF-Compact-Rounded-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Bold.otf"; sourceTree = ""; }; + F975C2152BD91322006CC401 /* SF-Compact-Rounded-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Compact-Rounded-Regular.otf"; sourceTree = ""; }; + F975C2162BD96994006CC401 /* Int+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extension.swift"; sourceTree = ""; }; F976E82B29368E0D0088BBA1 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; F976E85029395B350088BBA1 /* ShareExtensionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionViewModel.swift; sourceTree = ""; }; F98017172BBC29A7004F2EA7 /* SCZ+Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SCZ+Color.swift"; sourceTree = ""; }; @@ -642,6 +648,7 @@ F9AC2BB92935D34C00165820 /* Image+View.swift */, A3439AFA2939B0E80043E273 /* UserDefaults+Extension.swift */, F96D45BA29804057000C2441 /* EnvironmentValues+Alerter.swift */, + F975C2162BD96994006CC401 /* Int+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -722,6 +729,7 @@ A329F70F2BCA4F8F00ED20DA /* Resources */ = { isa = PBXGroup; children = ( + F975C2112BD91309006CC401 /* SFCompactRounded */, A329F7102BCA4FB200ED20DA /* Pretendard */, ); path = Resources; @@ -801,6 +809,17 @@ path = SettingViews; sourceTree = ""; }; + F975C2112BD91309006CC401 /* SFCompactRounded */ = { + isa = PBXGroup; + children = ( + F975C2142BD91322006CC401 /* SF-Compact-Rounded-Bold.otf */, + F975C2132BD91321006CC401 /* SF-Compact-Rounded-Medium.otf */, + F975C2152BD91322006CC401 /* SF-Compact-Rounded-Regular.otf */, + F975C2122BD91321006CC401 /* SF-Compact-Rounded-Semibold.otf */, + ); + path = SFCompactRounded; + sourceTree = ""; + }; F9DB8ECB293B30EC00516CE1 /* Recovered References */ = { isa = PBXGroup; children = ( @@ -937,6 +956,7 @@ packageReferences = ( F94B435B2907B19A00987819 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 4D6A9EFD29A36E9C00D02522 /* XCRemoteSwiftPackageReference "WrappingHStack" */, + F975C2182BDA3A70006CC401 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, ); productRefGroup = 87E99C6B28F94EA6009B691F /* Products */; projectDirPath = ""; @@ -1070,6 +1090,7 @@ 87E99C7028F94EA6009B691F /* ShortcutTabView.swift in Sources */, F99569182901DC4D0060AAEF /* UIFont+Extension.swift in Sources */, F91A72C32999160E00CA135A /* Alerter.swift in Sources */, + F975C2172BD96994006CC401 /* Int+Extension.swift in Sources */, A3D348552BD1233000DE814C /* View+Font.swift in Sources */, 87E99CAD28FFF261009B691F /* ReadShortcutView.swift in Sources */, F930E0002BBD51EC003C2686 /* Seal.swift in Sources */, @@ -1610,6 +1631,14 @@ minimumVersion = 10.0.0; }; }; + F975C2182BDA3A70006CC401 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/swiftui-introspect"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.3; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ diff --git a/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/Contents.json b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/Contents.json new file mode 100644 index 00000000..29ca914c --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "profileIcon.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/profileIcon.svg b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/profileIcon.svg new file mode 100644 index 00000000..a85dd99d --- /dev/null +++ b/HappyAnding/HappyAnding/Assets.xcassets/profileIcon.imageset/profileIcon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HappyAnding/HappyAnding/Extensions/Int+Extension.swift b/HappyAnding/HappyAnding/Extensions/Int+Extension.swift new file mode 100644 index 00000000..1c943226 --- /dev/null +++ b/HappyAnding/HappyAnding/Extensions/Int+Extension.swift @@ -0,0 +1,17 @@ +// +// Int+Extension.swift +// HappyAnding +// +// Created by JeonJimin on 4/25/24. +// + +import Foundation + +extension Int { + /// Int 타입의 숫자를 받아서 천 단위로 콤마를 추가한 문자열을 반환 + func formatNumber() -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter.string(from: NSNumber(value: self)) ?? "" + } +} diff --git a/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift b/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift index 7ba67af5..f4abdd16 100644 --- a/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift +++ b/HappyAnding/HappyAnding/Extensions/SCZ+Color.swift @@ -582,4 +582,12 @@ extension SCZColor { static let opacity16 = Color(hexString: "DEE7FF") static let opacity08 = Color(hexString: "EFF3FF") } + + struct SCZRed { + static let dangerouslyRed = Color(hexString: "DD0008") + static let red = Color(hexString: "FF453A") + } + + static let systemWhite = Color(hexString: "FFFFFF") + static let systemBlack = Color(hexString: "000000") } diff --git a/HappyAnding/HappyAnding/Extensions/String/String+Date.swift b/HappyAnding/HappyAnding/Extensions/String/String+Date.swift index 3e12ed5a..b7f78ebd 100644 --- a/HappyAnding/HappyAnding/Extensions/String/String+Date.swift +++ b/HappyAnding/HappyAnding/Extensions/String/String+Date.swift @@ -65,4 +65,39 @@ extension String { return nil } } + + func getCommentDateFormat() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMddHHmmss" + + let commentDateFormatter = DateFormatter() + commentDateFormatter.dateFormat = "yyyy.MM.dd" + + guard let date = dateFormatter.date(from: self) else { + return "" + } + + let calendar = Calendar.current + let currentDate = Date() + + let difference = calendar.dateComponents([.year, .month, .weekOfYear, .day, .hour, .minute], from: date, to: currentDate) + + if let years = difference.year, years > 0 { + return commentDateFormatter.string(from: date) + } else if let months = difference.month, months >= 11 { + return commentDateFormatter.string(from: date) + } else if let months = difference.month, months > 0 { + return "\(months)개월 전" + } else if let weeks = difference.weekOfYear, weeks > 0 { + return "\(weeks)주 전" + } else if let days = difference.day, days > 0 { + return "\(days)일 전" + } else if let hours = difference.hour, hours > 0 { + return "\(hours)시간 전" + } else if let minutes = difference.minute, minutes > 0 { + return "\(minutes)분 전" + } else { + return "방금 전" + } + } } diff --git a/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift b/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift index e523ed04..f6121a9c 100644 --- a/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift +++ b/HappyAnding/HappyAnding/Extensions/View/View+Shape.swift @@ -31,13 +31,11 @@ extension View { .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) } - func roundedBackground() -> some View { + func roundedBackground(background: Color) -> some View { self - .font(.system(size: 14, weight: .regular)) - .foregroundStyle(SCZColor.CharcoalGray.opacity64) .padding(.horizontal, 10) .padding(.vertical, 8) - .background(SCZColor.CharcoalGray.opacity04) + .background(background) .roundedBorder(cornerRadius: 16, color: Color.white.opacity(0.12), isNormalBlend: true) } } @@ -56,3 +54,16 @@ struct RoundedCorner: Shape { return Path(path.cgPath) } } + +//MARK: - 삼각형 +/// 삼각형 모양을 그리는 경우에 사용합니다. +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + Path { path in + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.closeSubpath() + } + } +} diff --git a/HappyAnding/HappyAnding/TextLiteral.swift b/HappyAnding/HappyAnding/TextLiteral.swift index fc358608..0a9d8f7e 100644 --- a/HappyAnding/HappyAnding/TextLiteral.swift +++ b/HappyAnding/HappyAnding/TextLiteral.swift @@ -20,11 +20,12 @@ enum TextLiteral { static let confirm: String = "확인" static let close: String = "닫기" static let done: String = "완료" - static let edit: String = "편집" + static let edit: String = "수정하기" static let update: String = "업데이트" static let later: String = "나중에" - static let share: String = "공유" - static let delete: String = "삭제" + static let share: String = "공유하기" + static let delete: String = "삭제하기" + static let report: String = "신고하기" static let withdrawnUser: String = "탈퇴한 사용자" static let defaultUser: String = "user" @@ -53,7 +54,6 @@ enum TextLiteral { // MARK: - RecentRegisteredView static let recentRegisteredViewTitle: String = "최신 단축어" - static let newShortcutsTitle: String = "새로 올라온" // MARK: - LovedShortcutView static let lovedShortcutViewTitle: String = "사랑받는" @@ -117,35 +117,27 @@ enum TextLiteral { static let categoryModalViewTitle: String = "카테고리" // MARK: - ReadShortcutView - static let readShortcutViewBasicTabTitle: String = "기본 정보" - static let readShortcutViewVersionTabTitle: String = "버전 정보" - static let readShortcutViewCommentTabTitle: String = "댓글" + static let readShortcutViewVersionTitle: String = "버전 업데이트 정보" + static let readShortcutViewCommentTitle: String = "댓글" static let readShortcutViewDeletionTitle: String = "단축어 삭제" static let readShortcutViewDeletionMessage: String = "단축어를 삭제하시겠어요?" static let readShortcutViewDeletionMessageDowngrade: String = "단축어를 삭제하시겠어요? \n이 글을 삭제하면 등급이 내려가요." - static let readShortcutViewDeleteFixesTitle: String = "수정사항 삭제" - static let readShortcutViewDeleteFixes: String = "수정사항을 삭제하시겠어요?" - static let readShortcutViewKeepFixes: String = "계속 작성" + static let readShortcutViewFilterNew: String = "최신" + static let readShortcutViewFilterAll: String = "전체" static let readShortcutViewCommentDescriptionBeforeLogin: String = "로그인 후 댓글을 작성할 수 있어요" - static let readShortcutViewCommentDescription: String = "댓글을 입력하세요" - - // MARK: - ReadShortcutContentView - static let readShortcutContentViewDescription: String = "단축어 설명" - static let readShortcutContentViewPostedDate: String = "작성 날짜" + static let readShortcutViewCommentDescription: String = "댓글 남기기" + static let readShortcutViewShortcutHeart: String = "하트를 날려 감사를 표했어요" static let readShortcutContentViewCategory: String = "카테고리" - static let readShortcutContentViewRequiredApps: String = "단축어 사용에 필요한 앱" - static let readShortcutContentViewRequirements: String = "단축어 사용을 위한 요구사항" - - // MARK: - ReadShortcutVersionView - static let readShortcutVersionViewNoUpdates: String = "최신 버전의 단축어에요" - static let readShortcutVersionViewUpdateContent: String = "업데이트 내용" - static let readShortcutVersionViewDownloadPreviousVersion: String = "이전 버전 다운로드" + static let readShortcutContentViewRequiredApps: String = "필요한 앱" + //추후 사용안하는 경우 삭제 + static let readShortcutViewDeleteFixesTitle: String = "수정사항 삭제" + static let readShortcutViewDeleteFixes: String = "수정사항을 삭제하시겠어요?" + static let readShortcutViewKeepFixes: String = "계속 작성" + // MARK: - ReadShortcutCommentView - static let readShortcutCommentViewNoComments: String = "등록된 댓글이 없어요" static let readShortcutCommentViewDeletionTitle: String = "댓글 삭제" static let readShortcutCommentViewDeletionMessage: String = "답글도 함께 삭제돼요. 댓글을 삭제하시겠어요?" - static let readShortcutCommentViewReply: String = "답글" static let readShortcutCommentViewEdit: String = "수정" // MARK: - UpdateShortcutView @@ -243,9 +235,6 @@ enum TextLiteral { // MARK: - SearchView static let searchViewPrompt: String = "제목 또는 관련앱으로 검색하세요" - static let searchViewRecommendedKeyword: String = "추천 검색어" - static let searchViewProposal: String = "단축어 제안하기" - static let searchViewProposalURL: String = "https://docs.google.com/forms/d/e/1FAIpQLScQc3KeYjDGCE-C2YRU-Hwy2XNy5bt89KVX1OMUzRiySaMX1Q/viewform" static let searchViewMoreResult: String = "더 많은 검색 결과 보기" static let searchViewRelatedShortcut: String = "관련된 단축어" static let searchVIewRelatedPost: String = "관련된 글" diff --git a/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift b/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift index 017dcf0b..5a38c809 100644 --- a/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift +++ b/HappyAnding/HappyAnding/ViewModel/ReadShortcutViewModels/ReadShortcutViewModel.swift @@ -47,12 +47,13 @@ final class ReadShortcutViewModel: ObservableObject { @Published var isLinkValid = false @Published var isDescriptionValid = false + @Published var isVersionFolded = true + var isUpdateValid: Bool { isLinkValid && isDescriptionValid } init(data: Shortcuts) { - print("*****init") self.author = User() self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: data.id) ?? data self.isMyLike = shortcutsZipViewModel.checkLikedShortrcut(shortcutID: data.id) @@ -72,16 +73,6 @@ final class ReadShortcutViewModel: ObservableObject { } } - func moveTab(to tab: Int) { - self.currentTab = tab - } - - func setReply(to comment: Comment) { - self.nestedCommentTarget = comment.user_nickname - self.comment.bundle_id = comment.bundle_id - self.comment.depth = 1 - } - func checkIfDownloaded() { if (shortcutsZipViewModel.userInfo?.downloadedShortcuts.firstIndex(where: { $0.id == shortcut.id })) == nil { shortcut.numberOfDownload += 1 @@ -94,10 +85,41 @@ final class ReadShortcutViewModel: ObservableObject { func onViewDisappear() { if isMyLike != isMyFirstLike { - shortcutsZipViewModel.updateNumberOfLike(isMyLike: isMyLike, shortcut: shortcut) + } } + func toggleIsMyLike() { + isMyLike.toggle() + shortcutsZipViewModel.updateNumberOfLike(isMyLike: isMyLike, shortcut: shortcut) + } + func checkAuthor() -> Bool { + return self.shortcut.author == shortcutsZipViewModel.currentUser() + } + + func getUrl() -> URL? { + if let url = URL(string: shortcut.downloadLink[0]) { + checkIfDownloaded() + isDownloadingShortcut = true + return url + } + return nil + } + + + //MARK: - Shortcut + + func refreshShortcut() { + self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut + } + + func updateShortcut() { + shortcutsZipViewModel.updateShortcutVersion(shortcut: shortcut, + updateDescription: updateDescription, + updateLink: updatedLink) + self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut + isUpdatingShortcut.toggle() + } func deleteShortcut() { shortcutsZipViewModel.deleteShortcutIDInUser(shortcutID: shortcut.id) shortcutsZipViewModel.deleteShortcutInCuration(curationsIDs: shortcut.curationIDs, shortcutID: shortcut.id) @@ -105,13 +127,27 @@ final class ReadShortcutViewModel: ObservableObject { shortcutsZipViewModel.shortcutsMadeByUser = shortcutsZipViewModel.shortcutsMadeByUser.filter { $0.id != shortcut.id } shortcutsZipViewModel.updateShortcutGrade() } + func shareShortcut() { + guard let deepLink = URL(string: "ShortcutsZip://myPage/detailView?shortcutID=\(shortcut.id)") else { return } + let activityVC = UIActivityViewController(activityItems: [deepLink], applicationActivities: nil) + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + guard let window = windowScene?.windows.first else { return } + window.rootViewController?.present(activityVC, animated: true, completion: nil) + } - func cancelEditingComment() { - self.isEditingComment.toggle() - self.comment = self.comment.resetComment() - self.commentText = "" + //MARK: - UserGrading + func checkDownGrading() { + isDeletingShortcut.toggle() + isDowngradingUserLevel = shortcutsZipViewModel.isShortcutDowngrade() } + func fetchUserGrade(id: String) -> Image { + shortcutsZipViewModel.fetchShortcutGradeImage(isBig: false, shortcutGrade: shortcutsZipViewModel.checkShortcutGrade(userID: id)) + } +} + +//MARK: - Comment +extension ReadShortcutViewModel { func postComment() { if !isEditingComment { comment.contents = commentText @@ -131,34 +167,10 @@ final class ReadShortcutViewModel: ObservableObject { self.comments.comments = comments.fetchSortedComment() } - func cancelNestedComment() { - comment.bundle_id = "\(Date().getDate())_\(UUID().uuidString)" - comment.depth = 0 - } - - func checkAuthor() -> Bool { - return self.shortcut.author == shortcutsZipViewModel.currentUser() - } - - func shareShortcut() { - guard let deepLink = URL(string: "ShortcutsZip://myPage/detailView?shortcutID=\(shortcut.id)") else { return } - let activityVC = UIActivityViewController(activityItems: [deepLink], applicationActivities: nil) - let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene - guard let window = windowScene?.windows.first else { return } - window.rootViewController?.present(activityVC, animated: true, completion: nil) - } - - func checkDowngrading() { - isDeletingShortcut.toggle() - isDowngradingUserLevel = shortcutsZipViewModel.isShortcutDowngrade() - } - - func updateShortcut() { - shortcutsZipViewModel.updateShortcutVersion(shortcut: shortcut, - updateDescription: updateDescription, - updateLink: updatedLink) - self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut - isUpdatingShortcut.toggle() + func cancelEditingComment() { + self.isEditingComment.toggle() + self.comment = self.comment.resetComment() + self.commentText = "" } func deleteComment() { @@ -171,11 +183,20 @@ final class ReadShortcutViewModel: ObservableObject { shortcutsZipViewModel.setData(model: comments) } - func fetchUserGrade(id: String) -> Image { - shortcutsZipViewModel.fetchShortcutGradeImage(isBig: false, shortcutGrade: shortcutsZipViewModel.checkShortcutGrade(userID: id)) + + //MARK: - 대댓글 + func getReplyNumber(bundleId: String) -> Int { + self.comments.comments.filter{ $0.bundle_id == bundleId }.count-1 } - func refreshShortcut() { - self.shortcut = shortcutsZipViewModel.fetchShortcutDetail(id: shortcut.id) ?? shortcut + func setReply(to comment: Comment) { + self.nestedCommentTarget = comment.user_nickname + " " + comment.contents + self.comment.bundle_id = comment.bundle_id + self.comment.depth = 1 + } + + func cancelReply() { + comment.bundle_id = "\(Date().getDate())_\(UUID().uuidString)" + comment.depth = 0 } } diff --git a/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift b/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift index 4065cb6c..627f0fde 100644 --- a/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift +++ b/HappyAnding/HappyAnding/ViewModel/ShortcutsZipViewModel.swift @@ -998,7 +998,7 @@ extension ShortcutsZipViewModel { func fetchShortcutGradeImage(isBig: Bool, shortcutGrade: ShortcutGrade) -> Image { switch shortcutGrade { case .level0: - return Image(systemName: "person.crop.circle.fill") + return Image("profileIcon") case .level1: return Image(isBig ? "level1Big" : "level1Small") case .level2: diff --git a/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift b/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift index f3ae547c..91d350dc 100644 --- a/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift +++ b/HappyAnding/HappyAnding/Views/ReadShortcutViews/ReadShortcutView.swift @@ -7,116 +7,356 @@ import SwiftUI -import WrappingHStack - struct ReadShortcutView: View { @Environment(\.presentationMode) var presentation: Binding @Environment(\.openURL) private var openURL @Environment(\.loginAlertKey) var loginAlerter + + @AppStorage("useWithoutSignIn") var useWithoutSignIn: Bool = false + @StateObject var viewModel: ReadShortcutViewModel + @FocusState private var isFocused: Bool + + @State var isCommentSectionActivated = false + var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - - Text("\" \(viewModel.shortcut.subtitle) \"") - .font(.system(size: 16, weight: .semibold)) - .foregroundStyle(SCZColor.Basic) - .frame(maxWidth: .infinity) - .padding(.vertical, 10) - .background(SCZColor.CharcoalGray.opacity04) - .roundedBorder(cornerRadius: 16, color: Color.white.opacity(0.12), isNormalBlend: true) - .padding(.top, 8) - - Divider() - .padding(.vertical, 8) - - ReadShortcutHeader(viewModel: self.viewModel) - - Divider() - .padding(.vertical, 8) - - ScrollView(.horizontal) { - //TODO: 이미지 추가 - } - .padding(.horizontal, 8) - - Divider() - .padding(.vertical, 8) - Text(viewModel.shortcut.description.replacingOccurrences(of: "\\n", with: "\n")) - .font(.system(size: 14, weight: .regular)) - .foregroundStyle(SCZColor.CharcoalGray.color) + ScrollViewReader { proxy in + ScrollView { + VStack(alignment: .leading, spacing: 12) { + //Header - 단축어 기본정보 (title, subtitle, author) + ReadShortcutHeader(viewModel: self.viewModel) + .padding(.top, 16) + + //TODO: 이미지 없는경우 예외처리 + + // Divider() + // .background(SCZColor.CharcoalGray.opacity08) + // .padding(.vertical, 8) + // + //이미지 + // ScrollView(.horizontal) { + // } + // .padding(.horizontal, 8) + + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) + + //description + Text(viewModel.shortcut.description.replacingOccurrences(of: "\\n", with: "\n")) + .descriptionReadable() + .foregroundStyle(SCZColor.CharcoalGray.color) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + + if !viewModel.shortcut.requiredApp.isEmpty { + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) + + //필요한 앱 + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 6) { + SectionTitle(text: TextLiteral.readShortcutContentViewRequiredApps) + Image(systemName: "info.circle.fill") + .customSF(size: 20) + .foregroundStyle(SCZColor.CharcoalGray.opacity24) + } + VStack(alignment: .leading) { + ForEach(viewModel.shortcut.requiredApp, id: \.self) { requirement in + Text(requirement) + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.opacity88) + .roundedBackground(background: SCZColor.CharcoalGray.opacity08) + } + } + } + .padding(.horizontal, 8) + } + + Divider() + .background(SCZColor.CharcoalGray.opacity08) + .padding(.vertical, 8) + + //버전 업데이트 정보 + VStack(alignment: .leading, spacing: 6) { + HStack { + SectionTitle(text: TextLiteral.readShortcutViewVersionTitle) + Spacer() + Menu { + Button(TextLiteral.readShortcutViewFilterNew) { + withAnimation { + viewModel.isVersionFolded = true + } + } + Button(TextLiteral.readShortcutViewFilterAll) { + withAnimation { + viewModel.isVersionFolded = false + } + } + } label: { + HStack(spacing: 4) { + Text(viewModel.isVersionFolded ? TextLiteral.readShortcutViewFilterNew : TextLiteral.readShortcutViewFilterAll) + .body1() + Image(systemName: "chevron.down") + .customSF(size: 12) + } + .foregroundStyle(SCZColor.CharcoalGray.opacity64) + .roundedBackground(background: SCZColor.CharcoalGray.opacity04) + } + } + VStack(alignment: .leading, spacing: 8) { + UpdateListItem( + isLatest: true, + version: viewModel.shortcut.updateDescription.count - 0, + description: viewModel.shortcut.updateDescription[0], + date: viewModel.shortcut.date[0], + openDownloadURL: { + if !useWithoutSignIn { + if let url = URL(string: viewModel.shortcut.downloadLink[0]) { + viewModel.checkIfDownloaded() + viewModel.isDownloadingShortcut = true + openURL(url) + } + viewModel.updateNumberOfDownload(index: 0) + } else { + loginAlerter.isPresented = true + } + } + ) + + if !viewModel.isVersionFolded { + ForEach(1.. () + + var body: some View { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 16) { HStack { - Text("Ver \(index).0") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(SCZColor.CharcoalGray.opacity64) - Text("최신") - .roundedBackground() + Button { + openDownloadURL() + } label: { + Text("Ver \(version).0") + .underline() + .medium16() + .foregroundStyle(SCZColor.CharcoalGray.opacity64) + } + if isLatest { + Image(systemName: "sparkles") + .customSF(size: 24) + .foregroundStyle(SCZColor.CharcoalGray.opacity88) + } + } + if !description.isEmpty { + Text(description) + .regular16() + .foregroundStyle(SCZColor.CharcoalGray.color) } - Text(description) Text(date.getVersionDateFormat() ?? "") + .regular16() .foregroundStyle(SCZColor.CharcoalGray.opacity48) - }.font(.system(size: 14, weight: .regular)) + } } } @@ -124,8 +364,7 @@ private struct SectionTitle: View { let text: String var body: some View { Text(text) - //Pretendard 14 medium - .font(.system(size: 14, weight: .medium)) + .medium16() .foregroundStyle(SCZColor.CharcoalGray.opacity48) } } @@ -133,89 +372,60 @@ private struct SectionTitle: View { struct ReadShortcutHeader: View { @StateObject var viewModel: ReadShortcutViewModel var body: some View { - HStack(spacing: 12) { + HStack(spacing: 8) { ShortcutIcon(sfSymbol: viewModel.shortcut.sfSymbol, color: viewModel.shortcut.color, size: 96) .padding(.horizontal, 8) VStack(alignment: .leading, spacing: 3) { + Text(viewModel.shortcut.subtitle) + .body1() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) Text(viewModel.shortcut.title) - .font(.system(size: 20, weight: .bold)) - .foregroundStyle(SCZColor.CharcoalGray.color) - .frame(maxWidth: .infinity, alignment: .leading) - HStack { - ForEach (0.. 512 ? "xmark.circle.fill" : "arrow.up.circle.fill") + .customSF(size: 32) + .foregroundStyle( + viewModel.commentText.isEmpty ? SCZColor.CharcoalGray.opacity24 : + viewModel.commentText.count > 512 ? SCZColor.systemWhite : SCZColor.systemWhite, + viewModel.commentText.isEmpty ? SCZColor.CharcoalGray.opacity08 : + viewModel.commentText.count > 512 ? SCZColor.SCZRed.red : SCZColor.SCZBlue.strong + ) + } + .disabled(viewModel.commentText == "" || viewModel.commentText.count > 512 ? true : false) - Divider() - .background(Color.gray1) + if viewModel.commentText.count > 512 { + Text("-\(viewModel.commentText.count - 512)") + .body2() + .foregroundStyle(SCZColor.SCZRed.dangerouslyRed) + } } + .padding(.trailing, 8) } - .padding(.bottom, 16) + .background( + Rectangle() + .fill(SCZColor.CharcoalGray.opacity08) + .cornerRadius(16 ,corners: (viewModel.comment.depth == 1) && (!viewModel.isEditingComment) ? [.bottomLeft, .bottomRight] : .allCorners) + ) } + .padding(.bottom, isFocused ? 7.5 : 70) + .id("CommentTextField") + } + private var nestedCommentTargetView: some View { + + HStack { + Text("@\(viewModel.nestedCommentTarget)") + .lineLimit(1) + .descriptionReadable() + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + viewModel.cancelReply() + } label: { + Image(systemName: "xmark") + .customSF(size: 15) + .foregroundStyle(SCZColor.CharcoalGray.opacity48) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 16) + .background( + Rectangle() + .fill(SCZColor.CharcoalGray.opacity16) + .cornerRadius(16 ,corners: [.topLeft, .topRight]) + ) + } +} + +private struct CommentTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + configuration + .keyboardType(.default) + .disableAutocorrection(true) + .textInputAutocapitalization(.never) + .descriptionReadable() + .lineLimit(11) + .frame(maxWidth: .infinity, alignment: .leading) } }