[tvOS] App Settings & Splashscreen (#1419)

* Bring over iOS app settings

* Re-enable advanced settings menu option

* Conditionally show splash screen

* Disable app appearance setting

* cleanup

* File rename

* Change how version is displayed

* Disable app icon option due to not working

* comment

* Bring over signout interval section

* Enforce sign-out on close

* Revert change

* localizations

* wip

---------

Co-authored-by: chickdan <=>
Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Daniel Chick 2025-02-06 21:59:18 -06:00 committed by GitHub
parent c388ca2dec
commit 07c895ddba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 286 additions and 133 deletions

View File

@ -516,6 +516,8 @@ internal enum L10n {
internal static func duplicateUserSaved(_ p1: Any) -> String {
return L10n.tr("Localizable", "duplicateUserSaved", String(describing: p1), fallback: "%@ is already saved")
}
/// Duration
internal static let duration = L10n.tr("Localizable", "duration", fallback: "Duration")
/// DVD
internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD")
/// Edit
@ -1202,6 +1204,14 @@ internal enum L10n {
internal static func signInToServer(_ p1: UnsafePointer<CChar>) -> String {
return L10n.tr("Localizable", "signInToServer", p1, fallback: "Sign In to %s")
}
/// Sign out on background
internal static let signoutBackground = L10n.tr("Localizable", "signoutBackground", fallback: "Sign out on background")
/// Signs out the last user when Swiftfin has been in the background without media playback after some time
internal static let signoutBackgroundFooter = L10n.tr("Localizable", "signoutBackgroundFooter", fallback: "Signs out the last user when Swiftfin has been in the background without media playback after some time")
/// Sign out on close
internal static let signoutClose = L10n.tr("Localizable", "signoutClose", fallback: "Sign out on close")
/// Signs out the last user when Swiftfin has been force closed
internal static let signoutCloseFooter = L10n.tr("Localizable", "signoutCloseFooter", fallback: "Signs out the last user when Swiftfin has been force closed")
/// Slider
internal static let slider = L10n.tr("Localizable", "slider", fallback: "Slider")
/// Slider Color
@ -1222,6 +1232,10 @@ internal enum L10n {
internal static let sourceCode = L10n.tr("Localizable", "sourceCode", fallback: "Source Code")
/// Special Features
internal static let specialFeatures = L10n.tr("Localizable", "specialFeatures", fallback: "Special Features")
/// Splashscreen
internal static let splashscreen = L10n.tr("Localizable", "splashscreen", fallback: "Splashscreen")
/// When All Servers is selected, use the splashscreen from a single server or a random server
internal static let splashscreenFooter = L10n.tr("Localizable", "splashscreenFooter", fallback: "When All Servers is selected, use the splashscreen from a single server or a random server")
/// Sports
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
/// Start Time
@ -1402,6 +1416,8 @@ internal enum L10n {
}
/// Users
internal static let users = L10n.tr("Localizable", "users", fallback: "Users")
/// Use splashscreen
internal static let useSplashscreen = L10n.tr("Localizable", "useSplashscreen", fallback: "Use splashscreen")
/// Version
internal static let version = L10n.tr("Localizable", "version", fallback: "Version")
/// Video

View File

@ -26,30 +26,28 @@ final class SettingsViewModel: ViewModel {
override init() {
guard let iconName = UIApplication.shared.alternateIconName else {
if let iconName = UIApplication.shared.alternateIconName {
if let appicon = PrimaryAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = DarkAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = InvertedDarkAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = InvertedLightAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = LightAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
} else {
currentAppIcon = PrimaryAppIcon.primary
super.init()
return
}
if let appicon = PrimaryAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = DarkAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = InvertedDarkAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = InvertedLightAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
if let appicon = LightAppIcon.createCase(iconName: iconName) {
currentAppIcon = appicon
}
super.init()

View File

@ -52,6 +52,11 @@ struct SwiftfinApp: App {
// UIKit
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.label]
// don't keep last user id
if Defaults[.signOutOnClose] {
Defaults[.lastSignedInUserID] = .signedOut
}
}
var body: some Scene {

View File

@ -0,0 +1,100 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import Stinsen
import SwiftUI
struct AppSettingsView: View {
@Default(.selectUserUseSplashscreen)
private var selectUserUseSplashscreen
@Default(.selectUserAllServersSplashscreen)
private var selectUserAllServersSplashscreen
@Default(.appAppearance)
private var appearance
@EnvironmentObject
private var router: AppSettingsCoordinator.Router
@StateObject
private var viewModel = SettingsViewModel()
@State
private var resetUserSettingsSelected: Bool = false
@State
private var removeAllServersSelected: Bool = false
var body: some View {
SplitFormWindowView()
.descriptionView {
Image(.jellyfinBlobBlue)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400)
}
.contentView {
TextPairView(
leading: L10n.version,
trailing: "\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))"
)
Section {
Toggle(L10n.useSplashscreen, isOn: $selectUserUseSplashscreen)
if selectUserUseSplashscreen {
Menu {
Picker(L10n.servers, selection: $selectUserAllServersSplashscreen) {
Label(L10n.random, systemImage: "dice.fill")
.tag(SelectUserServerSelection.all)
ForEach(viewModel.servers) { server in
Text(server.name)
.tag(SelectUserServerSelection.server(id: server.id))
}
}
} label: {
HStack {
Text(L10n.servers)
.frame(maxWidth: .infinity, alignment: .leading)
if selectUserAllServersSplashscreen == .all {
Label(L10n.random, systemImage: "dice.fill")
} else if let server = viewModel.servers.first(
where: { server in
selectUserAllServersSplashscreen == .server(id: server.id)
}
) {
Text(server.name)
}
}
}
.listRowBackground(Color.clear)
.listRowInsets(.zero)
}
} header: {
Text(L10n.splashscreen)
} footer: {
if selectUserUseSplashscreen {
Text(L10n.splashscreenFooter)
}
}
SignOutIntervalSection()
ChevronButton(L10n.logs)
.onSelect {
router.route(to: \.log)
}
}
}
}

View File

@ -0,0 +1,72 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
extension AppSettingsView {
struct SignOutIntervalSection: View {
@Default(.backgroundSignOutInterval)
private var backgroundSignOutInterval
@Default(.signOutOnBackground)
private var signOutOnBackground
@Default(.signOutOnClose)
private var signOutOnClose
@State
private var isEditingBackgroundSignOutInterval: Bool = false
var body: some View {
Section {
Toggle(L10n.signoutClose, isOn: $signOutOnClose)
} footer: {
Text(L10n.signoutCloseFooter)
}
// TODO: need to consider date picker options to re-enable
// Section {
// Toggle(L10n.signoutBackground, isOn: $signOutOnBackground)
//
// if signOutOnBackground {
// HStack {
// Text(L10n.duration)
//
// Spacer()
//
// Button {
// isEditingBackgroundSignOutInterval.toggle()
// } label: {
// HStack {
// Text(backgroundSignOutInterval, format: .hourMinute)
// .foregroundStyle(.secondary)
//
// Image(systemName: "chevron.right")
// .font(.body.weight(.semibold))
// .foregroundStyle(.secondary)
// .rotationEffect(isEditingBackgroundSignOutInterval ? .degrees(90) : .zero)
// .animation(.linear(duration: 0.075), value: isEditingBackgroundSignOutInterval)
// }
// }
// .foregroundStyle(.primary, .secondary)
// }
//
// if isEditingBackgroundSignOutInterval {
// HourMinutePicker(interval: $backgroundSignOutInterval)
// }
// }
// } footer: {
// Text(
// L10n.signoutBackgroundFooter
// )
// }
// .animation(.linear(duration: 0.15), value: isEditingBackgroundSignOutInterval)
}
}
}

View File

@ -1,90 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import Stinsen
import SwiftUI
#warning("TODO: implement")
struct AppSettingsView: View {
var body: some View {
Text("TODO")
}
}
// struct BasicAppSettingsView: View {
//
// @EnvironmentObject
// private var router: BasicAppSettingsCoordinator.Router
//
// @ObservedObject
// var viewModel: SettingsViewModel
//
// @State
// private var resetUserSettingsSelected: Bool = false
// @State
// private var removeAllServersSelected: Bool = false
//
// var body: some View {
// SplitFormWindowView()
// .descriptionView {
// Image(.jellyfinBlobBlue)
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(maxWidth: 400)
// }
// .contentView {
//
// Section {
//
// Button {
// TextPairView(
// leading: L10n.version,
// trailing: "\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))"
// )
// }
//
// ChevronButton(L10n.logs)
// .onSelect {
// router.route(to: \.log)
// }
// }
//
// Section {
//
// Button {
// resetUserSettingsSelected = true
// } label: {
// L10n.resetUserSettings.text
// }
//
// Button {
// removeAllServersSelected = true
// } label: {
// Text(L10n.removeAllServers)
// }
// }
// }
// .withDescriptionTopPadding()
// .navigationTitle(L10n.settings)
// .alert(L10n.resetUserSettings, isPresented: $resetUserSettingsSelected) {
// Button(L10n.reset, role: .destructive) {
//// viewModel.resetUserSettings()
// }
// } message: {
// Text(L10n.resetAllSettings)
// }
// .alert(L10n.removeAllServers, isPresented: $removeAllServersSelected) {
// Button(L10n.reset, role: .destructive) {
//// viewModel.removeAllServers()
// }
// }
// }
// }

View File

@ -12,6 +12,11 @@ extension SelectUserView {
struct SelectUserBottomBar: View {
// MARK: - State & Environment Objects
@EnvironmentObject
private var router: SelectUserCoordinator.Router
@Binding
private var isEditing: Bool
@ -21,6 +26,8 @@ extension SelectUserView {
@ObservedObject
private var viewModel: SelectUserViewModel
// MARK: - Variables
private let areUsersSelected: Bool
private let userCount: Int
@ -35,13 +42,12 @@ extension SelectUserView {
Button(L10n.editUsers, systemImage: "person.crop.circle") {
isEditing.toggle()
}
// TODO: Advanced settings on tvOS?
//
// Divider()
//
// Button(L10n.advanced, systemImage: "gearshape.fill") {
// router.route(to: \.advancedSettings)
// }
Divider()
Button(L10n.advanced, systemImage: "gearshape.fill") {
router.route(to: \.advancedSettings)
}
} label: {
Label(L10n.advanced, systemImage: "gearshape.fill")
.font(.body.weight(.semibold))

View File

@ -28,6 +28,8 @@ struct SelectUserView: View {
@Default(.selectUserServerSelection)
private var serverSelection
@Default(.selectUserUseSplashscreen)
private var selectUserUseSplashscreen
// MARK: - Environment Variable
@ -286,7 +288,7 @@ struct SelectUserView: View {
}
.animation(.linear(duration: 0.1), value: scrollViewOffset)
.background {
if let splashScreenImageSource {
if let splashScreenImageSource, selectUserUseSplashscreen {
ZStack {
ImageView(splashScreenImageSource)
.aspectRatio(contentMode: .fill)

View File

@ -396,6 +396,7 @@
BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */; };
BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577D2C1140810078CEF8 /* TransitionSection.swift */; };
BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */; };
BDF8BB6C2D5456B400628ADB /* SignOutIntervalSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF8BB6B2D5456B400628ADB /* SignOutIntervalSection.swift */; };
BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */; };
BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */; };
BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */; };
@ -1067,7 +1068,7 @@
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
E1D4BF8A2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* AppSettingsCoordinator.swift */; };
E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* AppSettingsCoordinator.swift */; };
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; };
E1D4BF8F271A079A00A11E64 /* AppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* AppSettingsView.swift */; };
E1D5C39628DF90C100CDBEFB /* Slider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D5C39528DF90C100CDBEFB /* Slider.swift */; };
E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D5C39828DF914700CDBEFB /* CapsuleSlider.swift */; };
E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D5C39A28DF993400CDBEFB /* ThumbSlider.swift */; };
@ -1533,6 +1534,7 @@
BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampSection.swift; sourceTree = "<group>"; };
BD39577D2C1140810078CEF8 /* TransitionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionSection.swift; sourceTree = "<group>"; };
BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserBottomBar.swift; sourceTree = "<group>"; };
BDF8BB6B2D5456B400628ADB /* SignOutIntervalSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutIntervalSection.swift; sourceTree = "<group>"; };
BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityView.swift; sourceTree = "<group>"; };
BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsView.swift; sourceTree = "<group>"; };
C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = "<group>"; };
@ -1944,7 +1946,7 @@
E1D4BF7B2719D05000A11E64 /* AppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsView.swift; sourceTree = "<group>"; };
E1D4BF802719D22800A11E64 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = "<group>"; };
E1D4BF892719D3D000A11E64 /* AppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsCoordinator.swift; sourceTree = "<group>"; };
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
E1D4BF8E271A079A00A11E64 /* AppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsView.swift; sourceTree = "<group>"; };
E1D5C39528DF90C100CDBEFB /* Slider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Slider.swift; sourceTree = "<group>"; };
E1D5C39828DF914700CDBEFB /* CapsuleSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleSlider.swift; sourceTree = "<group>"; };
E1D5C39A28DF993400CDBEFB /* ThumbSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbSlider.swift; sourceTree = "<group>"; };
@ -3624,6 +3626,23 @@
path = Sections;
sourceTree = "<group>";
};
BDF8BB692D54566400628ADB /* AppSettingsView */ = {
isa = PBXGroup;
children = (
BDF8BB6A2D5456AA00628ADB /* Components */,
E1D4BF8E271A079A00A11E64 /* AppSettingsView.swift */,
);
path = AppSettingsView;
sourceTree = "<group>";
};
BDF8BB6A2D5456AA00628ADB /* Components */ = {
isa = PBXGroup;
children = (
BDF8BB6B2D5456B400628ADB /* SignOutIntervalSection.swift */,
);
path = Components;
sourceTree = "<group>";
};
BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */ = {
isa = PBXGroup;
children = (
@ -3997,8 +4016,8 @@
E12186E02718F23B0010884C /* Views */ = {
isa = PBXGroup;
children = (
BDF8BB692D54566400628ADB /* AppSettingsView */,
E1763A752BF3FF01004DF6AB /* AppLoadingView.swift */,
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */,
E10231522BCF8AF8009D71FC /* ChannelLibraryView */,
4E4DAC3A2D11F54300E13FF9 /* ConnectToServerView */,
E154967B296CBB1A00C4EF88 /* FontPickerView.swift */,
@ -5668,6 +5687,7 @@
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */,
E11BDF7B2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */,
BDF8BB6C2D5456B400628ADB /* SignOutIntervalSection.swift in Sources */,
BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */,
4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */,
E1575E8C293E7B1E001665B1 /* UIScreen.swift in Sources */,
@ -5784,7 +5804,7 @@
E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */,
E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */,
BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */,
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
E1D4BF8F271A079A00A11E64 /* AppSettingsView.swift in Sources */,
E1575E9A293E7B1E001665B1 /* Array.swift in Sources */,
E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */,
E187A60329AB28F0008387E6 /* RotateContentView.swift in Sources */,

View File

@ -59,7 +59,7 @@ struct AppSettingsView: View {
Section {
Toggle("Use splashscreen", isOn: $selectUserUseSplashscreen)
Toggle(L10n.useSplashscreen, isOn: $selectUserUseSplashscreen)
if selectUserUseSplashscreen {
Picker(L10n.servers, selection: $selectUserAllServersSplashscreen) {
@ -76,10 +76,10 @@ struct AppSettingsView: View {
}
}
} header: {
Text("Splashscreen")
Text(L10n.splashscreen)
} footer: {
if selectUserUseSplashscreen {
Text("When All Servers is selected, use the splashscreen from a single server or a random server")
Text(L10n.splashscreenFooter)
}
}

View File

@ -25,17 +25,17 @@ extension AppSettingsView {
var body: some View {
Section {
Toggle("Sign out on close", isOn: $signOutOnClose)
Toggle(L10n.signoutClose, isOn: $signOutOnClose)
} footer: {
Text("Signs out the last user when Swiftfin has been force closed")
Text(L10n.signoutCloseFooter)
}
Section {
Toggle("Sign out on background", isOn: $signOutOnBackground)
Toggle(L10n.signoutBackground, isOn: $signOutOnBackground)
if signOutOnBackground {
HStack {
Text("Duration")
Text(L10n.duration)
Spacer()
@ -62,7 +62,7 @@ extension AppSettingsView {
}
} footer: {
Text(
"Signs out the last user when Swiftfin has been in the background without media playback after some time"
L10n.signoutBackgroundFooter
)
}
.animation(.linear(duration: 0.15), value: isEditingBackgroundSignOutInterval)

View File

@ -727,6 +727,9 @@
/// %@ is already saved
"duplicateUserSaved" = "%@ is already saved";
/// Duration
"duration" = "Duration";
/// DVD
"dvd" = "DVD";
@ -1717,6 +1720,18 @@
/// Sign In to %s
"signInToServer" = "Sign In to %s";
/// Sign out on background
"signoutBackground" = "Sign out on background";
/// Signs out the last user when Swiftfin has been in the background without media playback after some time
"signoutBackgroundFooter" = "Signs out the last user when Swiftfin has been in the background without media playback after some time";
/// Sign out on close
"signoutClose" = "Sign out on close";
/// Signs out the last user when Swiftfin has been force closed
"signoutCloseFooter" = "Signs out the last user when Swiftfin has been force closed";
/// Slider
"slider" = "Slider";
@ -1747,6 +1762,12 @@
/// Special Features
"specialFeatures" = "Special Features";
/// Splashscreen
"splashscreen" = "Splashscreen";
/// When All Servers is selected, use the splashscreen from a single server or a random server
"splashscreenFooter" = "When All Servers is selected, use the splashscreen from a single server or a random server";
/// Sports
"sports" = "Sports";
@ -2011,6 +2032,9 @@
/// Users
"users" = "Users";
/// Use splashscreen
"useSplashscreen" = "Use splashscreen";
/// Version
"version" = "Version";