Merge pull request #187 from PangMo5/R.swift
Swiftgen and Clean up Localizable.strings
This commit is contained in:
commit
0f78534157
|
@ -96,3 +96,4 @@ iOSInjectionProject/
|
|||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Shared/Generated/Strings.swift
|
||||
|
|
|
@ -20,7 +20,7 @@ struct MediaPlayButtonRowView: View {
|
|||
NavigationLink(destination: VideoPlayerView(item: viewModel.item).ignoresSafeArea()) {
|
||||
MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView)
|
||||
}
|
||||
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : "Play")
|
||||
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : L10n.play)
|
||||
.font(.caption)
|
||||
}
|
||||
VStack {
|
||||
|
|
|
@ -68,7 +68,7 @@ struct PortraitItemElement: View {
|
|||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
||||
Text(L10n.seasonAndEpisode(String(item.parentIndexNumber ?? 0), String(item.indexNumber ?? 0)))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
|
|
|
@ -12,17 +12,17 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct BasicAppSettingsView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
||||
@State var resetTapped: Bool = false
|
||||
|
||||
|
||||
@Default(.appAppearance) var appAppearance
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
|
@ -30,21 +30,21 @@ struct BasicAppSettingsView: View {
|
|||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||
})
|
||||
} header: {
|
||||
Text("Accessibility")
|
||||
L10n.accessibility.text
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
resetTapped = true
|
||||
} label: {
|
||||
Text("Reset")
|
||||
L10n.reset.text
|
||||
}
|
||||
}
|
||||
.alert("Reset", isPresented: $resetTapped, actions: {
|
||||
.alert(L10n.reset, isPresented: $resetTapped, actions: {
|
||||
Button(role: .destructive) {
|
||||
viewModel.reset()
|
||||
basicAppSettingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
L10n.reset.text
|
||||
}
|
||||
})
|
||||
.navigationTitle("Settings")
|
||||
|
|
|
@ -10,14 +10,14 @@ import SwiftUI
|
|||
import Stinsen
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
|
||||
|
||||
@StateObject var viewModel = ConnectToServerViewModel()
|
||||
@State var uri = ""
|
||||
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
||||
TextField(L10n.serverURL, text: $uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
|
@ -25,7 +25,7 @@ struct ConnectToServerView: View {
|
|||
viewModel.connectToServer(uri: uri)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
L10n.connect.text
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
|
@ -36,8 +36,8 @@ struct ConnectToServerView: View {
|
|||
} header: {
|
||||
Text("Connect to a Jellyfin server")
|
||||
}
|
||||
|
||||
Section(header: Text("Local Servers")) {
|
||||
|
||||
Section(header: L10n.localServers.text) {
|
||||
if viewModel.searching {
|
||||
ProgressView()
|
||||
}
|
||||
|
@ -68,6 +68,6 @@ struct ConnectToServerView: View {
|
|||
message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"),
|
||||
dismissButton: .cancel())
|
||||
}
|
||||
.navigationTitle("Connect")
|
||||
.navigationTitle(L10n.connect)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ struct ContinueWatchingView: View {
|
|||
@Namespace private var namespace
|
||||
|
||||
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if items.count > 0 {
|
||||
Text("Continue Watching")
|
||||
L10n.continueWatching.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
|
|
|
@ -38,7 +38,7 @@ struct HomeView: View {
|
|||
self.homeRouter.route(to: \.modalLibrary, (.init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Latest \(library?.name ?? "")")
|
||||
Text(L10n.latestWithString(library?.name ?? ""))
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
Image(systemName: "chevron.forward.circle.fill")
|
||||
|
|
|
@ -76,7 +76,7 @@ struct EpisodeItemView: View {
|
|||
HStack(alignment: .top) {
|
||||
VStack(alignment: .trailing) {
|
||||
if studio != nil {
|
||||
Text("STUDIO")
|
||||
L10n.studio.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -88,7 +88,7 @@ struct EpisodeItemView: View {
|
|||
}
|
||||
|
||||
if director != nil {
|
||||
Text("DIRECTOR")
|
||||
L10n.director.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -100,7 +100,7 @@ struct EpisodeItemView: View {
|
|||
}
|
||||
|
||||
if !actors.isEmpty {
|
||||
Text("CAST")
|
||||
L10n.cast.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -133,7 +133,7 @@ struct EpisodeItemView: View {
|
|||
NavigationLink(destination: VideoPlayerView(item: viewModel.item).ignoresSafeArea()) {
|
||||
MediaViewActionButton(icon: "play.fill")
|
||||
}
|
||||
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : "Play")
|
||||
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : L10n.play)
|
||||
.font(.caption)
|
||||
}
|
||||
VStack {
|
||||
|
@ -152,7 +152,7 @@ struct EpisodeItemView: View {
|
|||
}.padding(.top, 50)
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
L10n.moreLikeThis.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
ScrollView(.horizontal) {
|
||||
|
|
|
@ -40,7 +40,7 @@ struct ItemView: View {
|
|||
} else if item.type == "Episode" {
|
||||
EpisodeItemView(viewModel: .init(item: item))
|
||||
} else {
|
||||
Text("Type: \(item.type ?? "") not implemented yet :(")
|
||||
Text(L10n.notImplementedYetWithType(item.type ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
VStack(alignment: .trailing) {
|
||||
if studio != nil {
|
||||
Text("STUDIO")
|
||||
L10n.studio.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -89,7 +89,7 @@ struct MovieItemView: View {
|
|||
}
|
||||
|
||||
if director != nil {
|
||||
Text("DIRECTOR")
|
||||
L10n.director.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -101,7 +101,7 @@ struct MovieItemView: View {
|
|||
}
|
||||
|
||||
if !actors.isEmpty {
|
||||
Text("CAST")
|
||||
L10n.cast.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -133,7 +133,7 @@ struct MovieItemView: View {
|
|||
}.padding(.top, 50)
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
L10n.moreLikeThis.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
ScrollView(.horizontal) {
|
||||
|
|
|
@ -95,7 +95,7 @@ struct SeasonItemView: View {
|
|||
}.padding(.top, 50)
|
||||
|
||||
if !viewModel.episodes.isEmpty {
|
||||
Text("Episodes")
|
||||
L10n.episodes.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
ScrollView(.horizontal) {
|
||||
|
|
|
@ -79,7 +79,7 @@ struct SeriesItemView: View {
|
|||
HStack {
|
||||
VStack(alignment: .trailing) {
|
||||
if studio != nil {
|
||||
Text("STUDIO")
|
||||
L10n.studio.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -91,7 +91,7 @@ struct SeriesItemView: View {
|
|||
}
|
||||
|
||||
if director != nil {
|
||||
Text("DIRECTOR")
|
||||
L10n.director.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -103,7 +103,7 @@ struct SeriesItemView: View {
|
|||
}
|
||||
|
||||
if !actors.isEmpty {
|
||||
Text("CAST")
|
||||
L10n.cast.text
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -135,7 +135,7 @@ struct SeriesItemView: View {
|
|||
}
|
||||
}.padding(.top, 50)
|
||||
if !viewModel.seasons.isEmpty {
|
||||
Text("Seasons")
|
||||
L10n.seasons.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
ScrollView(.horizontal) {
|
||||
|
@ -153,7 +153,7 @@ struct SeriesItemView: View {
|
|||
}
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
L10n.moreLikeThis.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
ScrollView(.horizontal) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct LibraryFilterView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||
@Binding var filters: LibraryFilters
|
||||
var parentId: String = ""
|
||||
|
@ -31,32 +31,32 @@ struct LibraryFilterView: View {
|
|||
} else {
|
||||
Form {
|
||||
if viewModel.enabledFilterType.contains(.genre) {
|
||||
MultiSelector(label: NSLocalizedString("Genres", comment: ""),
|
||||
MultiSelector(label: L10n.genres,
|
||||
options: viewModel.possibleGenres,
|
||||
optionToString: { $0.name ?? "" },
|
||||
selected: $viewModel.modifiedFilters.withGenres)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.filter) {
|
||||
MultiSelector(label: NSLocalizedString("Filters", comment: ""),
|
||||
MultiSelector(label: L10n.filters,
|
||||
options: viewModel.possibleItemFilters,
|
||||
optionToString: { $0.localized },
|
||||
selected: $viewModel.modifiedFilters.filters)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.tag) {
|
||||
MultiSelector(label: NSLocalizedString("Tags", comment: ""),
|
||||
MultiSelector(label: L10n.tags,
|
||||
options: viewModel.possibleTags,
|
||||
optionToString: { $0 },
|
||||
selected: $viewModel.modifiedFilters.tags)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||
Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) {
|
||||
Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) {
|
||||
ForEach(viewModel.possibleSortBys, id: \.self) { so in
|
||||
Text(so.localized).tag(so)
|
||||
}
|
||||
}
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.sortOrder) {
|
||||
Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) {
|
||||
Picker(selection: $viewModel.selectedSortOrder, label: L10n.displayOrder.text) {
|
||||
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
||||
Text(so.rawValue).tag(so)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ struct LibraryFilterView: View {
|
|||
self.filters = viewModel.modifiedFilters
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
L10n.reset.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ struct LibraryFilterView: View {
|
|||
self.filters = viewModel.modifiedFilters
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Apply")
|
||||
L10n.apply.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ struct LibrarySearchView: View {
|
|||
var suggestionsListView: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8) {
|
||||
Text("Suggestions")
|
||||
L10n.suggestions.text
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
@ -88,7 +88,7 @@ struct LibraryView: View {
|
|||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
L10n.noResults.text
|
||||
Button { } label: {
|
||||
Text("Reload")
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ struct MovieLibrariesView: View {
|
|||
@EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router
|
||||
@StateObject var viewModel: MovieLibrariesViewModel
|
||||
var title: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
|
@ -77,7 +77,7 @@ struct MovieLibrariesView: View {
|
|||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
L10n.noResults.text
|
||||
Button {
|
||||
print("movieLibraries reload")
|
||||
} label: {
|
||||
|
|
|
@ -13,13 +13,13 @@ import Stinsen
|
|||
|
||||
struct NextUpView: View {
|
||||
var items: [BaseItemDto]
|
||||
|
||||
|
||||
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if items.count > 0 {
|
||||
Text("Next Up")
|
||||
L10n.nextUp.text
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
|
|
|
@ -11,10 +11,10 @@ import CoreStore
|
|||
import SwiftUI
|
||||
|
||||
struct ServerListView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
||||
@ObservedObject var viewModel: ServerListViewModel
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
|
@ -27,22 +27,22 @@ struct ServerListView: View {
|
|||
Image(systemName: "server.rack")
|
||||
.font(.system(size: 72))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(server.name)
|
||||
.font(.title2)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
|
||||
Text(server.uri)
|
||||
.font(.footnote)
|
||||
.disabled(true)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
||||
Text(viewModel.userTextFor(server: server))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ struct ServerListView: View {
|
|||
}
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var noServerView: some View {
|
||||
VStack {
|
||||
|
@ -68,18 +68,18 @@ struct ServerListView: View {
|
|||
.frame(minWidth: 50, maxWidth: 500)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
|
||||
|
||||
Button {
|
||||
serverListRouter.route(to: \.connectToServer)
|
||||
} label: {
|
||||
Text("Connect")
|
||||
L10n.connect.text
|
||||
.bold()
|
||||
.font(.callout)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if viewModel.servers.isEmpty {
|
||||
|
@ -89,7 +89,7 @@ struct ServerListView: View {
|
|||
listView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var trailingToolbarContent: some View {
|
||||
if viewModel.servers.isEmpty {
|
||||
|
@ -109,7 +109,7 @@ struct ServerListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle("Servers")
|
||||
|
|
|
@ -22,7 +22,7 @@ struct SettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Playback settings")) {
|
||||
Section(header: L10n.playbackSettings.text) {
|
||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
Text(bitrate.name).tag(bitrate.value)
|
||||
|
@ -36,7 +36,7 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Accessibility")) {
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
||||
SearchablePicker(label: "Preferred subtitle language",
|
||||
options: viewModel.langs,
|
||||
|
@ -58,12 +58,12 @@ struct SettingsView: View {
|
|||
|
||||
Section(header: Text(SessionManager.main.currentLogin.server.name)) {
|
||||
HStack {
|
||||
Text("Signed in as \(SessionManager.main.currentLogin.user.username)").foregroundColor(.primary)
|
||||
Text(L10n.signedInAsWithString(SessionManager.main.currentLogin.user.username)).foregroundColor(.primary)
|
||||
Spacer()
|
||||
Button {
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||
} label: {
|
||||
Text("Switch user").font(.callout)
|
||||
L10n.switchUser.text.font(.callout)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
|
|
|
@ -14,7 +14,7 @@ struct TVLibrariesView: View {
|
|||
@EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router
|
||||
@StateObject var viewModel: TVLibrariesViewModel
|
||||
var title: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
|
@ -77,7 +77,7 @@ struct TVLibrariesView: View {
|
|||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
L10n.noResults.text
|
||||
Button {
|
||||
print("tvLibraries reload")
|
||||
} label: {
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
import SwiftUI
|
||||
|
||||
struct UserListView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
||||
@ObservedObject var viewModel: UserListViewModel
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
|
@ -25,9 +25,9 @@ struct UserListView: View {
|
|||
HStack {
|
||||
Text(user.username)
|
||||
.font(.title2)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ struct UserListView: View {
|
|||
}
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var noUserView: some View {
|
||||
VStack {
|
||||
|
@ -55,7 +55,7 @@ struct UserListView: View {
|
|||
.frame(minWidth: 50, maxWidth: 500)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
|
||||
|
||||
Button {
|
||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||
} label: {
|
||||
|
@ -66,7 +66,7 @@ struct UserListView: View {
|
|||
.padding(.top, 40)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if viewModel.users.isEmpty {
|
||||
|
@ -76,7 +76,7 @@ struct UserListView: View {
|
|||
listView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarContent: some View {
|
||||
if viewModel.users.isEmpty {
|
||||
|
@ -91,7 +91,7 @@ struct UserListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle(viewModel.server.name)
|
||||
|
|
|
@ -11,28 +11,28 @@ import SwiftUI
|
|||
import Stinsen
|
||||
|
||||
struct UserSignInView: View {
|
||||
|
||||
|
||||
@ObservedObject var viewModel: UserSignInViewModel
|
||||
@State private var username: String = ""
|
||||
@State private var password: String = ""
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
|
||||
Section {
|
||||
TextField("Username", text: $username)
|
||||
TextField(L10n.username, text: $username)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
SecureField("Password", text: $password)
|
||||
|
||||
SecureField(L10n.password, text: $password)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
|
||||
Button {
|
||||
viewModel.login(username: username, password: password)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
L10n.connect.text
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
|
|
|
@ -14,7 +14,7 @@ class AudioViewController: InfoTabViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tabBarItem.title = NSLocalizedString("Audio", comment: "")
|
||||
tabBarItem.title = "Audio"
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class MediaInfoViewController: InfoTabViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tabBarItem.title = NSLocalizedString("Info", comment: "")
|
||||
tabBarItem.title = "Info"
|
||||
}
|
||||
|
||||
func setMedia(item: BaseItemDto) {
|
||||
|
|
|
@ -14,7 +14,7 @@ class SubtitlesViewController: InfoTabViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tabBarItem.title = NSLocalizedString("Subtitles", comment: "")
|
||||
tabBarItem.title = "Subtitles"
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
|||
let builder = DeviceProfileBuilder()
|
||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||
let profile = builder.buildProfile()
|
||||
|
||||
|
||||
let currentUser = SessionManager.main.currentLogin.user
|
||||
|
||||
let playbackInfo = PlaybackInfoDto(userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; };
|
||||
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; };
|
||||
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */; };
|
||||
363CADF08820D3B2055CF1D8 /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BE2D324B040DCA2629C110D /* Pods_JellyfinPlayer_tvOS.framework */; };
|
||||
531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */; };
|
||||
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069512684E7EE00CFFDBA /* MediaInfoView.swift */; };
|
||||
531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069522684E7EE00CFFDBA /* SubtitlesView.swift */; };
|
||||
|
@ -148,12 +149,11 @@
|
|||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; };
|
||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
|
||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
|
||||
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */; };
|
||||
53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */; };
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; };
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */; };
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
||||
560CA59B3956A4CA13EDAC05 /* Pods_JellyfinPlayer_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86BAC42C3764D232C8DF8F5E /* Pods_JellyfinPlayer_iOS.framework */; };
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; };
|
||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
||||
|
@ -175,6 +175,10 @@
|
|||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; };
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; };
|
||||
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; };
|
||||
6264E88A27384A6F0081A12A /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
||||
6264E88C273850380081A12A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6264E88B273850380081A12A /* Strings.swift */; };
|
||||
6264E88D273850380081A12A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6264E88B273850380081A12A /* Strings.swift */; };
|
||||
6264E88E273850380081A12A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6264E88B273850380081A12A /* Strings.swift */; };
|
||||
62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; };
|
||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
||||
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
||||
|
@ -352,17 +356,17 @@
|
|||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
|
||||
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
|
||||
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
|
||||
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
|
||||
EABFD69FA6D5DBB248A494AA /* Pods_WidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59AFF849629F3C787909A911 /* Pods_WidgetExtension.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
628B952B2670CABE0091AF3B /* PBXContainerItemProxy */ = {
|
||||
6264E888273848760081A12A /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 5377CBE9263B596A003A4E83 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 628B951F2670CABD0091AF3B;
|
||||
remoteInfo = WidgetExtensionExtension;
|
||||
remoteInfo = WidgetExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
|
@ -394,8 +398,10 @@
|
|||
091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadCastConnection.swift; sourceTree = "<group>"; };
|
||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = "<group>"; };
|
||||
0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUpNextView.swift; sourceTree = "<group>"; };
|
||||
3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
14E199C7BBA98782CAD2F0D4 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||
20CA36DDD247EED8D16438A5 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||
4BDCEE3B49CF70A9E9BA3CD8 /* Pods-WidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-WidgetExtension/Pods-WidgetExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
4BE2D324B040DCA2629C110D /* Pods_JellyfinPlayer_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
531069512684E7EE00CFFDBA /* MediaInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaInfoView.swift; sourceTree = "<group>"; };
|
||||
531069522684E7EE00CFFDBA /* SubtitlesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubtitlesView.swift; sourceTree = "<group>"; };
|
||||
|
@ -493,6 +499,7 @@
|
|||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemView.swift; sourceTree = "<group>"; };
|
||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
|
||||
59AFF849629F3C787909A911 /* Pods_WidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
||||
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||
|
@ -512,6 +519,7 @@
|
|||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TVVLCKit.xcframework; path = Carthage/Build/TVVLCKit.xcframework; sourceTree = "<group>"; };
|
||||
6264E88B273850380081A12A /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||
6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = "<group>"; };
|
||||
6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = "<group>"; };
|
||||
628B95202670CABD0091AF3B /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -538,8 +546,10 @@
|
|||
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
|
||||
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
|
||||
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
|
||||
772F6DAB8534FD1DB45B7687 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
86BAC42C3764D232C8DF8F5E /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
|
||||
BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B598C62749E5EFD37280FCC3 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = "<group>"; };
|
||||
C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = "<group>"; };
|
||||
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = "<group>"; };
|
||||
|
@ -549,8 +559,6 @@
|
|||
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = "<group>"; };
|
||||
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||
DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
|
||||
|
@ -606,7 +614,7 @@
|
|||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
|
||||
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
|
||||
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
|
||||
EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FDEDADB92FA8523BC8432E45 /* Pods-WidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetExtension/Pods-WidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -616,7 +624,6 @@
|
|||
files = (
|
||||
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
|
||||
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */,
|
||||
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
||||
E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */,
|
||||
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */,
|
||||
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
|
||||
|
@ -627,6 +634,7 @@
|
|||
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
||||
E12186DE2718F1C50010884C /* Defaults in Frameworks */,
|
||||
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */,
|
||||
363CADF08820D3B2055CF1D8 /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -638,7 +646,6 @@
|
|||
53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */,
|
||||
62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */,
|
||||
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */,
|
||||
53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */,
|
||||
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */,
|
||||
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
|
||||
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
|
||||
|
@ -646,6 +653,7 @@
|
|||
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
|
||||
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */,
|
||||
53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */,
|
||||
560CA59B3956A4CA13EDAC05 /* Pods_JellyfinPlayer_iOS.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -661,6 +669,7 @@
|
|||
E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */,
|
||||
628B95352670CAEA0091AF3B /* JellyfinAPI in Frameworks */,
|
||||
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */,
|
||||
EABFD69FA6D5DBB248A494AA /* Pods_WidgetExtension.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -791,6 +800,7 @@
|
|||
535870752669D60C00D05A09 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6286F09F271C0AA500C40ED5 /* Generated */,
|
||||
62C29E9D26D0FE5900C1D2E7 /* Coordinators */,
|
||||
E1FCD08E26C466F3007C8DCF /* Errors */,
|
||||
621338912660106C00A81A2A /* Extensions */,
|
||||
|
@ -1026,8 +1036,9 @@
|
|||
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */,
|
||||
628B95212670CABD0091AF3B /* WidgetKit.framework */,
|
||||
628B95232670CABD0091AF3B /* SwiftUI.framework */,
|
||||
3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */,
|
||||
EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */,
|
||||
86BAC42C3764D232C8DF8F5E /* Pods_JellyfinPlayer_iOS.framework */,
|
||||
4BE2D324B040DCA2629C110D /* Pods_JellyfinPlayer_tvOS.framework */,
|
||||
59AFF849629F3C787909A911 /* Pods_WidgetExtension.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1059,6 +1070,14 @@
|
|||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6286F09F271C0AA500C40ED5 /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6264E88B273850380081A12A /* Strings.swift */,
|
||||
);
|
||||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
628B95252670CABD0091AF3B /* WidgetExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1124,10 +1143,12 @@
|
|||
C78797A232E2B8774099D1E9 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */,
|
||||
BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */,
|
||||
DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */,
|
||||
D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */,
|
||||
772F6DAB8534FD1DB45B7687 /* Pods-JellyfinPlayer iOS.debug.xcconfig */,
|
||||
14E199C7BBA98782CAD2F0D4 /* Pods-JellyfinPlayer iOS.release.xcconfig */,
|
||||
B598C62749E5EFD37280FCC3 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */,
|
||||
20CA36DDD247EED8D16438A5 /* Pods-JellyfinPlayer tvOS.release.xcconfig */,
|
||||
4BDCEE3B49CF70A9E9BA3CD8 /* Pods-WidgetExtension.debug.xcconfig */,
|
||||
FDEDADB92FA8523BC8432E45 /* Pods-WidgetExtension.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1326,11 +1347,12 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "JellyfinPlayer tvOS" */;
|
||||
buildPhases = (
|
||||
E7370E1AA68C6CB254E46F2C /* [CP] Check Pods Manifest.lock */,
|
||||
3D0F2756C71CDF6B9EEBD4E0 /* [CP] Check Pods Manifest.lock */,
|
||||
6286F0A3271C0ABA00C40ED5 /* R.swift */,
|
||||
5358705C2669D21600D05A09 /* Sources */,
|
||||
5358705D2669D21600D05A09 /* Frameworks */,
|
||||
5358705E2669D21600D05A09 /* Resources */,
|
||||
6AB6F1DD2C8AD942F71C8A32 /* [CP] Embed Pods Frameworks */,
|
||||
879C22C1CCC48E68C86E904C /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -1358,19 +1380,20 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer iOS" */;
|
||||
buildPhases = (
|
||||
6435C3C2E610FE34AD537AC1 /* [CP] Check Pods Manifest.lock */,
|
||||
1C7487D3432E90546DA855B5 /* [CP] Check Pods Manifest.lock */,
|
||||
6286F09E271C093000C40ED5 /* R.swift */,
|
||||
5377CBED263B596A003A4E83 /* Sources */,
|
||||
5377CBEE263B596A003A4E83 /* Frameworks */,
|
||||
5377CBEF263B596A003A4E83 /* Resources */,
|
||||
5302F8322658B74800647A2E /* CopyFiles */,
|
||||
628B95312670CABE0091AF3B /* Embed App Extensions */,
|
||||
E8DDF21F62DFCE8CE76666BA /* [CP] Embed Pods Frameworks */,
|
||||
83FD120CA10FD0E91DAD83C9 /* [CP] Copy Pods Resources */,
|
||||
8D1E0C963DCE6C6F328B3EBB /* [CP] Embed Pods Frameworks */,
|
||||
DB8CA7C37DF78BEDCE4E37C1 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
628B952C2670CABE0091AF3B /* PBXTargetDependency */,
|
||||
6264E889273848760081A12A /* PBXTargetDependency */,
|
||||
);
|
||||
name = "JellyfinPlayer iOS";
|
||||
packageProductDependencies = (
|
||||
|
@ -1394,6 +1417,7 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 628B952E2670CABE0091AF3B /* Build configuration list for PBXNativeTarget "WidgetExtension" */;
|
||||
buildPhases = (
|
||||
D4D3981ADF75BCD341D590C0 /* [CP] Check Pods Manifest.lock */,
|
||||
628B951C2670CABD0091AF3B /* Sources */,
|
||||
628B951D2670CABD0091AF3B /* Frameworks */,
|
||||
628B951E2670CABD0091AF3B /* Resources */,
|
||||
|
@ -1568,7 +1592,7 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6435C3C2E610FE34AD537AC1 /* [CP] Check Pods Manifest.lock */ = {
|
||||
1C7487D3432E90546DA855B5 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
|
@ -1590,41 +1614,7 @@
|
|||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6AB6F1DD2C8AD942F71C8A32 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
83FD120CA10FD0E91DAD83C9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E7370E1AA68C6CB254E46F2C /* [CP] Check Pods Manifest.lock */ = {
|
||||
3D0F2756C71CDF6B9EEBD4E0 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
|
@ -1646,7 +1636,60 @@
|
|||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E8DDF21F62DFCE8CE76666BA /* [CP] Embed Pods Frameworks */ = {
|
||||
6286F09E271C093000C40ED5 /* R.swift */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = R.swift;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${PROJECT_DIR}/bin/swiftgen\"\n";
|
||||
};
|
||||
6286F0A3271C0ABA00C40ED5 /* R.swift */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = R.swift;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${PROJECT_DIR}/bin/swiftgen\"\n";
|
||||
};
|
||||
879C22C1CCC48E68C86E904C /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
8D1E0C963DCE6C6F328B3EBB /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
|
@ -1663,6 +1706,45 @@
|
|||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D4D3981ADF75BCD341D590C0 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-WidgetExtension-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DB8CA7C37DF78BEDCE4E37C1 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
@ -1765,6 +1847,7 @@
|
|||
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||
6264E88D273850380081A12A /* Strings.swift in Sources */,
|
||||
536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */,
|
||||
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
|
||||
5310695C2684E7EE00CFFDBA /* VideoPlayerViewController.swift in Sources */,
|
||||
|
@ -1835,6 +1918,7 @@
|
|||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||
|
@ -1923,6 +2007,7 @@
|
|||
files = (
|
||||
53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */,
|
||||
E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||
6264E88A27384A6F0081A12A /* NetworkError.swift in Sources */,
|
||||
E19169D0272514760085832A /* HTTPScheme.swift in Sources */,
|
||||
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */,
|
||||
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */,
|
||||
|
@ -1930,6 +2015,7 @@
|
|||
E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */,
|
||||
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */,
|
||||
6264E88E273850380081A12A /* Strings.swift in Sources */,
|
||||
E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
||||
|
@ -1937,7 +2023,6 @@
|
|||
E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */,
|
||||
6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,
|
||||
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */,
|
||||
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||
E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||
62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
|
||||
|
@ -1949,10 +2034,10 @@
|
|||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
628B952C2670CABE0091AF3B /* PBXTargetDependency */ = {
|
||||
6264E889273848760081A12A /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 628B951F2670CABD0091AF3B /* WidgetExtension */;
|
||||
targetProxy = 628B952B2670CABE0091AF3B /* PBXContainerItemProxy */;
|
||||
targetProxy = 6264E888273848760081A12A /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
|
@ -2090,7 +2175,7 @@
|
|||
/* Begin XCBuildConfiguration section */
|
||||
535870722669D21700D05A09 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */;
|
||||
baseConfigurationReference = B598C62749E5EFD37280FCC3 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "Dev App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
@ -2120,7 +2205,7 @@
|
|||
};
|
||||
535870732669D21700D05A09 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */;
|
||||
baseConfigurationReference = 20CA36DDD247EED8D16438A5 /* Pods-JellyfinPlayer tvOS.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
@ -2270,7 +2355,7 @@
|
|||
};
|
||||
5377CC1C263B596B003A4E83 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */;
|
||||
baseConfigurationReference = 772F6DAB8534FD1DB45B7687 /* Pods-JellyfinPlayer iOS.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Dev";
|
||||
|
@ -2306,7 +2391,7 @@
|
|||
};
|
||||
5377CC1D263B596B003A4E83 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */;
|
||||
baseConfigurationReference = 14E199C7BBA98782CAD2F0D4 /* Pods-JellyfinPlayer iOS.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
|
@ -2342,6 +2427,7 @@
|
|||
};
|
||||
628B952F2670CABE0091AF3B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4BDCEE3B49CF70A9E9BA3CD8 /* Pods-WidgetExtension.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
|
@ -2368,6 +2454,7 @@
|
|||
};
|
||||
628B95302670CABE0091AF3B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FDEDADB92FA8523BC8432E45 /* Pods-WidgetExtension.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>DisableBuildSystemDeprecationDiagnostic</key>
|
||||
<true/>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -12,12 +12,12 @@ import UIKit
|
|||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
static var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
|
||||
// Lazily initialize datastack
|
||||
let _ = SwiftfinStore.dataStack
|
||||
|
||||
_ = SwiftfinStore.dataStack
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import SwiftUI
|
|||
import MessageUI
|
||||
|
||||
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||
|
||||
|
||||
public static let shared = EmailHelper()
|
||||
|
||||
|
||||
override private init() { }
|
||||
|
||||
func sendLogs(logURL: URL) {
|
||||
|
|
|
@ -13,7 +13,7 @@ import SwiftUI
|
|||
// MARK: JellyfinPlayerApp
|
||||
@main
|
||||
struct JellyfinPlayerApp: App {
|
||||
|
||||
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@Default(.appAppearance) var appAppearance
|
||||
|
||||
|
@ -35,7 +35,7 @@ struct JellyfinPlayerApp: App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func setupAppearance() {
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||
}
|
||||
|
|
|
@ -77,8 +77,7 @@ extension AppURLHandler {
|
|||
// /Users/{UserID}/Items/{ItemID}
|
||||
if url.pathComponents[safe: 2]?.lowercased() == "items",
|
||||
let userID = url.pathComponents[safe: 1],
|
||||
let itemID = url.pathComponents[safe: 3]
|
||||
{
|
||||
let itemID = url.pathComponents[safe: 3] {
|
||||
// It would be nice if the ItemViewModel could be initialized to id later.
|
||||
getItem(userID: userID, itemID: itemID) { item in
|
||||
guard let item = item else { return }
|
||||
|
|
|
@ -11,10 +11,10 @@ import SwiftUI
|
|||
import JellyfinAPI
|
||||
|
||||
struct EpisodeCardVStackView: View {
|
||||
|
||||
|
||||
let items: [BaseItemDto]
|
||||
let selectedAction: (BaseItemDto) -> Void
|
||||
|
||||
|
||||
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
||||
HStack {
|
||||
ZStack {
|
||||
|
@ -30,7 +30,7 @@ struct EpisodeCardVStackView: View {
|
|||
.padding(.leading, 2)
|
||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1)
|
||||
|
||||
|
||||
ZStack {
|
||||
if item.userData?.played ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
|
@ -42,7 +42,7 @@ struct EpisodeCardVStackView: View {
|
|||
.opacity(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
|
@ -50,7 +50,7 @@ struct EpisodeCardVStackView: View {
|
|||
selectedAction(item)
|
||||
} label: {
|
||||
HStack {
|
||||
|
||||
|
||||
// MARK: Image
|
||||
ImageView(src: item.getPrimaryImage(maxWidth: 150),
|
||||
bh: item.getPrimaryImageBlurHash(),
|
||||
|
@ -65,37 +65,37 @@ struct EpisodeCardVStackView: View {
|
|||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
.overlay(buildCardOverlayView(item: item), alignment: .topTrailing)
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
|
||||
// MARK: Title
|
||||
Text(item.title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(2)
|
||||
|
||||
|
||||
HStack {
|
||||
Text(item.getEpisodeLocator() ?? "")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
||||
Text(item.getItemRuntime())
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Overview
|
||||
Text(item.overview ?? "")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
import SwiftUI
|
||||
|
||||
struct PillHStackView<ItemType: PillStackable>: View {
|
||||
|
||||
|
||||
let title: String
|
||||
let items: [ItemType]
|
||||
// let navigationView: (ItemType) -> NavigationView
|
||||
let selectedAction: (ItemType) -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
|
@ -23,7 +23,7 @@ struct PillHStackView<ItemType: PillStackable>: View {
|
|||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(items, id: \.title) { item in
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
import SwiftUI
|
||||
|
||||
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||
|
||||
|
||||
let items: [ItemType]
|
||||
let maxWidth: Int
|
||||
let horizontalAlignment: HorizontalAlignment
|
||||
let topBarView: () -> TopBarView
|
||||
let selectedAction: (ItemType) -> Void
|
||||
|
||||
|
||||
init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) {
|
||||
self.items = items
|
||||
self.maxWidth = maxWidth
|
||||
|
@ -24,18 +24,18 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
|||
self.topBarView = topBarView
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
topBarView()
|
||||
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
HStack(alignment: .top) {
|
||||
|
||||
|
||||
Spacer().frame(width: 16)
|
||||
|
||||
|
||||
ForEach(items, id: \.title) { item in
|
||||
Button {
|
||||
selectedAction(item)
|
||||
|
@ -47,7 +47,7 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
|||
.frame(width: 100, height: CGFloat(maxWidth))
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
|
||||
|
||||
Text(item.title)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
|
@ -55,7 +55,7 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
|||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
|
||||
|
||||
if let description = item.description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
|
|
|
@ -13,7 +13,7 @@ import JellyfinAPI
|
|||
// Not implemented on iOS, but used by a shared Coordinator.
|
||||
struct PortraitItemElement: View {
|
||||
var item: BaseItemDto
|
||||
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ struct PortraitItemView: View {
|
|||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
||||
Text(L10n.seasonAndEpisode(String(item.parentIndexNumber ?? 0), String(item.indexNumber ?? 0)))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
|
|
|
@ -14,7 +14,7 @@ import UIKit
|
|||
class RefreshHelper {
|
||||
var refreshControl: UIRefreshControl?
|
||||
var refreshAction: (() -> Void)?
|
||||
|
||||
|
||||
@objc func didRefresh() {
|
||||
guard let refreshControl = refreshControl else { return }
|
||||
refreshAction?()
|
||||
|
|
|
@ -12,18 +12,18 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct BasicAppSettingsView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
||||
@State var resetTapped: Bool = false
|
||||
|
||||
|
||||
@Default(.appAppearance) var appAppearance
|
||||
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ struct BasicAppSettingsView: View {
|
|||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||
})
|
||||
} header: {
|
||||
Text("Accessibility")
|
||||
L10n.accessibility.text
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
Picker("Default Scheme", selection: $defaultHTTPScheme) {
|
||||
ForEach(HTTPScheme.allCases, id: \.self) { scheme in
|
||||
|
@ -43,19 +43,19 @@ struct BasicAppSettingsView: View {
|
|||
} header: {
|
||||
Text("Networking")
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
resetTapped = true
|
||||
} label: {
|
||||
Text("Reset")
|
||||
L10n.reset.text
|
||||
}
|
||||
}
|
||||
.alert("Reset", isPresented: $resetTapped, actions: {
|
||||
.alert(L10n.reset, isPresented: $resetTapped, actions: {
|
||||
Button(role: .destructive) {
|
||||
viewModel.reset()
|
||||
basicAppSettingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
L10n.reset.text
|
||||
}
|
||||
})
|
||||
.navigationBarTitle("Settings", displayMode: .inline)
|
||||
|
|
|
@ -11,16 +11,16 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
|
||||
|
||||
@StateObject var viewModel: ConnectToServerViewModel
|
||||
@State var uri = ""
|
||||
|
||||
|
||||
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
||||
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
||||
TextField(L10n.serverURL, text: $uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
|
@ -29,7 +29,7 @@ struct ConnectToServerView: View {
|
|||
uri = "\(defaultHTTPScheme.rawValue)://"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if viewModel.isLoading {
|
||||
Button(role: .destructive) {
|
||||
viewModel.cancelConnection()
|
||||
|
@ -40,14 +40,14 @@ struct ConnectToServerView: View {
|
|||
Button {
|
||||
viewModel.connectToServer(uri: uri)
|
||||
} label: {
|
||||
Text("Connect")
|
||||
L10n.connect.text
|
||||
}
|
||||
.disabled(uri.isEmpty)
|
||||
}
|
||||
} header: {
|
||||
Text("Connect to a Jellyfin server")
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
if viewModel.searching {
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
|
@ -88,9 +88,9 @@ struct ConnectToServerView: View {
|
|||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Local Servers")
|
||||
L10n.localServers.text
|
||||
Spacer()
|
||||
|
||||
|
||||
Button {
|
||||
viewModel.discoverServers()
|
||||
} label: {
|
||||
|
@ -106,7 +106,7 @@ struct ConnectToServerView: View {
|
|||
message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"),
|
||||
dismissButton: .cancel())
|
||||
}
|
||||
.navigationTitle("Connect")
|
||||
.navigationTitle(L10n.connect)
|
||||
.onAppear {
|
||||
viewModel.discoverServers()
|
||||
AppURLHandler.shared.appURLState = .allowedInLogin
|
||||
|
|
|
@ -12,10 +12,10 @@ import Introspect
|
|||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
@StateObject var viewModel = HomeViewModel()
|
||||
|
||||
|
||||
private let refreshHelper = RefreshHelper()
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -31,10 +31,10 @@ struct HomeView: View {
|
|||
if !viewModel.nextUpItems.isEmpty {
|
||||
NextUpView(items: viewModel.nextUpItems)
|
||||
}
|
||||
|
||||
|
||||
ForEach(viewModel.libraries, id: \.self) { library in
|
||||
HStack {
|
||||
Text("Latest \(library.name ?? "")")
|
||||
Text(L10n.latestWithString(library.name ?? ""))
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
|
@ -45,7 +45,7 @@ struct HomeView: View {
|
|||
title: library.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||
L10n.seeAll.text.font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
}
|
||||
}
|
||||
|
@ -58,10 +58,10 @@ struct HomeView: View {
|
|||
}
|
||||
.introspectScrollView { scrollView in
|
||||
let control = UIRefreshControl()
|
||||
|
||||
|
||||
refreshHelper.refreshControl = control
|
||||
refreshHelper.refreshAction = viewModel.refresh
|
||||
|
||||
|
||||
control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged)
|
||||
scrollView.refreshControl = control
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ struct HomeView: View {
|
|||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle(NSLocalizedString("Home", comment: ""))
|
||||
.navigationTitle(L10n.home)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
|
|
|
@ -28,7 +28,7 @@ struct ItemNavigationView: View {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate struct ItemView: View {
|
||||
private struct ItemView: View {
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
|
||||
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
|
||||
|
|
|
@ -29,7 +29,7 @@ struct ItemViewBody: View {
|
|||
PortraitImageHStackView(items: seriesViewModel.seasons,
|
||||
maxWidth: 150,
|
||||
topBarView: {
|
||||
Text("Seasons")
|
||||
L10n.seasons.text
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
|
@ -41,7 +41,7 @@ struct ItemViewBody: View {
|
|||
|
||||
// MARK: Genres
|
||||
|
||||
PillHStackView(title: "Genres",
|
||||
PillHStackView(title: L10n.genres,
|
||||
items: viewModel.item.genreItems ?? [],
|
||||
selectedAction: { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
|
@ -50,7 +50,7 @@ struct ItemViewBody: View {
|
|||
// MARK: Studios
|
||||
|
||||
if let studios = viewModel.item.studios {
|
||||
PillHStackView(title: "Studios",
|
||||
PillHStackView(title: L10n.studios,
|
||||
items: studios) { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ struct ItemViewBody: View {
|
|||
PortraitImageHStackView(items: viewModel.similarItems,
|
||||
maxWidth: 150,
|
||||
topBarView: {
|
||||
Text("More Like This")
|
||||
L10n.moreLikeThis.text
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
|
|
|
@ -10,22 +10,22 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ItemLandscapeTopBarView: View {
|
||||
|
||||
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
|
||||
// MARK: Name
|
||||
|
||||
|
||||
Text(viewModel.getItemDisplayName())
|
||||
.font(.title)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.leading, 16)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
|
||||
if viewModel.item.itemType.showDetails {
|
||||
// MARK: Runtime
|
||||
Text(viewModel.item.getItemRuntime())
|
||||
|
@ -34,7 +34,7 @@ struct ItemLandscapeTopBarView: View {
|
|||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Details
|
||||
HStack {
|
||||
if viewModel.item.productionYear != nil {
|
||||
|
@ -53,9 +53,9 @@ struct ItemLandscapeTopBarView: View {
|
|||
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1))
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
if viewModel.item.itemType.showDetails {
|
||||
// MARK: Favorite
|
||||
Button {
|
||||
|
@ -70,7 +70,7 @@ struct ItemLandscapeTopBarView: View {
|
|||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
|
||||
|
||||
// MARK: Watched
|
||||
Button {
|
||||
viewModel.updateWatchState()
|
||||
|
|
|
@ -11,22 +11,22 @@ import SwiftUI
|
|||
import JellyfinAPI
|
||||
|
||||
struct PortraitHeaderOverlayView: View {
|
||||
|
||||
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .bottom, spacing: 12) {
|
||||
|
||||
|
||||
// MARK: Portrait Image
|
||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130))
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Spacer()
|
||||
|
||||
|
||||
// MARK: Name
|
||||
Text(viewModel.getItemDisplayName())
|
||||
.font(.title2)
|
||||
|
@ -34,7 +34,7 @@ struct PortraitHeaderOverlayView: View {
|
|||
.foregroundColor(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
|
||||
if viewModel.item.itemType.showDetails {
|
||||
// MARK: Runtime
|
||||
if viewModel.shouldDisplayRuntime() {
|
||||
|
@ -45,7 +45,7 @@ struct PortraitHeaderOverlayView: View {
|
|||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Details
|
||||
HStack {
|
||||
if let productionYear = viewModel.item.productionYear {
|
||||
|
@ -55,7 +55,7 @@ struct PortraitHeaderOverlayView: View {
|
|||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
Text(officialRating)
|
||||
.font(.subheadline)
|
||||
|
@ -70,9 +70,9 @@ struct PortraitHeaderOverlayView: View {
|
|||
}
|
||||
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
|
||||
// MARK: Play
|
||||
Button {
|
||||
if let playButtonItem = viewModel.playButtonItem {
|
||||
|
@ -93,9 +93,9 @@ struct PortraitHeaderOverlayView: View {
|
|||
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
||||
.cornerRadius(10)
|
||||
}.disabled(viewModel.playButtonItem == nil)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
if viewModel.item.itemType.showDetails {
|
||||
// MARK: Favorite
|
||||
Button {
|
||||
|
@ -112,7 +112,7 @@ struct PortraitHeaderOverlayView: View {
|
|||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
|
||||
|
||||
// MARK: Watched
|
||||
Button {
|
||||
viewModel.updateWatchState()
|
||||
|
|
|
@ -10,7 +10,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct LibraryFilterView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||
@Binding var filters: LibraryFilters
|
||||
var parentId: String = ""
|
||||
|
@ -31,32 +31,32 @@ struct LibraryFilterView: View {
|
|||
} else {
|
||||
Form {
|
||||
if viewModel.enabledFilterType.contains(.genre) {
|
||||
MultiSelector(label: NSLocalizedString("Genres", comment: ""),
|
||||
MultiSelector(label: L10n.genres,
|
||||
options: viewModel.possibleGenres,
|
||||
optionToString: { $0.name ?? "" },
|
||||
selected: $viewModel.modifiedFilters.withGenres)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.filter) {
|
||||
MultiSelector(label: NSLocalizedString("Filters", comment: ""),
|
||||
MultiSelector(label: L10n.filters,
|
||||
options: viewModel.possibleItemFilters,
|
||||
optionToString: { $0.localized },
|
||||
selected: $viewModel.modifiedFilters.filters)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.tag) {
|
||||
MultiSelector(label: NSLocalizedString("Tags", comment: ""),
|
||||
MultiSelector(label: L10n.tags,
|
||||
options: viewModel.possibleTags,
|
||||
optionToString: { $0 },
|
||||
selected: $viewModel.modifiedFilters.tags)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||
Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) {
|
||||
Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) {
|
||||
ForEach(viewModel.possibleSortBys, id: \.self) { so in
|
||||
Text(so.localized).tag(so)
|
||||
}
|
||||
}
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.sortOrder) {
|
||||
Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) {
|
||||
Picker(selection: $viewModel.selectedSortOrder, label: L10n.displayOrder.text) {
|
||||
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
||||
Text(so.rawValue).tag(so)
|
||||
}
|
||||
|
@ -68,11 +68,11 @@ struct LibraryFilterView: View {
|
|||
self.filters = viewModel.modifiedFilters
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
L10n.reset.text
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("Filter Results", comment: ""), displayMode: .inline)
|
||||
.navigationBarTitle(L10n.filterResults, displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
|
@ -87,7 +87,7 @@ struct LibraryFilterView: View {
|
|||
self.filters = viewModel.modifiedFilters
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Apply")
|
||||
L10n.apply.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ struct LibraryListView: View {
|
|||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Your Favorites")
|
||||
L10n.yourFavorites.text
|
||||
.foregroundColor(.black)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -39,12 +39,12 @@ struct LibraryListView: View {
|
|||
.padding(.bottom, 5)
|
||||
|
||||
NavigationLink(destination: LazyView {
|
||||
Text("WIP")
|
||||
L10n.wip.text
|
||||
}) {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("All Genres")
|
||||
L10n.allGenres.text
|
||||
.foregroundColor(.black)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -98,7 +98,7 @@ struct LibraryListView: View {
|
|||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("All Media", comment: ""))
|
||||
.navigationTitle(L10n.allMedia)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
|
|
|
@ -46,7 +46,7 @@ struct LibrarySearchView: View {
|
|||
var suggestionsListView: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8) {
|
||||
Text("Suggestions")
|
||||
L10n.suggestions.text
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
@ -55,7 +55,7 @@ struct LibraryView: View {
|
|||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 25))
|
||||
}.disabled(!viewModel.hasPreviousPage)
|
||||
Text("Page \(String(viewModel.currentPage + 1)) of \(String(viewModel.totalPages))")
|
||||
Text(L10n.pageOfWithNumbers(String(viewModel.currentPage + 1), String(viewModel.totalPages)))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Button {
|
||||
|
@ -72,7 +72,7 @@ struct LibraryView: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Text("No results.")
|
||||
L10n.noResults.text
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(title, displayMode: .inline)
|
||||
|
|
|
@ -32,7 +32,7 @@ struct LoadingView<Content>: View where Content: View {
|
|||
// indicator, with some text underneath showing what we are doing
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Text(text ?? L10n.loading).fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Spacer()
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 10))
|
||||
|
@ -70,7 +70,7 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
|||
// indicator, with some text underneath showing what we are doing
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Text(text ?? L10n.loading).fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Spacer()
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 10))
|
||||
|
|
|
@ -17,7 +17,7 @@ struct NextUpView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Next Up")
|
||||
L10n.nextUp.text
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding(.leading, 16)
|
||||
|
|
|
@ -11,10 +11,10 @@ import CoreStore
|
|||
import SwiftUI
|
||||
|
||||
struct ServerListView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
||||
@ObservedObject var viewModel: ServerListViewModel
|
||||
|
||||
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
|
@ -27,22 +27,22 @@ struct ServerListView: View {
|
|||
.foregroundColor(Color(UIColor.secondarySystemFill))
|
||||
.frame(height: 100)
|
||||
.cornerRadius(10)
|
||||
|
||||
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "server.rack")
|
||||
.font(.system(size: 36))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(server.name)
|
||||
.font(.title2)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
|
||||
Text(server.uri)
|
||||
.font(.footnote)
|
||||
.disabled(true)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
||||
Text(viewModel.userTextFor(server: server))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
|
@ -62,13 +62,13 @@ struct ServerListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var noServerView: some View {
|
||||
VStack {
|
||||
Text("Connect to a Jellyfin server to get started")
|
||||
.frame(minWidth: 50, maxWidth: 240)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
|
||||
Button {
|
||||
serverListRouter.route(to: \.connectToServer)
|
||||
} label: {
|
||||
|
@ -80,15 +80,15 @@ struct ServerListView: View {
|
|||
.cornerRadius(10)
|
||||
.padding(.horizontal, 30)
|
||||
.padding([.top, .bottom], 20)
|
||||
|
||||
Text("Connect")
|
||||
|
||||
L10n.connect.text
|
||||
.foregroundColor(Color.white)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if viewModel.servers.isEmpty {
|
||||
|
@ -98,7 +98,7 @@ struct ServerListView: View {
|
|||
listView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var trailingToolbarContent: some View {
|
||||
if viewModel.servers.isEmpty {
|
||||
|
@ -111,7 +111,7 @@ struct ServerListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var leadingToolbarContent: some View {
|
||||
Button {
|
||||
serverListRouter.route(to: \.basicAppSettings)
|
||||
|
@ -119,7 +119,7 @@ struct ServerListView: View {
|
|||
Image(systemName: "gearshape.fill")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle("Servers")
|
||||
|
|
|
@ -11,7 +11,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
|
@ -27,7 +27,7 @@ struct SettingsView: View {
|
|||
var body: some View {
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
|
||||
|
||||
// There is a bug where the SettingsView attmempts to remake itself upon signing out
|
||||
// so this check is made
|
||||
if SessionManager.main.currentLogin == nil {
|
||||
|
@ -81,7 +81,7 @@ struct SettingsView: View {
|
|||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(header: Text("Playback")) {
|
||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
|
@ -108,7 +108,7 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Accessibility")) {
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
||||
SearchablePicker(label: "Preferred subtitle language",
|
||||
options: viewModel.langs,
|
||||
|
@ -129,7 +129,7 @@ struct SettingsView: View {
|
|||
.auto
|
||||
},
|
||||
set: { autoSelectAudioLangcode = $0.isoCode }))
|
||||
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
import SwiftUI
|
||||
|
||||
struct UserListView: View {
|
||||
|
||||
|
||||
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
||||
@ObservedObject var viewModel: UserListViewModel
|
||||
|
||||
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
|
@ -26,13 +26,13 @@ struct UserListView: View {
|
|||
.foregroundColor(Color(UIColor.secondarySystemFill))
|
||||
.frame(height: 50)
|
||||
.cornerRadius(10)
|
||||
|
||||
|
||||
HStack {
|
||||
Text(user.username)
|
||||
.font(.title2)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
|
@ -51,13 +51,13 @@ struct UserListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var noUserView: some View {
|
||||
VStack {
|
||||
Text("Sign in to get started")
|
||||
.frame(minWidth: 50, maxWidth: 240)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
|
||||
Button {
|
||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||
} label: {
|
||||
|
@ -69,7 +69,7 @@ struct UserListView: View {
|
|||
.cornerRadius(10)
|
||||
.padding(.horizontal, 30)
|
||||
.padding([.top, .bottom], 20)
|
||||
|
||||
|
||||
Text("Sign in")
|
||||
.foregroundColor(Color.white)
|
||||
.bold()
|
||||
|
@ -77,7 +77,7 @@ struct UserListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if viewModel.users.isEmpty {
|
||||
|
@ -87,7 +87,7 @@ struct UserListView: View {
|
|||
listView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarContent: some View {
|
||||
if viewModel.users.isEmpty {
|
||||
|
@ -102,7 +102,7 @@ struct UserListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle(viewModel.server.name)
|
||||
|
|
|
@ -11,23 +11,23 @@ import SwiftUI
|
|||
import Stinsen
|
||||
|
||||
struct UserSignInView: View {
|
||||
|
||||
|
||||
@ObservedObject var viewModel: UserSignInViewModel
|
||||
@State private var username: String = ""
|
||||
@State private var password: String = ""
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
|
||||
Section {
|
||||
TextField("Username", text: $username)
|
||||
TextField(L10n.username, text: $username)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
SecureField("Password", text: $password)
|
||||
|
||||
SecureField(L10n.password, text: $password)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
|
||||
if viewModel.isLoading {
|
||||
Button(role: .destructive) {
|
||||
viewModel.cancelSignIn()
|
||||
|
|
|
@ -153,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
sendProgressReport(eventName: "unpause")
|
||||
} else {
|
||||
sendJellyfinCommand(command: "Seek", options: [
|
||||
"position": Int(secondsScrubbedTo),
|
||||
"position": Int(secondsScrubbedTo)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -421,7 +421,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
if manifest.type == "Movie" {
|
||||
titleLabel.text = manifest.name ?? ""
|
||||
} else {
|
||||
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”"
|
||||
titleLabel.text = "\(L10n.seasonAndEpisode(String(manifest.parentIndexNumber ?? 0), String(manifest.indexNumber ?? 0))) “\(manifest.name ?? "")”"
|
||||
|
||||
setupNextUpView()
|
||||
upNextViewModel.delegate = self
|
||||
|
@ -664,8 +664,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
subtitleTrackArray.forEach { subtitle in
|
||||
if Defaults[.isAutoSelectSubtitles] {
|
||||
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
||||
subtitle.languageCode.contains(Locale.current.languageCode ?? "")
|
||||
{
|
||||
subtitle.languageCode.contains(Locale.current.languageCode ?? "") {
|
||||
selectedCaptionTrack = subtitle.id
|
||||
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
||||
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
||||
|
@ -816,7 +815,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
shouldShowLoadingScreen = true
|
||||
videoControlsView.isHidden = true
|
||||
|
||||
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”"
|
||||
titleLabel.text = "\(L10n.seasonAndEpisode(String(manifest.parentIndexNumber ?? 0), String(manifest.indexNumber ?? 0))) “\(manifest.name ?? "")”"
|
||||
|
||||
setupMediaPlayer()
|
||||
getNextEpisode()
|
||||
|
@ -854,7 +853,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
|||
if hasSentRemoteSeek == false {
|
||||
hasSentRemoteSeek = true
|
||||
sendJellyfinCommand(command: "Seek", options: [
|
||||
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position),
|
||||
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -880,7 +879,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
|||
"serverId": SessionManager.main.currentLogin.server.id,
|
||||
"serverVersion": "10.8.0",
|
||||
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
|
||||
"subtitleBurnIn": false,
|
||||
"subtitleBurnIn": false
|
||||
]
|
||||
let jsonData = JSON(payload)
|
||||
|
||||
|
@ -935,8 +934,8 @@ extension PlayerViewController: GCKSessionManagerListener {
|
|||
"Name": manifest.name!,
|
||||
"Type": manifest.type!,
|
||||
"MediaType": manifest.mediaType!,
|
||||
"IsFolder": manifest.isFolder!,
|
||||
]],
|
||||
"IsFolder": manifest.isFolder!
|
||||
]]
|
||||
]
|
||||
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
|
||||
}
|
||||
|
@ -1104,8 +1103,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
|||
|
||||
typealias UIViewControllerType = PlayerViewController
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls
|
||||
.UIViewControllerType
|
||||
{
|
||||
.UIViewControllerType {
|
||||
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
||||
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
||||
customViewController.manifest = item
|
||||
|
|
|
@ -56,7 +56,7 @@ struct VideoPlayerCastDeviceSelector: View {
|
|||
delegate?.castPopoverDismissed()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
L10n.connect.text
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
Image(systemName: "bonjour")
|
||||
|
@ -66,14 +66,14 @@ struct VideoPlayerCastDeviceSelector: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Text("No Cast devices found..")
|
||||
L10n.noCastdevicesfound.text
|
||||
.foregroundColor(.secondary)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(NSLocalizedString("Select Cast Destination", comment: ""))
|
||||
.navigationTitle(L10n.selectCastDestination)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
|
@ -82,7 +82,7 @@ struct VideoPlayerCastDeviceSelector: View {
|
|||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back").font(.callout)
|
||||
L10n.back.text.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ struct VideoPlayerSettings: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
Picker(NSLocalizedString("Closed Captions", comment: ""), selection: $captionTrack) {
|
||||
Picker(L10n.closedCaptions, selection: $captionTrack) {
|
||||
ForEach(delegate.subtitleTrackArray, id: \.id) { caption in
|
||||
Text(caption.name).tag(caption.id)
|
||||
}
|
||||
|
@ -48,14 +48,14 @@ struct VideoPlayerSettings: View {
|
|||
.onChange(of: captionTrack) { track in
|
||||
self.delegate.subtitleTrackChanged(newTrackID: track)
|
||||
}
|
||||
Picker(NSLocalizedString("Audio Track", comment: ""), selection: $audioTrack) {
|
||||
Picker(L10n.audioTrack, selection: $audioTrack) {
|
||||
ForEach(delegate.audioTrackArray, id: \.id) { caption in
|
||||
Text(caption.name).tag(caption.id).lineLimit(1)
|
||||
}
|
||||
}.onChange(of: audioTrack) { track in
|
||||
self.delegate.audioTrackChanged(newTrackID: track)
|
||||
}
|
||||
Picker(NSLocalizedString("Playback Speed", comment: ""), selection: $playbackSpeedSelection) {
|
||||
Picker(L10n.playbackSpeed, selection: $playbackSpeedSelection) {
|
||||
ForEach(delegate.playbackSpeeds.indices, id: \.self) { speedIndex in
|
||||
let speed = delegate.playbackSpeeds[speedIndex]
|
||||
Text("\(String(speed))x").tag(speedIndex)
|
||||
|
@ -65,7 +65,7 @@ struct VideoPlayerSettings: View {
|
|||
self.delegate.playbackSpeedChanged(index: index)
|
||||
})
|
||||
}.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(NSLocalizedString("Audio & Captions", comment: ""))
|
||||
.navigationTitle(L10n.audioAndCaptions)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
|
@ -74,7 +74,7 @@ struct VideoPlayerSettings: View {
|
|||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back").font(.callout)
|
||||
L10n.back.text.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ struct VideoUpNextView: View {
|
|||
} label: {
|
||||
HStack {
|
||||
VStack {
|
||||
Text("Play Next")
|
||||
L10n.playNext.text
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
|
13
Podfile
13
Podfile
|
@ -1,11 +1,20 @@
|
|||
use_frameworks!
|
||||
inhibit_all_warnings!
|
||||
def shared_pods
|
||||
pod 'SwiftGen'
|
||||
end
|
||||
|
||||
target 'JellyfinPlayer iOS' do
|
||||
platform :ios, '14.0'
|
||||
use_frameworks!
|
||||
shared_pods
|
||||
pod 'google-cast-sdk'
|
||||
pod 'MobileVLCKit'
|
||||
end
|
||||
target 'JellyfinPlayer tvOS' do
|
||||
platform :tvos, '14.0'
|
||||
use_frameworks!
|
||||
shared_pods
|
||||
pod 'TVVLCKit'
|
||||
end
|
||||
target 'WidgetExtension' do
|
||||
shared_pods
|
||||
end
|
|
@ -12,11 +12,11 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class BasicAppSettingsCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start)
|
||||
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
|
||||
}
|
||||
|
|
|
@ -12,16 +12,16 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class ConnectToServerCoodinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var userSignIn = makeUserSignIn
|
||||
|
||||
|
||||
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
|
||||
return UserSignInCoordinator(viewModel: .init(server: server))
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
ConnectToServerView(viewModel: ConnectToServerViewModel())
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ import SwiftUI
|
|||
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
||||
|
||||
final class FilterCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \FilterCoordinator.start)
|
||||
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
@Binding var filters: LibraryFilters
|
||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class HomeCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \HomeCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
@ -34,11 +34,11 @@ final class HomeCoordinator: NavigationCoordinatable {
|
|||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
|
||||
|
||||
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
||||
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
||||
}
|
||||
|
||||
|
||||
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
|
||||
return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class ItemCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \ItemCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
|
|
@ -15,7 +15,7 @@ import SwiftUI
|
|||
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
|
||||
|
||||
final class LibraryCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \LibraryCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
@ -49,7 +49,7 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
|||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
|
||||
|
||||
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
||||
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class LibraryListCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
|
|
@ -25,13 +25,13 @@ final class MainCoordinator: NavigationCoordinatable {
|
|||
} else {
|
||||
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
|
||||
}
|
||||
|
||||
|
||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
UIScrollView.appearance().keyboardDismissMode = .onDrag
|
||||
|
||||
|
||||
// Back bar button item setup
|
||||
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
|
||||
let barAppearance = UINavigationBar.appearance()
|
||||
|
|
|
@ -26,7 +26,7 @@ final class MainTabCoordinator: TabCoordinatable {
|
|||
|
||||
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||
Image(systemName: "house")
|
||||
Text("Home")
|
||||
L10n.home.text
|
||||
}
|
||||
|
||||
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
|
@ -35,14 +35,14 @@ final class MainTabCoordinator: TabCoordinatable {
|
|||
|
||||
@ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View {
|
||||
Image(systemName: "folder")
|
||||
Text("All Media")
|
||||
L10n.allMedia.text
|
||||
}
|
||||
|
||||
@ViewBuilder func customize(_ view: AnyView) -> some View {
|
||||
view.onAppear {
|
||||
AppURLHandler.shared.appURLState = .allowed
|
||||
// TODO: todo
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
AppURLHandler.shared.processLaunchedURLIfNeeded()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,14 +17,14 @@ final class MainCoordinator: NavigationCoordinatable {
|
|||
|
||||
@Root var mainTab = makeMainTab
|
||||
@Root var serverList = makeServerList
|
||||
|
||||
|
||||
init() {
|
||||
if SessionManager.main.currentLogin != nil {
|
||||
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
|
||||
} else {
|
||||
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
|
||||
}
|
||||
|
||||
|
||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||
|
||||
|
|
|
@ -19,24 +19,24 @@ final class MainTabCoordinator: TabCoordinatable {
|
|||
\MainTabCoordinator.other,
|
||||
\MainTabCoordinator.settings
|
||||
])
|
||||
|
||||
|
||||
@Route(tabItem: makeHomeTab) var home = makeHome
|
||||
@Route(tabItem: makeTvTab) var tv = makeTv
|
||||
@Route(tabItem: makeMoviesTab) var movies = makeMovies
|
||||
@Route(tabItem: makeOtherTab) var other = makeOther
|
||||
@Route(tabItem: makeSettingsTab) var settings = makeSettings
|
||||
|
||||
|
||||
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
|
||||
return NavigationViewCoordinator(HomeCoordinator())
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "house")
|
||||
Text("Home")
|
||||
L10n.home.text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> {
|
||||
return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows"))
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ final class MainTabCoordinator: TabCoordinatable {
|
|||
Text("TV Shows")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> {
|
||||
return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies"))
|
||||
}
|
||||
|
@ -69,11 +69,11 @@ final class MainTabCoordinator: TabCoordinatable {
|
|||
Text("Other")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
|
||||
return NavigationViewCoordinator(SettingsCoordinator())
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "gearshape.fill")
|
||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class MovieLibrariesCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
|
|
@ -13,7 +13,7 @@ import SwiftUI
|
|||
import JellyfinAPI
|
||||
|
||||
final class SearchCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \SearchCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
|
|
@ -12,26 +12,26 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class ServerListCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \ServerListCoordinator.start)
|
||||
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var connectToServer = makeConnectToServer
|
||||
@Route(.push) var userList = makeUserList
|
||||
@Route(.modal) var basicAppSettings = makeBasicAppSettings
|
||||
|
||||
|
||||
func makeConnectToServer() -> ConnectToServerCoodinator {
|
||||
ConnectToServerCoodinator()
|
||||
}
|
||||
|
||||
|
||||
func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator {
|
||||
UserListCoordinator(viewModel: .init(server: server))
|
||||
}
|
||||
|
||||
|
||||
func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> {
|
||||
NavigationViewCoordinator(BasicAppSettingsCoordinator())
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
ServerListView(viewModel: ServerListViewModel())
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class SettingsCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class TVLibrariesCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \TVLibrariesCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
|
|
@ -12,22 +12,22 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class UserListCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \UserListCoordinator.start)
|
||||
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var userSignIn = makeUserSignIn
|
||||
|
||||
|
||||
let viewModel: UserListViewModel
|
||||
|
||||
|
||||
init(viewModel: UserListViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
|
||||
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
|
||||
return UserSignInCoordinator(viewModel: .init(server: server))
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
UserListView(viewModel: viewModel)
|
||||
}
|
||||
|
|
|
@ -12,17 +12,17 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class UserSignInCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \UserSignInCoordinator.start)
|
||||
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
|
||||
let viewModel: UserSignInViewModel
|
||||
|
||||
|
||||
init(viewModel: UserSignInViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
UserSignInView(viewModel: viewModel)
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||
|
||||
|
||||
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
|
||||
let item: BaseItemDto
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
|
|
|
@ -16,7 +16,7 @@ struct ErrorMessage: Identifiable {
|
|||
let title: String
|
||||
let displayMessage: String
|
||||
let logConstructor: LogConstructor
|
||||
|
||||
|
||||
// Chosen value such that if an error has this code, don't show the code to the UI
|
||||
// This was chosen because of its unlikelyhood to ever be used
|
||||
static let noShowErrorCode = -69420
|
||||
|
|
|
@ -85,13 +85,13 @@ enum NetworkError: Error {
|
|||
logMessage = "Cannot connect to host."
|
||||
logConstructor.message = logMessage
|
||||
errorMessage = ErrorMessage(code: err._code,
|
||||
title: "Error",
|
||||
title: L10n.error,
|
||||
displayMessage: displayMessage,
|
||||
logConstructor: logConstructor)
|
||||
default:
|
||||
logConstructor.message = logMessage
|
||||
errorMessage = ErrorMessage(code: err._code,
|
||||
title: "Error",
|
||||
title: L10n.error,
|
||||
displayMessage: displayMessage,
|
||||
logConstructor: logConstructor)
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ enum NetworkError: Error {
|
|||
case .error:
|
||||
logConstructor.message = logMessage
|
||||
errorMessage = ErrorMessage(code: 0,
|
||||
title: "Error",
|
||||
title: L10n.error,
|
||||
displayMessage: displayMessage,
|
||||
logConstructor: logConstructor)
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ enum NetworkError: Error {
|
|||
default:
|
||||
logConstructor.message = logMessage
|
||||
errorMessage = ErrorMessage(code: code,
|
||||
title: "Error",
|
||||
title: L10n.error,
|
||||
displayMessage: displayMessage,
|
||||
logConstructor: logConstructor)
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ extension BaseItemDto: PortraitImageStackable {
|
|||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||
return self.getPrimaryImage(maxWidth: maxWidth)
|
||||
}
|
||||
|
||||
|
||||
public var title: String {
|
||||
return self.name ?? ""
|
||||
}
|
||||
|
||||
|
||||
public var description: String? {
|
||||
switch self.itemType {
|
||||
case .season:
|
||||
|
@ -31,11 +31,11 @@ extension BaseItemDto: PortraitImageStackable {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var blurHash: String {
|
||||
return self.getPrimaryImageBlurHash()
|
||||
}
|
||||
|
||||
|
||||
public var failureInitials: String {
|
||||
guard let name = self.name else { return "" }
|
||||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||
|
|
|
@ -14,7 +14,7 @@ import UIKit
|
|||
// 001fC^ = dark grey plain blurhash
|
||||
|
||||
public extension BaseItemDto {
|
||||
|
||||
|
||||
// MARK: Images
|
||||
|
||||
func getSeriesBackdropImageBlurHash() -> String {
|
||||
|
@ -80,7 +80,7 @@ public extension BaseItemDto {
|
|||
|
||||
func getEpisodeLocator() -> String? {
|
||||
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
|
||||
return "S\(seasonNo):E\(episodeNo)"
|
||||
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -152,17 +152,17 @@ public extension BaseItemDto {
|
|||
return "\(String(progminutes))m"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: ItemType
|
||||
|
||||
|
||||
enum ItemType: String {
|
||||
case movie = "Movie"
|
||||
case season = "Season"
|
||||
case episode = "Episode"
|
||||
case series = "Series"
|
||||
|
||||
|
||||
case unknown
|
||||
|
||||
|
||||
var showDetails: Bool {
|
||||
switch self {
|
||||
case .season, .series:
|
||||
|
@ -172,14 +172,14 @@ public extension BaseItemDto {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var itemType: ItemType {
|
||||
guard let originalType = self.type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
|
||||
return knownType
|
||||
}
|
||||
|
||||
|
||||
// MARK: PortraitHeaderViewURL
|
||||
|
||||
|
||||
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
||||
switch self.itemType {
|
||||
case .movie, .season, .series:
|
||||
|
|
|
@ -10,7 +10,7 @@ import JellyfinAPI
|
|||
import UIKit
|
||||
|
||||
extension BaseItemPerson {
|
||||
|
||||
|
||||
// MARK: Get Image
|
||||
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
||||
let imageType = "Primary"
|
||||
|
@ -28,10 +28,9 @@ extension BaseItemPerson {
|
|||
|
||||
return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: First Role
|
||||
|
||||
|
||||
// Jellyfin will grab all roles the person played in the show which makes the role
|
||||
// text too long. This will grab the first role which:
|
||||
// - assumes that the most important role is the first
|
||||
|
@ -40,16 +39,16 @@ extension BaseItemPerson {
|
|||
guard let role = self.role else { return nil }
|
||||
let split = role.split(separator: "/")
|
||||
guard split.count > 1 else { return role }
|
||||
|
||||
|
||||
guard let firstRole = split.first?.trimmingCharacters(in: CharacterSet(charactersIn: " ")), let lastRole = split.last?.trimmingCharacters(in: CharacterSet(charactersIn: " ")) else { return role }
|
||||
|
||||
|
||||
var final = firstRole
|
||||
|
||||
|
||||
if let lastOpenIndex = lastRole.lastIndex(of: "("), let lastClosingIndex = lastRole.lastIndex(of: ")") {
|
||||
let roleText = lastRole[lastOpenIndex...lastClosingIndex]
|
||||
final.append(" \(roleText)")
|
||||
}
|
||||
|
||||
|
||||
return final
|
||||
}
|
||||
}
|
||||
|
@ -59,19 +58,19 @@ extension BaseItemPerson: PortraitImageStackable {
|
|||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||
return self.getImage(baseURL: SessionManager.main.currentLogin.server.uri, maxWidth: maxWidth)
|
||||
}
|
||||
|
||||
|
||||
public var title: String {
|
||||
return self.name ?? ""
|
||||
}
|
||||
|
||||
|
||||
public var description: String? {
|
||||
return self.firstRole()
|
||||
}
|
||||
|
||||
|
||||
public var blurHash: String {
|
||||
return self.getBlurHash()
|
||||
}
|
||||
|
||||
|
||||
public var failureInitials: String {
|
||||
guard let name = self.name else { return "" }
|
||||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||
|
@ -81,7 +80,7 @@ extension BaseItemPerson: PortraitImageStackable {
|
|||
|
||||
// MARK: DiplayedType
|
||||
extension BaseItemPerson {
|
||||
|
||||
|
||||
// Only displayed person types.
|
||||
// Will ignore people like "GuestStar"
|
||||
enum DisplayedType: String, CaseIterable {
|
||||
|
@ -89,7 +88,7 @@ extension BaseItemPerson {
|
|||
case director = "Director"
|
||||
case writer = "Writer"
|
||||
case producer = "Producer"
|
||||
|
||||
|
||||
static var allCasesRaw: [String] {
|
||||
return self.allCases.map({ $0.rawValue })
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
import Foundation
|
||||
|
||||
struct JellyfinAPIError: Error {
|
||||
|
||||
|
||||
private let message: String
|
||||
|
||||
|
||||
init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
|
||||
var localizedDescription: String {
|
||||
return message
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension String {
|
||||
func removeRegexMatches(pattern: String, replaceWith: String = "") -> String {
|
||||
|
@ -31,4 +32,8 @@ extension String {
|
|||
|
||||
return "\(padString)\(self)"
|
||||
}
|
||||
|
||||
var text: Text {
|
||||
Text(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
// swiftlint:disable all
|
||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable superfluous_disable_command file_length implicit_return
|
||||
|
||||
// MARK: - Strings
|
||||
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
internal enum L10n {
|
||||
/// Accessibility
|
||||
internal static let accessibility = L10n.tr("Localizable", "accessibility")
|
||||
/// All Genres
|
||||
internal static let allGenres = L10n.tr("Localizable", "allGenres")
|
||||
/// All Media
|
||||
internal static let allMedia = L10n.tr("Localizable", "allMedia")
|
||||
/// Appearance
|
||||
internal static let appearance = L10n.tr("Localizable", "appearance")
|
||||
/// Apply
|
||||
internal static let apply = L10n.tr("Localizable", "apply")
|
||||
/// Audio & Captions
|
||||
internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions")
|
||||
/// Audio Track
|
||||
internal static let audioTrack = L10n.tr("Localizable", "audioTrack")
|
||||
/// Back
|
||||
internal static let back = L10n.tr("Localizable", "back")
|
||||
/// CAST
|
||||
internal static let cast = L10n.tr("Localizable", "cast")
|
||||
/// Change Server
|
||||
internal static let changeServer = L10n.tr("Localizable", "changeServer")
|
||||
/// Closed Captions
|
||||
internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions")
|
||||
/// Connect
|
||||
internal static let connect = L10n.tr("Localizable", "connect")
|
||||
/// Connect Manually
|
||||
internal static let connectManually = L10n.tr("Localizable", "connectManually")
|
||||
/// Connect to Jellyfin
|
||||
internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin")
|
||||
/// Connect to Server
|
||||
internal static let connectToServer = L10n.tr("Localizable", "connectToServer")
|
||||
/// Continue Watching
|
||||
internal static let continueWatching = L10n.tr("Localizable", "continueWatching")
|
||||
/// Dark
|
||||
internal static let dark = L10n.tr("Localizable", "dark")
|
||||
/// DIRECTOR
|
||||
internal static let director = L10n.tr("Localizable", "director")
|
||||
/// Discovered Servers
|
||||
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers")
|
||||
/// Display order
|
||||
internal static let displayOrder = L10n.tr("Localizable", "displayOrder")
|
||||
/// Empty Next Up
|
||||
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp")
|
||||
/// Episodes
|
||||
internal static let episodes = L10n.tr("Localizable", "episodes")
|
||||
/// Error
|
||||
internal static let error = L10n.tr("Localizable", "error")
|
||||
/// Filter Results
|
||||
internal static let filterResults = L10n.tr("Localizable", "filterResults")
|
||||
/// Filters
|
||||
internal static let filters = L10n.tr("Localizable", "filters")
|
||||
/// Genres
|
||||
internal static let genres = L10n.tr("Localizable", "genres")
|
||||
/// Home
|
||||
internal static let home = L10n.tr("Localizable", "home")
|
||||
/// Latest %@
|
||||
internal static func latestWithString(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "latestWithString", String(describing: p1))
|
||||
}
|
||||
/// Library
|
||||
internal static let library = L10n.tr("Localizable", "library")
|
||||
/// Light
|
||||
internal static let light = L10n.tr("Localizable", "light")
|
||||
/// Loading
|
||||
internal static let loading = L10n.tr("Localizable", "loading")
|
||||
/// Local Servers
|
||||
internal static let localServers = L10n.tr("Localizable", "localServers")
|
||||
/// Login
|
||||
internal static let login = L10n.tr("Localizable", "login")
|
||||
/// Login to %@
|
||||
internal static func loginToWithString(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "loginToWithString", String(describing: p1))
|
||||
}
|
||||
/// More Like This
|
||||
internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
|
||||
/// Next Up
|
||||
internal static let nextUp = L10n.tr("Localizable", "nextUp")
|
||||
/// No Cast devices found..
|
||||
internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound")
|
||||
/// No results.
|
||||
internal static let noResults = L10n.tr("Localizable", "noResults")
|
||||
/// Type: %@ not implemented yet :(
|
||||
internal static func notImplementedYetWithType(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1))
|
||||
}
|
||||
/// Ok
|
||||
internal static let ok = L10n.tr("Localizable", "ok")
|
||||
/// Other User
|
||||
internal static let otherUser = L10n.tr("Localizable", "otherUser")
|
||||
/// Page %1$@ of %2$@
|
||||
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// Password
|
||||
internal static let password = L10n.tr("Localizable", "password")
|
||||
/// Play
|
||||
internal static let play = L10n.tr("Localizable", "play")
|
||||
/// Playback settings
|
||||
internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings")
|
||||
/// Playback Speed
|
||||
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed")
|
||||
/// Play Next
|
||||
internal static let playNext = L10n.tr("Localizable", "playNext")
|
||||
/// Reset
|
||||
internal static let reset = L10n.tr("Localizable", "reset")
|
||||
/// Search...
|
||||
internal static let search = L10n.tr("Localizable", "search")
|
||||
/// S%1$@:E%2$@
|
||||
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// Seasons
|
||||
internal static let seasons = L10n.tr("Localizable", "seasons")
|
||||
/// See All
|
||||
internal static let seeAll = L10n.tr("Localizable", "seeAll")
|
||||
/// Select Cast Destination
|
||||
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
|
||||
/// Server Information
|
||||
internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
|
||||
/// Server URL
|
||||
internal static let serverURL = L10n.tr("Localizable", "serverURL")
|
||||
/// Signed in as %@
|
||||
internal static func signedInAsWithString(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1))
|
||||
}
|
||||
/// Sort by
|
||||
internal static let sortBy = L10n.tr("Localizable", "sortBy")
|
||||
/// STUDIO
|
||||
internal static let studio = L10n.tr("Localizable", "studio")
|
||||
/// Studios
|
||||
internal static let studios = L10n.tr("Localizable", "studios")
|
||||
/// Suggestions
|
||||
internal static let suggestions = L10n.tr("Localizable", "suggestions")
|
||||
/// Switch user
|
||||
internal static let switchUser = L10n.tr("Localizable", "switchUser")
|
||||
/// System
|
||||
internal static let system = L10n.tr("Localizable", "system")
|
||||
/// Tags
|
||||
internal static let tags = L10n.tr("Localizable", "tags")
|
||||
/// Try again
|
||||
internal static let tryAgain = L10n.tr("Localizable", "tryAgain")
|
||||
/// Username
|
||||
internal static let username = L10n.tr("Localizable", "username")
|
||||
/// Who's watching?
|
||||
internal static let whosWatching = L10n.tr("Localizable", "WhosWatching")
|
||||
/// WIP
|
||||
internal static let wip = L10n.tr("Localizable", "wip")
|
||||
/// Your Favorites
|
||||
internal static let yourFavorites = L10n.tr("Localizable", "yourFavorites")
|
||||
}
|
||||
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
|
||||
// MARK: - Implementation Details
|
||||
|
||||
extension L10n {
|
||||
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
|
||||
let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table)
|
||||
return String(format: format, locale: Locale.current, arguments: args)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable convenience_type
|
||||
private final class BundleToken {
|
||||
static let bundle: Bundle = {
|
||||
#if SWIFT_PACKAGE
|
||||
return Bundle.module
|
||||
#else
|
||||
return Bundle(for: BundleToken.self)
|
||||
#endif
|
||||
}()
|
||||
}
|
||||
// swiftlint:enable convenience_type
|
|
@ -16,7 +16,14 @@ enum AppAppearance: String, CaseIterable, Defaults.Serializable {
|
|||
case light
|
||||
|
||||
var localizedName: String {
|
||||
return NSLocalizedString(self.rawValue.capitalized, comment: "")
|
||||
switch self {
|
||||
case .system:
|
||||
return L10n.system
|
||||
case .dark:
|
||||
return L10n.dark
|
||||
case .light:
|
||||
return L10n.light
|
||||
}
|
||||
}
|
||||
|
||||
var style: UIUserInterfaceStyle {
|
||||
|
|
|
@ -18,11 +18,8 @@ enum DetailItemType: String {
|
|||
}
|
||||
|
||||
struct DetailItem {
|
||||
|
||||
|
||||
let baseItem: BaseItemDto
|
||||
let type: DetailItemType
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -73,11 +73,11 @@ enum ItemType: String {
|
|||
case movie = "Movie"
|
||||
case series = "Series"
|
||||
case season = "Season"
|
||||
|
||||
|
||||
var localized: String {
|
||||
switch self {
|
||||
case .episode:
|
||||
return "Episodes"
|
||||
return L10n.episodes
|
||||
case .movie:
|
||||
return "Movies"
|
||||
case .series:
|
||||
|
|
|
@ -46,7 +46,7 @@ public class ServerDiscovery {
|
|||
case name = "Name"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let broadcastConn: UDPBroadcastConnection
|
||||
|
||||
public init() {
|
||||
|
|
|
@ -19,83 +19,83 @@ typealias CurrentLogin = (server: SwiftfinStore.State.Server, user: SwiftfinStor
|
|||
|
||||
// MARK: NewSessionManager
|
||||
final class SessionManager {
|
||||
|
||||
|
||||
// MARK: currentLogin
|
||||
private(set) var currentLogin: CurrentLogin!
|
||||
|
||||
|
||||
// MARK: main
|
||||
static let main = SessionManager()
|
||||
|
||||
|
||||
private init() {
|
||||
if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID],
|
||||
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) {
|
||||
|
||||
|
||||
guard let server = user.server, let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") }
|
||||
guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return }
|
||||
|
||||
|
||||
JellyfinAPI.basePath = server.uri
|
||||
setAuthHeader(with: accessToken.value)
|
||||
currentLogin = (server: existingServer.state, user: user.state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func generateServerUserID(server: SwiftfinStore.Models.StoredServer, user: SwiftfinStore.Models.StoredUser) -> String {
|
||||
return "\(server.id)-\(user.id)"
|
||||
}
|
||||
|
||||
|
||||
func fetchServers() -> [SwiftfinStore.State.Server] {
|
||||
let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>())
|
||||
return servers.map({ $0.state })
|
||||
}
|
||||
|
||||
|
||||
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
|
||||
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
|
||||
else { fatalError("No stored server associated with given state server?") }
|
||||
return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username })
|
||||
}
|
||||
|
||||
|
||||
// Connects to a server at the given uri, storing if successful
|
||||
func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
|
||||
var uriComponents = URLComponents(string: uri) ?? URLComponents()
|
||||
|
||||
|
||||
if uriComponents.scheme == nil {
|
||||
uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue
|
||||
}
|
||||
|
||||
|
||||
var uri = uriComponents.string ?? ""
|
||||
|
||||
|
||||
if uri.last == "/" {
|
||||
uri = String(uri.dropLast())
|
||||
}
|
||||
|
||||
|
||||
JellyfinAPI.basePath = uri
|
||||
|
||||
|
||||
return SystemAPI.getPublicSystemInfo()
|
||||
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
|
||||
|
||||
|
||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||
let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>())
|
||||
|
||||
|
||||
guard let name = response.serverName,
|
||||
let id = response.id,
|
||||
let os = response.operatingSystem,
|
||||
let version = response.version else { throw JellyfinAPIError("Missing server data from network call") }
|
||||
|
||||
|
||||
newServer.uri = uri
|
||||
newServer.name = name
|
||||
newServer.id = id
|
||||
newServer.os = os
|
||||
newServer.version = version
|
||||
newServer.users = []
|
||||
|
||||
|
||||
// Check for existing server on device
|
||||
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", newServer.id)]) {
|
||||
throw SwiftfinStore.Errors.existingServer(existingServer.state)
|
||||
}
|
||||
|
||||
|
||||
return (newServer, transaction)
|
||||
})
|
||||
.handleEvents(receiveOutput: { (_, transaction) in
|
||||
|
@ -106,57 +106,57 @@ final class SessionManager {
|
|||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
// Logs in a user with an associated server, storing if successful
|
||||
func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||
setAuthHeader(with: "")
|
||||
|
||||
|
||||
JellyfinAPI.basePath = server.uri
|
||||
|
||||
|
||||
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
|
||||
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
||||
|
||||
|
||||
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
||||
|
||||
|
||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
||||
|
||||
|
||||
guard let username = response.user?.name,
|
||||
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
||||
|
||||
|
||||
newUser.username = username
|
||||
newUser.id = id
|
||||
newUser.appleTVID = ""
|
||||
|
||||
|
||||
// Check for existing user on device
|
||||
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", newUser.id)]) {
|
||||
throw SwiftfinStore.Errors.existingUser(existingUser.state)
|
||||
}
|
||||
|
||||
|
||||
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
||||
newAccessToken.value = accessToken
|
||||
newUser.accessToken = newAccessToken
|
||||
|
||||
|
||||
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)])
|
||||
else { fatalError("No stored server associated with given state server?") }
|
||||
|
||||
|
||||
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
||||
editUserServer.users.insert(newUser)
|
||||
|
||||
|
||||
return (editUserServer, newUser, transaction)
|
||||
})
|
||||
.handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in
|
||||
setAuthHeader(with: user.accessToken?.value ?? "")
|
||||
try? transaction.commitAndWait()
|
||||
|
||||
|
||||
// Fetch for the right queue
|
||||
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
||||
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
||||
|
||||
|
||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
||||
|
||||
|
||||
currentLogin = (server: currentServer.state, user: currentUser.state)
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||
})
|
||||
|
@ -165,7 +165,7 @@ final class SessionManager {
|
|||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
||||
JellyfinAPI.basePath = server.uri
|
||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
||||
|
@ -173,7 +173,7 @@ final class SessionManager {
|
|||
currentLogin = (server: server, user: user)
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||
}
|
||||
|
||||
|
||||
func logout() {
|
||||
currentLogin = nil
|
||||
JellyfinAPI.basePath = ""
|
||||
|
@ -181,66 +181,66 @@ final class SessionManager {
|
|||
SwiftfinStore.Defaults.suite[.lastServerUserID] = nil
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||
}
|
||||
|
||||
|
||||
func purge() {
|
||||
// Delete all servers
|
||||
let servers = fetchServers()
|
||||
|
||||
|
||||
for server in servers {
|
||||
delete(server: server)
|
||||
}
|
||||
|
||||
|
||||
// Delete UserDefaults
|
||||
SwiftfinStore.Defaults.suite.removeAll()
|
||||
|
||||
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
||||
}
|
||||
|
||||
|
||||
func delete(user: SwiftfinStore.State.User) {
|
||||
guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)]) else { fatalError("No stored user for state user?")}
|
||||
_delete(user: storedUser, transaction: nil)
|
||||
}
|
||||
|
||||
|
||||
func delete(server: SwiftfinStore.State.Server) {
|
||||
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { fatalError("No stored server for state server?")}
|
||||
_delete(server: storedServer, transaction: nil)
|
||||
}
|
||||
|
||||
|
||||
private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) {
|
||||
guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?")}
|
||||
|
||||
|
||||
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
|
||||
transaction.delete(storedAccessToken)
|
||||
transaction.delete(user)
|
||||
try? transaction.commitAndWait()
|
||||
}
|
||||
|
||||
|
||||
private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) {
|
||||
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
|
||||
|
||||
|
||||
for user in server.users {
|
||||
_delete(user: user, transaction: transaction)
|
||||
}
|
||||
|
||||
|
||||
transaction.delete(server)
|
||||
try? transaction.commitAndWait()
|
||||
}
|
||||
|
||||
|
||||
private func setAuthHeader(with accessToken: String) {
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
var deviceName = UIDevice.current.name
|
||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||
deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) })
|
||||
|
||||
|
||||
let platform: String
|
||||
#if os(tvOS)
|
||||
platform = "tvOS"
|
||||
#else
|
||||
platform = "iOS"
|
||||
#endif
|
||||
|
||||
|
||||
var header = "MediaBrowser "
|
||||
header.append("Client=\"Jellyfin \(platform)\", ")
|
||||
header.append("Device=\"\(deviceName)\", ")
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
import Foundation
|
||||
|
||||
enum SwiftfinNotificationCenter {
|
||||
|
||||
|
||||
static let main: NotificationCenter = {
|
||||
return NotificationCenter()
|
||||
}()
|
||||
|
||||
|
||||
enum Keys {
|
||||
static let didSignIn = Notification.Name("didSignIn")
|
||||
static let didSignOut = Notification.Name("didSignOut")
|
||||
|
|
|
@ -12,12 +12,12 @@ import CoreStore
|
|||
import Defaults
|
||||
|
||||
enum SwiftfinStore {
|
||||
|
||||
|
||||
// MARK: State
|
||||
// Safe, copyable representations of their underlying CoreStoredObject's
|
||||
// Relationships are represented by the related object's IDs or value
|
||||
enum State {
|
||||
|
||||
|
||||
struct Server {
|
||||
let uri: String
|
||||
let name: String
|
||||
|
@ -25,7 +25,7 @@ enum SwiftfinStore {
|
|||
let os: String
|
||||
let version: String
|
||||
let userIDs: [String]
|
||||
|
||||
|
||||
fileprivate init(uri: String, name: String, id: String, os: String, version: String, usersIDs: [String]) {
|
||||
self.uri = uri
|
||||
self.name = name
|
||||
|
@ -34,54 +34,54 @@ enum SwiftfinStore {
|
|||
self.version = version
|
||||
self.userIDs = usersIDs
|
||||
}
|
||||
|
||||
|
||||
static var sample: Server {
|
||||
return Server(uri: "https://www.notaurl.com", name: "Johnny's Tree", id: "123abc", os: "macOS", version: "1.1.1", usersIDs: ["1", "2"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct User {
|
||||
let username: String
|
||||
let id: String
|
||||
let serverID: String
|
||||
let accessToken: String
|
||||
|
||||
|
||||
fileprivate init(username: String, id: String, serverID: String, accessToken: String) {
|
||||
self.username = username
|
||||
self.id = id
|
||||
self.serverID = serverID
|
||||
self.accessToken = accessToken
|
||||
}
|
||||
|
||||
|
||||
static var sample: User {
|
||||
return User(username: "JohnnyAppleseed", id: "123abc", serverID: "123abc", accessToken: "open-sesame")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Models
|
||||
enum Models {
|
||||
|
||||
|
||||
final class StoredServer: CoreStoreObject {
|
||||
|
||||
|
||||
@Field.Stored("uri")
|
||||
var uri: String = ""
|
||||
|
||||
|
||||
@Field.Stored("name")
|
||||
var name: String = ""
|
||||
|
||||
|
||||
@Field.Stored("id")
|
||||
var id: String = ""
|
||||
|
||||
|
||||
@Field.Stored("os")
|
||||
var os: String = ""
|
||||
|
||||
|
||||
@Field.Stored("version")
|
||||
var version: String = ""
|
||||
|
||||
|
||||
@Field.Relationship("users", inverse: \StoredUser.$server)
|
||||
var users: Set<StoredUser>
|
||||
|
||||
|
||||
var state: State.Server {
|
||||
return State.Server(uri: uri,
|
||||
name: name,
|
||||
|
@ -91,24 +91,24 @@ enum SwiftfinStore {
|
|||
usersIDs: users.map({ $0.id }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class StoredUser: CoreStoreObject {
|
||||
|
||||
|
||||
@Field.Stored("username")
|
||||
var username: String = ""
|
||||
|
||||
|
||||
@Field.Stored("id")
|
||||
var id: String = ""
|
||||
|
||||
|
||||
@Field.Stored("appleTVID")
|
||||
var appleTVID: String = ""
|
||||
|
||||
|
||||
@Field.Relationship("server")
|
||||
var server: StoredServer?
|
||||
|
||||
|
||||
@Field.Relationship("accessToken", inverse: \StoredAccessToken.$user)
|
||||
var accessToken: StoredAccessToken?
|
||||
|
||||
|
||||
var state: State.User {
|
||||
guard let server = server else { fatalError("No server associated with user") }
|
||||
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
|
||||
|
@ -118,23 +118,23 @@ enum SwiftfinStore {
|
|||
accessToken: accessToken.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class StoredAccessToken: CoreStoreObject {
|
||||
|
||||
|
||||
@Field.Stored("value")
|
||||
var value: String = ""
|
||||
|
||||
|
||||
@Field.Relationship("user")
|
||||
var user: StoredUser?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Errors
|
||||
enum Errors {
|
||||
case existingServer(State.Server)
|
||||
case existingUser(State.User)
|
||||
}
|
||||
|
||||
|
||||
// MARK: dataStack
|
||||
static let dataStack: DataStack = {
|
||||
let schema = CoreStoreSchema(modelVersion: "V1",
|
||||
|
@ -148,7 +148,7 @@ enum SwiftfinStore {
|
|||
"Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db],
|
||||
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
|
||||
])
|
||||
|
||||
|
||||
let _dataStack = DataStack(schema)
|
||||
try! _dataStack.addStorageAndWait(
|
||||
SQLiteStore(
|
||||
|
@ -162,7 +162,7 @@ enum SwiftfinStore {
|
|||
|
||||
// MARK: LocalizedError
|
||||
extension SwiftfinStore.Errors: LocalizedError {
|
||||
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .existingServer(_):
|
||||
|
@ -171,7 +171,7 @@ extension SwiftfinStore.Errors: LocalizedError {
|
|||
return "Existing User"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .existingServer(let server):
|
||||
|
|
|
@ -11,9 +11,9 @@ import Defaults
|
|||
import Foundation
|
||||
|
||||
extension SwiftfinStore {
|
||||
|
||||
|
||||
enum Defaults {
|
||||
|
||||
|
||||
static let suite: UserDefaults = {
|
||||
return UserDefaults(suiteName: "swiftfinstore-defaults")!
|
||||
}()
|
||||
|
@ -22,7 +22,7 @@ extension SwiftfinStore {
|
|||
|
||||
extension Defaults.Keys {
|
||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.suite)
|
||||
|
||||
|
||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite)
|
||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
import SwiftUI
|
||||
|
||||
final class BasicAppSettingsViewModel: ViewModel {
|
||||
|
||||
|
||||
let appearances = AppAppearance.allCases
|
||||
|
||||
|
||||
func reset() {
|
||||
SessionManager.main.purge()
|
||||
}
|
||||
|
|
|
@ -13,12 +13,12 @@ import JellyfinAPI
|
|||
import Stinsen
|
||||
|
||||
final class ConnectToServerViewModel: ViewModel {
|
||||
|
||||
|
||||
@RouterObject var router: ConnectToServerCoodinator.Router?
|
||||
@Published var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
|
||||
@Published var searching = false
|
||||
private let discovery = ServerDiscovery()
|
||||
|
||||
|
||||
var alertTitle: String {
|
||||
var message: String = ""
|
||||
if errorMessage?.code != ErrorMessage.noShowErrorCode {
|
||||
|
@ -64,12 +64,12 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cancelConnection() {
|
||||
for cancellable in cancellables {
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,9 @@ final class HomeViewModel: ViewModel {
|
|||
self.handleAPIRequestError(completion: completion)
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
|
||||
|
||||
var newLibraries: [BaseItemDto] = []
|
||||
|
||||
|
||||
response.items!.forEach { item in
|
||||
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
|
||||
if item.collectionType == "movies" || item.collectionType == "tvshows" {
|
||||
|
@ -60,13 +60,13 @@ final class HomeViewModel: ViewModel {
|
|||
}
|
||||
}, receiveValue: { response in
|
||||
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : []
|
||||
|
||||
|
||||
for excludeID in excludeIDs {
|
||||
newLibraries.removeAll { library in
|
||||
return library.id == excludeID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.libraries = newLibraries
|
||||
})
|
||||
.store(in: &self.cancellables)
|
||||
|
@ -88,7 +88,7 @@ final class HomeViewModel: ViewModel {
|
|||
}
|
||||
}, receiveValue: { response in
|
||||
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
|
||||
|
||||
|
||||
self.resumeItems = response.items ?? []
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
@ -105,7 +105,7 @@ final class HomeViewModel: ViewModel {
|
|||
}
|
||||
}, receiveValue: { response in
|
||||
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
|
||||
|
||||
|
||||
self.nextUpItems = response.items ?? []
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
|
|
@ -11,7 +11,7 @@ import Foundation
|
|||
import JellyfinAPI
|
||||
|
||||
class ItemViewModel: ViewModel {
|
||||
|
||||
|
||||
@Published var item: BaseItemDto
|
||||
@Published var playButtonItem: BaseItemDto?
|
||||
@Published var similarItems: [BaseItemDto] = []
|
||||
|
@ -20,32 +20,32 @@ class ItemViewModel: ViewModel {
|
|||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
|
||||
|
||||
switch item.itemType {
|
||||
case .episode, .movie:
|
||||
self.playButtonItem = item
|
||||
default: ()
|
||||
}
|
||||
|
||||
|
||||
isFavorited = item.userData?.isFavorite ?? false
|
||||
isWatched = item.userData?.played ?? false
|
||||
super.init()
|
||||
|
||||
getSimilarItems()
|
||||
}
|
||||
|
||||
|
||||
func playButtonText() -> String {
|
||||
return item.getItemProgressString() == "" ? "Play" : item.getItemProgressString()
|
||||
return item.getItemProgressString() == "" ? L10n.play : item.getItemProgressString()
|
||||
}
|
||||
|
||||
|
||||
func getItemDisplayName() -> String {
|
||||
return item.name ?? ""
|
||||
}
|
||||
|
||||
|
||||
func shouldDisplayRuntime() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func getSimilarItems() {
|
||||
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||
.trackActivity(loading)
|
||||
|
|
|
@ -36,7 +36,7 @@ final class LibraryViewModel: ViewModel {
|
|||
|
||||
// temp
|
||||
@Published var filters: LibraryFilters
|
||||
|
||||
|
||||
private let columns: Int
|
||||
private var libraries = [BaseItemDto]()
|
||||
|
||||
|
@ -64,11 +64,10 @@ final class LibraryViewModel: ViewModel {
|
|||
self.columns = columns
|
||||
super.init()
|
||||
|
||||
|
||||
$filters
|
||||
.sink(receiveValue: requestItems(with:))
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
}
|
||||
|
||||
func requestItems(with filters: LibraryFilters) {
|
||||
|
@ -147,7 +146,7 @@ final class LibraryViewModel: ViewModel {
|
|||
currentPage -= 1
|
||||
requestItems(with: filters)
|
||||
}
|
||||
|
||||
|
||||
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
|
||||
guard itemList.count > 0 else { return [] }
|
||||
let rowCount = itemList.count / columns
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue