Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sleep Timer: add the option to select the number of episodes #1640

Merged
merged 13 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- New design for the Podcasts grid layouts. [#1628]
- Shake the device to restart Sleep Timer [#1627]
- Playback fades out when finishing a sleep timer [#1629]
- Select number of episodes for sleep timer [#1640]
- New design for the mini-player bar [#1634]

7.63
Expand Down
4 changes: 4 additions & 0 deletions podcasts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
8B484EFF28D257E2001AFA97 /* DataManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B484EFE28D257E2001AFA97 /* DataManagerMock.swift */; };
8B484F0228D25DB5001AFA97 /* UIViewController+requestReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B484F0028D25D1E001AFA97 /* UIViewController+requestReview.swift */; };
8B4881E929A3AE6D00E7C016 /* SearchHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4881E829A3AE6D00E7C016 /* SearchHistoryView.swift */; };
8B4C6FE52BD6EC2E00D0BB9D /* CustomStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4C6FE42BD6EC2E00D0BB9D /* CustomStepper.swift */; };
8B5AB488290188F30018C637 /* IntroStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5AB487290188F30018C637 /* IntroStory.swift */; };
8B5AB48A29018A950018C637 /* EpilogueStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5AB48929018A950018C637 /* EpilogueStory.swift */; };
8B5AB48C29018EFA0018C637 /* StoriesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5AB48B29018EFA0018C637 /* StoriesConfiguration.swift */; };
Expand Down Expand Up @@ -2310,6 +2311,7 @@
8B484EFE28D257E2001AFA97 /* DataManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManagerMock.swift; sourceTree = "<group>"; };
8B484F0028D25D1E001AFA97 /* UIViewController+requestReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+requestReview.swift"; sourceTree = "<group>"; };
8B4881E829A3AE6D00E7C016 /* SearchHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryView.swift; sourceTree = "<group>"; };
8B4C6FE42BD6EC2E00D0BB9D /* CustomStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStepper.swift; sourceTree = "<group>"; };
8B5AB487290188F30018C637 /* IntroStory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroStory.swift; sourceTree = "<group>"; };
8B5AB48929018A950018C637 /* EpilogueStory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpilogueStory.swift; sourceTree = "<group>"; };
8B5AB48B29018EFA0018C637 /* StoriesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoriesConfiguration.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4901,6 +4903,7 @@
BD2D0AD2243C4138000B313A /* MainEpisodeActionView+Pointer.swift */,
BDEDCF7024627F02005910C1 /* CustomSegmentedControl.swift */,
BDCCBC8724BC444F009B4D1D /* CustomTimeStepper.swift */,
8B4C6FE42BD6EC2E00D0BB9D /* CustomStepper.swift */,
40AF75A224761E920000AD9D /* PodcastHeartView.swift */,
);
name = "Common Components";
Expand Down Expand Up @@ -9216,6 +9219,7 @@
408529A3247C9A53007FE8AA /* PodcastSupporterCell.swift in Sources */,
8BD256D22A5C7090006648BE /* SharingHelper+swipeButton.swift in Sources */,
BDCD1E82244D636D00B83602 /* LenticularFilter.swift in Sources */,
8B4C6FE52BD6EC2E00D0BB9D /* CustomStepper.swift in Sources */,
8B9DEFD829071AD200075EAB /* MDCSwiftUIWrapper.swift in Sources */,
8B5FB8212B88FF52007576F0 /* DeselectChaptersAnnouncementViewModel.swift in Sources */,
4006E31423AC453700174DEB /* ExpandedCollectionViewController+CollectionView.swift in Sources */,
Expand Down
188 changes: 188 additions & 0 deletions podcasts/CustomStepper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import UIKit

