Remove iOS code and target - tvOS only fork

- Removed entire jellypig iOS directory
- Removed jellypig iOS.xcscheme
- jellypig is now tvOS-only for Apple TV usage
- Focusing on Jellyfin.Xtream plugin compatibility
This commit is contained in:
Ashik K 2025-10-17 08:51:26 +02:00
parent 65d5a4b176
commit fe8e0487a9
453 changed files with 0 additions and 31089 deletions

View File

@ -1,34 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import AVFAudio
import CoreStore
import Defaults
import Logging
import Pulse
import PulseLogHandler
import SwiftUI
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback)
} catch {
print("setting category AVAudioSessionCategoryPlayback failed")
}
return true
}
}

View File

@ -1,122 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Defaults
import Foundation
import SwiftUI
// Following class is necessary to observe values that can either
// be a user *or* an app setting and only one should apply at a time.
//
// Also just to separate out value observation
// TODO: could clean up?
extension SwiftfinApp {
class ValueObservation: ObservableObject {
private var accentColorCancellable: AnyCancellable?
private var appearanceCancellable: AnyCancellable?
private var lastSignInUserIDCancellable: AnyCancellable?
private var splashScreenCancellable: AnyCancellable?
init() {
// MARK: signed in observation
lastSignInUserIDCancellable = Task {
for await newValue in Defaults.updates(.lastSignedInUserID) {
if case .signedIn = newValue {
setUserDefaultsObservation()
} else {
setAppDefaultsObservation()
}
}
}
.asAnyCancellable()
}
// MARK: user observation
private func setUserDefaultsObservation() {
accentColorCancellable?.cancel()
appearanceCancellable?.cancel()
splashScreenCancellable?.cancel()
accentColorCancellable = Task {
for await newValue in Defaults.updates(.userAccentColor) {
await MainActor.run {
Defaults[.accentColor] = newValue
UIApplication.shared.setAccentColor(newValue.uiColor)
}
}
}
.asAnyCancellable()
appearanceCancellable = Task {
for await newValue in Defaults.updates(.userAppearance) {
await MainActor.run {
Defaults[.appearance] = newValue
UIApplication.shared.setAppearance(newValue.style)
}
}
}
.asAnyCancellable()
}
// MARK: app observation
private func setAppDefaultsObservation() {
accentColorCancellable?.cancel()
appearanceCancellable?.cancel()
splashScreenCancellable?.cancel()
accentColorCancellable = Task {
for await newValue in Defaults.updates(.appAccentColor) {
await MainActor.run {
Defaults[.accentColor] = newValue
UIApplication.shared.setAccentColor(newValue.uiColor)
}
}
}
.asAnyCancellable()
appearanceCancellable = Task {
for await newValue in Defaults.updates(.appAppearance) {
// other cancellable will set appearance if enabled
// and need to avoid races
guard !Defaults[.selectUserUseSplashscreen] else { continue }
await MainActor.run {
Defaults[.appearance] = newValue
UIApplication.shared.setAppearance(newValue.style)
}
}
}
.asAnyCancellable()
splashScreenCancellable = Task {
for await newValue in Defaults.updates(.selectUserUseSplashscreen) {
await MainActor.run {
if newValue {
Defaults[.appearance] = .dark
UIApplication.shared.setAppearance(.dark)
} else {
Defaults[.appearance] = Defaults[.appAppearance]
UIApplication.shared.setAppearance(Defaults[.appAppearance].style)
}
}
}
}
.asAnyCancellable()
}
}
}

View File

@ -1,125 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import CoreStore
import Defaults
import Factory
import Logging
import Nuke
import PreferencesView
import Pulse
import PulseLogHandler
import SwiftUI
@main
struct SwiftfinApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
var appDelegate
@StateObject
private var valueObservation = ValueObservation()
init() {
// CoreStore
CoreStoreDefaults.dataStack = SwiftfinStore.dataStack
CoreStoreDefaults.logger = SwiftfinCorestoreLogger()
// Logging
LoggingSystem.bootstrap { label in
var loggers: [LogHandler] = [PersistentLogHandler(label: label).withLogLevel(.trace)]
#if DEBUG
loggers.append(SwiftfinConsoleLogger())
#endif
return MultiplexLogHandler(loggers)
}
// Nuke
ImageCache.shared.costLimit = 1024 * 1024 * 200 // 200 MB
ImageCache.shared.ttl = 300 // 5 min
ImageDecoderRegistry.shared.register { context in
guard let mimeType = context.urlResponse?.mimeType else { return nil }
return mimeType.contains("svg") ? ImageDecoders.Empty() : nil
}
ImagePipeline.shared = .Swiftfin.posters
// UIKit
UIScrollView.appearance().keyboardDismissMode = .onDrag
// Sometimes the tab bar won't appear properly on push, always have material background
UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(idiom: .unspecified)
// Swiftfin
// don't keep last user id
if Defaults[.signOutOnClose] {
Defaults[.lastSignedInUserID] = .signedOut
}
}
// TODO: removed after iOS 15 support removed
@ViewBuilder
private var versionedView: some View {
if #available(iOS 16, *) {
PreferencesView {
MainCoordinator()
.view()
.supportedOrientations(UIDevice.isPad ? .allButUpsideDown : .portrait)
}
} else {
PreferencesView {
PreferencesView {
MainCoordinator()
.view()
.supportedOrientations(UIDevice.isPad ? .allButUpsideDown : .portrait)
}
.ignoresSafeArea()
}
}
}
var body: some Scene {
WindowGroup {
versionedView
.ignoresSafeArea()
.onNotification(.applicationDidEnterBackground) {
Defaults[.backgroundTimeStamp] = Date.now
}
.onNotification(.applicationWillEnterForeground) {
// TODO: needs to check if any background playback is happening
// - atow, background video playback isn't officially supported
let backgroundedInterval = Date.now.timeIntervalSince(Defaults[.backgroundTimeStamp])
if Defaults[.signOutOnBackground], backgroundedInterval > Defaults[.backgroundSignOutInterval] {
Defaults[.lastSignedInUserID] = .signedOut
Container.shared.currentUserSession.reset()
Notifications[.didSignOut].post()
}
}
}
}
}
extension UINavigationController {
// Remove back button text
override open func viewWillLayoutSubviews() {
navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}

View File

@ -1,55 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct BasicStepper<Value: CustomStringConvertible & Strideable>: View {
@Binding
private var value: Value
private let title: String
private let range: ClosedRange<Value>
private let step: Value.Stride
private var formatter: (Value) -> String
var body: some View {
Stepper(value: $value, in: range, step: step) {
HStack {
Text(title)
Spacer()
formatter(value).text
.foregroundColor(.secondary)
}
}
}
}
extension BasicStepper {
init(
title: String,
value: Binding<Value>,
range: ClosedRange<Value>,
step: Value.Stride
) {
self.init(
value: value,
title: title,
range: range,
step: step,
formatter: { $0.description }
)
}
func valueFormatter(_ formatter: @escaping (Value) -> String) -> Self {
copy(modifying: \.formatter, with: formatter)
}
}

View File

@ -1,85 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
// SwiftUI gauge style not available on iOS 15
struct GaugeProgressStyle: ProgressViewStyle {
@Default(.accentColor)
private var accentColor
@State
private var contentSize: CGSize = .zero
private var lineWidthRatio: CGFloat
private var systemImage: String?
func makeBody(configuration: Configuration) -> some View {
ZStack {
if let systemImage {
Image(systemName: systemImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: contentSize.width / 2.5, maxHeight: contentSize.height / 2.5)
.foregroundStyle(.secondary)
.padding(6)
}
Circle()
.stroke(
Color.gray.opacity(0.2),
lineWidth: contentSize.width / lineWidthRatio
)
Circle()
.trim(from: 0, to: configuration.fractionCompleted ?? 0)
.stroke(
accentColor,
style: StrokeStyle(
lineWidth: contentSize.width / lineWidthRatio,
lineCap: .round
)
)
.rotationEffect(.degrees(-90))
}
.animation(.linear(duration: 0.1), value: configuration.fractionCompleted)
.trackingSize($contentSize)
}
}
extension GaugeProgressStyle {
init() {
self.init(
lineWidthRatio: 5,
systemImage: nil
)
}
init(systemImage: String) {
self.init(
lineWidthRatio: 8,
systemImage: systemImage
)
}
}
extension ProgressViewStyle where Self == GaugeProgressStyle {
static var gauge: GaugeProgressStyle {
GaugeProgressStyle()
}
static func gauge(systemImage: String) -> GaugeProgressStyle {
GaugeProgressStyle(systemImage: systemImage)
}
}

View File

@ -1,47 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct CountryPicker: View {
let title: String
@Binding
var selectedCountryCode: String?
// MARK: - Get all localized countries
private var countries: [(code: String?, name: String)] {
var uniqueCountries = Set<String>()
var countryList: [(code: String?, name: String)] = Locale.isoRegionCodes.compactMap { code in
let locale = Locale.current
if let name = locale.localizedString(forRegionCode: code),
!uniqueCountries.contains(code)
{
uniqueCountries.insert(code)
return (code, name)
}
return nil
}
.sorted { $0.name < $1.name }
// Add None as an option at the top of the list
countryList.insert((code: nil, name: L10n.none), at: 0)
return countryList
}
// MARK: - Body
var body: some View {
Picker(title, selection: $selectedCountryCode) {
ForEach(countries, id: \.code) { country in
Text(country.name).tag(country.code)
}
}
}
}

View File

@ -1,36 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import SwiftUI
// TODO: retry button and/or loading text after a few more seconds
struct DelayedProgressView: View {
@State
private var interval = 0
private let timer: Publishers.Autoconnect<Timer.TimerPublisher>
init(interval: Double = 0.5) {
self.timer = Timer.publish(every: interval, on: .main, in: .common).autoconnect()
}
var body: some View {
VStack {
if interval > 0 {
ProgressView()
}
}
.onReceive(timer) { _ in
withAnimation {
interval += 1
}
}
}
}

View File

@ -1,24 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct DotHStack<Content: View>: View {
@ViewBuilder
var content: () -> Content
var body: some View {
SeparatorHStack(content)
.separator {
Circle()
.frame(width: 2, height: 2)
.padding(.horizontal, 5)
}
}
}

View File

@ -1,49 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: should use environment refresh instead?
struct ErrorView<ErrorType: Error>: View {
private let error: ErrorType
private var onRetry: (() -> Void)?
var body: some View {
VStack(spacing: 20) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 72))
.foregroundColor(Color.red)
Text(error.localizedDescription)
.frame(minWidth: 50, maxWidth: 240)
.multilineTextAlignment(.center)
if let onRetry {
PrimaryButton(title: L10n.retry)
.onSelect(onRetry)
.frame(maxWidth: 300)
.frame(height: 50)
}
}
}
}
extension ErrorView {
init(error: ErrorType) {
self.init(
error: error,
onRetry: nil
)
}
func onRetry(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onRetry, with: action)
}
}

View File

@ -1,320 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Foundation
import SwiftUI
// TODO: change swipe to directional
// TODO: figure out way for multitap near the middle be distinguished as different sides
// state, point, velocity, translation
typealias PanGestureHandler = (UIGestureRecognizer.State, UnitPoint, CGFloat, CGFloat) -> Void
// state, point, scale
typealias PinchGestureHandler = (UIGestureRecognizer.State, UnitPoint, CGFloat) -> Void
// point, direction, amount
typealias SwipeGestureHandler = (UnitPoint, Bool, Int) -> Void
// point, amount
typealias TapGestureHandler = (UnitPoint, Int) -> Void
struct GestureView: UIViewRepresentable {
private var onHorizontalPan: PanGestureHandler?
private var onHorizontalSwipe: SwipeGestureHandler?
private var onLongPress: ((UnitPoint) -> Void)?
private var onPinch: PinchGestureHandler?
private var onTap: TapGestureHandler?
private var onDoubleTouch: TapGestureHandler?
private var onVerticalPan: PanGestureHandler?
private var longPressMinimumDuration: TimeInterval
private var samePointPadding: CGFloat
private var samePointTimeout: TimeInterval
private var swipeTranslation: CGFloat
private var swipeVelocity: CGFloat
private var sameSwipeDirectionTimeout: TimeInterval
func makeUIView(context: Context) -> UIGestureView {
UIGestureView(
onHorizontalPan: onHorizontalPan,
onHorizontalSwipe: onHorizontalSwipe,
onLongPress: onLongPress,
onPinch: onPinch,
onTap: onTap,
onDoubleTouch: onDoubleTouch,
onVerticalPan: onVerticalPan,
longPressMinimumDuration: longPressMinimumDuration,
samePointPadding: samePointPadding,
samePointTimeout: samePointTimeout,
swipeTranslation: swipeTranslation,
swipeVelocity: swipeVelocity,
sameSwipeDirectionTimeout: sameSwipeDirectionTimeout
)
}
func updateUIView(_ uiView: UIGestureView, context: Context) {}
}
extension GestureView {
init() {
self.init(
longPressMinimumDuration: 0,
samePointPadding: 0,
samePointTimeout: 0,
swipeTranslation: 0,
swipeVelocity: 0,
sameSwipeDirectionTimeout: 0
)
}
func onHorizontalPan(_ action: @escaping PanGestureHandler) -> Self {
copy(modifying: \.onHorizontalPan, with: action)
}
func onHorizontalSwipe(
translation: CGFloat,
velocity: CGFloat,
sameSwipeDirectionTimeout: TimeInterval = 0,
_ action: @escaping SwipeGestureHandler
) -> Self {
copy(modifying: \.swipeTranslation, with: translation)
.copy(modifying: \.swipeVelocity, with: velocity)
.copy(modifying: \.sameSwipeDirectionTimeout, with: sameSwipeDirectionTimeout)
.copy(modifying: \.onHorizontalSwipe, with: action)
}
func onPinch(_ action: @escaping PinchGestureHandler) -> Self {
copy(modifying: \.onPinch, with: action)
}
func onTap(
samePointPadding: CGFloat,
samePointTimeout: TimeInterval,
_ action: @escaping TapGestureHandler
) -> Self {
copy(modifying: \.samePointPadding, with: samePointPadding)
.copy(modifying: \.samePointTimeout, with: samePointTimeout)
.copy(modifying: \.onTap, with: action)
}
func onDoubleTouch(_ action: @escaping TapGestureHandler) -> Self {
copy(modifying: \.onDoubleTouch, with: action)
}
func onLongPress(minimumDuration: TimeInterval, _ action: @escaping (UnitPoint) -> Void) -> Self {
copy(modifying: \.longPressMinimumDuration, with: minimumDuration)
.copy(modifying: \.onLongPress, with: action)
}
func onVerticalPan(_ action: @escaping PanGestureHandler) -> Self {
copy(modifying: \.onVerticalPan, with: action)
}
}
class UIGestureView: UIView {
private let onHorizontalPan: PanGestureHandler?
private let onHorizontalSwipe: SwipeGestureHandler?
private let onLongPress: ((UnitPoint) -> Void)?
private let onPinch: PinchGestureHandler?
private let onTap: TapGestureHandler?
private let onDoubleTouch: TapGestureHandler?
private let onVerticalPan: PanGestureHandler?
private let longPressMinimumDuration: TimeInterval
private let samePointPadding: CGFloat
private let samePointTimeout: TimeInterval
private let swipeTranslation: CGFloat
private let swipeVelocity: CGFloat
private var sameSwipeDirectionTimeout: TimeInterval
private var hasSwiped: Bool = false
private var lastSwipeDirection: Bool?
private var lastTouchLocation: CGPoint?
private var multiTapWorkItem: DispatchWorkItem?
private var sameSwipeWorkItem: DispatchWorkItem?
private var multiTapAmount: Int = 0
private var sameSwipeAmount: Int = 0
init(
onHorizontalPan: PanGestureHandler?,
onHorizontalSwipe: SwipeGestureHandler?,
onLongPress: ((UnitPoint) -> Void)?,
onPinch: PinchGestureHandler?,
onTap: TapGestureHandler?,
onDoubleTouch: TapGestureHandler?,
onVerticalPan: PanGestureHandler?,
longPressMinimumDuration: TimeInterval,
samePointPadding: CGFloat,
samePointTimeout: TimeInterval,
swipeTranslation: CGFloat,
swipeVelocity: CGFloat,
sameSwipeDirectionTimeout: TimeInterval
) {
self.onHorizontalPan = onHorizontalPan
self.onHorizontalSwipe = onHorizontalSwipe
self.onLongPress = onLongPress
self.onPinch = onPinch
self.onTap = onTap
self.onDoubleTouch = onDoubleTouch
self.onVerticalPan = onVerticalPan
self.longPressMinimumDuration = longPressMinimumDuration
self.samePointPadding = samePointPadding
self.samePointTimeout = samePointTimeout
self.swipeTranslation = swipeTranslation
self.swipeVelocity = swipeVelocity
self.sameSwipeDirectionTimeout = sameSwipeDirectionTimeout
super.init(frame: .zero)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPerformPinch))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didPerformTap))
let doubleTouchGesture = UITapGestureRecognizer(target: self, action: #selector(didPerformDoubleTouch))
doubleTouchGesture.numberOfTouchesRequired = 2
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didPerformLongPress))
longPressGesture.minimumPressDuration = longPressMinimumDuration
let verticalPanGesture = PanDirectionGestureRecognizer(
direction: .vertical,
target: self,
action: #selector(didPerformVerticalPan)
)
let horizontalPanGesture = PanDirectionGestureRecognizer(
direction: .horizontal,
target: self,
action: #selector(didPerformHorizontalPan)
)
addGestureRecognizer(pinchGesture)
addGestureRecognizer(tapGesture)
addGestureRecognizer(doubleTouchGesture)
addGestureRecognizer(longPressGesture)
addGestureRecognizer(verticalPanGesture)
addGestureRecognizer(horizontalPanGesture)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc
private func didPerformHorizontalPan(_ gestureRecognizer: PanDirectionGestureRecognizer) {
let translation = gestureRecognizer.translation(in: self).x
let unitPoint = gestureRecognizer.unitPoint(in: self)
let velocity = gestureRecognizer.velocity(in: self).x
onHorizontalPan?(gestureRecognizer.state, unitPoint, velocity, translation)
if !hasSwiped,
abs(translation) >= swipeTranslation,
abs(velocity) >= swipeVelocity
{
didPerformSwipe(unitPoint: unitPoint, direction: translation > 0)
hasSwiped = true
}
if gestureRecognizer.state == .ended {
hasSwiped = false
}
}
private func didPerformSwipe(unitPoint: UnitPoint, direction: Bool) {
if lastSwipeDirection == direction {
sameSwipeOccurred(unitPoint: unitPoint, direction: direction)
onHorizontalSwipe?(unitPoint, direction, sameSwipeAmount)
} else {
sameSwipeOccurred(unitPoint: unitPoint, direction: direction)
onHorizontalSwipe?(unitPoint, direction, 1)
}
}
private func sameSwipeOccurred(unitPoint: UnitPoint, direction: Bool) {
guard sameSwipeDirectionTimeout > 0 else { return }
lastSwipeDirection = direction
sameSwipeAmount += 1
sameSwipeWorkItem?.cancel()
let task = DispatchWorkItem {
self.sameSwipeAmount = 0
self.lastSwipeDirection = nil
}
sameSwipeWorkItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + sameSwipeDirectionTimeout, execute: task)
}
@objc
private func didPerformLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard let onLongPress, gestureRecognizer.state == .began else { return }
let unitPoint = gestureRecognizer.unitPoint(in: self)
onLongPress(unitPoint)
}
@objc
private func didPerformPinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let onPinch else { return }
let unitPoint = gestureRecognizer.unitPoint(in: self)
onPinch(gestureRecognizer.state, unitPoint, gestureRecognizer.scale)
}
@objc
private func didPerformTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let onTap else { return }
let location = gestureRecognizer.location(in: self)
let unitPoint = gestureRecognizer.unitPoint(in: self)
if let lastTouchLocation, lastTouchLocation.isNear(lastTouchLocation, padding: samePointPadding) {
multiTapOccurred(at: location)
onTap(unitPoint, multiTapAmount)
} else {
multiTapOccurred(at: location)
onTap(unitPoint, 1)
}
}
@objc
private func didPerformDoubleTouch(_ gestureRecognizer: UITapGestureRecognizer) {
guard let onDoubleTouch else { return }
let unitPoint = gestureRecognizer.unitPoint(in: self)
onDoubleTouch(unitPoint, 1)
}
@objc
private func didPerformVerticalPan(_ gestureRecognizer: PanDirectionGestureRecognizer) {
guard let onVerticalPan else { return }
let translation = gestureRecognizer.translation(in: self).y
let unitPoint = gestureRecognizer.unitPoint(in: self)
let velocity = gestureRecognizer.velocity(in: self).y
onVerticalPan(gestureRecognizer.state, unitPoint, velocity, translation)
}
private func multiTapOccurred(at location: CGPoint) {
guard samePointTimeout > 0 else { return }
lastTouchLocation = location
multiTapAmount += 1
multiTapWorkItem?.cancel()
let task = DispatchWorkItem {
self.multiTapAmount = 0
self.lastTouchLocation = nil
}
multiTapWorkItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + samePointTimeout, execute: task)
}
}

View File

@ -1,52 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct HourMinutePicker: UIViewRepresentable {
let interval: Binding<TimeInterval>
func makeUIView(context: Context) -> some UIView {
let picker = UIDatePicker(frame: .zero)
picker.translatesAutoresizingMaskIntoConstraints = false
picker.datePickerMode = .countDownTimer
picker.countDownDuration = interval.wrappedValue
context.coordinator.add(picker: picker)
context.coordinator.interval = interval
return picker
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator {
var interval: Binding<TimeInterval>!
func add(picker: UIDatePicker) {
picker.addTarget(
self,
action: #selector(
dateChanged
),
for: .valueChanged
)
}
@objc
func dateChanged(_ picker: UIDatePicker) {
interval.wrappedValue = picker.countDownDuration
}
}
}

View File

@ -1,99 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
// TODO: fix relative padding, or remove?
// TODO: gradient should grow/shrink with content, not relative to container
struct LandscapePosterProgressBar<Content: View>: View {
@Default(.accentColor)
private var accentColor
// Scale padding depending on view width
@State
private var paddingScale: CGFloat = 1.0
@State
private var width: CGFloat = 0
private let content: () -> Content
private let progress: Double
var body: some View {
ZStack(alignment: .bottom) {
Color.clear
LinearGradient(
stops: [
.init(color: .clear, location: 0),
.init(color: .black.opacity(0.7), location: 1),
],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 40)
VStack(alignment: .leading, spacing: 3 * paddingScale) {
content()
ProgressBar(progress: progress)
.foregroundColor(accentColor)
.frame(height: 3)
}
.padding(.horizontal, 5 * paddingScale)
.padding(.bottom, 7 * paddingScale)
}
.onSizeChanged { newSize in
width = newSize.width
}
}
}
extension LandscapePosterProgressBar where Content == Text {
init(
title: String,
progress: Double
) {
self.init(
content: {
Text(title)
.font(.subheadline)
.foregroundColor(.white)
},
progress: progress
)
}
}
extension LandscapePosterProgressBar where Content == EmptyView {
init(progress: Double) {
self.init(
content: { EmptyView() },
progress: progress
)
}
}
extension LandscapePosterProgressBar {
init(
progress: Double,
@ViewBuilder content: @escaping () -> Content
) {
self.init(
content: content,
progress: progress
)
}
}

View File

@ -1,48 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct LanguagePicker: View {
let title: String
@Binding
var selectedLanguageCode: String?
// MARK: - Get all localized languages
private var languages: [(code: String?, name: String)] {
var uniqueLanguages = Set<String>()
var languageList: [(code: String?, name: String)] = Locale.availableIdentifiers.compactMap { identifier in
let locale = Locale(identifier: identifier)
if let code = locale.languageCode,
let name = locale.localizedString(forLanguageCode: code),
!uniqueLanguages.contains(code)
{
uniqueLanguages.insert(code)
return (code, name)
}
return nil
}
.sorted { $0.name < $1.name }
// Add None as an option at the top of the list
languageList.insert((code: nil, name: L10n.none), at: 0)
return languageList
}
// MARK: - Body
var body: some View {
Picker(title, selection: $selectedLanguageCode) {
ForEach(languages, id: \.code) { language in
Text(language.name).tag(language.code)
}
}
}
}

View File

@ -1,71 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct LearnMoreButton: View {
@State
private var isPresented: Bool = false
private let title: String
private let items: [TextPair]
// MARK: - Initializer
init(_ title: String, @ArrayBuilder<TextPair> items: () -> [TextPair]) {
self.title = title
self.items = items()
}
// MARK: - Body
var body: some View {
Button(L10n.learnMore + "\u{2026}") {
isPresented = true
}
.foregroundStyle(Color.accentColor)
.buttonStyle(.plain)
.frame(maxWidth: .infinity, alignment: .leading)
.sheet(isPresented: $isPresented) {
learnMoreView
}
}
// MARK: - Learn More View
private var learnMoreView: some View {
NavigationView {
ScrollView {
SeparatorVStack(alignment: .leading) {
Divider()
} content: {
ForEach(items) { content in
VStack(alignment: .leading, spacing: 8) {
Text(content.title)
.font(.headline)
.foregroundStyle(.primary)
Text(content.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.vertical, 16)
}
}
.edgePadding(.horizontal)
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.navigationBarCloseButton {
isPresented = false
}
}
.foregroundStyle(Color.primary, Color.secondary)
}
}

View File

@ -1,53 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
extension LetterPickerBar {
struct LetterPickerButton: View {
@Default(.accentColor)
private var accentColor
@Environment(\.isSelected)
private var isSelected
private let letter: ItemLetter
private let size: CGFloat
private let viewModel: FilterViewModel
init(letter: ItemLetter, size: CGFloat, viewModel: FilterViewModel) {
self.letter = letter
self.size = size
self.viewModel = viewModel
}
var body: some View {
Button {
if viewModel.currentFilters.letter.contains(letter) {
viewModel.send(.update(.letter, []))
} else {
viewModel.send(.update(.letter, [ItemLetter(stringLiteral: letter.value).asAnyItemFilter]))
}
} label: {
ZStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: size, height: size)
.foregroundStyle(isSelected ? accentColor : Color.clear)
Text(letter.value)
.font(.headline)
.foregroundStyle(isSelected ? accentColor.overlayColor : accentColor)
.frame(width: size, height: size, alignment: .center)
}
}
}
}
}

View File

@ -1,49 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
struct LetterPickerBar: View {
@ObservedObject
var viewModel: FilterViewModel
// MARK: - Body
@ViewBuilder
var body: some View {
VStack(spacing: 0) {
Spacer()
ForEach(ItemLetter.allCases, id: \.hashValue) { filterLetter in
LetterPickerButton(
letter: filterLetter,
size: LetterPickerBar.size,
viewModel: viewModel
)
.environment(\.isSelected, viewModel.currentFilters.letter.contains(filterLetter))
}
Spacer()
}
.scrollIfLargerThanContainer()
.frame(width: LetterPickerBar.size, alignment: .center)
}
// MARK: - Letter Button Size
static var size: CGFloat {
String().height(
withConstrainedWidth: CGFloat.greatestFiniteMagnitude,
font: UIFont.preferredFont(
forTextStyle: .headline
)
)
}
}

View File

@ -1,77 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: come up with better name along with `ListRowButton`
// Meant to be used when making a custom list without `List` or `Form`
struct ListRow<Leading: View, Content: View>: View {
@State
private var contentSize: CGSize = .zero
private let leading: () -> Leading
private let content: () -> Content
private var action: () -> Void
private var insets: EdgeInsets
private var isSeparatorVisible: Bool
var body: some View {
ZStack(alignment: .bottomTrailing) {
Button {
action()
} label: {
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
leading()
content()
.frame(maxHeight: .infinity)
.trackingSize($contentSize)
}
.padding(.top, insets.top)
.padding(.bottom, insets.bottom)
.padding(.leading, insets.leading)
.padding(.trailing, insets.trailing)
}
.foregroundStyle(.primary, .secondary)
Color.secondarySystemFill
.frame(width: contentSize.width, height: 1)
.padding(.trailing, insets.trailing)
.visible(isSeparatorVisible)
}
}
}
extension ListRow {
init(
insets: EdgeInsets = .zero,
@ViewBuilder leading: @escaping () -> Leading,
@ViewBuilder content: @escaping () -> Content
) {
self.init(
leading: leading,
content: content,
action: {},
insets: insets,
isSeparatorVisible: true
)
}
func isSeparatorVisible(_ isVisible: Bool) -> Self {
copy(modifying: \.isSeparatorVisible, with: isVisible)
}
func onSelect(perform action: @escaping () -> Void) -> Self {
copy(modifying: \.action, with: action)
}
}

View File

@ -1,65 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: come up with better name along with `ListRow`
// Meant to be used within `List` or `Form`
struct ListRowButton: View {
private let title: String
private let role: ButtonRole?
private let action: () -> Void
init(_ title: String, role: ButtonRole? = nil, action: @escaping () -> Void) {
self.title = title
self.role = role
self.action = action
}
var body: some View {
Button(title, role: role, action: action)
.buttonStyle(ListRowButtonStyle())
.listRowInsets(.zero)
}
}
private struct ListRowButtonStyle: ButtonStyle {
@Environment(\.isEnabled)
private var isEnabled
private func primaryStyle(configuration: Configuration) -> some ShapeStyle {
if configuration.role == .destructive || configuration.role == .cancel {
return AnyShapeStyle(Color.red)
} else {
return AnyShapeStyle(HierarchicalShapeStyle.primary)
}
}
private func secondaryStyle(configuration: Configuration) -> some ShapeStyle {
if configuration.role == .destructive || configuration.role == .cancel {
return AnyShapeStyle(Color.red.opacity(0.2))
} else {
return isEnabled ? AnyShapeStyle(HierarchicalShapeStyle.secondary) : AnyShapeStyle(Color.gray)
}
}
func makeBody(configuration: Configuration) -> some View {
ZStack {
Rectangle()
.fill(secondaryStyle(configuration: configuration))
configuration.label
.foregroundStyle(primaryStyle(configuration: configuration))
}
.opacity(configuration.isPressed ? 0.75 : 1)
.font(.body.weight(.bold))
}
}

View File

@ -1,192 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
// TODO: image
// TODO: rename
struct ListTitleSection: View {
private let title: String
private let description: String?
private let onLearnMore: (() -> Void)?
var body: some View {
Section {
VStack(alignment: .center, spacing: 10) {
Text(title)
.font(.title3)
.fontWeight(.semibold)
.multilineTextAlignment(.center)
if let description {
Text(description)
.multilineTextAlignment(.center)
}
if let onLearnMore {
Button(L10n.learnMore + "\u{2026}", action: onLearnMore)
}
}
.font(.subheadline)
.frame(maxWidth: .infinity)
}
}
}
extension ListTitleSection {
init(
_ title: String,
description: String? = nil
) {
self.init(
title: title,
description: description,
onLearnMore: nil
)
}
init(
_ title: String,
description: String? = nil,
onLearnMore: @escaping () -> Void
) {
self.init(
title: title,
description: description,
onLearnMore: onLearnMore
)
}
}
/// A view that mimics an inset grouped section, meant to be
/// used as a header for a `List` with `listStyle(.plain)`.
struct InsetGroupedListHeader<Content: View>: View {
@Default(.accentColor)
private var accentColor
private let content: () -> Content
private let title: Text?
private let description: Text?
private let onLearnMore: (() -> Void)?
@ViewBuilder
private var header: some View {
Button {
onLearnMore?()
} label: {
VStack(alignment: .center, spacing: 10) {
if let title {
title
.font(.title3)
.fontWeight(.semibold)
}
if let description {
description
.multilineTextAlignment(.center)
}
if onLearnMore != nil {
Text(L10n.learnMore + "\u{2026}")
.foregroundStyle(accentColor)
}
}
.font(.subheadline)
.frame(maxWidth: .infinity)
.padding(16)
}
.foregroundStyle(.primary, .secondary)
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(Color.secondarySystemBackground)
SeparatorVStack {
RowDivider()
} content: {
if title != nil || description != nil {
header
}
content()
.listRowSeparator(.hidden)
.padding(.init(vertical: 5, horizontal: 20))
.listRowInsets(.init(vertical: 10, horizontal: 20))
}
}
}
}
extension InsetGroupedListHeader {
init(
_ title: String? = nil,
description: String? = nil,
onLearnMore: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) {
self.init(
content: content,
title: title == nil ? nil : Text(title!),
description: description == nil ? nil : Text(description!),
onLearnMore: onLearnMore
)
}
init(
title: Text,
description: Text? = nil,
onLearnMore: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) {
self.init(
content: content,
title: title,
description: description,
onLearnMore: onLearnMore
)
}
}
extension InsetGroupedListHeader where Content == EmptyView {
init(
_ title: String,
description: String? = nil,
onLearnMore: (() -> Void)? = nil
) {
self.init(
content: { EmptyView() },
title: Text(title),
description: description == nil ? nil : Text(description!),
onLearnMore: onLearnMore
)
}
init(
title: Text,
description: Text? = nil,
onLearnMore: (() -> Void)? = nil
) {
self.init(
content: { EmptyView() },
title: title,
description: description,
onLearnMore: onLearnMore
)
}
}

View File

@ -1,81 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
extension NavigationBarFilterDrawer {
struct FilterDrawerButton: View {
@Default(.accentColor)
private var accentColor
@Environment(\.isSelected)
private var isSelected
private let systemName: String?
private let title: String
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
HStack(spacing: 2) {
Group {
if let systemName = systemName {
Image(systemName: systemName)
} else {
Text(title)
}
}
.font(.footnote.weight(.semibold))
Image(systemName: "chevron.down")
.font(.caption)
}
.foregroundColor(.primary)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background {
Capsule()
.foregroundColor(isSelected ? accentColor : Color(UIColor.secondarySystemFill))
.opacity(0.5)
}
.overlay {
Capsule()
.stroke(isSelected ? accentColor : Color(UIColor.secondarySystemFill), lineWidth: 1)
}
}
}
}
}
extension NavigationBarFilterDrawer.FilterDrawerButton {
init(title: String) {
self.init(
systemName: nil,
title: title,
onSelect: {}
)
}
init(systemName: String) {
self.init(
systemName: systemName,
title: "",
onSelect: {}
)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -1,67 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import JellyfinAPI
import SwiftUI
struct NavigationBarFilterDrawer: View {
@ObservedObject
private var viewModel: FilterViewModel
private var filterTypes: [ItemFilterType]
private var onSelect: (FilterCoordinator.Parameters) -> Void
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
if viewModel.currentFilters.hasFilters {
Menu {
Button(L10n.reset, role: .destructive) {
viewModel.send(.reset())
}
} label: {
FilterDrawerButton(systemName: "line.3.horizontal.decrease.circle.fill")
.environment(\.isSelected, true)
}
}
ForEach(filterTypes, id: \.self) { type in
FilterDrawerButton(
title: type.displayTitle
)
.onSelect {
onSelect(.init(type: type, viewModel: viewModel))
}
.environment(
\.isSelected,
viewModel.isFilterSelected(type: type)
)
}
}
.padding(.horizontal)
.padding(.vertical, 1)
}
}
}
extension NavigationBarFilterDrawer {
init(viewModel: FilterViewModel, types: [ItemFilterType]) {
self.init(
viewModel: viewModel,
filterTypes: types,
onSelect: { _ in }
)
}
func onSelect(_ action: @escaping (FilterCoordinator.Parameters) -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -1,124 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct OrderedSectionSelectorView<Element: Displayable & Hashable>: View {
@Environment(\.editMode)
private var editMode
@StateObject
private var selection: BindingBox<[Element]>
private var disabledSelection: [Element] {
sources.subtracting(selection.value)
}
private var label: (Element) -> any View
private let sources: [Element]
private func move(from source: IndexSet, to destination: Int) {
selection.value.move(fromOffsets: source, toOffset: destination)
}
private func select(element: Element) {
UIDevice.impact(.light)
if selection.value.contains(element) {
selection.value.removeAll(where: { $0 == element })
} else {
selection.value.append(element)
}
}
private var isReordering: Bool {
editMode?.wrappedValue.isEditing ?? false
}
var body: some View {
List {
Section(L10n.enabled) {
if selection.value.isEmpty {
L10n.none.text
.foregroundStyle(.secondary)
}
ForEach(selection.value, id: \.self) { element in
Button {
if !isReordering {
select(element: element)
}
} label: {
HStack {
label(element)
.eraseToAnyView()
Spacer()
if !isReordering {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
}
}
.foregroundColor(.primary)
}
}
.onMove(perform: move)
}
Section(L10n.disabled) {
if disabledSelection.isEmpty {
L10n.none.text
.foregroundStyle(.secondary)
}
ForEach(disabledSelection, id: \.self) { element in
Button {
if !isReordering {
select(element: element)
}
} label: {
HStack {
label(element)
.eraseToAnyView()
Spacer()
if !isReordering {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
}
}
.foregroundColor(.primary)
}
}
}
}
.animation(.linear(duration: 0.2), value: selection.value)
.toolbar {
EditButton()
}
}
}
extension OrderedSectionSelectorView {
init(selection: Binding<[Element]>, sources: [Element]) {
self._selection = StateObject(wrappedValue: BindingBox(source: selection))
self.sources = sources
self.label = { Text($0.displayTitle).foregroundColor(.primary) }
}
func label(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self {
copy(modifying: \.label, with: content)
}
}

View File

@ -1,62 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct PillHStack<Item: Displayable>: View {
private var title: String
private var items: [Item]
private var onSelect: (Item) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(title)
.font(.title2)
.fontWeight(.semibold)
.accessibility(addTraits: [.isHeader])
.edgePadding(.leading)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(items, id: \.displayTitle) { item in
Button {
onSelect(item)
} label: {
Text(item.displayTitle)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.padding(10)
.background {
Color.systemFill
.cornerRadius(10)
}
}
}
}
.edgePadding(.horizontal)
}
}
}
}
extension PillHStack {
init(title: String, items: [Item]) {
self.init(
title: title,
items: items,
onSelect: { _ in }
)
}
func onSelect(_ action: @escaping (Item) -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -1,266 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import JellyfinAPI
import SwiftUI
// TODO: expose `ImageView.image` modifier for image aspect fill/fit
// TODO: allow `content` to trigger `onSelect`?
// - not in button label to avoid context menu visual oddities
// TODO: why don't shadows work with failure image views?
// - due to `Color`?
/// Retrieving images by exact pixel dimensions is a bit
/// intense for normal usage and eases cache usage and modifications.
private let landscapeMaxWidth: CGFloat = 200
private let portraitMaxWidth: CGFloat = 200
struct PosterButton<Item: Poster>: View {
private var item: Item
private var type: PosterDisplayType
private var content: () -> any View
private var imageOverlay: () -> any View
private var contextMenu: () -> any View
private var onSelect: () -> Void
private func imageView(from item: Item) -> ImageView {
switch type {
case .landscape:
ImageView(item.landscapeImageSources(maxWidth: landscapeMaxWidth))
case .portrait:
ImageView(item.portraitImageSources(maxWidth: portraitMaxWidth))
}
}
var body: some View {
VStack(alignment: .leading) {
Button {
onSelect()
} label: {
ZStack {
Color.clear
imageView(from: item)
.failure {
if item.showTitle {
SystemImageContentView(systemName: item.systemImage)
} else {
SystemImageContentView(
title: item.displayTitle,
systemName: item.systemImage
)
}
}
imageOverlay()
.eraseToAnyView()
}
.posterStyle(type)
}
.buttonStyle(.plain)
.contextMenu(menuItems: {
contextMenu()
.eraseToAnyView()
})
.posterShadow()
content()
.eraseToAnyView()
}
}
}
extension PosterButton {
init(
item: Item,
type: PosterDisplayType
) {
self.init(
item: item,
type: type,
content: { TitleSubtitleContentView(item: item) },
imageOverlay: { DefaultOverlay(item: item) },
contextMenu: { EmptyView() },
onSelect: {}
)
}
func content(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.content, with: content)
}
func imageOverlay(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.imageOverlay, with: content)
}
func contextMenu(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.contextMenu, with: content)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}
// TODO: Shared default content with tvOS?
// - check if content is generally same
extension PosterButton {
// MARK: Default Content
struct TitleContentView: View {
let item: Item
var body: some View {
Text(item.displayTitle)
.font(.footnote.weight(.regular))
.foregroundColor(.primary)
}
}
struct SubtitleContentView: View {
let item: Item
var body: some View {
Text(item.subtitle ?? " ")
.font(.caption.weight(.medium))
.foregroundColor(.secondary)
}
}
struct TitleSubtitleContentView: View {
let item: Item
var body: some View {
iOS15View {
VStack(alignment: .leading, spacing: 0) {
if item.showTitle {
TitleContentView(item: item)
.backport
.lineLimit(1, reservesSpace: true)
.iOS15 { v in
v.font(.footnote.weight(.regular))
}
}
SubtitleContentView(item: item)
.backport
.lineLimit(1, reservesSpace: true)
.iOS15 { v in
v.font(.caption.weight(.medium))
}
}
} content: {
VStack(alignment: .leading) {
if item.showTitle {
TitleContentView(item: item)
.backport
.lineLimit(1, reservesSpace: true)
}
SubtitleContentView(item: item)
.backport
.lineLimit(1, reservesSpace: true)
}
}
}
}
// Content specific for BaseItemDto episode items
struct EpisodeContentSubtitleContent: View {
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
private var useSeriesLandscapeBackdrop
let item: Item
var body: some View {
if let item = item as? BaseItemDto {
// Unsure why this needs 0 spacing
// compared to other default content
VStack(alignment: .leading, spacing: 0) {
if item.showTitle, let seriesName = item.seriesName {
Text(seriesName)
.font(.footnote.weight(.regular))
.foregroundColor(.primary)
.backport
.lineLimit(1, reservesSpace: true)
}
SeparatorHStack {
Text(item.seasonEpisodeLabel ?? .emptyDash)
if item.showTitle || useSeriesLandscapeBackdrop {
Text(item.displayTitle)
} else if let seriesName = item.seriesName {
Text(seriesName)
}
}
.separator {
Circle()
.frame(width: 2, height: 2)
.padding(.horizontal, 3)
}
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
}
// MARK: Default Overlay
struct DefaultOverlay: View {
@Default(.accentColor)
private var accentColor
@Default(.Customization.Indicators.showFavorited)
private var showFavorited
@Default(.Customization.Indicators.showProgress)
private var showProgress
@Default(.Customization.Indicators.showUnplayed)
private var showUnplayed
@Default(.Customization.Indicators.showPlayed)
private var showPlayed
let item: Item
var body: some View {
ZStack {
if let item = item as? BaseItemDto {
if item.userData?.isPlayed ?? false {
WatchedIndicator(size: 25)
.visible(showPlayed)
} else {
if (item.userData?.playbackPositionTicks ?? 0) > 0 {
ProgressIndicator(progress: (item.userData?.playedPercentage ?? 0) / 100, height: 5)
.visible(showProgress)
} else {
UnwatchedIndicator(size: 25)
.foregroundColor(accentColor)
.visible(showUnplayed)
}
}
if item.userData?.isFavorite ?? false {
FavoriteIndicator(size: 25)
.visible(showFavorited)
}
}
}
}
}
}

View File

@ -1,154 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import CollectionHStack
import OrderedCollections
import SwiftUI
struct PosterHStack<Element: Poster & Identifiable, Data: Collection>: View where Data.Element == Element, Data.Index == Int {
private var data: Data
private var header: () -> any View
private var title: String?
private var type: PosterDisplayType
private var content: (Element) -> any View
private var imageOverlay: (Element) -> any View
private var contextMenu: (Element) -> any View
private var trailingContent: () -> any View
private var onSelect: (Element) -> Void
@ViewBuilder
private var padHStack: some View {
CollectionHStack(
uniqueElements: data,
minWidth: type == .portrait ? 140 : 220
) { item in
PosterButton(
item: item,
type: type
)
.content { content(item).eraseToAnyView() }
.imageOverlay { imageOverlay(item).eraseToAnyView() }
.contextMenu { contextMenu(item).eraseToAnyView() }
.onSelect { onSelect(item) }
}
.clipsToBounds(false)
.dataPrefix(20)
.insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.edgePadding / 2)
.scrollBehavior(.continuousLeadingEdge)
}
@ViewBuilder
private var phoneHStack: some View {
CollectionHStack(
uniqueElements: data,
columns: type == .portrait ? 3 : 2
) { item in
PosterButton(
item: item,
type: type
)
.content { content(item).eraseToAnyView() }
.imageOverlay { imageOverlay(item).eraseToAnyView() }
.contextMenu { contextMenu(item).eraseToAnyView() }
.onSelect { onSelect(item) }
}
.clipsToBounds(false)
.dataPrefix(20)
.insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.edgePadding / 2)
.scrollBehavior(.continuousLeadingEdge)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
header()
.eraseToAnyView()
Spacer()
trailingContent()
.eraseToAnyView()
}
.edgePadding(.horizontal)
if UIDevice.isPhone {
phoneHStack
} else {
padHStack
}
}
}
}
extension PosterHStack {
init(
title: String? = nil,
type: PosterDisplayType,
items: Data
) {
self.init(
data: items,
header: { DefaultHeader(title: title) },
title: title,
type: type,
content: { PosterButton.TitleSubtitleContentView(item: $0) },
imageOverlay: { PosterButton.DefaultOverlay(item: $0) },
contextMenu: { _ in EmptyView() },
trailingContent: { EmptyView() },
onSelect: { _ in }
)
}
func header(@ViewBuilder _ header: @escaping () -> any View) -> Self {
copy(modifying: \.header, with: header)
}
func content(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self {
copy(modifying: \.content, with: content)
}
func imageOverlay(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self {
copy(modifying: \.imageOverlay, with: content)
}
func contextMenu(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self {
copy(modifying: \.contextMenu, with: content)
}
func trailing(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trailingContent, with: content)
}
func onSelect(_ action: @escaping (Element) -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}
// MARK: Default Header
extension PosterHStack {
struct DefaultHeader: View {
let title: String?
var body: some View {
if let title {
Text(title)
.font(.title2)
.fontWeight(.semibold)
.accessibility(addTraits: [.isHeader])
}
}
}
}

View File

@ -1,51 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
struct PrimaryButton: View {
@Default(.accentColor)
private var accentColor
private let title: String
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
ZStack {
Rectangle()
.foregroundColor(accentColor)
.frame(maxWidth: 400)
.frame(height: 50)
.cornerRadius(10)
Text(title)
.fontWeight(.bold)
.foregroundColor(accentColor.overlayColor)
}
}
}
}
extension PrimaryButton {
init(title: String) {
self.init(
title: title,
onSelect: {}
)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -1,39 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct SeeAllButton: View {
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
HStack {
L10n.seeAll.text
Image(systemName: "chevron.right")
}
.font(.subheadline.bold())
}
}
}
extension SeeAllButton {
init() {
self.init(
onSelect: {}
)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -1,36 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Factory
import SwiftUI
struct SettingsBarButton: View {
let server: ServerState
let user: UserState
let action: () -> Void
var body: some View {
Button(action: action) {
AlternateLayoutView {
// Seems necessary for button layout
Image(systemName: "gearshape.fill")
} content: {
UserProfileImage(
userID: user.id,
source: user.profileImageSource(
client: server.client,
maxWidth: 120
),
pipeline: .Swiftfin.local
)
}
}
.accessibilityLabel(L10n.settings)
}
}

View File

@ -1,91 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
struct CapsuleSlider: View {
@Default(.VideoPlayer.Overlay.sliderColor)
private var sliderColor
@Binding
private var isEditing: Bool
@Binding
private var progress: CGFloat
private var trackMask: () -> any View
private var topContent: () -> any View
private var bottomContent: () -> any View
private var leadingContent: () -> any View
private var trailingContent: () -> any View
var body: some View {
Slider(progress: $progress)
.gestureBehavior(.track)
.trackGesturePadding(.init(top: 10, leading: 0, bottom: 30, trailing: 0))
.onEditingChanged { isEditing in
self.isEditing = isEditing
}
.track {
Capsule()
.frame(height: isEditing ? 20 : 10)
.foregroundColor(isEditing ? sliderColor : sliderColor.opacity(0.8))
}
.trackBackground {
Capsule()
.frame(height: isEditing ? 20 : 10)
.foregroundColor(Color.gray)
.opacity(0.5)
}
.trackMask(trackMask)
.topContent(topContent)
.bottomContent(bottomContent)
.leadingContent(leadingContent)
.trailingContent(trailingContent)
}
}
extension CapsuleSlider {
init(progress: Binding<CGFloat>) {
self.init(
isEditing: .constant(false),
progress: progress,
trackMask: { Color.white },
topContent: { EmptyView() },
bottomContent: { EmptyView() },
leadingContent: { EmptyView() },
trailingContent: { EmptyView() }
)
}
func isEditing(_ isEditing: Binding<Bool>) -> Self {
copy(modifying: \._isEditing, with: isEditing)
}
func trackMask(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trackMask, with: content)
}
func topContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.topContent, with: content)
}
func bottomContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.bottomContent, with: content)
}
func leadingContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.leadingContent, with: content)
}
func trailingContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trailingContent, with: content)
}
}

View File

@ -1,206 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct Slider: View {
enum Behavior {
case thumb
case track
}
@Binding
private var progress: CGFloat
@State
private var isEditing: Bool = false
@State
private var totalWidth: CGFloat = 0
@State
private var dragStartProgress: CGFloat = 0
@State
private var currentTranslationStartLocation: CGPoint = .zero
@State
private var currentTranslation: CGFloat = 0
@State
private var thumbSize: CGSize = .zero
private var sliderBehavior: Behavior
private var trackGesturePadding: EdgeInsets
private var track: () -> any View
private var trackBackground: () -> any View
private var trackMask: () -> any View
private var thumb: () -> any View
private var topContent: () -> any View
private var bottomContent: () -> any View
private var leadingContent: () -> any View
private var trailingContent: () -> any View
private var onEditingChanged: (Bool) -> Void
private var progressAnimation: Animation
private var trackDrag: some Gesture {
DragGesture(coordinateSpace: .global)
.onChanged { value in
if !isEditing {
isEditing = true
onEditingChanged(true)
dragStartProgress = progress
currentTranslationStartLocation = value.location
currentTranslation = 0
}
currentTranslation = currentTranslationStartLocation.x - value.location.x
let newProgress: CGFloat = dragStartProgress - currentTranslation / totalWidth
progress = min(max(0, newProgress), 1)
}
.onEnded { _ in
isEditing = false
onEditingChanged(false)
}
}
var body: some View {
HStack(alignment: .sliderCenterAlignmentGuide, spacing: 0) {
leadingContent()
.eraseToAnyView()
.alignmentGuide(.sliderCenterAlignmentGuide) { context in
context[VerticalAlignment.center]
}
VStack(spacing: 0) {
topContent()
.eraseToAnyView()
ZStack(alignment: .leading) {
ZStack {
trackBackground()
.eraseToAnyView()
track()
.eraseToAnyView()
.mask(alignment: .leading) {
Color.white
.frame(width: progress * totalWidth)
}
}
.mask {
trackMask()
.eraseToAnyView()
}
thumb()
.eraseToAnyView()
.if(sliderBehavior == .thumb) { view in
view.gesture(trackDrag)
}
.onSizeChanged { newSize in
thumbSize = newSize
}
.offset(x: progress * totalWidth - thumbSize.width / 2)
}
.onSizeChanged { size in
totalWidth = size.width
}
.if(sliderBehavior == .track) { view in
view.overlay {
Color.clear
.padding(trackGesturePadding)
.contentShape(Rectangle())
.highPriorityGesture(trackDrag)
}
}
.alignmentGuide(.sliderCenterAlignmentGuide) { context in
context[VerticalAlignment.center]
}
bottomContent()
.eraseToAnyView()
}
trailingContent()
.eraseToAnyView()
.alignmentGuide(.sliderCenterAlignmentGuide) { context in
context[VerticalAlignment.center]
}
}
.animation(progressAnimation, value: progress)
.animation(.linear(duration: 0.2), value: isEditing)
}
}
extension Slider {
init(progress: Binding<CGFloat>) {
self.init(
progress: progress,
sliderBehavior: .track,
trackGesturePadding: .zero,
track: { EmptyView() },
trackBackground: { EmptyView() },
trackMask: { EmptyView() },
thumb: { EmptyView() },
topContent: { EmptyView() },
bottomContent: { EmptyView() },
leadingContent: { EmptyView() },
trailingContent: { EmptyView() },
onEditingChanged: { _ in },
progressAnimation: .linear(duration: 0.05)
)
}
func track(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.track, with: content)
}
func trackBackground(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trackBackground, with: content)
}
func trackMask(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trackMask, with: content)
}
func thumb(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.thumb, with: content)
}
func topContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.topContent, with: content)
}
func bottomContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.bottomContent, with: content)
}
func leadingContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.leadingContent, with: content)
}
func trailingContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trailingContent, with: content)
}
func trackGesturePadding(_ insets: EdgeInsets) -> Self {
copy(modifying: \.trackGesturePadding, with: insets)
}
func onEditingChanged(_ action: @escaping (Bool) -> Void) -> Self {
copy(modifying: \.onEditingChanged, with: action)
}
func gestureBehavior(_ sliderBehavior: Behavior) -> Self {
copy(modifying: \.sliderBehavior, with: sliderBehavior)
}
func progressAnimation(_ animation: Animation) -> Self {
copy(modifying: \.progressAnimation, with: animation)
}
}

View File

@ -1,105 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
struct ThumbSlider: View {
@Default(.VideoPlayer.Overlay.sliderColor)
private var sliderColor
@Binding
private var isEditing: Bool
@Binding
private var progress: CGFloat
private var trackMask: () -> any View
private var topContent: () -> any View
private var bottomContent: () -> any View
private var leadingContent: () -> any View
private var trailingContent: () -> any View
var body: some View {
Slider(progress: $progress)
.gestureBehavior(.thumb)
.onEditingChanged { isEditing in
self.isEditing = isEditing
}
.track {
Capsule()
.foregroundColor(sliderColor)
.frame(height: 5)
}
.trackBackground {
Capsule()
.foregroundColor(Color.gray)
.opacity(0.5)
.frame(height: 5)
}
.thumb {
ZStack {
Color.clear
.frame(height: 25)
Circle()
.foregroundColor(sliderColor)
.frame(width: isEditing ? 25 : 20)
}
.overlay {
Color.clear
.frame(width: 50, height: 50)
.contentShape(Rectangle())
}
}
.trackMask(trackMask)
.topContent(topContent)
.bottomContent(bottomContent)
.leadingContent(leadingContent)
.trailingContent(trailingContent)
}
}
extension ThumbSlider {
init(progress: Binding<CGFloat>) {
self.init(
isEditing: .constant(false),
progress: progress,
trackMask: { Color.white },
topContent: { EmptyView() },
bottomContent: { EmptyView() },
leadingContent: { EmptyView() },
trailingContent: { EmptyView() }
)
}
func isEditing(_ isEditing: Binding<Bool>) -> Self {
copy(modifying: \._isEditing, with: isEditing)
}
func trackMask(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trackMask, with: content)
}
func topContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.topContent, with: content)
}
func bottomContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.bottomContent, with: content)
}
func leadingContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.leadingContent, with: content)
}
func trailingContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.trailingContent, with: content)
}
}

View File

@ -1,75 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
class SplitContentViewProxy: ObservableObject {
@Published
private(set) var isPresentingSplitView: Bool = false
func present() {
isPresentingSplitView = true
}
func hide() {
isPresentingSplitView = false
}
}
struct SplitContentView: View {
@ObservedObject
private var proxy: SplitContentViewProxy
private var content: () -> any View
private var splitContent: () -> any View
private var splitContentWidth: CGFloat
var body: some View {
HStack(spacing: 0) {
content()
.eraseToAnyView()
.frame(maxWidth: .infinity)
if proxy.isPresentingSplitView {
splitContent()
.eraseToAnyView()
.transition(.move(edge: .bottom))
.frame(width: splitContentWidth)
.zIndex(100)
}
}
.animation(.easeInOut(duration: 0.35), value: proxy.isPresentingSplitView)
}
}
extension SplitContentView {
init(splitContentWidth: CGFloat = 400) {
self.init(
proxy: .init(),
content: { EmptyView() },
splitContent: { EmptyView() },
splitContentWidth: splitContentWidth
)
}
func proxy(_ proxy: SplitContentViewProxy) -> Self {
copy(modifying: \.proxy, with: proxy)
}
func content(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.content, with: content)
}
func splitContent(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.splitContent, with: content)
}
}

View File

