From c3414f97676363e4bc5828da4562304a389f41ce Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 19 Oct 2024 21:00:30 -0600 Subject: [PATCH 1/8] Implementation. TODO: Make the button go to the Admin Dashboard. But this would work better if all of the Admin Dashboard items were in their own dashboard. --- .../StoredValue/StoredValues+User.swift | 8 + Swiftfin.xcodeproj/project.pbxproj | 4 + .../Components/ActiveSessionsIndicator.swift | 176 ++++++++++++++++++ Swiftfin/Views/HomeView/HomeView.swift | 21 ++- .../Components/Sections/HomeSection.swift | 11 ++ .../CustomizeViewsSettings.swift | 4 + 6 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 Swiftfin/Components/ActiveSessionsIndicator.swift diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift index 2d994e1f8..62b58c9ec 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift @@ -141,6 +141,14 @@ extension StoredValues.Keys { ) } + static var activeSessionIndicator: Key { + CurrentUserKey( + "activeSessionIndicator", + domain: "activeSessionIndicator", + default: false + ) + } + static var customDeviceProfiles: Key<[CustomDeviceProfile]> { CurrentUserKey( "customDeviceProfiles", diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 8c9c9bcef..45f3961ca 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; }; 4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; }; 4E0A8FFC2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */; }; + 4E10F7F32CC49BDE0032C4B7 /* ActiveSessionsIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E10F7F22CC49BCF0032C4B7 /* ActiveSessionsIndicator.swift */; }; 4E11805F2CBF52380077A588 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 4E12F9172CBE9619006C217E /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; }; 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */; }; @@ -1028,6 +1029,7 @@ /* Begin PBXFileReference section */ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; 4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCompletionStatus.swift; sourceTree = ""; }; + 4E10F7F22CC49BCF0032C4B7 /* ActiveSessionsIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsIndicator.swift; sourceTree = ""; }; 4E12F9152CBE9615006C217E /* DeviceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceType.swift; sourceTree = ""; }; 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = ""; }; 4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = ""; }; @@ -2440,6 +2442,7 @@ 53F866422687A45400DCD1D7 /* Components */ = { isa = PBXGroup; children = ( + 4E10F7F22CC49BCF0032C4B7 /* ActiveSessionsIndicator.swift */, E1D8429429346C6400D1041A /* BasicStepper.swift */, E133328C2953AE4B00EE76AB /* CircularProgressView.swift */, E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */, @@ -4932,6 +4935,7 @@ E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */, E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */, E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */, + 4E10F7F32CC49BDE0032C4B7 /* ActiveSessionsIndicator.swift in Sources */, E1B33ED128EB860A0073B0FD /* LargePlaybackButtons.swift in Sources */, E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */, E102313F2BCF8A3C009D71FC /* DetailedChannelView.swift in Sources */, diff --git a/Swiftfin/Components/ActiveSessionsIndicator.swift b/Swiftfin/Components/ActiveSessionsIndicator.swift new file mode 100644 index 000000000..33357e4e3 --- /dev/null +++ b/Swiftfin/Components/ActiveSessionsIndicator.swift @@ -0,0 +1,176 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct ActiveSessionIndicator: View { + @ObservedObject + var viewModel = ActiveSessionsViewModel() + + let action: () -> Void + + // MARK: - View Model Update Timer + + private let timer = Timer.publish(every: 60, on: .main, in: .common) + .autoconnect() + + // MARK: - Spinner States + + @State + private var isSpinning = false + @State + private var showSpinner = false + + // MARK: - Session States + + var activeSessions: [SessionInfo] { + viewModel.sessions.compactMap(\.value.value).filter { + $0.nowPlayingItem != nil + } + } + + // MARK: - Do Active Sessions Exist + + var isEnabled: Bool { + activeSessions.isNotEmpty + } + + // MARK: - Initializer + + init(action: @escaping () -> Void) { + self.action = action + self.viewModel.send(.getSessions) + } + + // MARK: - Body + + var body: some View { + Button(action: action) { + contentView + .onReceive(timer) { _ in + viewModel.send(.getSessions) + } + } + } + + // MARK: - Content View + + var contentView: some View { + switch viewModel.state { + case .content, .initial: + AnyView(sessionsView) + default: + AnyView(errorView) + } + } + + // MARK: - Sessions View + + var sessionsView: some View { + HStack(alignment: .bottom) { + if isEnabled { + counterView + .offset(x: 5) + } + ZStack { + imageView + if showSpinner { + loadingSpinner + } else { + idleCircle + } + } + .onChange(of: viewModel.backgroundStates) { newState in + if newState.contains(.gettingSessions) { + showSpinner = true + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + if !viewModel.backgroundStates.contains(.gettingSessions) { + showSpinner = false + } + } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + showSpinner = false + } + } + } + } + } + + // MARK: - Image View + + var imageView: some View { + Image(systemName: "waveform.path.ecg") + .resizable() + .scaledToFit() + .padding(4) + .frame(width: 25, height: 23) + .foregroundColor(.primary) + .background( + Circle() + .fill(isEnabled ? Color.accentColor : .secondary) + ) + } + + // MARK: - Error View + + var errorView: some View { + Image(systemName: "exclamationmark.triangle") + .resizable() + .scaledToFit() + .padding(4) + .frame(width: 25, height: 25) + .foregroundColor(.black) + .background( + Circle() + .fill(.yellow) + ) + } + + // MARK: - Loading Spinner View + + var loadingSpinner: some View { + Circle() + .trim(from: 0.25, to: 0.75) + .stroke(showSpinner ? (isEnabled ? Color.accentColor : .secondary) : Color.clear, lineWidth: 2) + .frame(width: 30, height: 30) + .rotationEffect( + Angle(degrees: isSpinning ? 360 : 0) + ) + .animation( + .linear(duration: 1.5) + .repeatForever(autoreverses: false), + value: isSpinning + ) + .onAppear { + isSpinning = true + } + .onDisappear { + isSpinning = false + } + } + + // MARK: - Spacer Spinner View + + var idleCircle: some View { + // This exists to ensure spacing so the image doesn't move when loading happens + Circle() + .stroke(Color.clear, lineWidth: 2) + .frame(width: 30, height: 30) + } + + // MARK: - Counter View + + var counterView: some View { + Text("\(activeSessions.count)") + .font(.headline) + .padding(0) + .foregroundStyle(isEnabled ? Color.accentColor : .secondary) + } +} diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index 64da0366a..bff9e0a19 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -16,6 +16,9 @@ import SwiftUI // - indicated by snapping to the top struct HomeView: View { + @StoredValue(.User.activeSessionIndicator) + private var activeSessionIndicator + @Default(.Customization.nextUpPosterType) private var nextUpPosterType @Default(.Customization.Home.showRecentlyAdded) @@ -87,11 +90,19 @@ struct HomeView: View { ProgressView() } - SettingsBarButton( - server: viewModel.userSession.server, - user: viewModel.userSession.user - ) { - mainRouter.route(to: \.settings) + HStack(spacing: 0) { + if activeSessionIndicator { + ActiveSessionIndicator { + print("This will take you to the Admin Dashboard once it's on its own coordinator...") + } + } + + SettingsBarButton( + server: viewModel.userSession.server, + user: viewModel.userSession.user + ) { + mainRouter.route(to: \.settings) + } } } .sinceLastDisappear { interval in diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index 8c1b5bdf0..d9d0e6c48 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -7,12 +7,19 @@ // import Defaults +import Factory import SwiftUI extension CustomizeViewsSettings { struct HomeSection: View { + @Injected(\.currentUserSession) + private var userSession + + @StoredValue(.User.activeSessionIndicator) + private var activeSessionIndicator + @Default(.Customization.Home.showRecentlyAdded) private var showRecentlyAdded @Default(.Customization.Home.maxNextUp) @@ -23,6 +30,10 @@ extension CustomizeViewsSettings { var body: some View { Section(L10n.home) { + if userSession?.user.isAdministrator ?? false { + Toggle("Activity Indicator", isOn: $activeSessionIndicator) + } + Toggle(L10n.showRecentlyAdded, isOn: $showRecentlyAdded) Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp) diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index b4ae734be..51216721e 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -7,12 +7,16 @@ // import Defaults +import Factory import SwiftUI // TODO: will be entirely re-organized struct CustomizeViewsSettings: View { + @Injected(\.currentUserSession) + private var userSesssion + @Default(.Customization.itemViewType) private var itemViewType @Default(.Customization.CinematicItemViewType.usePrimaryImage) From 6347994026e4764df2c9c4c63f1d9b70e73e8b43 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 19 Oct 2024 22:19:45 -0600 Subject: [PATCH 2/8] Review Items --- Shared/Strings/Strings.swift | 2 ++ Swiftfin/Components/ActiveSessionsIndicator.swift | 5 +++-- Swiftfin/Views/HomeView/HomeView.swift | 2 +- .../Components/Sections/HomeSection.swift | 2 +- Translations/en.lproj/Localizable.strings | 3 +++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 4c51e9cc4..7c0a36399 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -20,6 +20,8 @@ internal enum L10n { internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility") /// ActiveSessionsView Header internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices") + /// Activity Indicator naming / settings label + internal static let activityIndicator = L10n.tr("Localizable", "activityIndicator", fallback: "Activity Indicator") /// Select Server View - Add Server internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server") /// Add URL diff --git a/Swiftfin/Components/ActiveSessionsIndicator.swift b/Swiftfin/Components/ActiveSessionsIndicator.swift index 33357e4e3..79cd7e755 100644 --- a/Swiftfin/Components/ActiveSessionsIndicator.swift +++ b/Swiftfin/Components/ActiveSessionsIndicator.swift @@ -61,12 +61,13 @@ struct ActiveSessionIndicator: View { // MARK: - Content View + @ViewBuilder var contentView: some View { switch viewModel.state { case .content, .initial: - AnyView(sessionsView) + sessionsView default: - AnyView(errorView) + errorView } } diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index bff9e0a19..42a31a467 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -93,7 +93,7 @@ struct HomeView: View { HStack(spacing: 0) { if activeSessionIndicator { ActiveSessionIndicator { - print("This will take you to the Admin Dashboard once it's on its own coordinator...") + // TODO: "This will take you to the Admin Dashboard once it's on its own coordinator } } diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index d9d0e6c48..9a4688d46 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -31,7 +31,7 @@ extension CustomizeViewsSettings { Section(L10n.home) { if userSession?.user.isAdministrator ?? false { - Toggle("Activity Indicator", isOn: $activeSessionIndicator) + Toggle(L10n.activityIndicator, isOn: $activeSessionIndicator) } Toggle(L10n.showRecentlyAdded, isOn: $showRecentlyAdded) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 50941eb59..b05c83755 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -738,3 +738,6 @@ /* Section Title for Column Configuration */ "columns" = "Columns"; + +/* Activity Indicator naming / settings label*/ +"activityIndicator" = "Activity Indicator"; From 5467f57fca313ef5ffe826708051ece344c61cb0 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 21 Oct 2024 09:27:32 -0600 Subject: [PATCH 3/8] New Activity Bage and remove the loading circle indicator. Remove 0 spacing between User Icon and this on Home View --- Swiftfin.xcodeproj/project.pbxproj | 4 + .../Components/ActiveSessionsIndicator.swift | 77 ++++--------------- Swiftfin/Components/ActivityBadge.swift | 59 ++++++++++++++ Swiftfin/Views/HomeView/HomeView.swift | 20 +++-- 4 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 Swiftfin/Components/ActivityBadge.swift diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 45f3961ca..bbd0eae16 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; }; 4EDBDCD12CBDD6590033D347 /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */; }; 4EDBDCD22CBDD6590033D347 /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */; }; + 4EE11E1A2CC6A513004BF852 /* ActivityBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE11E192CC6A50D004BF852 /* ActivityBadge.swift */; }; 4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ProgressSection.swift */; }; 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; }; 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; @@ -1078,6 +1079,7 @@ 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = ""; }; 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = ""; }; 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; + 4EE11E192CC6A50D004BF852 /* ActivityBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityBadge.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSection.swift; sourceTree = ""; }; 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; @@ -2443,6 +2445,7 @@ isa = PBXGroup; children = ( 4E10F7F22CC49BCF0032C4B7 /* ActiveSessionsIndicator.swift */, + 4EE11E192CC6A50D004BF852 /* ActivityBadge.swift */, E1D8429429346C6400D1041A /* BasicStepper.swift */, E133328C2953AE4B00EE76AB /* CircularProgressView.swift */, E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */, @@ -4687,6 +4690,7 @@ E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */, E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, + 4EE11E1A2CC6A513004BF852 /* ActivityBadge.swift in Sources */, E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */, E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, diff --git a/Swiftfin/Components/ActiveSessionsIndicator.swift b/Swiftfin/Components/ActiveSessionsIndicator.swift index 79cd7e755..8a428ea9f 100644 --- a/Swiftfin/Components/ActiveSessionsIndicator.swift +++ b/Swiftfin/Components/ActiveSessionsIndicator.swift @@ -75,32 +75,26 @@ struct ActiveSessionIndicator: View { var sessionsView: some View { HStack(alignment: .bottom) { - if isEnabled { - counterView - .offset(x: 5) - } - ZStack { - imageView - if showSpinner { - loadingSpinner - } else { - idleCircle + imageView + .overlay { + if !activeSessions.isEmpty { + ActivityBadge(value: activeSessions.count) + } } - } - .onChange(of: viewModel.backgroundStates) { newState in - if newState.contains(.gettingSessions) { - showSpinner = true - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - if !viewModel.backgroundStates.contains(.gettingSessions) { + .onChange(of: viewModel.backgroundStates) { newState in + if newState.contains(.gettingSessions) { + showSpinner = true + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + if !viewModel.backgroundStates.contains(.gettingSessions) { + showSpinner = false + } + } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { showSpinner = false } } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - showSpinner = false - } } - } } } @@ -133,45 +127,4 @@ struct ActiveSessionIndicator: View { .fill(.yellow) ) } - - // MARK: - Loading Spinner View - - var loadingSpinner: some View { - Circle() - .trim(from: 0.25, to: 0.75) - .stroke(showSpinner ? (isEnabled ? Color.accentColor : .secondary) : Color.clear, lineWidth: 2) - .frame(width: 30, height: 30) - .rotationEffect( - Angle(degrees: isSpinning ? 360 : 0) - ) - .animation( - .linear(duration: 1.5) - .repeatForever(autoreverses: false), - value: isSpinning - ) - .onAppear { - isSpinning = true - } - .onDisappear { - isSpinning = false - } - } - - // MARK: - Spacer Spinner View - - var idleCircle: some View { - // This exists to ensure spacing so the image doesn't move when loading happens - Circle() - .stroke(Color.clear, lineWidth: 2) - .frame(width: 30, height: 30) - } - - // MARK: - Counter View - - var counterView: some View { - Text("\(activeSessions.count)") - .font(.headline) - .padding(0) - .foregroundStyle(isEnabled ? Color.accentColor : .secondary) - } } diff --git a/Swiftfin/Components/ActivityBadge.swift b/Swiftfin/Components/ActivityBadge.swift new file mode 100644 index 000000000..3a2ed1c1e --- /dev/null +++ b/Swiftfin/Components/ActivityBadge.swift @@ -0,0 +1,59 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct ActivityBadge: View { + var value: Int + + @State + var foreground: Color = .primary + @State + var background: Color = .accentColor + + private let size = 16.0 + private let x = 20.0 + private let y = 0.0 + + var body: some View { + ZStack { + Capsule() + .fill(background) + .frame(width: size * widthMultplier(), height: size, alignment: .topTrailing) + .position(x: x, y: y) + + if hasTwoOrLessDigits() { + Text("\(value)") + .foregroundColor(foreground) + .font(Font.caption) + .position(x: x, y: y) + } else { + Text("99+") + .foregroundColor(foreground) + .font(Font.caption) + .frame(width: size * widthMultplier(), height: size, alignment: .center) + .position(x: x, y: y) + } + } + .opacity(value == 0 ? 0 : 1) + } + + func hasTwoOrLessDigits() -> Bool { + value < 100 + } + + func widthMultplier() -> Double { + if value < 10 { + return 1.0 + } else if value < 100 { + return 1.5 + } else { + return 2.0 + } + } +} diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index 42a31a467..def7de99f 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -90,19 +90,17 @@ struct HomeView: View { ProgressView() } - HStack(spacing: 0) { - if activeSessionIndicator { - ActiveSessionIndicator { - // TODO: "This will take you to the Admin Dashboard once it's on its own coordinator - } + if activeSessionIndicator { + ActiveSessionIndicator { + // TODO: "This will take you to the Admin Dashboard once it's on its own coordinator } + } - SettingsBarButton( - server: viewModel.userSession.server, - user: viewModel.userSession.user - ) { - mainRouter.route(to: \.settings) - } + SettingsBarButton( + server: viewModel.userSession.server, + user: viewModel.userSession.user + ) { + mainRouter.route(to: \.settings) } } .sinceLastDisappear { interval in From d14694f15107c4e64a8fbcf0a448b65b0902b34c Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 22 Oct 2024 23:26:14 -0600 Subject: [PATCH 4/8] Cleanup + Color Changes --- .../Components/ActiveSessionsIndicator.swift | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/Swiftfin/Components/ActiveSessionsIndicator.swift b/Swiftfin/Components/ActiveSessionsIndicator.swift index 8a428ea9f..054c67098 100644 --- a/Swiftfin/Components/ActiveSessionsIndicator.swift +++ b/Swiftfin/Components/ActiveSessionsIndicator.swift @@ -20,13 +20,6 @@ struct ActiveSessionIndicator: View { private let timer = Timer.publish(every: 60, on: .main, in: .common) .autoconnect() - // MARK: - Spinner States - - @State - private var isSpinning = false - @State - private var showSpinner = false - // MARK: - Session States var activeSessions: [SessionInfo] { @@ -35,12 +28,6 @@ struct ActiveSessionIndicator: View { } } - // MARK: - Do Active Sessions Exist - - var isEnabled: Bool { - activeSessions.isNotEmpty - } - // MARK: - Initializer init(action: @escaping () -> Void) { @@ -81,20 +68,6 @@ struct ActiveSessionIndicator: View { ActivityBadge(value: activeSessions.count) } } - .onChange(of: viewModel.backgroundStates) { newState in - if newState.contains(.gettingSessions) { - showSpinner = true - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - if !viewModel.backgroundStates.contains(.gettingSessions) { - showSpinner = false - } - } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - showSpinner = false - } - } - } } } @@ -105,11 +78,11 @@ struct ActiveSessionIndicator: View { .resizable() .scaledToFit() .padding(4) - .frame(width: 25, height: 23) + .frame(width: 25, height: 25) .foregroundColor(.primary) .background( Circle() - .fill(isEnabled ? Color.accentColor : .secondary) + .fill(.secondary) ) } From 382eea5119e203f4e9ba1325fca4ab50df7efd36 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 23 Oct 2024 16:55:32 -0600 Subject: [PATCH 5/8] Update ActivityBadge.swift --- Swiftfin/Components/ActivityBadge.swift | 45 +++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/Swiftfin/Components/ActivityBadge.swift b/Swiftfin/Components/ActivityBadge.swift index 3a2ed1c1e..e740896af 100644 --- a/Swiftfin/Components/ActivityBadge.swift +++ b/Swiftfin/Components/ActivityBadge.swift @@ -9,12 +9,8 @@ import SwiftUI struct ActivityBadge: View { - var value: Int - @State - var foreground: Color = .primary - @State - var background: Color = .accentColor + let value: Int private let size = 16.0 private let x = 20.0 @@ -23,19 +19,26 @@ struct ActivityBadge: View { var body: some View { ZStack { Capsule() - .fill(background) + .fill(.primary) .frame(width: size * widthMultplier(), height: size, alignment: .topTrailing) + .overlay { + Capsule() + .stroke(Color.systemBackground, lineWidth: 2) + .clipped() + } .position(x: x, y: y) if hasTwoOrLessDigits() { Text("\(value)") - .foregroundColor(foreground) - .font(Font.caption) + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.secondary) .position(x: x, y: y) } else { Text("99+") - .foregroundColor(foreground) - .font(Font.caption) + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.secondary) .frame(width: size * widthMultplier(), height: size, alignment: .center) .position(x: x, y: y) } @@ -57,3 +60,25 @@ struct ActivityBadge: View { } } } + +#Preview { + VStack { + Button {} label: { + Image(systemName: "waveform.path.ecg") + .resizable() + .backport + .fontWeight(.semibold) + .padding(4) + .frame(width: 25, height: 25) + .foregroundColor(.primary) + .background( + Circle() + .fill(.secondary) + ) + .overlay { + ActivityBadge(value: 24) + .foregroundStyle(.blue, .blue.overlayColor) + } + } + } +} From e3f6311538c57af66d7a0ea19e4249ad31bd2e55 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 23 Oct 2024 16:56:36 -0600 Subject: [PATCH 6/8] Update ActivityBadge.swift --- Swiftfin/Components/ActivityBadge.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Swiftfin/Components/ActivityBadge.swift b/Swiftfin/Components/ActivityBadge.swift index e740896af..48a478fa8 100644 --- a/Swiftfin/Components/ActivityBadge.swift +++ b/Swiftfin/Components/ActivityBadge.swift @@ -23,8 +23,7 @@ struct ActivityBadge: View { .frame(width: size * widthMultplier(), height: size, alignment: .topTrailing) .overlay { Capsule() - .stroke(Color.systemBackground, lineWidth: 2) - .clipped() + .stroke(Color.systemBackground, lineWidth: 1.5) } .position(x: x, y: y) From ef7a287d05a5283889af5456e79e7faa8d95d6e6 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 23 Oct 2024 20:22:42 -0600 Subject: [PATCH 7/8] Merge Fixes + Missing number. TODO: Figure out coloring that looks good/native? --- Swiftfin/Views/HomeView/HomeView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index def7de99f..2c9385964 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -94,6 +94,7 @@ struct HomeView: View { ActiveSessionIndicator { // TODO: "This will take you to the Admin Dashboard once it's on its own coordinator } + .foregroundStyle(Color.accentColor, .secondary) } SettingsBarButton( From 6b25793fa2a941a5b3e9d65289dcf1bbd6a74c3f Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 4 Nov 2024 21:20:30 -0700 Subject: [PATCH 8/8] Activity Indicator will now directly open the Admin Dashboard --- Shared/Coordinators/HomeCoordinator.swift | 6 ++++++ Shared/Strings/Strings.swift | 4 ++-- Swiftfin/Views/HomeView/HomeView.swift | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index d4fc91f48..7eab492c5 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -28,6 +28,8 @@ final class HomeCoordinator: NavigationCoordinatable { var item = makeItem @Route(.push) var library = makeLibrary + @Route(.push) + var adminDashboard = makeAdminDashboard #endif #if os(tvOS) @@ -46,6 +48,10 @@ final class HomeCoordinator: NavigationCoordinatable { func makeLibrary(viewModel: PagingLibraryViewModel) -> LibraryCoordinator { LibraryCoordinator(viewModel: viewModel) } + + func makeAdminDashboard() -> NavigationViewCoordinator { + NavigationViewCoordinator(AdminDashboardCoordinator()) + } #endif @ViewBuilder diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index dab86a317..8f3b1571e 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -22,10 +22,10 @@ internal enum L10n { internal static let active = L10n.tr("Localizable", "active", fallback: "Active") /// ActiveSessionsView Header internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices") - /// Activity Indicator naming / settings label - internal static let activityIndicator = L10n.tr("Localizable", "activityIndicator", fallback: "Activity Indicator") /// Activity internal static let activity = L10n.tr("Localizable", "activity", fallback: "Activity") + /// Activity Indicator + internal static let activityIndicator = L10n.tr("Localizable", "activityIndicator", fallback: "Activity Indicator") /// Add internal static let add = L10n.tr("Localizable", "add", fallback: "Add") /// Add API key diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index 2c9385964..19f1c01b7 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -92,7 +92,7 @@ struct HomeView: View { if activeSessionIndicator { ActiveSessionIndicator { - // TODO: "This will take you to the Admin Dashboard once it's on its own coordinator + router.route(to: \.adminDashboard) } .foregroundStyle(Color.accentColor, .secondary) }