From 4916528e71f0747de2be21c96f46562b69a24398 Mon Sep 17 00:00:00 2001 From: Mathijs Bernson Date: Sun, 26 Jan 2025 13:48:30 +0100 Subject: [PATCH] Format all sources using `swift-format` --- .swift-format | 7 ++ .swiftformat | 3 - .swiftlint.yml | 4 - CCCApi/Package.swift | 4 +- CCCApi/Sources/CCCApi/ApiService.swift | 18 ++-- CCCApi/Sources/CCCApi/Models/Conference.swift | 10 ++- CCCApi/Sources/CCCApi/Models/Examples.swift | 86 +++++++++++++------ CCCApi/Sources/CCCApi/Models/Recording.swift | 6 +- CCCApi/Sources/CCCApi/Models/Talk.swift | 12 ++- CCCTube/ContentView.swift | 4 +- CCCTube/Extensions/Alert+Error.swift | 9 +- CCCTube/Features/Browse/BrowseView.swift | 16 ++-- .../Features/Conferences/ConferenceCell.swift | 2 +- .../Features/Conferences/ConferenceView.swift | 13 +-- .../Conferences/ConferencesView.swift | 28 +++--- .../Features/Search/SearchSuggestion.swift | 3 +- CCCTube/Features/Search/SearchView.swift | 60 ++++++------- CCCTube/Features/Talk/MediaAnalyzer.swift | 5 +- CCCTube/Features/Talk/TalkCell.swift | 19 ++-- .../Features/Talk/TalkMetadataFactory.swift | 4 +- CCCTube/Features/Talk/TalkPlayerView.swift | 42 ++++----- .../Features/Talk/TalkPlayerViewModel.swift | 12 ++- CCCTube/Features/Talk/TalkView.swift | 30 ++++--- CCCTube/Features/Talk/TalksGrid.swift | 54 ++++++------ .../VideoPlayer/VideoPlayerView.swift | 17 ++-- CCCTube/URLParser.swift | 2 +- CCCTubeTests/URLParserTests.swift | 3 +- CCCTubeUITests/CCCTubeUIScreenshotTests.swift | 10 ++- README.md | 12 +++ TopShelf/TopShelfContentFactory.swift | 4 +- 30 files changed, 306 insertions(+), 193 deletions(-) create mode 100644 .swift-format delete mode 100644 .swiftformat delete mode 100644 .swiftlint.yml diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..1cec92c --- /dev/null +++ b/.swift-format @@ -0,0 +1,7 @@ +{ + "indentation" : { + "spaces" : 4 + }, + "tabWidth" : 4, + "version" : 1 +} diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 5ffbfff..0000000 --- a/.swiftformat +++ /dev/null @@ -1,3 +0,0 @@ ---disable unusedArguments ---disable redundantRawValues ---indent 4 diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index 7ca66e6..0000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,4 +0,0 @@ -disabled_rules: - - trailing_comma - - line_length - - unused_closure_parameter diff --git a/CCCApi/Package.swift b/CCCApi/Package.swift index 18df593..954bb4a 100644 --- a/CCCApi/Package.swift +++ b/CCCApi/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library( name: "CCCApi", targets: ["CCCApi"] - ), + ) ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -23,6 +23,6 @@ let package = Package( .target( name: "CCCApi", dependencies: [] - ), + ) ] ) diff --git a/CCCApi/Sources/CCCApi/ApiService.swift b/CCCApi/Sources/CCCApi/ApiService.swift index 04bd39a..59ca67a 100644 --- a/CCCApi/Sources/CCCApi/ApiService.swift +++ b/CCCApi/Sources/CCCApi/ApiService.swift @@ -32,7 +32,8 @@ public class ApiService { if let date = self.iso8601Formatter.date(from: dateString) { return date } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to parse ISO 8601 date") + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "Unable to parse ISO 8601 date") } } } @@ -46,14 +47,16 @@ public class ApiService { } public func conference(acronym: String) async throws -> Conference { - let (data, _) = try await session.data(from: baseURL.appendingPathComponent("conferences").appendingPathComponent(acronym)) + let (data, _) = try await session.data( + from: baseURL.appendingPathComponent("conferences").appendingPathComponent(acronym)) return try decoder.decode(Conference.self, from: data) } // MARK: Talks public func talk(id: String) async throws -> Talk { - let (data, _) = try await session.data(from: baseURL.appendingPathComponent("events").appendingPathComponent(id)) + let (data, _) = try await session.data( + from: baseURL.appendingPathComponent("events").appendingPathComponent(id)) let response = try decoder.decode(Talk.self, from: data) return response } @@ -65,7 +68,8 @@ public class ApiService { } public func recentTalks() async throws -> [Talk] { - let (data, _) = try await session.data(from: baseURL.appendingPathComponent("events").appendingPathComponent("recent")) + let (data, _) = try await session.data( + from: baseURL.appendingPathComponent("events").appendingPathComponent("recent")) let response = try decoder.decode(EventsResponse.self, from: data) return response.events } @@ -91,12 +95,14 @@ public class ApiService { // MARK: Recordings public func recordings(for talk: Talk) async throws -> [Recording] { - let (data, _) = try await session.data(from: baseURL.appendingPathComponent("events").appendingPathComponent(talk.guid)) + let (data, _) = try await session.data( + from: baseURL.appendingPathComponent("events").appendingPathComponent(talk.guid)) let response = try decoder.decode(TalkExtended.self, from: data) guard let recordings = response.recordings else { return [] } - return recordings + return + recordings // Remove formats Apple doesn't support .filter { !$0.mimeType.contains("opus") } .filter { !$0.mimeType.contains("webm") } diff --git a/CCCApi/Sources/CCCApi/Models/Conference.swift b/CCCApi/Sources/CCCApi/Models/Conference.swift index a3c2594..841aa60 100644 --- a/CCCApi/Sources/CCCApi/Models/Conference.swift +++ b/CCCApi/Sources/CCCApi/Models/Conference.swift @@ -30,7 +30,12 @@ public struct Conference: Decodable, Identifiable, Sendable { public var id: String { slug } - init(acronym: String, slug: String, title: String, updatedAt: Date, eventLastReleasedAt: Date? = nil, events: [Talk]? = nil, link: URL? = nil, description: String? = nil, aspectRatio: AspectRatio? = nil, webgenLocation: String, url: URL, logoURL: URL, imagesURL: URL? = nil, recordingsURL: URL? = nil) { + init( + acronym: String, slug: String, title: String, updatedAt: Date, + eventLastReleasedAt: Date? = nil, events: [Talk]? = nil, link: URL? = nil, + description: String? = nil, aspectRatio: AspectRatio? = nil, webgenLocation: String, + url: URL, logoURL: URL, imagesURL: URL? = nil, recordingsURL: URL? = nil + ) { self.acronym = acronym self.slug = slug self.title = title @@ -101,7 +106,8 @@ public struct AspectRatio: Decodable, Sendable { let parts = string.components(separatedBy: ":") .compactMap(Double.init) if parts.count != 2 { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid aspect ratio") + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "Invalid aspect ratio") } else { width = parts[0] height = parts[1] diff --git a/CCCApi/Sources/CCCApi/Models/Examples.swift b/CCCApi/Sources/CCCApi/Models/Examples.swift index 82ee99f..50efb39 100644 --- a/CCCApi/Sources/CCCApi/Models/Examples.swift +++ b/CCCApi/Sources/CCCApi/Models/Examples.swift @@ -9,15 +9,16 @@ import Foundation // swiftlint:disable force_try -public extension Conference { - static let example = Conference( +extension Conference { + public static let example = Conference( acronym: "MCH2022", slug: "conferences/camp-NL/mch2022", title: "May Contain Hackers 2022", updatedAt: try! Date("2022-07-29T20:45:05Z", strategy: .iso8601), eventLastReleasedAt: try! Date("2022-07-26T00:00:00Z", strategy: .iso8601), link: URL(string: "https://mch2022.org/")!, - description: "MCH2022 was a nonprofit outdoor hacker camp taking place in Zeewolde, the Netherlands, July 22 to 26 2022. The event is organized for and by volunteers from and around all facets of the worldwide hacker community.\r\n\r\nKnowledge sharing, technological advancement, experimentation, connecting with your hacker peers and hacking are some of the core values of this event.\r\n\r\nMCH2022 is the successor of a string of similar events happening every four years since 1989. These are GHP, HEU, HIP, HAL, WTH, HAR, OHM and SHA.", + description: + "MCH2022 was a nonprofit outdoor hacker camp taking place in Zeewolde, the Netherlands, July 22 to 26 2022. The event is organized for and by volunteers from and around all facets of the worldwide hacker community.\r\n\r\nKnowledge sharing, technological advancement, experimentation, connecting with your hacker peers and hacking are some of the core values of this event.\r\n\r\nMCH2022 is the successor of a string of similar events happening every four years since 1989. These are GHP, HEU, HIP, HAL, WTH, HAR, OHM and SHA.", aspectRatio: AspectRatio(width: 16, height: 9), webgenLocation: "conferences/camp-NL/mch2022", url: URL(string: "https://static.media.ccc.de/media/events/MCH2022/logo.png")!, @@ -27,8 +28,8 @@ public extension Conference { ) } -public extension Talk { - static let example = Talk( +extension Talk { + public static let example = Talk( guid: "cf4dc17c-aab4-5868-9b57-100a55a1c2fb", title: "⚠️ May Contain Hackers 2022 Closing", subtitle: nil, @@ -37,7 +38,7 @@ public extension Talk { description: "It's over before you know it...", originalLanguage: "eng", persons: [ - "Elger \"Stitch\" Jonker", + "Elger \"Stitch\" Jonker" ], tags: [ "mch2022", @@ -54,18 +55,32 @@ public extension Talk { duration: 1066, conferenceTitle: "May Contain Hackers 2022", conferenceURL: URL(string: "https://api.media.ccc.de/public/conferences/MCH2022")!, - thumbURL: URL(string: "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb.jpg")!, - posterURL: URL(string: "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb_preview.jpg")!, - timelineURL: URL(string: "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb.timeline.jpg")!, - thumbnailsURL: URL(string: "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb.thumbnails.vtt")!, - frontendLink: URL(string: "https://media.ccc.de/v/mch2022-110--may-contain-hackers-2022-closing")!, - url: URL(string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, + thumbURL: URL( + string: + "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb.jpg" + )!, + posterURL: URL( + string: + "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb_preview.jpg" + )!, + timelineURL: URL( + string: + "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb.timeline.jpg" + )!, + thumbnailsURL: URL( + string: + "https://static.media.ccc.de/media/events/MCH2022/110-cf4dc17c-aab4-5868-9b57-100a55a1c2fb.thumbnails.vtt" + )!, + frontendLink: URL( + string: "https://media.ccc.de/v/mch2022-110--may-contain-hackers-2022-closing")!, + url: URL( + string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, related: [] ) } -public extension Recording { - static let example = Recording( +extension Recording { + public static let example = Recording( size: 461, length: 1066, mimeType: "video/mp4", @@ -77,14 +92,18 @@ public extension Recording { width: 1920, height: 1080, updatedAt: try! Date("2022-07-26T17:41:57Z", strategy: .iso8601), - recordingURL: URL(string: "https://cdn.media.ccc.de/events/MCH2022/h264-hd/mch2022-110-eng-May_Contain_Hackers_2022_Closing_hd.mp4")!, url: URL(string: "https://api.media.ccc.de/public/recordings/60586")!, - eventURL: URL(string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, + recordingURL: URL( + string: + "https://cdn.media.ccc.de/events/MCH2022/h264-hd/mch2022-110-eng-May_Contain_Hackers_2022_Closing_hd.mp4" + )!, url: URL(string: "https://api.media.ccc.de/public/recordings/60586")!, + eventURL: URL( + string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, conferenceURL: URL(string: "https://api.media.ccc.de/public/conferences/MCH2022")! ) } -public extension Array where Element == RelatedTalk { - static let example: [RelatedTalk] = [ +extension Array where Element == RelatedTalk { + public static let example: [RelatedTalk] = [ RelatedTalk( eventID: 2291, eventGUID: "2f68e356-6c3f-4034-9640-c06d717ed96b", @@ -113,8 +132,8 @@ public extension Array where Element == RelatedTalk { ] } -public extension Array where Element == Recording { - static let example: [Recording] = [ +extension Array where Element == Recording { + public static let example: [Recording] = [ Recording( size: 16, length: 1066, @@ -127,9 +146,14 @@ public extension Array where Element == Recording { width: 0, height: 0, updatedAt: try! Date("2022-07-26T23:32:01Z", strategy: .iso8601), - recordingURL: URL(string: "https://cdn.media.ccc.de/events/MCH2022/mp3/mch2022-110-eng-May_Contain_Hackers_2022_Closing_mp3.mp3")!, + recordingURL: URL( + string: + "https://cdn.media.ccc.de/events/MCH2022/mp3/mch2022-110-eng-May_Contain_Hackers_2022_Closing_mp3.mp3" + )!, url: URL(string: "https://api.media.ccc.de/public/recordings/60723")!, - eventURL: URL(string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, + eventURL: URL( + string: + "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, conferenceURL: URL(string: "https://api.media.ccc.de/public/conferences/MCH2022")! ), Recording( @@ -144,9 +168,14 @@ public extension Array where Element == Recording { width: 720, height: 576, updatedAt: try! Date("2022-07-26T23:30:43Z", strategy: .iso8601), - recordingURL: URL(string: "https://cdn.media.ccc.de/events/MCH2022/h264-sd/mch2022-110-eng-May_Contain_Hackers_2022_Closing_sd.mp4")!, + recordingURL: URL( + string: + "https://cdn.media.ccc.de/events/MCH2022/h264-sd/mch2022-110-eng-May_Contain_Hackers_2022_Closing_sd.mp4" + )!, url: URL(string: "https://api.media.ccc.de/public/recordings/60722")!, - eventURL: URL(string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, + eventURL: URL( + string: + "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, conferenceURL: URL(string: "https://api.media.ccc.de/public/conferences/MCH2022")! ), Recording( @@ -161,9 +190,14 @@ public extension Array where Element == Recording { width: 1920, height: 1080, updatedAt: try! Date("2022-07-26T17:41:57Z", strategy: .iso8601), - recordingURL: URL(string: "https://cdn.media.ccc.de/events/MCH2022/h264-hd/mch2022-110-eng-May_Contain_Hackers_2022_Closing_hd.mp4")!, + recordingURL: URL( + string: + "https://cdn.media.ccc.de/events/MCH2022/h264-hd/mch2022-110-eng-May_Contain_Hackers_2022_Closing_hd.mp4" + )!, url: URL(string: "https://api.media.ccc.de/public/recordings/60586")!, - eventURL: URL(string: "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, + eventURL: URL( + string: + "https://api.media.ccc.de/public/events/cf4dc17c-aab4-5868-9b57-100a55a1c2fb")!, conferenceURL: URL(string: "https://api.media.ccc.de/public/conferences/MCH2022")! ), ] diff --git a/CCCApi/Sources/CCCApi/Models/Recording.swift b/CCCApi/Sources/CCCApi/Models/Recording.swift index b6e0c98..ab1e1f1 100644 --- a/CCCApi/Sources/CCCApi/Models/Recording.swift +++ b/CCCApi/Sources/CCCApi/Models/Recording.swift @@ -38,7 +38,11 @@ public struct Recording: Decodable, Identifiable, Equatable, Sendable { mimeType.starts(with: "video") } - init(size: Int?, length: TimeInterval?, mimeType: String, language: String, filename: String, state: String, folder: String, isHighQuality: Bool, width: Int?, height: Int?, updatedAt: Date, recordingURL: URL, url: URL, eventURL: URL, conferenceURL: URL) { + init( + size: Int?, length: TimeInterval?, mimeType: String, language: String, filename: String, + state: String, folder: String, isHighQuality: Bool, width: Int?, height: Int?, + updatedAt: Date, recordingURL: URL, url: URL, eventURL: URL, conferenceURL: URL + ) { self.size = size self.length = length self.mimeType = mimeType diff --git a/CCCApi/Sources/CCCApi/Models/Talk.swift b/CCCApi/Sources/CCCApi/Models/Talk.swift index 3122982..cdf7356 100644 --- a/CCCApi/Sources/CCCApi/Models/Talk.swift +++ b/CCCApi/Sources/CCCApi/Models/Talk.swift @@ -44,7 +44,14 @@ public struct Talk: Decodable, Identifiable, Equatable, Sendable { public let url: URL public let related: [RelatedTalk] - init(guid: String, title: String, subtitle: String?, slug: String, link: URL?, description: String?, originalLanguage: String?, persons: [String], tags: [String], viewCount: Int, isPromoted: Bool, date: Date?, releaseDate: Date, updatedAt: Date, length: TimeInterval, duration: TimeInterval, conferenceTitle: String, conferenceURL: URL, thumbURL: URL?, posterURL: URL, timelineURL: URL, thumbnailsURL: URL, frontendLink: URL, url: URL, related: [RelatedTalk]) { + init( + guid: String, title: String, subtitle: String?, slug: String, link: URL?, + description: String?, originalLanguage: String?, persons: [String], tags: [String], + viewCount: Int, isPromoted: Bool, date: Date?, releaseDate: Date, updatedAt: Date, + length: TimeInterval, duration: TimeInterval, conferenceTitle: String, conferenceURL: URL, + thumbURL: URL?, posterURL: URL, timelineURL: URL, thumbnailsURL: URL, frontendLink: URL, + url: URL, related: [RelatedTalk] + ) { self.guid = guid self.title = title self.subtitle = subtitle @@ -139,7 +146,8 @@ struct TalkExtended: Decodable { return [] } - return recordings + return + recordings .filter { $0.mimeType == "video/mp4" } } } diff --git a/CCCTube/ContentView.swift b/CCCTube/ContentView.swift index ecab242..32f4c0c 100644 --- a/CCCTube/ContentView.swift +++ b/CCCTube/ContentView.swift @@ -75,7 +75,9 @@ struct ContentView: View { case let .playTalk(id): let talk = try await api.talk(id: id) let recordings = try await api.recordings(for: talk) - let recording = recordings.first(where: { $0.isHighQuality }) ?? recordings.first(where: { $0.isVideo }) + let recording = + recordings.first(where: { $0.isHighQuality }) + ?? recordings.first(where: { $0.isVideo }) self.talk = TalkToPlay(talk: talk, recordingToPlay: recording) } } catch { diff --git a/CCCTube/Extensions/Alert+Error.swift b/CCCTube/Extensions/Alert+Error.swift index d7b1639..e649f4f 100644 --- a/CCCTube/Extensions/Alert+Error.swift +++ b/CCCTube/Extensions/Alert+Error.swift @@ -10,7 +10,10 @@ import SwiftUI extension View { /// Presents an alert when an error is present. - func alert(_ titleKey: LocalizedStringKey, error: Binding, buttonTitleKey: LocalizedStringKey = "Ok") -> some View { + func alert( + _ titleKey: LocalizedStringKey, error: Binding, + buttonTitleKey: LocalizedStringKey = "Ok" + ) -> some View { modifier(ErrorAlert(error: error, titleKey: titleKey, buttonTitleKey: buttonTitleKey)) } } @@ -42,5 +45,7 @@ private struct ErrorAlert: ViewModifier { #Preview("Error alert") { Text(verbatim: "Preview text") - .alert("Preview error title", error: .constant(CCCTubeError(message: "Failed to load all the things"))) + .alert( + "Preview error title", + error: .constant(CCCTubeError(message: "Failed to load all the things"))) } diff --git a/CCCTube/Features/Browse/BrowseView.swift b/CCCTube/Features/Browse/BrowseView.swift index 81e8b22..5554e23 100644 --- a/CCCTube/Features/Browse/BrowseView.swift +++ b/CCCTube/Features/Browse/BrowseView.swift @@ -33,20 +33,20 @@ struct BrowseView: View { NavigationStack { ScrollView { #if os(tvOS) - if query == .popular { - YearPicker(year: $year) - } + if query == .popular { + YearPicker(year: $year) + } #endif TalksGrid(talks: talks) } .toolbar { #if !os(tvOS) - if query == .popular { - ToolbarItem(placement: .topBarTrailing) { - YearPicker(year: $year) + if query == .popular { + ToolbarItem(placement: .topBarTrailing) { + YearPicker(year: $year) + } } - } #endif } .overlay { @@ -57,7 +57,7 @@ struct BrowseView: View { } } #if !os(tvOS) - .navigationTitle(query.localizedTitle) + .navigationTitle(query.localizedTitle) #endif .task(id: year) { await refresh() diff --git a/CCCTube/Features/Conferences/ConferenceCell.swift b/CCCTube/Features/Conferences/ConferenceCell.swift index 1b152b1..877ac48 100644 --- a/CCCTube/Features/Conferences/ConferenceCell.swift +++ b/CCCTube/Features/Conferences/ConferenceCell.swift @@ -5,8 +5,8 @@ // Created by Mathijs Bernson on 26/01/2025. // -import SwiftUI import CCCApi +import SwiftUI struct ConferenceCell: View { let conference: Conference diff --git a/CCCTube/Features/Conferences/ConferenceView.swift b/CCCTube/Features/Conferences/ConferenceView.swift index d32f80c..e44c149 100644 --- a/CCCTube/Features/Conferences/ConferenceView.swift +++ b/CCCTube/Features/Conferences/ConferenceView.swift @@ -27,9 +27,12 @@ struct ConferenceView: View { } #else let filterQuery = filterQuery.lowercased() - TalksGrid(talks: filterQuery.isEmpty ? talks : talks.filter { talk in - talk.title.lowercased().contains(filterQuery) - }) + TalksGrid( + talks: filterQuery.isEmpty + ? talks + : talks.filter { talk in + talk.title.lowercased().contains(filterQuery) + }) #endif } .overlay { @@ -40,8 +43,8 @@ struct ConferenceView: View { } } #if !os(tvOS) - .searchable(text: $filterQuery) - .navigationTitle(conference.title) + .searchable(text: $filterQuery) + .navigationTitle(conference.title) #endif .task { await refresh() diff --git a/CCCTube/Features/Conferences/ConferencesView.swift b/CCCTube/Features/Conferences/ConferencesView.swift index efac1d6..2a6c572 100644 --- a/CCCTube/Features/Conferences/ConferencesView.swift +++ b/CCCTube/Features/Conferences/ConferencesView.swift @@ -17,13 +17,16 @@ struct ConferencesView: View { NavigationStack { ScrollView { let filterQuery = filterQuery.lowercased() - ConferencesGrid(conferences: filterQuery.isEmpty ? conferences : conferences.filter { conference in - conference.title.lowercased().contains(filterQuery) - }) + ConferencesGrid( + conferences: filterQuery.isEmpty + ? conferences + : conferences.filter { conference in + conference.title.lowercased().contains(filterQuery) + }) } #if !os(tvOS) - .searchable(text: $filterQuery) - .navigationTitle("Conferences") + .searchable(text: $filterQuery) + .navigationTitle("Conferences") #endif .task { await refresh() @@ -59,8 +62,9 @@ struct ConferencesGrid: View { #if os(tvOS) let columns: [GridItem] = Array(repeating: GridItem(), count: 4) #else - let columns: [GridItem] = Array(repeating: GridItem(.adaptive(minimum: 200, maximum: 400)), - count: 2) + let columns: [GridItem] = Array( + repeating: GridItem(.adaptive(minimum: 200, maximum: 400)), + count: 2) #endif var body: some View { @@ -71,9 +75,9 @@ struct ConferencesGrid: View { } label: { ConferenceCell(conference: conference) #if os(visionOS) - .padding() - .contentShape(RoundedRectangle(cornerRadius: 16)) - .hoverEffect(.lift) + .padding() + .contentShape(RoundedRectangle(cornerRadius: 16)) + .hoverEffect(.lift) #endif } .buttonStyle(.plain) @@ -84,8 +88,8 @@ struct ConferencesGrid: View { .focusSection() .buttonStyle(.card) #endif - .accessibilityIdentifier("ConferencesGrid") - .accessibilityElement(children: .contain) + .accessibilityIdentifier("ConferencesGrid") + .accessibilityElement(children: .contain) } } diff --git a/CCCTube/Features/Search/SearchSuggestion.swift b/CCCTube/Features/Search/SearchSuggestion.swift index ec7f3e7..8f1e10c 100644 --- a/CCCTube/Features/Search/SearchSuggestion.swift +++ b/CCCTube/Features/Search/SearchSuggestion.swift @@ -30,7 +30,8 @@ extension SearchSuggestion { "Cybersecurity", "Digital rights", "Ethical hacking", "Data Science", "Internet governance", "Hacktivism", "Open source", "Free software", - "Malware analysis", "Data protection", "Forensics", "Social engineering", "Network security", + "Malware analysis", "Data protection", "Forensics", "Social engineering", + "Network security", "Virtual reality (VR)", "Augmented reality (AR)", "Wearable tech", "Penetration testing", "Bug bounties", "Reverse engineering", "Dark web", "Anonymity", diff --git a/CCCTube/Features/Search/SearchView.swift b/CCCTube/Features/Search/SearchView.swift index cd4ea18..a510e76 100644 --- a/CCCTube/Features/Search/SearchView.swift +++ b/CCCTube/Features/Search/SearchView.swift @@ -20,41 +20,41 @@ struct SearchView: View { NavigationStack { Group { #if os(tvOS) - List { - ForEach(results) { talk in - NavigationLink { - TalkView(talk: talk) - } label: { - TalkListItem(talk: talk) - } + List { + ForEach(results) { talk in + NavigationLink { + TalkView(talk: talk) + } label: { + TalkListItem(talk: talk) + } + } } - } #else - if isLoading { - ProgressView() - } else if results.isEmpty && !query.isEmpty { - Text("No talks found") - } else if results.isEmpty { - List { - ForEach(suggestions) { suggestion in - Button(suggestion.title) { - query = suggestion.title - runSearch() + if isLoading { + ProgressView() + } else if results.isEmpty && !query.isEmpty { + Text("No talks found") + } else if results.isEmpty { + List { + ForEach(suggestions) { suggestion in + Button(suggestion.title) { + query = suggestion.title + runSearch() + } + .foregroundColor(.accentColor) } - .foregroundColor(.accentColor) + } + .listStyle(.plain) + } else { + ScrollView { + TalksGrid(talks: results) } } - .listStyle(.plain) - } else { - ScrollView { - TalksGrid(talks: results) - } - } #endif } #if !os(tvOS) - .navigationTitle("Search") + .navigationTitle("Search") #endif .searchable(text: $query, prompt: "Search talks...") .onChange(of: query) { _, query in @@ -64,11 +64,11 @@ struct SearchView: View { runSearch(query) } #if os(tvOS) - .searchSuggestions { - ForEach(suggestions) { suggest in - Text(suggest.title).searchCompletion(suggest.title) + .searchSuggestions { + ForEach(suggestions) { suggest in + Text(suggest.title).searchCompletion(suggest.title) + } } - } #endif .onAppear(perform: runSearch) .onSubmit(of: .search, runSearch) diff --git a/CCCTube/Features/Talk/MediaAnalyzer.swift b/CCCTube/Features/Talk/MediaAnalyzer.swift index d15bc23..0f9acdf 100644 --- a/CCCTube/Features/Talk/MediaAnalyzer.swift +++ b/CCCTube/Features/Talk/MediaAnalyzer.swift @@ -14,7 +14,10 @@ struct MediaAnalyzer { let asset = AVURLAsset(url: recording.recordingURL) let metadata = try await asset.load(.metadata) for meta in metadata { - if meta.identifier == .commonIdentifierCopyrights || meta.identifier == .id3MetadataCopyright || meta.identifier == .iTunesMetadataCopyright { + if meta.identifier == .commonIdentifierCopyrights + || meta.identifier == .id3MetadataCopyright + || meta.identifier == .iTunesMetadataCopyright + { return try await meta.load(.stringValue) } } diff --git a/CCCTube/Features/Talk/TalkCell.swift b/CCCTube/Features/Talk/TalkCell.swift index 79d64b1..f1627ec 100644 --- a/CCCTube/Features/Talk/TalkCell.swift +++ b/CCCTube/Features/Talk/TalkCell.swift @@ -5,8 +5,8 @@ // Created by Mathijs Bernson on 03/06/2024. // -import SwiftUI import CCCApi +import SwiftUI struct TalkCell: View { let talk: Talk @@ -20,16 +20,13 @@ struct TalkCell: View { .font(.headline) .lineLimit(1) - ( - Text(talk.conferenceTitle) + - Text(verbatim: " • ") + - Text("\(talk.viewCount) views") + - Text(verbatim: " • ") + - Text(talk.releaseDate ?? talk.updatedAt, format: .relative(presentation: .named)) - ) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) + (Text(talk.conferenceTitle) + Text(verbatim: " • ") + + Text("\(talk.viewCount) views") + Text(verbatim: " • ") + + Text( + talk.releaseDate ?? talk.updatedAt, format: .relative(presentation: .named))) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/CCCTube/Features/Talk/TalkMetadataFactory.swift b/CCCTube/Features/Talk/TalkMetadataFactory.swift index a44b20d..5314742 100644 --- a/CCCTube/Features/Talk/TalkMetadataFactory.swift +++ b/CCCTube/Features/Talk/TalkMetadataFactory.swift @@ -42,7 +42,9 @@ struct TalkMetadataFactory { typealias AVMetadataValue = NSCopying & NSObjectProtocol - func createMetadataItem(for identifier: AVMetadataIdentifier, value: AVMetadataValue, language: String?) -> AVMetadataItem { + func createMetadataItem( + for identifier: AVMetadataIdentifier, value: AVMetadataValue, language: String? + ) -> AVMetadataItem { let item = AVMutableMetadataItem() item.identifier = identifier item.value = value diff --git a/CCCTube/Features/Talk/TalkPlayerView.swift b/CCCTube/Features/Talk/TalkPlayerView.swift index 30fd02e..c800517 100644 --- a/CCCTube/Features/Talk/TalkPlayerView.swift +++ b/CCCTube/Features/Talk/TalkPlayerView.swift @@ -18,30 +18,30 @@ struct TalkPlayerView: View { var body: some View { VideoPlayerView(player: viewModel.player) - .accessibilityIdentifier("Video") - .ignoresSafeArea() - .task(id: recording) { - guard recording != viewModel.currentRecording else { return } + .accessibilityIdentifier("Video") + .ignoresSafeArea() + .task(id: recording) { + guard recording != viewModel.currentRecording else { return } - isLoading = true - await viewModel.prepareForPlayback(recording: recording, talk: talk) - if automaticallyStartsPlayback { - viewModel.play() - } else { - await viewModel.preroll() + isLoading = true + await viewModel.prepareForPlayback(recording: recording, talk: talk) + if automaticallyStartsPlayback { + viewModel.play() + } else { + await viewModel.preroll() + } + isLoading = false } - isLoading = false - } - .onDisappear { - viewModel.pause() - } - #if os(iOS) - .overlay { - if isLoading { - VideoProgressIndicator() + .onDisappear { + viewModel.pause() } - } - #endif + #if os(iOS) + .overlay { + if isLoading { + VideoProgressIndicator() + } + } + #endif } } diff --git a/CCCTube/Features/Talk/TalkPlayerViewModel.swift b/CCCTube/Features/Talk/TalkPlayerViewModel.swift index 28b70d5..63f76e7 100644 --- a/CCCTube/Features/Talk/TalkPlayerViewModel.swift +++ b/CCCTube/Features/Talk/TalkPlayerViewModel.swift @@ -18,7 +18,8 @@ final class TalkPlayerViewModel { var currentRecording: Recording? private let factory = TalkMetadataFactory() - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TalkPlayerViewModel") + private let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, category: "TalkPlayerViewModel") func prepareForPlayback(recording: Recording, talk: Talk) async { let item = AVPlayerItem(url: recording.recordingURL) @@ -27,11 +28,14 @@ final class TalkPlayerViewModel { let player = AVPlayer(playerItem: item) self.player = player self.currentRecording = recording - logger.info("Preparing playback of recording: \(recording.recordingURL.absoluteString, privacy: .public)") + logger.info( + "Preparing playback of recording: \(recording.recordingURL.absoluteString, privacy: .public)" + ) if let imageURL = talk.posterURL ?? talk.thumbURL, - let posterImageData = try? await factory.fetchImageData(forURL: imageURL), - let posterImageMetadata = factory.createArtworkMetadataItem(imageData: posterImageData) { + let posterImageData = try? await factory.fetchImageData(forURL: imageURL), + let posterImageMetadata = factory.createArtworkMetadataItem(imageData: posterImageData) + { item.externalMetadata.append(posterImageMetadata) } } diff --git a/CCCTube/Features/Talk/TalkView.swift b/CCCTube/Features/Talk/TalkView.swift index a8b8c7e..19b2580 100644 --- a/CCCTube/Features/Talk/TalkView.swift +++ b/CCCTube/Features/Talk/TalkView.swift @@ -20,16 +20,20 @@ struct TalkView: View { HStack(alignment: .top) { TalkMainView(talk: talk, viewModel: viewModel) - TalkMetaView(talk: talk, selectedRecording: $selectedRecording, viewModel: viewModel) - .frame(maxWidth: 480, maxHeight: .infinity) + TalkMetaView( + talk: talk, selectedRecording: $selectedRecording, viewModel: viewModel + ) + .frame(maxWidth: 480, maxHeight: .infinity) } #else VStack(spacing: 20) { TalkMainView(talk: talk, viewModel: viewModel) - TalkMetaView(talk: talk, selectedRecording: $selectedRecording, viewModel: viewModel) - .padding(.horizontal) - .frame(maxWidth: .infinity, alignment: .leading) + TalkMetaView( + talk: talk, selectedRecording: $selectedRecording, viewModel: viewModel + ) + .padding(.horizontal) + .frame(maxWidth: .infinity, alignment: .leading) } .navigationBarTitleDisplayMode(.inline) #endif @@ -71,7 +75,7 @@ private struct TVPlayerView: View { .hoverEffect() } #if !os(tvOS) - .buttonStyle(.plain) + .buttonStyle(.plain) #endif .disabled(recording == nil) .fullScreenCover(item: $selectedRecording) { recording in @@ -92,7 +96,9 @@ private struct TalkMainView: View { #else Group { if let preferredRecording = viewModel.preferredRecording { - TalkPlayerView(talk: talk, recording: preferredRecording, automaticallyStartsPlayback: true) + TalkPlayerView( + talk: talk, recording: preferredRecording, + automaticallyStartsPlayback: true) } else { Rectangle() .fill(.black) @@ -119,7 +125,7 @@ private struct TalkMainView: View { .focusSection() .buttonStyle(.card) #endif - .multilineTextAlignment(.leading) + .multilineTextAlignment(.leading) #if !os(tvOS) .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -146,9 +152,13 @@ private struct CopyrightView: View { Text(string) case .unknown: if let link = talk.link { - Text("No copyright information encoded in video. Please refer to the schedule of the organizer of \(talk.conferenceTitle) at: \(link)") + Text( + "No copyright information encoded in video. Please refer to the schedule of the organizer of \(talk.conferenceTitle) at: \(link)" + ) } else { - Text("No copyright information encoded in video. Please refer to the website of the organizer of \(talk.conferenceTitle) at: \(talk.conferenceURL)") + Text( + "No copyright information encoded in video. Please refer to the website of the organizer of \(talk.conferenceTitle) at: \(talk.conferenceURL)" + ) } } } diff --git a/CCCTube/Features/Talk/TalksGrid.swift b/CCCTube/Features/Talk/TalksGrid.swift index 35c6cb2..6a12df7 100644 --- a/CCCTube/Features/Talk/TalksGrid.swift +++ b/CCCTube/Features/Talk/TalksGrid.swift @@ -12,9 +12,9 @@ struct TalksGrid: View { let talks: [Talk] var body: some View { #if os(tvOS) - TalksGridTV(talks: talks) + TalksGridTV(talks: talks) #else - TalksGridRegular(talks: talks) + TalksGridRegular(talks: talks) #endif } } @@ -31,9 +31,9 @@ private struct TalksGridRegular: View { } label: { TalkCell(talk: talk) #if os(visionOS) - .padding() - .contentShape(RoundedRectangle(cornerRadius: 16)) - .hoverEffect(.lift) + .padding() + .contentShape(RoundedRectangle(cornerRadius: 16)) + .hoverEffect(.lift) #endif } .buttonStyle(.plain) @@ -47,34 +47,34 @@ private struct TalksGridRegular: View { } #if os(tvOS) -private struct TalksGridTV: View { - let talks: [Talk] - let columns: [GridItem] = Array(repeating: GridItem(), count: 4) + private struct TalksGridTV: View { + let talks: [Talk] + let columns: [GridItem] = Array(repeating: GridItem(), count: 4) - var body: some View { - LazyVGrid(columns: columns, spacing: 64) { - ForEach(talks) { talk in - VStack(alignment: .leading) { - NavigationLink { - TalkView(talk: talk) - } label: { - TalkThumbnail(talk: talk) - } + var body: some View { + LazyVGrid(columns: columns, spacing: 64) { + ForEach(talks) { talk in + VStack(alignment: .leading) { + NavigationLink { + TalkView(talk: talk) + } label: { + TalkThumbnail(talk: talk) + } - Text(talk.title) - .font(.headline) - .lineLimit(2, reservesSpace: true) + Text(talk.title) + .font(.headline) + .lineLimit(2, reservesSpace: true) + } } } + .padding() + .multilineTextAlignment(.center) + .focusSection() + .buttonStyle(.card) + .accessibilityIdentifier("TalksGrid") + .accessibilityElement(children: .contain) } - .padding() - .multilineTextAlignment(.center) - .focusSection() - .buttonStyle(.card) - .accessibilityIdentifier("TalksGrid") - .accessibilityElement(children: .contain) } -} #endif struct TalksGrid_Previews: PreviewProvider { diff --git a/CCCTube/Features/VideoPlayer/VideoPlayerView.swift b/CCCTube/Features/VideoPlayer/VideoPlayerView.swift index 2845480..b304eae 100644 --- a/CCCTube/Features/VideoPlayer/VideoPlayerView.swift +++ b/CCCTube/Features/VideoPlayer/VideoPlayerView.swift @@ -6,8 +6,8 @@ // import AVKit -import os.log import SwiftUI +import os.log struct VideoPlayerView: UIViewControllerRepresentable { let player: AVPlayer? @@ -28,7 +28,8 @@ struct VideoPlayerView: UIViewControllerRepresentable { return playerViewController } - func updateUIViewController(_ playerViewController: VideoPlayerViewController, context: Context) { + func updateUIViewController(_ playerViewController: VideoPlayerViewController, context: Context) + { playerViewController.player = player } @@ -36,16 +37,22 @@ struct VideoPlayerView: UIViewControllerRepresentable { VideoPlayerCoordinator() } - static func dismantleUIViewController(_ playerViewController: VideoPlayerViewController, coordinator: Coordinator) { + static func dismantleUIViewController( + _ playerViewController: VideoPlayerViewController, coordinator: Coordinator + ) { playerViewController.player?.cancelPendingPrerolls() playerViewController.player?.pause() } } class VideoPlayerCoordinator: NSObject, AVPlayerViewControllerDelegate { - let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VideoPlayerCoordinator") + let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, category: "VideoPlayerCoordinator") - func playerViewController(_ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: Error) { + func playerViewController( + _ playerViewController: AVPlayerViewController, + failedToStartPictureInPictureWithError error: Error + ) { logger.error("Failed to start picture in picture: \(error)") } } diff --git a/CCCTube/URLParser.swift b/CCCTube/URLParser.swift index 290d0f3..23cbf71 100644 --- a/CCCTube/URLParser.swift +++ b/CCCTube/URLParser.swift @@ -14,7 +14,7 @@ struct URLParser { let components = url.pathComponents.filter { $0 != "/" } guard components.count >= 1, - let id = components.first + let id = components.first else { return nil } if components.last == "play" { diff --git a/CCCTubeTests/URLParserTests.swift b/CCCTubeTests/URLParserTests.swift index 86ccfb2..5ee4153 100644 --- a/CCCTubeTests/URLParserTests.swift +++ b/CCCTubeTests/URLParserTests.swift @@ -5,9 +5,10 @@ // Created by Mathijs Bernson on 30/07/2022. // -@testable import CCCTube import XCTest +@testable import CCCTube + class URLParserTests: XCTestCase { let parser = URLParser() diff --git a/CCCTubeUITests/CCCTubeUIScreenshotTests.swift b/CCCTubeUITests/CCCTubeUIScreenshotTests.swift index 6449380..f836ddd 100644 --- a/CCCTubeUITests/CCCTubeUIScreenshotTests.swift +++ b/CCCTubeUITests/CCCTubeUIScreenshotTests.swift @@ -17,15 +17,17 @@ final class CCCTubeUIScreenshotTests: XCTestCase { } func testRecentTalks() { - XCTAssertTrue(app.otherElements["TalksGrid"].buttons.firstMatch.waitForExistence(timeout: 3.0)) - wait(forTimeInterval: 2.0) // Give the app some time to load + XCTAssertTrue( + app.otherElements["TalksGrid"].buttons.firstMatch.waitForExistence(timeout: 3.0)) + wait(forTimeInterval: 2.0) // Give the app some time to load takeScreenshot("RecentTalks") } func testConferences() { app.buttons["Conferences"].firstMatch.tap() - XCTAssertTrue(app.otherElements["ConferencesGrid"].buttons.firstMatch.waitForExistence(timeout: 3.0)) - wait(forTimeInterval: 2.0) // Give the app some time to load + XCTAssertTrue( + app.otherElements["ConferencesGrid"].buttons.firstMatch.waitForExistence(timeout: 3.0)) + wait(forTimeInterval: 2.0) // Give the app some time to load takeScreenshot("Conferences") } diff --git a/README.md b/README.md index d791149..c08603c 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,18 @@ It uses the native video player, which supports behaviours such as picture-in-pi The app is localized to English, German and Dutch. +### Code formatting + +Apple's `swift-format` is used for linting and formatting the source code. +You can run it using the following commands: + +``` +# Lint +xcrun swift-format lint -r . +# Format all files +xcrun swift-format format -i -r . +``` + ## License As all other C3VOC tools, this software is distributed under the GPL v3. See the `LICENSE.txt` file. diff --git a/TopShelf/TopShelfContentFactory.swift b/TopShelf/TopShelfContentFactory.swift index ea92ff7..39ff064 100644 --- a/TopShelf/TopShelfContentFactory.swift +++ b/TopShelf/TopShelfContentFactory.swift @@ -10,7 +10,9 @@ import Foundation import TVServices struct TopShelfContentFactory { - func makeTopShelfSections(recentTalks: [Talk], popularTalks: [Talk]) -> [TVTopShelfItemCollection] { + func makeTopShelfSections(recentTalks: [Talk], popularTalks: [Talk]) + -> [TVTopShelfItemCollection] + { let recents = TVTopShelfItemCollection(items: makeTopShelfItems(talks: recentTalks)) recents.title = String(localized: "Recent talks", comment: "Top shelf title")