diff --git a/HNReader.xcodeproj/project.pbxproj b/HNReader.xcodeproj/project.pbxproj index 3d6f2d3..5b47b55 100644 --- a/HNReader.xcodeproj/project.pbxproj +++ b/HNReader.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,7 @@ 330718D415D21296AA14E7CA /* HackerNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071291447141A6D31E671B /* HackerNewsTests.swift */; }; 330719203034BDB177F28C41 /* +DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071B0E5439D8D207CB68F4 /* +DateTests.swift */; }; 33071F1C64D4742E1F947FAA /* ItemDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */; }; + 5F109D592AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */; }; C93F99B6267554F00046F870 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B5267554F00046F870 /* ItemCell.swift */; }; C93F99B8267557FC0046F870 /* ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B7267557FC0046F870 /* ItemList.swift */; }; C93F99BA267580CE0046F870 /* HTMLText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B9267580CE0046F870 /* HTMLText.swift */; }; @@ -49,6 +50,7 @@ 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemDownloader.swift; sourceTree = ""; }; 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HackerNewsClientTests.swift; sourceTree = ""; }; 33071EEBE46634E658582AE3 /* ItemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemTests.swift; sourceTree = ""; }; + 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalRedactedModifier.swift; sourceTree = ""; }; C93F99B5267554F00046F870 /* ItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCell.swift; sourceTree = ""; }; C93F99B7267557FC0046F870 /* ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemList.swift; sourceTree = ""; }; C93F99B9267580CE0046F870 /* HTMLText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLText.swift; sourceTree = ""; }; @@ -190,6 +192,7 @@ C93F99B5267554F00046F870 /* ItemCell.swift */, C93F99B7267557FC0046F870 /* ItemList.swift */, C9926691267588B80035A88F /* Components */, + 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */, ); path = View; sourceTree = ""; @@ -269,8 +272,9 @@ C9D0936B26741BBE002CC786 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1510; TargetAttributes = { C9D0937226741BBE002CC786 = { CreatedOnToolsVersion = 12.5; @@ -330,6 +334,7 @@ C9D0938026741BBF002CC786 /* Persistence.swift in Sources */, C9D0937926741BBE002CC786 /* HomeView.swift in Sources */, C93F99B6267554F00046F870 /* ItemCell.swift in Sources */, + 5F109D592AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift in Sources */, C93F99B8267557FC0046F870 /* ItemList.swift in Sources */, C9E9BCFD2674C80E001B4E19 /* AppState.swift in Sources */, C9E9BD032674D095001B4E19 /* User.swift in Sources */, @@ -370,6 +375,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -400,9 +406,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -431,6 +439,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -461,9 +470,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -489,10 +500,11 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = HNReader/HNReader.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"HNReader/Preview Content\""; DEVELOPMENT_TEAM = H89RFW5UZ6; ENABLE_HARDENED_RUNTIME = YES; @@ -521,10 +533,11 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = HNReader/HNReader.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"HNReader/Preview Content\""; DEVELOPMENT_TEAM = H89RFW5UZ6; ENABLE_HARDENED_RUNTIME = YES; @@ -552,6 +565,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = H89RFW5UZ6; INFOPLIST_FILE = HNReaderTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -574,6 +588,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = H89RFW5UZ6; INFOPLIST_FILE = HNReaderTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate b/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate index e6fcee4..8e97a0f 100644 Binary files a/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate and b/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/HNReader/HNReaderApp.swift b/HNReader/HNReaderApp.swift index 200e9b2..21db274 100644 --- a/HNReader/HNReaderApp.swift +++ b/HNReader/HNReaderApp.swift @@ -26,11 +26,13 @@ struct HNReaderApp: App { var body: some Scene { WindowGroup { HomeView() + .frame(minWidth: 800, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity) .onAppear { displayMode = appState.getColorScheme() } .preferredColorScheme(displayMode) .environmentObject(appState) + } Settings { @@ -45,7 +47,7 @@ struct HNReaderApp: App { } } .frame(minHeight: 100) - .frame(minWidth: 300) + .frame(minWidth: 100) .preferredColorScheme(displayMode) } } diff --git a/HNReader/Model/Item.swift b/HNReader/Model/Item.swift index 2973e6e..112d4d1 100644 --- a/HNReader/Model/Item.swift +++ b/HNReader/Model/Item.swift @@ -54,6 +54,11 @@ public struct Item: Decodable { } } + public var scoreString: String? { + guard let score = score else { return nil } + return "\(score)" + } + public var timeStringRepresentation: String? { Date().timeElapsedStringRepresentation(since: Date(timeIntervalSince1970: TimeInterval(time!))) } diff --git a/HNReader/View/ConditionalRedactedModifier.swift b/HNReader/View/ConditionalRedactedModifier.swift new file mode 100644 index 0000000..9ba29ee --- /dev/null +++ b/HNReader/View/ConditionalRedactedModifier.swift @@ -0,0 +1,38 @@ +// +// ConditionalRedacted.swift +// HNReader +// +// Created by Mattia Righetti on 04/11/23. +// + +import SwiftUI + +struct ConditionalRedactedModifier: ViewModifier { + var isRedacted: Bool + + func body(content: Content) -> some View { + if isRedacted { + content.redacted(reason: .placeholder) + } else { + content + } + } +} + +extension View { + func redactIfNull(_ obj: Optional) -> some View { + switch obj { + case .none: + return self.modifier(ConditionalRedactedModifier(isRedacted: true)) + case .some(_): + return self.modifier(ConditionalRedactedModifier(isRedacted: false)) + } + } +} + +#Preview { + VStack { + Text("Some Text").redactIfNull(Optional.none) + } + .frame(width: 500, height: 500) +} diff --git a/HNReader/View/ItemCell.swift b/HNReader/View/ItemCell.swift index b286dcb..e6a977d 100644 --- a/HNReader/View/ItemCell.swift +++ b/HNReader/View/ItemCell.swift @@ -20,22 +20,93 @@ struct ItemCell: View { } var body: some View { - VStack(alignment: .leading, spacing: 5) { - TitleView() - HostText() - -// if let text = item.text { -// HTMLText(text: text) -// .font(.body) -// .lineLimit(3) -// .multilineTextAlignment(.leading) -// } - - HStack { - ScoreText() - AuthorText() - Spacer() + HStack { + VStack(alignment: .leading, spacing: 5) { + Text(item?.title ?? String(repeating: "-", count: 30)) + .font(.title2) + .fontWeight(.semibold) + .redactIfNull(item) + + Text(item?.urlHost ?? String(repeating: "-", count: 30)) + .font(.callout) + .fontWeight(.light) + .foregroundColor(.white) + .redactIfNull(item) + + HStack { + Text(item?.scoreString ?? String(repeating: "-", count: 3)) + .font(.callout) + .fontWeight(.bold) + .redactIfNull(item) + + Text("Posted by \(item?.by ?? "?")") + .font(.callout) + .redactIfNull(item) + + Text("\(item?.timeStringRepresentation ?? String(repeating: "-", count: 3))") + .font(.callout) + .redactIfNull(item) + + Spacer() + } } + + HStack { + + ZStack { + RoundedRectangle(cornerRadius: 15) + .foregroundStyle(Color.gray.opacity(0.1)) + .frame(width: 50, height: 50) + + Label(title: {}, icon: { + Image(systemName: "bubble.left") + }) + .padding() + } + .frame(width: 50, height: 50) + .onHover { isHovered in + DispatchQueue.main.async { + if (isHovered) { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + .onTapGesture { + if let item = item { + guard let url = URL(string: "https://news.ycombinator.com/item?id=\(item.id)") else { return } + NSWorkspace.shared.open(url) + } + } + + ZStack { + RoundedRectangle(cornerRadius: 15) + .foregroundStyle(Color.gray.opacity(0.1)) + .frame(width: 50, height: 50) + + Label(title: {}, icon: { + Image(systemName: "link") + }) + .padding() + } + .frame(width: 50, height: 50) + .onHover { isHovered in + DispatchQueue.main.async { + if (isHovered) { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + .onTapGesture { + if let item = item { + guard let url = URL(string: item.url!) else { return } + NSWorkspace.shared.open(url) + } + } + }.padding(.leading) } .padding() .background(colorScheme == .dark ? Color.black.opacity(0.3) : Color.white) @@ -45,85 +116,6 @@ struct ItemCell: View { fetchItem() } } - .onTapGesture { - if let item = item { - guard let url = URL(string: item.url!) else { return } - NSWorkspace.shared.open(url) - } - } - } - - @ViewBuilder - private func TitleView() -> some View { - if let item = item { - Text(item.title ?? "No title") - .font(.system(.title, design: .rounded)) - .fontWeight(.bold) - } else { - Text("No title") - .font(.system(.title, design: .rounded)) - .fontWeight(.bold) - .redacted(reason: .placeholder) - } - } - - @ViewBuilder - private func HostText() -> some View { - if let item = item { - Text(item.urlHost ?? "") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.blue) - } else { - Text("No url") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.blue) - .redacted(reason: .placeholder) - } - } - - @ViewBuilder - private func ScoreText() -> some View { - if let item = item { - Text("\(item.score ?? 0)") - .font(.system(.callout, design: .rounded)) - .foregroundColor(.orange) - .fontWeight(.bold) - } else { - Text("0") - .font(.system(.callout, design: .rounded)) - .foregroundColor(.orange) - .fontWeight(.bold) - .redacted(reason: .placeholder) - } - } - - @ViewBuilder - private func AuthorText() -> some View { - HStack { - Text("•") - .padding(.horizontal, 1) - Text("Posted by") - .foregroundColor(.gray) - if let item = item { - Text("\(item.by ?? "anonymous")") - .foregroundColor(.yellow) - .fontWeight(.bold) - } else { - Text("No author") - .redacted(reason: .placeholder) - } - Text("•") - .padding(.horizontal, 1) - if let item = item { - Text("\(item.timeStringRepresentation ?? "")") - .foregroundColor(.gray) - } else { - Text("").redacted(reason: .placeholder) - } - } - .font(.system(.callout, design: .rounded)) } private func fetchItem() { diff --git a/HNReader/View/ItemList.swift b/HNReader/View/ItemList.swift index d8e7c5b..f2d186a 100644 --- a/HNReader/View/ItemList.swift +++ b/HNReader/View/ItemList.swift @@ -15,19 +15,18 @@ struct ItemList: View { private var itemLimitOptions: [Int] = [25, 50, 100] var body: some View { - ScrollView { - LazyVStack(alignment: .leading) { - ForEach(viewModel.storiesIds, id: \.self) { itemId in - ItemCell(itemId: itemId) - .padding(.horizontal) - } + List { + ForEach(viewModel.storiesIds, id: \.self) { itemId in + ItemCell(itemId: itemId) + .listRowSeparator(.hidden) } - .padding(.vertical) } .onAppear { viewModel.currentNewsSelection = appState.newsSelection } - .onChange(of: appState.newsSelection, perform: fetchItems) + .onChange(of: appState.newsSelection, { + fetchItems(by: appState.newsSelection) + }) .toolbar { MaxItemPicker(enabled: false) Button(action: viewModel.refreshStories) {