10 Commits

Author SHA1 Message Date
Zhigang Fang
0ff62e7df1 Add 15 min 2017-11-28 23:53:53 +08:00
Zhigang Fang
c84b8956c0 Not stealing the focus 2017-11-28 23:51:30 +08:00
Zhigang Fang
95986feec0 Keep in break duration 2017-11-28 10:22:09 +08:00
Zhigang Fang
31715af50a Bump version 2017-11-28 01:38:12 +08:00
Zhigang Fang
eeedf2dc6a Fix sleep 2017-11-28 01:37:50 +08:00
Zhigang Fang
ec8f1c8b16 Disable reminder when app is in the background 2017-11-28 01:34:30 +08:00
Zhigang Fang
fbf68cb025 Update interval 2017-11-28 01:22:30 +08:00
Zhigang Fang
9684f2c8a7 Add reminder and auto apply 2017-11-28 01:21:29 +08:00
Zhigang Fang
060d9550d4 Fix enter key 2017-11-27 23:45:37 +08:00
Zhigang Fang
a449c6c635 Sort project by most recently used 2017-11-27 23:41:30 +08:00
6 changed files with 275 additions and 39 deletions

16
CHANGELOG Normal file
View File

@@ -0,0 +1,16 @@
Changelog
---------------
### 1.1.1
* Not stealing user focus when showing reminder
* Adding 15 min as reminder interval
### 1.1.0
* In recent entries, sort project by usage.
* When showing recent entries, enter key autocomplete instead of start timer.
* Add option to reminder you at certain interval.
* Add option to auto apply yes on interval timeout.
* Disable reminder when computer went to sleep.
* Keep break duration in timer if it's less than 10 min

View File

@@ -24,6 +24,7 @@
041E2BB41F9EF0370036687C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 041E2BB41F9EF0370036687C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
041E2BB51F9EF0370036687C /* FloatingToggl.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FloatingToggl.entitlements; sourceTree = "<group>"; }; 041E2BB51F9EF0370036687C /* FloatingToggl.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FloatingToggl.entitlements; sourceTree = "<group>"; };
041E2BBB1F9EF3450036687C /* FloatingPannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPannel.swift; sourceTree = "<group>"; }; 041E2BBB1F9EF3450036687C /* FloatingPannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPannel.swift; sourceTree = "<group>"; };
04D507151FCC84360038E7E0 /* CHANGELOG */ = {isa = PBXFileReference; lastKnownFileType = text; path = CHANGELOG; sourceTree = "<group>"; };
60799DCC63FBAD0197655FF4 /* Pods-FloatingToggl.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FloatingToggl.release.xcconfig"; path = "Pods/Target Support Files/Pods-FloatingToggl/Pods-FloatingToggl.release.xcconfig"; sourceTree = "<group>"; }; 60799DCC63FBAD0197655FF4 /* Pods-FloatingToggl.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FloatingToggl.release.xcconfig"; path = "Pods/Target Support Files/Pods-FloatingToggl/Pods-FloatingToggl.release.xcconfig"; sourceTree = "<group>"; };
6A61B7517328FB18AF3456FF /* Pods-FloatingToggl.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FloatingToggl.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FloatingToggl/Pods-FloatingToggl.debug.xcconfig"; sourceTree = "<group>"; }; 6A61B7517328FB18AF3456FF /* Pods-FloatingToggl.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FloatingToggl.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FloatingToggl/Pods-FloatingToggl.debug.xcconfig"; sourceTree = "<group>"; };
B3256CA209129AD71D5691BF /* Pods_FloatingToggl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FloatingToggl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B3256CA209129AD71D5691BF /* Pods_FloatingToggl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FloatingToggl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -44,6 +45,7 @@
041E2B9F1F9EF0370036687C = { 041E2B9F1F9EF0370036687C = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
04D507151FCC84360038E7E0 /* CHANGELOG */,
041E2BAA1F9EF0370036687C /* FloatingToggl */, 041E2BAA1F9EF0370036687C /* FloatingToggl */,
041E2BA91F9EF0370036687C /* Products */, 041E2BA91F9EF0370036687C /* Products */,
AB897BA953C7D862BCC1F2D0 /* Pods */, AB897BA953C7D862BCC1F2D0 /* Pods */,

View File

