Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
commit
1f0cefba0d
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue