mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Merge pull request #639 from stonerl/sponsor-block
SponsorBlock Improvements
This commit is contained in:
commit
9d291cca28
@ -71,13 +71,13 @@ final class SeekModel: ObservableObject {
|
|||||||
func showOSD() {
|
func showOSD() {
|
||||||
guard !presentingOSD else { return }
|
guard !presentingOSD else { return }
|
||||||
|
|
||||||
withAnimation(.easeIn(duration: 0.1)) { self.presentingOSD = true }
|
presentingOSD = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func hideOSD() {
|
func hideOSD() {
|
||||||
guard presentingOSD else { return }
|
guard presentingOSD else { return }
|
||||||
|
|
||||||
withAnimation(.easeIn(duration: 0.1)) { self.presentingOSD = false }
|
presentingOSD = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func hideOSDWithDelay() {
|
func hideOSDWithDelay() {
|
||||||
|
@ -5,7 +5,7 @@ import Logging
|
|||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class SponsorBlockAPI: ObservableObject {
|
final class SponsorBlockAPI: ObservableObject {
|
||||||
static let categories = ["sponsor", "selfpromo", "intro", "outro", "interaction", "music_offtopic"]
|
static let categories = ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "filler", "music_offtopic"]
|
||||||
|
|
||||||
let logger = Logger(label: "stream.yattee.app.sb")
|
let logger = Logger(label: "stream.yattee.app.sb")
|
||||||
|
|
||||||
@ -21,15 +21,19 @@ final class SponsorBlockAPI: ObservableObject {
|
|||||||
case "sponsor":
|
case "sponsor":
|
||||||
return "Sponsor".localized()
|
return "Sponsor".localized()
|
||||||
case "selfpromo":
|
case "selfpromo":
|
||||||
return "Self-promotion".localized()
|
return "Unpaid/Self Promotion".localized()
|
||||||
case "intro":
|
|
||||||
return "Intro".localized()
|
|
||||||
case "outro":
|
|
||||||
return "Outro".localized()
|
|
||||||
case "interaction":
|
case "interaction":
|
||||||
return "Interaction".localized()
|
return "Interaction Reminder (Subscribe)".localized()
|
||||||
|
case "intro":
|
||||||
|
return "Intermission/Intro Animation".localized()
|
||||||
|
case "outro":
|
||||||
|
return "Endcards/Credits".localized()
|
||||||
|
case "preview":
|
||||||
|
return "Preview/Recap/Hook".localized()
|
||||||
|
case "filler":
|
||||||
|
return "Filler Tangent/Jokes".localized()
|
||||||
case "music_offtopic":
|
case "music_offtopic":
|
||||||
return "Offtopic in Music Videos".localized()
|
return "Music: Non-Music Section".localized()
|
||||||
default:
|
default:
|
||||||
return name.capitalized
|
return name.capitalized
|
||||||
}
|
}
|
||||||
@ -46,9 +50,14 @@ final class SponsorBlockAPI: ObservableObject {
|
|||||||
"The creator will receive payment or compensation in the form of money or free products.").localized()
|
"The creator will receive payment or compensation in the form of money or free products.").localized()
|
||||||
|
|
||||||
case "selfpromo":
|
case "selfpromo":
|
||||||
return ("Promoting a product or service that is directly related to the creator themselves. " +
|
return ("The creator will not receive any payment in exchange for this promotion. " +
|
||||||
|
"This includes charity drives or free shout outs for products or other people they like.\n\n" +
|
||||||
|
"Promoting a product or service that is directly related to the creator themselves. " +
|
||||||
"This usually includes merchandise or promotion of monetized platforms.").localized()
|
"This usually includes merchandise or promotion of monetized platforms.").localized()
|
||||||
|
|
||||||
|
case "interaction":
|
||||||
|
return "Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video).".localized()
|
||||||
|
|
||||||
case "intro":
|
case "intro":
|
||||||
return ("Segments typically found at the start of a video that include an animation, " +
|
return ("Segments typically found at the start of a video that include an animation, " +
|
||||||
"still frame or clip which are also seen in other videos by the same creator.").localized()
|
"still frame or clip which are also seen in other videos by the same creator.").localized()
|
||||||
@ -56,8 +65,11 @@ final class SponsorBlockAPI: ObservableObject {
|
|||||||
case "outro":
|
case "outro":
|
||||||
return "Typically near or at the end of the video when the credits pop up and/or endcards are shown.".localized()
|
return "Typically near or at the end of the video when the credits pop up and/or endcards are shown.".localized()
|
||||||
|
|
||||||
case "interaction":
|
case "preview":
|
||||||
return "Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video).".localized()
|
return "Collection of clips that show what is coming up in in this video or other videos in a series where all information is repeated later in the video".localized()
|
||||||
|
|
||||||
|
case "filler":
|
||||||
|
return "Filler Tangent/ Jokes is only for tangential scenes added only for filler or humor that are not required to understand the main content of the video.".localized()
|
||||||
|
|
||||||
case "music_offtopic":
|
case "music_offtopic":
|
||||||
return "For videos which feature music as the primary content.".localized()
|
return "For videos which feature music as the primary content.".localized()
|
||||||
@ -100,8 +112,8 @@ final class SponsorBlockAPI: ObservableObject {
|
|||||||
self.segments = JSON(value).arrayValue.map(SponsorBlockSegment.init).sorted { $0.end < $1.end }
|
self.segments = JSON(value).arrayValue.map(SponsorBlockSegment.init).sorted { $0.end < $1.end }
|
||||||
|
|
||||||
self.logger.info("loaded \(self.segments.count) SponsorBlock segments")
|
self.logger.info("loaded \(self.segments.count) SponsorBlock segments")
|
||||||
self.segments.forEach {
|
for segment in self.segments {
|
||||||
self.logger.info("\($0.start) -> \($0.end)")
|
self.logger.info("\(segment.start) -> \(segment.end)")
|
||||||
}
|
}
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
self.segments = []
|
self.segments = []
|
||||||
|
@ -243,6 +243,10 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
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 sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
|
||||||
|
static let sponsorBlockColors = Key<[String: String]>("sponsorBlockColors", default: SponsorBlockColors.dictionary)
|
||||||
|
static let sponsorBlockShowTimeWithSkipsRemoved = Key<Bool>("sponsorBlockShowTimeWithSkipsRemoved", default: false)
|
||||||
|
static let sponsorBlockShowCategoriesInTimeline = Key<Bool>("sponsorBlockShowCategoriesInTimeline", default: true)
|
||||||
|
static let sponsorBlockShowNoticeAfterSkip = Key<Bool>("sponsorBlockShowNoticeAfterSkip", default: true)
|
||||||
|
|
||||||
// MARK: GROUP - Locations
|
// MARK: GROUP - Locations
|
||||||
|
|
||||||
@ -580,3 +584,26 @@ enum WidgetListingStyle: String, CaseIterable, Defaults.Serializable {
|
|||||||
case horizontalCells
|
case horizontalCells
|
||||||
case list
|
case list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SponsorBlockColors: String {
|
||||||
|
case sponsor = "#00D400" // Green
|
||||||
|
case selfpromo = "#FFFF00" // Yellow
|
||||||
|
case interaction = "#CC00FF" // Purple
|
||||||
|
case intro = "#00FFFF" // Cyan
|
||||||
|
case outro = "#0202ED" // Dark Blue
|
||||||
|
case preview = "#008FD6" // Light Blue
|
||||||
|
case filler = "#7300FF" // Violet
|
||||||
|
case music_offtopic = "#FF9900" // Orange
|
||||||
|
|
||||||
|
// Define all cases, can be used to iterate over the colors
|
||||||
|
static let allCases: [SponsorBlockColors] = [.sponsor, .selfpromo, .interaction, .intro, .outro, .preview, .filler, .music_offtopic]
|
||||||
|
|
||||||
|
// Create a dictionary with the category names as keys and colors as values
|
||||||
|
static let dictionary: [String: String] = {
|
||||||
|
var dict = [String: String]()
|
||||||
|
for item in allCases {
|
||||||
|
dict[String(describing: item)] = item.rawValue
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
@ -13,6 +13,18 @@ struct Seek: View {
|
|||||||
|
|
||||||
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
||||||
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
||||||
|
@Default(.sponsorBlockColors) private var sponsorBlockColors
|
||||||
|
@Default(.sponsorBlockShowNoticeAfterSkip) private var showNoticeAfterSkip
|
||||||
|
|
||||||
|
private func getColor(for category: String) -> Color {
|
||||||
|
if let hexString = sponsorBlockColors[category], let rgbValue = Int(hexString.dropFirst(), radix: 16) {
|
||||||
|
let r = Double((rgbValue >> 16) & 0xFF) / 255.0
|
||||||
|
let g = Double((rgbValue >> 8) & 0xFF) / 255.0
|
||||||
|
let b = Double(rgbValue & 0xFF) / 255.0
|
||||||
|
return Color(red: r, green: g, blue: b)
|
||||||
|
}
|
||||||
|
return Color("AppRedColor") // Fallback color if no match found
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@ -25,6 +37,7 @@ struct Seek: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.opacity(visible || YatteeApp.isForPreviews ? 1 : 0)
|
.opacity(visible || YatteeApp.isForPreviews ? 1 : 0)
|
||||||
|
.animation(.easeIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
@ -51,7 +64,8 @@ struct Seek: View {
|
|||||||
if let segment = projectedSegment {
|
if let segment = projectedSegment {
|
||||||
Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor")
|
Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor")
|
||||||
.font(.system(size: playerControlsLayout.segmentFontSize))
|
.font(.system(size: playerControlsLayout.segmentFontSize))
|
||||||
.foregroundColor(Color("AppRedColor"))
|
.foregroundColor(getColor(for: segment.category))
|
||||||
|
.padding(.bottom, 3)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@ -69,7 +83,8 @@ struct Seek: View {
|
|||||||
Divider()
|
Divider()
|
||||||
Text(SponsorBlockAPI.categoryDescription(category) ?? "Sponsor")
|
Text(SponsorBlockAPI.categoryDescription(category) ?? "Sponsor")
|
||||||
.font(.system(size: playerControlsLayout.segmentFontSize))
|
.font(.system(size: playerControlsLayout.segmentFontSize))
|
||||||
.foregroundColor(Color("AppRedColor"))
|
.foregroundColor(getColor(for: category))
|
||||||
|
.padding(.bottom, 3)
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@ -117,6 +132,7 @@ struct Seek: View {
|
|||||||
var visible: Bool {
|
var visible: Bool {
|
||||||
guard !(model.lastSeekTime.isNil && !model.isSeeking) else { return false }
|
guard !(model.lastSeekTime.isNil && !model.isSeeking) else { return false }
|
||||||
if let type = model.lastSeekType, !type.presentable { return false }
|
if let type = model.lastSeekType, !type.presentable { return false }
|
||||||
|
if !showNoticeAfterSkip { if case .segmentSkip? = model.lastSeekType { return false }}
|
||||||
|
|
||||||
return !controls.presentingControls && !controls.presentingOverlays && model.presentingOSD
|
return !controls.presentingControls && !controls.presentingOverlays && model.presentingOSD
|
||||||
}
|
}
|
||||||
|
@ -51,11 +51,24 @@ struct TimelineView: View {
|
|||||||
|
|
||||||
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
||||||
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
||||||
|
@Default(.sponsorBlockColors) private var sponsorBlockColors
|
||||||
|
@Default(.sponsorBlockShowTimeWithSkipsRemoved) private var showTimeWithSkipsRemoved
|
||||||
|
@Default(.sponsorBlockShowCategoriesInTimeline) private var showCategoriesInTimeline
|
||||||
|
|
||||||
var playerControlsLayout: PlayerControlsLayout {
|
var playerControlsLayout: PlayerControlsLayout {
|
||||||
player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout
|
player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getColor(for category: String) -> Color {
|
||||||
|
if let hexString = sponsorBlockColors[category], let rgbValue = Int(hexString.dropFirst(), radix: 16) {
|
||||||
|
let r = Double((rgbValue >> 16) & 0xFF) / 255.0
|
||||||
|
let g = Double((rgbValue >> 8) & 0xFF) / 255.0
|
||||||
|
let b = Double(rgbValue & 0xFF) / 255.0
|
||||||
|
return Color(red: r, green: g, blue: b)
|
||||||
|
}
|
||||||
|
return Color("AppRedColor") // Fallback color if no match found
|
||||||
|
}
|
||||||
|
|
||||||
var chapters: [Chapter] {
|
var chapters: [Chapter] {
|
||||||
player.currentVideo?.chapters ?? []
|
player.currentVideo?.chapters ?? []
|
||||||
}
|
}
|
||||||
@ -73,13 +86,15 @@ struct TimelineView: View {
|
|||||||
Group {
|
Group {
|
||||||
VStack(spacing: 3) {
|
VStack(spacing: 3) {
|
||||||
if dragging {
|
if dragging {
|
||||||
if let segment = projectedSegment,
|
if showCategoriesInTimeline {
|
||||||
let description = SponsorBlockAPI.categoryDescription(segment.category)
|
if let segment = projectedSegment,
|
||||||
{
|
let description = SponsorBlockAPI.categoryDescription(segment.category)
|
||||||
Text(description)
|
{
|
||||||
.font(.system(size: playerControlsLayout.segmentFontSize))
|
Text(description)
|
||||||
.fixedSize()
|
.font(.system(size: playerControlsLayout.segmentFontSize))
|
||||||
.foregroundColor(Color("AppRedColor"))
|
.fixedSize()
|
||||||
|
.foregroundColor(getColor(for: segment.category))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let chapter = projectedChapter {
|
if let chapter = projectedChapter {
|
||||||
Text(chapter.title)
|
Text(chapter.title)
|
||||||
@ -145,8 +160,10 @@ struct TimelineView: View {
|
|||||||
.frame(width: (dragging ? projectedValue : current) * oneUnitWidth)
|
.frame(width: (dragging ? projectedValue : current) * oneUnitWidth)
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
|
|
||||||
segmentsLayers
|
if showCategoriesInTimeline {
|
||||||
.zIndex(2)
|
segmentsLayers
|
||||||
|
.zIndex(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
|
||||||
@ -236,7 +253,7 @@ struct TimelineView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(dragging ? playerTime.durationPlaybackTime : playerTime.withoutSegmentsPlaybackTime)
|
Text(dragging || !showTimeWithSkipsRemoved ? playerTime.durationPlaybackTime : playerTime.withoutSegmentsPlaybackTime)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 3))
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||||||
.frame(minWidth: 35)
|
.frame(minWidth: 35)
|
||||||
}
|
}
|
||||||
@ -299,7 +316,7 @@ struct TimelineView: View {
|
|||||||
ForEach(segments, id: \.uuid) { segment in
|
ForEach(segments, id: \.uuid) { segment in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.offset(x: segmentLayerHorizontalOffset(segment))
|
.offset(x: segmentLayerHorizontalOffset(segment))
|
||||||
.foregroundColor(Color("AppRedColor"))
|
.foregroundColor(getColor(for: segment.category))
|
||||||
.frame(maxHeight: height)
|
.frame(maxHeight: height)
|
||||||
.frame(width: segmentLayerWidth(segment))
|
.frame(width: segmentLayerWidth(segment))
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
struct SponsorBlockSettings: View {
|
struct SponsorBlockSettings: View {
|
||||||
|
@ObservedObject private var settings = SettingsModel.shared
|
||||||
|
|
||||||
@Default(.sponsorBlockInstance) private var sponsorBlockInstance
|
@Default(.sponsorBlockInstance) private var sponsorBlockInstance
|
||||||
@Default(.sponsorBlockCategories) private var sponsorBlockCategories
|
@Default(.sponsorBlockCategories) private var sponsorBlockCategories
|
||||||
|
@Default(.sponsorBlockColors) private var sponsorBlockColors
|
||||||
|
@Default(.sponsorBlockShowTimeWithSkipsRemoved) private var showTimeWithSkipsRemoved
|
||||||
|
@Default(.sponsorBlockShowCategoriesInTimeline) private var showCategoriesInTimeline
|
||||||
|
@Default(.sponsorBlockShowNoticeAfterSkip) private var showNoticeAfterSkip
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
sections
|
sections
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
#else
|
#else
|
||||||
List {
|
List {
|
||||||
@ -35,41 +41,70 @@ struct SponsorBlockSettings: View {
|
|||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: SettingsHeader(text: "Categories to Skip".localized()), footer: categoriesDetails) {
|
Section(header: Text("Playback")) {
|
||||||
#if os(macOS)
|
Toggle("Categories in timeline", isOn: $showCategoriesInTimeline)
|
||||||
let list = ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
Toggle("Post-skip notice", isOn: $showNoticeAfterSkip)
|
||||||
MultiselectRow(
|
Toggle("Adjusted total time", isOn: $showTimeWithSkipsRemoved)
|
||||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
}
|
||||||
selected: sponsorBlockCategories.contains(category)
|
|
||||||
) { value in
|
|
||||||
toggleCategory(category, value: value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Group {
|
Section(header: SettingsHeader(text: "Categories to Skip".localized())) {
|
||||||
if #available(macOS 12.0, *) {
|
categoryRows
|
||||||
list
|
}
|
||||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
colorSection
|
||||||
} else {
|
|
||||||
list
|
Button {
|
||||||
.listStyle(.inset)
|
settings.presentAlert(
|
||||||
}
|
Alert(
|
||||||
}
|
title: Text("Restore Default Colors?"),
|
||||||
Spacer()
|
message: Text("This action will reset all custom colors back to their original defaults. " +
|
||||||
#else
|
"Any custom color changes you've made will be lost."),
|
||||||
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
primaryButton: .destructive(Text("Restore")) {
|
||||||
MultiselectRow(
|
resetColors()
|
||||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
},
|
||||||
selected: sponsorBlockCategories.contains(category)
|
secondaryButton: .cancel()
|
||||||
) { value in
|
)
|
||||||
toggleCategory(category, value: value)
|
)
|
||||||
}
|
} label: {
|
||||||
}
|
Text("Restore Default Colors …")
|
||||||
#endif
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(footer: categoriesDetails) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var colorSection: some View {
|
||||||
|
Section(header: SettingsHeader(text: "Colors for Categories")) {
|
||||||
|
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||||
|
LazyVStack(alignment: .leading) {
|
||||||
|
ColorPicker(
|
||||||
|
SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||||
|
selection: Binding(
|
||||||
|
get: { getColor(for: category) },
|
||||||
|
set: { setColor($0, for: category) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var categoryRows: some View {
|
||||||
|
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||||
|
LazyVStack(alignment: .leading) {
|
||||||
|
MultiselectRow(
|
||||||
|
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||||
|
selected: sponsorBlockCategories.contains(category)
|
||||||
|
) { value in
|
||||||
|
toggleCategory(category, value: value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,17 +114,17 @@ struct SponsorBlockSettings: View {
|
|||||||
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||||
Text(SponsorBlockAPI.categoryDescription(category) ?? "Category")
|
Text(SponsorBlockAPI.categoryDescription(category) ?? "Category")
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
.padding(.bottom, 0.5)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.focusable()
|
.focusable()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Text(SponsorBlockAPI.categoryDetails(category) ?? "Details")
|
Text(SponsorBlockAPI.categoryDetails(category) ?? "Details")
|
||||||
.padding(.bottom, 3)
|
.padding(.bottom, 10)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.top, 3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleCategory(_ category: String, value: Bool) {
|
func toggleCategory(_ category: String, value: Bool) {
|
||||||
@ -99,6 +134,40 @@ struct SponsorBlockSettings: View {
|
|||||||
sponsorBlockCategories.insert(category)
|
sponsorBlockCategories.insert(category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getColor(for category: String) -> Color {
|
||||||
|
if let hexString = sponsorBlockColors[category], let rgbValue = Int(hexString.dropFirst(), radix: 16) {
|
||||||
|
let r = Double((rgbValue >> 16) & 0xFF) / 255.0
|
||||||
|
let g = Double((rgbValue >> 8) & 0xFF) / 255.0
|
||||||
|
let b = Double(rgbValue & 0xFF) / 255.0
|
||||||
|
return Color(red: r, green: g, blue: b)
|
||||||
|
}
|
||||||
|
return Color("AppRedColor") // Fallback color if no match found
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setColor(_ color: Color, for category: String) {
|
||||||
|
let uiColor = UIColor(color)
|
||||||
|
|
||||||
|
// swiftlint:disable no_cgfloat
|
||||||
|
var red: CGFloat = 0
|
||||||
|
var green: CGFloat = 0
|
||||||
|
var blue: CGFloat = 0
|
||||||
|
var alpha: CGFloat = 0
|
||||||
|
// swiftlint:enable no_cgfloat
|
||||||
|
|
||||||
|
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||||
|
|
||||||
|
let r = Int(red * 255.0)
|
||||||
|
let g = Int(green * 255.0)
|
||||||
|
let b = Int(blue * 255.0)
|
||||||
|
|
||||||
|
let rgbValue = (r << 16) | (g << 8) | b
|
||||||
|
sponsorBlockColors[category] = String(format: "#%06x", rgbValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetColors() {
|
||||||
|
sponsorBlockColors = SponsorBlockColors.dictionary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SponsorBlockSettings_Previews: PreviewProvider {
|
struct SponsorBlockSettings_Previews: PreviewProvider {
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
"Enter fullscreen in landscape" = "Enter fullscreen in landscape";
|
"Enter fullscreen in landscape" = "Enter fullscreen in landscape";
|
||||||
"Error" = "Error";
|
"Error" = "Error";
|
||||||
"Error when accessing playlist" = "Error when accessing playlist";
|
"Error when accessing playlist" = "Error when accessing playlist";
|
||||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video).\n";
|
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video).";
|
||||||
"Favorites" = "Favorites";
|
"Favorites" = "Favorites";
|
||||||
"Filter" = "Filter";
|
"Filter" = "Filter";
|
||||||
"Filter: active" = "Filter: active";
|
"Filter: active" = "Filter: active";
|
||||||
|
Loading…
Reference in New Issue
Block a user