mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 04:04:07 +00:00
Redesigned settings (fixes #47)
This commit is contained in:
@@ -15,10 +15,10 @@ struct AccountsNavigationLink: View {
|
||||
}
|
||||
|
||||
private func removeInstanceButton(_ instance: Instance) -> some View {
|
||||
if #available(iOS 15.0, *) {
|
||||
return Button("Remove", role: .destructive) { removeAction(instance) }
|
||||
} else {
|
||||
return Button("Remove") { removeAction(instance) }
|
||||
Button {
|
||||
removeAction(instance)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
|
124
Shared/Settings/Help.swift
Normal file
124
Shared/Settings/Help.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Help: View {
|
||||
static let wikiURL = URL(string: "https://github.com/yattee/yattee/wiki")!
|
||||
static let matrixURL = URL(string: "https://tinyurl.com/yattee-matrix")!
|
||||
static let issuesURL = URL(string: "https://github.com/yattee/yattee/issues")!
|
||||
static let milestonesURL = URL(string: "https://github.com/yattee/yattee/milestones")!
|
||||
static let donationsURL = URL(string: "https://github.com/yattee/yattee/wiki/Donations")!
|
||||
static let contributingURL = URL(string: "https://github.com/yattee/yattee/wiki/Contributing")!
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Section {
|
||||
header("I am lost")
|
||||
|
||||
Text("You can find information about using Yattee in the Wiki pages.")
|
||||
.padding(.bottom, 8)
|
||||
|
||||
helpItemLink("Wiki", url: Self.wikiURL, systemImage: "questionmark.circle")
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Section {
|
||||
header("I want to ask a question")
|
||||
|
||||
Text("Discussions take place in Matrix chat channel. It's a good spot for general questions.")
|
||||
.padding(.bottom, 8)
|
||||
|
||||
helpItemLink("Matrix Channel", url: Self.matrixURL, systemImage: "message")
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Section {
|
||||
header("I found a bug /")
|
||||
header("I have a feature request")
|
||||
|
||||
Text("Bugs and great feature ideas can be sent to the GitHub issues tracker. ")
|
||||
Text("If you are reporting a bug, include all relevant details (especially: app\u{00a0}version, used device and system version, steps to reproduce).")
|
||||
Text("If you are interested what's coming in future updates, you can track project Milestones.")
|
||||
.padding(.bottom, 8)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
helpItemLink("Issues Tracker", url: Self.issuesURL, systemImage: "ladybug")
|
||||
helpItemLink("Milestones", url: Self.milestonesURL, systemImage: "list.star")
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Section {
|
||||
header("I like this app!")
|
||||
|
||||
Text("That's nice to hear. It is fun to deliver apps other people want to use. " +
|
||||
"You can consider donating to the project or help by contributing to new features development.")
|
||||
.padding(.bottom, 8)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
helpItemLink("Donations", url: Self.donationsURL, systemImage: "dollarsign.circle")
|
||||
helpItemLink("Contributing", url: Self.contributingURL, systemImage: "hammer")
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#else
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#endif
|
||||
.navigationTitle("Help")
|
||||
}
|
||||
|
||||
func header(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.fontWeight(.bold)
|
||||
.font(.title3)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
|
||||
func helpItemLink(_ label: String, url: URL, systemImage: String) -> some View {
|
||||
Group {
|
||||
#if os(tvOS)
|
||||
VStack {
|
||||
Button {} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: systemImage)
|
||||
Text(label)
|
||||
}
|
||||
.font(.system(size: 25).bold())
|
||||
}
|
||||
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
#else
|
||||
Button {
|
||||
openURL(url)
|
||||
} label: {
|
||||
Label(label, systemImage: systemImage)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Help_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Help()
|
||||
}
|
||||
}
|
@@ -19,31 +19,66 @@ struct HistorySettings: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Section(header: SettingsHeader(text: "History")) {
|
||||
Toggle("Save recent queries and channels", isOn: $saveRecents)
|
||||
Toggle("Save history of played videos", isOn: $saveHistory)
|
||||
Toggle("Show progress of watching on thumbnails", isOn: $showWatchingProgress)
|
||||
.disabled(!saveHistory)
|
||||
#if os(macOS)
|
||||
sections
|
||||
#else
|
||||
List {
|
||||
sections
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
.navigationTitle("History")
|
||||
}
|
||||
|
||||
private var sections: some View {
|
||||
Group {
|
||||
#if os(tvOS)
|
||||
Section(header: SettingsHeader(text: "History")) {
|
||||
Toggle("Save history of searches, channels and playlists", isOn: $saveRecents)
|
||||
Toggle("Save history of played videos", isOn: $saveHistory)
|
||||
Toggle("Show progress of watching on thumbnails", isOn: $showWatchingProgress)
|
||||
.disabled(!saveHistory)
|
||||
|
||||
watchedVideoPlayNowBehaviorPicker
|
||||
|
||||
#if !os(tvOS)
|
||||
watchedThresholdPicker
|
||||
resetWatchedStatusOnPlayingToggle
|
||||
watchedVideoStylePicker
|
||||
watchedVideoBadgeColorPicker
|
||||
}
|
||||
#else
|
||||
Section(header: SettingsHeader(text: "History")) {
|
||||
Toggle("Save history of searches, channels and playlists", isOn: $saveRecents)
|
||||
Toggle("Save history of played videos", isOn: $saveHistory)
|
||||
Toggle("Show progress of watching on thumbnails", isOn: $showWatchingProgress)
|
||||
.disabled(!saveHistory)
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Watched")) {
|
||||
watchedVideoPlayNowBehaviorPicker
|
||||
#if os(macOS)
|
||||
.padding(.top, 1)
|
||||
#endif
|
||||
watchedThresholdPicker
|
||||
resetWatchedStatusOnPlayingToggle
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Interface")) {
|
||||
watchedVideoStylePicker
|
||||
#if os(macOS)
|
||||
.padding(.top, 1)
|
||||
#endif
|
||||
watchedVideoBadgeColorPicker
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
Spacer()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
watchedThresholdPicker
|
||||
watchedVideoStylePicker
|
||||
watchedVideoBadgeColorPicker
|
||||
watchedVideoPlayNowBehaviorPicker
|
||||
resetWatchedStatusOnPlayingToggle
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
Spacer()
|
||||
#endif
|
||||
|
||||
clearHistoryButton
|
||||
@@ -51,7 +86,7 @@ struct HistorySettings: View {
|
||||
}
|
||||
|
||||
private var watchedThresholdPicker: some View {
|
||||
Section(header: header("Mark video as watched after playing")) {
|
||||
Section(header: SettingsHeader(text: "Mark video as watched after playing", secondary: true)) {
|
||||
Picker("Mark video as watched after playing", selection: $watchedThreshold) {
|
||||
ForEach(Self.watchedThresholds, id: \.self) { threshold in
|
||||
Text("\(threshold)%").tag(threshold)
|
||||
@@ -69,7 +104,7 @@ struct HistorySettings: View {
|
||||
}
|
||||
|
||||
private var watchedVideoStylePicker: some View {
|
||||
Section(header: header("Mark watched videos with")) {
|
||||
Section(header: SettingsHeader(text: "Mark watched videos with", secondary: true)) {
|
||||
Picker("Mark watched videos with", selection: $watchedVideoStyle) {
|
||||
Text("Nothing").tag(WatchedVideoStyle.nothing)
|
||||
Text("Badge").tag(WatchedVideoStyle.badge)
|
||||
@@ -88,7 +123,7 @@ struct HistorySettings: View {
|
||||
}
|
||||
|
||||
private var watchedVideoBadgeColorPicker: some View {
|
||||
Section(header: header("Badge color")) {
|
||||
Section(header: SettingsHeader(text: "Badge color", secondary: true)) {
|
||||
Picker("Badge color", selection: $watchedVideoBadgeColor) {
|
||||
Text("Based on system color scheme").tag(WatchedVideoBadgeColor.colorSchemeBased)
|
||||
Text("Blue").tag(WatchedVideoBadgeColor.blue)
|
||||
@@ -96,6 +131,7 @@ struct HistorySettings: View {
|
||||
}
|
||||
.disabled(!saveHistory)
|
||||
.disabled(watchedVideoStyle == .decreasedOpacity)
|
||||
.disabled(watchedVideoStyle == .nothing)
|
||||
.labelsHidden()
|
||||
|
||||
#if os(iOS)
|
||||
@@ -107,7 +143,7 @@ struct HistorySettings: View {
|
||||
}
|
||||
|
||||
private var watchedVideoPlayNowBehaviorPicker: some View {
|
||||
Section(header: header("When partially watched video is played")) {
|
||||
Section(header: SettingsHeader(text: "When partially watched video is played", secondary: true)) {
|
||||
Picker("When partially watched video is played", selection: $watchedVideoPlayNowBehavior) {
|
||||
Text("Continue").tag(WatchedVideoPlayNowBehavior.continue)
|
||||
Text("Restart").tag(WatchedVideoPlayNowBehavior.restart)
|
||||
@@ -125,6 +161,7 @@ struct HistorySettings: View {
|
||||
|
||||
private var resetWatchedStatusOnPlayingToggle: some View {
|
||||
Toggle("Reset watched status when playing again", isOn: $resetWatchedStatusOnPlaying)
|
||||
.disabled(!saveHistory)
|
||||
}
|
||||
|
||||
private var clearHistoryButton: some View {
|
||||
@@ -149,19 +186,6 @@ struct HistorySettings: View {
|
||||
.foregroundColor(.red)
|
||||
.disabled(!saveHistory)
|
||||
}
|
||||
|
||||
private func header(_ text: String) -> some View {
|
||||
#if os(iOS)
|
||||
return EmptyView()
|
||||
#elseif os(macOS)
|
||||
return Text(text)
|
||||
.opacity(saveHistory ? 1 : 0.3)
|
||||
#else
|
||||
return Text(text)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(saveHistory ? 1 : 0.2)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct HistorySettings_Previews: PreviewProvider {
|
||||
|
@@ -14,25 +14,7 @@ struct InstanceSettings: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if instance.app.hasFrontendURL {
|
||||
Section(header: Text("Frontend URL")) {
|
||||
TextField(
|
||||
"Frontend URL",
|
||||
text: $frontendURL
|
||||
)
|
||||
.onAppear {
|
||||
frontendURL = instance.frontendURL ?? ""
|
||||
}
|
||||
.onChange(of: frontendURL) { newValue in
|
||||
InstancesModel.setFrontendURL(instance, newValue)
|
||||
}
|
||||
.labelsHidden()
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Accounts"), footer: sectionFooter) {
|
||||
Section(header: Text("Accounts")) {
|
||||
if instance.app.supportsAccounts {
|
||||
ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in
|
||||
#if os(tvOS)
|
||||
@@ -54,15 +36,21 @@ struct InstanceSettings: View {
|
||||
Spacer()
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Remove") { removeAccount(account) }
|
||||
Button {
|
||||
removeAccount(account)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.redrawOn(change: accountsChanged)
|
||||
|
||||
Button("Add account...") {
|
||||
Button {
|
||||
presentingAccountForm = true
|
||||
} label: {
|
||||
Label("Add Account...", systemImage: "plus")
|
||||
}
|
||||
.sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) {
|
||||
AccountForm(instance: instance)
|
||||
@@ -75,6 +63,23 @@ struct InstanceSettings: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
if instance.app.hasFrontendURL {
|
||||
Section(header: Text("Frontend URL")) {
|
||||
TextField(
|
||||
"Frontend URL",
|
||||
text: $frontendURL
|
||||
)
|
||||
.onAppear {
|
||||
frontendURL = instance.frontendURL ?? ""
|
||||
}
|
||||
.onChange(of: frontendURL) { newValue in
|
||||
InstancesModel.setFrontendURL(instance, newValue)
|
||||
}
|
||||
.labelsHidden()
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
@@ -85,15 +90,6 @@ struct InstanceSettings: View {
|
||||
.navigationTitle(instance.description)
|
||||
}
|
||||
|
||||
private var sectionFooter: some View {
|
||||
if !instance.app.supportsAccounts {
|
||||
return Text("")
|
||||
}
|
||||
|
||||
return Text("Tap and hold to remove account")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func removeAccount(_ account: Account) {
|
||||
AccountsModel.remove(account)
|
||||
accountsChanged.toggle()
|
||||
|
@@ -1,10 +1,16 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PlaybackSettings: View {
|
||||
struct PlayerSettings: View {
|
||||
@Default(.instances) private var instances
|
||||
@Default(.playerInstanceID) private var playerInstanceID
|
||||
@Default(.quality) private var quality
|
||||
@Default(.commentsInstanceID) private var commentsInstanceID
|
||||
|
||||
#if !os(tvOS)
|
||||
@Default(.commentsPlacement) private var commentsPlacement
|
||||
#endif
|
||||
|
||||
@Default(.playerSidebar) private var playerSidebar
|
||||
@Default(.showHistoryInPlayer) private var showHistory
|
||||
@Default(.showKeywords) private var showKeywords
|
||||
@@ -29,66 +35,74 @@ struct PlaybackSettings: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Section(header: SettingsHeader(text: "Player")) {
|
||||
sourcePicker
|
||||
qualityPicker
|
||||
#if os(macOS)
|
||||
sections
|
||||
|
||||
Spacer()
|
||||
#else
|
||||
List {
|
||||
sections
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
.navigationTitle("Player")
|
||||
}
|
||||
|
||||
private var sections: some View {
|
||||
Group {
|
||||
Section(header: SettingsHeader(text: "Playback")) {
|
||||
sourcePicker
|
||||
qualityPicker
|
||||
pauseOnHidingPlayerToggle
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Comments")) {
|
||||
commentsInstancePicker
|
||||
#if !os(tvOS)
|
||||
commentsPlacementPicker
|
||||
.disabled(!CommentsModel.enabled)
|
||||
#endif
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Interface")) {
|
||||
#if os(iOS)
|
||||
if idiom == .pad {
|
||||
sidebarPicker
|
||||
}
|
||||
|
||||
keywordsToggle
|
||||
showHistoryToggle
|
||||
channelSubscribersToggle
|
||||
pauseOnHidingPlayerToggle
|
||||
|
||||
if idiom == .pad {
|
||||
enterFullscreenInLandscapeToggle
|
||||
}
|
||||
|
||||
honorSystemOrientationLockToggle
|
||||
lockLandscapeWhenEnteringFullscreenToggle
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||
closePiPOnNavigationToggle
|
||||
closePiPOnOpeningPlayerToggle
|
||||
closePiPAndOpenPlayerOnEnteringForegroundToggle
|
||||
}
|
||||
#else
|
||||
Section(header: SettingsHeader(text: "Source")) {
|
||||
sourcePicker
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Quality")) {
|
||||
qualityPicker
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
Section(header: SettingsHeader(text: "Sidebar")) {
|
||||
sidebarPicker
|
||||
}
|
||||
sidebarPicker
|
||||
#endif
|
||||
|
||||
keywordsToggle
|
||||
showHistoryToggle
|
||||
channelSubscribersToggle
|
||||
pauseOnHidingPlayerToggle
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||
closePiPOnNavigationToggle
|
||||
closePiPOnOpeningPlayerToggle
|
||||
#if !os(macOS)
|
||||
closePiPAndOpenPlayerOnEnteringForegroundToggle
|
||||
#endif
|
||||
#if os(iOS)
|
||||
Section(header: SettingsHeader(text: "Orientation")) {
|
||||
if idiom == .pad {
|
||||
enterFullscreenInLandscapeToggle
|
||||
}
|
||||
honorSystemOrientationLockToggle
|
||||
lockLandscapeWhenEnteringFullscreenToggle
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
Spacer()
|
||||
#endif
|
||||
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||
closePiPOnNavigationToggle
|
||||
closePiPOnOpeningPlayerToggle
|
||||
#if !os(macOS)
|
||||
closePiPAndOpenPlayerOnEnteringForegroundToggle
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sourcePicker: some View {
|
||||
@@ -122,17 +136,46 @@ struct PlaybackSettings: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var commentsInstancePicker: some View {
|
||||
Picker("Source", selection: $commentsInstanceID) {
|
||||
Text("Disabled").tag(Optional(""))
|
||||
|
||||
ForEach(InstancesModel.all.filter { $0.app.supportsComments }) { instance in
|
||||
Text(instance.description).tag(Optional(instance.id))
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
#if os(iOS)
|
||||
.pickerStyle(.automatic)
|
||||
#elseif os(tvOS)
|
||||
.pickerStyle(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
private var commentsPlacementPicker: some View {
|
||||
Picker("Placement", selection: $commentsPlacement) {
|
||||
Text("Below video description").tag(CommentsPlacement.info)
|
||||
Text("Separate tab").tag(CommentsPlacement.separate)
|
||||
}
|
||||
.labelsHidden()
|
||||
#if os(iOS)
|
||||
.pickerStyle(.automatic)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
private var sidebarPicker: some View {
|
||||
Picker("Sidebar", selection: $playerSidebar) {
|
||||
#if os(macOS)
|
||||
Text("Show").tag(PlayerSidebarSetting.always)
|
||||
Text("Show sidebar").tag(PlayerSidebarSetting.always)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
Text("Show sidebar when space permits").tag(PlayerSidebarSetting.whenFits)
|
||||
#endif
|
||||
|
||||
Text("Hide").tag(PlayerSidebarSetting.never)
|
||||
Text("Hide sidebar").tag(PlayerSidebarSetting.never)
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
@@ -144,15 +187,15 @@ struct PlaybackSettings: View {
|
||||
}
|
||||
|
||||
private var keywordsToggle: some View {
|
||||
Toggle("Show video keywords", isOn: $showKeywords)
|
||||
Toggle("Show keywords", isOn: $showKeywords)
|
||||
}
|
||||
|
||||
private var showHistoryToggle: some View {
|
||||
Toggle("Show history of videos", isOn: $showHistory)
|
||||
Toggle("Show history", isOn: $showHistory)
|
||||
}
|
||||
|
||||
private var channelSubscribersToggle: some View {
|
||||
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
|
||||
Toggle("Show subscribers count", isOn: $channelSubscribers)
|
||||
}
|
||||
|
||||
private var pauseOnHidingPlayerToggle: some View {
|
||||
@@ -161,7 +204,7 @@ struct PlaybackSettings: View {
|
||||
|
||||
#if os(iOS)
|
||||
private var honorSystemOrientationLockToggle: some View {
|
||||
Toggle("Honor system orientation lock", isOn: $honorSystemOrientationLock)
|
||||
Toggle("Honor orientation lock", isOn: $honorSystemOrientationLock)
|
||||
.disabled(!enterFullscreenInLandscape)
|
||||
}
|
||||
|
||||
@@ -170,7 +213,7 @@ struct PlaybackSettings: View {
|
||||
}
|
||||
|
||||
private var lockLandscapeWhenEnteringFullscreenToggle: some View {
|
||||
Toggle("Lock landscape orientation when entering fullscreen", isOn: $lockLandscapeWhenEnteringFullscreen)
|
||||
Toggle("Lock landscape on rotation", isOn: $lockLandscapeWhenEnteringFullscreen)
|
||||
.disabled(!enterFullscreenInLandscape)
|
||||
}
|
||||
#endif
|
||||
@@ -192,7 +235,7 @@ struct PlaybackSettings: View {
|
||||
|
||||
struct PlaybackSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlaybackSettings()
|
||||
PlayerSettings()
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
@@ -1,152 +0,0 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct ServicesSettings: View {
|
||||
@Default(.sponsorBlockInstance) private var sponsorBlockInstance
|
||||
@Default(.sponsorBlockCategories) private var sponsorBlockCategories
|
||||
@Default(.commentsInstanceID) private var commentsInstanceID
|
||||
|
||||
#if !os(tvOS)
|
||||
@Default(.commentsPlacement) private var commentsPlacement
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
Section(header: SettingsHeader(text: "Comments")) {
|
||||
commentsInstancePicker
|
||||
#if !os(tvOS)
|
||||
commentsPlacementPicker
|
||||
.disabled(!CommentsModel.enabled)
|
||||
#endif
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "SponsorBlock API")) {
|
||||
TextField(
|
||||
"SponsorBlock API Instance",
|
||||
text: $sponsorBlockInstance
|
||||
)
|
||||
.labelsHidden()
|
||||
#if !os(macOS)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Categories to Skip")) {
|
||||
#if os(macOS)
|
||||
let list = ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
SponsorBlockCategorySelectionRow(
|
||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||
selected: sponsorBlockCategories.contains(category)
|
||||
) { value in
|
||||
toggleCategory(category, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
.listStyle(.inset)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
#else
|
||||
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
SponsorBlockCategorySelectionRow(
|
||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||
selected: sponsorBlockCategories.contains(category)
|
||||
) { value in
|
||||
toggleCategory(category, value: value)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private var commentsInstancePicker: some View {
|
||||
Picker("Source", selection: $commentsInstanceID) {
|
||||
Text("Disabled").tag(Optional(""))
|
||||
|
||||
ForEach(InstancesModel.all.filter { $0.app.supportsComments }) { instance in
|
||||
Text(instance.description).tag(Optional(instance.id))
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
#if os(iOS)
|
||||
.pickerStyle(.automatic)
|
||||
#elseif os(tvOS)
|
||||
.pickerStyle(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
private var commentsPlacementPicker: some View {
|
||||
Picker("Placement", selection: $commentsPlacement) {
|
||||
Text("Below video description").tag(CommentsPlacement.info)
|
||||
Text("Separate tab").tag(CommentsPlacement.separate)
|
||||
}
|
||||
.labelsHidden()
|
||||
#if os(iOS)
|
||||
.pickerStyle(.automatic)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
func toggleCategory(_ category: String, value: Bool) {
|
||||
if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value {
|
||||
sponsorBlockCategories.remove(at: index)
|
||||
} else if value {
|
||||
sponsorBlockCategories.insert(category)
|
||||
}
|
||||
}
|
||||
|
||||
struct SponsorBlockCategorySelectionRow: View {
|
||||
let title: String
|
||||
let selected: Bool
|
||||
var action: (Bool) -> Void
|
||||
|
||||
@State private var toggleChecked = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: { action(!selected) }) {
|
||||
HStack {
|
||||
#if os(macOS)
|
||||
Toggle(isOn: $toggleChecked) {
|
||||
Text(self.title)
|
||||
Spacer()
|
||||
}
|
||||
.onAppear {
|
||||
toggleChecked = selected
|
||||
}
|
||||
.onChange(of: toggleChecked) { new in
|
||||
action(new)
|
||||
}
|
||||
#else
|
||||
Text(self.title)
|
||||
Spacer()
|
||||
if selected {
|
||||
Image(systemName: "checkmark")
|
||||
#if os(iOS)
|
||||
.foregroundColor(.accentColor)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.buttonStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ServicesSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
ServicesSettings()
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,13 +2,28 @@ import SwiftUI
|
||||
|
||||
struct SettingsHeader: View {
|
||||
var text: String
|
||||
var secondary = false
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
#if os(macOS) || os(tvOS)
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.focusable(false)
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if secondary {
|
||||
EmptyView()
|
||||
} else {
|
||||
Text(text)
|
||||
}
|
||||
#else
|
||||
Text(text)
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.font(secondary ? .footnote : .title3)
|
||||
.foregroundColor(.secondary)
|
||||
.focusable(false)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.font(secondary ? .system(size: 13) : .system(size: 15))
|
||||
.foregroundColor(secondary ? Color.primary : .secondary)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,10 @@ import SwiftUI
|
||||
struct SettingsView: View {
|
||||
#if os(macOS)
|
||||
private enum Tabs: Hashable {
|
||||
case instances, browsing, history, playback, services, updates
|
||||
case instances, browsing, player, history, sponsorBlock, updates, help
|
||||
}
|
||||
|
||||
@State private var selection = Tabs.instances
|
||||
#endif
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@@ -24,7 +26,7 @@ struct SettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
TabView {
|
||||
TabView(selection: $selection) {
|
||||
Form {
|
||||
InstancesSettings()
|
||||
.environmentObject(accounts)
|
||||
@@ -42,6 +44,14 @@ struct SettingsView: View {
|
||||
}
|
||||
.tag(Tabs.browsing)
|
||||
|
||||
Form {
|
||||
PlayerSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Player", systemImage: "play.rectangle")
|
||||
}
|
||||
.tag(Tabs.player)
|
||||
|
||||
Form {
|
||||
HistorySettings()
|
||||
}
|
||||
@@ -51,20 +61,12 @@ struct SettingsView: View {
|
||||
.tag(Tabs.history)
|
||||
|
||||
Form {
|
||||
PlaybackSettings()
|
||||
SponsorBlockSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Playback", systemImage: "play.rectangle")
|
||||
Label("SponsorBlock", systemImage: "dollarsign.circle")
|
||||
}
|
||||
.tag(Tabs.playback)
|
||||
|
||||
Form {
|
||||
ServicesSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Services", systemImage: "puzzlepiece")
|
||||
}
|
||||
.tag(Tabs.services)
|
||||
.tag(Tabs.sponsorBlock)
|
||||
|
||||
Form {
|
||||
UpdatesSettings()
|
||||
@@ -73,20 +75,22 @@ struct SettingsView: View {
|
||||
Label("Updates", systemImage: "gearshape.2")
|
||||
}
|
||||
.tag(Tabs.updates)
|
||||
|
||||
Form {
|
||||
Help()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Help", systemImage: "questionmark.circle")
|
||||
}
|
||||
.tag(Tabs.help)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 400, height: 400)
|
||||
.frame(width: 480, height: windowHeight)
|
||||
#else
|
||||
NavigationView {
|
||||
List {
|
||||
#if os(tvOS)
|
||||
AccountSelectionView()
|
||||
|
||||
Section(header: SettingsHeader(text: "Favorites")) {
|
||||
NavigationLink("Edit favorites...") {
|
||||
EditFavorites()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Section(header: Text("Instances")) {
|
||||
@@ -96,14 +100,55 @@ struct SettingsView: View {
|
||||
addInstanceButton
|
||||
}
|
||||
|
||||
BrowsingSettings()
|
||||
HistorySettings()
|
||||
PlaybackSettings()
|
||||
ServicesSettings()
|
||||
#if os(tvOS)
|
||||
Divider()
|
||||
#endif
|
||||
|
||||
Section {
|
||||
#if os(tvOS)
|
||||
NavigationLink {
|
||||
EditFavorites()
|
||||
} label: {
|
||||
Label("Favorites", systemImage: "heart.fill")
|
||||
}
|
||||
#endif
|
||||
|
||||
NavigationLink {
|
||||
BrowsingSettings()
|
||||
} label: {
|
||||
Label("Browsing", systemImage: "list.and.film")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
PlayerSettings()
|
||||
} label: {
|
||||
Label("Player", systemImage: "play.rectangle")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
HistorySettings()
|
||||
} label: {
|
||||
Label("History", systemImage: "clock.arrow.circlepath")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
SponsorBlockSettings()
|
||||
} label: {
|
||||
Label("SponsorBlock", systemImage: "dollarsign.circle")
|
||||
}
|
||||
}
|
||||
|
||||
Section(footer: versionString) {
|
||||
NavigationLink {
|
||||
Help()
|
||||
} label: {
|
||||
Label("Help", systemImage: "questionmark.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
#if !os(tvOS)
|
||||
Button("Done") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
@@ -126,9 +171,39 @@ struct SettingsView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private var windowHeight: Double {
|
||||
switch selection {
|
||||
case .instances:
|
||||
return 390
|
||||
case .browsing:
|
||||
return 350
|
||||
case .player:
|
||||
return 450
|
||||
case .history:
|
||||
return 480
|
||||
case .sponsorBlock:
|
||||
return 290
|
||||
case .updates:
|
||||
return 200
|
||||
case .help:
|
||||
return 570
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private var versionString: some View {
|
||||
Text("Yattee \(YatteeApp.version) (build \(YatteeApp.build))")
|
||||
#if os(tvOS)
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var addInstanceButton: some View {
|
||||
Button("Add Instance...") {
|
||||
Button {
|
||||
presentingInstanceForm = true
|
||||
} label: {
|
||||
Label("Add Instance...", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
132
Shared/Settings/SponsorBlockSettings.swift
Normal file
132
Shared/Settings/SponsorBlockSettings.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct SponsorBlockSettings: View {
|
||||
@Default(.sponsorBlockInstance) private var sponsorBlockInstance
|
||||
@Default(.sponsorBlockCategories) private var sponsorBlockCategories
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(macOS)
|
||||
sections
|
||||
|
||||
Spacer()
|
||||
#else
|
||||
List {
|
||||
sections
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxWidth: 1000)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#endif
|
||||
.navigationTitle("SponsorBlock")
|
||||
}
|
||||
|
||||
private var sections: some View {
|
||||
Group {
|
||||
Section(header: SettingsHeader(text: "SponsorBlock API")) {
|
||||
TextField(
|
||||
"SponsorBlock API Instance",
|
||||
text: $sponsorBlockInstance
|
||||
)
|
||||
.labelsHidden()
|
||||
#if !os(macOS)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "Categories to Skip")) {
|
||||
#if os(macOS)
|
||||
let list = ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
SponsorBlockCategorySelectionRow(
|
||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||
selected: sponsorBlockCategories.contains(category)
|
||||
) { value in
|
||||
toggleCategory(category, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
.listStyle(.inset)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
#else
|
||||
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
SponsorBlockCategorySelectionRow(
|
||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||
selected: sponsorBlockCategories.contains(category)
|
||||
) { value in
|
||||
toggleCategory(category, value: value)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toggleCategory(_ category: String, value: Bool) {
|
||||
if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value {
|
||||
sponsorBlockCategories.remove(at: index)
|
||||
} else if value {
|
||||
sponsorBlockCategories.insert(category)
|
||||
}
|
||||
}
|
||||
|
||||
struct SponsorBlockCategorySelectionRow: View {
|
||||
let title: String
|
||||
let selected: Bool
|
||||
var action: (Bool) -> Void
|
||||
|
||||
@State private var toggleChecked = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: { action(!selected) }) {
|
||||
HStack {
|
||||
#if os(macOS)
|
||||
Toggle(isOn: $toggleChecked) {
|
||||
Text(self.title)
|
||||
Spacer()
|
||||
}
|
||||
.onAppear {
|
||||
toggleChecked = selected
|
||||
}
|
||||
.onChange(of: toggleChecked) { new in
|
||||
action(new)
|
||||
}
|
||||
#else
|
||||
Text(self.title)
|
||||
Spacer()
|
||||
if selected {
|
||||
Image(systemName: "checkmark")
|
||||
#if os(iOS)
|
||||
.foregroundColor(.accentColor)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.buttonStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ServicesSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
SponsorBlockSettings()
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,6 +3,14 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct YatteeApp: App {
|
||||
static var version: String {
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
||||
}
|
||||
|
||||
static var build: String {
|
||||
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@StateObject private var updater = UpdaterModel()
|
||||
|
Reference in New Issue
Block a user