@@ -9,17 +9,98 @@
import Cocoa import Cocoa
import KeychainSwift import KeychainSwift
extension UserDefaults {
var reminderInterval: Int {
get { return UserDefaults.standard.integer(forKey: "com.floatingtoggl.reminder") }
set { UserDefaults.standard.set(newValue, forKey: "com.floatingtoggl.reminder") }
}
var shouldAutoApply: Bool {
get { return UserDefaults.standard.bool(forKey: "com.floatingtoggl.autoapply") }
set { UserDefaults.standard.set(newValue, forKey: "com.floatingtoggl.autoapply") }
}
}
extension Notification.Name {
static let reminderIntervalUpdated: Notification.Name = Notification.Name("com.floatingtoggl.reminderintervalupdated")
}
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
var reminderInterval: Int = 0 {
didSet {
fiveMinuteReminder.isChecked = reminderInterval == 5
fifteenMinuteReminder.isChecked = reminderInterval == 15
thirtyMinuteReminder.isChecked = reminderInterval == 30
noReminder.isChecked = reminderInterval == 0
if reminderInterval != oldValue {
UserDefaults.standard.reminderInterval = reminderInterval
NotificationCenter.default.post(name: .reminderIntervalUpdated, object: nil)
}
}
}
@IBOutlet weak var fiveMinuteReminder: NSMenuItem!
@IBOutlet weak var fifteenMinuteReminder: NSMenuItem!
@IBOutlet weak var thirtyMinuteReminder: NSMenuItem!
@IBOutlet weak var noReminder: NSMenuItem!
@IBAction func reminder5minTapped(_ sender: NSMenuItem) {
reminderInterval = 5
}
@IBAction func reminder15minTapped(_ sender: NSMenuItem) {
reminderInterval = 15
}
@IBAction func reminder30minTapped(_ sender: NSMenuItem) {
reminderInterval = 30
}
@IBAction func reminderNoneTapped(_ sender: NSMenuItem) {
reminderInterval = 0
}
var shouldAutoApply: Bool = false {
didSet {
autoApply.isChecked = shouldAutoApply
if shouldAutoApply != oldValue {
UserDefaults.standard.shouldAutoApply = shouldAutoApply
}
}
}
@IBOutlet weak var autoApply: NSMenuItem!
@IBAction func autoApplyTapped(_ sender: NSMenuItem) {
shouldAutoApply = !shouldAutoApply
}
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application // Insert code here to initialize your application
reminderInterval = UserDefaults.standard.reminderInterval
shouldAutoApply = UserDefaults.standard.shouldAutoApply
} }
func applicationWillTerminate(_ aNotification: Notification) { func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application // Insert code here to tear down your application
} }
} }
extension NSMenuItem {
var isChecked: Bool {
get { return state ~= .on }
set { state = newValue ? .on : .off }
}
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13196" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13196"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<capability name="stacking Non-gravity area distributions on NSStackView" minToolsVersion="7.0" minSystemVersion="10.11"/> <capability name="stacking Non-gravity area distributions on NSStackView" minToolsVersion="7.0" minSystemVersion="10.11"/>
</dependencies> </dependencies>
@@ -293,26 +293,40 @@
</items> </items>
</menu> </menu>
</menuItem> </menuItem>
<menuItem title="Window" id="aUF-d1-5bR"> <menuItem title="Reminder" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> <menu key="submenu" title="Reminder" systemMenu="window" id="Td7-aD-5lo">
<items> <items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> <menuItem title="5 min" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/> <action selector="reminder5minTapped:" target="Voe-Tx-rLC" id="Wyl-dO-evG"/>
</connections>
</menuItem>
<menuItem title="15 min" id="31U-4c-cuC">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="reminder15minTapped:" target="Voe-Tx-rLC" id="0KX-yg-Vht"/>
</connections>
</menuItem>
<menuItem title="30 min" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="reminder30minTapped:" target="Voe-Tx-rLC" id="vJT-TE-vap"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ"> <menuItem title="None" state="on" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/> <action selector="reminderNoneTapped:" target="Voe-Tx-rLC" id="wVc-dE-AmQ"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="3Tu-fH-Lk1"/>
<menuItem title="Auto Apply" id="ho0-J6-R08">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="autoApplyTapped:" target="Voe-Tx-rLC" id="Vax-TS-cKp"/>
</connections> </connections>
</menuItem> </menuItem>
</items> </items>
@@ -336,7 +350,15 @@
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/> <outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections> </connections>
</application> </application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Toggl_Bar" customModuleProvider="target"/> <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="In_Time" customModuleProvider="target">
<connections>
<outlet property="autoApply" destination="ho0-J6-R08" id="tBW-iT-pGt"/>
<outlet property="fifteenMinuteReminder" destination="31U-4c-cuC" id="AFh-yY-wvS"/>
<outlet property="fiveMinuteReminder" destination="OY7-WF-poV" id="tab-3t-lh3"/>
<outlet property="noReminder" destination="LE2-aR-0XJ" id="73T-RG-g3y"/>
<outlet property="thirtyMinuteReminder" destination="R4o-n2-Eq4" id="eOc-0v-Xgl"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/> <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
@@ -345,10 +367,10 @@
<!--Window Controller--> <!--Window Controller-->
<scene sceneID="R2V-B0-nI4"> <scene sceneID="R2V-B0-nI4">
<objects> <objects>
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" customClass="FloatingPannel" customModule="Toggl_Bar" customModuleProvider="target" sceneMemberID="viewController"> <windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" customClass="FloatingPannel" customModule="In_Time" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" animationBehavior="default" id="IQv-IB-iLA" customClass="NSPanel"> <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" animationBehavior="default" id="IQv-IB-iLA" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" resizable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES" fullSizeContentView="YES"/> <windowStyleMask key="styleMask" titled="YES" resizable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES" fullSizeContentView="YES"/>
<windowCollectionBehavior key="collectionBehavior" moveToActiveSpace="YES"/> <windowCollectionBehavior key="collectionBehavior" moveToActiveSpace="YES" fullScreenNone="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" bottomStrut="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="95" y="73" width="426" height="100"/> <rect key="contentRect" x="95" y="73" width="426" height="100"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/> <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
@@ -367,7 +389,7 @@
<!--View Controller--> <!--View Controller-->
<scene sceneID="hIz-AP-VOD"> <scene sceneID="hIz-AP-VOD">
<objects> <objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Toggl_Bar" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="In_Time" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl"> <view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="364" height="60"/> <rect key="frame" x="0.0" y="0.0" width="364" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
@@ -375,7 +397,7 @@
<stackView distribution="fill" orientation="horizontal" alignment="centerY" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wu7-dI-dhr"> <stackView distribution="fill" orientation="horizontal" alignment="centerY" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wu7-dI-dhr">
<rect key="frame" x="20" y="0.0" width="324" height="60"/> <rect key="frame" x="20" y="0.0" width="324" height="60"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="249" verticalHuggingPriority="750" horizontalCompressionResistancePriority="999" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BPy-6c-wmT" customClass="AutoGrowTextField" customModule="Toggl_Bar" customModuleProvider="target"> <textField horizontalHuggingPriority="249" verticalHuggingPriority="750" horizontalCompressionResistancePriority="999" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BPy-6c-wmT" customClass="AutoGrowTextField" customModule="In_Time" customModuleProvider="target">
<rect key="frame" x="-2" y="20.5" width="197" height="19"/> <rect key="frame" x="-2" y="20.5" width="197" height="19"/>
<constraints> <constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="Tug-Tb-Szc"/> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="Tug-Tb-Szc"/>

View File

@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>1.1.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>5</string> <string>6</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string> <string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -14,7 +14,7 @@ import KeychainSwift
struct TimeEntry: Decodable { struct TimeEntry: Decodable {
let id: Int64 let id: Int64
let start: String let start: Date
let description: String? let description: String?
} }
@@ -25,6 +25,7 @@ struct DataResponse<T: Decodable>: Decodable {
struct Project: Decodable { struct Project: Decodable {
let id: Int64 let id: Int64
let name: String let name: String
let at: Date
} }
struct User: Decodable { struct User: Decodable {
@@ -44,6 +45,36 @@ private extension URL {
} }
extension JSONEncoder {
static var toggle: JSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}
}
extension JSONDecoder {
static var toggle: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}
}
extension ISO8601DateFormatter {
static var toggle: ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.timeZone = .current
return formatter
}
}
struct Endpoint<T: Decodable> { struct Endpoint<T: Decodable> {
let method: String let method: String
let url: URL let url: URL
@@ -66,7 +97,7 @@ struct Endpoint<T: Decodable> {
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
} }
return URLSession.shared.rx.data(request: request).map({ data in return URLSession.shared.rx.data(request: request).map({ data in
(try JSONDecoder().decode(DataResponse<T>.self, from: data)).data (try JSONDecoder.toggle.decode(DataResponse<T>.self, from: data)).data
}) })
}) })
} }
@@ -98,6 +129,14 @@ struct Endpoint<T: Decodable> {
] as NSDictionary) ] as NSDictionary)
} }
static func update(timeEntry: Int64, duration: Int) -> Endpoint<TimeEntry> {
return Endpoint<TimeEntry>(method: "POST", url: URL.api("time_entries/\(timeEntry)"), body: [
"time_entry": [
"duration": duration
]
])
}
} }
@@ -114,6 +153,7 @@ class TogglViewModel {
let input = Variable<String>("") let input = Variable<String>("")
let active = Variable<Bool>(NSApplication.shared.isActive) let active = Variable<Bool>(NSApplication.shared.isActive)
let awake = Variable<Bool>(true)
private let disposeBag = DisposeBag() private let disposeBag = DisposeBag()
@@ -123,7 +163,7 @@ class TogglViewModel {
var completions: Driver<[String]> { var completions: Driver<[String]> {
return user.asDriver().map({ user -> [String] in return user.asDriver().map({ user -> [String] in
guard let user = user else { return [] } guard let user = user else { return [] }
let projects = user.projects?.map({"#\($0.name)"}) ?? [] let projects = user.projects?.sorted(by: {$0.at > $1.at}).map({"#\($0.name)"}) ?? []
let entries = user.time_entries?.sorted(by: {$0.start > $1.start}).flatMap({$0.description}) ?? [] let entries = user.time_entries?.sorted(by: {$0.start > $1.start}).flatMap({$0.description}) ?? []
return Array(NSOrderedSet(array: projects + entries)).flatMap({$0 as? String}) return Array(NSOrderedSet(array: projects + entries)).flatMap({$0 as? String})
}).flatMapLatest({[weak self] (completion:[String]) -> Driver<[String]> in }).flatMapLatest({[weak self] (completion:[String]) -> Driver<[String]> in
@@ -177,7 +217,7 @@ class TogglViewModel {
token.asDriver().flatMapLatest({[weak self] token -> Driver<User?> in token.asDriver().flatMapLatest({[weak self] token -> Driver<User?> in
if let token = token { if let token = token {
return self?.current.asDriver().flatMapLatest({ _ in return self?.current.asDriver().flatMapLatest({ _ in
Endpoint<User>.me.request(with: token).map(Optional.some).debug("Test").asDriver(onErrorJustReturn: nil) Endpoint<User>.me.request(with: token).map(Optional.some).asDriver(onErrorJustReturn: nil)
}) ?? .just(nil) }) ?? .just(nil)
} }
return Driver<User?>.just(nil) return Driver<User?>.just(nil)
@@ -186,31 +226,48 @@ class TogglViewModel {
NSWorkspace.shared.notificationCenter NSWorkspace.shared.notificationCenter
.rx.notification(NSWorkspace.screensDidSleepNotification) .rx.notification(NSWorkspace.screensDidSleepNotification)
.subscribe(onNext: {[weak self] _ in .subscribe(onNext: {[weak self] _ in
self?.awake.value = false
guard let entry = self?.current.value else { return } guard let entry = self?.current.value else { return }
self?.timeEntryWhenSlept = entry self?.timeEntryWhenSlept = entry
self?.screenSleptAt = Date() self?.screenSleptAt = Date()
self?.stopTimer()
}).disposed(by: self.disposeBag) }).disposed(by: self.disposeBag)
NSWorkspace.shared.notificationCenter NSWorkspace.shared.notificationCenter
.rx.notification(NSWorkspace.screensDidWakeNotification) .rx.notification(NSWorkspace.screensDidWakeNotification)
.subscribe(onNext: {[weak self] _ in .subscribe(onNext: {[weak self] _ in
self?.awake.value = true
defer {
self?.screenSleptAt = nil
self?.timeEntryWhenSlept = nil
}
guard guard
let date = self?.screenSleptAt, let date = self?.screenSleptAt,
let timer = self?.timeEntryWhenSlept, let timer = self?.timeEntryWhenSlept,
Date().timeIntervalSince(date) < 60 * 10 Date().timeIntervalSince(date) > 60 * 10
else { else {
self?.screenSleptAt = nil
self?.timeEntryWhenSlept = nil
return return
} }
self?.input.value = timer.description ?? "" self?.stopTimer(entry: timer, stoppedAt: date)
self?.startTimer()
}).disposed(by: self.disposeBag) }).disposed(by: self.disposeBag)
} }
var presentReminder: Observable<()> {
return Observable.merge([
self.awake.asObservable().distinctUntilChanged().map({_ in ()}),
NotificationCenter.default.rx.notification(.reminderIntervalUpdated).map({_ in ()}),
self.input.asObservable().distinctUntilChanged().map({_ in ()})
]).flatMapLatest({[weak self] _ -> Observable<()> in
if self?.awake.value != true { return .empty() }
let interval = UserDefaults.standard.reminderInterval
if interval == 0 { return .empty() }
return Observable<Int>.interval(RxTimeInterval(interval * 60), scheduler: MainScheduler.asyncInstance).map({ _ in ()})
})
}
func startTimer() { func startTimer() {
let inputValue = input.value let inputValue = input.value
if let projectName = inputValue.hashKey { if let projectName = inputValue.hashKey {
@@ -256,10 +313,17 @@ class TogglViewModel {
.disposed(by: self.disposeBag) .disposed(by: self.disposeBag)
} }
func stopTimer() { func stopTimer(entry: TimeEntry? = nil, stoppedAt: Date? = nil) {
guard let token = self.token.value else { return } guard let token = self.token.value else { return }
guard let entryId = self.current.value?.id else { return } guard let entry = entry ?? self.current.value else { return }
Endpoint<TimeEntry>.stop(timeEntry: entryId).request(with: token) Endpoint<TimeEntry>.stop(timeEntry: entry.id).request(with: token)
.flatMap({ entry -> Observable<TimeEntry> in
if let stopped = stoppedAt {
let duration = Int(stopped.timeIntervalSince(entry.start))
return Endpoint<TimeEntry>.update(timeEntry: entry.id, duration: duration).request(with: token)
}
return .just(entry)
})
.map({_ in nil}) .map({_ in nil})
.catchErrorJustReturn(nil) .catchErrorJustReturn(nil)
.bind(to: current) .bind(to: current)
@@ -361,11 +425,8 @@ class ViewController: NSViewController {
self.selectedRow = 0 self.selectedRow = 0
}).disposed(by: self.disposeBag) }).disposed(by: self.disposeBag)
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
viewModel.current.asDriver().flatMapLatest({ entry -> Driver<String> in viewModel.current.asDriver().flatMapLatest({ entry -> Driver<String> in
guard let start = entry?.start, let date = df.date(from: start) else { return .empty() } guard let date = entry?.start else { return .empty() }
return Driver<Int>.interval(1).startWith(1).map({ _ in return Driver<Int>.interval(1).startWith(1).map({ _ in
let time: Int = Int(Date().timeIntervalSince(date)) let time: Int = Int(Date().timeIntervalSince(date))
let hours = time / 3600 let hours = time / 3600
@@ -393,6 +454,10 @@ class ViewController: NSViewController {
self?.placeCursorAtTheEnd() self?.placeCursorAtTheEnd()
self?.resizeWindow() self?.resizeWindow()
}).disposed(by: self.disposeBag) }).disposed(by: self.disposeBag)
viewModel.presentReminder.subscribe(onNext: {[weak self] in
self?.presentReminder()
}).disposed(by: disposeBag)
} }
var trackingRect: NSView.TrackingRectTag? var trackingRect: NSView.TrackingRectTag?
@@ -470,6 +535,52 @@ class ViewController: NSViewController {
tokenField.becomeFirstResponder() tokenField.becomeFirstResponder()
} }
func presentDontForget() {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Don't forget to track your time"
alert.beginSheetModal(for: self.view.window!, completionHandler: nil)
alert.window.resignKey()
}
func presentReminder() {
guard let current = viewModel.current.value else {
presentDontForget()
return
}
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Are you still working on \n \(current.description ?? "Untitled")"
let button = alert.addButton(withTitle: "YES")
var disposable: Disposable?
if UserDefaults.standard.shouldAutoApply {
let autoApplyInterval = 60
disposable = Observable<Int>.interval(1, scheduler: MainScheduler.asyncInstance)
.map({autoApplyInterval - $0})
.subscribe(onNext: {[weak self] countdown in
if countdown > 0 {
button.title = "YES (\(countdown))"
} else if countdown == 0 {
self?.view.window?.endSheet(alert.window)
}
})
button.title = "YES (\(autoApplyInterval))"
}
alert.addButton(withTitle: "No")
alert.beginSheetModal(for: self.view.window!) { (response) in
disposable?.dispose()
if response == .alertSecondButtonReturn {
self.viewModel.stopTimer()
self.inputLabel.becomeFirstResponder()
}
}
alert.window.resignKey()
}
@IBAction func setToken(_ sender: NSMenuItem) { @IBAction func setToken(_ sender: NSMenuItem) {
presentSetToken() presentSetToken()
} }
@@ -526,7 +637,11 @@ extension ViewController: NSTextFieldDelegate {
case #selector(textView.insertTab(_:)): case #selector(textView.insertTab(_:)):
insertSelection() insertSelection()
case #selector(textView.insertNewline(_:)): case #selector(textView.insertNewline(_:)):
self.viewModel.startTimer() if isShowingRecentEntries {
insertSelection()
} else {
self.viewModel.startTimer()
}
default: default:
return false return false
} }