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) -> 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!媒体播放器") } }