Skip to content

Commit

Permalink
Support time in custom date format (pakerwreah#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
pakerwreah authored Aug 1, 2023
1 parent 97b8a36 commit adbc1ac
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 46 deletions.
3 changes: 2 additions & 1 deletion Calendr/Main/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ class MainViewController: NSViewController, NSPopoverDelegate {
dateProvider: dateProvider,
screenProvider: screenProvider,
calendarService: calendarService,
notificationCenter: notificationCenter
notificationCenter: notificationCenter,
scheduler: MainScheduler.instance
)

settingsViewController = SettingsViewController(
Expand Down
111 changes: 68 additions & 43 deletions Calendr/MenuBar/StatusItemViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,70 @@ class StatusItemViewModel {
dateProvider: DateProviding,
screenProvider: ScreenProviding,
calendarService: CalendarServiceProviding,
notificationCenter: NotificationCenter
notificationCenter: NotificationCenter,
scheduler: SchedulerType
) {

let dateObservable = dateChanged.map { dateProvider.now }

let hasBirthdaysObservable = Observable.combineLatest(
dateObservable,
nextEventCalendars
)
.repeat(when: calendarService.changeObservable)
.flatMapLatest { date, calendars in
let start = dateProvider.calendar.startOfDay(for: date)
let end = dateProvider.calendar.endOfDay(for: date)
return calendarService
.events(from: start, to: end, calendars: calendars)
.map { $0.contains(where: \.type.isBirthday) }
}
let hasBirthdaysObservable = nextEventCalendars
.repeat(when: dateChanged)
.repeat(when: calendarService.changeObservable)
.flatMapLatest { calendars in
let date = dateProvider.now
let start = dateProvider.calendar.startOfDay(for: date)
let end = dateProvider.calendar.endOfDay(for: date)
return calendarService
.events(from: start, to: end, calendars: calendars)
.map { $0.contains(where: \.type.isBirthday) }
}

let localeChangeObservable = notificationCenter.rx
.notification(NSLocale.currentLocaleDidChangeNotification)
.void()

let dateFormatterObservable = Observable
.combineLatest(settings.statusItemDateStyle, settings.statusItemDateFormat)
let dateTextObservable = Observable
.combineLatest(
settings.showStatusItemDate,
settings.statusItemDateStyle,
settings.statusItemDateFormat
)
.repeat(when: localeChangeObservable)
.map { style, format in
.flatMapLatest { showDate, style, format -> Observable<String> in

guard showDate else { return .just("") }

let formatter = DateFormatter(calendar: dateProvider.calendar)

let ticker: Observable<Void>

if style.isCustom {
formatter.dateFormat = format
if dateFormatContainsTime(format) {
ticker = Observable<Int>.interval(.seconds(1), scheduler: scheduler).void()
} else {
ticker = dateChanged
}
} else {
formatter.dateStyle = style
ticker = dateChanged
}

return formatter
return ticker.startWith(()).map {
let text = formatter.string(from: dateProvider.now)
return text.isEmpty ? "???" : text
}
}
.distinctUntilChanged()
.share(replay: 1)

self.iconsAndText = Observable.combineLatest(
dateObservable,
dateTextObservable,
settings.showStatusItemIcon,
settings.showStatusItemDate,
settings.statusItemIconStyle,
dateFormatterObservable,
hasBirthdaysObservable
)
.map { date, showIcon, showDate, iconStyle, dateFormatter, hasBirthdays in
.map { title, showIcon, iconStyle, hasBirthdays in

let showDate = !title.isEmpty

var icons: [NSImage] = []

Expand All @@ -87,53 +104,57 @@ class StatusItemViewModel {
icons.append(StatusItemIconFactory.icon(size: iconSize, style: iconStyle, dateProvider: dateProvider))
}

let title: String

if showDate {
let text = dateFormatter.string(from: date)
title = text.isEmpty ? "???" : text
} else {
title = ""
}

return (icons, title)
}
.share(replay: 1)

var titleWidth: CGFloat = 0
var currDateFormat = ""

self.image = Observable.combineLatest(
iconsAndText,
settings.showStatusItemDate,
settings.showStatusItemBackground
settings.showStatusItemBackground,
settings.statusItemDateFormat
)
.map { iconsAndText, showDate, showBackground in
.debounce(.nanoseconds(1), scheduler: scheduler)
.map { iconsAndText, showBackground, dateFormat in

let (icons, text) = iconsAndText

let title = NSAttributedString(string: text, attributes: [
let title = text.isEmpty ? nil : NSAttributedString(string: text, attributes: [
.font: NSFont.systemFont(ofSize: 12.5, weight: showBackground ? .regular : .medium)
])

if let title {
if currDateFormat == dateFormat && dateFormatContainsTime(dateFormat) {
titleWidth = max(titleWidth, title.size().width)
} else {
titleWidth = title.size().width
}
currDateFormat = dateFormat
} else {
titleWidth = 0
currDateFormat = ""
}

let radius: CGFloat = 3
let border: CGFloat = 0.5
let padding: NSPoint = showDate ? .init(x: 4, y: 1.5) : .init(x: border, y: border)
let textSize = title.length > 0 ? title.size() : .zero
let padding: NSPoint = text.isEmpty ? .init(x: border, y: border) : .init(x: 4, y: 1.5)
let spacing: CGFloat = 4
var iconsWidth = icons.map(\.size.width).reduce(0) { $0 + $1 + spacing }
let height = max(icons.map(\.size.height).reduce(0, max), 15)
if title.length == 0 {
if text.isEmpty {
iconsWidth -= spacing
}
var size = CGSize(width: iconsWidth + textSize.width, height: height)
var size = CGSize(width: iconsWidth + titleWidth, height: height)

let textImage = NSImage(size: size, flipped: false) {
var offsetX: CGFloat = 0
for icon in icons {
icon.draw(at: .init(x: offsetX, y: 0), from: $0, operation: .sourceOver, fraction: 1)
offsetX += icon.size.width + spacing
}
if title.length > 0 {
title.draw(at: .init(x: offsetX, y: 0))
}
title?.draw(at: .init(x: offsetX, y: 0))
return true
}

Expand All @@ -159,3 +180,7 @@ class StatusItemViewModel {
}
}
}

private func dateFormatContainsTime(_ format: String) -> Bool {
["H", "h", "m", "s"].contains(where: { format.contains($0) })
}
3 changes: 2 additions & 1 deletion Calendr/Previews/StatusItemPreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ struct StatusItemPreview: PreviewProvider {
dateProvider: dateProvider,
screenProvider: screenProvider,
calendarService: calendarService,
notificationCenter: notificationCenter
notificationCenter: notificationCenter,
scheduler: MainScheduler.instance
)

var image: NSImage!
Expand Down
20 changes: 19 additions & 1 deletion CalendrTests/StatusItemViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class StatusItemViewModelTests: XCTestCase {
let screenProvider = MockScreenProvider()
let calendarService = MockCalendarServiceProvider()
let settings = MockStatusItemSettings()
let scheduler = HistoricalScheduler()

let notificationCenter = NotificationCenter()

Expand All @@ -30,7 +31,8 @@ class StatusItemViewModelTests: XCTestCase {
dateProvider: dateProvider,
screenProvider: screenProvider,
calendarService: calendarService,
notificationCenter: notificationCenter
notificationCenter: notificationCenter,
scheduler: scheduler
)

var iconsAndText: ([NSImage], String)?
Expand Down Expand Up @@ -179,6 +181,19 @@ class StatusItemViewModelTests: XCTestCase {
XCTAssertEqual(lastText, "1/1/21")
}

func testDateFormatWithTime() {

setUp(showIcon: false, showDate: true, iconStyle: .calendar)

settings.statusItemDateStyleObserver.onNext(.none)
settings.statusItemDateFormatObserver.onNext("HH:mm:ss")
XCTAssertEqual(lastText, "00:00:00")

dateProvider.add(1, .second)
scheduler.advance(.seconds(1))
XCTAssertEqual(lastText, "00:00:01")
}

func testBackground() {

var image: NSImage?
Expand All @@ -187,14 +202,17 @@ class StatusItemViewModelTests: XCTestCase {
.bind { image = $0 }
.disposed(by: disposeBag)

scheduler.advance(.nanoseconds(1))
XCTAssertNotNil(image)

image = nil
settings.toggleBackground.onNext(true)
scheduler.advance(.nanoseconds(1))
XCTAssertNotNil(image)

image = nil
settings.toggleBackground.onNext(false)
scheduler.advance(.nanoseconds(1))
XCTAssertNotNil(image)
}
}

0 comments on commit adbc1ac

Please sign in to comment.