├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Localizable.xcstrings ├── Lyric Fever.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── SpotifyLyricsInMenubar.xcscheme ├── README.md ├── SongObject+CoreDataClass.swift ├── SongObject+CoreDataProperties.swift ├── SpotifyLyricsInMenubar ├── AppleMusicScripting.swift ├── Assets.xcassets │ ├── 30.imageset │ │ ├── 30.png │ │ └── Contents.json │ ├── 40.imageset │ │ ├── 40.png │ │ └── Contents.json │ ├── 50.imageset │ │ ├── 50.png │ │ └── Contents.json │ ├── 60.imageset │ │ ├── 60.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Group 1-4-2.png │ │ ├── Icon-1024.png │ │ ├── Icon-128.png │ │ ├── Icon-256 1.png │ │ ├── Icon-256.png │ │ ├── Icon-32 1.png │ │ ├── Icon-32.png │ │ ├── Icon-512 1.png │ │ ├── Icon-512.png │ │ └── Icon-64.png │ ├── Contents.json │ ├── checkAgain.imageset │ │ ├── Contents.json │ │ └── checkAgain.png │ ├── hi.imageset │ │ ├── Contents.json │ │ └── Frame 1-12 1.png │ ├── music.imageset │ │ ├── Contents.json │ │ └── Untitled.png │ └── spotify.imageset │ │ ├── Contents.json │ │ └── spotify.png ├── FloatingPanel.swift ├── FullscreenButtonIconStyle.swift ├── FullscreenView.swift ├── Info.plist ├── KaraokeSettings.swift ├── KaraokeView.swift ├── Lyrics.xcdatamodeld │ └── Lyrics.xcdatamodel │ │ └── contents ├── LyricsParser │ ├── Extensions.swift │ ├── LyricsHeader.swift │ └── LyricsParser.swift ├── Model │ ├── ColorStop.swift │ ├── GradientParams.swift │ └── Uniforms.swift ├── MulticolorGradient.swift ├── OnboardingWindow.swift ├── OverridePrint.swift ├── Shaders │ └── MulticolorGradientShader.metal ├── SpotifyLyricsInMenubar.entitlements ├── SpotifyLyricsInMenubarApp.swift ├── SpotifyScripting.swift ├── Update22Window.swift ├── WebLoginView.swift ├── lyricJsonStruct.swift └── viewModel.swift ├── appcast.xml ├── crossfade.gif ├── logo.png ├── newPermissionMac.gif ├── screenshot2.png ├── spotifylogin.gif └── superShy.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | SpotifyLyricsInMenubar/amplitudeKey.swift 6 | ## User settings 7 | xcuserdata/ 8 | 9 | .DS_Store 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | Lyric Fever.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | aviwad@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Avi Wadhwa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "" : { 5 | "localizations" : { 6 | "zh-Hans" : { 7 | "stringUnit" : { 8 | "state" : "new", 9 | "value" : "" 10 | } 11 | } 12 | } 13 | }, 14 | "%lld Characters" : { 15 | "localizations" : { 16 | "zh-Hans" : { 17 | "stringUnit" : { 18 | "state" : "translated", 19 | "value" : "%lld 字符" 20 | } 21 | } 22 | } 23 | }, 24 | "%lld%%" : { 25 | "localizations" : { 26 | "zh-Hans" : { 27 | "stringUnit" : { 28 | "state" : "translated", 29 | "value" : "%lld%%" 30 | } 31 | } 32 | } 33 | }, 34 | "⚠️ Complete Setup (Click Settings)" : { 35 | "localizations" : { 36 | "zh-Hans" : { 37 | "stringUnit" : { 38 | "state" : "translated", 39 | "value" : "⚠️ 完成设置 (点击设置)" 40 | } 41 | } 42 | } 43 | }, 44 | "⚠️ Please Update (Click Check Updates)" : { 45 | "localizations" : { 46 | "zh-Hans" : { 47 | "stringUnit" : { 48 | "state" : "translated", 49 | "value" : "⚠️ 请更新 (点击检查更新)" 50 | } 51 | } 52 | } 53 | }, 54 | "AirPlay Audio Delay" : { 55 | 56 | }, 57 | "Animate on startup" : { 58 | 59 | }, 60 | "API Key: Advanced" : { 61 | "localizations" : { 62 | "zh-Hans" : { 63 | "stringUnit" : { 64 | "state" : "translated", 65 | "value" : "API Key: 高级" 66 | } 67 | } 68 | } 69 | }, 70 | "Apple Music" : { 71 | "localizations" : { 72 | "zh-Hans" : { 73 | "stringUnit" : { 74 | "state" : "translated", 75 | "value" : "Apple Music" 76 | } 77 | } 78 | } 79 | }, 80 | "Back" : { 81 | "localizations" : { 82 | "zh-Hans" : { 83 | "stringUnit" : { 84 | "state" : "translated", 85 | "value" : "返回" 86 | } 87 | } 88 | } 89 | }, 90 | "Blur surrounding lyrics" : { 91 | 92 | }, 93 | "Change fullscreen settings here!" : { 94 | 95 | }, 96 | "Check for Lyrics Again" : { 97 | "localizations" : { 98 | "zh-Hans" : { 99 | "stringUnit" : { 100 | "state" : "translated", 101 | "value" : "重新搜索歌词" 102 | } 103 | } 104 | } 105 | }, 106 | "Check for Updates…" : { 107 | "localizations" : { 108 | "zh-Hans" : { 109 | "stringUnit" : { 110 | "state" : "translated", 111 | "value" : "检查更新…" 112 | } 113 | } 114 | } 115 | }, 116 | "Close" : { 117 | "localizations" : { 118 | "zh-Hans" : { 119 | "stringUnit" : { 120 | "state" : "translated", 121 | "value" : "关闭" 122 | } 123 | } 124 | } 125 | }, 126 | "Decrease Offset to %lld" : { 127 | "localizations" : { 128 | "zh-Hans" : { 129 | "stringUnit" : { 130 | "state" : "translated", 131 | "value" : "将延迟设置为 %lld" 132 | } 133 | } 134 | } 135 | }, 136 | "Decrease Size to %lld" : { 137 | "localizations" : { 138 | "zh-Hans" : { 139 | "stringUnit" : { 140 | "state" : "translated", 141 | "value" : "菜单栏歌词设置为 %lld" 142 | } 143 | } 144 | } 145 | }, 146 | "Decrease volume by 5 (Down Arrow)" : { 147 | "localizations" : { 148 | "zh-Hans" : { 149 | "stringUnit" : { 150 | "state" : "translated", 151 | "value" : "音量减少 5 (方向键下)" 152 | } 153 | } 154 | } 155 | }, 156 | "Delete Lyrics (wrong lyrics)" : { 157 | "localizations" : { 158 | "zh-Hans" : { 159 | "stringUnit" : { 160 | "state" : "translated", 161 | "value" : "删除歌词(错误歌词)" 162 | } 163 | } 164 | } 165 | }, 166 | "Display fullscreen options" : { 167 | 168 | }, 169 | "Done" : { 170 | "localizations" : { 171 | "zh-Hans" : { 172 | "stringUnit" : { 173 | "state" : "translated", 174 | "value" : "完成" 175 | } 176 | } 177 | } 178 | }, 179 | "Email me at [aviwad@gmail.com](mailto:aviwad@gmail.com) for any support\n⚠️ Disclaimer: I do not own the rights to Spotify or the lyric content presented.\nMusixmatch and Spotify own all rights to the lyrics.\n [Lyric Fever GitHub](https://github.com/aviwad/LyricFever)\nVersion 2.2" : { 180 | "localizations" : { 181 | "zh-Hans" : { 182 | "stringUnit" : { 183 | "state" : "translated", 184 | "value" : "请给我发送邮件 [aviwad@gmail.com](mailto:aviwad@gmail.com) 获取帮助\n⚠️ 免责声明: 我不拥有Spotify或展示歌词的版权。\n歌词所有权归Musixmatch和Spotify所有。\n [Lyric Fever GitHub](https://github.com/aviwad/LyricFever)\nVersion 2.2" 185 | } 186 | } 187 | } 188 | }, 189 | "Enter your SP_DC Cookie Here" : { 190 | "localizations" : { 191 | "zh-Hans" : { 192 | "stringUnit" : { 193 | "state" : "translated", 194 | "value" : "在此输入您的 SP_DC Cookie" 195 | } 196 | } 197 | } 198 | }, 199 | "Font Selected: %@, Size: %lld" : { 200 | "localizations" : { 201 | "zh-Hans" : { 202 | "stringUnit" : { 203 | "state" : "translated", 204 | "value" : "所选字体: %@, 大小: %lld" 205 | } 206 | } 207 | } 208 | }, 209 | "Fullscreen" : { 210 | "localizations" : { 211 | "zh-Hans" : { 212 | "stringUnit" : { 213 | "state" : "translated", 214 | "value" : "全屏歌词" 215 | } 216 | } 217 | } 218 | }, 219 | "Give Apple Music Library Permissions" : { 220 | "localizations" : { 221 | "zh-Hans" : { 222 | "stringUnit" : { 223 | "state" : "translated", 224 | "value" : "授予访问 Apple Music 音乐库权限" 225 | } 226 | } 227 | } 228 | }, 229 | "Give Apple Music Permissions" : { 230 | "localizations" : { 231 | "zh-Hans" : { 232 | "stringUnit" : { 233 | "state" : "translated", 234 | "value" : "授予访问 Apple Music权限" 235 | } 236 | } 237 | } 238 | }, 239 | "Give Spotify Permissions" : { 240 | "localizations" : { 241 | "zh-Hans" : { 242 | "stringUnit" : { 243 | "state" : "translated", 244 | "value" : "授予访问 Spotify 权限" 245 | } 246 | } 247 | } 248 | }, 249 | "Hi %@!" : { 250 | 251 | }, 252 | "Hide Karaoke window when mouse passes by" : { 253 | "localizations" : { 254 | "zh-Hans" : { 255 | "stringUnit" : { 256 | "state" : "translated", 257 | "value" : "鼠标经过时隐藏 K歌 窗口" 258 | } 259 | } 260 | } 261 | }, 262 | "Hide lyrics (⌘ + H)" : { 263 | "localizations" : { 264 | "zh-Hans" : { 265 | "stringUnit" : { 266 | "state" : "translated", 267 | "value" : "隐藏歌词 (⌘ + H)" 268 | } 269 | } 270 | } 271 | }, 272 | "Hide translations (⌘ + T)" : { 273 | "localizations" : { 274 | "zh-Hans" : { 275 | "stringUnit" : { 276 | "state" : "translated", 277 | "value" : "隐藏翻译 (⌘ + T)" 278 | } 279 | } 280 | } 281 | }, 282 | "I download lyrics from Spotify (and use LRCLIB and NetEase as backups)" : { 283 | "localizations" : { 284 | "zh-Hans" : { 285 | "stringUnit" : { 286 | "state" : "translated", 287 | "value" : "从Spotify下载歌词(备用渠道是LRCLIB和网易云音乐)" 288 | } 289 | } 290 | } 291 | }, 292 | "Increase Offset to %lld" : { 293 | "localizations" : { 294 | "zh-Hans" : { 295 | "stringUnit" : { 296 | "state" : "translated", 297 | "value" : "将延迟设置为 %lld" 298 | } 299 | } 300 | } 301 | }, 302 | "Increase Size to %lld " : { 303 | "localizations" : { 304 | "zh-Hans" : { 305 | "stringUnit" : { 306 | "state" : "translated", 307 | "value" : "菜单栏歌词设置为 %lld " 308 | } 309 | } 310 | } 311 | }, 312 | "Increase volume by 5 (Up Arrow)" : { 313 | "localizations" : { 314 | "zh-Hans" : { 315 | "stringUnit" : { 316 | "state" : "translated", 317 | "value" : "音量增加 5(方向键上)" 318 | } 319 | } 320 | } 321 | }, 322 | "Karaoke Background Appearance" : { 323 | "localizations" : { 324 | "zh-Hans" : { 325 | "stringUnit" : { 326 | "state" : "translated", 327 | "value" : "K 歌 歌词背景样式" 328 | } 329 | } 330 | } 331 | }, 332 | "Karaoke Behaviour" : { 333 | "localizations" : { 334 | "zh-Hans" : { 335 | "stringUnit" : { 336 | "state" : "translated", 337 | "value" : "K 歌模式设置" 338 | } 339 | } 340 | } 341 | }, 342 | "Karaoke Font Appearance" : { 343 | "localizations" : { 344 | "zh-Hans" : { 345 | "stringUnit" : { 346 | "state" : "translated", 347 | "value" : "K歌 歌词字体样式" 348 | } 349 | } 350 | } 351 | }, 352 | "Karaoke Mode" : { 353 | "localizations" : { 354 | "zh-Hans" : { 355 | "stringUnit" : { 356 | "state" : "translated", 357 | "value" : "K 歌模式" 358 | } 359 | } 360 | } 361 | }, 362 | "Karaoke Mode (Enable Show Lyrics)" : { 363 | "localizations" : { 364 | "zh-Hans" : { 365 | "stringUnit" : { 366 | "state" : "translated", 367 | "value" : "K 歌模式(启用显示歌词)" 368 | } 369 | } 370 | } 371 | }, 372 | "Karaoke Window" : { 373 | "localizations" : { 374 | "zh-Hans" : { 375 | "stringUnit" : { 376 | "state" : "translated", 377 | "value" : "K 歌模式设置" 378 | } 379 | } 380 | } 381 | }, 382 | "Launch at Login" : { 383 | "localizations" : { 384 | "zh-Hans" : { 385 | "stringUnit" : { 386 | "state" : "translated", 387 | "value" : "开机自动启动" 388 | } 389 | } 390 | } 391 | }, 392 | "Log Out" : { 393 | "localizations" : { 394 | "zh-Hans" : { 395 | "stringUnit" : { 396 | "state" : "translated", 397 | "value" : "注销" 398 | } 399 | } 400 | } 401 | }, 402 | "Lyric Fever Search for %@" : { 403 | 404 | }, 405 | "Lyric Fever: Fullscreen" : { 406 | "localizations" : { 407 | "zh-Hans" : { 408 | "stringUnit" : { 409 | "state" : "translated", 410 | "value" : "Lyric Fever: 全屏" 411 | } 412 | } 413 | } 414 | }, 415 | "Lyric Fever: Onboarding" : { 416 | "localizations" : { 417 | "zh-Hans" : { 418 | "stringUnit" : { 419 | "state" : "translated", 420 | "value" : "Lyric Fever: 引导" 421 | } 422 | } 423 | } 424 | }, 425 | "Lyric Fever: Update 2.1" : { 426 | "extractionState" : "stale", 427 | "localizations" : { 428 | "zh-Hans" : { 429 | "stringUnit" : { 430 | "state" : "translated", 431 | "value" : "Lyric Fever: 更新 2.1" 432 | } 433 | } 434 | } 435 | }, 436 | "Lyric Fever: Update 2.2" : { 437 | 438 | }, 439 | "Lyrics Found 😃" : { 440 | "localizations" : { 441 | "zh-Hans" : { 442 | "stringUnit" : { 443 | "state" : "translated", 444 | "value" : "找到歌词 😃" 445 | } 446 | } 447 | } 448 | }, 449 | "Main Settings" : { 450 | "localizations" : { 451 | "zh-Hans" : { 452 | "stringUnit" : { 453 | "state" : "translated", 454 | "value" : "主设置" 455 | } 456 | } 457 | } 458 | }, 459 | "Menubar Size is %lld" : { 460 | "localizations" : { 461 | "zh-Hans" : { 462 | "stringUnit" : { 463 | "state" : "translated", 464 | "value" : "当前菜单歌词大小为 %lld" 465 | } 466 | } 467 | } 468 | }, 469 | "Next" : { 470 | "localizations" : { 471 | "zh-Hans" : { 472 | "stringUnit" : { 473 | "state" : "translated", 474 | "value" : "下一步" 475 | } 476 | } 477 | } 478 | }, 479 | "No Lyrics (Couldn't find Spotify ID) ☹️" : { 480 | "localizations" : { 481 | "zh-Hans" : { 482 | "stringUnit" : { 483 | "state" : "translated", 484 | "value" : "无歌词(找不到 Spotify ID) ☹️" 485 | } 486 | } 487 | } 488 | }, 489 | "No Lyrics Found ☹️" : { 490 | "localizations" : { 491 | "zh-Hans" : { 492 | "stringUnit" : { 493 | "state" : "translated", 494 | "value" : "未找到歌词 ☹️" 495 | } 496 | } 497 | } 498 | }, 499 | "No Translation ☹️" : { 500 | "localizations" : { 501 | "zh-Hans" : { 502 | "stringUnit" : { 503 | "state" : "translated", 504 | "value" : "无翻译 ☹️" 505 | } 506 | } 507 | } 508 | }, 509 | "Now Paused: %@ - %@" : { 510 | "comment" : "Now Paused: Song - Artist", 511 | "localizations" : { 512 | "zh-Hans" : { 513 | "stringUnit" : { 514 | "state" : "translated", 515 | "value" : "暂停: %@ - %@" 516 | } 517 | } 518 | } 519 | }, 520 | "Now Playing: %@ - %@" : { 521 | "comment" : "Now Playing: Song - Artist", 522 | "localizations" : { 523 | "zh-Hans" : { 524 | "stringUnit" : { 525 | "state" : "translated", 526 | "value" : "播放: %@ - %@" 527 | } 528 | } 529 | } 530 | }, 531 | "Offset is %lld ms" : { 532 | "localizations" : { 533 | "zh-Hans" : { 534 | "stringUnit" : { 535 | "state" : "translated", 536 | "value" : "当前延迟为 %lld 毫秒" 537 | } 538 | } 539 | } 540 | }, 541 | "Opacity Level:" : { 542 | "localizations" : { 543 | "zh-Hans" : { 544 | "stringUnit" : { 545 | "state" : "translated", 546 | "value" : "透明度等级:" 547 | } 548 | } 549 | } 550 | }, 551 | "Opacity Level: %lld%%" : { 552 | "localizations" : { 553 | "zh-Hans" : { 554 | "stringUnit" : { 555 | "state" : "translated", 556 | "value" : "透明度等级: %lld%%" 557 | } 558 | } 559 | } 560 | }, 561 | "Open %@!" : { 562 | "localizations" : { 563 | "zh-Hans" : { 564 | "stringUnit" : { 565 | "state" : "translated", 566 | "value" : "打开 %@!" 567 | } 568 | } 569 | } 570 | }, 571 | "Open Automation Panel" : { 572 | "localizations" : { 573 | "zh-Hans" : { 574 | "stringUnit" : { 575 | "state" : "translated", 576 | "value" : "打开自动化面板" 577 | } 578 | } 579 | } 580 | }, 581 | "Open Music Panel" : { 582 | "localizations" : { 583 | "zh-Hans" : { 584 | "stringUnit" : { 585 | "state" : "translated", 586 | "value" : "打开音乐面板" 587 | } 588 | } 589 | } 590 | }, 591 | "Open Spotify on the Web" : { 592 | "localizations" : { 593 | "zh-Hans" : { 594 | "stringUnit" : { 595 | "state" : "translated", 596 | "value" : "在网页中打开Spotify" 597 | } 598 | } 599 | } 600 | }, 601 | "Open Spotify!" : { 602 | "extractionState" : "manual", 603 | "localizations" : { 604 | "zh-Hans" : { 605 | "stringUnit" : { 606 | "state" : "translated", 607 | "value" : "打开 Spotify!" 608 | } 609 | } 610 | } 611 | }, 612 | "Pause (spacebar)" : { 613 | "localizations" : { 614 | "zh-Hans" : { 615 | "stringUnit" : { 616 | "state" : "translated", 617 | "value" : "暂停 (空格键)" 618 | } 619 | } 620 | } 621 | }, 622 | "Pause animations (saves battery) (⌘ + A)" : { 623 | "localizations" : { 624 | "zh-Hans" : { 625 | "stringUnit" : { 626 | "state" : "translated", 627 | "value" : "关闭动画(节省电量) (⌘ + A)" 628 | } 629 | } 630 | } 631 | }, 632 | "Play (spacebar)" : { 633 | "localizations" : { 634 | "zh-Hans" : { 635 | "stringUnit" : { 636 | "state" : "translated", 637 | "value" : "播放(空格键)" 638 | } 639 | } 640 | } 641 | }, 642 | "Please download the [official Spotify desktop client](https://www.spotify.com/in-en/download/mac/)" : { 643 | "extractionState" : "manual", 644 | "localizations" : { 645 | "zh-Hans" : { 646 | "stringUnit" : { 647 | "state" : "translated", 648 | "value" : "请下载 [Spotify 官方桌面客户端](https://www.spotify.com/in-en/download/mac/)" 649 | } 650 | } 651 | } 652 | }, 653 | "Please give required permissions!" : { 654 | "extractionState" : "manual", 655 | "localizations" : { 656 | "zh-Hans" : { 657 | "stringUnit" : { 658 | "state" : "translated", 659 | "value" : "请授权必要的权限!" 660 | } 661 | } 662 | } 663 | }, 664 | "Please give us Apple Music Library permissions!" : { 665 | "extractionState" : "manual", 666 | "localizations" : { 667 | "zh-Hans" : { 668 | "stringUnit" : { 669 | "state" : "translated", 670 | "value" : "请授权 Apple Music 库权限!" 671 | } 672 | } 673 | } 674 | }, 675 | "Please give us Apple Music permissions!" : { 676 | "extractionState" : "manual", 677 | "localizations" : { 678 | "zh-Hans" : { 679 | "stringUnit" : { 680 | "state" : "translated", 681 | "value" : "请授权 Apple Music 权限!" 682 | } 683 | } 684 | } 685 | }, 686 | "Please give us required permissions!" : { 687 | "extractionState" : "manual", 688 | "localizations" : { 689 | "zh-Hans" : { 690 | "stringUnit" : { 691 | "state" : "translated", 692 | "value" : "请授权必要的权限!" 693 | } 694 | } 695 | } 696 | }, 697 | "Please log into Spotify" : { 698 | "localizations" : { 699 | "zh-Hans" : { 700 | "stringUnit" : { 701 | "state" : "translated", 702 | "value" : "请登录 Spotify" 703 | } 704 | } 705 | } 706 | }, 707 | "Please open Apple Music!" : { 708 | "extractionState" : "manual", 709 | "localizations" : { 710 | "zh-Hans" : { 711 | "stringUnit" : { 712 | "state" : "translated", 713 | "value" : "请打开 Apple Music!" 714 | } 715 | } 716 | } 717 | }, 718 | "Please open Spotify!" : { 719 | "extractionState" : "manual", 720 | "localizations" : { 721 | "zh-Hans" : { 722 | "stringUnit" : { 723 | "state" : "translated", 724 | "value" : "请打开 Spotify!" 725 | } 726 | } 727 | } 728 | }, 729 | "Please pick between Spotify and Apple Music" : { 730 | "localizations" : { 731 | "zh-Hans" : { 732 | "stringUnit" : { 733 | "state" : "translated", 734 | "value" : "请选择 Spotify 或 Apple Music" 735 | } 736 | } 737 | } 738 | }, 739 | "Quit" : { 740 | "localizations" : { 741 | "zh-Hans" : { 742 | "stringUnit" : { 743 | "state" : "translated", 744 | "value" : "退出" 745 | } 746 | } 747 | } 748 | }, 749 | "Refresh Lyrics" : { 750 | "localizations" : { 751 | "zh-Hans" : { 752 | "stringUnit" : { 753 | "state" : "translated", 754 | "value" : "刷新歌词" 755 | } 756 | } 757 | } 758 | }, 759 | "Reset to default" : { 760 | "localizations" : { 761 | "zh-Hans" : { 762 | "stringUnit" : { 763 | "state" : "translated", 764 | "value" : "重置设置" 765 | } 766 | } 767 | } 768 | }, 769 | "Romanize" : { 770 | "localizations" : { 771 | "zh-Hans" : { 772 | "stringUnit" : { 773 | "state" : "translated", 774 | "value" : "拼音化文字" 775 | } 776 | } 777 | } 778 | }, 779 | "Search Window" : { 780 | 781 | }, 782 | "Select a Font:" : { 783 | "localizations" : { 784 | "zh-Hans" : { 785 | "stringUnit" : { 786 | "state" : "translated", 787 | "value" : "选择字体:" 788 | } 789 | } 790 | } 791 | }, 792 | "Set a background color" : { 793 | "localizations" : { 794 | "zh-Hans" : { 795 | "stringUnit" : { 796 | "state" : "translated", 797 | "value" : "设置背景颜色" 798 | } 799 | } 800 | } 801 | }, 802 | "Set the Lyric Size" : { 803 | "localizations" : { 804 | "zh-Hans" : { 805 | "stringUnit" : { 806 | "state" : "translated", 807 | "value" : "设置歌词大小" 808 | } 809 | } 810 | } 811 | }, 812 | "Settings (New Karaoke Settings!)" : { 813 | "localizations" : { 814 | "zh-Hans" : { 815 | "stringUnit" : { 816 | "state" : "translated", 817 | "value" : "设置(新增 K 歌设置)" 818 | } 819 | } 820 | } 821 | }, 822 | "Share Spotify link" : { 823 | 824 | }, 825 | "Show Lyrics" : { 826 | "localizations" : { 827 | "zh-Hans" : { 828 | "stringUnit" : { 829 | "state" : "translated", 830 | "value" : "显示歌词" 831 | } 832 | } 833 | } 834 | }, 835 | "Show lyrics (⌘ + H)" : { 836 | "localizations" : { 837 | "zh-Hans" : { 838 | "stringUnit" : { 839 | "state" : "translated", 840 | "value" : "显示歌词 (⌘ + H)" 841 | } 842 | } 843 | } 844 | }, 845 | "Show multilingual lyrics when translating in Karaoke window" : { 846 | "localizations" : { 847 | "zh-Hans" : { 848 | "stringUnit" : { 849 | "state" : "translated", 850 | "value" : "K 歌窗口翻译时,显示多语言" 851 | } 852 | } 853 | } 854 | }, 855 | "Show Song Details in Menubar" : { 856 | "localizations" : { 857 | "zh-Hans" : { 858 | "stringUnit" : { 859 | "state" : "translated", 860 | "value" : "在菜单栏显示歌曲详情" 861 | } 862 | } 863 | } 864 | }, 865 | "Spotify" : { 866 | "localizations" : { 867 | "zh-Hans" : { 868 | "stringUnit" : { 869 | "state" : "translated", 870 | "value" : "Spotify" 871 | } 872 | } 873 | } 874 | }, 875 | "Spotify Connect Audio Delay" : { 876 | "localizations" : { 877 | "zh-Hans" : { 878 | "stringUnit" : { 879 | "state" : "translated", 880 | "value" : "Spotify 连接音频延迟" 881 | } 882 | } 883 | } 884 | }, 885 | "Spotify Login" : { 886 | "localizations" : { 887 | "zh-Hans" : { 888 | "stringUnit" : { 889 | "state" : "translated", 890 | "value" : "Spotify 登录" 891 | } 892 | } 893 | } 894 | }, 895 | "This depends on how much free space you have in your menu bar!" : { 896 | "localizations" : { 897 | "zh-Hans" : { 898 | "stringUnit" : { 899 | "state" : "translated", 900 | "value" : "这受限于您菜单栏的可用空间!" 901 | } 902 | } 903 | } 904 | }, 905 | "Translate lyrics (⌘ + T)" : { 906 | "localizations" : { 907 | "zh-Hans" : { 908 | "stringUnit" : { 909 | "state" : "translated", 910 | "value" : "翻译歌词 (⌘ + T)" 911 | } 912 | } 913 | } 914 | }, 915 | "Translate To %@" : { 916 | "localizations" : { 917 | "zh-Hans" : { 918 | "stringUnit" : { 919 | "state" : "translated", 920 | "value" : "翻译为 %@" 921 | } 922 | } 923 | } 924 | }, 925 | "Translated Lyrics 😃" : { 926 | "localizations" : { 927 | "zh-Hans" : { 928 | "stringUnit" : { 929 | "state" : "translated", 930 | "value" : "翻译歌词 😃" 931 | } 932 | } 933 | } 934 | }, 935 | "Translation Help" : { 936 | "localizations" : { 937 | "zh-Hans" : { 938 | "stringUnit" : { 939 | "state" : "translated", 940 | "value" : "翻译帮助" 941 | } 942 | } 943 | } 944 | }, 945 | "Truncation Length" : { 946 | "localizations" : { 947 | "zh-Hans" : { 948 | "stringUnit" : { 949 | "state" : "translated", 950 | "value" : "菜单栏歌词大小" 951 | } 952 | } 953 | } 954 | }, 955 | "Unpause animations (uses battery) (⌘ + A)" : { 956 | "localizations" : { 957 | "zh-Hans" : { 958 | "stringUnit" : { 959 | "state" : "translated", 960 | "value" : "启用动画(耗电增加) (⌘ + A)" 961 | } 962 | } 963 | } 964 | }, 965 | "Update to macOS 14.0 to use fullscreen" : { 966 | "localizations" : { 967 | "zh-Hans" : { 968 | "stringUnit" : { 969 | "state" : "translated", 970 | "value" : "更新至 macOS 14.0 以使用全屏歌词" 971 | } 972 | } 973 | } 974 | }, 975 | "Update to macOS 15 to enable translation" : { 976 | "localizations" : { 977 | "zh-Hans" : { 978 | "stringUnit" : { 979 | "state" : "translated", 980 | "value" : "更新至 macOS 15 以启用翻译" 981 | } 982 | } 983 | } 984 | }, 985 | "Upload Local LRC File" : { 986 | "localizations" : { 987 | "zh-Hans" : { 988 | "stringUnit" : { 989 | "state" : "translated", 990 | "value" : "上传本地歌词文件" 991 | } 992 | } 993 | } 994 | }, 995 | "Use album color for Karaoke window" : { 996 | "localizations" : { 997 | "zh-Hans" : { 998 | "stringUnit" : { 999 | "state" : "translated", 1000 | "value" : "使用专辑颜色作为 K 歌窗口颜色" 1001 | } 1002 | } 1003 | } 1004 | }, 1005 | "Welcome to Lyric Fever! 🎉" : { 1006 | "localizations" : { 1007 | "zh-Hans" : { 1008 | "stringUnit" : { 1009 | "state" : "translated", 1010 | "value" : "欢迎使用 Lyric Fever! 🎉" 1011 | } 1012 | } 1013 | } 1014 | }, 1015 | "WRONG SP DC COOKIE TRY AGAIN ⚠️" : { 1016 | "localizations" : { 1017 | "zh-Hans" : { 1018 | "stringUnit" : { 1019 | "state" : "translated", 1020 | "value" : "SP DC Cookie 错误,请重试 ⚠️" 1021 | } 1022 | } 1023 | } 1024 | }, 1025 | "You're Logged In 🙂" : { 1026 | "localizations" : { 1027 | "zh-Hans" : { 1028 | "stringUnit" : { 1029 | "state" : "translated", 1030 | "value" : "您已登录 🙂" 1031 | } 1032 | } 1033 | } 1034 | } 1035 | }, 1036 | "version" : "1.0" 1037 | } 1038 | -------------------------------------------------------------------------------- /Lyric Fever.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8F0849342CB5D1C100644244 /* KaraokeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0849332CB5D1BD00644244 /* KaraokeView.swift */; }; 11 | 8F10AC8E2D7D04DC0036664A /* OverridePrint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F10AC8D2D7D04D90036664A /* OverridePrint.swift */; }; 12 | 8F1979952DA72D49008C113D /* IPADic in Frameworks */ = {isa = PBXBuildFile; productRef = 8F1979942DA72D49008C113D /* IPADic */; }; 13 | 8F1979972DA72D49008C113D /* Mecab-Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 8F1979962DA72D49008C113D /* Mecab-Swift */; }; 14 | 8F28BFD42D578456002E2CE6 /* KaraokeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F28BFD32D578456002E2CE6 /* KaraokeSettings.swift */; }; 15 | 8F28FDC22AA25D2700439D8D /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 8F28FDC12AA25D2700439D8D /* Sparkle */; }; 16 | 8F2D534D2D6C1B50006F33B0 /* LyricsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2D53492D6C1B50006F33B0 /* LyricsParser.swift */; }; 17 | 8F2D53512D6C1B88006F33B0 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2D534E2D6C1B88006F33B0 /* Extensions.swift */; }; 18 | 8F2D53532D6C1D29006F33B0 /* LyricsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2D53522D6C1D29006F33B0 /* LyricsHeader.swift */; }; 19 | 8F39ED8E2A78EB5900574203 /* lyricJsonStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F39ED8D2A78EB5900574203 /* lyricJsonStruct.swift */; }; 20 | 8F4F49522A7EC32D00097888 /* (null) in Sources */ = {isa = PBXBuildFile; }; 21 | 8F4F495D2A7FB3D400097888 /* SongObject+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F4F495B2A7FB3D400097888 /* SongObject+CoreDataClass.swift */; }; 22 | 8F4F495E2A7FB3D400097888 /* SongObject+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F4F495C2A7FB3D400097888 /* SongObject+CoreDataProperties.swift */; }; 23 | 8F58B8CF2C55AEB1009ADA1A /* FullscreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F58B8CE2C55AEB1009ADA1A /* FullscreenView.swift */; }; 24 | 8F6BD2952A8A61C9008BBF88 /* AmplitudeSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 8F6BD2942A8A61C9008BBF88 /* AmplitudeSwift */; }; 25 | 8F6BD2972A8A6278008BBF88 /* amplitudeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6BD2962A8A6278008BBF88 /* amplitudeKey.swift */; }; 26 | 8F6BD2992A8A6B7D008BBF88 /* viewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6BD2982A8A6B7D008BBF88 /* viewModel.swift */; }; 27 | 8F8196872D93CCDC00F03741 /* StringMetric in Frameworks */ = {isa = PBXBuildFile; productRef = 8F8196862D93CCDC00F03741 /* StringMetric */; }; 28 | 8F8196892D93E6DA00F03741 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8F8196882D93E6DA00F03741 /* Localizable.xcstrings */; }; 29 | 8F832CAD2D6BEC52008C1C2B /* FontPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 8F832CAC2D6BEC52008C1C2B /* FontPicker */; }; 30 | 8F85A8222BBFD9F6004A774D /* AppleMusicScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F85A8212BBFD9F6004A774D /* AppleMusicScripting.swift */; }; 31 | 8F89E1112C90DA4900F511EB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 8F89E1102C90DA4900F511EB /* LaunchAtLogin */; }; 32 | 8F9193D82CD1A2CA002B24DA /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F9193D72CD1A2C7002B24DA /* FloatingPanel.swift */; }; 33 | 8F91CD652DCB296F00FDF8DC /* FullscreenButtonIconStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F91CD642DCB296600FDF8DC /* FullscreenButtonIconStyle.swift */; }; 34 | 8FA436AE2BC49D480072016C /* newPermissionMac.gif in Resources */ = {isa = PBXBuildFile; fileRef = 8FA436AD2BC49D480072016C /* newPermissionMac.gif */; }; 35 | 8FA4E9782CE73DB900CEEF35 /* ColorKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8FA4E9772CE73DB900CEEF35 /* ColorKit */; }; 36 | 8FBA5E042BEADEC9000E4DEF /* WebLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FBA5E032BEADEC9000E4DEF /* WebLoginView.swift */; }; 37 | 8FC8E9492A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC8E9482A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift */; }; 38 | 8FC8E94D2A704EED00F69915 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8FC8E94C2A704EED00F69915 /* Assets.xcassets */; }; 39 | 8FCFD1C32AE35DEA00B22023 /* spotifylogin.gif in Resources */ = {isa = PBXBuildFile; fileRef = 8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */; }; 40 | 8FE454282A8916C30039EFA7 /* SpotifyScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE454272A8916C30039EFA7 /* SpotifyScripting.swift */; }; 41 | 8FEF70F72D8B5CE40069AC01 /* Update22Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FEF70F62D8B5CE40069AC01 /* Update22Window.swift */; }; 42 | 8FF184ED2D626F1E0026D2A2 /* CompactSlider in Frameworks */ = {isa = PBXBuildFile; productRef = 8FF184EC2D626F1E0026D2A2 /* CompactSlider */; }; 43 | 8FF59E2E2A798D2B00F0A382 /* Lyrics.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8FF59E2C2A798D2B00F0A382 /* Lyrics.xcdatamodeld */; }; 44 | 8FF9ADA02C5B426C00A57A75 /* MulticolorGradientShader.metal in Sources */ = {isa = PBXBuildFile; fileRef = 8FF9AD9F2C5B426C00A57A75 /* MulticolorGradientShader.metal */; }; 45 | 8FF9ADA62C5B440B00A57A75 /* MulticolorGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FF9ADA52C5B440B00A57A75 /* MulticolorGradient.swift */; }; 46 | 8FF9ADAB2C5B5E7500A57A75 /* ColorStop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FF9ADA72C5B5E7500A57A75 /* ColorStop.swift */; }; 47 | 8FF9ADAC2C5B5E7500A57A75 /* GradientParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FF9ADA82C5B5E7500A57A75 /* GradientParams.swift */; }; 48 | 8FF9ADAD2C5B5E7500A57A75 /* Uniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FF9ADA92C5B5E7500A57A75 /* Uniforms.swift */; }; 49 | 8FFA9F312AA1B1E600BAEC5C /* OnboardingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFA9F302AA1B1E600BAEC5C /* OnboardingWindow.swift */; }; 50 | 8FFA9F342AA1B3CB00BAEC5C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 8FFA9F332AA1B3CB00BAEC5C /* SDWebImageSwiftUI */; }; 51 | 8FFA9F382AA1B63500BAEC5C /* crossfade.gif in Resources */ = {isa = PBXBuildFile; fileRef = 8FFA9F362AA1B63500BAEC5C /* crossfade.gif */; }; 52 | 8FFED07B2D8A26E700EA1510 /* SwiftOTP in Frameworks */ = {isa = PBXBuildFile; productRef = 8FFED07A2D8A26E700EA1510 /* SwiftOTP */; }; 53 | /* End PBXBuildFile section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 8F0849332CB5D1BD00644244 /* KaraokeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KaraokeView.swift; sourceTree = ""; }; 57 | 8F10AC8D2D7D04D90036664A /* OverridePrint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePrint.swift; sourceTree = ""; }; 58 | 8F28BFD32D578456002E2CE6 /* KaraokeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KaraokeSettings.swift; sourceTree = ""; }; 59 | 8F2D53492D6C1B50006F33B0 /* LyricsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LyricsParser.swift; sourceTree = ""; }; 60 | 8F2D534E2D6C1B88006F33B0 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 61 | 8F2D53522D6C1D29006F33B0 /* LyricsHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LyricsHeader.swift; sourceTree = ""; }; 62 | 8F39ED8D2A78EB5900574203 /* lyricJsonStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lyricJsonStruct.swift; sourceTree = ""; }; 63 | 8F4F495B2A7FB3D400097888 /* SongObject+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SongObject+CoreDataClass.swift"; sourceTree = ""; }; 64 | 8F4F495C2A7FB3D400097888 /* SongObject+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SongObject+CoreDataProperties.swift"; sourceTree = ""; }; 65 | 8F58B8CE2C55AEB1009ADA1A /* FullscreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenView.swift; sourceTree = ""; }; 66 | 8F6BD2962A8A6278008BBF88 /* amplitudeKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = amplitudeKey.swift; sourceTree = ""; }; 67 | 8F6BD2982A8A6B7D008BBF88 /* viewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = viewModel.swift; sourceTree = ""; }; 68 | 8F8196882D93E6DA00F03741 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 69 | 8F85A8212BBFD9F6004A774D /* AppleMusicScripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleMusicScripting.swift; sourceTree = ""; }; 70 | 8F9193D72CD1A2C7002B24DA /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = ""; }; 71 | 8F91CD642DCB296600FDF8DC /* FullscreenButtonIconStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenButtonIconStyle.swift; sourceTree = ""; }; 72 | 8FA436AD2BC49D480072016C /* newPermissionMac.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = newPermissionMac.gif; sourceTree = ""; }; 73 | 8FBA5E032BEADEC9000E4DEF /* WebLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLoginView.swift; sourceTree = ""; }; 74 | 8FC8E9452A704EEB00F69915 /* Lyric Fever.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Lyric Fever.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 75 | 8FC8E9482A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyLyricsInMenubarApp.swift; sourceTree = ""; }; 76 | 8FC8E94C2A704EED00F69915 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 77 | 8FC8E9512A704EED00F69915 /* SpotifyLyricsInMenubar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpotifyLyricsInMenubar.entitlements; sourceTree = ""; }; 78 | 8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = spotifylogin.gif; sourceTree = ""; }; 79 | 8FE454272A8916C30039EFA7 /* SpotifyScripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyScripting.swift; sourceTree = ""; }; 80 | 8FE454292A891EBD0039EFA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 81 | 8FEF70F62D8B5CE40069AC01 /* Update22Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update22Window.swift; sourceTree = ""; }; 82 | 8FF59E2D2A798D2B00F0A382 /* Lyrics.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Lyrics.xcdatamodel; sourceTree = ""; }; 83 | 8FF9AD9F2C5B426C00A57A75 /* MulticolorGradientShader.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = MulticolorGradientShader.metal; sourceTree = ""; }; 84 | 8FF9ADA52C5B440B00A57A75 /* MulticolorGradient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MulticolorGradient.swift; sourceTree = ""; }; 85 | 8FF9ADA72C5B5E7500A57A75 /* ColorStop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorStop.swift; sourceTree = ""; }; 86 | 8FF9ADA82C5B5E7500A57A75 /* GradientParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientParams.swift; sourceTree = ""; }; 87 | 8FF9ADA92C5B5E7500A57A75 /* Uniforms.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Uniforms.swift; sourceTree = ""; }; 88 | 8FFA9F302AA1B1E600BAEC5C /* OnboardingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingWindow.swift; sourceTree = ""; }; 89 | 8FFA9F362AA1B63500BAEC5C /* crossfade.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = crossfade.gif; sourceTree = ""; }; 90 | /* End PBXFileReference section */ 91 | 92 | /* Begin PBXFrameworksBuildPhase section */ 93 | 8FC8E9422A704EEB00F69915 /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | 8F1979952DA72D49008C113D /* IPADic in Frameworks */, 98 | 8F1979972DA72D49008C113D /* Mecab-Swift in Frameworks */, 99 | 8FFED07B2D8A26E700EA1510 /* SwiftOTP in Frameworks */, 100 | 8FA4E9782CE73DB900CEEF35 /* ColorKit in Frameworks */, 101 | 8FF184ED2D626F1E0026D2A2 /* CompactSlider in Frameworks */, 102 | 8F28FDC22AA25D2700439D8D /* Sparkle in Frameworks */, 103 | 8F832CAD2D6BEC52008C1C2B /* FontPicker in Frameworks */, 104 | 8F8196872D93CCDC00F03741 /* StringMetric in Frameworks */, 105 | 8FFA9F342AA1B3CB00BAEC5C /* SDWebImageSwiftUI in Frameworks */, 106 | 8F89E1112C90DA4900F511EB /* LaunchAtLogin in Frameworks */, 107 | 8F6BD2952A8A61C9008BBF88 /* AmplitudeSwift in Frameworks */, 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | /* End PBXFrameworksBuildPhase section */ 112 | 113 | /* Begin PBXGroup section */ 114 | 8F2D534A2D6C1B50006F33B0 /* LyricsParser */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 8F2D534E2D6C1B88006F33B0 /* Extensions.swift */, 118 | 8F2D53522D6C1D29006F33B0 /* LyricsHeader.swift */, 119 | 8F2D53492D6C1B50006F33B0 /* LyricsParser.swift */, 120 | ); 121 | path = LyricsParser; 122 | sourceTree = ""; 123 | }; 124 | 8FC1F4E42A7969C70043D92A /* Frameworks */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | ); 128 | name = Frameworks; 129 | sourceTree = ""; 130 | }; 131 | 8FC8E93C2A704EEB00F69915 = { 132 | isa = PBXGroup; 133 | children = ( 134 | 8F8196882D93E6DA00F03741 /* Localizable.xcstrings */, 135 | 8F4F495B2A7FB3D400097888 /* SongObject+CoreDataClass.swift */, 136 | 8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */, 137 | 8FFA9F362AA1B63500BAEC5C /* crossfade.gif */, 138 | 8FA436AD2BC49D480072016C /* newPermissionMac.gif */, 139 | 8F4F495C2A7FB3D400097888 /* SongObject+CoreDataProperties.swift */, 140 | 8FC8E9472A704EEB00F69915 /* SpotifyLyricsInMenubar */, 141 | 8FC8E9462A704EEB00F69915 /* Products */, 142 | 8FC1F4E42A7969C70043D92A /* Frameworks */, 143 | ); 144 | sourceTree = ""; 145 | }; 146 | 8FC8E9462A704EEB00F69915 /* Products */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 8FC8E9452A704EEB00F69915 /* Lyric Fever.app */, 150 | ); 151 | name = Products; 152 | sourceTree = ""; 153 | }; 154 | 8FC8E9472A704EEB00F69915 /* SpotifyLyricsInMenubar */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 8FF9AD9E2C5B426C00A57A75 /* Shaders */, 158 | 8FE454292A891EBD0039EFA7 /* Info.plist */, 159 | 8FC8E9482A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift */, 160 | 8F10AC8D2D7D04D90036664A /* OverridePrint.swift */, 161 | 8F9193D72CD1A2C7002B24DA /* FloatingPanel.swift */, 162 | 8FF9ADAA2C5B5E7500A57A75 /* Model */, 163 | 8FFA9F302AA1B1E600BAEC5C /* OnboardingWindow.swift */, 164 | 8FEF70F62D8B5CE40069AC01 /* Update22Window.swift */, 165 | 8F28BFD32D578456002E2CE6 /* KaraokeSettings.swift */, 166 | 8FBA5E032BEADEC9000E4DEF /* WebLoginView.swift */, 167 | 8F2D534A2D6C1B50006F33B0 /* LyricsParser */, 168 | 8F6BD2982A8A6B7D008BBF88 /* viewModel.swift */, 169 | 8FF9ADA52C5B440B00A57A75 /* MulticolorGradient.swift */, 170 | 8F6BD2962A8A6278008BBF88 /* amplitudeKey.swift */, 171 | 8FF59E2C2A798D2B00F0A382 /* Lyrics.xcdatamodeld */, 172 | 8F39ED8D2A78EB5900574203 /* lyricJsonStruct.swift */, 173 | 8FC8E94C2A704EED00F69915 /* Assets.xcassets */, 174 | 8FC8E9512A704EED00F69915 /* SpotifyLyricsInMenubar.entitlements */, 175 | 8FE454272A8916C30039EFA7 /* SpotifyScripting.swift */, 176 | 8F85A8212BBFD9F6004A774D /* AppleMusicScripting.swift */, 177 | 8F58B8CE2C55AEB1009ADA1A /* FullscreenView.swift */, 178 | 8F91CD642DCB296600FDF8DC /* FullscreenButtonIconStyle.swift */, 179 | 8F0849332CB5D1BD00644244 /* KaraokeView.swift */, 180 | ); 181 | path = SpotifyLyricsInMenubar; 182 | sourceTree = ""; 183 | }; 184 | 8FF9AD9E2C5B426C00A57A75 /* Shaders */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 8FF9AD9F2C5B426C00A57A75 /* MulticolorGradientShader.metal */, 188 | ); 189 | path = Shaders; 190 | sourceTree = ""; 191 | }; 192 | 8FF9ADAA2C5B5E7500A57A75 /* Model */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 8FF9ADA72C5B5E7500A57A75 /* ColorStop.swift */, 196 | 8FF9ADA82C5B5E7500A57A75 /* GradientParams.swift */, 197 | 8FF9ADA92C5B5E7500A57A75 /* Uniforms.swift */, 198 | ); 199 | path = Model; 200 | sourceTree = ""; 201 | }; 202 | /* End PBXGroup section */ 203 | 204 | /* Begin PBXNativeTarget section */ 205 | 8FC8E9442A704EEB00F69915 /* Lyric Fever */ = { 206 | isa = PBXNativeTarget; 207 | buildConfigurationList = 8FC8E9542A704EED00F69915 /* Build configuration list for PBXNativeTarget "Lyric Fever" */; 208 | buildPhases = ( 209 | 8FC8E9412A704EEB00F69915 /* Sources */, 210 | 8FC8E9422A704EEB00F69915 /* Frameworks */, 211 | 8FC8E9432A704EEB00F69915 /* Resources */, 212 | ); 213 | buildRules = ( 214 | ); 215 | dependencies = ( 216 | ); 217 | name = "Lyric Fever"; 218 | packageProductDependencies = ( 219 | 8F6BD2942A8A61C9008BBF88 /* AmplitudeSwift */, 220 | 8FFA9F332AA1B3CB00BAEC5C /* SDWebImageSwiftUI */, 221 | 8F28FDC12AA25D2700439D8D /* Sparkle */, 222 | 8F89E1102C90DA4900F511EB /* LaunchAtLogin */, 223 | 8FA4E9772CE73DB900CEEF35 /* ColorKit */, 224 | 8FF184EC2D626F1E0026D2A2 /* CompactSlider */, 225 | 8F832CAC2D6BEC52008C1C2B /* FontPicker */, 226 | 8FFED07A2D8A26E700EA1510 /* SwiftOTP */, 227 | 8F8196862D93CCDC00F03741 /* StringMetric */, 228 | 8F1979942DA72D49008C113D /* IPADic */, 229 | 8F1979962DA72D49008C113D /* Mecab-Swift */, 230 | ); 231 | productName = SpotifyLyricsInMenubar; 232 | productReference = 8FC8E9452A704EEB00F69915 /* Lyric Fever.app */; 233 | productType = "com.apple.product-type.application"; 234 | }; 235 | /* End PBXNativeTarget section */ 236 | 237 | /* Begin PBXProject section */ 238 | 8FC8E93D2A704EEB00F69915 /* Project object */ = { 239 | isa = PBXProject; 240 | attributes = { 241 | BuildIndependentTargetsInParallel = 1; 242 | LastSwiftUpdateCheck = 1500; 243 | LastUpgradeCheck = 1610; 244 | TargetAttributes = { 245 | 8FC8E9442A704EEB00F69915 = { 246 | CreatedOnToolsVersion = 15.0; 247 | }; 248 | }; 249 | }; 250 | buildConfigurationList = 8FC8E9402A704EEB00F69915 /* Build configuration list for PBXProject "Lyric Fever" */; 251 | compatibilityVersion = "Xcode 14.0"; 252 | developmentRegion = en; 253 | hasScannedForEncodings = 0; 254 | knownRegions = ( 255 | en, 256 | Base, 257 | "zh-Hans", 258 | ); 259 | mainGroup = 8FC8E93C2A704EEB00F69915; 260 | packageReferences = ( 261 | 8F6BD2932A8A61C8008BBF88 /* XCRemoteSwiftPackageReference "Amplitude-Swift" */, 262 | 8FFA9F322AA1B3CB00BAEC5C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, 263 | 8F28FDC02AA25D2700439D8D /* XCRemoteSwiftPackageReference "Sparkle" */, 264 | 8F89E10F2C90DA4900F511EB /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, 265 | 8FA4E9762CE73DB900CEEF35 /* XCRemoteSwiftPackageReference "ColorKit-macOS" */, 266 | 8FF184EB2D626F1E0026D2A2 /* XCRemoteSwiftPackageReference "CompactSlider" */, 267 | 8F832CAB2D6BEC52008C1C2B /* XCRemoteSwiftPackageReference "FontPicker" */, 268 | 8FFED0792D8A26E700EA1510 /* XCRemoteSwiftPackageReference "SwiftOTP" */, 269 | 8F8196852D93CCDC00F03741 /* XCRemoteSwiftPackageReference "StringMetric" */, 270 | 8F1979932DA72D49008C113D /* XCRemoteSwiftPackageReference "Mecab-Swift" */, 271 | ); 272 | productRefGroup = 8FC8E9462A704EEB00F69915 /* Products */; 273 | projectDirPath = ""; 274 | projectRoot = ""; 275 | targets = ( 276 | 8FC8E9442A704EEB00F69915 /* Lyric Fever */, 277 | ); 278 | }; 279 | /* End PBXProject section */ 280 | 281 | /* Begin PBXResourcesBuildPhase section */ 282 | 8FC8E9432A704EEB00F69915 /* Resources */ = { 283 | isa = PBXResourcesBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | 8FA436AE2BC49D480072016C /* newPermissionMac.gif in Resources */, 287 | 8F8196892D93E6DA00F03741 /* Localizable.xcstrings in Resources */, 288 | 8FC8E94D2A704EED00F69915 /* Assets.xcassets in Resources */, 289 | 8FFA9F382AA1B63500BAEC5C /* crossfade.gif in Resources */, 290 | 8FCFD1C32AE35DEA00B22023 /* spotifylogin.gif in Resources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXResourcesBuildPhase section */ 295 | 296 | /* Begin PBXSourcesBuildPhase section */ 297 | 8FC8E9412A704EEB00F69915 /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 8FF59E2E2A798D2B00F0A382 /* Lyrics.xcdatamodeld in Sources */, 302 | 8F91CD652DCB296F00FDF8DC /* FullscreenButtonIconStyle.swift in Sources */, 303 | 8F6BD2972A8A6278008BBF88 /* amplitudeKey.swift in Sources */, 304 | 8F4F49522A7EC32D00097888 /* (null) in Sources */, 305 | 8FBA5E042BEADEC9000E4DEF /* WebLoginView.swift in Sources */, 306 | 8F2D53532D6C1D29006F33B0 /* LyricsHeader.swift in Sources */, 307 | 8F4F495D2A7FB3D400097888 /* SongObject+CoreDataClass.swift in Sources */, 308 | 8FF9ADA62C5B440B00A57A75 /* MulticolorGradient.swift in Sources */, 309 | 8F9193D82CD1A2CA002B24DA /* FloatingPanel.swift in Sources */, 310 | 8F28BFD42D578456002E2CE6 /* KaraokeSettings.swift in Sources */, 311 | 8FEF70F72D8B5CE40069AC01 /* Update22Window.swift in Sources */, 312 | 8F10AC8E2D7D04DC0036664A /* OverridePrint.swift in Sources */, 313 | 8F39ED8E2A78EB5900574203 /* lyricJsonStruct.swift in Sources */, 314 | 8F0849342CB5D1C100644244 /* KaraokeView.swift in Sources */, 315 | 8F6BD2992A8A6B7D008BBF88 /* viewModel.swift in Sources */, 316 | 8FC8E9492A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift in Sources */, 317 | 8F85A8222BBFD9F6004A774D /* AppleMusicScripting.swift in Sources */, 318 | 8F2D53512D6C1B88006F33B0 /* Extensions.swift in Sources */, 319 | 8FE454282A8916C30039EFA7 /* SpotifyScripting.swift in Sources */, 320 | 8FF9ADAC2C5B5E7500A57A75 /* GradientParams.swift in Sources */, 321 | 8FF9ADA02C5B426C00A57A75 /* MulticolorGradientShader.metal in Sources */, 322 | 8FFA9F312AA1B1E600BAEC5C /* OnboardingWindow.swift in Sources */, 323 | 8F4F495E2A7FB3D400097888 /* SongObject+CoreDataProperties.swift in Sources */, 324 | 8F2D534D2D6C1B50006F33B0 /* LyricsParser.swift in Sources */, 325 | 8F58B8CF2C55AEB1009ADA1A /* FullscreenView.swift in Sources */, 326 | 8FF9ADAD2C5B5E7500A57A75 /* Uniforms.swift in Sources */, 327 | 8FF9ADAB2C5B5E7500A57A75 /* ColorStop.swift in Sources */, 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | /* End PBXSourcesBuildPhase section */ 332 | 333 | /* Begin XCBuildConfiguration section */ 334 | 8FC8E9522A704EED00F69915 /* Debug */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ALWAYS_SEARCH_USER_PATHS = NO; 338 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 339 | CLANG_ANALYZER_NONNULL = YES; 340 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 341 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 342 | CLANG_ENABLE_MODULES = YES; 343 | CLANG_ENABLE_OBJC_ARC = YES; 344 | CLANG_ENABLE_OBJC_WEAK = YES; 345 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 346 | CLANG_WARN_BOOL_CONVERSION = YES; 347 | CLANG_WARN_COMMA = YES; 348 | CLANG_WARN_CONSTANT_CONVERSION = YES; 349 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 350 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 351 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 352 | CLANG_WARN_EMPTY_BODY = YES; 353 | CLANG_WARN_ENUM_CONVERSION = YES; 354 | CLANG_WARN_INFINITE_RECURSION = YES; 355 | CLANG_WARN_INT_CONVERSION = YES; 356 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 357 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 358 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 360 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 361 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 362 | CLANG_WARN_STRICT_PROTOTYPES = YES; 363 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 364 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 365 | CLANG_WARN_UNREACHABLE_CODE = YES; 366 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 367 | COPY_PHASE_STRIP = NO; 368 | DEAD_CODE_STRIPPING = YES; 369 | DEBUG_INFORMATION_FORMAT = dwarf; 370 | ENABLE_APP_SANDBOX = YES; 371 | ENABLE_STRICT_OBJC_MSGSEND = YES; 372 | ENABLE_TESTABILITY = YES; 373 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 374 | GCC_C_LANGUAGE_STANDARD = gnu17; 375 | GCC_DYNAMIC_NO_PIC = NO; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_OPTIMIZATION_LEVEL = 0; 378 | GCC_PREPROCESSOR_DEFINITIONS = ( 379 | "DEBUG=1", 380 | "$(inherited)", 381 | ); 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 389 | MACOSX_DEPLOYMENT_TARGET = 13.0; 390 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 391 | MTL_FAST_MATH = YES; 392 | ONLY_ACTIVE_ARCH = YES; 393 | SDKROOT = macosx; 394 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 395 | SWIFT_EMIT_LOC_STRINGS = YES; 396 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 397 | }; 398 | name = Debug; 399 | }; 400 | 8FC8E9532A704EED00F69915 /* Release */ = { 401 | isa = XCBuildConfiguration; 402 | buildSettings = { 403 | ALWAYS_SEARCH_USER_PATHS = NO; 404 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 405 | CLANG_ANALYZER_NONNULL = YES; 406 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 407 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 408 | CLANG_ENABLE_MODULES = YES; 409 | CLANG_ENABLE_OBJC_ARC = YES; 410 | CLANG_ENABLE_OBJC_WEAK = YES; 411 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 412 | CLANG_WARN_BOOL_CONVERSION = YES; 413 | CLANG_WARN_COMMA = YES; 414 | CLANG_WARN_CONSTANT_CONVERSION = YES; 415 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 416 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 417 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 418 | CLANG_WARN_EMPTY_BODY = YES; 419 | CLANG_WARN_ENUM_CONVERSION = YES; 420 | CLANG_WARN_INFINITE_RECURSION = YES; 421 | CLANG_WARN_INT_CONVERSION = YES; 422 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 423 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 424 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 426 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 427 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 428 | CLANG_WARN_STRICT_PROTOTYPES = YES; 429 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 430 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 431 | CLANG_WARN_UNREACHABLE_CODE = YES; 432 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 433 | COPY_PHASE_STRIP = NO; 434 | DEAD_CODE_STRIPPING = YES; 435 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 436 | ENABLE_APP_SANDBOX = YES; 437 | ENABLE_NS_ASSERTIONS = NO; 438 | ENABLE_STRICT_OBJC_MSGSEND = YES; 439 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 440 | GCC_C_LANGUAGE_STANDARD = gnu17; 441 | GCC_NO_COMMON_BLOCKS = YES; 442 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 443 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 444 | GCC_WARN_UNDECLARED_SELECTOR = YES; 445 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 446 | GCC_WARN_UNUSED_FUNCTION = YES; 447 | GCC_WARN_UNUSED_VARIABLE = YES; 448 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 449 | MACOSX_DEPLOYMENT_TARGET = 13.0; 450 | MTL_ENABLE_DEBUG_INFO = NO; 451 | MTL_FAST_MATH = YES; 452 | SDKROOT = macosx; 453 | SWIFT_COMPILATION_MODE = wholemodule; 454 | SWIFT_EMIT_LOC_STRINGS = YES; 455 | }; 456 | name = Release; 457 | }; 458 | 8FC8E9552A704EED00F69915 /* Debug */ = { 459 | isa = XCBuildConfiguration; 460 | buildSettings = { 461 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 462 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 463 | CODE_SIGN_ENTITLEMENTS = SpotifyLyricsInMenubar/SpotifyLyricsInMenubar.entitlements; 464 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 465 | CODE_SIGN_STYLE = Automatic; 466 | COMBINE_HIDPI_IMAGES = YES; 467 | CURRENT_PROJECT_VERSION = 2.2; 468 | DEAD_CODE_STRIPPING = YES; 469 | DEVELOPMENT_ASSET_PATHS = ""; 470 | DEVELOPMENT_TEAM = 38TP6LZLJ5; 471 | ENABLE_APP_SANDBOX = YES; 472 | ENABLE_HARDENED_RUNTIME = YES; 473 | ENABLE_PREVIEWS = YES; 474 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 475 | GENERATE_INFOPLIST_FILE = YES; 476 | INFOPLIST_FILE = SpotifyLyricsInMenubar/Info.plist; 477 | INFOPLIST_KEY_CFBundleDisplayName = "Lyric Fever"; 478 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; 479 | INFOPLIST_KEY_LSUIElement = YES; 480 | INFOPLIST_KEY_NSAppleEventsUsageDescription = "I need to access Spotify / Apple Music to play the lyrics properly."; 481 | INFOPLIST_KEY_NSAppleMusicUsageDescription = "This permission is required to interact with your Apple Music library."; 482 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 483 | LD_RUNPATH_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "@executable_path/../Frameworks", 486 | ); 487 | MACOSX_DEPLOYMENT_TARGET = 13.5; 488 | MARKETING_VERSION = 2.2; 489 | PRODUCT_BUNDLE_IDENTIFIER = com.aviwadhwa.SpotifyLyricsInMenubar; 490 | PRODUCT_NAME = "$(TARGET_NAME)"; 491 | SWIFT_EMIT_LOC_STRINGS = YES; 492 | SWIFT_STRICT_CONCURRENCY = complete; 493 | SWIFT_VERSION = 5.0; 494 | }; 495 | name = Debug; 496 | }; 497 | 8FC8E9562A704EED00F69915 /* Release */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 501 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 502 | CODE_SIGN_ENTITLEMENTS = SpotifyLyricsInMenubar/SpotifyLyricsInMenubar.entitlements; 503 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 504 | CODE_SIGN_STYLE = Automatic; 505 | COMBINE_HIDPI_IMAGES = YES; 506 | CURRENT_PROJECT_VERSION = 2.2; 507 | DEAD_CODE_STRIPPING = YES; 508 | DEVELOPMENT_ASSET_PATHS = ""; 509 | DEVELOPMENT_TEAM = 38TP6LZLJ5; 510 | ENABLE_APP_SANDBOX = YES; 511 | ENABLE_HARDENED_RUNTIME = YES; 512 | ENABLE_PREVIEWS = YES; 513 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 514 | GENERATE_INFOPLIST_FILE = YES; 515 | INFOPLIST_FILE = SpotifyLyricsInMenubar/Info.plist; 516 | INFOPLIST_KEY_CFBundleDisplayName = "Lyric Fever"; 517 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; 518 | INFOPLIST_KEY_LSUIElement = YES; 519 | INFOPLIST_KEY_NSAppleEventsUsageDescription = "I need to access Spotify / Apple Music to play the lyrics properly."; 520 | INFOPLIST_KEY_NSAppleMusicUsageDescription = "This permission is required to interact with your Apple Music library."; 521 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 522 | LD_RUNPATH_SEARCH_PATHS = ( 523 | "$(inherited)", 524 | "@executable_path/../Frameworks", 525 | ); 526 | MACOSX_DEPLOYMENT_TARGET = 13.5; 527 | MARKETING_VERSION = 2.2; 528 | PRODUCT_BUNDLE_IDENTIFIER = com.aviwadhwa.SpotifyLyricsInMenubar; 529 | PRODUCT_NAME = "$(TARGET_NAME)"; 530 | SWIFT_EMIT_LOC_STRINGS = YES; 531 | SWIFT_STRICT_CONCURRENCY = complete; 532 | SWIFT_VERSION = 5.0; 533 | }; 534 | name = Release; 535 | }; 536 | /* End XCBuildConfiguration section */ 537 | 538 | /* Begin XCConfigurationList section */ 539 | 8FC8E9402A704EEB00F69915 /* Build configuration list for PBXProject "Lyric Fever" */ = { 540 | isa = XCConfigurationList; 541 | buildConfigurations = ( 542 | 8FC8E9522A704EED00F69915 /* Debug */, 543 | 8FC8E9532A704EED00F69915 /* Release */, 544 | ); 545 | defaultConfigurationIsVisible = 0; 546 | defaultConfigurationName = Release; 547 | }; 548 | 8FC8E9542A704EED00F69915 /* Build configuration list for PBXNativeTarget "Lyric Fever" */ = { 549 | isa = XCConfigurationList; 550 | buildConfigurations = ( 551 | 8FC8E9552A704EED00F69915 /* Debug */, 552 | 8FC8E9562A704EED00F69915 /* Release */, 553 | ); 554 | defaultConfigurationIsVisible = 0; 555 | defaultConfigurationName = Release; 556 | }; 557 | /* End XCConfigurationList section */ 558 | 559 | /* Begin XCRemoteSwiftPackageReference section */ 560 | 8F1979932DA72D49008C113D /* XCRemoteSwiftPackageReference "Mecab-Swift" */ = { 561 | isa = XCRemoteSwiftPackageReference; 562 | repositoryURL = "https://github.com/shinjukunian/Mecab-Swift"; 563 | requirement = { 564 | kind = upToNextMajorVersion; 565 | minimumVersion = 0.8.0; 566 | }; 567 | }; 568 | 8F28FDC02AA25D2700439D8D /* XCRemoteSwiftPackageReference "Sparkle" */ = { 569 | isa = XCRemoteSwiftPackageReference; 570 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 571 | requirement = { 572 | kind = upToNextMajorVersion; 573 | minimumVersion = 2.4.2; 574 | }; 575 | }; 576 | 8F6BD2932A8A61C8008BBF88 /* XCRemoteSwiftPackageReference "Amplitude-Swift" */ = { 577 | isa = XCRemoteSwiftPackageReference; 578 | repositoryURL = "https://github.com/amplitude/Amplitude-Swift"; 579 | requirement = { 580 | branch = main; 581 | kind = branch; 582 | }; 583 | }; 584 | 8F8196852D93CCDC00F03741 /* XCRemoteSwiftPackageReference "StringMetric" */ = { 585 | isa = XCRemoteSwiftPackageReference; 586 | repositoryURL = "https://github.com/autozimu/StringMetric.swift"; 587 | requirement = { 588 | kind = upToNextMajorVersion; 589 | minimumVersion = 0.3.2; 590 | }; 591 | }; 592 | 8F832CAB2D6BEC52008C1C2B /* XCRemoteSwiftPackageReference "FontPicker" */ = { 593 | isa = XCRemoteSwiftPackageReference; 594 | repositoryURL = "https://github.com/tyagishi/FontPicker"; 595 | requirement = { 596 | kind = upToNextMajorVersion; 597 | minimumVersion = 1.2.0; 598 | }; 599 | }; 600 | 8F89E10F2C90DA4900F511EB /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { 601 | isa = XCRemoteSwiftPackageReference; 602 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; 603 | requirement = { 604 | branch = main; 605 | kind = branch; 606 | }; 607 | }; 608 | 8FA4E9762CE73DB900CEEF35 /* XCRemoteSwiftPackageReference "ColorKit-macOS" */ = { 609 | isa = XCRemoteSwiftPackageReference; 610 | repositoryURL = "https://github.com/aviwad/ColorKit-macOS"; 611 | requirement = { 612 | branch = master; 613 | kind = branch; 614 | }; 615 | }; 616 | 8FF184EB2D626F1E0026D2A2 /* XCRemoteSwiftPackageReference "CompactSlider" */ = { 617 | isa = XCRemoteSwiftPackageReference; 618 | repositoryURL = "https://github.com/buh/CompactSlider.git"; 619 | requirement = { 620 | kind = exactVersion; 621 | version = 1.1.6; 622 | }; 623 | }; 624 | 8FFA9F322AA1B3CB00BAEC5C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { 625 | isa = XCRemoteSwiftPackageReference; 626 | repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git"; 627 | requirement = { 628 | branch = master; 629 | kind = branch; 630 | }; 631 | }; 632 | 8FFED0792D8A26E700EA1510 /* XCRemoteSwiftPackageReference "SwiftOTP" */ = { 633 | isa = XCRemoteSwiftPackageReference; 634 | repositoryURL = "https://github.com/lachlanbell/SwiftOTP"; 635 | requirement = { 636 | kind = upToNextMajorVersion; 637 | minimumVersion = 3.0.2; 638 | }; 639 | }; 640 | /* End XCRemoteSwiftPackageReference section */ 641 | 642 | /* Begin XCSwiftPackageProductDependency section */ 643 | 8F1979942DA72D49008C113D /* IPADic */ = { 644 | isa = XCSwiftPackageProductDependency; 645 | package = 8F1979932DA72D49008C113D /* XCRemoteSwiftPackageReference "Mecab-Swift" */; 646 | productName = IPADic; 647 | }; 648 | 8F1979962DA72D49008C113D /* Mecab-Swift */ = { 649 | isa = XCSwiftPackageProductDependency; 650 | package = 8F1979932DA72D49008C113D /* XCRemoteSwiftPackageReference "Mecab-Swift" */; 651 | productName = "Mecab-Swift"; 652 | }; 653 | 8F28FDC12AA25D2700439D8D /* Sparkle */ = { 654 | isa = XCSwiftPackageProductDependency; 655 | package = 8F28FDC02AA25D2700439D8D /* XCRemoteSwiftPackageReference "Sparkle" */; 656 | productName = Sparkle; 657 | }; 658 | 8F6BD2942A8A61C9008BBF88 /* AmplitudeSwift */ = { 659 | isa = XCSwiftPackageProductDependency; 660 | package = 8F6BD2932A8A61C8008BBF88 /* XCRemoteSwiftPackageReference "Amplitude-Swift" */; 661 | productName = AmplitudeSwift; 662 | }; 663 | 8F8196862D93CCDC00F03741 /* StringMetric */ = { 664 | isa = XCSwiftPackageProductDependency; 665 | package = 8F8196852D93CCDC00F03741 /* XCRemoteSwiftPackageReference "StringMetric" */; 666 | productName = StringMetric; 667 | }; 668 | 8F832CAC2D6BEC52008C1C2B /* FontPicker */ = { 669 | isa = XCSwiftPackageProductDependency; 670 | package = 8F832CAB2D6BEC52008C1C2B /* XCRemoteSwiftPackageReference "FontPicker" */; 671 | productName = FontPicker; 672 | }; 673 | 8F89E1102C90DA4900F511EB /* LaunchAtLogin */ = { 674 | isa = XCSwiftPackageProductDependency; 675 | package = 8F89E10F2C90DA4900F511EB /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; 676 | productName = LaunchAtLogin; 677 | }; 678 | 8FA4E9772CE73DB900CEEF35 /* ColorKit */ = { 679 | isa = XCSwiftPackageProductDependency; 680 | package = 8FA4E9762CE73DB900CEEF35 /* XCRemoteSwiftPackageReference "ColorKit-macOS" */; 681 | productName = ColorKit; 682 | }; 683 | 8FF184EC2D626F1E0026D2A2 /* CompactSlider */ = { 684 | isa = XCSwiftPackageProductDependency; 685 | package = 8FF184EB2D626F1E0026D2A2 /* XCRemoteSwiftPackageReference "CompactSlider" */; 686 | productName = CompactSlider; 687 | }; 688 | 8FFA9F332AA1B3CB00BAEC5C /* SDWebImageSwiftUI */ = { 689 | isa = XCSwiftPackageProductDependency; 690 | package = 8FFA9F322AA1B3CB00BAEC5C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; 691 | productName = SDWebImageSwiftUI; 692 | }; 693 | 8FFED07A2D8A26E700EA1510 /* SwiftOTP */ = { 694 | isa = XCSwiftPackageProductDependency; 695 | package = 8FFED0792D8A26E700EA1510 /* XCRemoteSwiftPackageReference "SwiftOTP" */; 696 | productName = SwiftOTP; 697 | }; 698 | /* End XCSwiftPackageProductDependency section */ 699 | 700 | /* Begin XCVersionGroup section */ 701 | 8FF59E2C2A798D2B00F0A382 /* Lyrics.xcdatamodeld */ = { 702 | isa = XCVersionGroup; 703 | children = ( 704 | 8FF59E2D2A798D2B00F0A382 /* Lyrics.xcdatamodel */, 705 | ); 706 | currentVersion = 8FF59E2D2A798D2B00F0A382 /* Lyrics.xcdatamodel */; 707 | path = Lyrics.xcdatamodeld; 708 | sourceTree = ""; 709 | versionGroupType = wrapper.xcdatamodel; 710 | }; 711 | /* End XCVersionGroup section */ 712 | }; 713 | rootObject = 8FC8E93D2A704EEB00F69915 /* Project object */; 714 | } 715 | -------------------------------------------------------------------------------- /Lyric Fever.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Lyric Fever.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Lyric Fever.xcodeproj/xcshareddata/xcschemes/SpotifyLyricsInMenubar.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 65 | 67 | 73 | 74 | 75 | 76 | 78 | 79 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lyric Fever 2 | 3 | Logo 4 | 5 | The Best Lyrics Experience for Spotify & Apple Music on macOS. It Just Works. 6 | 7 | ## Installation 8 | 9 | ### Manual 10 | Download the DMG file [here](https://github.com/aviwad/LyricFever/releases/download/v2.1/Lyric.Fever.2.1.dmg). 11 | 12 | ### Homebrew 13 | Run `brew install lyric-fever` 14 | 15 | ## Screenshots 16 | 17 | First Screenshot 18 | Screenshot 1 19 | Screenshot 2 20 | 21 | 22 | ## Features 23 | - Automatic Lyric Playback on Menubar 24 | - Fullscreen Mode (Modeled after Apple Music’s fullscreen view) 25 | - Karaoke Mode (Lyric popup that stays on screen) 26 | - Karaoke Mode customization (font color, etc) 27 | - Lyric Translation (using Apple’s on device APIs) 28 | - Offline caching! Lyrics are automatically stored offline efficiently using CoreData 29 | - Play some music on the Spotify / Apple Music app and watch the lyrics play on the menu bar automatically. 30 | - Lyrics fetched from Spotify, LRCLIB, and NetEase as a backup lyric provider 31 | - Simplified Chinese language support in app UI, thanks to @InTheManXG 32 | 33 | ## YouTube Promo Vid: 34 | 35 | [![LyricFever Promo Vid](https://img.youtube.com/vi/Bxc7d-O9-rM/0.jpg)](https://www.youtube.com/watch?v=Bxc7d-O9-rM) 36 | 37 | ### Requirements 38 | 39 | - macOS Ventura or higher (Sonoma required for fullscreen, Sequoia required for translation) 40 | - Spotify Desktop Client (if using Spotify) 41 | 42 | ## Technical Details 43 | 44 | - UI is built using SwiftUI. 45 | - The lyrics are updated and fetched using Swift Concurrency and Swift Tasks 46 | - The lyrics are stored into disk using CoreData. 47 | - I interface with Spotify & Apple Music using their AppleScript methods as well as by subscribing to their playback state change notifications. 48 | - I interface with Spotify and Apple Music's AppleScript methods by using Apple's provided ScriptingBridge interface. 49 | - I additionally use private APIs to get the currently playing Apple Music song's iTunes ID, and use MusicKit to map that to an ISRC code 50 | - I map Apple Music songs to equivalent Spotify ID using ISRC to display Lyrics fetched from Spotify for either platform 51 | - Lyrics are fetched from LRCLIB as a backup when Spotify fails 52 | - I fetch the song “background color” with each lyric, and the color is used for the karaoke mode window background 53 | - Songs translated using Apple's Translation API. 54 | - The fullscreen view uses a custom mesh gradient and extracts colors from the album art using ColorKit 55 | - Spiritual successor to LyricsX (95% more efficient, 0.1% CPU usage of Lyric Fever vs 3% of LyricsX) 56 | - Technical write-up coming soon 57 | 58 | ## Translation Help 59 | [Crowdin Translation website](https://crowdin.com/project/lyric-fever/invite?h=29165351cb7d916e369d00386e37ef602390778) 60 | 61 | Please open a GitHub issue request to translate to more languages. Thank you very much. 62 | 63 | ## Other Contributors 64 | - [InTheManXG](https://github.com/InTheManXG) for Simplified Chinese translations 65 | - [lcandy2](https://github.com/lcandy2) For their [pull request](https://github.com/aviwad/LyricFever/pull/68) 66 | 67 | ## Acknowledgements / Special Thanks 68 | - [Sparkle:](https://github.com/sparkle-project/Sparkle) For app updates 69 | - [Amplitude:](https://amplitude.com) For app analytics 70 | - [Spotify:](https://spotify.com) The music platform this project depends on! (for playback, for lyrics) 71 | - [Apple MusicKit:](https://developer.apple.com/musickit/) Apple Music API 72 | - [Apple Music:](https://music.apple.com/us/browse) Another platform that this project depends on 73 | - [ColorKit-macOS:](https://github.com/aviwad/ColorKit-macOS) My port of [ColorKit](https://github.com/Boris-Em/ColorKit) for macOS 74 | - Cindori for their blog post on writing an NSPanel view for SwiftUI 75 | - autozimu for https://github.com/autozimu/StringMetric.swift, used to determine how similar NetEase search results are from search query 76 | - [tranxuanthang](https://github.com/tranxuanthang) for [LRCLIB](https://lrclib.net), an open source Lyric library. Used when Spotify fails. 77 | - NetEase for their Lyrics, used when LRCLIB and Spotify fail. 78 | - https://neteasecloudmusicapi-ten-wine.vercel.app for their NetEase API 79 | - [f728743](https://github.com/f728743) for the mesh gradient view. 80 | - [jayasme](https://github.com/jayasme/) for the [LRC Lyric Parser](https://github.com/jayasme/SpotlightLyrics) I used as a base 81 | - MusicBrainz & the CoverArtArchive projects for their MBID and Cover Art APIs (used for non-spotify / local files) 82 | - LyricsX for their Spotify TOTP fix and NetEase lyric provider 83 | - [Mecab-Swift](https://github.com/shinjukunian/Mecab-Swift) by [shinjukunian](https://github.com/shinjukunian) for the excellent Japanese Romanization 84 | - Christian Selig for his efficient image color averaging technique 85 | - Various StackOverflow snippets 86 | -------------------------------------------------------------------------------- /SongObject+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SongObject+CoreDataClass.swift 3 | // SpotifyLyricsInMenubar 4 | // 5 | // Created by Avi Wadhwa on 06/08/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(SongObject) 13 | public class SongObject: NSManagedObject, Decodable { 14 | enum CodingKeys: String, CodingKey { 15 | case lines, language, syncType 16 | } 17 | 18 | convenience init(from LRCLyrics: LRCLyrics, with context: NSManagedObjectContext, trackID: String, trackName: String, duration: TimeInterval) { 19 | self.init(context: context) 20 | self.id = trackID 21 | self.title = trackName 22 | self.downloadDate = Date.now 23 | self.language = "" 24 | // self.duration = duration 25 | if !LRCLyrics.lyrics.isEmpty { 26 | var newLyrics = LRCLyrics.lyrics 27 | newLyrics.removeAll { $0.words == ""} 28 | newLyrics.append(LyricLine(startTime: duration+5000, words: "Now Playing: \(title)")) 29 | self.lyricsTimestamps = newLyrics.map {$0.startTimeMS} 30 | self.lyricsWords = newLyrics.map {$0.words} 31 | } else { 32 | self.lyricsTimestamps = [] 33 | self.lyricsWords = [] 34 | } 35 | 36 | } 37 | 38 | convenience init(from LocalLyrics: [LyricLine], with context: NSManagedObjectContext, trackID: String, trackName: String, duration: TimeInterval) { 39 | self.init(context: context) 40 | self.id = trackID 41 | self.title = trackName 42 | self.downloadDate = Date.now 43 | self.language = "" 44 | // self.duration = duration 45 | if !LocalLyrics.isEmpty { 46 | var newLyrics = LocalLyrics 47 | newLyrics.append(LyricLine(startTime: duration+5000, words: "Now Playing: \(title)")) 48 | self.lyricsTimestamps = newLyrics.map {$0.startTimeMS} 49 | self.lyricsWords = newLyrics.map {$0.words} 50 | } else { 51 | self.lyricsTimestamps = [] 52 | self.lyricsWords = [] 53 | } 54 | 55 | } 56 | 57 | public required convenience init(from decoder: Decoder) throws { 58 | guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext, let trackID = decoder.userInfo[CodingUserInfoKey.trackID] as? String, let trackName = decoder.userInfo[CodingUserInfoKey.trackName] as? String, let duration = decoder.userInfo[CodingUserInfoKey.duration] as? TimeInterval else { 59 | fatalError() 60 | } 61 | 62 | self.init(context: context) 63 | self.id = trackID 64 | self.title = trackName 65 | self.downloadDate = Date.now 66 | let container = try decoder.container(keyedBy: CodingKeys.self) 67 | self.language = (try? container.decode(String.self, forKey: .language)) ?? "" 68 | if let syncType = try? container.decode(String.self, forKey: .syncType), syncType == "LINE_SYNCED", var lyrics = try? container.decode([LyricLine].self, forKey: .lines) { 69 | // Dummy lyric at the end to keep the timer going past the last lyric, necessary for someone playing a single song on repeat 70 | // Spotify doesn't give playback notifications when it's the same song on repeat 71 | // Apple Music does, but unfortunately has every song slightly longer than it's spotify counterpart so this doesn't help us 72 | lyrics.removeAll { $0.words == ""} 73 | if !lyrics.isEmpty { 74 | lyrics.append(LyricLine(startTime: duration+5000, words: "Now Playing: \(title)")) 75 | } 76 | self.lyricsTimestamps = lyrics.map {$0.startTimeMS} 77 | self.lyricsWords = lyrics.map {$0.words} 78 | } else { 79 | self.lyricsWords = [] 80 | self.lyricsTimestamps = [] 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /SongObject+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SongObject+CoreDataProperties.swift 3 | // SpotifyLyricsInMenubar 4 | // 5 | // Created by Avi Wadhwa on 06/08/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension SongObject { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "SongObject") 17 | } 18 | 19 | @NSManaged public var downloadDate: Date 20 | @NSManaged public var id: String 21 | @NSManaged public var title: String 22 | @NSManaged public var language: String 23 | @NSManaged public var lyricsWords: [String] 24 | @NSManaged public var lyricsTimestamps: [TimeInterval] 25 | 26 | } 27 | 28 | extension SongObject : Identifiable { 29 | 30 | } 31 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/30.imageset/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/30.imageset/30.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/30.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "30.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/40.imageset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/40.imageset/40.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/40.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/50.imageset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/50.imageset/50.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/50.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "50.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/60.imageset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/60.imageset/60.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/60.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "60.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 1-4-2.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Group 1-4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Group 1-4-2.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-512.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/AppIcon.appiconset/Icon-64.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/checkAgain.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "checkAgain.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/checkAgain.imageset/checkAgain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/checkAgain.imageset/checkAgain.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/hi.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Frame 1-12 1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/hi.imageset/Frame 1-12 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/hi.imageset/Frame 1-12 1.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/music.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Untitled.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/music.imageset/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/music.imageset/Untitled.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/spotify.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "spotify.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Assets.xcassets/spotify.imageset/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/SpotifyLyricsInMenubar/Assets.xcassets/spotify.imageset/spotify.png -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/FloatingPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingPanel.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2024-10-29. 6 | // 7 | 8 | // Taken from the Cindori blog, updated to fit my needs 9 | 10 | import SwiftUI 11 | 12 | extension View { 13 | /** Present a ``FloatingPanel`` in SwiftUI fashion 14 | - Parameter isPresented: A boolean binding that keeps track of the panel's presentation state 15 | - Parameter contentRect: The initial content frame of the window 16 | - Parameter content: The displayed content 17 | **/ 18 | func floatingPanel(isPresented: Binding, 19 | contentRect: CGRect = CGRect(x: 0, y: 0, width: 800, height: 100), 20 | @ViewBuilder content: @escaping () -> Content) -> some View { 21 | // self.modifier(FloatingPanelModifier(isPresented: isPresented, contentRect: contentRect, view: content)) 22 | self.modifier(FloatingPanelModifier(isPresented: isPresented, contentRect: contentRect, view: content)) 23 | } 24 | } 25 | 26 | // Add a ``FloatingPanel`` to a view hierarchy 27 | struct FloatingPanelModifier: ViewModifier { 28 | /// Determines wheter the panel should be presented or not 29 | @Binding var isPresented: Bool 30 | 31 | /// Determines the starting size of the panel 32 | /// .frame(minWidth: 600, maxWidth: 600, minHeight: 100, maxHeight: 100, alignment: .center) 33 | var contentRect: CGRect// = CGRect(x: 0, y: 0, width: 600, height: 100) 34 | 35 | /// Holds the panel content's view closure 36 | @ViewBuilder let view: () -> PanelContent 37 | 38 | /// Stores the panel instance with the same generic type as the view closure 39 | @State var panel: FloatingPanel? 40 | 41 | func body(content: Content) -> some View { 42 | content 43 | .onAppear { 44 | /// When the view appears, create, center and present the panel if ordered 45 | panel = FloatingPanel(view: view, contentRect: contentRect, isPresented: $isPresented) 46 | panel?.center() 47 | if isPresented { 48 | present() 49 | } 50 | }.onDisappear { 51 | /// When the view disappears, close and kill the panel 52 | panel?.close() 53 | panel = nil 54 | }.onChange(of: isPresented) { value in 55 | /// On change of the presentation state, make the panel react accordingly 56 | if value { 57 | // present() 58 | panel?.orderFront(nil) 59 | panel?.fadeIn() 60 | } else { 61 | // panel?.close() 62 | panel?.fadeOut() 63 | } 64 | } 65 | .onChange(of: viewModel.shared.karaoke) { value in 66 | if !value { 67 | panel?.close() 68 | } 69 | } 70 | } 71 | 72 | /// Present the panel and make it the key window 73 | func present() { 74 | panel?.orderFront(nil) 75 | // panel?.makeKey() 76 | panel?.fadeIn() 77 | } 78 | } 79 | 80 | private struct FloatingPanelKey: EnvironmentKey { 81 | static let defaultValue: NSPanel? = nil 82 | } 83 | 84 | extension EnvironmentValues { 85 | var floatingPanel: NSPanel? { 86 | get { self[FloatingPanelKey.self] } 87 | set { self[FloatingPanelKey.self] = newValue } 88 | } 89 | } 90 | class FloatingPanel: NSPanel { 91 | @Binding var isPresented: Bool 92 | init(view: () -> Content, 93 | contentRect: NSRect, 94 | backing: NSWindow.BackingStoreType = .buffered, 95 | defer flag: Bool = false, 96 | isPresented: Binding) { 97 | /// Initialize the binding variable by assigning the whole value via an underscore 98 | self._isPresented = isPresented 99 | 100 | /// Init the window as usual 101 | super.init(contentRect: contentRect, 102 | styleMask: [.nonactivatingPanel, .fullSizeContentView, .closable], //.resizable, .closable], //.fullSizeContentView], 103 | backing: .buffered, 104 | defer: true) 105 | 106 | /// Allow the panel to be on top of other windows 107 | isFloatingPanel = true 108 | level = .mainMenu 109 | 110 | /// Allow the pannel to be overlaid in a fullscreen space 111 | collectionBehavior.insert(.canJoinAllSpaces) 112 | 113 | // layer?.cornerRadius = 16.0 114 | 115 | /// Don't show a window title, even if it's set 116 | titleVisibility = .hidden 117 | titlebarAppearsTransparent = true 118 | // func centerAtBottom() { 119 | // guard let screenFrame = NSScreen.main?.frame else { return } 120 | // 121 | // // Calculate x position to center horizontally 122 | // let xPosition = (screenFrame.width - contentRect.width) / 2 123 | // 124 | // // Set y position to a small offset above the bottom of the screen 125 | // let yPosition: CGFloat = 50 // Adjust this to move it slightly up from the screen's bottom 126 | // 127 | // // Set the panel's frame origin 128 | // setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) 129 | // } 130 | // centerAtBottom() 131 | 132 | /// Since there is no title bar make the window moveable by dragging on the background 133 | isMovableByWindowBackground = true 134 | 135 | /// Hide when unfocused 136 | hidesOnDeactivate = false 137 | backgroundColor = NSColor.clear 138 | 139 | 140 | 141 | // hasTitleBar = true 142 | 143 | /// Hide all traffic light buttons 144 | // standardWindowButton(.closeButton)?.isHidden = true 145 | standardWindowButton(.miniaturizeButton)?.isHidden = true 146 | standardWindowButton(.zoomButton)?.isHidden = true 147 | 148 | /// Sets animations accordingly 149 | // animationBehavior = .documentWindow 150 | 151 | /// Set the content view. 152 | /// The safe area is ignored because the title bar still interferes with the geometry 153 | contentView = NSHostingView(rootView: view() 154 | // .ignoresSafeArea() 155 | .preferredColorScheme(.dark) 156 | // .background(VisualEffectView())//.ignoresSafeArea()) 157 | .environment(\.floatingPanel, self)) 158 | hasShadow = false 159 | // let visualEffect = NSVisualEffectView() 160 | // visualEffect.blendingMode = .behindWindow 161 | // visualEffect.state = .active 162 | // visualEffect.material = .hudWindow 163 | 164 | // contentView?.addSubview(visualEffect) 165 | // guard let constraints = self.contentView else { 166 | // return 167 | // } 168 | // visualEffect.leadingAnchor.constraint(equalTo: constraints.leadingAnchor).isActive = true 169 | // visualEffect.trailingAnchor.constraint(equalTo: constraints.trailingAnchor).isActive = true 170 | // visualEffect.topAnchor.constraint(equalTo: constraints.topAnchor).isActive = true 171 | // visualEffect.bottomAnchor.constraint(equalTo: constraints.bottomAnchor).isActive = true 172 | // contentView?.layer?.cornerRadius = 20 173 | // contentView?.layer?.backgroundColor = Color(hue: 3, saturation: 3, brightness: 3) 174 | // 175 | // let animation: CABasicAnimation = .init() 176 | // animation.delegate = self as! any CAAnimationDelegate 177 | // self.animations = [.alpha: animation] 178 | } 179 | 180 | // override func mouseEntered(with event: NSEvent) { 181 | // close() 182 | // } 183 | // 184 | // override func mouseExited(with event: NSEvent) { 185 | // orderFront(nil) 186 | //// makeKey() 187 | // fadeIn() 188 | // } 189 | 190 | override func resignMain() { 191 | super.resignMain() 192 | // close() 193 | } 194 | 195 | func fadeIn() { 196 | self.alphaValue = 0.0 197 | self.animator().alphaValue = 1.0 198 | } 199 | 200 | func fadeOut() { 201 | // let lol: NSAnimationContext = .init() 202 | NSAnimationContext.runAnimationGroup { hi in 203 | hi.duration = 0.1 204 | self.animator().alphaValue = 0.0 205 | } 206 | } 207 | 208 | 209 | /// Close and toggle presentation, so that it matches the current state of the panel 210 | override func close() { 211 | // fadeOut() 212 | NSAnimationContext.runAnimationGroup { hi in 213 | hi.completionHandler = { 214 | super.close() 215 | // self.isPresented = false 216 | } 217 | self.animator().alphaValue = 0.0 218 | } 219 | // super.close() 220 | // isPresented = false 221 | } 222 | 223 | /// `canBecomeKey` and `canBecomeMain` are both required so that text inputs inside the panel can receive focus 224 | // this is disabled because we don't want to steal key focus when karaoke pops back up 225 | // override var canBecomeKey: Bool { 226 | // return true 227 | // } 228 | 229 | override var canBecomeMain: Bool { 230 | return true 231 | } 232 | override func center() { 233 | let rect = self.screen?.frame 234 | self.setFrameOrigin(NSPoint(x: (rect!.width - self.frame.width)/2, y: (rect!.height - self.frame.height)/5)) 235 | } 236 | } 237 | 238 | //extension NSPanel. { 239 | // 240 | // enum Horizontal { 241 | // case left, center, right 242 | // } 243 | // 244 | // enum Vertical { 245 | // case top, center, bottom 246 | // } 247 | //} 248 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/FullscreenButtonIconStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullscreenIconStyle.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2025-05-06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Define a common button style for icons 11 | struct FullscreenButtonIconStyle: ButtonStyle { 12 | @Environment(\.isEnabled) private var isEnabled 13 | 14 | var ishovering = false 15 | func makeBody(configuration: Configuration) -> some View { 16 | configuration.label 17 | .foregroundColor(isEnabled ? .primary : .gray) 18 | .background(configuration.isPressed ? Color.gray.opacity(0.4) : Color.clear) 19 | .clipShape(RoundedRectangle(cornerRadius: 6)) 20 | .contentShape(RoundedRectangle(cornerRadius: 6)) 21 | .overlay( 22 | RoundedRectangle(cornerRadius: 6) 23 | .stroke(Color.gray.opacity(0.2), lineWidth: configuration.isPressed ? 0 : 0) 24 | ) 25 | } 26 | } 27 | 28 | struct HoverableIcon: View { 29 | let systemName: String 30 | var sideLength: CGFloat = 32 31 | @State private var isHovering = false 32 | var disabled: Bool = false 33 | 34 | var body: some View { 35 | ZStack { 36 | Image(systemName: systemName) 37 | if disabled { 38 | Capsule() 39 | .frame(width: 40, height: 4) 40 | .rotationEffect(.degrees(-45)) 41 | .blendMode(.destinationOut) 42 | 43 | // Actual slash on top 44 | Capsule() 45 | // .fill(Color.white) 46 | .frame(width: 32, height: 2) 47 | .rotationEffect(.degrees(-45)) 48 | } 49 | } 50 | .compositingGroup() 51 | .frame(width: sideLength, height: sideLength) 52 | // .padding(6) 53 | .background(isHovering ? Color.gray.opacity(0.4) : Color.clear) 54 | .onHover { hover in 55 | isHovering = hover 56 | } 57 | .overlay( 58 | RoundedRectangle(cornerRadius: 6) 59 | .stroke(Color.gray.opacity(0.2), lineWidth: isHovering ? 1 : 0) 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/FullscreenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullscreenView.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2024-07-27. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | import ColorKit 11 | import Combine 12 | import TipKit 13 | 14 | 15 | @available(macOS 14.0, *) 16 | struct NewSettings: Tip { 17 | var title: Text { 18 | Text("Change fullscreen settings here!") 19 | } 20 | 21 | } 22 | 23 | @available(macOS 14.0, *) 24 | struct FullscreenView: View { 25 | @EnvironmentObject var viewmodel: viewModel 26 | @State var newSpotifyMusicArtworkImage: NSImage? 27 | @Binding var spotifyOrAppleMusic: Bool 28 | @State var newArtworkUrl: String? 29 | @State var newAppleMusicArtworkImage: NSImage? 30 | @State var animate = true 31 | @State var showSettingsPopover = false 32 | @State var gradient = [Color(red: 33/255, green: 69/255, blue: 152/255),Color(red: 218/255, green: 62/255, blue: 136/255)] 33 | @State var timer = Timer 34 | .publish(every: BackgroundView.animationDuration, on: .main, in: .common) 35 | .autoconnect() 36 | @State var currentHover = hoverOptions.none 37 | @State var points: ColorSpots = .init() 38 | 39 | var canDisplayLyrics: Bool { 40 | viewmodel.showLyrics && !viewmodel.lyricsIsEmptyPostLoad 41 | } 42 | 43 | enum hoverOptions { 44 | case playpause 45 | case showlyrics 46 | case pauseanimation 47 | case volumelow 48 | case volumehigh 49 | case translate 50 | case none 51 | case settings 52 | case sharing 53 | } 54 | 55 | @ViewBuilder func FullscreenButtons() -> some View { 56 | let highlightTip = NewSettings() 57 | HStack(alignment: .center, spacing: 6) { 58 | Button { 59 | if !spotifyOrAppleMusic, let soundVolume = viewmodel.spotifyScript?.soundVolume { 60 | viewmodel.spotifyScript?.setSoundVolume?(soundVolume-5) 61 | } else if let soundVolume = viewmodel.appleMusicScript?.soundVolume { 62 | viewmodel.appleMusicScript?.setSoundVolume?(soundVolume-5) 63 | } 64 | } label: { 65 | HoverableIcon(systemName: "speaker.minus") 66 | } 67 | .buttonStyle(FullscreenButtonIconStyle()) 68 | .onHover { hover in currentHover = hover ? .volumelow : .none } 69 | .keyboardShortcut(.downArrow, modifiers: []) 70 | 71 | Button { 72 | spotifyOrAppleMusic ? viewmodel.appleMusicScript?.playpause?() : viewmodel.spotifyScript?.playpause?() 73 | } label: { 74 | HoverableIcon(systemName: viewmodel.isPlaying ? "pause" : "play") 75 | } 76 | .buttonStyle(FullscreenButtonIconStyle()) 77 | .onHover { hover in currentHover = hover ? .playpause : .none } 78 | .keyboardShortcut(" ", modifiers: []) 79 | 80 | Button { 81 | if !spotifyOrAppleMusic, let soundVolume = viewmodel.spotifyScript?.soundVolume { 82 | viewmodel.spotifyScript?.setSoundVolume?(soundVolume+5) 83 | } else if let soundVolume = viewmodel.appleMusicScript?.soundVolume { 84 | viewmodel.appleMusicScript?.setSoundVolume?(soundVolume+5) 85 | } 86 | } label: { 87 | HoverableIcon(systemName: "speaker.plus") 88 | } 89 | .buttonStyle(FullscreenButtonIconStyle()) 90 | .onHover { hover in currentHover = hover ? .volumehigh : .none } 91 | .keyboardShortcut(.upArrow, modifiers: []) 92 | } 93 | .font(.system(size: 15)) 94 | // .font(.system(size: 16)) // consistent icon size 95 | HStack(alignment: .center, spacing: 5) { 96 | Button { 97 | if viewmodel.showLyrics { 98 | viewmodel.showLyrics = false 99 | viewmodel.stopLyricUpdater() 100 | } else { 101 | viewmodel.showLyrics = true 102 | // Only Spotify has access to Fullscreen view 103 | viewmodel.startLyricUpdater(appleMusicOrSpotify: spotifyOrAppleMusic) 104 | } 105 | 106 | } label: { 107 | HoverableIcon(systemName: "music.note.list", sideLength: 28, disabled: !viewmodel.showLyrics) 108 | 109 | } 110 | .buttonStyle(FullscreenButtonIconStyle()) 111 | .onHover { hover in 112 | currentHover = hover ? .showlyrics : .none 113 | } 114 | .keyboardShortcut("h") 115 | .disabled(viewmodel.currentlyPlayingLyrics.isEmpty) 116 | 117 | Button { 118 | viewmodel.translate.toggle() 119 | } label: { 120 | HoverableIcon(systemName: "translate", sideLength: 28, disabled: !viewmodel.translate) 121 | } 122 | .buttonStyle(FullscreenButtonIconStyle()) 123 | .onHover { hover in 124 | currentHover = hover ? .translate : .none 125 | } 126 | .keyboardShortcut("t") 127 | .disabled(viewmodel.currentlyPlayingLyrics.isEmpty) 128 | 129 | 130 | 131 | Button { 132 | // withAnimation { 133 | animate.toggle() 134 | // } 135 | if animate { 136 | timer = Timer 137 | .publish(every: BackgroundView.animationDuration, on: .main, in: .common) 138 | .autoconnect() 139 | withAnimation(.easeInOut(duration: BackgroundView.animationDuration)) { 140 | points = self.gradient.map { .random(withColor: $0) } 141 | } 142 | } else { 143 | timer.upstream.connect().cancel() 144 | } 145 | } label: { 146 | HoverableIcon(systemName: "leaf", sideLength: 28, disabled: !animate) 147 | } 148 | .buttonStyle(FullscreenButtonIconStyle()) 149 | .onHover { hover in 150 | currentHover = hover ? .pauseanimation : .none 151 | } 152 | .keyboardShortcut("a") 153 | 154 | Button { 155 | highlightTip.invalidate(reason: .actionPerformed) 156 | showSettingsPopover = true 157 | } label: { 158 | HoverableIcon(systemName: "gear", sideLength: 28) 159 | } 160 | .buttonStyle(FullscreenButtonIconStyle()) 161 | .popoverTip(highlightTip, arrowEdge: .bottom) 162 | .onHover { hover in 163 | currentHover = hover ? .settings : .none 164 | } 165 | .popover(isPresented: $showSettingsPopover) { 166 | VStack(spacing: 7) { 167 | Toggle("Blur surrounding lyrics", isOn: $viewmodel.blurFullscreen) 168 | 169 | // Toggle("Album art size scaling:", isOn: $viewmodel) 170 | // Toggle("Lyrics size scaling: ", isOn: <#T##Binding#>) 171 | Toggle("Animate on startup", isOn: $viewmodel.animateOnStartupFullscreen) 172 | // Toggle("Hide dock icon on fullscreen", isOn: $viewmodel.hideDockFullscreen) 173 | Button("Reset to default") { 174 | 175 | } 176 | } 177 | .padding(10) 178 | } 179 | if let currentlyPlaying = viewmodel.currentlyPlaying, currentlyPlaying.count == 22 { 180 | ShareLink(item: URL(string: "http://open.spotify.com/track/\(currentlyPlaying)")!) { 181 | HoverableIcon(systemName: "square.and.arrow.up.circle.fill", sideLength: 30) 182 | } 183 | .imageScale(.large) 184 | .buttonStyle(FullscreenButtonIconStyle()) 185 | .onHover { hover in 186 | currentHover = hover ? .sharing : .none 187 | } 188 | } 189 | } 190 | .font(.system(size: 12)) 191 | } 192 | 193 | @ViewBuilder var albumArt: some View { 194 | VStack { 195 | Spacer() 196 | if spotifyOrAppleMusic, let newAppleMusicArtworkImage { 197 | Image(nsImage: newAppleMusicArtworkImage) 198 | .resizable() 199 | .clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10, bottomTrailing: 10, topTrailing: 10))) 200 | .shadow(radius: 5) 201 | .frame(width: canDisplayLyrics ? 550 : 700, height: canDisplayLyrics ? 550 : 700) 202 | } 203 | else if let newArtworkUrl { 204 | WebImage(url: .init(string: newArtworkUrl), options: .queryMemoryData) 205 | .resizable() 206 | .placeholder(content: { 207 | Image(systemName: "music.note.list") 208 | .resizable() 209 | .shadow(radius: 3) 210 | .scaleEffect(0.5) 211 | .background(.gray) 212 | }) 213 | .onSuccess { image, data, cacheType in 214 | if let data { 215 | newSpotifyMusicArtworkImage = NSImage(data: data) 216 | } 217 | } 218 | .clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10, bottomTrailing: 10, topTrailing: 10))) 219 | .shadow(radius: 5) 220 | .frame(width: canDisplayLyrics ? 550 : 700, height: canDisplayLyrics ? 550 : 700) 221 | } else { 222 | Image(systemName: "music.note.list") 223 | .resizable() 224 | .shadow(radius: 3) 225 | .scaleEffect(0.5) 226 | .background(.gray) 227 | .clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10, bottomTrailing: 10, topTrailing: 10))) 228 | .shadow(radius: 5) 229 | .frame(width: canDisplayLyrics ? 550 : 650, height: canDisplayLyrics ? 550 : 650) 230 | } 231 | Group { 232 | Text(verbatim: viewmodel.currentlyPlayingName ?? "") 233 | .font(.title) 234 | .bold() 235 | .padding(.top, 30) 236 | Text(verbatim: viewmodel.currentlyPlayingArtist ?? "") 237 | .font(.title2) 238 | } 239 | .frame(height: 35) 240 | FullscreenButtons() 241 | .frame(height: 25) 242 | .buttonStyle(.plain) 243 | .imageScale(.large) 244 | .bold() 245 | Text(displayHoverTooltip()) 246 | .textCase(.uppercase) 247 | .font(.system(size: 14, weight: .light, design: .monospaced)) 248 | .frame(height: 20) 249 | Spacer() 250 | } 251 | } 252 | 253 | func displayHoverTooltip() -> LocalizedStringKey { 254 | switch currentHover { 255 | case .playpause: 256 | viewmodel.isPlaying ? "Pause (spacebar)" : "Play (spacebar)" 257 | case .showlyrics: 258 | viewmodel.showLyrics ? "Hide lyrics (⌘ + H)" : "Show lyrics (⌘ + H)" 259 | case .pauseanimation: 260 | animate ? "Pause animations (saves battery) (⌘ + A)" : "Unpause animations (uses battery) (⌘ + A)" 261 | case .volumelow: 262 | "Decrease volume by 5 (Down Arrow)" 263 | case .volumehigh: 264 | "Increase volume by 5 (Up Arrow)" 265 | case .none: 266 | "" 267 | case .translate: 268 | viewmodel.translate ? "Hide translations (⌘ + T)" : "Translate lyrics (⌘ + T)" 269 | case .settings: 270 | "Display fullscreen options" 271 | case .sharing: 272 | "Share Spotify link" 273 | } 274 | } 275 | 276 | @ViewBuilder func lyricLineView(for element: LyricLine, index: Int) -> some View { 277 | VStack(alignment: .leading, spacing: 3) { 278 | if !viewmodel.romanizedLyrics.isEmpty { 279 | Text(verbatim: viewmodel.romanizedLyrics[index]) 280 | .foregroundStyle(.white) 281 | } else { 282 | Text(verbatim: element.words) 283 | .foregroundStyle(.white) 284 | } 285 | if viewmodel.translationExists { 286 | Text(verbatim: viewmodel.translatedLyric[index]) 287 | .font(.system(size: 33, weight: .semibold, design: .default)) 288 | .opacity(0.85) 289 | } 290 | } 291 | } 292 | 293 | @ViewBuilder func lyrics(padding: CGFloat) -> some View { 294 | ZStack { 295 | if viewmodel.currentlyPlayingLyrics.isEmpty { 296 | ProgressView() 297 | } 298 | VStack(alignment: .leading){ 299 | Spacer() 300 | ScrollViewReader { proxy in 301 | List (Array(viewmodel.currentlyPlayingLyrics.enumerated()), id: \.element) { offset, element in 302 | lyricLineView(for: element, index: offset) 303 | .opacity(offset == viewmodel.currentlyPlayingLyrics.count - 1 ? 0 : (offset == viewmodel.currentlyPlayingLyricsIndex ? 1 : 0.8)) 304 | .font(.system(size: 40, weight: .bold, design: .default)) 305 | .padding(20) 306 | .listRowSeparator(.hidden) 307 | .blur(radius: viewmodel.blurFullscreen ? (offset == viewmodel.currentlyPlayingLyricsIndex ? 0 : 5) : 0) 308 | } 309 | .onAppear { 310 | Task { 311 | try? await Task.sleep(nanoseconds: 1_000_000_000) 312 | if let currentIndex = viewmodel.currentlyPlayingLyricsIndex { 313 | proxy.scrollTo(viewmodel.currentlyPlayingLyrics[currentIndex], anchor: .center) 314 | } 315 | } 316 | } 317 | .padding(.trailing, 100) 318 | .safeAreaInset(edge: .top) { 319 | Spacer() 320 | .id("first") 321 | .frame(height: padding) 322 | } 323 | .safeAreaInset(edge: .bottom) { 324 | Spacer() 325 | .id("last") 326 | .frame(height: padding) 327 | } 328 | .onChange(of: viewmodel.translatedLyric) { 329 | withAnimation() { 330 | if let currentIndex = viewmodel.currentlyPlayingLyricsIndex { 331 | proxy.scrollTo(viewmodel.currentlyPlayingLyrics[currentIndex], anchor: .center) 332 | } else { 333 | proxy.scrollTo("first", anchor: .top) 334 | } 335 | 336 | } 337 | } 338 | .onChange(of: viewmodel.currentlyPlayingLyricsIndex) { 339 | withAnimation() { 340 | if let currentIndex = viewmodel.currentlyPlayingLyricsIndex { 341 | proxy.scrollTo(viewmodel.currentlyPlayingLyrics[currentIndex], anchor: .center) 342 | } else { 343 | proxy.scrollTo("first", anchor: .top) 344 | } 345 | 346 | } 347 | } 348 | } 349 | .scrollContentBackground(.hidden) 350 | .scrollDisabled(true) 351 | .mask(LinearGradient(gradient: Gradient(colors: [.clear, .black, .clear]), startPoint: .top, endPoint: .bottom)) 352 | Spacer() 353 | 354 | } 355 | } 356 | } 357 | 358 | var body: some View { 359 | if viewmodel.fullscreenInProgress { 360 | // The animation to fullscreen can look jarring otherwise 361 | Color(.windowBackgroundColor) 362 | } else { 363 | GeometryReader { geo in 364 | HStack { 365 | albumArt 366 | .frame( minWidth: 0.50*(geo.size.width), maxWidth: canDisplayLyrics ? 0.50*(geo.size.width) : .infinity) 367 | if canDisplayLyrics { 368 | lyrics(padding: 0.5*(geo.size.height)) 369 | .frame( minWidth: 0.50*(geo.size.width), maxWidth: 0.50*(geo.size.width)) 370 | } 371 | } 372 | } 373 | .background { 374 | ZStack { 375 | BackgroundView(colors: $gradient, timer: $timer, points: $points) 376 | } 377 | .ignoresSafeArea() 378 | .transition(.opacity) 379 | } 380 | .onAppear { 381 | if !viewmodel.animateOnStartupFullscreen { 382 | animate = false 383 | timer.upstream.connect().cancel() 384 | } 385 | do { 386 | // try Tips.resetDatastore() 387 | try Tips.configure() 388 | } 389 | catch { 390 | print("Error configuring tips: \(error)") 391 | } 392 | 393 | if !spotifyOrAppleMusic, let artworkUrl = viewmodel.spotifyScript?.currentTrack?.artworkUrl, artworkUrl != "" { 394 | print(artworkUrl) 395 | withAnimation { 396 | self.newArtworkUrl = artworkUrl 397 | } 398 | } 399 | else if spotifyOrAppleMusic, let artwork = (viewmodel.appleMusicScript?.currentTrack?.artworks?().firstObject as? MusicArtwork)?.data { 400 | newAppleMusicArtworkImage = artwork 401 | } 402 | else if let artistName = viewmodel.currentlyPlayingArtist, let albumName = viewmodel.spotifyScript?.currentTrack?.album { 403 | print("\(artistName) \(albumName)") 404 | Task { 405 | if let mbid = await viewmodel.findMbid(albumName: albumName, artistName: artistName) { 406 | withAnimation { 407 | self.newArtworkUrl = "https://coverartarchive.org/release/\(mbid)/front" 408 | } 409 | } 410 | 411 | } 412 | } 413 | } 414 | .onChange(of: newAppleMusicArtworkImage) { newArtwork in 415 | print("NEW ARTWORK") 416 | if let newArtwork, let dominantColors = try? newArtwork.dominantColors(with: .best, algorithm: .kMeansClustering) { 417 | gradient = dominantColors.map({adjustedColor($0)}) 418 | } 419 | } 420 | .onChange(of: newSpotifyMusicArtworkImage) { newArtwork in 421 | print("NEW ARTWORK") 422 | if let newArtwork, var dominantColors = try? newArtwork.dominantColors(with: .best, algorithm: .kMeansClustering) { 423 | dominantColors.sort(by: {$0.saturationComponent > $1.saturationComponent}) 424 | gradient = dominantColors.map({adjustedColor($0)}) 425 | } 426 | } 427 | .onChange(of: viewmodel.currentlyPlayingName) { _ in 428 | if !spotifyOrAppleMusic, let artworkUrl = viewmodel.spotifyScript?.currentTrack?.artworkUrl, artworkUrl != "" { 429 | print("spotify artwork is \(artworkUrl)") 430 | withAnimation { 431 | self.newArtworkUrl = artworkUrl 432 | } 433 | } 434 | else if spotifyOrAppleMusic, let artwork = (viewmodel.appleMusicScript?.currentTrack?.artworks?().firstObject as? MusicArtwork)?.data { 435 | newAppleMusicArtworkImage = artwork 436 | } 437 | else if let artistName = viewmodel.currentlyPlayingArtist, let albumName = spotifyOrAppleMusic ? viewmodel.appleMusicScript?.currentTrack?.album : viewmodel.spotifyScript?.currentTrack?.album { 438 | print("\(artistName) \(albumName)") 439 | Task { 440 | if let mbid = await viewmodel.findMbid(albumName: albumName, artistName: artistName) { 441 | withAnimation { 442 | print("setting newartwork url to: https://coverartarchive.org/release/\(mbid)/front") 443 | self.newArtworkUrl = "https://coverartarchive.org/release/\(mbid)/front" 444 | } 445 | } 446 | 447 | } 448 | } 449 | } 450 | } 451 | } 452 | func adjustedColor(_ nsColor: NSColor) -> Color { 453 | // Convert NSColor to HSB components 454 | var hue: CGFloat = 0 455 | var saturation: CGFloat = 0 456 | var brightness: CGFloat = 0 457 | var alpha: CGFloat = 0 458 | 459 | nsColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) 460 | 461 | // Adjust brightness 462 | brightness = max(brightness - 0.2, 0.1) 463 | 464 | if saturation < 0.9 { 465 | // Adjust contrast 466 | saturation = max(0.1, saturation * 3) 467 | } 468 | 469 | // Create new NSColor with modified HSB values 470 | let modifiedNSColor = NSColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) 471 | 472 | // Convert NSColor to SwiftUI Color 473 | return Color(modifiedNSColor) 474 | } 475 | } 476 | 477 | @available(macOS 14.0, *) 478 | struct BackgroundView: View { 479 | @Binding var colors: [SwiftUI.Color] 480 | @Binding var timer: Publishers.Autoconnect 481 | @Binding var points: ColorSpots 482 | 483 | static let animationDuration: Double = 20 484 | @State var bias: Float = 0.002 485 | @State var power: Float = 2.5 486 | @State var noise: Float = 2 487 | 488 | var body: some View { 489 | MulticolorGradient( 490 | points: points, 491 | bias: bias, 492 | power: power, 493 | noise: noise 494 | ) 495 | .onChange(of: colors) { 496 | print("change color called") 497 | withAnimation(.easeInOut(duration: BackgroundView.animationDuration/2)){ 498 | points = self.colors.map { .random(withColor: $0) } 499 | } 500 | } 501 | .onReceive(timer) { _ in 502 | withAnimation(.easeInOut(duration: BackgroundView.animationDuration)) { 503 | points = self.colors.map { .random(withColor: $0) } 504 | } 505 | } 506 | } 507 | 508 | } 509 | 510 | private extension ColorSpot { 511 | static func random(withColor color: SwiftUI.Color) -> ColorSpot { 512 | .init( 513 | position: .init(x: CGFloat.random(in: 0 ..< 1), y: CGFloat.random(in: 0 ..< 1)), 514 | color: color 515 | ) 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SUAllowsAutomaticUpdates 6 | 7 | SUAutomaticallyUpdate 8 | 9 | SUEnableAutomaticChecks 10 | 11 | SUEnableDownloaderService 12 | 13 | SUEnableInstallerLauncherService 14 | 15 | SUFeedURL 16 | https://aviwad.github.io/SpotifyLyricsInMenubar/appcast.xml 17 | SUPublicEDKey 18 | FSX9uBGItlJwhydb/y9bFiAoZKFy1YaTjgiR/0bcVbI= 19 | UTExportedTypeDeclarations 20 | 21 | 22 | UTTypeIcons 23 | 24 | UTTypeTagSpecification 25 | 26 | public.filename-extension 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/KaraokeSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KaraokeSettings.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2025-02-08. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | import CompactSlider 11 | import FontPicker 12 | 13 | struct KaraokeSettingsView: View { 14 | @EnvironmentObject var viewmodel: viewModel 15 | var body: some View { 16 | VStack(spacing: 12) { 17 | 18 | Text("Karaoke Behaviour") 19 | // .bold() 20 | .font(.system(size: 15, weight: .bold)) 21 | Toggle(isOn: $viewmodel.karaokeModeHoveringSetting) { 22 | Text("Hide Karaoke window when mouse passes by") 23 | } 24 | .toggleStyle(.checkbox) 25 | Toggle(isOn: $viewmodel.karaokeShowMultilingual) { 26 | Text("Show multilingual lyrics when translating in Karaoke window") 27 | } 28 | .toggleStyle(.checkbox) 29 | .padding(.bottom, 20) 30 | // Toggle(isOn: $viewmodel.karaokeConstantBackgroundWindow) { 31 | // Text("Hide Karaoke window when mouse passes by") 32 | // } 33 | // .toggleStyle(.checkbox) 34 | 35 | Text("Karaoke Background Appearance") 36 | .font(.system(size: 15, weight: .bold)) 37 | 38 | Toggle(isOn: $viewmodel.karaokeUseAlbumColor.animation(.bouncy)) { 39 | Text("Use album color for Karaoke window") 40 | } 41 | .toggleStyle(.checkbox) 42 | if !viewmodel.karaokeUseAlbumColor { 43 | ColorPicker("Set a background color", selection: viewmodel.colorBinding, supportsOpacity: false) 44 | } 45 | Text("Opacity Level: \(Int(viewmodel.karaokeTransparency))%") 46 | CompactSlider(value: $viewmodel.karaokeTransparency, in: 1...100, step: 5) { 47 | Text("Opacity Level:") 48 | Spacer() 49 | Text("\(Int(viewmodel.karaokeTransparency))%") 50 | } 51 | .frame(width: 300, height: 24) 52 | .padding(.bottom, 20) 53 | 54 | Text("Karaoke Font Appearance") 55 | // .bold() 56 | .font(.system(size: 15, weight: .bold)) 57 | FontPicker("Select a Font:", selection: $viewmodel.karaokeFont) 58 | Text("Font Selected: \(viewmodel.karaokeFont.displayName ?? ""), Size: \(Int(viewmodel.karaokeFont.pointSize))") 59 | .font(.custom(viewmodel.karaokeFont.fontName, size: 13)) 60 | 61 | .frame(width: 300, height: 24) 62 | Button("Reset to default") { 63 | viewmodel.karaokeModeHoveringSetting = false 64 | viewmodel.karaokeUseAlbumColor = true 65 | viewmodel.karaokeShowMultilingual = true 66 | viewmodel.karaokeTransparency = 50 67 | viewmodel.karaokeFont = NSFont.boldSystemFont(ofSize: 30) 68 | // viewmodel.karaokeFontSize = 30 69 | viewmodel.colorBinding.wrappedValue = Color(.sRGB, red: 0.98, green: 0.0, blue: 0.98) 70 | 71 | } 72 | } 73 | .padding(.horizontal) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/KaraokeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KaraokeView.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2024-10-08. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | import ColorKit 11 | import Combine 12 | 13 | struct VisualEffectView: NSViewRepresentable { 14 | // let material: NSVisualEffectView.Material 15 | // let blendingMode: NSVisualEffectView.BlendingMode 16 | func makeNSView(context: Context) -> NSVisualEffectView { 17 | let view = NSVisualEffectView() 18 | 19 | view.blendingMode = .behindWindow 20 | view.state = .active 21 | view.material = .hudWindow 22 | // view.layer?.cornerRadius = 16.0 23 | // visualEffect.layer?.cornerRadius = 16.0 24 | 25 | return view 26 | } 27 | 28 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) { 29 | // 30 | nsView.material = .hudWindow 31 | nsView.blendingMode = .behindWindow 32 | } 33 | } 34 | 35 | //@available(macOS 14.0, *) 36 | struct KaraokeView: View { 37 | @EnvironmentObject var viewmodel: viewModel 38 | @Namespace var animation 39 | func multilingualView(_ currentlyPlayingLyricsIndex: Int) -> some View { 40 | VStack(spacing: 6) { 41 | if !viewmodel.romanizedLyrics.isEmpty { 42 | Text(verbatim: viewmodel.romanizedLyrics[currentlyPlayingLyricsIndex]) 43 | } else { 44 | Text(verbatim: viewmodel.currentlyPlayingLyrics[currentlyPlayingLyricsIndex].words) 45 | } 46 | Text(verbatim: viewmodel.translatedLyric[currentlyPlayingLyricsIndex]) 47 | .font(.custom(viewmodel.karaokeFont.fontName, size: 0.9*(viewmodel.karaokeFont.pointSize))) 48 | .opacity(0.85) 49 | } 50 | } 51 | 52 | @ViewBuilder func lyricsView() -> some View { 53 | if let currentlyPlayingLyricsIndex = viewmodel.currentlyPlayingLyricsIndex { 54 | if viewmodel.translationExists { 55 | if viewmodel.karaokeShowMultilingual { 56 | multilingualView(currentlyPlayingLyricsIndex) 57 | } 58 | else { 59 | Text(verbatim: viewmodel.translatedLyric[currentlyPlayingLyricsIndex]) 60 | } 61 | } else { 62 | if !viewmodel.romanizedLyrics.isEmpty { 63 | Text(verbatim: viewmodel.romanizedLyrics[currentlyPlayingLyricsIndex]) 64 | } else { 65 | Text(verbatim: viewmodel.currentlyPlayingLyrics[currentlyPlayingLyricsIndex].words) 66 | } 67 | } 68 | } else { 69 | Text("") 70 | } 71 | } 72 | 73 | var body: some View { 74 | lyricsView() 75 | // .animation(.easeInOut(duration: 0.2)) 76 | .lineLimit(2) 77 | // .id(viewmodel.currentlyPlayingLyricsIndex) 78 | .foregroundStyle(.white) 79 | .minimumScaleFactor(0.9) 80 | // .animation(.smooth(duration: 0.2)) 81 | // .transition( AnyTransition.asymmetric(insertion: .scale, removal: .opacity)) 82 | 83 | // .minimumScaleFactor(0.1) 84 | .font(.custom(viewmodel.karaokeFont.fontName, size: viewmodel.karaokeFont.pointSize)) 85 | // .font(.system(size: viewmodel.karaokeFontSize, weight: .bold, design: .default)) 86 | .padding(10) 87 | .padding(.horizontal, 10) 88 | .background { 89 | Group { 90 | if viewmodel.karaokeUseAlbumColor, let currentBackground = viewmodel.currentBackground { 91 | currentBackground 92 | } else { 93 | viewmodel.colorBinding.wrappedValue 94 | } 95 | } 96 | .transition(.opacity) 97 | .opacity(viewModel.shared.karaokeTransparency/100) 98 | // .drawingGroup() 99 | // .transition(.opacity) 100 | // .animation(nil) 101 | // .animation(.snappy(duration: 0.1), value: viewmodel.currentlyPlayingLyricsIndex) 102 | } 103 | .drawingGroup() 104 | .background( 105 | VisualEffectView().ignoresSafeArea() 106 | ) 107 | .cornerRadius(16) 108 | // .background(VisualEffectView().animation(nil)) 109 | .onHover { hover in 110 | if viewmodel.karaokeModeHoveringSetting { 111 | viewmodel.karaokeModeHovering = hover 112 | } 113 | } 114 | .multilineTextAlignment(.center) 115 | .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) 116 | // .animation(.default, value: viewmodel.currentlyPlayingLyricsIndex) 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Lyrics.xcdatamodeld/Lyrics.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/LyricsParser/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // SpotlightLyrics 4 | // 5 | // Created by Scott Rong on 2017/7/28. 6 | // Copyright © 2017 Scott Rong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CharacterSet { 12 | public static var quotes = CharacterSet(charactersIn: "\"'") 13 | } 14 | 15 | extension String { 16 | public func emptyToNil() -> String? { 17 | return self == "" ? nil : self 18 | } 19 | 20 | public func blankToNil() -> String? { 21 | return self.trimmingCharacters(in: .whitespacesAndNewlines) == "" ? nil : self 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/LyricsParser/LyricsHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LyricsHeader.swift 3 | // SpotlightLyrics 4 | // 5 | // Created by Scott Rong on 2017/7/28. 6 | // Copyright © 2017 Scott Rong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct LyricsHeader { 12 | // ti 13 | public var title: String? 14 | // ar 15 | public var author: String? 16 | // al 17 | public var album: String? 18 | // by 19 | public var by: String? 20 | // offset 21 | public var offset: TimeInterval = 0 22 | // re 23 | public var editor: String? 24 | // ve 25 | public var version: String? 26 | } 27 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/LyricsParser/LyricsParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LyricsParser.swift 3 | // SpotlightLyrics 4 | // 5 | // Created by Scott Rong on 2017/4/2. 6 | // Copyright © 2017 Scott Rong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class LyricsParser { 12 | 13 | public var header: LyricsHeader 14 | var lyrics: [LyricLine] = [] 15 | 16 | // MARK: Initializers 17 | 18 | public init(lyrics: String) { 19 | header = LyricsHeader() 20 | commonInit(lyrics: lyrics) 21 | } 22 | 23 | 24 | private func commonInit(lyrics: String) { 25 | header = LyricsHeader() 26 | parse(lyrics: lyrics) 27 | } 28 | 29 | 30 | // MARK: Privates 31 | 32 | private func parse(lyrics: String) { 33 | let lines = lyrics 34 | .replacingOccurrences(of: "\\n", with: "\n") 35 | .trimmingCharacters(in: .quotes) 36 | .trimmingCharacters(in: .newlines) 37 | .components(separatedBy: .newlines) 38 | 39 | for line in lines { 40 | parseLine(line: line) 41 | } 42 | 43 | // sort by time 44 | self.lyrics.sort{ $0.startTimeMS < $1.startTimeMS } 45 | 46 | // parse header into lyrics 47 | // insert header distribute by averge time intervals 48 | // if self.lyrics.count > 0 { 49 | // var headers: [String] = [] 50 | // 51 | // if let title = header.title { 52 | // headers.append(title) 53 | // } 54 | // 55 | // if let author = header.author { 56 | // headers.append(author) 57 | // } 58 | // if let album = header.album { 59 | // headers.append(album) 60 | // } 61 | // if let by = header.by { 62 | // headers.append(by) 63 | // } 64 | // if let editor = header.editor { 65 | // headers.append(editor) 66 | // } 67 | // 68 | //// let intervalPerHeader = self.lyrics[0].time / TimeInterval(headers.count) 69 | // 70 | //// var headerLyrics: [LyricsItem] = headers.enumerated().map { LyricsItem(time: intervalPerHeader * TimeInterval($0.offset), text: $0.element) } 71 | //// if (headerLyrics.count > 0) { 72 | //// headerLyrics.append(LyricsItem(time: intervalPerHeader * TimeInterval(headerLyrics.count), text: "")) 73 | //// } 74 | //// 75 | //// self.lyrics.insert(contentsOf: headerLyrics, at: 0) 76 | // } 77 | 78 | } 79 | 80 | private func parseLine(line: String) { 81 | guard let line = line.blankToNil() else { 82 | return 83 | } 84 | 85 | // if let title = parseHeader(prefix: "ti", line: line) { 86 | // header.title = title 87 | // return 88 | // } 89 | // if let author = parseHeader(prefix: "ar", line: line) { 90 | // header.author = author 91 | // return 92 | // } 93 | // if let album = parseHeader(prefix: "al", line: line) { 94 | // header.album = album 95 | // return 96 | // } 97 | // if let by = parseHeader(prefix: "by", line: line) { 98 | // header.by = by 99 | // return 100 | // } 101 | if let offset = parseHeader(prefix: "offset", line: line) { 102 | header.offset = TimeInterval(offset) ?? 0 103 | return 104 | } 105 | if !line.hasSuffix("]") { 106 | lyrics += parseLyric(line: line) 107 | } 108 | // if let editor = parseHeader(prefix: "re", line: line) { 109 | // header.editor = editor 110 | // return 111 | // } 112 | // if let version = parseHeader(prefix: "ve", line: line) { 113 | // header.version = version 114 | // return 115 | // } 116 | 117 | } 118 | 119 | private func parseHeader(prefix: String, line: String) -> String? { 120 | if line.hasPrefix("[" + prefix + ":") && line.hasSuffix("]") { 121 | let startIndex = line.index(line.startIndex, offsetBy: prefix.count + 2) 122 | let endIndex = line.index(line.endIndex, offsetBy: -1) 123 | return String(line[startIndex.. [LyricLine] { 130 | var cLine = line 131 | var items : [LyricLine] = [] 132 | while(cLine.hasPrefix("[")) { 133 | guard let closureIndex = cLine.range(of: "]")?.lowerBound else { 134 | break 135 | } 136 | 137 | let startIndex = cLine.index(cLine.startIndex, offsetBy: 1) 138 | let endIndex = cLine.index(closureIndex, offsetBy: -1) 139 | let amidString = String(cLine[startIndex..= 1 { 146 | second = TimeInterval(amidStrings[amidStrings.count - 1]) ?? 0 147 | } 148 | if amidStrings.count >= 2 { 149 | minute = TimeInterval(amidStrings[amidStrings.count - 2]) ?? 0 150 | } 151 | if amidStrings.count >= 3 { 152 | hour = TimeInterval(amidStrings[amidStrings.count - 3]) ?? 0 153 | } 154 | 155 | // items.append(LyricLine(startTime: 1000*(hour * 3600 + minute * 60 + second + header.offset), words: <#T##String#>)) 156 | // 157 | // cLine.removeSubrange(line.startIndex.. 18 | 19 | public var animatableData: ColorSpot.AnimatableData { 20 | get { 21 | .init(position.animatableData, color.resolve(in: .init()).animatableData) 22 | } 23 | set { 24 | position = .init(newValue.first) 25 | color = .init(newValue.second) 26 | } 27 | } 28 | } 29 | 30 | @available(macOS 14.0, *) 31 | private extension ColorSpot { 32 | static var zero: ColorSpot { 33 | .init(position: .zero, color: .black) 34 | } 35 | 36 | init(_ animatableData: ColorSpot.AnimatableData) { 37 | self.init( 38 | position: .init(animatableData.first), 39 | color: .init(animatableData.second) 40 | ) 41 | } 42 | } 43 | 44 | @available(macOS 14.0, *) 45 | private extension Color { 46 | init(_ animatableData: Color.Resolved.AnimatableData) { 47 | var resolvedColor = Color.Resolved(red: 0, green: 0, blue: 0) 48 | resolvedColor.animatableData = animatableData 49 | self.init(resolvedColor) 50 | } 51 | } 52 | 53 | private extension UnitPoint { 54 | static let animatableDataRatio = 55 | UnitPoint(x: 1, y: 1).animatableData.first / UnitPoint(x: 1, y: 1).x 56 | 57 | init(_ animatableData: UnitPoint.AnimatableData) { 58 | self.init( 59 | x: animatableData.first / UnitPoint.animatableDataRatio, 60 | y: animatableData.second / UnitPoint.animatableDataRatio 61 | ) 62 | } 63 | } 64 | 65 | typealias ColorSpots = [ColorSpot] 66 | 67 | @available(macOS 14.0, *) 68 | extension ColorSpots: Animatable { 69 | public var animatableData: ColorSpotsAnimatableData { 70 | get { .init(values: map { point in point.animatableData }) } 71 | set { self = newValue.values.map { .init($0) } } 72 | } 73 | } 74 | 75 | @available(macOS 14.0, *) 76 | extension ColorSpots { 77 | init(_ animatableData: ColorSpotsAnimatableData) { 78 | self = animatableData.values.map { .init($0) } 79 | } 80 | } 81 | 82 | @available(macOS 14.0, *) 83 | public struct ColorSpotsAnimatableData { 84 | var values: [ColorSpot.AnimatableData] 85 | } 86 | 87 | @available(macOS 14.0, *) 88 | extension ColorSpotsAnimatableData: VectorArithmetic { 89 | public static func - (lhs: ColorSpotsAnimatableData, rhs: ColorSpotsAnimatableData) -> ColorSpotsAnimatableData { 90 | .init( 91 | values: (0 ..< max(lhs.values.count, rhs.values.count)).map { 92 | (lhs.values[safe: $0] ?? .zero) - (rhs.values[safe: $0] ?? .zero) 93 | } 94 | ) 95 | } 96 | 97 | public static func + (lhs: ColorSpotsAnimatableData, rhs: ColorSpotsAnimatableData) -> ColorSpotsAnimatableData { 98 | .init( 99 | values: (0 ..< max(lhs.values.count, rhs.values.count)).map { 100 | (lhs.values[safe: $0] ?? .zero) + (rhs.values[safe: $0] ?? .zero) 101 | } 102 | ) 103 | } 104 | 105 | public mutating func scale(by rhs: Double) { 106 | values = values.map { $0.scaled(by: rhs) } 107 | } 108 | 109 | public var magnitudeSquared: Double { 110 | values.reduce(0) { $0 + $1.magnitudeSquared } 111 | } 112 | 113 | public static var zero: ColorSpotsAnimatableData { 114 | .init(values: [.zero]) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Model/GradientParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientParams.swift 3 | // RandomPathAinmation 4 | // 5 | // Created by Alexey Vorobyov on 09.09.2023. 6 | // 7 | 8 | struct GradientParams { 9 | var spots: ColorSpots = [] 10 | var bias: Float = 0.001 11 | var power: Float = 2 12 | var noise: Float = 2.0 13 | } 14 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Model/Uniforms.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Uniforms.swift 3 | // RandomPathAinmation 4 | // 5 | // Created by Alexey Vorobyov on 09.09.2023. 6 | // 7 | 8 | import simd 9 | import SwiftUI 10 | 11 | struct Uniforms { 12 | let pointCount: simd_int1 13 | 14 | let bias: simd_float1 15 | let power: simd_float1 16 | let noise: simd_float1 17 | 18 | let point0: simd_float2 19 | let point1: simd_float2 20 | let point2: simd_float2 21 | let point3: simd_float2 22 | let point4: simd_float2 23 | let point5: simd_float2 24 | let point6: simd_float2 25 | let point7: simd_float2 26 | 27 | let color0: simd_float3 28 | let color1: simd_float3 29 | let color2: simd_float3 30 | let color3: simd_float3 31 | let color4: simd_float3 32 | let color5: simd_float3 33 | let color6: simd_float3 34 | let color7: simd_float3 35 | } 36 | 37 | extension Uniforms { 38 | init(params: GradientParams) { 39 | self.init( 40 | pointCount: simd_int1(params.spots.count), 41 | bias: params.bias, 42 | power: params.power, 43 | noise: params.noise, 44 | point0: params.spots[safe: 0]?.position.simd ?? .zero, 45 | point1: params.spots[safe: 1]?.position.simd ?? .zero, 46 | point2: params.spots[safe: 2]?.position.simd ?? .zero, 47 | point3: params.spots[safe: 3]?.position.simd ?? .zero, 48 | point4: params.spots[safe: 4]?.position.simd ?? .zero, 49 | point5: params.spots[safe: 5]?.position.simd ?? .zero, 50 | point6: params.spots[safe: 6]?.position.simd ?? .zero, 51 | point7: params.spots[safe: 7]?.position.simd ?? .zero, 52 | color0: params.spots[safe: 0]?.color.simd ?? .zero, 53 | color1: params.spots[safe: 1]?.color.simd ?? .zero, 54 | color2: params.spots[safe: 2]?.color.simd ?? .zero, 55 | color3: params.spots[safe: 3]?.color.simd ?? .zero, 56 | color4: params.spots[safe: 4]?.color.simd ?? .zero, 57 | color5: params.spots[safe: 5]?.color.simd ?? .zero, 58 | color6: params.spots[safe: 6]?.color.simd ?? .zero, 59 | color7: params.spots[safe: 7]?.color.simd ?? .zero 60 | ) 61 | } 62 | } 63 | 64 | extension UnitPoint { 65 | var simd: simd_float2 { simd_float2(Float(x), Float(y)) } 66 | } 67 | 68 | extension Color { 69 | var simd: simd_float3 { 70 | // Convert SwiftUI Color to NSColor 71 | let nsColor = NSColor(self) 72 | 73 | // Convert the color to the sRGB color space 74 | let rgbColor = nsColor.usingColorSpace(.sRGB) ?? NSColor.white 75 | 76 | var red: CGFloat = 0 77 | var green: CGFloat = 0 78 | var blue: CGFloat = 0 79 | rgbColor.getRed(&red, green: &green, blue: &blue, alpha: nil) 80 | 81 | return simd_float3(Float(red), Float(green), Float(blue)) 82 | } 83 | } 84 | 85 | extension Collection { 86 | subscript(safe index: Index) -> Element? { 87 | return indices.contains(index) ? self[index] : nil 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/MulticolorGradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MulticolorGradient.swift 3 | // RandomPathAinmation 4 | // 5 | // Created by Alexey Vorobyov on 09.09.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 14.0, *) 11 | struct MulticolorGradient: View, Animatable { 12 | var points: ColorSpots 13 | var bias: Float = 0.001 14 | var power: Float = 2 15 | var noise: Float = 2 16 | 17 | var animatableData: ColorSpots.AnimatableData { 18 | get { points.animatableData } 19 | set { points = .init(newValue) } 20 | } 21 | 22 | var uniforms: Uniforms { 23 | .init(params: .init(spots: points, bias: bias, power: power, noise: noise)) 24 | } 25 | 26 | var body: some View { 27 | Rectangle() 28 | .colorEffect(ShaderLibrary.gradient(.boundingRect, .uniforms(uniforms))) 29 | } 30 | } 31 | 32 | @available(macOS 14.0, *) 33 | extension Shader.Argument { 34 | static func uniforms(_ param: Uniforms) -> Shader.Argument { 35 | var copy = param 36 | return .data(Data(bytes: ©, count: MemoryLayout.stride)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/OnboardingWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingWindow.swift 3 | // SpotifyLyricsInMenubar 4 | // 5 | // Created by Avi Wadhwa on 01/09/23. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | import ScriptingBridge 11 | import MusicKit 12 | import WebKit 13 | 14 | struct OnboardingWindow: View { 15 | @State var spotifyPermission: Bool = false 16 | @Environment(\.dismiss) var dismiss 17 | @Environment(\.controlActiveState) var controlActiveState 18 | @State var appleMusicPermission: Bool = false 19 | @State var appleMusicLibraryPermission: Bool = false 20 | @State var permissionMissing: Bool = false 21 | @State var isAnimating = true 22 | // @State private var selection: Int? = nil 23 | @AppStorage("spotifyOrAppleMusic") var spotifyOrAppleMusic: Bool = false 24 | @AppStorage("hasOnboarded") var hasOnboarded: Bool = false 25 | @State var errorMessage: String = "Please download the [official Spotify desktop client](https://www.spotify.com/in-en/download/mac/)" 26 | var body: some View { 27 | TabView { 28 | 29 | NavigationStack() { 30 | VStack(alignment: .center, spacing: 20) { 31 | Group { 32 | if permissionMissing { 33 | Group { 34 | AnimatedImage(name: "newPermissionMac.gif", isAnimating: $isAnimating) 35 | .resizable() 36 | .frame(width: 397, height: 340) 37 | HStack { 38 | Button("Open Automation Panel", action: { 39 | let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")! 40 | NSWorkspace.shared.open(url) 41 | }) 42 | if spotifyOrAppleMusic { 43 | Button("Open Music Panel", action: { 44 | let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Media")! 45 | NSWorkspace.shared.open(url) 46 | }) 47 | } 48 | } 49 | } 50 | } else { 51 | Group { 52 | Image("hi") 53 | .resizable() 54 | .frame(width: 150, height: 150, alignment: .center) 55 | 56 | Text("Welcome to Lyric Fever! 🎉") 57 | .font(.largeTitle) 58 | .onAppear() { 59 | 60 | } 61 | 62 | Text("Please pick between Spotify and Apple Music") 63 | .font(.title) 64 | } 65 | } 66 | } 67 | .transition(.fade) 68 | 69 | Group { 70 | Picker("", selection: $spotifyOrAppleMusic) { 71 | VStack { 72 | Image("spotify") 73 | .resizable() 74 | .frame(width: 70.0, height: 70.0) 75 | Text("Spotify") 76 | }.tag(false) 77 | VStack { 78 | Image("music") 79 | .resizable() 80 | .frame(width: 70.0, height: 70.0) 81 | Text("Apple Music") 82 | }.tag(true) 83 | } 84 | .font(.title2) 85 | .frame(width: 500) 86 | .pickerStyle(.radioGroup) 87 | .horizontalRadioGroupLayout() 88 | 89 | 90 | 91 | Text(LocalizedStringKey(errorMessage)) 92 | .transition(.opacity) 93 | .id(errorMessage) 94 | 95 | if spotifyPermission && appleMusicPermission && appleMusicLibraryPermission { 96 | NavigationLink("Next", destination: ApiView()) 97 | .font(.headline) 98 | .controlSize(.large) 99 | .buttonStyle(.borderedProminent) 100 | } else { 101 | HStack { 102 | Button("Give Spotify Permissions") { 103 | 104 | let target = NSAppleEventDescriptor(bundleIdentifier: "com.spotify.client") 105 | // Can cause a freeze if app we're querying for isn't open 106 | // See: https://forums.developer.apple.com/forums/thread/666528 107 | guard let spotify = NSRunningApplication.runningApplications(withBundleIdentifier: "com.spotify.client").first else { 108 | withAnimation { 109 | errorMessage = "Please open Spotify!" 110 | } 111 | return 112 | } 113 | let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true) 114 | switch status { 115 | case -600: 116 | errorMessage = "Please open Spotify!" 117 | case -0: 118 | withAnimation { 119 | permissionMissing = false 120 | spotifyPermission = true 121 | errorMessage = "" 122 | } 123 | default: 124 | withAnimation { 125 | errorMessage = "Please give required permissions!" 126 | permissionMissing = true 127 | isAnimating = true 128 | } 129 | } 130 | 131 | } 132 | 133 | .disabled(spotifyPermission) 134 | Button("Give Apple Music Permissions") { 135 | let target = NSAppleEventDescriptor(bundleIdentifier: "com.apple.Music") 136 | guard let music = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music").first else { 137 | withAnimation { 138 | errorMessage = "Please open Apple Music!" 139 | } 140 | return 141 | } 142 | let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true) 143 | switch status { 144 | case -600: 145 | errorMessage = "Please open Apple Music!" 146 | case -0: 147 | withAnimation { 148 | appleMusicPermission = true 149 | permissionMissing = false 150 | } 151 | isAnimating = false 152 | if appleMusicLibraryPermission { 153 | errorMessage = "" 154 | } else { 155 | errorMessage = "Please give us Apple Music Library permissions!" 156 | } 157 | default: 158 | withAnimation { 159 | permissionMissing = true 160 | } 161 | errorMessage = "Please give us required permissions!" 162 | permissionMissing = true 163 | isAnimating = true 164 | // OPEN AUTOMATION PANEL 165 | } 166 | 167 | } 168 | .disabled(appleMusicPermission) 169 | Button("Give Apple Music Library Permissions") { 170 | Task { 171 | let status = await MusicKit.MusicAuthorization.request() 172 | 173 | if status == .authorized { 174 | withAnimation { 175 | appleMusicLibraryPermission = true 176 | permissionMissing = false 177 | } 178 | isAnimating = false 179 | if appleMusicPermission { 180 | errorMessage = "" 181 | } else { 182 | errorMessage = "Please give us Apple Music permissions!" 183 | } 184 | } 185 | else { 186 | errorMessage = "Please give us required permissions!" 187 | withAnimation { 188 | permissionMissing = true 189 | } 190 | isAnimating = true 191 | } 192 | } 193 | } 194 | .disabled(appleMusicLibraryPermission) 195 | } 196 | } 197 | 198 | 199 | Text("Email me at [aviwad@gmail.com](mailto:aviwad@gmail.com) for any support\n⚠️ Disclaimer: I do not own the rights to Spotify or the lyric content presented.\nMusixmatch and Spotify own all rights to the lyrics.\n [Lyric Fever GitHub](https://github.com/aviwad/LyricFever)\nVersion 2.2") 200 | .multilineTextAlignment(.center) 201 | .font(.callout) 202 | .padding(.top, 10) 203 | .frame(alignment: .bottom) 204 | } 205 | .transition(.fade) 206 | 207 | } 208 | .onAppear { 209 | if spotifyOrAppleMusic { 210 | errorMessage = "Please open Apple Music!" 211 | spotifyPermission = true 212 | appleMusicPermission = false 213 | appleMusicLibraryPermission = false 214 | } else { 215 | errorMessage = "Please download the [official Spotify desktop client](https://www.spotify.com/in-en/download/mac/)" 216 | appleMusicPermission = true 217 | appleMusicLibraryPermission = true 218 | spotifyPermission = false 219 | } 220 | } 221 | .onReceive(NotificationCenter.default.publisher(for: Notification.Name("didClickSettings"))) { newValue in 222 | if spotifyOrAppleMusic { 223 | // first set spotify button to true, because we dont run the spotify or apple music boolean check on window open anymore 224 | errorMessage = "Please open Apple Music!" 225 | spotifyPermission = true 226 | appleMusicPermission = false 227 | appleMusicLibraryPermission = false 228 | 229 | 230 | // Check Apple Music Automation permission 231 | guard let music = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music").first else { 232 | withAnimation { 233 | errorMessage = "Please open Apple Music!" 234 | } 235 | return 236 | } 237 | let target = NSAppleEventDescriptor(bundleIdentifier: "com.apple.Music") 238 | let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true) 239 | switch status { 240 | case -600: 241 | errorMessage = "Please open Apple Music!" 242 | case -0: 243 | appleMusicPermission = true 244 | permissionMissing = false 245 | isAnimating = false 246 | if appleMusicLibraryPermission { 247 | errorMessage = "" 248 | } else { 249 | errorMessage = "Please give us Apple Music Library permissions!" 250 | } 251 | // case -1744: 252 | // Alert(title: Text("Please give permission by going to the Automation panel")) 253 | default: 254 | withAnimation { 255 | permissionMissing = true 256 | } 257 | errorMessage = "Please give us required permissions!" 258 | permissionMissing = true 259 | isAnimating = true 260 | // OPEN AUTOMATION PANEL 261 | } 262 | 263 | // Check Media Library Permission 264 | Task { 265 | let status = await MusicKit.MusicAuthorization.request() 266 | 267 | if status == .authorized { 268 | withAnimation { 269 | appleMusicLibraryPermission = true 270 | permissionMissing = false 271 | } 272 | isAnimating = false 273 | if appleMusicPermission { 274 | errorMessage = "" 275 | } else { 276 | errorMessage = "Please give us Apple Music permissions!" 277 | } 278 | } 279 | else { 280 | errorMessage = "Please give us required permissions!" 281 | withAnimation { 282 | permissionMissing = true 283 | } 284 | isAnimating = true 285 | } 286 | } 287 | 288 | } else { 289 | errorMessage = "Please download the [official Spotify desktop client](https://www.spotify.com/in-en/download/mac/)" 290 | appleMusicPermission = true 291 | appleMusicLibraryPermission = true 292 | spotifyPermission = false 293 | // Check Spotify 294 | guard let spotify = NSRunningApplication.runningApplications(withBundleIdentifier: "com.spotify.client").first else { 295 | withAnimation { 296 | errorMessage = "Please open Spotify!" 297 | } 298 | return 299 | } 300 | let target = NSAppleEventDescriptor(bundleIdentifier: "com.spotify.client") 301 | let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true) 302 | switch status { 303 | case -600: 304 | errorMessage = "Please open Spotify!" 305 | case -0: 306 | withAnimation { 307 | permissionMissing = false 308 | spotifyPermission = true 309 | errorMessage = "" 310 | } 311 | // case -1744: 312 | // Alert(title: Text("Please give permission by going to the Automation panel")) 313 | default: 314 | withAnimation { 315 | errorMessage = "Please give required permissions!" 316 | permissionMissing = true 317 | isAnimating = true 318 | } 319 | // OPEN AUTOMATION PANEL 320 | } 321 | } 322 | } 323 | .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in 324 | isAnimating = false 325 | permissionMissing = false 326 | } 327 | .onReceive(NotificationCenter.default.publisher(for: NSWindow.willMiniaturizeNotification)) { newValue in 328 | isAnimating = false 329 | permissionMissing = false 330 | } 331 | .onChange(of: spotifyOrAppleMusic) { newSpotifyOrAppleMusic in 332 | print("Updating permission booleans based on media player change") 333 | if spotifyOrAppleMusic { 334 | errorMessage = "Please open Apple Music!" 335 | spotifyPermission = true 336 | appleMusicPermission = false 337 | appleMusicLibraryPermission = false 338 | } else { 339 | errorMessage = "Please download the [official Spotify desktop client](https://www.spotify.com/in-en/download/mac/)" 340 | appleMusicPermission = true 341 | appleMusicLibraryPermission = true 342 | spotifyPermission = false 343 | } 344 | } 345 | .onChange(of: controlActiveState) { newState in 346 | if newState == .inactive { 347 | isAnimating = false 348 | } else { 349 | isAnimating = true 350 | } 351 | } 352 | } 353 | .tabItem { 354 | Label("Main Settings", systemImage: "person.crop.circle") 355 | } 356 | KaraokeSettingsView() 357 | .padding(.horizontal, 100) 358 | .tabItem { 359 | Label("Karaoke Window", systemImage: "person.crop.circle") 360 | } 361 | } 362 | } 363 | } 364 | 365 | struct ApiView: View { 366 | @Environment(\.dismiss) var dismiss 367 | @Environment(\.controlActiveState) var controlActiveState 368 | @State var isAnimating = true 369 | @State private var isShowingDetailView = false 370 | @AppStorage("spDcCookie") var spDcCookie: String = "" 371 | @State var isLoading = false 372 | @State var error = false 373 | @StateObject var navigationState = NavigationState() 374 | @State var loginMethod = true 375 | @State var loggedIn = false 376 | var body: some View { 377 | VStack(alignment: .leading, spacing: 16) { 378 | StepView(title: "Please log into Spotify", description: "I download lyrics from Spotify (and use LRCLIB and NetEase as backups)") 379 | 380 | Picker("", selection: $loginMethod) { 381 | Text("Spotify Login").tag(true) 382 | Text("API Key: Advanced").tag(false) 383 | } 384 | .pickerStyle(.segmented) 385 | 386 | if loginMethod { 387 | ZStack { 388 | // Blurred web view 389 | WebView(request: URLRequest(url: URL(string: "https://accounts.spotify.com/en/login?continue=https%3A%2F%2Fopen.spotify.com%2F")!), navigationState: navigationState) 390 | .disabled(loggedIn) 391 | .brightness(loggedIn ? -0.4 : 0) 392 | .blur(radius: loggedIn ? 15 : 0) 393 | 394 | if loggedIn { 395 | VStack { 396 | Text("You're Logged In 🙂") 397 | .font(.largeTitle) 398 | 399 | // Next button centered on the web view 400 | Button("Next") { 401 | Task { 402 | await checkForLogin() 403 | } 404 | // Handle next button action 405 | } 406 | .font(.headline) 407 | .controlSize(.large) 408 | .buttonStyle(.borderedProminent) 409 | Button("Log Out") { 410 | Task { 411 | loggedIn = false 412 | viewModel.shared.cookie = "" 413 | navigationState.webView.load(URLRequest(url: URL(string: "https://www.spotify.com/logout/")!)) 414 | try await Task.sleep(nanoseconds: 2000000000) 415 | navigationState.webView.load(URLRequest(url: URL(string: "https://accounts.spotify.com/en/login?continue=https%3A%2F%2Fopen.spotify.com%2F")!)) 416 | } 417 | } 418 | .font(.headline) 419 | .controlSize(.large) 420 | } 421 | } 422 | } 423 | } else { 424 | HStack { 425 | Spacer() 426 | AnimatedImage(name: "spotifylogin.gif", isAnimating: $isAnimating) 427 | .resizable() 428 | Spacer() 429 | } 430 | 431 | TextField("Enter your SP_DC Cookie Here", text: $spDcCookie) 432 | } 433 | 434 | HStack { 435 | Button("Back") { 436 | dismiss() 437 | } 438 | Button("Open Spotify on the Web", action: { 439 | let url = URL(string: "https://open.spotify.com")! 440 | NSWorkspace.shared.open(url) 441 | }) 442 | Spacer() 443 | NavigationLink(destination: FinalTruncationView(), isActive: $isShowingDetailView) {EmptyView()} 444 | .hidden() 445 | if error && !isLoading { 446 | Text("WRONG SP DC COOKIE TRY AGAIN ⚠️") 447 | .foregroundStyle(.red) 448 | } 449 | if isLoading { 450 | ProgressView() 451 | .scaleEffect(0.5) 452 | .frame(height: 20) 453 | } 454 | Button("Next") { 455 | Task { 456 | await checkForLogin() 457 | } 458 | // replace button with spinner 459 | // check if the cookie is legit 460 | // isLoading = false 461 | //isShowingDetailView = true 462 | } 463 | .buttonStyle(.borderedProminent) 464 | .disabled(isLoading || spDcCookie.count == 0) 465 | } 466 | .padding(.vertical, 15) 467 | 468 | } 469 | .padding(.horizontal, 20) 470 | .navigationBarBackButtonHidden(true) 471 | // .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in 472 | // dismiss() 473 | // } 474 | .onReceive(NotificationCenter.default.publisher(for: Notification.Name("didLogIn"))) { newValue in 475 | loggedIn = true 476 | } 477 | .onAppear { 478 | if spDcCookie.count > 0 { 479 | loggedIn = true 480 | } 481 | } 482 | .onReceive(NotificationCenter.default.publisher(for: NSWindow.willMiniaturizeNotification)) { newValue in 483 | dismiss() 484 | } 485 | .onChange(of: controlActiveState) { newState in 486 | if newState == .inactive { 487 | isAnimating = false 488 | print("inactive") 489 | } else { 490 | isAnimating = true 491 | } 492 | } 493 | } 494 | 495 | 496 | func checkForLogin() async { 497 | isLoading = true 498 | do { 499 | let serverTimeRequest = URLRequest(url: .init(string: "https://open.spotify.com/server-time")!) 500 | let serverTimeData = try await viewModel.shared.fakeSpotifyUserAgentSession.data(for: serverTimeRequest).0 501 | let serverTime = try JSONDecoder().decode(SpotifyServerTime.self, from: serverTimeData).serverTime 502 | if let totp = viewModel.TOTPGenerator.generate(serverTimeSeconds: serverTime), let url = URL(string: "https://open.spotify.com/get_access_token?reason=transport&productType=web-player&totp=\(totp)&totpServer=\(Int(Date().timeIntervalSince1970))&totpVer=5&sTime=\(serverTime)&cTime=\(serverTime)") { 503 | var request = URLRequest(url: url) 504 | request.setValue("sp_dc=\(spDcCookie)", forHTTPHeaderField: "Cookie") 505 | let accessTokenData = try await viewModel.shared.fakeSpotifyUserAgentSession.data(for: request) 506 | print(String(decoding: accessTokenData.0, as: UTF8.self)) 507 | do { 508 | let access = try JSONDecoder().decode(accessTokenJSON.self, from: accessTokenData.0) 509 | viewModel.shared.accessToken = access 510 | if await !viewModel.shared.fetchHomeTest() { 511 | viewModel.shared.accessToken = nil 512 | self.error = true 513 | isLoading = false 514 | } else { 515 | print("ACCESS TOKEN IS SAVED") 516 | // set onboarded to true here, no need to wait for user to finish selecting truncation 517 | UserDefaults().set(true, forKey: "hasOnboarded") 518 | error = false 519 | isLoading = false 520 | isShowingDetailView = true 521 | } 522 | } catch { 523 | self.error = true 524 | isLoading = false 525 | // do { 526 | let errorWrap = try? JSONDecoder().decode(ErrorWrapper.self, from: accessTokenData.0) 527 | if errorWrap?.error.code == 401 { 528 | loggedIn = false 529 | } 530 | // } catch { 531 | // // silently fail 532 | // } 533 | // print("json error decoding the access token, therefore bad cookie therefore un-onboard") 534 | } 535 | 536 | } 537 | } catch { 538 | self.error = true 539 | isLoading = false 540 | } 541 | // if let url = URL(string: "https://open.spotify.com/get_access_token?reason=transport&productType=web_player"){//&totp=\(getTotp())&totpVer=5&ts=\(getTimestamp())") { 542 | // do { 543 | // var request = URLRequest(url: url) 544 | // request.setValue("sp_dc=\(spDcCookie)", forHTTPHeaderField: "Cookie") 545 | // let accessTokenData = try await URLSession.shared.data(for: request) 546 | // print(String(decoding: accessTokenData.0, as: UTF8.self)) 547 | // do { 548 | // try JSONDecoder().decode(accessTokenJSON.self, from: accessTokenData.0) 549 | // 550 | // print("ACCESS TOKEN IS SAVED") 551 | // // set onboarded to true here, no need to wait for user to finish selecting truncation 552 | // UserDefaults().set(true, forKey: "hasOnboarded") 553 | // error = false 554 | // isLoading = false 555 | // isShowingDetailView = true 556 | // } 557 | // catch { 558 | // print("JSON ERROR CAUGHT") 559 | // self.error = true 560 | // isLoading = false 561 | // } 562 | // } 563 | // catch { 564 | // self.error = true 565 | // isLoading = false 566 | // } 567 | // } 568 | } 569 | } 570 | 571 | struct FinalTruncationView: View { 572 | @Environment(\.dismiss) var dismiss 573 | //@AppStorage("truncationLength") var truncationLength: Int = 40 574 | @State var truncationLength: Int = UserDefaults.standard.integer(forKey: "truncationLength") 575 | @Environment(\.controlActiveState) var controlActiveState 576 | let allTruncations = [30,40,50,60] 577 | var body: some View { 578 | VStack(alignment: .leading, spacing: 16) { 579 | StepView(title: "Set the Lyric Size", description: "This depends on how much free space you have in your menu bar!") 580 | 581 | HStack { 582 | Spacer() 583 | Image("\(truncationLength)") 584 | .resizable() 585 | .scaledToFit() 586 | .onAppear() { 587 | if truncationLength == 0 { 588 | truncationLength = 40 589 | } 590 | } 591 | Spacer() 592 | } 593 | 594 | HStack { 595 | Spacer() 596 | Picker("Truncation Length", selection: $truncationLength) { 597 | ForEach(allTruncations, id:\.self) { oneThing in 598 | Text("\(oneThing) Characters") 599 | } 600 | } 601 | .pickerStyle(.radioGroup) 602 | Spacer() 603 | } 604 | 605 | HStack { 606 | Button("Back") { 607 | dismiss() 608 | } 609 | Spacer() 610 | Button("Done") { 611 | NSApplication.shared.keyWindow?.close() 612 | 613 | } 614 | .buttonStyle(.borderedProminent) 615 | } 616 | .padding(.vertical, 15) 617 | 618 | } 619 | .onChange(of: truncationLength) { newLength in 620 | UserDefaults.standard.set(newLength, forKey: "truncationLength") 621 | } 622 | .padding(.horizontal, 20) 623 | .navigationBarBackButtonHidden(true) 624 | .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in 625 | dismiss() 626 | dismiss() 627 | dismiss() 628 | } 629 | .onReceive(NotificationCenter.default.publisher(for: NSWindow.willMiniaturizeNotification)) { newValue in 630 | dismiss() 631 | dismiss() 632 | dismiss() 633 | } 634 | } 635 | } 636 | 637 | 638 | struct StepView: View { 639 | var title: LocalizedStringKey 640 | var description: LocalizedStringKey 641 | 642 | var body: some View { 643 | VStack(alignment: .leading, spacing: 8) { 644 | Text(title) 645 | .font(.title2) 646 | .bold() 647 | 648 | Text(description) 649 | .font(.title3) 650 | } 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/OverridePrint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverridePrint.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2025-03-08. 6 | // 7 | 8 | //func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { 9 | // 10 | // #if DEBUG 11 | // 12 | // var idx = items.startIndex 13 | // let endIdx = items.endIndex 14 | // 15 | // repeat { 16 | // Swift.print(items[idx], separator: separator, terminator: idx == (endIdx - 1) ? terminator : separator) 17 | // idx += 1 18 | // } 19 | // while idx < endIdx 20 | // 21 | // #endif 22 | //} 23 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Shaders/MulticolorGradientShader.metal: -------------------------------------------------------------------------------- 1 | // 2 | // MulticolorGradientShader.metal 3 | // RandomPathAinmation 4 | // 5 | // Created by Alexey Vorobyov on 09.09.2023. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | typedef struct { 12 | int32_t pointCount; 13 | float bias; 14 | float power; 15 | float noise; 16 | float2 points[8]; 17 | float3 colors[8]; 18 | } Uniforms; 19 | 20 | float2 hash23(float3 p3) { 21 | p3 = fract(p3 * float3(443.897, 441.423, .0973)); 22 | p3 += dot(p3, p3.yzx + 19.19); 23 | return fract((p3.xx + p3.yz) * p3.zy); 24 | } 25 | 26 | 27 | [[ stitchable ]] half4 gradient( 28 | float2 position, 29 | half4 currentColor, 30 | float4 box, 31 | constant Uniforms& uniforms [[buffer(0)]], 32 | int size_in_bytes 33 | ) { 34 | float2 size = box.zw; 35 | float2 noise = hash23(float3(position / float2(size.x, size.x), 0)); 36 | float2 uv = (position + float2(sin(noise.x * 2 * M_PI_F), sin(noise.y * 2 * M_PI_F)) * uniforms.noise) / float2(size.x, size.x); 37 | 38 | float totalContribution = 0.0; 39 | float contribution[8]; 40 | 41 | // Compute contributions 42 | for (int i = 0; i < uniforms.pointCount; i++) { 43 | float2 pos = uniforms.points[i] * float2(1.0, float(size.y) / float(size.x)); 44 | pos = uv - pos; 45 | float dist = length(pos); 46 | float c = 1.0 / (uniforms.bias + pow(dist, uniforms.power)); 47 | contribution[i] = c; 48 | totalContribution += c; 49 | } 50 | 51 | // Contributions normalisation 52 | float3 col = float3(0, 0, 0); 53 | float inverseContribution = 1.0 / totalContribution; 54 | for (int i = 0; i < uniforms.pointCount; i++) { 55 | col += contribution[i] * inverseContribution * uniforms.colors[i]; 56 | } 57 | return half4(half3(col), 1.0); 58 | } 59 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/SpotifyLyricsInMenubar.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.automation.apple-events 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.scripting-targets 14 | 15 | com.apple.Music 16 | 17 | com.apple.Music.device 18 | com.apple.Music.library.playback 19 | com.apple.Music.user-interface 20 | com.apple.Music.playback 21 | com.apple.Music.playerInfo 22 | com.apple.Music.library.read 23 | com.apple.Music.library.read-write 24 | com.apple.Music.podcast 25 | 26 | com.spotify.client 27 | 28 | com.spotify.playback 29 | com.spotify.library 30 | 31 | 32 | com.apple.security.temporary-exception.mach-lookup.global-name 33 | 34 | $(PRODUCT_BUNDLE_IDENTIFIER)-spks 35 | $(PRODUCT_BUNDLE_IDENTIFIER)-spki 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/SpotifyScripting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpotifyScripting.swift 3 | // SpotifyLyricsInMenubar 4 | // 5 | // Created by Avi Wadhwa on 13/08/23. 6 | // 7 | 8 | import AppKit 9 | import ScriptingBridge 10 | 11 | @objc public protocol SBObjectProtocol: NSObjectProtocol { 12 | func get() -> Any! 13 | } 14 | 15 | @objc public protocol SBApplicationProtocol: SBObjectProtocol { 16 | func activate() 17 | var delegate: SBApplicationDelegate! { get set } 18 | var isRunning: Bool { get } 19 | } 20 | 21 | // MARK: SpotifyEPlS 22 | @objc public enum SpotifyEPlS : AEKeyword { 23 | case stopped = 0x6b505353 /* 'kPSS' */ 24 | case playing = 0x6b505350 /* 'kPSP' */ 25 | case paused = 0x6b505370 /* 'kPSp' */ 26 | } 27 | 28 | // MARK: SpotifyApplication 29 | @objc public protocol SpotifyApplication: SBApplicationProtocol { 30 | @objc optional var currentTrack: SpotifyTrack { get } // The current playing track. 31 | @objc optional var soundVolume: Int { get } // The sound output volume (0 = minimum, 100 = maximum) 32 | @objc optional var playerState: SpotifyEPlS { get } // Is Spotify stopped, paused, or playing? 33 | @objc optional var playerPosition: Double { get } // The player’s position within the currently playing track in seconds. 34 | @objc optional var repeatingEnabled: Bool { get } // Is repeating enabled in the current playback context? 35 | @objc optional var repeating: Bool { get } // Is repeating on or off? 36 | @objc optional var shufflingEnabled: Bool { get } // Is shuffling enabled in the current playback context? 37 | @objc optional var shuffling: Bool { get } // Is shuffling on or off? 38 | @objc optional func nextTrack() // Skip to the next track. 39 | @objc optional func previousTrack() // Skip to the previous track. 40 | @objc optional func playpause() // Toggle play/pause. 41 | @objc optional func pause() // Pause playback. 42 | @objc optional func play() // Resume playback. 43 | @objc optional func playTrack(_ x: String!, inContext: String!) // Start playback of a track in the given context. 44 | @objc optional func setSoundVolume(_ soundVolume: Int) // The sound output volume (0 = minimum, 100 = maximum) 45 | @objc optional func setPlayerPosition(_ playerPosition: Double) // The player’s position within the currently playing track in seconds. 46 | @objc optional func setRepeating(_ repeating: Bool) // Is repeating on or off? 47 | @objc optional func setShuffling(_ shuffling: Bool) // Is shuffling on or off? 48 | @objc optional var name: String { get } // The name of the application. 49 | @objc optional var frontmost: Bool { get } // Is this the frontmost (active) application? 50 | @objc optional var version: String { get } // The version of the application. 51 | } 52 | extension SBApplication: SpotifyApplication {} 53 | 54 | // MARK: SpotifyTrack 55 | @objc public protocol SpotifyTrack: SBObjectProtocol { 56 | @objc optional var artist: String { get } // The artist of the track. 57 | @objc optional var album: String { get } // The album of the track. 58 | @objc optional var discNumber: Int { get } // The disc number of the track. 59 | @objc optional var duration: Int { get } // The length of the track in seconds. 60 | @objc optional var playedCount: Int { get } // The number of times this track has been played. 61 | @objc optional var trackNumber: Int { get } // The index of the track in its album. 62 | @objc optional var starred: Bool { get } // Is the track starred? 63 | @objc optional var popularity: Int { get } // How popular is this track? 0-100 64 | @objc optional func id() -> String // The ID of the item. 65 | @objc optional var name: String { get } // The name of the track. 66 | @objc optional var artworkUrl: String { get } // The URL of the track%apos;s album cover. 67 | @objc optional var artwork: NSImage { get } // The property is deprecated and will never be set. Use the 'artwork url' instead. 68 | @objc optional var albumArtist: String { get } // That album artist of the track. 69 | @objc optional var spotifyUrl: String { get } // The URL of the track. 70 | @objc optional func setSpotifyUrl(_ spotifyUrl: String!) // The URL of the track. 71 | } 72 | extension SBObject: SpotifyTrack {} 73 | 74 | public enum SpotifyScripting: String { 75 | case application = "application" 76 | case track = "track" 77 | } 78 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/Update22Window.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Update21Window.swift 3 | // Lyric Fever 4 | // 5 | // Created by Avi Wadhwa on 2025-03-19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Update22Window: View { 11 | @Environment(\.dismiss) var dismiss 12 | var body: some View { 13 | VStack(spacing: 25) { 14 | HStack { 15 | Image("hi") 16 | .resizable() 17 | .frame(width: 70, height: 70, alignment: .center) 18 | 19 | Text(verbatim:"Thanks for updating to Lyric Fever 2.2! 🎉") 20 | .font(.title) 21 | } 22 | 23 | Text(verbatim:"2.2 Changes") 24 | .font(.title2) 25 | .foregroundStyle(.green) 26 | ScrollView { 27 | VStack(spacing: 3) { 28 | Text(verbatim:"Improved Spotify connection: no more silent failures.") 29 | Text(verbatim:"Simplified Chinese UI thanks to InTheManXG") 30 | Text(verbatim:"Translation bugs fixed") 31 | Text(verbatim:"Karaoke color for non-Spotify lyrics should be MUCH better") 32 | Text(verbatim:"(to update karaoke color for a song, hit \"Refresh Lyrics\")") 33 | Text(verbatim:"Japanese Romanization should be much better") 34 | Text(verbatim:"Rapidly skipping through songs has been fixed (stale network requests are properly cancelled)") 35 | Text(verbatim:"Fullscreen button and UI improvements") 36 | Text(verbatim: "Fullscreen settings for lyric blur, animating on startup") 37 | Text(verbatim:"Get rid of flicker on lyric change in karaoke window") 38 | Text(verbatim:"Non-English lyric support improved on Apple Music") 39 | Text(verbatim:"Better NetEase lyric filter to prevent incorrect lyrics") 40 | Text(verbatim:"New Delete Lyrics button)") 41 | Text(verbatim:"More robust local file support") 42 | Text(verbatim:"Onboarding window touch-ups") 43 | Text(verbatim:"New fullscreen window share button") 44 | Text(verbatim: "AirPlay delay support") 45 | Text(verbatim: "Ensure freshly downloaded lyrics don't disappear on the last lyric") 46 | } 47 | .multilineTextAlignment(.leading) 48 | .padding(.horizontal,10) 49 | .padding(.vertical,5) 50 | .background(Color(nsColor: NSColor.darkGray).cornerRadius(7)) 51 | } 52 | Button("Close") { 53 | dismiss() 54 | } 55 | .font(.headline) 56 | .controlSize(.large) 57 | .buttonStyle(.borderedProminent) 58 | .padding(.bottom, 10) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/WebLoginView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | 4 | @MainActor 5 | class NavigationState: NSObject, ObservableObject { 6 | @Published var url: URL? 7 | let webView: WKWebView 8 | 9 | override init() { 10 | webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) 11 | webView.pageZoom = 0.7 12 | super.init() 13 | webView.navigationDelegate = self 14 | } 15 | } 16 | 17 | extension NavigationState: WKNavigationDelegate { 18 | func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { 19 | self.url = webView.url 20 | 21 | print("url is \(self.url)") 22 | 23 | if ((self.url?.absoluteString.starts(with: "https://open.spotify.com")) ?? false) { 24 | Task { 25 | await viewModel.shared.checkIfLoggedIn() 26 | } 27 | } 28 | // Task { 29 | // if await viewModel.shared. == true { 30 | // await FriendActivityBackend.shared.checkIfLoggedIn() 31 | // } 32 | // } 33 | 34 | if (self.url?.absoluteString.starts(with: "https://accounts.google.com/") ?? false) { 35 | print("google link discovered woah \(self.url?.absoluteString ?? "none" )") 36 | webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15" 37 | } 38 | } 39 | } 40 | 41 | struct WebView: NSViewRepresentable { 42 | let request: URLRequest 43 | @ObservedObject var navigationState: NavigationState 44 | 45 | func makeNSView(context: Context) -> WKWebView { 46 | navigationState.webView.load(request) 47 | return navigationState.webView 48 | } 49 | 50 | func updateNSView(_ nsView: WKWebView, context: Context) { 51 | // Update code if needed 52 | } 53 | } 54 | 55 | //struct WebviewLogin: View { 56 | // @StateObject var navigationState = NavigationState() 57 | // 58 | // var body: some View { 59 | // VStack { 60 | // WebView(request: URLRequest(url: URL(string: "https://accounts.spotify.com/en/login?continue=https%3A%2F%2Fopen.spotify.com%2F")!), navigationState: navigationState) 61 | // } 62 | //// .onAppear() { 63 | //// HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) 64 | //// print("All cookies deleted") 65 | //// 66 | //// WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in 67 | //// records.forEach { record in 68 | //// WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {}) 69 | //// print("Cookie ::: \(record) deleted") 70 | //// } 71 | //// } 72 | //// } 73 | // } 74 | //} 75 | -------------------------------------------------------------------------------- /SpotifyLyricsInMenubar/lyricJsonStruct.swift: -------------------------------------------------------------------------------- 1 | // 2 | // lyricJsonStruct.swift 3 | // SpotifyLyricsInMenubar 4 | // 5 | // Created by Avi Wadhwa on 01/08/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension CodingUserInfoKey { 12 | static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")! 13 | static let trackID = CodingUserInfoKey(rawValue: "trackID")! 14 | static let trackName = CodingUserInfoKey(rawValue: "trackName")! 15 | static let duration = CodingUserInfoKey(rawValue: "duration")! 16 | } 17 | 18 | struct LyricLine: Decodable, Hashable { 19 | let startTimeMS: TimeInterval 20 | let words: String 21 | let id = UUID() 22 | 23 | enum CodingKeys: String, CodingKey { 24 | case startTimeMS = "startTimeMs" 25 | case words 26 | } 27 | 28 | init(from decoder: Decoder) throws { 29 | let container = try decoder.container(keyedBy: CodingKeys.self) 30 | self.startTimeMS = TimeInterval(try container.decode(String.self, forKey: .startTimeMS))! 31 | self.words = try container.decode(String.self, forKey: .words) 32 | } 33 | 34 | init(startTime: TimeInterval, words: String) { 35 | self.startTimeMS = startTime 36 | self.words = words 37 | } 38 | } 39 | 40 | // access token json 41 | struct accessTokenJSON: Codable { 42 | let accessToken: String 43 | let accessTokenExpirationTimestampMs: TimeInterval 44 | let isAnonymous: Bool 45 | } 46 | 47 | struct SongObjectParent: Decodable { 48 | let lyrics: SongObject 49 | let colors: SpotifyColorData 50 | } 51 | 52 | struct SpotifyUser: Codable { 53 | let displayName: String 54 | 55 | enum CodingKeys: String, CodingKey { 56 | case displayName = "display_name" 57 | } 58 | } 59 | 60 | struct SpotifyColorData: Codable { 61 | let background, text, highlightText: Int 62 | 63 | init(from decoder: any Decoder) throws { 64 | guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext, let trackID = decoder.userInfo[CodingUserInfoKey.trackID] as? String, let trackName = decoder.userInfo[CodingUserInfoKey.trackName] as? String, let duration = decoder.userInfo[CodingUserInfoKey.duration] as? TimeInterval else { 65 | fatalError() 66 | } 67 | let container = try decoder.container(keyedBy: CodingKeys.self) 68 | self.background = try container.decode(Int.self, forKey: .background) 69 | self.text = try container.decode(Int.self, forKey: .text) 70 | self.highlightText = try container.decode(Int.self, forKey: .highlightText) 71 | let newColorMapping = IDToColor(context: context) 72 | newColorMapping.id = trackID 73 | newColorMapping.songColor = Int32(background) 74 | try context.save() 75 | } 76 | 77 | init(trackID: String, context: NSManagedObjectContext, background: Int32) { 78 | self.background = Int(background) 79 | self.text = 0 80 | self.highlightText = 0 81 | let newColorMapping = IDToColor(context: context) 82 | newColorMapping.id = trackID 83 | newColorMapping.songColor = background 84 | print("saving new background as \(background)") 85 | do { 86 | try context.save() 87 | } catch { 88 | print(error) 89 | } 90 | } 91 | } 92 | 93 | struct SpotifyResponse: Codable { 94 | let tracks: Tracks 95 | } 96 | 97 | struct Tracks: Codable { 98 | let items: [Item] 99 | } 100 | 101 | struct Item: Codable { 102 | let type: String 103 | let name: String 104 | let artists: [Artist] 105 | var firstArtistName: String? { 106 | return artists.first?.name 107 | } 108 | let id: String 109 | } 110 | 111 | struct Artist: Codable { 112 | let name: String 113 | } 114 | 115 | struct ErrorWrapper: Codable { 116 | struct Error: Codable { 117 | let code: Int 118 | let message: String 119 | } 120 | 121 | let error: Error 122 | } 123 | 124 | struct LRCLyrics: Decodable { 125 | let id: Int 126 | let name, trackName, artistName, albumName: String 127 | let duration: Int 128 | let instrumental: Bool 129 | let plainLyrics, syncedLyrics: String 130 | let lyrics: [LyricLine] 131 | 132 | enum CodingKeys: CodingKey { 133 | case id 134 | case name 135 | case trackName 136 | case artistName 137 | case albumName 138 | case duration 139 | case instrumental 140 | case plainLyrics 141 | case syncedLyrics 142 | // case lyrics 143 | } 144 | 145 | static func decodeLyrics(input: String) -> [LyricLine] { 146 | var lyricsArray: [LyricLine] = [] 147 | let lines = input.components(separatedBy: "\n") 148 | 149 | for line in lines { 150 | // Use regex to match the timestamp and the lyrics 151 | let regex = try! NSRegularExpression(pattern: #"\[(\d{2}:\d{2}\.\d{2})\]\s*(.*)"#) 152 | let matches = regex.matches(in: line, range: NSRange(line.startIndex.. TimeInterval { 193 | guard self != "" else { 194 | return 0 195 | } 196 | 197 | var interval: Double = 0 198 | 199 | let parts = self.components(separatedBy: ":") 200 | for (index, part) in parts.reversed().enumerated() { 201 | interval += (Double(part) ?? 0) * pow(Double(60), Double(index)) 202 | } 203 | 204 | return interval * 1000 // Convert seconds to milliseconds 205 | } 206 | } 207 | 208 | struct MusicBrainzReply: Codable { 209 | let created: String 210 | let count, offset: Int 211 | let releases: [MusicBrainzRelease] 212 | 213 | init(from decoder: Decoder) throws { 214 | let container = try decoder.container(keyedBy: CodingKeys.self) 215 | self.created = try container.decode(String.self, forKey: .created) 216 | self.count = try container.decode(Int.self, forKey: .count) 217 | self.offset = try container.decode(Int.self, forKey: .offset) 218 | 219 | // Decode all releases and filter out "Bootleg" ones 220 | let allReleases = try container.decode([MusicBrainzRelease].self, forKey: .releases) 221 | self.releases = allReleases.filter { $0.status != "Bootleg" } 222 | } 223 | } 224 | 225 | struct MusicBrainzRelease: Codable { 226 | let id: String 227 | let status: String? 228 | 229 | enum CodingKeys: CodingKey { 230 | case id,status 231 | } 232 | 233 | init(from decoder: any Decoder) throws { 234 | 235 | let container = try decoder.container(keyedBy: CodingKeys.self) 236 | self.id = try container.decode(String.self, forKey: .id) 237 | self.status = try? container.decode(String.self, forKey: .status) 238 | } 239 | } 240 | 241 | // Spotify TOTP Login Fix 242 | struct SpotifyServerTime: Decodable { 243 | let serverTime: Int 244 | } 245 | 246 | 247 | // From LyricsX: NetEase. Adapted for my needs 248 | struct NetEaseSearch: Decodable { 249 | let result: Result 250 | let code: Int 251 | 252 | struct Result: Decodable { 253 | let songs: [Song] 254 | let songCount: Int 255 | 256 | struct Song: Decodable { 257 | let name: String 258 | let id: Int 259 | let duration: Int // milliseconds 260 | let album: Album 261 | let artists: [Artist] 262 | } 263 | 264 | struct Album: Decodable { 265 | let name: String 266 | } 267 | 268 | struct Artist: Decodable { 269 | let name: String 270 | } 271 | } 272 | } 273 | 274 | struct NetEaseLyrics: Decodable { 275 | let lrc: Lyric? 276 | let klyric: Lyric? 277 | let tlyric: Lyric? 278 | let lyricUser: User? 279 | let yrc: Lyric? 280 | /* 281 | let sgc: Bool 282 | let sfy: Bool 283 | let qfy: Bool 284 | let code: Int 285 | let transUser: User 286 | */ 287 | 288 | struct User: Decodable { 289 | let nickname: String 290 | 291 | /* 292 | let id: Int 293 | let status: Int 294 | let demand: Int 295 | let userid: Int 296 | let uptime: Int 297 | */ 298 | } 299 | 300 | struct Lyric: Decodable { 301 | let lyric: String? 302 | 303 | /* 304 | let version: Int 305 | */ 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Lyric Fever 5 | 6 | 1.8 7 | Thu, 11 Apr 2024 14:29:12 -0700 8 | 1.8 9 | 1.8 10 | 13.0 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /crossfade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/crossfade.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/logo.png -------------------------------------------------------------------------------- /newPermissionMac.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/newPermissionMac.gif -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/screenshot2.png -------------------------------------------------------------------------------- /spotifylogin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/spotifylogin.gif -------------------------------------------------------------------------------- /superShy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviwad/LyricFever/5a4dd84b008f0682e980698f7642578a78d37c62/superShy.gif --------------------------------------------------------------------------------