mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Remove the tracks details panel page
The quality selector sheet already covers quality, audio, subtitles and rate, so the embedded tracks page duplicated it. Drop the page type, its per-page settings, the editor toggles, and the visibleSections plumbing in QualitySelectorView.
This commit is contained in:
@@ -7241,17 +7241,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel.page.tracks" : {
|
||||
"comment" : "Details panel page name",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tracks"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel.region.belowVideo" : {
|
||||
"comment" : "Corner layout region option",
|
||||
"localizations" : {
|
||||
|
||||
@@ -17,7 +17,6 @@ enum PanelPageType: String, Codable, CaseIterable, Sendable {
|
||||
case related
|
||||
case chapters
|
||||
case queue
|
||||
case tracks
|
||||
|
||||
/// Localized display name shown in the page bar and settings.
|
||||
var displayName: String {
|
||||
@@ -32,8 +31,6 @@ enum PanelPageType: String, Codable, CaseIterable, Sendable {
|
||||
return String(localized: "player.chapters")
|
||||
case .queue:
|
||||
return String(localized: "panel.page.queue")
|
||||
case .tracks:
|
||||
return String(localized: "panel.page.tracks")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +63,6 @@ enum PanelPageType: String, Codable, CaseIterable, Sendable {
|
||||
return "list.bullet.rectangle"
|
||||
case .queue:
|
||||
return "list.bullet"
|
||||
case .tracks:
|
||||
return "slider.horizontal.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,19 +201,6 @@ struct ChaptersPageSettings: Codable, Hashable, Sendable {
|
||||
var effectiveShowThumbnails: Bool { showThumbnails ?? true }
|
||||
}
|
||||
|
||||
/// Element toggles for the tracks page.
|
||||
struct TracksPageSettings: Codable, Hashable, Sendable {
|
||||
var showQuality: Bool?
|
||||
var showAudio: Bool?
|
||||
var showSubtitles: Bool?
|
||||
var showRate: Bool?
|
||||
|
||||
var effectiveShowQuality: Bool { showQuality ?? true }
|
||||
var effectiveShowAudio: Bool { showAudio ?? true }
|
||||
var effectiveShowSubtitles: Bool { showSubtitles ?? true }
|
||||
var effectiveShowRate: Bool { showRate ?? true }
|
||||
}
|
||||
|
||||
/// Typed per-page settings payload. Uses the same discriminator pattern as
|
||||
/// `ButtonSettings`; an unknown discriminator from a future app version
|
||||
/// decodes to `nil` at the configuration level so the pages array never
|
||||
@@ -229,7 +211,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
|
||||
case related(RelatedPageSettings)
|
||||
case chapters(ChaptersPageSettings)
|
||||
case queue(QueuePageSettings)
|
||||
case tracks(TracksPageSettings)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
@@ -238,7 +219,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
|
||||
case related
|
||||
case chapters
|
||||
case queue
|
||||
case tracks
|
||||
}
|
||||
|
||||
private enum SettingsType: String, Codable {
|
||||
@@ -247,7 +227,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
|
||||
case related
|
||||
case chapters
|
||||
case queue
|
||||
case tracks
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@@ -265,8 +244,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
|
||||
self = .chapters(try container.decode(ChaptersPageSettings.self, forKey: .chapters))
|
||||
case .queue:
|
||||
self = .queue(try container.decode(QueuePageSettings.self, forKey: .queue))
|
||||
case .tracks:
|
||||
self = .tracks(try container.decode(TracksPageSettings.self, forKey: .tracks))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,9 +266,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
|
||||
case .queue(let settings):
|
||||
try container.encode(SettingsType.queue, forKey: .type)
|
||||
try container.encode(settings, forKey: .queue)
|
||||
case .tracks(let settings):
|
||||
try container.encode(SettingsType.tracks, forKey: .type)
|
||||
try container.encode(settings, forKey: .tracks)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,12 +323,6 @@ struct PanelPageConfiguration: Codable, Hashable, Sendable, Identifiable {
|
||||
return QueuePageSettings()
|
||||
}
|
||||
|
||||
/// Tracks page settings when set, defaults otherwise.
|
||||
var tracksSettings: TracksPageSettings {
|
||||
if case .tracks(let settings) = settings { return settings }
|
||||
return TracksPageSettings()
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case pageType
|
||||
case isEnabled
|
||||
@@ -415,7 +383,6 @@ struct PanelSettings: Codable, Hashable, Sendable {
|
||||
PanelPageConfiguration(pageType: .related, region: .primary),
|
||||
PanelPageConfiguration(pageType: .chapters, region: .primary),
|
||||
PanelPageConfiguration(pageType: .queue, region: .primary),
|
||||
PanelPageConfiguration(pageType: .tracks, isEnabled: false, region: .primary),
|
||||
])
|
||||
|
||||
var effectiveLayoutMode: PanelLayoutMode { layoutMode ?? .stacked }
|
||||
|
||||
@@ -891,15 +891,6 @@ final class PlayerControlsSettingsViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronously updates the tracks page element toggles.
|
||||
func updatePanelTracksSettingsSync(_ mutation: (inout TracksPageSettings) -> Void) {
|
||||
updatePanelSettingsSync { settings in
|
||||
var tracks = settings.configuration(for: .tracks)?.tracksSettings ?? TracksPageSettings()
|
||||
mutation(&tracks)
|
||||
settings.updateSettings(.tracks(tracks), for: .tracks)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the page bar shows labels next to icons.
|
||||
var panelShowPageBarLabels: Bool {
|
||||
panelSettings.effectiveShowPageBarLabels
|
||||
|
||||
@@ -63,8 +63,6 @@ struct PlayerPanelContentView: View {
|
||||
return !(playerState?.chapters ?? []).isEmpty
|
||||
case .queue:
|
||||
return appEnvironment?.settingsManager.queueEnabled ?? true
|
||||
case .tracks:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +176,6 @@ struct PlayerPanelContentView: View {
|
||||
case .queue:
|
||||
QueuePanelPage(context: context, settings: page.queueSettings, bottomInset: scrollConfig.bottomInset)
|
||||
.id(PanelPageType.queue)
|
||||
case .tracks:
|
||||
TracksPanelPage(context: context, settings: page.tracksSettings, bottomInset: scrollConfig.bottomInset)
|
||||
.id(PanelPageType.tracks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
//
|
||||
// TracksPanelPage.swift
|
||||
// Yattee
|
||||
//
|
||||
// Details panel page embedding the quality/audio/subtitles/rate selector.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
|
||||
struct TracksPanelPage: View {
|
||||
let context: PanelPageContext
|
||||
var settings: TracksPageSettings = TracksPageSettings()
|
||||
var bottomInset: CGFloat = 0
|
||||
|
||||
private var appEnvironment: AppEnvironment? { context.appEnvironment }
|
||||
private var playerService: PlayerService? { appEnvironment?.playerService }
|
||||
private var playerState: PlayerState? { playerService?.state }
|
||||
|
||||
var body: some View {
|
||||
if let playerService {
|
||||
let supportedFormats = playerService.currentBackendType.supportedFormats
|
||||
QualitySelectorView(
|
||||
streams: playerService.availableStreams.filter { stream in
|
||||
let format = StreamFormat.detect(from: stream)
|
||||
// DASH is never offered for manual selection
|
||||
if format == .dash {
|
||||
return false
|
||||
}
|
||||
return supportedFormats.contains(format)
|
||||
},
|
||||
captions: playerService.availableCaptions,
|
||||
currentStream: playerState?.currentStream,
|
||||
currentAudioStream: playerState?.currentAudioStream,
|
||||
currentCaption: playerService.currentCaption,
|
||||
isLoading: playerService.availableStreams.isEmpty && playerState?.playbackState == .loading,
|
||||
currentDownload: playerService.currentDownload,
|
||||
isLoadingOnlineStreams: playerService.isLoadingOnlineStreams,
|
||||
localCaptionURL: playerService.currentDownload.flatMap { download in
|
||||
guard let path = download.localCaptionPath else { return nil }
|
||||
return appEnvironment?.downloadManager.downloadsDirectory().appendingPathComponent(path)
|
||||
},
|
||||
currentRate: playerState?.rate ?? .x1,
|
||||
isControlsLocked: playerState?.isControlsLocked ?? false,
|
||||
isAudioMode: appEnvironment?.settingsManager.audioOnlyModeEnabled ?? false,
|
||||
embeddedAudioTracks: playerService.embeddedAudioTracks,
|
||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||
onStreamSelected: { stream, audioStream in
|
||||
Task {
|
||||
await playerService.selectStreamManually(stream, audioStream: audioStream)
|
||||
}
|
||||
},
|
||||
onCaptionSelected: { caption in
|
||||
playerService.loadCaption(caption)
|
||||
},
|
||||
onLoadOnlineStreams: {
|
||||
Task {
|
||||
await playerService.loadOnlineStreams()
|
||||
}
|
||||
},
|
||||
onSwitchToOnlineStream: { stream, audioStream in
|
||||
Task {
|
||||
await playerService.switchToOnlineStream(stream, audioStream: audioStream)
|
||||
}
|
||||
},
|
||||
onEmbeddedAudioTrackSelected: { trackID in
|
||||
playerService.selectEmbeddedAudioTrack(trackID)
|
||||
},
|
||||
onEmbeddedSubtitleTrackSelected: { trackID in
|
||||
playerService.selectEmbeddedSubtitleTrack(trackID)
|
||||
},
|
||||
onRateChanged: { rate in
|
||||
playerState?.rate = rate
|
||||
playerService.currentBackend?.rate = Float(rate.rawValue)
|
||||
},
|
||||
onLockToggled: { locked in
|
||||
playerState?.isControlsLocked = locked
|
||||
},
|
||||
onAudioModeToggled: { enabled in
|
||||
Task {
|
||||
await playerService.setAudioMode(enabled)
|
||||
}
|
||||
},
|
||||
isEmbedded: true,
|
||||
visibleSections: settings
|
||||
)
|
||||
.safeAreaPadding(.bottom, bottomInset)
|
||||
.panelListLeadingMargin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -346,19 +346,15 @@ extension QualitySelectorView {
|
||||
#if os(tvOS)
|
||||
// tvOS rows are styled individually to match the Settings rows.
|
||||
VStack(spacing: 8) {
|
||||
if visibleSections.effectiveShowRate {
|
||||
playbackSpeedRow
|
||||
}
|
||||
playbackSpeedRow
|
||||
|
||||
audioModeRow
|
||||
}
|
||||
#else
|
||||
VStack(spacing: 0) {
|
||||
if visibleSections.effectiveShowRate {
|
||||
playbackSpeedRow
|
||||
playbackSpeedRow
|
||||
|
||||
Divider()
|
||||
}
|
||||
Divider()
|
||||
|
||||
audioModeRow
|
||||
|
||||
|
||||
@@ -64,14 +64,10 @@ struct QualitySelectorView: View {
|
||||
/// the tvOS half-screen panel). When nil, falls back to `@Environment(\.dismiss)`.
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
/// Embedded inside a container (details panel tracks page): no close
|
||||
/// toolbar item, no sheet detents, and no minimum frame.
|
||||
/// Embedded inside a container: no close toolbar item, no sheet detents,
|
||||
/// and no minimum frame.
|
||||
var isEmbedded: Bool = false
|
||||
|
||||
/// Section visibility from the tracks page preset settings. Defaults show
|
||||
/// everything, so the standalone sheet is unaffected.
|
||||
var visibleSections: TracksPageSettings = TracksPageSettings()
|
||||
|
||||
#if os(tvOS)
|
||||
/// Bound to the first focusable row so we can programmatically pull focus
|
||||
/// into the panel on appear (the system doesn't auto-focus an inline
|
||||
@@ -119,18 +115,13 @@ struct QualitySelectorView: View {
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
/// Available tabs based on streams and the tracks page section settings
|
||||
/// Available tabs based on streams
|
||||
var availableTabs: [QualitySelectorTab] {
|
||||
var tabs: [QualitySelectorTab] = []
|
||||
if visibleSections.effectiveShowQuality {
|
||||
tabs.append(.video)
|
||||
}
|
||||
if visibleSections.effectiveShowAudio,
|
||||
(hasVideoOnlyStreams && !audioStreams.isEmpty) || embeddedAudioTracks.count > 1 {
|
||||
var tabs: [QualitySelectorTab] = [.video]
|
||||
if (hasVideoOnlyStreams && !audioStreams.isEmpty) || embeddedAudioTracks.count > 1 {
|
||||
tabs.append(.audio)
|
||||
}
|
||||
if visibleSections.effectiveShowSubtitles,
|
||||
!captions.isEmpty || !embeddedSubtitleTracks.isEmpty || canLoadExternalSubtitles {
|
||||
if !captions.isEmpty || !embeddedSubtitleTracks.isEmpty || canLoadExternalSubtitles {
|
||||
tabs.append(.subtitles)
|
||||
}
|
||||
// The selector never renders with zero tabs; fall back to video.
|
||||
@@ -222,8 +213,7 @@ struct QualitySelectorView: View {
|
||||
onLockToggled: ((Bool) -> Void)? = nil,
|
||||
onAudioModeToggled: ((Bool) -> Void)? = nil,
|
||||
onDismiss: (() -> Void)? = nil,
|
||||
isEmbedded: Bool = false,
|
||||
visibleSections: TracksPageSettings = TracksPageSettings()
|
||||
isEmbedded: Bool = false
|
||||
) {
|
||||
self.streams = streams
|
||||
self.captions = captions
|
||||
@@ -255,7 +245,6 @@ struct QualitySelectorView: View {
|
||||
self.onAudioModeToggled = onAudioModeToggled
|
||||
self.onDismiss = onDismiss
|
||||
self.isEmbedded = isEmbedded
|
||||
self.visibleSections = visibleSections
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
@@ -56,9 +56,6 @@ struct TVDetailsPanel: View {
|
||||
return !(playerState?.chapters ?? []).isEmpty
|
||||
case .queue:
|
||||
return appEnvironment?.settingsManager.queueEnabled ?? true
|
||||
case .tracks:
|
||||
// The tvOS quality panel covers tracks; never offered here.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +87,7 @@ struct TVDetailsPanel: View {
|
||||
// Content for the selected page
|
||||
Group {
|
||||
switch effectivePage {
|
||||
case .info, .tracks:
|
||||
case .info:
|
||||
infoContent
|
||||
case .comments:
|
||||
commentsContent
|
||||
|
||||
@@ -24,10 +24,6 @@ struct PanelPageEditorView: View {
|
||||
@State private var showRelatedThumbnails: Bool = true
|
||||
@State private var showChapterThumbnails: Bool = true
|
||||
@State private var showHistory: Bool = true
|
||||
@State private var showQuality: Bool = true
|
||||
@State private var showAudio: Bool = true
|
||||
@State private var showSubtitles: Bool = true
|
||||
@State private var showRate: Bool = true
|
||||
|
||||
private var configuration: PanelPageConfiguration? {
|
||||
viewModel.panelSettings.configuration(for: pageType)
|
||||
@@ -57,8 +53,6 @@ struct PanelPageEditorView: View {
|
||||
chaptersElementsSection
|
||||
case .queue:
|
||||
queueElementsSection
|
||||
case .tracks:
|
||||
tracksElementsSection
|
||||
}
|
||||
}
|
||||
.navigationTitle(pageType.displayName)
|
||||
@@ -243,51 +237,6 @@ struct PanelPageEditorView: View {
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Tracks Elements Section
|
||||
|
||||
private var tracksElementsSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "player.quality.video"),
|
||||
isOn: $showQuality
|
||||
)
|
||||
.onChange(of: showQuality) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowQuality else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showQuality = newValue }
|
||||
}
|
||||
|
||||
Toggle(
|
||||
String(localized: "stream.audio"),
|
||||
isOn: $showAudio
|
||||
)
|
||||
.onChange(of: showAudio) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowAudio else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showAudio = newValue }
|
||||
}
|
||||
|
||||
Toggle(
|
||||
String(localized: "stream.subtitles"),
|
||||
isOn: $showSubtitles
|
||||
)
|
||||
.onChange(of: showSubtitles) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowSubtitles else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showSubtitles = newValue }
|
||||
}
|
||||
|
||||
Toggle(
|
||||
String(localized: "controls.button.speed"),
|
||||
isOn: $showRate
|
||||
)
|
||||
.onChange(of: showRate) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowRate else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showRate = newValue }
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "panel.settings.elements.header"))
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var infoSettings: InfoPageSettings {
|
||||
@@ -310,10 +259,6 @@ struct PanelPageEditorView: View {
|
||||
configuration?.queueSettings ?? QueuePageSettings()
|
||||
}
|
||||
|
||||
private var tracksSettings: TracksPageSettings {
|
||||
configuration?.tracksSettings ?? TracksPageSettings()
|
||||
}
|
||||
|
||||
private func syncLocalState() {
|
||||
isEnabled = configuration?.isEnabled ?? true
|
||||
region = configuration?.region ?? .primary
|
||||
@@ -326,11 +271,6 @@ struct PanelPageEditorView: View {
|
||||
showRelatedThumbnails = relatedSettings.effectiveShowThumbnails
|
||||
showChapterThumbnails = chaptersSettings.effectiveShowThumbnails
|
||||
showHistory = queueSettings.effectiveShowHistory
|
||||
let tracks = tracksSettings
|
||||
showQuality = tracks.effectiveShowQuality
|
||||
showAudio = tracks.effectiveShowAudio
|
||||
showSubtitles = tracks.effectiveShowSubtitles
|
||||
showRate = tracks.effectiveShowRate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ struct TVPlayerPanelsSettingsView: View {
|
||||
private struct TVPlayerPanelsContent: View {
|
||||
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
||||
|
||||
/// Pages offered on tvOS; tracks is covered by the quality panel.
|
||||
/// Pages offered on tvOS.
|
||||
private var tvOSPages: [PanelPageConfiguration] {
|
||||
viewModel.panelPages.filter { $0.pageType != .tracks }
|
||||
viewModel.panelPages
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -143,9 +143,9 @@ private struct TVPanelPageEditorView: View {
|
||||
isEnabled && viewModel.enabledPanelPageCount <= 1
|
||||
}
|
||||
|
||||
/// Pages as displayed in the tvOS list (tracks hidden).
|
||||
/// Pages as displayed in the tvOS list.
|
||||
private var displayedPages: [PanelPageConfiguration] {
|
||||
viewModel.panelPages.filter { $0.pageType != .tracks }
|
||||
viewModel.panelPages
|
||||
}
|
||||
|
||||
private var displayedIndex: Int? {
|
||||
|
||||
@@ -57,10 +57,6 @@ struct PanelSettingsTests {
|
||||
settings.updateSettings(.related(RelatedPageSettings(showThumbnails: false)), for: .related)
|
||||
settings.updateSettings(.chapters(ChaptersPageSettings(showThumbnails: false)), for: .chapters)
|
||||
settings.updateSettings(.queue(QueuePageSettings(showHistory: false)), for: .queue)
|
||||
settings.updateSettings(
|
||||
.tracks(TracksPageSettings(showQuality: false, showSubtitles: false)),
|
||||
for: .tracks
|
||||
)
|
||||
|
||||
let decoded = try roundTrip(settings)
|
||||
#expect(decoded == settings)
|
||||
@@ -68,13 +64,6 @@ struct PanelSettingsTests {
|
||||
#expect(decoded.configuration(for: .related)?.relatedSettings.effectiveShowThumbnails == false)
|
||||
#expect(decoded.configuration(for: .chapters)?.chaptersSettings.effectiveShowThumbnails == false)
|
||||
#expect(decoded.configuration(for: .queue)?.queueSettings.effectiveShowHistory == false)
|
||||
|
||||
let tracks = try #require(decoded.configuration(for: .tracks)?.tracksSettings)
|
||||
#expect(tracks.effectiveShowQuality == false)
|
||||
#expect(tracks.effectiveShowSubtitles == false)
|
||||
// Unset fields keep their effective defaults
|
||||
#expect(tracks.effectiveShowAudio == true)
|
||||
#expect(tracks.effectiveShowRate == true)
|
||||
}
|
||||
|
||||
@Test("Typed accessors fall back to defaults for mismatched payloads")
|
||||
@@ -86,7 +75,6 @@ struct PanelSettingsTests {
|
||||
)
|
||||
#expect(page.commentsSettings.effectiveShowAvatars == true)
|
||||
#expect(page.queueSettings.effectiveShowHistory == true)
|
||||
#expect(page.tracksSettings.effectiveShowQuality == true)
|
||||
}
|
||||
|
||||
// MARK: - Backward Compatibility
|
||||
@@ -185,8 +173,6 @@ struct PanelSettingsTests {
|
||||
#expect(primary == [.related, .chapters, .queue])
|
||||
#expect(belowVideo == [.info, .comments])
|
||||
#expect(all == [.info, .comments, .related, .chapters, .queue])
|
||||
// Tracks is disabled by default
|
||||
#expect(!all.contains(.tracks))
|
||||
}
|
||||
|
||||
// MARK: - Mutations
|
||||
|
||||
Reference in New Issue
Block a user