what i've done so far

This commit is contained in:
eevee committed 2025-06-17 19:25:23 +03:00
1 parent 727fd664ef
commit 92e6cc7900
47 files changed
+298 -271

No files matched your search

@@ -0,0 +1,9 @@
import Foundation
extension CharacterSet {
static var urlQueryAllowedStrict: CharacterSet {
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: "/?@&=+$")
return allowed
}
}
@@ -0,0 +1,10 @@
import Foundation
extension Collection {
func dropLast(while predicate: (Element) throws -> Bool) rethrows -> SubSequence {
guard let index = try indices.reversed().first(where: { try !predicate(self[$0]) }) else {
return self[startIndex..<startIndex]
}
return self[...index]
}
}
@@ -0,0 +1,76 @@
import SwiftUI
import UIKit
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3:
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6:
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8:
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
guard UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) else {
return (0, 0, 0, 0)
}
return (r, g, b, a)
}
func lighter(by amount: CGFloat = 0.2) -> Self { Self(UIColor(self).lighter(by: amount)) }
func darker(by amount: CGFloat = 0.2) -> Self { Self(UIColor(self).darker(by: amount)) }
var brightness: CGFloat {
(
components.red * 299
+ components.green * 587
+ components.blue * 114
) / 1000
}
func normalized(_ by: CGFloat) -> Color {
brightness < 0.5
? self.lighter(by: max(by - brightness, 0))
: self.darker(by: max(brightness - by, 0))
}
var hexString: String {
String(
format: "%02X%02X%02X",
Int(components.red * 255),
Int(components.green * 255),
Int(components.blue * 255)
)
}
var uInt32: UInt32 {
UInt32(components.alpha * 255) << 24
| UInt32(components.red * 255) << 16
| UInt32(components.green * 255) << 8
| UInt32(components.blue * 255)
}
}
@@ -0,0 +1,15 @@
import Foundation
extension Data {
static func random(_ length: Int) -> Data {
return Data((0 ..< length).map { _ in UInt8.random(in: UInt8.min ... UInt8.max) })
}
var hexEncodedString: String {
map { String(format: "%02hhx", $0) }.joined()
}
static var musixmatchTokenPlaceholder: String {
"2" + self.random(53).hexEncodedString
}
}
@@ -0,0 +1,15 @@
import Foundation
extension Dictionary {
var queryString: String {
return self
.compactMap({ (key, value) -> String? in
guard let keyString = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowedStrict),
let valueString = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowedStrict) else {
return nil
}
return "\(keyString)=\(valueString)"
})
.joined(separator: "&")
}
}
@@ -0,0 +1,7 @@
import Foundation
extension Locale {
static func isInRegion(_ regionCode: String, orHasLanguage languageCode: String) -> Bool {
self.current.regionCode == regionCode || self.preferredLanguages.contains(languageCode)
}
}
@@ -0,0 +1,80 @@
import Foundation
import UIKit
import NaturalLanguage
extension String {
static func ~= (lhs: String, rhs: String) -> Bool {
lhs.firstMatch(rhs) != nil
}
var localized: String {
BundleHelper.shared.localizedString(self)
}
var uiKitLocalized: String {
let bundle = Bundle(for: UIApplication.self)
return bundle.localizedString(forKey: self, value: nil, table: nil)
}
func localizeWithFormat(_ arguments: CVarArg...) -> String {
String(format: self.localized, arguments: arguments)
}
var range: NSRange {
NSRange(self.startIndex..., in: self)
}
var strippedTrackTitle: String {
String(
self
.removeMatches("\\(.*\\)")
.removeMatches("- .*")
.trimmingCharacters(in: .whitespaces)
)
}
var isHex: Bool {
self ~= "^[a-f0-9]+$"
}
var lyricsNoteIfEmpty: String {
self.isEmpty ? "♪" : self
}
func containsInsensitive<S: StringProtocol>(_ s: S) -> Bool {
self.range(of: s, options: .caseInsensitive) != nil
}
func firstMatch(_ pattern: String) -> NSTextCheckingResult? {
try! NSRegularExpression(pattern: pattern)
.firstMatch(in: self, range: self.range)
}
func removeMatches(_ pattern: String) -> String {
try! NSRegularExpression(pattern: pattern)
.stringByReplacingMatches(
in: self,
range: self.range,
withTemplate: ""
)
}
var isCanBeRomanizedLanguage: Bool {
["ja", "ko", "z1"].contains(self) || self.contains("zh")
}
var hexadecimal: Data? {
var data = Data(capacity: count / 2)
let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in
let byteString = (self as NSString).substring(with: match!.range)
let num = UInt8(byteString, radix: 16)!
data.append(num)
}
guard data.count > 0 else { return nil }
return data
}
}
@@ -0,0 +1,20 @@
import Foundation
import NaturalLanguage
extension Array where Element == String {
var canBeRomanized: Bool {
var languageList: [NLLanguage] = []
for line in self {
if let language = NLLanguageRecognizer.dominantLanguage(for: line) {
languageList.append(language)
}
}
let canBeRomanizedLanguageCount = languageList.filter {
[.japanese, .korean, .simplifiedChinese, .traditionalChinese].contains($0)
}.count
return Double(canBeRomanizedLanguageCount) / Double(languageList.count) > 0.15
}
}
@@ -0,0 +1,22 @@
import SwiftUI
import UIKit
extension UIColor {
func mix(with target: UIColor, amount: CGFloat) -> Self {
var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0
var r2: CGFloat = 0, g2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0
self.getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
target.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
return Self(
red: r1 * (1.0 - amount) + r2 * amount,
green: g1 * (1.0 - amount) + g2 * amount,
blue: b1 * (1.0 - amount) + b2 * amount,
alpha: a1 * (1.0 - amount) + a2 * amount
)
}
func lighter(by amount: CGFloat = 0.2) -> Self { mix(with: .white, amount: amount) }
func darker(by amount: CGFloat = 0.2) -> Self { mix(with: .black, amount: amount) }
}
@@ -0,0 +1,13 @@
import UIKit
extension UIDevice {
var isIpad: Bool {
self.userInterfaceIdiom == .pad
}
var musixmatchAppId: String {
UIDevice.current.isIpad
? "mac-ios-ipad-v1.0"
: "mac-ios-v2.0"
}
}
@@ -0,0 +1,34 @@
import UIKit
extension UIView {
func hasParent(matching regex: String) -> Bool {
guard let parent = self.superview else {
return false
}
let parentClassName = NSStringFromClass(type(of: parent))
if parentClassName ~= regex {
return true
} else {
return parent.hasParent(matching: regex)
}
}
func subviews(matching regex: String) -> [UIView] {
var matchingSubviews = [UIView]()
var stack = [self]
while !stack.isEmpty {
let currentView = stack.removeLast()
for subview in currentView.subviews {
if NSStringFromClass(type(of: subview)) ~= regex {
matchingSubviews.append(subview)
}
stack.append(subview)
}
}
return matchingSubviews
}
}
@@ -0,0 +1,19 @@
import Foundation
extension URL {
var isLyrics: Bool {
self.path.contains("color-lyrics/v2")
}
var isOpenSpotifySafariExtension: Bool {
self.host == "eevee"
}
var isCustomize: Bool {
self.path.contains("v1/customize")
}
var isBootstrap: Bool {
self.path.contains("v1/bootstrap")
}
}
@@ -0,0 +1,114 @@
import Foundation
extension UserDefaults {
private static let defaults = UserDefaults.standard
private static let lyricsSourceKey = "lyricsSource"
private static let musixmatchTokenKey = "musixmatchToken"
private static let darkPopUpsKey = "darkPopUps"
private static let patchTypeKey = "patchType"
private static let overwriteConfigurationKey = "overwriteConfiguration"
private static let lyricsColorsKey = "lyricsColors"
private static let lyricsOptionsKey = "lyricsOptions"
private static let hasShownCommonIssuesTipKey = "hasShownCommonIssuesTip"
static var lyricsSource: LyricsSource {
get {
if let rawValue = defaults.object(forKey: lyricsSourceKey) as? Int {
return LyricsSource(rawValue: rawValue)!
}
return LyricsSource.defaultSource
}
set (newSource) {
defaults.set(newSource.rawValue, forKey: lyricsSourceKey)
}
}
static var musixmatchToken: String {
get {
defaults.string(forKey: musixmatchTokenKey) ?? ""
}
set (token) {
defaults.set(token, forKey: musixmatchTokenKey)
}
}
static var lyricsOptions: LyricsOptions {
get {
if let data = defaults.object(forKey: lyricsOptionsKey) as? Data,
let lyricsOptions = try? JSONDecoder().decode(LyricsOptions.self, from: data) {
return lyricsOptions
}
return LyricsOptions(
romanization: false,
musixmatchLanguage: Locale.current.languageCode ?? "",
lrclibUrl: LrclibLyricsRepository.originalApiUrl,
geniusFallback: true,
showFallbackReasons: true
)
}
set (lyricsOptions) {
defaults.set(try! JSONEncoder().encode(lyricsOptions), forKey: lyricsOptionsKey)
}
}
static var darkPopUps: Bool {
get {
defaults.object(forKey: darkPopUpsKey) as? Bool ?? true
}
set (darkPopUps) {
defaults.set(darkPopUps, forKey: darkPopUpsKey)
}
}
static var patchType: PatchType {
get {
if let rawValue = defaults.object(forKey: patchTypeKey) as? Int {
return PatchType(rawValue: rawValue) ?? .requests
}
return .notSet
}
set (patchType) {
defaults.set(patchType.rawValue, forKey: patchTypeKey)
}
}
static var overwriteConfiguration: Bool {
get {
defaults.bool(forKey: overwriteConfigurationKey)
}
set (overwriteConfiguration) {
defaults.set(overwriteConfiguration, forKey: overwriteConfigurationKey)
}
}
static var hasShownCommonIssuesTip: Bool {
get {
defaults.bool(forKey: hasShownCommonIssuesTipKey)
}
set (hasShownCommonIssuesTip) {
defaults.set(hasShownCommonIssuesTip, forKey: hasShownCommonIssuesTipKey)
}
}
static var lyricsColors: LyricsColorsSettings {
get {
if let data = defaults.object(forKey: lyricsColorsKey) as? Data {
return try! JSONDecoder().decode(LyricsColorsSettings.self, from: data)
}
return LyricsColorsSettings(
displayOriginalColors: true,
useStaticColor: false,
staticColor: "",
normalizationFactor: 0.5
)
}
set (lyricsColors) {
defaults.set(try! JSONEncoder().encode(lyricsColors), forKey: lyricsColorsKey)
}
}
}