diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 265c2aaf..13ed6492 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -12,74 +12,72 @@ import JellyfinAPI struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { var path = Path() - + let tl = CGPoint(x: rect.minX, y: rect.minY) let tr = CGPoint(x: rect.maxX, y: rect.minY) let br = CGPoint(x: rect.maxX, y: rect.maxY) let bls = CGPoint(x: rect.minX + 10, y: rect.maxY) let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10) - + path.move(to: tl) path.addLine(to: tr) path.addLine(to: br) path.addLine(to: bls) path.addRelativeArc(center: blc, radius: 10, - startAngle: Angle.degrees(90), delta: Angle.degrees(90)) - + startAngle: Angle.degrees(90), delta: Angle.degrees(90)) + return path } } struct ContinueWatchingView: View { var items: [BaseItemDto] - + var body: some View { ScrollView(.horizontal, showsIndicators: false) { - if items.count > 0 { - LazyHStack { - Spacer().frame(width: 14) - ForEach(items, id: \.id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - Spacer().frame(height: 10) - ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) - .frame(width: 320, height: 180) - .cornerRadius(10) - .overlay( - Group { - if item.type == "Episode" { - Text("\(item.name ?? "")") - .font(.caption) - .padding(6) - .foregroundColor(.white) - } - }.background(Color.black) + LazyHStack { + Spacer().frame(width: 14) + ForEach(items, id: \.id) { item in + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + Spacer().frame(height: 10) + ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) + .frame(width: 320, height: 180) + .cornerRadius(10) + .overlay( + Group { + if item.type == "Episode" { + Text("\(item.name ?? "")") + .font(.caption) + .padding(6) + .foregroundColor(.white) + } + }.background(Color.black) .opacity(0.8) .cornerRadius(10.0) .padding(6), alignment: .topTrailing - ) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(ProgressBar()) - .frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7) - .padding(0), alignment: .bottomLeading - ) - Text(item.seriesName ?? item.name ?? "") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - .frame(width: 320, alignment: .leading) - Spacer().frame(height: 5) - } + ) + .overlay( + Rectangle() + .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) + .mask(ProgressBar()) + .frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7) + .padding(0), alignment: .bottomLeading + ) + Text(item.seriesName ?? item.name ?? "") + .font(.callout) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + .frame(width: 320, alignment: .leading) + Spacer().frame(height: 5) } - Spacer().frame(width: 16) } - Spacer().frame(width: 2) - }.frame(height: 215) + Spacer().frame(width: 16) + } + Spacer().frame(width: 2) + }.frame(height: 215) .padding(.bottom, 10) - } } } } diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index ea192537..0d0def5f 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -52,6 +52,7 @@ struct EpisodeItemView: View { .stroke(Color.secondary, lineWidth: 1)) } } + .padding(.top, 1) } .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) } @@ -89,10 +90,10 @@ struct EpisodeItemView: View { viewModel.updateWatchState() } label: { if viewModel.isWatched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle").foregroundColor(Color.primary) .font(.system(size: 20)) } } @@ -254,6 +255,8 @@ struct EpisodeItemView: View { Spacer() }.frame(maxWidth: .infinity, alignment: .leading) .offset(x: 14) + .padding(.top, 1) + }.frame(maxWidth: .infinity, alignment: .leading) Spacer() HStack { @@ -273,10 +276,10 @@ struct EpisodeItemView: View { viewModel.updateWatchState() } label: { if viewModel.isWatched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle").foregroundColor(Color.primary) .font(.system(size: 20)) } } diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index 33cf87e9..ec9b6236 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -15,16 +15,17 @@ struct HomeView: View { @Environment(\.horizontalSizeClass) var hSizeClass @Environment(\.verticalSizeClass) var vSizeClass @State var showingSettings = false - - var body: some View { + + @ViewBuilder + var innerBody: some View { if(viewModel.isLoading) { ProgressView() } else { ScrollView { LazyVStack(alignment: .leading) { - Spacer().frame(height: hSizeClass == .compact && vSizeClass == .regular ? 0 : 16) if !viewModel.resumeItems.isEmpty { ContinueWatchingView(items: viewModel.resumeItems) + .padding(.top, hSizeClass == .compact && vSizeClass == .regular ? 0 : 16) } if !viewModel.nextUpItems.isEmpty { NextUpView(items: viewModel.nextUpItems) @@ -52,10 +53,15 @@ struct HomeView: View { }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } } - - Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) } + .padding(.top, hSizeClass == .compact && vSizeClass == .regular ? 0 : 16) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) } + } + } + + var body: some View { + innerBody .navigationTitle(MainTabView.Tab.home.localized) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { @@ -69,6 +75,5 @@ struct HomeView: View { .fullScreenCover(isPresented: $showingSettings) { SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) } - } } } diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 01bc0bf0..0b351c7e 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -8,38 +8,36 @@ import SwiftUI struct LatestMediaView: View { - @StateObject var viewModel: LatestMediaViewModel - + @ObservedObject var viewModel: LatestMediaViewModel + var body: some View { ScrollView(.horizontal, showsIndicators: false) { - ZStack { - LazyHStack { - Spacer().frame(width: 16) - ForEach(viewModel.items, id: \.id) { item in - if item.type == "Series" || item.type == "Movie" { - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - Spacer().frame(height: 10) - ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - Spacer().frame(height: 5) - Text(item.seriesName ?? item.name ?? "") + LazyHStack { + Spacer().frame(width: 16) + ForEach(viewModel.items, id: \.id) { item in + if item.type == "Series" || item.type == "Movie" { + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + Spacer().frame(height: 10) + ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) + .frame(width: 100, height: 150) + .cornerRadius(10) + Spacer().frame(height: 5) + Text(item.seriesName ?? item.name ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + if item.productionYear != nil { + Text(String(item.productionYear ?? 0)) + .foregroundColor(.secondary) .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if item.productionYear != nil { - Text(String(item.productionYear ?? 0)) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text(item.type!) - } - }.frame(width: 100) - Spacer().frame(width: 15) - } + .fontWeight(.medium) + } else { + Text(item.type!) + } + }.frame(width: 100) + Spacer().frame(width: 15) } } } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index b62b1ba6..f866392a 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -58,6 +58,7 @@ struct MovieItemView: View { .stroke(Color.secondary, lineWidth: 1)) } } + .padding(.top, 1) } .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) } @@ -95,10 +96,10 @@ struct MovieItemView: View { viewModel.updateWatchState() } label: { if viewModel.isWatched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle").foregroundColor(Color.primary) .font(.system(size: 20)) } } @@ -270,6 +271,7 @@ struct MovieItemView: View { Spacer() }.frame(maxWidth: .infinity, alignment: .leading) .offset(x: 14) + .padding(.top, 1) }.frame(maxWidth: .infinity, alignment: .leading) Spacer() HStack { @@ -289,10 +291,10 @@ struct MovieItemView: View { viewModel.updateWatchState() } label: { if viewModel.isWatched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + Image(systemName: "checkmark.circle").foregroundColor(Color.primary) .font(.system(size: 20)) } } diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 8c7bedc5..affe57f4 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -10,44 +10,42 @@ import Combine import JellyfinAPI struct NextUpView: View { - + var items: [BaseItemDto] - + var body: some View { VStack(alignment: .leading) { - if items.count != 0 { - Text("Next Up") - .font(.title2) - .fontWeight(.bold) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - Spacer().frame(width: 16) - ForEach(items, id: \.id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - ImageView(src: item.getSeriesPrimaryImage(maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - Spacer().frame(height: 5) - Text(item.seriesName!) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - }.frame(width: 100) - Spacer().frame(width: 16) - } + Text("Next Up") + .font(.title2) + .fontWeight(.bold) + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 16) + ForEach(items, id: \.id) { item in + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + ImageView(src: item.getSeriesPrimaryImage(maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) + .frame(width: 100, height: 150) + .cornerRadius(10) + Spacer().frame(height: 5) + Text(item.seriesName!) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + }.frame(width: 100) + Spacer().frame(width: 16) } } } - .frame(height: 200) } + .frame(height: 200) } .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index ce357d08..03d865e5 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -104,14 +104,16 @@ extension BaseItemDto { // MARK: Calculations func getItemRuntime() -> String { - let seconds = (self.runTimeTicks ?? 0) / 10_000_000 - let hours = (seconds / 3600) - let minutes = ((seconds - (hours * 3600)) / 60) - if hours != 0 { - return "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" - } else { - return "\(String(minutes))m" - } + let timeHMSFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .brief + formatter.allowedUnits = [.hour, .minute] + return formatter + }() + + let text = timeHMSFormatter.string(from: Double(self.runTimeTicks! / 10_000_000)) ?? "" + + return text } func getItemProgressString() -> String { diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index a726ce0c..aafa64ab 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -34,6 +34,7 @@ final class ConnectToServerViewModel: ViewModel { func getPublicUsers() { if ServerEnvironment.current.server != nil { UserAPI.getPublicUsers() + .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestCompletion(completion: completion) }, receiveValue: { response in @@ -56,6 +57,7 @@ final class ConnectToServerViewModel: ViewModel { func connectToServer() { ServerEnvironment.current.create(with: uriSubject.value) + .trackActivity(loading) .sink(receiveCompletion: { result in switch result { case let .failure(error): @@ -71,6 +73,7 @@ final class ConnectToServerViewModel: ViewModel { func login() { SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value) + .trackActivity(loading) .sink(receiveCompletion: { completion in switch completion { case .finished: