mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Add Sponsor Block and settings
This commit is contained in:
parent
e64a520d5e
commit
8e0af22b94
@ -40,8 +40,4 @@ final class InstancesModel: ObservableObject {
|
|||||||
accounts.forEach { AccountsModel.remove($0) }
|
accounts.forEach { AccountsModel.remove($0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func setLastAccount(_ account: Account?) {
|
|
||||||
Defaults[.lastAccountID] = account?.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -343,10 +343,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func extractChannelPlaylists(from json: JSON) -> [ChannelPlaylist] {
|
|
||||||
json.arrayValue.map(InvidiousAPI.extractChannelPlaylist)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
private static func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||||
details["videoThumbnails"].arrayValue.map { json in
|
details["videoThumbnails"].arrayValue.map { json in
|
||||||
Thumbnail(url: json["url"].url!, quality: .init(rawValue: json["quality"].string!)!)
|
Thumbnail(url: json["url"].url!, quality: .init(rawValue: json["quality"].string!)!)
|
||||||
|
@ -185,10 +185,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func extractChannelPlaylists(from json: JSON) -> [ChannelPlaylist] {
|
|
||||||
json.arrayValue.compactMap(PipedAPI.extractChannelPlaylist)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func extractVideo(from content: JSON) -> Video? {
|
private static func extractVideo(from content: JSON) -> Video? {
|
||||||
let details = content.dictionaryValue
|
let details = content.dictionaryValue
|
||||||
let url = details["url"]?.string
|
let url = details["url"]?.string
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import Alamofire
|
|
||||||
import Foundation
|
|
||||||
import SwiftyJSON
|
|
||||||
|
|
||||||
final class SponsorBlockAPI: ObservableObject {
|
|
||||||
static let categories = ["sponsor", "selfpromo", "outro", "intro", "music_offtopic", "interaction"]
|
|
||||||
|
|
||||||
var id: String
|
|
||||||
|
|
||||||
@Published var segments = [Segment]()
|
|
||||||
|
|
||||||
init(_ id: String) {
|
|
||||||
self.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func load() {
|
|
||||||
AF.request("https://sponsor.ajay.app/api/skipSegments", parameters: parameters).responseJSON { response in
|
|
||||||
switch response.result {
|
|
||||||
case let .success(value):
|
|
||||||
self.segments = JSON(value).arrayValue.map { SponsorBlockSegment($0) }
|
|
||||||
case let .failure(error):
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var parameters: [String: String] {
|
|
||||||
[
|
|
||||||
"videoID": id,
|
|
||||||
"categories": JSON(SponsorBlockAPI.categories).rawString(String.Encoding.utf8)!
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,6 +6,7 @@ import Logging
|
|||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
import Siesta
|
import Siesta
|
||||||
|
import SwiftUI
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class PlayerModel: ObservableObject {
|
final class PlayerModel: ObservableObject {
|
||||||
@ -22,8 +23,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var stream: Stream?
|
@Published var stream: Stream?
|
||||||
@Published var currentRate: Float?
|
@Published var currentRate: Float?
|
||||||
|
|
||||||
@Published var availableStreams = [Stream]() { didSet { rebuildStreamsMenu() } }
|
@Published var availableStreams = [Stream]() { didSet { rebuildTVMenu() } }
|
||||||
@Published var streamSelection: Stream? { didSet { rebuildStreamsMenu() } }
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||||
|
|
||||||
@Published var queue = [PlayerQueueItem]()
|
@Published var queue = [PlayerQueueItem]()
|
||||||
@Published var currentItem: PlayerQueueItem!
|
@Published var currentItem: PlayerQueueItem!
|
||||||
@ -33,6 +34,11 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var playerNavigationLinkActive = false
|
@Published var playerNavigationLinkActive = false
|
||||||
|
|
||||||
|
@Published var sponsorBlock = SponsorBlockAPI()
|
||||||
|
@Published var segmentRestorationTime: CMTime?
|
||||||
|
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
||||||
|
@Published var restoredSegments = [Segment]()
|
||||||
|
|
||||||
var accounts: AccountsModel
|
var accounts: AccountsModel
|
||||||
var instances: InstancesModel
|
var instances: InstancesModel
|
||||||
|
|
||||||
@ -117,6 +123,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
of video: Video,
|
of video: Video,
|
||||||
preservingTime: Bool = false
|
preservingTime: Bool = false
|
||||||
) {
|
) {
|
||||||
|
resetSegments()
|
||||||
|
sponsorBlock.loadSegments(videoID: video.videoID)
|
||||||
|
|
||||||
if let url = stream.singleAssetURL {
|
if let url = stream.singleAssetURL {
|
||||||
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
||||||
|
|
||||||
@ -218,7 +227,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try! compositionTrack.insertTimeRange(
|
try! compositionTrack.insertTimeRange(
|
||||||
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1000)),
|
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1_000_000)),
|
||||||
of: assetTrack,
|
of: assetTrack,
|
||||||
at: .zero
|
at: .zero
|
||||||
)
|
)
|
||||||
@ -327,19 +336,28 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
player.seek(
|
player.seek(
|
||||||
to: time,
|
to: time,
|
||||||
toleranceBefore: .init(seconds: 1, preferredTimescale: 1000),
|
toleranceBefore: .init(seconds: 1, preferredTimescale: 1_000_000),
|
||||||
toleranceAfter: .zero,
|
toleranceAfter: .zero,
|
||||||
completionHandler: completionHandler
|
completionHandler: completionHandler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addTimeObserver() {
|
private func addTimeObserver() {
|
||||||
let interval = CMTime(seconds: 0.5, preferredTimescale: 1000)
|
let interval = CMTime(seconds: 0.5, preferredTimescale: 1_000_000)
|
||||||
|
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
|
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
|
||||||
self.currentRate = self.player.rate
|
self.currentRate = self.player.rate
|
||||||
self.currentItem?.playbackTime = self.player.currentTime()
|
|
||||||
self.currentItem?.videoDuration = self.player.currentItem?.asset.duration.seconds
|
guard !self.currentItem.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = self.player.currentTime()
|
||||||
|
|
||||||
|
self.currentItem!.playbackTime = time
|
||||||
|
self.currentItem!.videoDuration = self.player.currentItem?.asset.duration.seconds
|
||||||
|
|
||||||
|
self.handleSegments(at: time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
77
Model/Player/PlayerSegments.swift
Normal file
77
Model/Player/PlayerSegments.swift
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import CoreMedia
|
||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension PlayerModel {
|
||||||
|
func handleSegments(at time: CMTime) {
|
||||||
|
if let segment = lastSkipped {
|
||||||
|
if time > CMTime(seconds: segment.end + 10, preferredTimescale: 1_000_000) {
|
||||||
|
resetLastSegment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let firstSegment = sponsorBlock.segments.first(where: { $0.timeInSegment(time) }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// find last segment in case they are 2 sec or less after each other
|
||||||
|
// to avoid multiple skips in a row
|
||||||
|
var nextSegments = [firstSegment]
|
||||||
|
|
||||||
|
while let segment = sponsorBlock.segments.first(where: {
|
||||||
|
$0.timeInSegment(CMTime(seconds: nextSegments.last!.end + 2, preferredTimescale: 1_000_000))
|
||||||
|
}) {
|
||||||
|
nextSegments.append(segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let segmentToSkip = nextSegments.last(where: { $0.endTime <= playerItemDuration ?? .zero }),
|
||||||
|
self.shouldSkip(segmentToSkip, at: time)
|
||||||
|
{
|
||||||
|
skip(segmentToSkip, at: time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func skip(_ segment: Segment, at time: CMTime) {
|
||||||
|
guard segment.endTime.seconds <= playerItemDuration?.seconds ?? .infinity else {
|
||||||
|
logger.error("item time is: \(time.seconds) and trying to skip to \(playerItemDuration?.seconds ?? .infinity)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.seek(to: segment.endTime)
|
||||||
|
lastSkipped = segment
|
||||||
|
segmentRestorationTime = time
|
||||||
|
logger.info("SponsorBlock skipping to: \(segment.endTime)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool {
|
||||||
|
guard isPlaying,
|
||||||
|
!restoredSegments.contains(segment),
|
||||||
|
Defaults[.sponsorBlockCategories].contains(segment.category)
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.seconds - segment.start < 2 && segment.end - time.seconds > 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreLastSkippedSegment() {
|
||||||
|
guard let segment = lastSkipped,
|
||||||
|
let time = segmentRestorationTime
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredSegments.append(segment)
|
||||||
|
player.seek(to: time)
|
||||||
|
resetLastSegment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetLastSegment() {
|
||||||
|
lastSkipped = nil
|
||||||
|
segmentRestorationTime = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetSegments() {
|
||||||
|
resetLastSegment()
|
||||||
|
restoredSegments = []
|
||||||
|
}
|
||||||
|
}
|
@ -57,47 +57,13 @@ extension PlayerModel {
|
|||||||
completionHandler: @escaping ([Stream]) -> Void
|
completionHandler: @escaping ([Stream]) -> Void
|
||||||
) {
|
) {
|
||||||
instancesWithLoadedStreams.append(instance)
|
instancesWithLoadedStreams.append(instance)
|
||||||
rebuildStreamsMenu()
|
rebuildTVMenu()
|
||||||
|
|
||||||
if instances.all.count == instancesWithLoadedStreams.count {
|
if instances.all.count == instancesWithLoadedStreams.count {
|
||||||
completionHandler(streams.sorted { $0.kind < $1.kind })
|
completionHandler(streams.sorted { $0.kind < $1.kind })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
var streamsMenu: UIMenu {
|
|
||||||
UIMenu(
|
|
||||||
title: "Streams",
|
|
||||||
image: UIImage(systemName: "antenna.radiowaves.left.and.right"),
|
|
||||||
children: streamsMenuActions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamsMenuActions: [UIAction] {
|
|
||||||
guard !availableStreams.isEmpty else {
|
|
||||||
return [ // swiftlint:disable:this implicit_return
|
|
||||||
UIAction(title: "Empty", attributes: .disabled) { _ in }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return availableStreamsSorted.map { stream in
|
|
||||||
let state = stream == streamSelection ? UIAction.State.on : .off
|
|
||||||
|
|
||||||
return UIAction(title: stream.description, state: state) { _ in
|
|
||||||
self.streamSelection = stream
|
|
||||||
self.upgradeToStream(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
func rebuildStreamsMenu() {
|
|
||||||
#if os(tvOS)
|
|
||||||
avPlayerViewController?.transportBarCustomMenuItems = [streamsMenu]
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] {
|
func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] {
|
||||||
streams.map { stream in
|
streams.map { stream in
|
||||||
stream.instance = instance
|
stream.instance = instance
|
||||||
|
55
Model/Player/PlayerTVMenu.swift
Normal file
55
Model/Player/PlayerTVMenu.swift
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import Foundation
|
||||||
|
#if !os(macOS)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extension PlayerModel {
|
||||||
|
#if os(tvOS)
|
||||||
|
var streamsMenu: UIMenu {
|
||||||
|
UIMenu(
|
||||||
|
title: "Streams",
|
||||||
|
image: UIImage(systemName: "antenna.radiowaves.left.and.right"),
|
||||||
|
children: streamsMenuActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamsMenuActions: [UIAction] {
|
||||||
|
guard !availableStreams.isEmpty else {
|
||||||
|
return [ // swiftlint:disable:this implicit_return
|
||||||
|
UIAction(title: "Empty", attributes: .disabled) { _ in }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableStreamsSorted.map { stream in
|
||||||
|
let state = stream == streamSelection ? UIAction.State.on : .off
|
||||||
|
|
||||||
|
return UIAction(title: stream.description, state: state) { _ in
|
||||||
|
self.streamSelection = stream
|
||||||
|
self.upgradeToStream(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var restoreLastSkippedSegmentAction: UIAction? {
|
||||||
|
guard let segment = lastSkipped else {
|
||||||
|
return nil // swiftlint:disable:this implicit_return
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIAction(
|
||||||
|
title: "Restore \(segment.category)",
|
||||||
|
image: UIImage(systemName: "arrow.uturn.left.circle")
|
||||||
|
) { _ in
|
||||||
|
self.restoreLastSkippedSegment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
func rebuildTVMenu() {
|
||||||
|
#if os(tvOS)
|
||||||
|
avPlayerViewController?.transportBarCustomMenuItems = [
|
||||||
|
restoreLastSkippedSegmentAction,
|
||||||
|
streamsMenu
|
||||||
|
].compactMap { $0 }
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
@ -13,10 +13,18 @@ class Segment: ObservableObject, Hashable {
|
|||||||
segment.first!
|
segment.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime: CMTime {
|
||||||
|
CMTime(seconds: start, preferredTimescale: 1000)
|
||||||
|
}
|
||||||
|
|
||||||
var end: Double {
|
var end: Double {
|
||||||
segment.last!
|
segment.last!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var endTime: CMTime {
|
||||||
|
CMTime(seconds: end, preferredTimescale: 1000)
|
||||||
|
}
|
||||||
|
|
||||||
var duration: Double {
|
var duration: Double {
|
||||||
end - start
|
end - start
|
||||||
}
|
}
|
||||||
@ -32,10 +40,6 @@ class Segment: ObservableObject, Hashable {
|
|||||||
(start ... end).contains(time.seconds)
|
(start ... end).contains(time.seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
var skipTo: CMTime {
|
|
||||||
CMTime(seconds: segment.last!, preferredTimescale: 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(uuid)
|
hasher.combine(uuid)
|
||||||
}
|
}
|
||||||
@ -47,8 +51,4 @@ class Segment: ObservableObject, Hashable {
|
|||||||
func title() -> String {
|
func title() -> String {
|
||||||
category
|
category
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldSkip(_ atTime: CMTime) -> Bool {
|
|
||||||
atTime.seconds - start < 2 && end - atTime.seconds > 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
73
Model/SponsorBlock/SponsorBlockAPI.swift
Normal file
73
Model/SponsorBlock/SponsorBlockAPI.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import Alamofire
|
||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import SwiftyJSON
|
||||||
|
|
||||||
|
final class SponsorBlockAPI: ObservableObject {
|
||||||
|
let logger = Logger(label: "net.yattee.app.sb")
|
||||||
|
|
||||||
|
static let categories = ["sponsor", "selfpromo", "intro", "outro", "interaction", "music_offtopic"]
|
||||||
|
|
||||||
|
@Published var videoID: String?
|
||||||
|
@Published var segments = [Segment]()
|
||||||
|
|
||||||
|
static func categoryDescription(_ name: String) -> String? {
|
||||||
|
guard SponsorBlockAPI.categories.contains(name) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "selfpromo":
|
||||||
|
return "Self-promotion"
|
||||||
|
case "music_offtopic":
|
||||||
|
return "Offtopic in Music Videos"
|
||||||
|
default:
|
||||||
|
return name.capitalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSegments(videoID: String) {
|
||||||
|
guard !skipSegmentsURL.isNil, self.videoID != videoID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoID = videoID
|
||||||
|
|
||||||
|
requestSegments()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestSegments() {
|
||||||
|
guard let url = skipSegmentsURL else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AF.request(url, parameters: parameters).responseJSON { response in
|
||||||
|
switch response.result {
|
||||||
|
case let .success(value):
|
||||||
|
self.segments = JSON(value).arrayValue.map(SponsorBlockSegment.init).sorted { $0.end < $1.end }
|
||||||
|
|
||||||
|
self.logger.info("loaded \(self.segments.count) SponsorBlock segments")
|
||||||
|
self.segments.forEach {
|
||||||
|
self.logger.info("\($0.start) -> \($0.end)")
|
||||||
|
}
|
||||||
|
case let .failure(error):
|
||||||
|
self.segments = []
|
||||||
|
|
||||||
|
self.logger.error("failed to load SponsorBlock segments: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var skipSegmentsURL: String? {
|
||||||
|
let url = Defaults[.sponsorBlockInstance]
|
||||||
|
return url.isEmpty ? nil : "\(url)/api/skipSegments"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var parameters: [String: String] {
|
||||||
|
[
|
||||||
|
"videoID": videoID!,
|
||||||
|
"categories": JSON(SponsorBlockAPI.categories).rawString(String.Encoding.utf8)!
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ final class SponsorBlockSegment: Segment {
|
|||||||
case "selfpromo":
|
case "selfpromo":
|
||||||
return "self-promotion"
|
return "self-promotion"
|
||||||
case "music_offtopic":
|
case "music_offtopic":
|
||||||
return "to music"
|
return "offtopic"
|
||||||
default:
|
default:
|
||||||
return category
|
return category
|
||||||
}
|
}
|
@ -109,10 +109,6 @@ class Stream: Equatable, Hashable, Identifiable {
|
|||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
}
|
}
|
||||||
|
|
||||||
var shortQuality: String {
|
|
||||||
kind == .hls ? "adaptive" : resolution.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var quality: String {
|
var quality: String {
|
||||||
kind == .hls ? "adaptive (HLS)" : "\(resolution.name) \(kind == .stream ? "(\(kind.rawValue))" : "")"
|
kind == .hls ? "adaptive (HLS)" : "\(resolution.name) \(kind == .stream ? "(\(kind.rawValue))" : "")"
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,12 @@
|
|||||||
374C053527242D9F009BDDBE /* ServicesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* ServicesSettings.swift */; };
|
374C053527242D9F009BDDBE /* ServicesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* ServicesSettings.swift */; };
|
||||||
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* ServicesSettings.swift */; };
|
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* ServicesSettings.swift */; };
|
||||||
374C053727242D9F009BDDBE /* ServicesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* ServicesSettings.swift */; };
|
374C053727242D9F009BDDBE /* ServicesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* ServicesSettings.swift */; };
|
||||||
|
374C053B2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
||||||
|
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
||||||
|
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
||||||
|
374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; };
|
||||||
|
374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; };
|
||||||
|
374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; };
|
||||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||||
@ -439,6 +445,8 @@
|
|||||||
37484C2C26FC844700287258 /* AccountsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettings.swift; sourceTree = "<group>"; };
|
37484C2C26FC844700287258 /* AccountsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettings.swift; sourceTree = "<group>"; };
|
||||||
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
||||||
374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = "<group>"; };
|
374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = "<group>"; };
|
||||||
|
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = "<group>"; };
|
||||||
|
374C053E272472C0009BDDBE /* PlayerSegments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSegments.swift; sourceTree = "<group>"; };
|
||||||
375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
|
375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
|
||||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
||||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||||
@ -709,7 +717,6 @@
|
|||||||
children = (
|
children = (
|
||||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
||||||
3700155A271B0D4D0049C794 /* PipedAPI.swift */,
|
3700155A271B0D4D0049C794 /* PipedAPI.swift */,
|
||||||
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */,
|
|
||||||
37D526DD2720AC4400ED2F5E /* VideosAPI.swift */,
|
37D526DD2720AC4400ED2F5E /* VideosAPI.swift */,
|
||||||
376A33DF2720CAD6000C1D6B /* VideosApp.swift */,
|
376A33DF2720CAD6000C1D6B /* VideosApp.swift */,
|
||||||
);
|
);
|
||||||
@ -722,7 +729,9 @@
|
|||||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
||||||
37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
|
37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
|
||||||
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
|
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
|
||||||
|
374C053E272472C0009BDDBE /* PlayerSegments.swift */,
|
||||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */,
|
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */,
|
||||||
|
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */,
|
||||||
);
|
);
|
||||||
path = Player;
|
path = Player;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -767,6 +776,15 @@
|
|||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
374C0539272436DA009BDDBE /* SponsorBlock */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */,
|
||||||
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
||||||
|
);
|
||||||
|
path = SponsorBlock;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3761AC0526F0F96100AA496F /* Modifiers */ = {
|
3761AC0526F0F96100AA496F /* Modifiers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -940,7 +958,9 @@
|
|||||||
3743B864272169E200261544 /* Applications */,
|
3743B864272169E200261544 /* Applications */,
|
||||||
3743B86527216A0600261544 /* Player */,
|
3743B86527216A0600261544 /* Player */,
|
||||||
37FB283F2721B20800A57617 /* Search */,
|
37FB283F2721B20800A57617 /* Search */,
|
||||||
|
374C0539272436DA009BDDBE /* SponsorBlock */,
|
||||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||||
|
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||||
37FB28402721B22200A57617 /* ContentItem.swift */,
|
37FB28402721B22200A57617 /* ContentItem.swift */,
|
||||||
37141672267A8E10006CA35D /* Country.swift */,
|
37141672267A8E10006CA35D /* Country.swift */,
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||||
@ -949,14 +969,12 @@
|
|||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
|
||||||
3797758A2689345500DD52A8 /* Store.swift */,
|
3797758A2689345500DD52A8 /* Store.swift */,
|
||||||
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||||
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */,
|
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */,
|
||||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
||||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||||
37D4B19626717E1500C925CA /* Video.swift */,
|
37D4B19626717E1500C925CA /* Video.swift */,
|
||||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1446,6 +1464,7 @@
|
|||||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||||
|
374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */,
|
||||||
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
|
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
|
||||||
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
||||||
37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */,
|
37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */,
|
||||||
@ -1461,6 +1480,7 @@
|
|||||||
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
|
374C053B2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
|
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||||
@ -1507,6 +1527,7 @@
|
|||||||
37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||||
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
|
374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */,
|
||||||
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */,
|
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */,
|
||||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
@ -1518,6 +1539,7 @@
|
|||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
|
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
@ -1635,6 +1657,7 @@
|
|||||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
|
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */,
|
37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */,
|
||||||
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||||
@ -1675,6 +1698,7 @@
|
|||||||
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
|
374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */,
|
||||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */,
|
37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */,
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc",
|
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
||||||
"version": "5.4.4"
|
"version": "5.4.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -19,6 +19,7 @@ extension Defaults.Keys {
|
|||||||
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
||||||
|
|
||||||
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
|
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
|
||||||
|
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
|
||||||
|
|
||||||
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest)
|
||||||
|
|
||||||
|
@ -21,6 +21,9 @@ struct PlaybackBar: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
|
if !player.lastSkipped.isNil {
|
||||||
|
restoreLastSkippedSegmentButton
|
||||||
|
}
|
||||||
if player.currentVideo!.live {
|
if player.currentVideo!.live {
|
||||||
Image(systemName: "dot.radiowaves.left.and.right")
|
Image(systemName: "dot.radiowaves.left.and.right")
|
||||||
} else if player.isLoadingAvailableStreams || player.isLoadingStream {
|
} else if player.isLoadingAvailableStreams || player.isLoadingStream {
|
||||||
@ -30,7 +33,6 @@ struct PlaybackBar: View {
|
|||||||
streamControl
|
streamControl
|
||||||
.disabled(player.isLoadingAvailableStreams)
|
.disabled(player.isLoadingAvailableStreams)
|
||||||
.frame(alignment: .trailing)
|
.frame(alignment: .trailing)
|
||||||
.environment(\.colorScheme, .dark)
|
|
||||||
.onChange(of: player.streamSelection) { selection in
|
.onChange(of: player.streamSelection) { selection in
|
||||||
guard !selection.isNil else {
|
guard !selection.isNil else {
|
||||||
return
|
return
|
||||||
@ -42,6 +44,7 @@ struct PlaybackBar: View {
|
|||||||
.frame(maxWidth: 180)
|
.frame(maxWidth: 180)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.environment(\.colorScheme, .dark)
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
@ -91,6 +94,19 @@ struct PlaybackBar: View {
|
|||||||
return "ends at \(timeFinishAtString)"
|
return "ends at \(timeFinishAtString)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var restoreLastSkippedSegmentButton: some View {
|
||||||
|
Button {
|
||||||
|
player.restoreLastSkippedSegment()
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "arrow.uturn.left.circle")
|
||||||
|
Text(player.lastSkipped!.category)
|
||||||
|
Text("•")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
private var streamControl: some View {
|
private var streamControl: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Picker("", selection: $player.streamSelection) {
|
Picker("", selection: $player.streamSelection) {
|
||||||
|
@ -17,8 +17,6 @@ struct InstanceForm: View {
|
|||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject<InstancesModel> private var instancesModel
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Group {
|
Group {
|
||||||
|
@ -2,12 +2,14 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ServicesSettings: View {
|
struct ServicesSettings: View {
|
||||||
@Default(.sponsorBlockInstance) private var sponsorBlock
|
@Default(.sponsorBlockInstance) private var sponsorBlockInstance
|
||||||
|
@Default(.sponsorBlockCategories) private var sponsorBlockCategories
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section(header: Text("SponsorBlock API")) {
|
Section(header: Text("SponsorBlock API")) {
|
||||||
TextField(
|
TextField(
|
||||||
"SponsorBlock API Instance",
|
"SponsorBlock API Instance",
|
||||||
text: $sponsorBlock,
|
text: $sponsorBlockInstance,
|
||||||
prompt: Text("SponsorBlock API URL, leave blank to disable")
|
prompt: Text("SponsorBlock API URL, leave blank to disable")
|
||||||
)
|
)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
@ -15,9 +17,77 @@ struct ServicesSettings: View {
|
|||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("SponsorBlock Categories to Skip")) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
List(SponsorBlockAPI.categories, id: \.self) { category in
|
||||||
|
SponsorBlockCategorySelectionRow(
|
||||||
|
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||||
|
selected: sponsorBlockCategories.contains(category)
|
||||||
|
) { value in
|
||||||
|
toggleCategory(category, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||||
Spacer()
|
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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ struct HorizontalCells: View {
|
|||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.id(UUID())
|
.id(items.map(\.id).joined())
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.frame(height: 560)
|
.frame(height: 560)
|
||||||
#else
|
#else
|
||||||
|
@ -17,7 +17,7 @@ struct VerticalCells: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.id(UUID())
|
.id(items.map(\.id).joined())
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.background()
|
.background()
|
||||||
@ -56,7 +56,7 @@ struct VerticalCells: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoCellsVertical_Previews: PreviewProvider {
|
struct VeticalCells_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VerticalCells(items: ContentItem.array(of: Video.allFixtures))
|
VerticalCells(items: ContentItem.array(of: Video.allFixtures))
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
|
@ -5,7 +5,6 @@ struct PlayerControlsView<Content: View>: View {
|
|||||||
|
|
||||||
@Environment(\.navigationStyle) private var navigationStyle
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
@EnvironmentObject<PlayerModel> private var model
|
@EnvironmentObject<PlayerModel> private var model
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
|
||||||
|
|
||||||
init(@ViewBuilder content: @escaping () -> Content) {
|
init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
self.content = content()
|
self.content = content()
|
||||||
|
Loading…
Reference in New Issue
Block a user