@ -1,138 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: use _UIHostingView for button animation workaround?
// - have a nice animation for toggle
struct UnmaskSecureField: UIViewRepresentable {
@Binding
private var text: String
private let onReturn: () -> Void
private let title: String
init(
_ title: String,
text: Binding<String>,
onReturn: @escaping () -> Void = {}
) {
self._text = text
self.title = title
self.onReturn = onReturn
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.font = context.environment.font?.uiFont ?? UIFont.preferredFont(forTextStyle: .body)
textField.adjustsFontForContentSizeCategory = true
textField.isSecureTextEntry = true
textField.keyboardType = .asciiCapable
textField.placeholder = title
textField.text = text
textField.addTarget(
context.coordinator,
action: #selector(Coordinator.textDidChange),
for: .editingChanged
)
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(
context.coordinator,
action: #selector(Coordinator.buttonPressed),
for: .touchUpInside
)
button.setImage(
UIImage(systemName: "eye.fill"),
for: .normal
)
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 50),
])
textField.rightView = button
textField.rightViewMode = .always
context.coordinator.button = button
context.coordinator.onReturn = onReturn
context.coordinator.textField = textField
context.coordinator.textDidChange()
context.coordinator.textBinding = _text
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
if text != textField.text {
textField.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, UITextFieldDelegate {
weak var button: UIButton?
weak var textField: UITextField?
var textBinding: Binding<String> = .constant("")
var onReturn: () -> Void = {}
@objc
func buttonPressed() {
guard let textField else { return }
textField.toggleSecureEntry()
let eye = textField.isSecureTextEntry ? "eye.fill" : "eye.slash"
button?.setImage(UIImage(systemName: eye), for: .normal)
}
@objc
func textDidChange() {
guard let textField, let text = textField.text else { return }
button?.isEnabled = !text.isEmpty
textBinding.wrappedValue = text
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
onReturn()
return true
}
}
}
private extension UITextField {
// https://stackoverflow.com/a/48115361
func toggleSecureEntry() {
isSecureTextEntry.toggle()
if let existingText = text, isSecureTextEntry {
deleteBackward()
if let textRange = textRange(from: beginningOfDocument, to: endOfDocument) {
replace(textRange, withText: existingText)
}
}
if let existingSelectedTextRange = selectedTextRange {
selectedTextRange = nil
selectedTextRange = existingSelectedTextRange
}
}
}

View File

@ -1,97 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: Better name
// TODO: don't use pushed to indicate a presented value
class UpdateViewProxy: ObservableObject {
@Published
private(set) var systemName: String? = nil
@Published
private(set) var iconSize: CGSize = .init(width: 25, height: 25)
@Published
private(set) var title: String = ""
@Published
private(set) var pushed: Bool = false
func present(systemName: String, title: String, iconSize: CGSize = .init(width: 25, height: 25)) {
self.systemName = systemName
self.iconSize = iconSize
self.title = title
pushed.toggle()
}
}
struct UpdateView: View {
@ObservedObject
private var proxy: UpdateViewProxy
@State
private var isPresenting: Bool = false
@State
private var workItem: DispatchWorkItem?
init(proxy: UpdateViewProxy) {
self.proxy = proxy
}
var body: some View {
ZStack {
if isPresenting {
HStack {
if let systemName = proxy.systemName {
Image(systemName: systemName)
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: proxy.iconSize.width, maxHeight: proxy.iconSize.height, alignment: .center)
}
Text(proxy.title)
.font(.body)
.fontWeight(.bold)
.monospacedDigit()
}
.padding(.horizontal, 24)
.padding(.vertical, 8)
.frame(minHeight: 50)
.background(BlurView())
.clipShape(Capsule())
.overlay(Capsule().stroke(Color.gray.opacity(0.2), lineWidth: 1))
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 6)
.compositingGroup()
.transition(.opacity)
}
}
.animation(.linear(duration: 0.1), value: proxy.systemName)
.animation(.linear(duration: 0.1), value: proxy.iconSize)
.onChange(of: proxy.pushed) { _ in
if !isPresenting {
withAnimation {
isPresenting = true
}
}
workItem?.cancel()
let task = DispatchWorkItem {
withAnimation(.spring()) {
isPresenting = false
}
}
workItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: task)
}
}
}

View File

@ -1,25 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
struct Video3DFormatPicker: View {
let title: String
@Binding
var selectedFormat: Video3DFormat?
var body: some View {
Picker(title, selection: $selectedFormat) {
Text(L10n.none).tag(nil as Video3DFormat?)
ForEach(Video3DFormat.allCases, id: \.self) { format in
Text(format.displayTitle).tag(format as Video3DFormat?)
}
}
}
}

View File

@ -1,24 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: remove when iOS 15 support removed
struct iOS15View<iOS15Content: View, Content: View>: View {
let iOS15: () -> iOS15Content
let content: () -> Content
var body: some View {
if #available(iOS 16, *) {
content()
} else {
iOS15()
}
}
}

View File

@ -1,42 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
extension ButtonStyle where Self == ToolbarPillButtonStyle {
static var toolbarPill: ToolbarPillButtonStyle {
ToolbarPillButtonStyle(primary: Defaults[.accentColor], secondary: .secondary)
}
static func toolbarPill(_ primary: Color, _ secondary: Color = Color.secondary) -> ToolbarPillButtonStyle {
ToolbarPillButtonStyle(primary: primary, secondary: secondary)
}
}
// TODO: don't take `Color`, take generic `ShapeStyle`
struct ToolbarPillButtonStyle: ButtonStyle {
@Environment(\.isEnabled)
private var isEnabled
let primary: Color
let secondary: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(isEnabled ? primary.overlayColor : secondary)
.font(.headline)
.padding(.vertical, 5)
.padding(.horizontal, 10)
.background(isEnabled ? primary : secondary)
.clipShape(RoundedRectangle(cornerRadius: 10))
.opacity(isEnabled && !configuration.isPressed ? 1 : 0.5)
}
}

View File

@ -1,68 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: see if could be moved to `Shared`
// MARK: EpisodeSelectorLabelStyle
extension LabelStyle where Self == EpisodeSelectorLabelStyle {
static var episodeSelector: EpisodeSelectorLabelStyle {
EpisodeSelectorLabelStyle()
}
}
struct EpisodeSelectorLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.title
configuration.icon
}
.font(.headline)
.padding(.vertical, 5)
.padding(.horizontal, 10)
.background {
Color.tertiarySystemFill
.cornerRadius(10)
}
.compositingGroup()
.shadow(radius: 1)
.font(.caption)
}
}
// MARK: SectionFooterWithImageLabelStyle
// TODO: rename as not only used in section footers
extension LabelStyle where Self == SectionFooterWithImageLabelStyle<AnyShapeStyle> {
static func sectionFooterWithImage<ImageStyle: ShapeStyle>(imageStyle: ImageStyle) -> SectionFooterWithImageLabelStyle<ImageStyle> {
SectionFooterWithImageLabelStyle(imageStyle: imageStyle)
}
}
struct SectionFooterWithImageLabelStyle<ImageStyle: ShapeStyle>: LabelStyle {
let imageStyle: ImageStyle
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
.foregroundStyle(imageStyle)
.backport
.fontWeight(.bold)
configuration.title
}
}
}

View File

@ -1,22 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct DetectOrientation: ViewModifier {
@Binding
var orientation: UIDeviceOrientation
func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientation = UIDevice.current.orientation
}
}
}

View File

@ -1,37 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
struct NavigationBarCloseButtonModifier: ViewModifier {
@Default(.accentColor)
private var accentColor
let disabled: Bool
let action: () -> Void
func body(content: Content) -> some View {
content.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
action()
} label: {
Image(systemName: "xmark.circle.fill")
.backport
.fontWeight(.bold)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
.opacity(disabled ? 0.75 : 1)
}
.disabled(disabled)
}
}
}
}

View File

@ -1,28 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct NavigationBarDrawerModifier<Drawer: View>: ViewModifier {
private let drawer: () -> Drawer
init(@ViewBuilder drawer: @escaping () -> Drawer) {
self.drawer = drawer
}
func body(content: Content) -> some View {
NavigationBarDrawerView {
drawer()
.ignoresSafeArea()
} content: {
content
}
.ignoresSafeArea()
}
}

View File

