265 lines
10 KiB
Swift
265 lines
10 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
|
|
private let widgetGroupId = "group.top.zhushenwudi.llmp"
|
|
private var lastLyricLine1 = ""
|
|
private var lastLyricLine2 = ""
|
|
|
|
struct Provider: TimelineProvider {
|
|
func placeholder(in context: Context) -> ExampleEntry {
|
|
ExampleEntry(
|
|
date: Date(),
|
|
widgetFamily: context.family,
|
|
songName: "",
|
|
songArtist: "",
|
|
isFavorite: false,
|
|
isPlaying: false,
|
|
playText: "",
|
|
lyricLine1: "",
|
|
lyricLine2: "",
|
|
currentLine: 1,
|
|
isShutdown: true,
|
|
bgColor: "255,255,255"
|
|
)
|
|
}
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping (ExampleEntry) -> Void) {
|
|
completion(readEntry(family: context.family))
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
|
|
completion(Timeline(entries: [readEntry(family: context.family)], policy: .atEnd))
|
|
}
|
|
|
|
private func readEntry(family: WidgetFamily) -> ExampleEntry {
|
|
let data = UserDefaults(suiteName: widgetGroupId)
|
|
let songName = data?.string(forKey: "songName") ?? ""
|
|
let songArtist = data?.string(forKey: "songArtist") ?? ""
|
|
let isFavorite = data?.bool(forKey: "songFavorite") ?? false
|
|
let isPlaying = data?.bool(forKey: "isPlaying") ?? false
|
|
let playText = data?.string(forKey: "playText") ?? "播放中,已暂停"
|
|
if let line1 = data?.string(forKey: "lyricLine1") { lastLyricLine1 = line1 }
|
|
if let line2 = data?.string(forKey: "lyricLine2") { lastLyricLine2 = line2 }
|
|
let currentLine = data?.integer(forKey: "currentLine") ?? 1
|
|
let isShutdown = data?.bool(forKey: "isShutdown") ?? true
|
|
let bgColor = data?.string(forKey: "bgColor") ?? "255,255,255"
|
|
return ExampleEntry(
|
|
date: Date(),
|
|
widgetFamily: family,
|
|
songName: songName,
|
|
songArtist: songArtist,
|
|
isFavorite: isFavorite,
|
|
isPlaying: isPlaying,
|
|
playText: playText,
|
|
lyricLine1: lastLyricLine1,
|
|
lyricLine2: lastLyricLine2,
|
|
currentLine: currentLine,
|
|
isShutdown: isShutdown,
|
|
bgColor: bgColor
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ExampleEntry: TimelineEntry {
|
|
let date: Date
|
|
let widgetFamily: WidgetFamily
|
|
let songName: String
|
|
let songArtist: String
|
|
let isFavorite: Bool
|
|
let isPlaying: Bool
|
|
let playText: String
|
|
let lyricLine1: String
|
|
let lyricLine2: String
|
|
let currentLine: Int
|
|
let isShutdown: Bool
|
|
let bgColor: String
|
|
}
|
|
|
|
struct HomeWidgetExampleEntryView: View {
|
|
var entry: Provider.Entry
|
|
let isWhiteBackground: Bool
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
let cdSize: CGFloat = 140
|
|
let coverSize = cdSize * 0.55
|
|
let coverAndCdDiffSize = (cdSize - coverSize) / 2
|
|
let playButtonAndCdDiffSize = (cdSize - 34) / 2
|
|
let offsetX = calcCdOffsetX(entry: entry, geometry: geometry)
|
|
let offsetY = calcCdOffsetY(entry: entry, geometry: geometry)
|
|
let lyricMaxWidth = geometry.size.width - cdSize
|
|
let bgColor = calcBgColor(entry: entry)
|
|
let playText = calcPlayText(entry: entry)
|
|
|
|
ZStack {
|
|
if isWhiteBackground {
|
|
Rectangle()
|
|
.fill(
|
|
AngularGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(red: 229/255, green: 233/255, blue: 235/255),
|
|
Color(red: 219/255, green: 223/255, blue: 225/255),
|
|
bgColor,
|
|
]),
|
|
center: .topLeading,
|
|
angle: .degrees(225)
|
|
)
|
|
)
|
|
} else {
|
|
Rectangle()
|
|
.fill(Color(red: 49/255, green: 49/255, blue: 49/255))
|
|
}
|
|
|
|
ZStack(alignment: .topTrailing) {
|
|
Image(isWhiteBackground ? "cd_white" : "cd_black")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: cdSize, height: cdSize)
|
|
.offset(x: offsetX, y: offsetY)
|
|
|
|
if let image = loadImageFromFile() {
|
|
Image(uiImage: image)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: coverSize, height: coverSize)
|
|
.clipShape(Circle())
|
|
.offset(x: -coverAndCdDiffSize + offsetX, y: coverAndCdDiffSize + offsetY)
|
|
}
|
|
|
|
if #available(iOSApplicationExtension 17, *) {
|
|
Button(intent: BackgroundIntent(url: URL(string: "homeWidgetExample://toggle_play"))) {
|
|
Image(systemName: entry.isPlaying ? "pause.fill" : "play.fill")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 18, height: 18)
|
|
.foregroundColor(.white)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.padding(8)
|
|
.offset(x: -playButtonAndCdDiffSize + offsetX, y: playButtonAndCdDiffSize + offsetY)
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading) {
|
|
Image("logo")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 30, height: 30)
|
|
.padding(.bottom, 5)
|
|
|
|
if entry.widgetFamily == .systemMedium {
|
|
Text(entry.lyricLine1)
|
|
.font(.system(size: 15))
|
|
.foregroundColor(lyricColor(for: entry.currentLine, active: 1))
|
|
.frame(maxWidth: entry.isPlaying ? .infinity : lyricMaxWidth, alignment: .leading)
|
|
Text(entry.lyricLine2)
|
|
.font(.system(size: 15))
|
|
.foregroundColor(lyricColor(for: entry.currentLine, active: 2))
|
|
.frame(maxWidth: entry.isPlaying ? .infinity : lyricMaxWidth, alignment: .leading)
|
|
}
|
|
|
|
Spacer()
|
|
Text(playText).font(.system(size: 10)).bold().foregroundColor(.gray)
|
|
Text(entry.songName).font(.system(size: 12)).bold()
|
|
.foregroundColor(isWhiteBackground ? .black : .white)
|
|
Text(entry.songArtist).font(.system(size: 12, weight: .light)).foregroundColor(.gray)
|
|
}
|
|
.padding()
|
|
|
|
VStack {
|
|
Spacer()
|
|
if #available(iOSApplicationExtension 17, *) {
|
|
Button(intent: BackgroundIntent(url: URL(string: "homeWidgetExample://toggle_love"))) {
|
|
Image(entry.isFavorite ? "FavoriteClick" : "FavoriteUnClick")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 34, height: 34)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.frame(maxWidth: .infinity, alignment: .bottomTrailing)
|
|
.padding(8)
|
|
}
|
|
}
|
|
|
|
if entry.isShutdown {
|
|
Link(destination: URL(string: "llmp://")!) { Color.clear }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func lyricColor(for currentLine: Int, active: Int) -> Color {
|
|
let highlighted = currentLine == -1 || currentLine == active
|
|
if highlighted {
|
|
return isWhiteBackground ? .black : .white
|
|
}
|
|
return Color(red: 0.7, green: 0.7, blue: 0.7, opacity: 0.7)
|
|
}
|
|
|
|
func calcCdOffsetX(entry: ExampleEntry, geometry: GeometryProxy) -> CGFloat {
|
|
switch entry.widgetFamily {
|
|
case .systemSmall: return (geometry.size.width - 70) / 2
|
|
case .systemMedium: return (geometry.size.width - 152) / 2 + (entry.isPlaying ? 40 : 0)
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
func calcCdOffsetY(entry: ExampleEntry, geometry: GeometryProxy) -> CGFloat {
|
|
switch entry.widgetFamily {
|
|
case .systemSmall: return (70 - geometry.size.height) / 2
|
|
case .systemMedium: return (152 - geometry.size.height) / 2 - (entry.isPlaying ? 40 : 0)
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
func calcBgColor(entry: ExampleEntry) -> Color {
|
|
let parts = entry.bgColor.split(separator: ",").compactMap { Double($0) }
|
|
let rgb = parts.count >= 3 ? parts : [230, 215, 210]
|
|
return Color(red: rgb[0]/255, green: rgb[1]/255, blue: rgb[2]/255)
|
|
}
|
|
|
|
func loadImageFromFile() -> UIImage? {
|
|
guard let fileURL = FileManager.default
|
|
.containerURL(forSecurityApplicationGroupIdentifier: widgetGroupId)?
|
|
.appendingPathComponent("sharedImage.png") else { return nil }
|
|
return UIImage(contentsOfFile: fileURL.path)
|
|
}
|
|
|
|
func calcPlayText(entry: ExampleEntry) -> String {
|
|
let parts = entry.playText.split(separator: ",").map(String.init)
|
|
let playing = parts.first ?? "播放中"
|
|
let paused = parts.count > 1 ? parts[1] : "已暂停"
|
|
return entry.isPlaying ? playing : paused
|
|
}
|
|
}
|
|
|
|
struct HomeWidgetExampleWhite: Widget {
|
|
let kind = "HomeWidgetExampleWhite"
|
|
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
|
HomeWidgetExampleEntryView(entry: entry, isWhiteBackground: true)
|
|
.containerBackground(.fill.tertiary, for: .widget)
|
|
}
|
|
.configurationDisplayName("LLMP音乐组件")
|
|
.contentMarginsDisabled()
|
|
.supportedFamilies([.systemSmall, .systemMedium])
|
|
.description("LoveLive!媒体播放器")
|
|
}
|
|
}
|
|
|
|
struct HomeWidgetExampleBlack: Widget {
|
|
let kind = "HomeWidgetExampleBlack"
|
|
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
|
HomeWidgetExampleEntryView(entry: entry, isWhiteBackground: false)
|
|
.containerBackground(.fill.tertiary, for: .widget)
|
|
}
|
|
.configurationDisplayName("LLMP音乐组件")
|
|
.contentMarginsDisabled()
|
|
.supportedFamilies([.systemSmall, .systemMedium])
|
|
.description("LoveLive!媒体播放器")
|
|
}
|
|
}
|