class CustomStepper: UIControl {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not proud of copying/pasting CustomTimeStepper here. I tried playing with generics but forgot that Storyboard only plays nice with Obj-C. 🤡

private static let buttonWidth: CGFloat = 44
private static let buttonHeight: CGFloat = 44

private static let initialHoldTime: TimeInterval = 1
private static let holdRepetition: TimeInterval = 0.15

let minusButton = UIButton(type: .custom)
let plusButton = UIButton(type: .custom)

override var tintColor: UIColor! {
didSet {
minusButton.tintColor = tintColor
plusButton.tintColor = tintColor
}
}

var minimumValue: Int = 0 {
didSet {
if currentValue < minimumValue {
currentValue = minimumValue
}
}
}

var maximumValue: Int = 200 {
didSet {
if currentValue > maximumValue {
currentValue = maximumValue
}
}
}

var bigIncrements: Int = 1
var smallIncrements: Int = 1
var smallIncrementThreshold: Int = 1
var currentValue: Int = 1 {
didSet {
accessibilityValue = "\(currentValue)"
}
}
Comment on lines +36 to +43
Copy link
Contributor

@SergioEstevao SergioEstevao Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional Suggestion: Instead of generics why not abstract this to a protocol and have two implementations one for Int and another for TimeInterval that you can set on init?


override init(frame: CGRect) {
super.init(frame: frame)

setup()
}

required init?(coder: NSCoder) {
super.init(coder: coder)

setup()
}

private func setup() {
isAccessibilityElement = true
accessibilityTraits = [.adjustable]

minusButton.setImage(UIImage(named: "player_effects_less"), for: .normal)
minusButton.addTarget(self, action: #selector(lessTouchUp), for: .touchUpInside)
minusButton.addTarget(self, action: #selector(touchCancelled), for: .touchCancel)
minusButton.addTarget(self, action: #selector(touchCancelled), for: .touchUpOutside)
minusButton.addTarget(self, action: #selector(lessTouchDown), for: .touchDown)
addSubview(minusButton)

minusButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
minusButton.leadingAnchor.constraint(equalTo: leadingAnchor),
minusButton.centerYAnchor.constraint(equalTo: centerYAnchor),
minusButton.widthAnchor.constraint(equalToConstant: Self.buttonWidth),
minusButton.heightAnchor.constraint(equalToConstant: Self.buttonHeight)
])

plusButton.setImage(UIImage(named: "player_effects_more"), for: .normal)
plusButton.addTarget(self, action: #selector(moreTouchUp), for: .touchUpInside)
plusButton.addTarget(self, action: #selector(touchCancelled), for: .touchCancel)
plusButton.addTarget(self, action: #selector(touchCancelled), for: .touchUpOutside)
plusButton.addTarget(self, action: #selector(moreTouchDown), for: .touchDown)
addSubview(plusButton)

plusButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
plusButton.trailingAnchor.constraint(equalTo: trailingAnchor),
plusButton.centerYAnchor.constraint(equalTo: centerYAnchor),
plusButton.widthAnchor.constraint(equalToConstant: Self.buttonWidth),
plusButton.heightAnchor.constraint(equalToConstant: Self.buttonHeight)
])
}

// MARK: - Hold Support

private var holdTimer: Timer?
@objc private func lessTouchDown() {
holdTimer?.invalidate()

holdTimer = Timer.scheduledTimer(withTimeInterval: Self.initialHoldTime, repeats: false, block: { [weak self] _ in
if self?.currentValue == self?.minimumValue { return }

self?.holdTimer = Timer.scheduledTimer(withTimeInterval: Self.holdRepetition, repeats: true, block: { _ in
self?.performHoldLessDown()
})
})
}

private func performHoldLessDown() {
if currentValue == minimumValue {
holdTimer?.invalidate()

return
}

currentValue = max(minimumValue, currentValue - negativeIncrement())
sendActions(for: .valueChanged)
}

@objc private func moreTouchDown() {
holdTimer?.invalidate()

holdTimer = Timer.scheduledTimer(withTimeInterval: Self.initialHoldTime, repeats: false, block: { [weak self] _ in
if self?.currentValue == self?.maximumValue { return }

self?.holdTimer = Timer.scheduledTimer(withTimeInterval: Self.holdRepetition, repeats: true, block: { _ in
self?.performHoldMoreDown()
})
})
}

private func performHoldMoreDown() {
if currentValue == maximumValue {
holdTimer?.invalidate()

return
}

currentValue = min(maximumValue, currentValue + positiveIncrement())
sendActions(for: .valueChanged)
}

@objc private func touchCancelled() {
holdTimer?.invalidate()
}

// MARK: - Button Taps

@objc private func lessTouchUp() {
performDecrementAction()
}

private func performDecrementAction() {
holdTimer?.invalidate()
if currentValue == minimumValue { return }

currentValue = max(minimumValue, currentValue - negativeIncrement())
sendActions(for: .valueChanged)
}

@objc private func moreTouchUp() {
performIncrementAction()
}

private func performIncrementAction() {
holdTimer?.invalidate()
if currentValue == maximumValue { return }

currentValue = min(maximumValue, currentValue + positiveIncrement())
sendActions(for: .valueChanged)
}

private func positiveIncrement() -> Int {
currentValue < smallIncrementThreshold ? smallIncrements : bigIncrements
}

private func negativeIncrement() -> Int {
currentValue <= smallIncrementThreshold ? smallIncrements : bigIncrements
}

// MARK: - Accessibility actions

override func accessibilityIncrement() {
performIncrementAction()
}

override func accessibilityDecrement() {
performDecrementAction()
}
}
17 changes: 9 additions & 8 deletions podcasts/PlaybackManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ class PlaybackManager: ServerPlaybackDelegate {
private let chapterManager = ChapterManager()

var sleepTimeRemaining = -1 as TimeInterval
var sleepOnEpisodeEnd = false {

var numberOfEpisodesToSleepAfter = 0 {
didSet {
if sleepOnEpisodeEnd {
if numberOfEpisodesToSleepAfter > 0 {
sleepTimeRemaining = -1
sleepTimerManager.recordSleepTimerDuration(duration: nil, onEpisodeEnd: true)
}
Expand Down Expand Up @@ -589,7 +590,7 @@ class PlaybackManager: ServerPlaybackDelegate {
NotificationCenter.postOnMainThread(notification: Constants.Notifications.upNextQueueChanged)
}

sleepOnEpisodeEnd = false
numberOfEpisodesToSleepAfter -= 1
NotificationCenter.postOnMainThread(notification: Constants.Notifications.playbackTrackChanged)
}

Expand Down Expand Up @@ -949,7 +950,7 @@ class PlaybackManager: ServerPlaybackDelegate {
}

func playerDidFinishPlayingEpisode() {
if sleepOnEpisodeEnd {
if numberOfEpisodesToSleepAfter == 1 {
pauseAndRecordSleepTimerFinished()
cancelSleepTimer()
return
Expand Down Expand Up @@ -1022,7 +1023,7 @@ class PlaybackManager: ServerPlaybackDelegate {

cancelSleepTimer()
} else {
playNextEpisode(autoPlay: !sleepOnEpisodeEnd)
playNextEpisode(autoPlay: !(numberOfEpisodesToSleepAfter == 1))
}
}

Expand Down Expand Up @@ -1340,7 +1341,7 @@ class PlaybackManager: ServerPlaybackDelegate {
let episodeDuration = duration()
let timeRemaining = episodeDuration - currentTime()
if episodeDuration > 0, episodeDuration > skipLast, timeRemaining < skipLast {
if sleepOnEpisodeEnd {
if numberOfEpisodesToSleepAfter == 1 {
pause()
cancelSleepTimer()
} else {
Expand Down Expand Up @@ -1479,12 +1480,12 @@ class PlaybackManager: ServerPlaybackDelegate {
func cancelSleepTimer(userInitiated: Bool = false) {
sleepTimerManager.cancelSleepTimer(userInitiated: userInitiated)
sleepTimeRemaining = -1
sleepOnEpisodeEnd = false
numberOfEpisodesToSleepAfter = 0
NotificationCenter.postOnMainThread(notification: Constants.Notifications.sleepTimerChanged)
}

func sleepTimerActive() -> Bool {
sleepTimeRemaining >= 0 || sleepOnEpisodeEnd
sleepTimeRemaining >= 0 || numberOfEpisodesToSleepAfter > 0
}

func setSleepTimerInterval(_ stopIn: TimeInterval) {
Expand Down
9 changes: 9 additions & 0 deletions podcasts/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,15 @@ class Settings: NSObject {
UserDefaults.standard.set(adjustedTime, forKey: "CustomSleepTime")
}

static var sleepTimerNumberOfEpisodes: Int {
get {
UserDefaults.standard.object(forKey: "sleep_timer_custom_number_of_episodes") as? Int ?? 1
}
set {
UserDefaults.standard.set(newValue, forKey: "sleep_timer_custom_number_of_episodes")
}
}

// MARK: - CarPlay/Lock Screen actions

static let mediaSessionActionsKey = "MediaSessionActions"
Expand Down
11 changes: 6 additions & 5 deletions podcasts/SleepTimerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@ class SleepTimerManager {
}

private func observePlaybackEndAndReactivateTime() {
NotificationCenter.default.addObserver(self, selector: #selector(playbackTrackChanged), name: Constants.Notifications.playbackTrackChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(episodeDurationChanged), name: Constants.Notifications.episodeDurationChanged, object: nil)
}

@objc private func playbackTrackChanged() {
@objc private func episodeDurationChanged() {
let numberOfEpisodes = Settings.sleepTimerNumberOfEpisodes
FileLog.shared.addMessage("Sleep Timer: restarting it automatically to the end of the episode")
Analytics.shared.track(.playerSleepTimerRestarted, properties: ["time": "end_of_episode"])
PlaybackManager.shared.sleepOnEpisodeEnd = true
NotificationCenter.default.removeObserver(self, name: Constants.Notifications.playbackTrackChanged, object: nil)
Analytics.shared.track(.playerSleepTimerRestarted, properties: ["time": "end_of_episode", "number_of_episodes": numberOfEpisodes])
PlaybackManager.shared.numberOfEpisodesToSleepAfter = numberOfEpisodes
NotificationCenter.default.removeObserver(self, name: Constants.Notifications.episodeDurationChanged, object: nil)
}

func playTone() {
Expand Down
Loading