diff --git a/Fosdem.xcodeproj/project.pbxproj b/Fosdem.xcodeproj/project.pbxproj index c87b7b8..5e3f96c 100644 --- a/Fosdem.xcodeproj/project.pbxproj +++ b/Fosdem.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 925321602925716A004C7E5C /* EventUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9253215F2925716A004C7E5C /* EventUserInfo.swift */; }; 925B0261298EAE0E00AFA83D /* LiveIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925B0260298EAE0E00AFA83D /* LiveIcon.swift */; }; 925B0263298EB2A400AFA83D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925B0262298EB2A400AFA83D /* VideoPlayer.swift */; }; + 9268D79A298F251E0067A6B1 /* ListPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9268D799298F251E0067A6B1 /* ListPredicate.swift */; }; 92A2BDF82948CAE50034864F /* schedule_2019.xml in Resources */ = {isa = PBXBuildFile; fileRef = 92A2BDF72948CAE50034864F /* schedule_2019.xml */; }; 92A2BDFC2948D4AC0034864F /* schedule_2023.xml in Resources */ = {isa = PBXBuildFile; fileRef = 92A2BDFA2948D4AC0034864F /* schedule_2023.xml */; }; 92A2BDFD2948D4AC0034864F /* schedule_2022.xml in Resources */ = {isa = PBXBuildFile; fileRef = 92A2BDFB2948D4AC0034864F /* schedule_2022.xml */; }; @@ -95,6 +96,7 @@ 9253215F2925716A004C7E5C /* EventUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventUserInfo.swift; sourceTree = ""; }; 925B0260298EAE0E00AFA83D /* LiveIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveIcon.swift; sourceTree = ""; }; 925B0262298EB2A400AFA83D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; + 9268D799298F251E0067A6B1 /* ListPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListPredicate.swift; sourceTree = ""; }; 92A2BDF72948CAE50034864F /* schedule_2019.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = schedule_2019.xml; sourceTree = ""; }; 92A2BDF92948CC5E0034864F /* FosdemTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = FosdemTests.xctestplan; path = Fosdem.xcodeproj/FosdemTests.xctestplan; sourceTree = ""; }; 92A2BDFA2948D4AC0034864F /* schedule_2023.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = schedule_2023.xml; sourceTree = ""; }; @@ -176,6 +178,7 @@ 92B987FB2981CD10007574EB /* AboutView.swift */, 925B0260298EAE0E00AFA83D /* LiveIcon.swift */, 925B0262298EB2A400AFA83D /* VideoPlayer.swift */, + 9268D799298F251E0067A6B1 /* ListPredicate.swift */, ); path = Views; sourceTree = ""; @@ -426,6 +429,7 @@ 142A8742222473A40034F6D7 /* UIColor+hexstring.swift in Sources */, 146A8677220601DD008A61AD /* XmlFinder.swift in Sources */, 92CE31AF28FC49DC0073813E /* YearHelper.swift in Sources */, + 9268D79A298F251E0067A6B1 /* ListPredicate.swift in Sources */, 92C271D02922292500E8C25D /* PreviewEvent.swift in Sources */, 925B0263298EB2A400AFA83D /* VideoPlayer.swift in Sources */, 14D2771C2205D07200740042 /* Fosdem.xcdatamodeld in Sources */, diff --git a/Fosdem/Info.plist b/Fosdem/Info.plist index b95319f..8346843 100644 --- a/Fosdem/Info.plist +++ b/Fosdem/Info.plist @@ -2,13 +2,6 @@ - UILaunchScreen - - UIColorName - Launchscreen-background - UIImageName - Launchscreen-logo - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -25,10 +18,21 @@ 1.2 CFBundleVersion 1 - LSRequiresIPhoneOS - ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + + UIBackgroundModes + + audio + + UILaunchScreen + + UIColorName + Launchscreen-background + UIImageName + Launchscreen-logo + UIRequiredDeviceCapabilities armv7 diff --git a/Fosdem/Utils/UrlHelper.swift b/Fosdem/Utils/UrlHelper.swift index cbe2564..5af8a0f 100644 --- a/Fosdem/Utils/UrlHelper.swift +++ b/Fosdem/Utils/UrlHelper.swift @@ -18,7 +18,7 @@ extension Room { } func chatLink() -> URL { - return URL(string: "https://chat.fosdem.org/#/room/%23\(YearHelper().year)-\(name.lowercased()):fosdem.org")! + return URL(string: "https://chat.fosdem.org/#/room/%23\(YearHelper().year)-\(shortName.lowercased()):fosdem.org")! } } diff --git a/Fosdem/Views/EventDetailHeader.swift b/Fosdem/Views/EventDetailHeader.swift index 3f46186..00536a7 100644 --- a/Fosdem/Views/EventDetailHeader.swift +++ b/Fosdem/Views/EventDetailHeader.swift @@ -41,12 +41,13 @@ struct EventDetailHeader: View { Divider() } SwiftUI.Link(destination: event.room.chatLink()) { - Label(event.room.name, systemImage: "bubble.left.circle") + Label("button.chat", systemImage: "bubble.left.circle") } if let item = Conference.roomStates.first(where: { $0.roomname == event.room.name }) { - Text("-").foregroundColor(.secondary) - Text(item.full ? "Full" : "Available").foregroundColor(item.full ? .red : .green) + Divider() + Text(item.full ? "room.full" : "room.available") + .foregroundColor(item.full ? .red : .green) } } Divider() diff --git a/Fosdem/Views/EventDetailView.swift b/Fosdem/Views/EventDetailView.swift index 388ffd8..cee0e3c 100644 --- a/Fosdem/Views/EventDetailView.swift +++ b/Fosdem/Views/EventDetailView.swift @@ -51,7 +51,12 @@ struct EventDetailView: View { Picker("Test", selection: $selectedTabIndex, content: { Text("Description").tag(0) Text("Links").disabled(event.links.isEmpty).tag(1) - if event.isOngoing { Text("Live Video").tag(2) } + if event.isOngoing { + HStack { + LiveIcon() + Text("Live Video").tag(2) + } + } }).pickerStyle(.segmented) ZStack { @@ -59,10 +64,10 @@ struct EventDetailView: View { HTMLFormattedText(event.desc ?? "", colorScheme: colorScheme) } if selectedTabIndex == 1 { - if event.links.isEmpty { - Text("No links").foregroundColor(.gray).font(.title2).padding() - } else { - VStack(alignment: .leading) { + VStack(alignment: .leading) { + if event.links.isEmpty { + Text("No links").foregroundColor(.gray).font(.title2).padding() + } else { ForEach(Array(event.links)) { link in SwiftUI.Link(destination: URL(string: link.href)!) { Label(link.name, systemImage: "link") @@ -72,7 +77,9 @@ struct EventDetailView: View { } } if selectedTabIndex == 2 { - VideoPlayer(event.room.liveStreamLink()) + VStack { + VideoPlayer(event.room.liveStreamLink()) + } } } } diff --git a/Fosdem/Views/EventListView.swift b/Fosdem/Views/EventListView.swift index 92cf083..38f7436 100644 --- a/Fosdem/Views/EventListView.swift +++ b/Fosdem/Views/EventListView.swift @@ -10,8 +10,11 @@ import SwiftUI struct EventListView: View { @State private var query = "" - @State private var onlyBookmark: Bool = false + private var listPredicate: ListPredicate = ListPredicate() @State private var isSheetPresented: Bool = false + @State private var bookmarks: Bool = false + @State private var future: Bool = false + @State private var localTime: Bool = false @State private var visibility: NavigationSplitViewVisibility = .doubleColumn var suffix = "" @@ -47,57 +50,56 @@ struct EventListView: View { predicate: nil ) var roomEvents: SectionedFetchResults + var trackList: some View { + List(trackEvents) { section in + Section(header: Text("\(section.id)")) { + ForEach(section) { event in + NavigationLink(value: event, label: { ListItem(event, bookmarkEmphasis: bookmarks) }) + } + } + }.overlay(Group { + if trackEvents.isEmpty && query.isEmpty { + ProgressView("Still loading events").progressViewStyle(CircularProgressViewStyle()) + } + }).listStyle(.plain) + } + + var roomList: some View { + List(roomEvents) { section in + Section(header: Text("\(section.id)")) { + ForEach(section) { event in + NavigationLink(value: event, label: { ListItem(event, bookmarkEmphasis: bookmarks) }) + } + } + }.overlay(Group { + if trackEvents.isEmpty && query.isEmpty { + ProgressView("Still loading events").progressViewStyle(CircularProgressViewStyle()) + } + }).listStyle(.plain) + } + + var personList: some View { + List(personEvents) { person in + Section(person.name) { + ForEach(Array(person.events)) { event in + NavigationLink(value: event, label: { ListItem(event, bookmarkEmphasis: bookmarks) }) + } + } + }.overlay(Group { + if personEvents.isEmpty && query.isEmpty { + ProgressView("Still loading events").progressViewStyle(CircularProgressViewStyle()) + } + }).listStyle(.plain) + } + var body: some View { NavigationStack { TabView { - List(trackEvents) { section in - Section(header: Text("\(section.id)")) { - ForEach(section) { event in - NavigationLink(value: event, label: { ListItem(event) }) - } - } - }.overlay(Group { - if trackEvents.isEmpty { - ProgressView("Still loading events").progressViewStyle(CircularProgressViewStyle()) - } - }).tabItem { - Label("Tracks", systemImage: "road.lanes") - }.onChange(of: query) { newValue in - trackEvents.nsPredicate = searchPredicate(query: newValue, keypaths: [#keyPath(Event.title), #keyPath(Event.track.name)]) - }.listStyle(.plain) + trackList.tabItem { Label("Tracks", systemImage: "road.lanes") } - List(personEvents) { person in - Section(person.name) { - ForEach(Array(person.events)) { event in - NavigationLink(value: event, label: { ListItem(event) }) - } - } - }.onChange(of: query) { newValue in - personEvents.nsPredicate = searchPredicate(query: newValue, keypaths: [#keyPath(Person.name)]) - }.tabItem { - Label("People", systemImage: "person.3") - }.overlay(Group { - if personEvents.isEmpty { - ProgressView("Still loading events").progressViewStyle(CircularProgressViewStyle()) - } - }).listStyle(.plain) - - List(roomEvents) { section in - Section(header: Text("\(section.id)")) { - ForEach(section) { event in - NavigationLink(value: event, label: { ListItem(event) }) - } - } - }.onChange(of: query) { newValue in - roomEvents.nsPredicate = searchPredicate(query: newValue, keypaths: [#keyPath(Event.room.name), #keyPath(Event.title)]) - }.tabItem { - Label("Rooms", systemImage: "door.left.hand.open") - }.overlay(Group { - if roomEvents.isEmpty { - ProgressView("Still loading events").progressViewStyle(CircularProgressViewStyle()) - } - }).listStyle(.plain) + personList.tabItem { Label("People", systemImage: "person.3") } + roomList.tabItem { Label("Rooms", systemImage: "door.left.hand.open") } List(myEvents) { event in NavigationLink(value: event, label: { ListItem(event, bookmarkEmphasis: false) }) @@ -108,13 +110,22 @@ struct EventListView: View { Label("No Bookmarks yet", systemImage: "bookmark.slash") } }) - }.onChange(of: onlyBookmark, perform: { value in - let predicate = NSPredicate(format: "userInfo.favorite == YES", []) - trackEvents.nsPredicate = value ? predicate : nil - roomEvents.nsPredicate = value ? predicate : nil - let personPredicate = NSPredicate(format: "ANY events.userInfo.favorite == YES", []) - personEvents.nsPredicate = value ? personPredicate : nil - }).searchable( + }.onChange(of: bookmarks, perform: { value in + listPredicate.bookmarks = bookmarks + trackEvents.nsPredicate = listPredicate.predicate(.track) + roomEvents.nsPredicate = listPredicate.predicate(.room) + personEvents.nsPredicate = listPredicate.predicate(.person) + }).onChange(of: future, perform: { value in + listPredicate.future = future + trackEvents.nsPredicate = listPredicate.predicate(.track) + roomEvents.nsPredicate = listPredicate.predicate(.room) + personEvents.nsPredicate = listPredicate.predicate(.person) + }).onChange(of: query) { newValue in + listPredicate.searchQuery = newValue + trackEvents.nsPredicate = listPredicate.predicate(.track) + roomEvents.nsPredicate = listPredicate.predicate(.room) + personEvents.nsPredicate = listPredicate.predicate(.person) + }.searchable( text: $query, placement: .navigationBarDrawer ).refreshable { @@ -134,26 +145,19 @@ struct EventListView: View { }, label: { Label("Filter", systemImage: "slider.horizontal.2.square.on.square")}) .sheet(isPresented: $isSheetPresented, content: { Form { - Toggle(isOn: $onlyBookmark, label: { Label("Bookmarks only", systemImage: "bookmark")}) + Section("Filters") { + Toggle(isOn: $bookmarks, label: { Label("Bookmarks only", systemImage: "bookmark")}) + Toggle(isOn: $future, label: { Label("Future events only", systemImage: "clock")}) + } + Section("Settings") { + Toggle(isOn: $localTime, label: { Label("button.timezone.local", systemImage: "globe")}) + } }.presentationDetents([.fraction(0.3)]) }) } }.navigationTitle("Fosdem \(YearHelper().year)") } } - - private func searchPredicate(query: String, keypaths: [String]) -> NSPredicate? { - if query.isEmpty { return nil } - var format: [String] = [] - var queries: [String] = [] - keypaths.forEach { keypath in - format.append("%K CONTAINS[cd] %@") - queries.append(keypath) - queries.append(query) - } - return NSPredicate(format: format.joined(separator: " OR "), argumentArray: queries) - } - } struct EventListView_Previews: PreviewProvider { diff --git a/Fosdem/Views/ListItem.swift b/Fosdem/Views/ListItem.swift index 7002b4c..7372452 100644 --- a/Fosdem/Views/ListItem.swift +++ b/Fosdem/Views/ListItem.swift @@ -21,17 +21,21 @@ struct ListItem: View { var body: some View { HStack { - if let lastSeen = event.userInfo.lastSeen, event.lastUpdated > lastSeen && !event.isEnded { - Circle().foregroundColor(.accentColor) - .frame(width: 7, height: 7) - } - VStack(alignment: .trailing) { - Text(event.startInFormat("EE").capitalized) - .foregroundColor(.secondary) - .italic() - Text(DateFormatter.localizedString(from: event.start, dateStyle: .none, timeStyle: .short) ) - .foregroundColor(.secondary) - .italic() + ZStack(alignment: .topLeading) { + if let lastSeen = event.userInfo.lastSeen, + event.lastUpdated > lastSeen && !event.isEnded { + Circle() + .foregroundColor(.accentColor) + .frame(width: 10, height: 10) + } + VStack(alignment: .trailing) { + Text(event.startInFormat("EE").capitalized) + .foregroundColor(.secondary) + .italic() + Text(DateFormatter.localizedString(from: event.start, dateStyle: .none, timeStyle: .short) ) + .foregroundColor(.secondary) + .italic() + } } if event.isOngoing { LiveIcon() } diff --git a/Fosdem/Views/ListPredicate.swift b/Fosdem/Views/ListPredicate.swift new file mode 100644 index 0000000..cdfe828 --- /dev/null +++ b/Fosdem/Views/ListPredicate.swift @@ -0,0 +1,74 @@ +// +// ListPredicate.swift +// Fosdem +// +// Created by Sean Molenaar on 05/02/2023. +// Copyright © 2023 Sean Molenaar. All rights reserved. +// + +import Foundation + +class ListPredicate: ObservableObject { + public var bookmarks: Bool = false + public var future: Bool = false + public var searchQuery: String = "" + + public func predicate(_ type: ListPredicateType) -> NSPredicate? { + if !future && !bookmarks && searchQuery.isEmpty { + return nil + } + + var predicates: [NSPredicate] = [] + if bookmarks { + switch(type) { + case .person: + predicates.append(NSPredicate(format: "ANY events.userInfo.favorite == YES")) + default: + predicates.append(NSPredicate(format: "userInfo.favorite == YES")) + } + + } + + if future { + let date = NSDate() + switch(type) { + case .person: + predicates.append(NSPredicate(format: "ANY events.start > %@", argumentArray: [date])) + default: + predicates.append(NSPredicate(format: "start > %@", argumentArray: [date])) + } + } + + if !searchQuery.isEmpty { + switch(type) { + case .person: + predicates.append(searchPredicate(keypaths: [#keyPath(Person.name)])) + case .room: + predicates.append(searchPredicate(keypaths: [#keyPath(Event.room.name), #keyPath(Event.title)])) + case .track: + predicates.append(searchPredicate(keypaths: [#keyPath(Event.track.name), #keyPath(Event.title)])) + default: + break + } + } + + return NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + } + + private func searchPredicate(keypaths: [String]) -> NSPredicate { + var format: [String] = [] + var queries: [String] = [] + keypaths.forEach { keypath in + format.append("%K CONTAINS[cd] %@") + queries.append(keypath) + queries.append(searchQuery) + } + return NSPredicate(format: format.joined(separator: " OR "), argumentArray: queries) + } +} + +enum ListPredicateType { + case person + case room + case track +} diff --git a/Fosdem/Views/VideoPlayer.swift b/Fosdem/Views/VideoPlayer.swift index 525790e..400e842 100644 --- a/Fosdem/Views/VideoPlayer.swift +++ b/Fosdem/Views/VideoPlayer.swift @@ -17,6 +17,13 @@ struct VideoPlayer: View { player.allowsExternalPlayback = true self.player = player + + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.playback) + } catch { + print("Setting category to AVAudioSessionCategoryPlayback failed.") + } } var body: some View {