revert: remove accidentally merged ios widget commits
Reverts commits d58c1a77 (feat: ios widget) and 08fe871d (wip: widget) that were accidentally merged from feat/ios-widget-build-target branch.
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"color-space": "display-p3",
|
||||
"components": {
|
||||
"red": 0.5764705882352941,
|
||||
"green": 0.2,
|
||||
"blue": 0.9176470588235294,
|
||||
"alpha": 1
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"color-space": "display-p3",
|
||||
"components": {
|
||||
"red": 0.10196078431372549,
|
||||
"green": 0.10196078431372549,
|
||||
"blue": 0.10196078431372549,
|
||||
"alpha": 1
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"color": {
|
||||
"color-space": "display-p3",
|
||||
"components": {
|
||||
"red": 0.10196078431372549,
|
||||
"green": 0.10196078431372549,
|
||||
"blue": 0.10196078431372549,
|
||||
"alpha": 1
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
@@ -1,122 +0,0 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "20x20",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-20x20@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "20x20",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-20x20@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-29x29@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-29x29@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-29x29@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-40x40@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-40x40@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-60x60@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-60x60@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "20x20",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-20x20@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "20x20",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-20x20@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "29x29",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-29x29@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "29x29",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-29x29@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "40x40",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-40x40@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "40x40",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-40x40@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "76x76",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-76x76@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "76x76",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-76x76@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "83.5x83.5",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-83.5x83.5@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ios-marketing",
|
||||
"size": "1024x1024",
|
||||
"scale": "1x",
|
||||
"filename": "ItunesArtwork@2x.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 311 KiB |
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,19 +0,0 @@
|
||||
/** @type {import('@bacons/apple-targets').Config} */
|
||||
module.exports = (config) => ({
|
||||
type: "widget",
|
||||
name: "StreamyfinWidget",
|
||||
icon: "../../assets/images/icon-ios-plain.png",
|
||||
deploymentTarget: "17.0",
|
||||
entitlements: {
|
||||
"com.apple.security.application-groups": config.ios?.entitlements?.[
|
||||
"com.apple.security.application-groups"
|
||||
] || ["group.com.fredrikburmester.streamyfin.widgets"],
|
||||
},
|
||||
colors: {
|
||||
$accent: "#9333EA",
|
||||
$widgetBackground: {
|
||||
light: "#1a1a1a",
|
||||
dark: "#1a1a1a",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.fredrikburmester.streamyfin.widgets</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,391 +0,0 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
import MediaPlayer
|
||||
|
||||
// MARK: - App Intents
|
||||
|
||||
struct TogglePlaybackIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Toggle Playback"
|
||||
static var description = IntentDescription("Play or pause the current track")
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.send()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct NextTrackIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Next Track"
|
||||
static var description = IntentDescription("Skip to next track")
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.send()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data Models
|
||||
|
||||
struct NowPlayingData: Codable {
|
||||
let title: String
|
||||
let artist: String
|
||||
let album: String
|
||||
let artworkUrl: String?
|
||||
let isPlaying: Bool
|
||||
let updatedAt: Double
|
||||
}
|
||||
|
||||
struct NowPlayingEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let title: String
|
||||
let artist: String
|
||||
let album: String
|
||||
let artworkImage: UIImage? // Store actual image, not URL (AsyncImage doesn't work in widgets)
|
||||
let isPlaying: Bool
|
||||
let isEmpty: Bool
|
||||
|
||||
static var empty: NowPlayingEntry {
|
||||
NowPlayingEntry(
|
||||
date: Date(),
|
||||
title: "Not Playing",
|
||||
artist: "Open Streamyfin to play music",
|
||||
album: "",
|
||||
artworkImage: nil,
|
||||
isPlaying: false,
|
||||
isEmpty: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline Provider
|
||||
|
||||
struct NowPlayingProvider: TimelineProvider {
|
||||
let appGroupId = "group.com.fredrikburmester.streamyfin.widgets"
|
||||
|
||||
func placeholder(in context: Context) -> NowPlayingEntry {
|
||||
NowPlayingEntry(
|
||||
date: Date(),
|
||||
title: "Song Title",
|
||||
artist: "Artist Name",
|
||||
album: "Album",
|
||||
artworkImage: nil,
|
||||
isPlaying: true,
|
||||
isEmpty: false
|
||||
)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (NowPlayingEntry) -> Void) {
|
||||
let entry = fetchCurrentEntry()
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<NowPlayingEntry>) -> Void) {
|
||||
let entry = fetchCurrentEntry()
|
||||
// Use .never policy since we manually refresh via WidgetCenter
|
||||
let timeline = Timeline(entries: [entry], policy: .never)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
private func fetchCurrentEntry() -> NowPlayingEntry {
|
||||
guard let defaults = UserDefaults(suiteName: appGroupId),
|
||||
let jsonString = defaults.string(forKey: "nowPlaying"),
|
||||
let data = jsonString.data(using: .utf8),
|
||||
let nowPlaying = try? JSONDecoder().decode(NowPlayingData.self, from: data) else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
// Download artwork synchronously (AsyncImage doesn't work in widgets)
|
||||
var artworkImage: UIImage? = nil
|
||||
if let urlString = nowPlaying.artworkUrl,
|
||||
let url = URL(string: urlString),
|
||||
let imageData = try? Data(contentsOf: url) {
|
||||
artworkImage = UIImage(data: imageData)
|
||||
}
|
||||
|
||||
return NowPlayingEntry(
|
||||
date: Date(),
|
||||
title: nowPlaying.title,
|
||||
artist: nowPlaying.artist,
|
||||
album: nowPlaying.album,
|
||||
artworkImage: artworkImage,
|
||||
isPlaying: nowPlaying.isPlaying,
|
||||
isEmpty: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Artwork Image View
|
||||
|
||||
struct ArtworkView: View {
|
||||
let image: UIImage?
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let uiImage = image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
placeholderView
|
||||
}
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
private var placeholderView: some View {
|
||||
ZStack {
|
||||
Color(hex: "#2a2a2a")
|
||||
Image(systemName: "music.note")
|
||||
.font(.system(size: size * 0.4))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Control Button
|
||||
|
||||
struct ControlButton: View {
|
||||
let systemName: String
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.15))
|
||||
Image(systemName: systemName)
|
||||
.font(.system(size: size, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Small Widget View
|
||||
|
||||
struct SmallWidgetView: View {
|
||||
let entry: NowPlayingEntry
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// Background artwork (blurred)
|
||||
if !entry.isEmpty {
|
||||
ArtworkView(image: entry.artworkImage, size: geometry.size.width)
|
||||
.blur(radius: 20)
|
||||
.opacity(0.6)
|
||||
}
|
||||
|
||||
// Content
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
|
||||
// Small artwork
|
||||
if !entry.isEmpty {
|
||||
ArtworkView(image: entry.artworkImage, size: 48)
|
||||
.shadow(radius: 4)
|
||||
} else {
|
||||
Image(systemName: "music.note.house.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(Color(hex: "#9333EA"))
|
||||
}
|
||||
|
||||
Spacer().frame(height: 8)
|
||||
|
||||
// Track info
|
||||
Text(entry.title)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(entry.artist)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.lineLimit(1)
|
||||
|
||||
// Play/Pause button
|
||||
if !entry.isEmpty {
|
||||
Button(intent: TogglePlaybackIntent()) {
|
||||
ControlButton(
|
||||
systemName: entry.isPlaying ? "pause.fill" : "play.fill",
|
||||
size: 14
|
||||
)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
||||
}
|
||||
}
|
||||
.containerBackground(for: .widget) {
|
||||
Color(hex: "#1a1a1a")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Medium Widget View
|
||||
|
||||
struct MediumWidgetView: View {
|
||||
let entry: NowPlayingEntry
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
// Artwork
|
||||
ArtworkView(image: entry.artworkImage, size: 100)
|
||||
.shadow(radius: 8)
|
||||
|
||||
// Track info
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.title)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(entry.artist)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.lineLimit(1)
|
||||
|
||||
if !entry.album.isEmpty {
|
||||
Text(entry.album)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Playback controls
|
||||
if !entry.isEmpty {
|
||||
HStack(spacing: 12) {
|
||||
// Play/Pause button
|
||||
Button(intent: TogglePlaybackIntent()) {
|
||||
ControlButton(
|
||||
systemName: entry.isPlaying ? "pause.fill" : "play.fill",
|
||||
size: 18
|
||||
)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Next button
|
||||
Button(intent: NextTrackIntent()) {
|
||||
ControlButton(systemName: "forward.fill", size: 14)
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(16)
|
||||
.containerBackground(for: .widget) {
|
||||
Color(hex: "#1a1a1a")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Configuration
|
||||
|
||||
struct StreamyfinNowPlayingWidget: Widget {
|
||||
let kind: String = "StreamyfinNowPlayingWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: NowPlayingProvider()) { entry in
|
||||
StreamyfinWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Now Playing")
|
||||
.description("See what's currently playing in Streamyfin")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamyfinWidgetEntryView: View {
|
||||
@Environment(\.widgetFamily) var family
|
||||
let entry: NowPlayingEntry
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
SmallWidgetView(entry: entry)
|
||||
case .systemMedium:
|
||||
MediumWidgetView(entry: entry)
|
||||
default:
|
||||
SmallWidgetView(entry: entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Bundle Entry Point
|
||||
|
||||
@main
|
||||
struct StreamyfinWidgets: WidgetBundle {
|
||||
var body: some Widget {
|
||||
StreamyfinNowPlayingWidget()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Color Extension
|
||||
|
||||
extension Color {
|
||||
init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (255, 0, 0, 0)
|
||||
}
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
StreamyfinNowPlayingWidget()
|
||||
} timeline: {
|
||||
NowPlayingEntry(
|
||||
date: Date(),
|
||||
title: "Bohemian Rhapsody",
|
||||
artist: "Queen",
|
||||
album: "A Night at the Opera",
|
||||
artworkImage: nil,
|
||||
isPlaying: true,
|
||||
isEmpty: false
|
||||
)
|
||||
NowPlayingEntry.empty
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
StreamyfinNowPlayingWidget()
|
||||
} timeline: {
|
||||
NowPlayingEntry(
|
||||
date: Date(),
|
||||
title: "Bohemian Rhapsody",
|
||||
artist: "Queen",
|
||||
album: "A Night at the Opera",
|
||||
artworkImage: nil,
|
||||
isPlaying: true,
|
||||
isEmpty: false
|
||||
)
|
||||
NowPlayingEntry.empty
|
||||
}
|
||||