@ -1,113 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct NavigationBarDrawerView<Content: View, Drawer: View>: UIViewControllerRepresentable {
private let buttons: () -> Drawer
private let content: () -> Content
init(
@ViewBuilder buttons: @escaping () -> Drawer,
@ViewBuilder content: @escaping () -> Content
) {
self.buttons = buttons
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationBarDrawerHostingController<Content, Drawer> {
UINavigationBarDrawerHostingController<Content, Drawer>(buttons: buttons, content: content)
}
func updateUIViewController(_ uiViewController: UINavigationBarDrawerHostingController<Content, Drawer>, context: Context) {}
}
class UINavigationBarDrawerHostingController<Content: View, Drawer: View>: UIHostingController<Content> {
private let drawer: () -> Drawer
private let content: () -> Content
// TODO: see if we can get the height instead from the view passed in
private let drawerHeight: CGFloat = 36
private lazy var blurView: UIVisualEffectView = {
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
blurView.translatesAutoresizingMaskIntoConstraints = false
return blurView
}()
private lazy var drawerButtonsView: UIHostingController<Drawer> = {
let drawerButtonsView = UIHostingController(rootView: drawer())
drawerButtonsView.view.translatesAutoresizingMaskIntoConstraints = false
drawerButtonsView.view.backgroundColor = nil
return drawerButtonsView
}()
init(
buttons: @escaping () -> Drawer,
content: @escaping () -> Content
) {
self.drawer = buttons
self.content = content
super.init(rootView: content())
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = nil
view.addSubview(blurView)
addChild(drawerButtonsView)
view.addSubview(drawerButtonsView.view)
drawerButtonsView.didMove(toParent: self)
NSLayoutConstraint.activate([
drawerButtonsView.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: -drawerHeight),
drawerButtonsView.view.heightAnchor.constraint(equalToConstant: drawerHeight),
drawerButtonsView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
drawerButtonsView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
NSLayoutConstraint.activate([
blurView.topAnchor.constraint(equalTo: view.topAnchor),
blurView.bottomAnchor.constraint(equalTo: drawerButtonsView.view.bottomAnchor),
blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
navigationController?.navigationBar.shadowImage = nil
}
override var additionalSafeAreaInsets: UIEdgeInsets {
get {
.init(top: drawerHeight, left: 0, bottom: 0, right: 0)
}
set {
super.additionalSafeAreaInsets = .init(top: drawerHeight, left: 0, bottom: 0, right: 0)
}
}
}

View File

@ -1,41 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
struct NavigationBarMenuButtonModifier<Content: View>: ViewModifier {
@Default(.accentColor)
private var accentColor
let isLoading: Bool
let isHidden: Bool
let items: () -> Content
func body(content: Self.Content) -> some View {
content.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
if isLoading {
ProgressView()
}
if !isHidden {
Menu(L10n.options, systemImage: "ellipsis.circle") {
items()
}
.labelStyle(.iconOnly)
.backport
.fontWeight(.semibold)
.foregroundStyle(accentColor)
}
}
}
}
}

View File

@ -1,29 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct NavigationBarOffsetModifier: ViewModifier {
@Binding
var scrollViewOffset: CGFloat
let start: CGFloat
let end: CGFloat
func body(content: Content) -> some View {
NavigationBarOffsetView(
scrollViewOffset: $scrollViewOffset,
start: start,
end: end
) {
content
}
.ignoresSafeArea()
}
}

View File

@ -1,95 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct NavigationBarOffsetView<Content: View>: UIViewControllerRepresentable {
@Binding
private var scrollViewOffset: CGFloat
private let start: CGFloat
private let end: CGFloat
private let content: () -> Content
init(
scrollViewOffset: Binding<CGFloat>,
start: CGFloat,
end: CGFloat,
@ViewBuilder content: @escaping () -> Content
) {
self._scrollViewOffset = scrollViewOffset
self.start = start
self.end = end
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationBarOffsetHostingController<Content> {
UINavigationBarOffsetHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UINavigationBarOffsetHostingController<Content>, context: Context) {
uiViewController.scrollViewDidScroll(scrollViewOffset, start: start, end: end)
}
}
class UINavigationBarOffsetHostingController<Content: View>: UIHostingController<Content> {
private var lastAlpha: CGFloat = 0
private lazy var blurView: UIVisualEffectView = {
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
blurView.translatesAutoresizingMaskIntoConstraints = false
return blurView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = nil
view.addSubview(blurView)
blurView.alpha = 0
NSLayoutConstraint.activate([
blurView.topAnchor.constraint(equalTo: view.topAnchor),
blurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
func scrollViewDidScroll(_ offset: CGFloat, start: CGFloat, end: CGFloat) {
let diff = end - start
let currentProgress = (offset - start) / diff
let alpha = clamp(currentProgress, min: 0, max: 1)
navigationController?.navigationBar
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(alpha)]
blurView.alpha = alpha
lastAlpha = alpha
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(lastAlpha)]
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label]
navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
navigationController?.navigationBar.shadowImage = nil
}
}

View File

@ -1,121 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
import SwiftUIIntrospect
extension View {
// TODO: remove after removing support for iOS 15
@ViewBuilder
func iOS15<Content: View>(@ViewBuilder _ content: (Self) -> Content) -> some View {
if #available(iOS 16, *) {
self
} else {
content(self)
}
}
func detectOrientation(_ orientation: Binding<UIDeviceOrientation>) -> some View {
modifier(DetectOrientation(orientation: orientation))
}
func navigationBarOffset(_ scrollViewOffset: Binding<CGFloat>, start: CGFloat, end: CGFloat) -> some View {
modifier(NavigationBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end))
}
func navigationBarDrawer<Drawer: View>(@ViewBuilder _ drawer: @escaping () -> Drawer) -> some View {
modifier(NavigationBarDrawerModifier(drawer: drawer))
}
@ViewBuilder
func navigationBarFilterDrawer(
viewModel: FilterViewModel,
types: [ItemFilterType],
onSelect: @escaping (FilterCoordinator.Parameters) -> Void
) -> some View {
if types.isEmpty {
self
} else {
navigationBarDrawer {
NavigationBarFilterDrawer(
viewModel: viewModel,
types: types
)
.onSelect(onSelect)
}
}
}
func onAppDidEnterBackground(_ action: @escaping () -> Void) -> some View {
onNotification(.applicationDidEnterBackground) {
action()
}
}
func onAppWillResignActive(_ action: @escaping () -> Void) -> some View {
onNotification(.applicationWillResignActive) { _ in
action()
}
}
func onAppWillTerminate(_ action: @escaping () -> Void) -> some View {
onNotification(.applicationWillTerminate) { _ in
action()
}
}
@ViewBuilder
func navigationBarCloseButton(
disabled: Bool = false,
_ action: @escaping () -> Void
) -> some View {
modifier(
NavigationBarCloseButtonModifier(
disabled: disabled,
action: action
)
)
}
@ViewBuilder
func navigationBarMenuButton<Content: View>(
isLoading: Bool = false,
isHidden: Bool = false,
@ViewBuilder
_ items: @escaping () -> Content
) -> some View {
modifier(
NavigationBarMenuButtonModifier(
isLoading: isLoading,
isHidden: isHidden,
items: items
)
)
}
@ViewBuilder
func listRowCornerRadius(_ radius: CGFloat) -> some View {
if #unavailable(iOS 16) {
introspect(.listCell, on: .iOS(.v15)) { cell in
cell.layer.cornerRadius = radius
}
} else {
introspect(
.listCell,
on: .iOS(.v16),
.iOS(.v17),
.iOS(.v18)
) { cell in
cell.layer.cornerRadius = radius
}
}
}
}

View File

@ -1,110 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Foundation
import JellyfinAPI
import Stinsen
final class AppURLHandler {
static let deepLinkScheme = "jellyfin"
enum AppURLState {
case launched
case allowedInLogin
case allowed
func allowedScheme(with url: URL) -> Bool {
switch self {
case .launched:
return false
case .allowed:
return true
case .allowedInLogin:
return false
}
}
}
static let shared = AppURLHandler()
var cancellables = Set<AnyCancellable>()
var appURLState: AppURLState = .launched
var launchURL: URL?
}
extension AppURLHandler {
@discardableResult
func processDeepLink(url: URL) -> Bool {
guard url.scheme == Self.deepLinkScheme || url.scheme == "widget-extension" else {
return false
}
if AppURLHandler.shared.appURLState.allowedScheme(with: url) {
return processURL(url)
} else {
launchURL = url
}
return true
}
func processLaunchedURLIfNeeded() {
guard let launchURL = launchURL,
launchURL.absoluteString.isNotEmpty else { return }
if processDeepLink(url: launchURL) {
self.launchURL = nil
}
}
private func processURL(_ url: URL) -> Bool {
if processURLForUser(url: url) {
return true
}
return false
}
private func processURLForUser(url: URL) -> Bool {
guard url.host?.lowercased() == "users",
url.pathComponents[safe: 1]?.isEmpty == false else { return false }
// /Users/{UserID}/Items/{ItemID}
if url.pathComponents[safe: 2]?.lowercased() == "items",
let userID = url.pathComponents[safe: 1],
let itemID = url.pathComponents[safe: 3]
{
// It would be nice if the ItemViewModel could be initialized to id later.
getItem(userID: userID, itemID: itemID) { item in
guard let item = item else { return }
// TODO: reimplement URL handling
// Notifications[.processDeepLink].post(DeepLink.item(item))
}
return true
}
return false
}
}
extension AppURLHandler {
func getItem(userID: String, itemID: String, completion: @escaping (BaseItemDto?) -> Void) {
// UserLibraryAPI.getItem(userId: userID, itemId: itemID)
// .sink(receiveCompletion: { innerCompletion in
// switch innerCompletion {
// case .failure:
// completion(nil)
// default:
// break
// }
// }, receiveValue: { item in
// completion(item)
// })
// .store(in: &cancellables)
}
}

View File

@ -1,14 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
enum DeepLink {
case item(BaseItemDto)
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-blue</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-blue" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-dark-blue.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-green</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-green" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-dark-green.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-jellyfin</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-dark-jellyfin.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-orange</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-orange" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-dark-orange.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-red</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-red" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-dark-red.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-yellow</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-dark-yellow.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-blue</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-blue" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedDark-blue.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-green</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-green" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedDark-green.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-jellyfin</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedDark-jellyfin.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-orange</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-orange" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedDark-orange.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-red</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-red" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedDark-red.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-yellow</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedDark-yellow.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-blue</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-blue" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedLight-blue.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-green</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-green" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedLight-green.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-jellyfin</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedLight-jellyfin.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-orange</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-orange" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedLight-orange.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-red</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-red" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedLight-red.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-yellow</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-invertedLight-yellow.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-blue</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-blue" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-light-blue.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-green</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-green" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-light-green.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-jellyfin</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-light-jellyfin.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-orange</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-orange" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-light-orange.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-red</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-red" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-light-red.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-yellow</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-light-yellow.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>primary</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Primary" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="primary" fill-rule="nonzero">
<rect id="solid-background" fill="#020B23" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-primary-primary.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Some files were not shown because too many files have changed in this diff Show More