[tvOS] Settings Cleanup (#1163)

* Settings Cleanup. Replace strings with labels. Enforce the same font. Ensure Forms don't get clipped by their boundries. Create consistent, reusable button sizing/coloring. Apply to all Settings Pages.

* Remove custom Button/Form styling in exchange for just using .scrollClipDisabled()

* Swap back to Jellyfin Purple from Purple.

* Remove Check Button. Check all Section Inits where possible. Make Server Details Server non-focusable.

Create a new menu for Server Details selection. This is a WIP awaiting feedback from https://github.com/jellyfin/Swiftfin/pull/1163#discussion_r1705957885

---------

Co-authored-by: Joseph Kribs <joseph@kribs.net>
This commit is contained in:
Joe 2024-08-06 20:56:24 -06:00 committed by GitHub
parent 0ae1324534
commit e4fd98c244
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 147 additions and 90 deletions

View File

@ -19,6 +19,7 @@ struct TextPairView: View {
var body: some View {
HStack {
Text(leading)
.foregroundColor(.primary)
Spacer()

View File

@ -18,6 +18,8 @@ internal enum L10n {
internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.")
/// Accessibility
internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility")
/// Add Server
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
/// Add URL
internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL")
/// Advanced
@ -30,6 +32,8 @@ internal enum L10n {
internal static let allGenres = L10n.tr("Localizable", "allGenres", fallback: "All Genres")
/// All Media
internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media")
/// All Servers
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers")
/// Appearance
internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance")
/// App Icon
@ -112,6 +116,8 @@ internal enum L10n {
internal static let chapterSlider = L10n.tr("Localizable", "chapterSlider", fallback: "Chapter Slider")
/// Cinematic
internal static let cinematic = L10n.tr("Localizable", "cinematic", fallback: "Cinematic")
/// Cinematic Background
internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background")
/// Cinematic Views
internal static let cinematicViews = L10n.tr("Localizable", "cinematicViews", fallback: "Cinematic Views")
/// Close
@ -160,6 +166,10 @@ internal enum L10n {
internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark")
/// Default Scheme
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme")
/// Delete
internal static let delete = L10n.tr("Localizable", "delete", fallback: "Delete")
/// Delete Server
internal static let deleteServer = L10n.tr("Localizable", "deleteServer", fallback: "Delete Server")
/// Delivery
internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery")
/// DIRECTOR
@ -176,6 +186,8 @@ internal enum L10n {
internal static let downloads = L10n.tr("Localizable", "downloads", fallback: "Downloads")
/// Edit Jump Lengths
internal static let editJumpLengths = L10n.tr("Localizable", "editJumpLengths", fallback: "Edit Jump Lengths")
/// Edit Server
internal static let editServer = L10n.tr("Localizable", "editServer", fallback: "Edit Server")
/// Empty Next Up
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up")
/// Enabled
@ -228,6 +240,8 @@ internal enum L10n {
internal static let invertedLight = L10n.tr("Localizable", "invertedLight", fallback: "Inverted Light")
/// Items
internal static let items = L10n.tr("Localizable", "items", fallback: "Items")
/// Jellyfin
internal static let jellyfin = L10n.tr("Localizable", "jellyfin", fallback: "Jellyfin")
/// Jump
internal static let jump = L10n.tr("Localizable", "jump", fallback: "Jump")
/// Jump Backward
@ -338,6 +352,8 @@ internal enum L10n {
}
/// No title
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
/// Offset
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
/// Ok
internal static let ok = L10n.tr("Localizable", "ok", fallback: "Ok")
/// 1 user
@ -380,6 +396,8 @@ internal enum L10n {
internal static let playback = L10n.tr("Localizable", "playback", fallback: "Playback")
/// Playback Buttons
internal static let playbackButtons = L10n.tr("Localizable", "playbackButtons", fallback: "Playback Buttons")
/// Playback Quality
internal static let playbackQuality = L10n.tr("Localizable", "playbackQuality", fallback: "Playback Quality")
/// Playback settings
internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings", fallback: "Playback settings")
/// Playback Speed
@ -474,12 +492,16 @@ internal enum L10n {
internal static let resetAppSettings = L10n.tr("Localizable", "resetAppSettings", fallback: "Reset App Settings")
/// Reset User Settings
internal static let resetUserSettings = L10n.tr("Localizable", "resetUserSettings", fallback: "Reset User Settings")
/// Resume
internal static let resume = L10n.tr("Localizable", "resume", fallback: "Resume")
/// Resume 5 Second Offset
internal static let resume5SecondOffset = L10n.tr("Localizable", "resume5SecondOffset", fallback: "Resume 5 Second Offset")
/// Resume Offset
internal static let resumeOffset = L10n.tr("Localizable", "resumeOffset", fallback: "Resume Offset")
/// Resume content seconds before the recorded resume time
internal static let resumeOffsetDescription = L10n.tr("Localizable", "resumeOffsetDescription", fallback: "Resume content seconds before the recorded resume time")
/// Resume Offset
internal static let resumeOffsetTitle = L10n.tr("Localizable", "resumeOffsetTitle", fallback: "Resume Offset")
/// Retrieving media information
internal static let retrievingMediaInformation = L10n.tr("Localizable", "retrievingMediaInformation", fallback: "Retrieving media information")
/// Retry
@ -540,6 +562,10 @@ internal enum L10n {
internal static let showCastAndCrew = L10n.tr("Localizable", "showCastAndCrew", fallback: "Show Cast & Crew")
/// Show Chapters Info In Bottom Overlay
internal static let showChaptersInfoInBottomOverlay = L10n.tr("Localizable", "showChaptersInfoInBottomOverlay", fallback: "Show Chapters Info In Bottom Overlay")
/// Show Favorited
internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited")
/// Show Favorites
internal static let showFavorites = L10n.tr("Localizable", "showFavorites", fallback: "Show Favorites")
/// Flatten Library Items
internal static let showFlattenView = L10n.tr("Localizable", "showFlattenView", fallback: "Flatten Library Items")
/// Show Missing Episodes
@ -548,6 +574,14 @@ internal enum L10n {
internal static let showMissingSeasons = L10n.tr("Localizable", "showMissingSeasons", fallback: "Show Missing Seasons")
/// Show Poster Labels
internal static let showPosterLabels = L10n.tr("Localizable", "showPosterLabels", fallback: "Show Poster Labels")
/// Show Progress
internal static let showProgress = L10n.tr("Localizable", "showProgress", fallback: "Show Progress")
/// Show Recently Added
internal static let showRecentlyAdded = L10n.tr("Localizable", "showRecentlyAdded", fallback: "Show Recently Added")
/// Show Unwatched
internal static let showUnwatched = L10n.tr("Localizable", "showUnwatched", fallback: "Show Unwatched")
/// Show Watched
internal static let showWatched = L10n.tr("Localizable", "showWatched", fallback: "Show Watched")
/// Signed in as %@
internal static func signedInAsWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1), fallback: "Signed in as %@")
@ -594,6 +628,8 @@ internal enum L10n {
internal static let subtitleOffset = L10n.tr("Localizable", "subtitleOffset", fallback: "Subtitle Offset")
/// Subtitles
internal static let subtitles = L10n.tr("Localizable", "subtitles", fallback: "Subtitles")
/// Settings only affect some subtitle types
internal static let subtitlesDisclaimer = L10n.tr("Localizable", "subtitlesDisclaimer", fallback: "Settings only affect some subtitle types")
/// Subtitle Size
internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: "Subtitle Size")
/// Suggestions

View File

@ -31,6 +31,7 @@ struct SplitFormWindowView: View {
.if(descriptionTopPadding) { view in
view.padding(.top)
}
.scrollClipDisabled()
}
}
}

View File

@ -39,7 +39,7 @@ struct SelectServerView: View {
}
var body: some View {
FullScreenMenu("Servers") {
FullScreenMenu(L10n.servers) {
Section {
Button {
router.popLast {
@ -47,7 +47,7 @@ struct SelectServerView: View {
}
} label: {
HStack {
Text("Add Server")
L10n.addServer.text
Spacer()
@ -62,7 +62,7 @@ struct SelectServerView: View {
}
} label: {
HStack {
Text("Edit Server")
L10n.editServer.text
Spacer()
@ -80,7 +80,7 @@ struct SelectServerView: View {
router.popLast()
} label: {
HStack {
Text("All Servers")
L10n.allServers.text
Spacer()
@ -116,9 +116,10 @@ struct SelectServerView: View {
.padding()
}
.buttonStyle(.card)
.padding(.horizontal)
}
} header: {
Text("Servers")
Text(L10n.servers)
}
.headerProminence(.increased)
}

View File

@ -35,36 +35,61 @@ struct EditServerView: View {
.frame(maxWidth: 400)
}
.contentView {
Section(L10n.server) {
TextPairView(
leading: L10n.name,
trailing: viewModel.server.name
)
.focusable(false)
}
Section("URL") {
ForEach(viewModel.server.urls.sorted(using: \.absoluteString)) { url in
if url == viewModel.server.currentURL {
Button(url.absoluteString, systemImage: "checkmark") {}
} else {
Button(url.absoluteString) {
Section(L10n.url) {
Menu {
ForEach(viewModel.server.urls.sorted(using: \.absoluteString), id: \.self) { url in
Button(action: {
guard viewModel.server.currentURL != url else { return }
viewModel.setCurrentURL(to: url)
}) {
HStack {
Text(url.absoluteString)
.foregroundColor(.primary)
Spacer()
if viewModel.server.currentURL == url {
Image(systemName: "checkmark")
.font(.body.weight(.regular))
.foregroundColor(.secondary)
}
}
}
}
} label: {
HStack {
Text(viewModel.server.currentURL.absoluteString)
.foregroundColor(.primary)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.secondary)
}
}
}
if isEditing {
ListRowButton("Delete") {
isPresentingConfirmDeletion = true
Section {
ListRowButton(L10n.delete) {
isPresentingConfirmDeletion = true
}
.foregroundStyle(.primary, .red.opacity(0.5))
}
.foregroundStyle(.primary, .red.opacity(0.5))
}
}
.withDescriptionTopPadding()
.navigationTitle(L10n.server)
.alert("Delete Server", isPresented: $isPresentingConfirmDeletion) {
Button("Delete", role: .destructive) {
.alert(L10n.deleteServer, isPresented: $isPresentingConfirmDeletion) {
Button(L10n.delete, role: .destructive) {
viewModel.delete()
router.popLast()
}

View File

@ -53,18 +53,16 @@ struct CustomizeViewsSettings: View {
}
.contentView {
Section {
Section(L10n.missingItems) {
Toggle(L10n.showMissingSeasons, isOn: $shouldShowMissingSeasons)
Toggle(L10n.showMissingEpisodes, isOn: $shouldShowMissingEpisodes)
} header: {
L10n.missingItems.text
}
Section {
Section(L10n.posters) {
ChevronButton("Indicators")
ChevronButton(L10n.indicators)
.onSelect {
router.route(to: \.indicatorSettings)
}
@ -82,23 +80,17 @@ struct CustomizeViewsSettings: View {
InlineEnumToggle(title: L10n.search, selection: $searchPosterType)
InlineEnumToggle(title: L10n.library, selection: $libraryViewType)
} header: {
Text("Posters")
}
Section {
Section(L10n.library) {
Toggle("Cinematic Background", isOn: $cinematicBackground)
Toggle(L10n.cinematicBackground, isOn: $cinematicBackground)
Toggle("Random Image", isOn: $libraryRandomImage)
Toggle(L10n.randomImage, isOn: $libraryRandomImage)
Toggle("Show Favorites", isOn: $showFavorites)
Toggle(L10n.showFavorites, isOn: $showFavorites)
Toggle("Show Recently Added", isOn: $showRecentlyAdded)
} header: {
L10n.library.text
Toggle(L10n.showRecentlyAdded, isOn: $showRecentlyAdded)
}
}
.withDescriptionTopPadding()

View File

@ -25,20 +25,15 @@ struct ExperimentalSettingsView: View {
.frame(maxWidth: 400)
}
.contentView {
Section {
Section("Video Player") {
Toggle("Force Direct Play", isOn: $forceDirectPlay)
} header: {
Text("Video Player")
}
Section {
Section("Live TV") {
Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay)
} header: {
Text("Live TV")
}
}
.navigationTitle(L10n.experimental)

View File

@ -32,18 +32,18 @@ struct IndicatorSettingsView: View {
}
.contentView {
Section {
Section(L10n.posters) {
Toggle("Show Favorited", isOn: $showFavorited)
Toggle(L10n.showFavorited, isOn: $showFavorited)
Toggle("Show Progress", isOn: $showProgress)
Toggle(L10n.showProgress, isOn: $showProgress)
Toggle("Show Unwatched", isOn: $showUnwatched)
Toggle(L10n.showUnwatched, isOn: $showUnwatched)
Toggle("Show Watched", isOn: $showWatched)
Toggle(L10n.showWatched, isOn: $showWatched)
}
}
.withDescriptionTopPadding()
.navigationTitle("Indicators")
.navigationTitle(L10n.indicators)
}
}

View File

@ -24,21 +24,19 @@ struct MaximumBitrateSettingsView: View {
.frame(maxWidth: 400)
}
.contentView {
Section {
CaseIterablePicker(
L10n.maximumBitrate,
selection: $appMaximumBitrate
)
InlineEnumToggle(title: L10n.maximumBitrate, selection: $appMaximumBitrate)
if appMaximumBitrate == PlaybackBitrate.auto {
CaseIterablePicker(
L10n.testSize,
selection: $appMaximumBitrateTest
)
InlineEnumToggle(title: L10n.testSize, selection: $appMaximumBitrateTest)
}
} header: {
L10n.playbackQuality.text
} footer: {
if appMaximumBitrate == PlaybackBitrate.auto {
Text(L10n.bitrateTestDescription)
L10n.bitrateTestDescription.text
}
}
}

View File

@ -31,7 +31,7 @@ struct SettingsView: View {
.frame(maxWidth: 400)
}
.contentView {
Section {
Section(L10n.jellyfin) {
Button {} label: {
TextPairView(
@ -51,14 +51,23 @@ struct SettingsView: View {
Button {
viewModel.signOut()
} label: {
L10n.switchUser.text
.foregroundColor(.jellyfinPurple)
HStack {
Text(L10n.switchUser)
.foregroundColor(.jellyfinPurple)
Spacer()
Image(systemName: "chevron.right")
.font(.body.weight(.regular))
.foregroundColor(.secondary)
}
}
}
Section {
Section(L10n.videoPlayer) {
InlineEnumToggle(title: "Video Player Type", selection: $videoPlayerType)
InlineEnumToggle(title: L10n.videoPlayerType, selection: $videoPlayerType)
ChevronButton(L10n.videoPlayer)
.onSelect {
@ -69,12 +78,9 @@ struct SettingsView: View {
.onSelect {
router.route(to: \.maximumBitrateSettings)
}
} header: {
L10n.videoPlayer.text
}
Section {
Section(L10n.accessibility) {
ChevronButton(L10n.customize)
.onSelect {
@ -85,14 +91,11 @@ struct SettingsView: View {
.onSelect {
router.route(to: \.experimentalSettings)
}
} header: {
L10n.accessibility.text
}
Section {
ChevronButton("Logs")
ChevronButton(L10n.logs)
.onSelect {
router.route(to: \.log)
}

View File

@ -43,46 +43,51 @@ struct VideoPlayerSettingsView: View {
.contentView {
Section {
ChevronButton(
"Resume Offset",
L10n.offset,
subtitle: resumeOffset.secondLabel
)
.onSelect {
isPresentingResumeOffsetStepper = true
}
} header: {
L10n.resume.text
} footer: {
Text("Resume content seconds before the recorded resume time")
L10n.resumeOffsetDescription.text
}
Section {
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName)
.onSelect {
router.route(to: \.fontPicker, $subtitleFontName)
}
} header: {
L10n.subtitles.text
} footer: {
Text("Settings only affect some subtitle types")
L10n.subtitlesDisclaimer.text
}
Section {
Toggle("Pause on background", isOn: $pauseOnBackground)
Toggle("Play on active", isOn: $playOnActive)
Section(L10n.playback) {
Toggle(L10n.pauseOnBackground, isOn: $pauseOnBackground)
Toggle(L10n.playOnActive, isOn: $playOnActive)
}
}
.navigationTitle("Video Player")
.blurFullScreenCover(isPresented: $isPresentingResumeOffsetStepper) {
StepperView(
title: "Resume Offset",
description: "Resume content seconds before the recorded resume time",
value: $resumeOffset,
range: 0 ... 30,
step: 1
)
.valueFormatter {
$0.secondLabel
}
.onCloseSelected {
isPresentingResumeOffsetStepper = false
.navigationTitle(L10n.videoPlayer.text)
.blurFullScreenCover(isPresented: $isPresentingResumeOffsetStepper) {
StepperView(
title: L10n.resumeOffsetTitle,
description: L10n.resumeOffsetDescription,
value: $resumeOffset,
range: 0 ... 30,
step: 1
)
.valueFormatter {
$0.secondLabel
}
.onCloseSelected {
isPresentingResumeOffsetStepper = false
}
}
}
}