Skip to content

Commit

Permalink
Improve search, lister views and thumbnails
Browse files Browse the repository at this point in the history
  • Loading branch information
mbernson committed Dec 29, 2023
1 parent a58ea94 commit a57a544
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 46 deletions.
18 changes: 16 additions & 2 deletions CCCApi/Sources/CCCApi/ApiService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,24 @@ public class ApiService: ObservableObject {
return response.events
}

public func popularTalks(year: Int) async throws -> [Talk] {
public enum PopularTalksYear {
case currentYear
case year(Int)

var yearValue: Int {
switch self {
case .currentYear:
Calendar.current.component(.year, from: Date.now)
case .year(let value):
value
}
}
}

public func popularTalks(in year: PopularTalksYear) async throws -> [Talk] {
let url = baseURL.appendingPathComponent("events").appendingPathComponent("popular")
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
components.queryItems = [URLQueryItem(name: "year", value: String(year))]
components.queryItems = [URLQueryItem(name: "year", value: String(year.yearValue))]
let (data, _) = try await session.data(from: components.url!)
let response = try decoder.decode(EventsResponse.self, from: data)
return response.events
Expand Down
1 change: 1 addition & 0 deletions CCCTube/Features/Conferences/ConferenceThumbnail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct ConferenceThumbnail: View {
ProgressView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(16 / 9, contentMode: .fit)
.background(.regularMaterial)
.cornerRadius(16)
Expand Down
7 changes: 7 additions & 0 deletions CCCTube/Features/Conferences/ConferenceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ struct ConferenceView: View {

var body: some View {
ScrollView {
#if os(tvOS)
VStack {
Text(conference.title)
.font(.largeTitle.bold())
.foregroundColor(.secondary)

TalksGrid(talks: talks)
}
#else
TalksGrid(talks: talks)
#endif
}
#if !os(tvOS)
.navigationTitle(conference.title)
#endif
.task {
do {
talks = try await api.conference(acronym: conference.acronym).events ?? []
Expand Down
12 changes: 3 additions & 9 deletions CCCTube/Features/Conferences/ConferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,9 @@ struct ConferencesGrid: View {
ConferenceThumbnail(conference: conference)
}

if #available(tvOS 16, iOS 16, *) {
Text(conference.title)
.font(.caption)
.lineLimit(2, reservesSpace: true)
} else {
Text(conference.title)
.font(.caption)
.lineLimit(2)
}
Text(conference.title)
.font(.caption)
.lineLimit(2, reservesSpace: true)
}
}
}
Expand Down
40 changes: 32 additions & 8 deletions CCCTube/Features/Search/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,47 @@ struct SearchView: View {

var body: some View {
NavigationStack {
List {
ForEach(viewModel.results) { talk in
NavigationLink {
TalkView(talk: talk)
} label: {
TalkListItem(talk: talk)
Group {
#if os(tvOS)
List {
ForEach(viewModel.results) { talk in
NavigationLink {
TalkView(talk: talk)
} label: {
TalkListItem(talk: talk)
}
}
}
#else
if viewModel.results.isEmpty {
List {
ForEach(suggestions) { suggestion in
Button(suggestion.title) {
viewModel.query = suggestion.title
runSearch()
}
.foregroundColor(.accentColor)
}
}
.listStyle(.plain)
} else {
ScrollView {
TalksGrid(talks: viewModel.results)
}
}
#endif
}
#if !os(tvOS)
.navigationTitle("Search")
#endif
.searchable(text: $viewModel.query, prompt: "Search talks...", suggestions: {
.searchable(text: $viewModel.query, prompt: "Search talks...")
#if os(tvOS)
.searchSuggestions {
ForEach(suggestions) { suggest in
Text(suggest.title).searchCompletion(suggest.title)
}
})
}
#endif
.onAppear(perform: runSearch)
.onSubmit(of: .search, runSearch)
.alert("Failed to load data from the media.ccc.de API", error: $viewModel.error)
Expand Down
12 changes: 11 additions & 1 deletion CCCTube/Features/Search/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import CCCApi
import Combine
import Foundation

@MainActor
@MainActor
class SearchViewModel: ObservableObject {
private let api = ApiService()

Expand All @@ -18,6 +18,16 @@ class SearchViewModel: ObservableObject {

@Published var error: Error?

private var cancellable: AnyCancellable?

init() {
cancellable = $query
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
.sink(receiveValue: { query in
self.search()
})
}

func search() {
Task {
do {
Expand Down
13 changes: 7 additions & 6 deletions CCCTube/Features/Talk/TalkListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
import CCCApi
import SwiftUI

private let minutesFormatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.allowedUnits = .minute
return f
}()

struct TalkListItem: View {
let talk: Talk
static let minutesFormatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.allowedUnits = .minute
return f
}()

var body: some View {
HStack(alignment: .top, spacing: 20) {
Expand Down Expand Up @@ -43,7 +44,7 @@ struct TalkListItem: View {
.font(.body)

HStack(alignment: .center, spacing: 20) {
Label("\(Self.minutesFormatter.string(from: talk.duration) ?? "0") min", systemImage: "clock")
Label("\(minutesFormatter.string(from: talk.duration) ?? "0") min", systemImage: "clock")

Label("Date \(talk.releaseDate, style: .date)", systemImage: "calendar")

Expand Down
4 changes: 3 additions & 1 deletion CCCTube/Features/Talk/TalkThumbnail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ struct TalkThumbnail: View {
let talk: Talk

var body: some View {
AsyncImage(url: talk.thumbURL) { phase in
AsyncImage(url: talk.thumbURL ?? talk.posterURL) { phase in
if let image = phase.image {
image.resizable().scaledToFit()
} else if phase.error != nil {
Image(systemName: "xmark.circle")
.foregroundStyle(.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.regularMaterial)
} else {
ProgressView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(16 / 9, contentMode: .fill)
}
}
Expand Down
5 changes: 4 additions & 1 deletion CCCTube/Features/Talk/TalkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,17 @@ private struct TalkMainView: View {
Button {
talkDescription = TalkDescription(text: description)
} label: {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 20) {
Text(shortDescription)
.lineLimit(5)
.multilineTextAlignment(.leading)

Text("Read more")
.foregroundColor(.accentColor)
}
#if os(tvOS)
.padding()
#endif
}
.foregroundStyle(.primary)
.buttonBorderShape(.roundedRectangle)
Expand Down
12 changes: 3 additions & 9 deletions CCCTube/Features/Talk/TalksGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,9 @@ struct TalksGrid: View {
#endif
}

if #available(tvOS 16, iOS 16, *) {
Text(talk.title)
.font(.subheadline)
.lineLimit(2, reservesSpace: true)
} else {
Text(talk.title)
.font(.subheadline)
.lineLimit(2)
}
Text(talk.title)
.font(.subheadline)
.lineLimit(2, reservesSpace: true)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion CCCTube/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Suche nach Talks..."
"value" : "Nach Vorträgen suchen..."
}
},
"nl" : {
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ Gewoon lekker gaan zitten, CCC Tube selecteren, en duik in de wonderbaarlijk div
## Development

This app is written in Swift and SwiftUI. It tries to stay close to the native conventions, using native user interface elements in order to fit in on the Apple platforms that the app supports.

It uses the native video player, which supports behaviours such as picture-in-picture out of the box.
It uses the native video player, which supports behaviours such as picture-in-picture out of the box. On tvOS, it supports a top shelf extension. Etcetera.

The app is localized to English, German and Dutch.

Expand Down
5 changes: 3 additions & 2 deletions TopShelf/ContentProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ class ContentProvider: TVTopShelfContentProvider {

override func loadTopShelfContent() async -> TVTopShelfContent? {
do {
let recentTalks = try await api.recentTalks().prefix(10)
let sections = factory.makeTopShelfSections(recentTalks: Array(recentTalks))
async let recentTalks = api.recentTalks()
async let popularTalks = api.popularTalks()
let sections = factory.makeTopShelfSections(recentTalks: try await recentTalks, popularTalks: try await popularTalks)
return TVTopShelfSectionedContent(sections: sections)
} catch {
return nil
Expand Down
19 changes: 18 additions & 1 deletion TopShelf/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
{
"sourceLanguage" : "en",
"strings" : {
"Popular talks" : {
"comment" : "Top shelf title",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Beliebte Vorträge"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Populaire talks"
}
}
}
},
"Recent talks" : {
"comment" : "Top shelf title",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aktuelle Gespräche"
"value" : "Aktuelle Vorträge"
}
},
"nl" : {
Expand Down
9 changes: 6 additions & 3 deletions TopShelf/TopShelfContentFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import Foundation
import TVServices

struct TopShelfContentFactory {
func makeTopShelfSections(recentTalks: [Talk]) -> [TVTopShelfItemCollection<TVTopShelfSectionedItem>] {
func makeTopShelfSections(recentTalks: [Talk], popularTalks: [Talk]) -> [TVTopShelfItemCollection<TVTopShelfSectionedItem>] {
let recents = TVTopShelfItemCollection(items: makeTopShelfItems(talks: recentTalks))
recents.title = NSLocalizedString("Recent talks", comment: "Top shelf title")
recents.title = String(localized: "Recent talks", comment: "Top shelf title")

return [recents]
let popular = TVTopShelfItemCollection(items: makeTopShelfItems(talks: popularTalks))
popular.title = String(localized: "Popular talks", comment: "Top shelf title")

return [recents, popular]
}

private func makeTopShelfItems(talks: [Talk]) -> [TVTopShelfSectionedItem] {
Expand Down

0 comments on commit a57a544

Please sign in to comment.