Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ff62e7df1 | ||
|
|
c84b8956c0 | ||
|
|
95986feec0 | ||
|
|
31715af50a | ||
|
|
eeedf2dc6a | ||
|
|
ec8f1c8b16 | ||
|
|
fbf68cb025 | ||
|
|
9684f2c8a7 | ||
|
|
060d9550d4 | ||
|
|
a449c6c635 | ||
|
|
4fda478078 | ||
|
|
0f7546836a | ||
|
|
4f9efc4fd0 | ||
|
|
2c04f58fb4 | ||
|
|
8c25c735d0 | ||
|
|
947ab18de6 | ||
|
|
b97e3fdec5 | ||
|
|
d7c1df72c3 | ||
|
|
428d2aead9 | ||
|
|
85d9616029 |
16
CHANGELOG
Normal 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
|
||||
@@ -16,7 +16,7 @@
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
041E2BA81F9EF0370036687C /* FloatingToggl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingToggl.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
041E2BA81F9EF0370036687C /* In Time.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "In Time.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
041E2BAB1F9EF0370036687C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
041E2BAD1F9EF0370036687C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
041E2BAF1F9EF0370036687C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -24,7 +24,7 @@
|
||||
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>"; };
|
||||
041E2BBB1F9EF3450036687C /* FloatingPannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPannel.swift; sourceTree = "<group>"; };
|
||||
046DF6521FA0763900FB970F /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -45,7 +45,7 @@
|
||||
041E2B9F1F9EF0370036687C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
046DF6521FA0763900FB970F /* MyPlayground.playground */,
|
||||
04D507151FCC84360038E7E0 /* CHANGELOG */,
|
||||
041E2BAA1F9EF0370036687C /* FloatingToggl */,
|
||||
041E2BA91F9EF0370036687C /* Products */,
|
||||
AB897BA953C7D862BCC1F2D0 /* Pods */,
|
||||
@@ -56,7 +56,7 @@
|
||||
041E2BA91F9EF0370036687C /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
041E2BA81F9EF0370036687C /* FloatingToggl.app */,
|
||||
041E2BA81F9EF0370036687C /* In Time.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -112,7 +112,7 @@
|
||||
);
|
||||
name = FloatingToggl;
|
||||
productName = FloatingToggl;
|
||||
productReference = 041E2BA81F9EF0370036687C /* FloatingToggl.app */;
|
||||
productReference = 041E2BA81F9EF0370036687C /* In Time.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -363,7 +363,7 @@
|
||||
INFOPLIST_FILE = FloatingToggl/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matrix.FloatingToggl;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PRODUCT_NAME = "In Time";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 4.0;
|
||||
};
|
||||
@@ -382,7 +382,7 @@
|
||||
INFOPLIST_FILE = FloatingToggl/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matrix.FloatingToggl;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PRODUCT_NAME = "In Time";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 4.0;
|
||||
};
|
||||
|
||||
@@ -9,17 +9,98 @@
|
||||
import Cocoa
|
||||
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
|
||||
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) {
|
||||
// Insert code here to initialize your application
|
||||
|
||||
reminderInterval = UserDefaults.standard.reminderInterval
|
||||
shouldAutoApply = UserDefaults.standard.shouldAutoApply
|
||||
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension NSMenuItem {
|
||||
|
||||
var isChecked: Bool {
|
||||
get { return state ~= .on }
|
||||
set { state = newValue ? .on : .off }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,53 +1,63 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-33.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-257.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-513.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon-1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
|
||||
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-128.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-16.png
Normal file
|
After Width: | Height: | Size: 531 B |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-257.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-33.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-512.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-513.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
FloatingToggl/Assets.xcassets/AppIcon.appiconset/Icon-64.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,8 +1,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>
|
||||
<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="stacking Non-gravity area distributions on NSStackView" minToolsVersion="7.0" minSystemVersion="10.11"/>
|
||||
</dependencies>
|
||||
@@ -13,11 +13,11 @@
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="FloatingToggl" id="1Xt-HY-uBw">
|
||||
<menuItem title="In Time" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="FloatingToggl" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<menu key="submenu" title="In Time" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About FloatingToggl" id="5kV-Vb-QxS">
|
||||
<menuItem title="About In Time" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
@@ -31,7 +31,7 @@
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide FloatingToggl" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<menuItem title="Hide In Time" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
@@ -49,7 +49,7 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit FloatingToggl" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<menuItem title="Quit In Time" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
@@ -293,26 +293,40 @@
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<menuItem title="Reminder" id="aUF-d1-5bR">
|
||||
<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>
|
||||
<menuItem title="Minimize" keyEquivalent="m" 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">
|
||||
<menuItem title="5 min" id="OY7-WF-poV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<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>
|
||||
</menuItem>
|
||||
<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"/>
|
||||
<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>
|
||||
</menuItem>
|
||||
</items>
|
||||
@@ -322,7 +336,7 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="FloatingToggl Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<menuItem title="In Time Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
@@ -336,7 +350,15 @@
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="FloatingToggl" 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="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -345,10 +367,10 @@
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" customClass="FloatingPannel" customModule="FloatingToggl" 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">
|
||||
<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"/>
|
||||
<rect key="contentRect" x="95" y="73" width="426" height="100"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
@@ -367,7 +389,7 @@
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="FloatingToggl" 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">
|
||||
<rect key="frame" x="0.0" y="0.0" width="364" height="60"/>
|
||||
<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">
|
||||
<rect key="frame" x="20" y="0.0" width="324" height="60"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="249" verticalHuggingPriority="750" horizontalCompressionResistancePriority="999" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BPy-6c-wmT" customClass="AutoGrowTextField" customModule="FloatingToggl" 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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="Tug-Tb-Szc"/>
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>6</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.productivity</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
@@ -14,7 +14,7 @@ import KeychainSwift
|
||||
|
||||
struct TimeEntry: Decodable {
|
||||
let id: Int64
|
||||
let start: String
|
||||
let start: Date
|
||||
let description: String?
|
||||
}
|
||||
|
||||
@@ -25,14 +25,15 @@ struct DataResponse<T: Decodable>: Decodable {
|
||||
struct Project: Decodable {
|
||||
let id: Int64
|
||||
let name: String
|
||||
let at: Date
|
||||
}
|
||||
|
||||
struct User: Decodable {
|
||||
|
||||
let id: Int64
|
||||
let fullname: String
|
||||
let projects: [Project]
|
||||
let time_entries: [TimeEntry]
|
||||
let projects: [Project]?
|
||||
let time_entries: [TimeEntry]?
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
let method: String
|
||||
let url: URL
|
||||
@@ -66,7 +97,7 @@ struct Endpoint<T: Decodable> {
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
|
||||
}
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -80,7 +111,7 @@ struct Endpoint<T: Decodable> {
|
||||
"time_entry": [
|
||||
"description": title,
|
||||
"pid": projectId ?? NSNull(),
|
||||
"created_with": "Toggl Bar"
|
||||
"created_with": "In Time"
|
||||
] as NSDictionary
|
||||
])
|
||||
}
|
||||
@@ -89,6 +120,23 @@ struct Endpoint<T: Decodable> {
|
||||
return Endpoint<TimeEntry>(method: "PUT", url: URL.api("time_entries/\(timeEntry)/stop"))
|
||||
}
|
||||
|
||||
static func create(project: String) -> Endpoint<Project> {
|
||||
return Endpoint<Project>.init(method: "POST", url: URL.api("projects"), body: [
|
||||
"project": [
|
||||
"name": project,
|
||||
"is_private": true
|
||||
]
|
||||
] 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
|
||||
]
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +153,7 @@ class TogglViewModel {
|
||||
let input = Variable<String>("")
|
||||
|
||||
let active = Variable<Bool>(NSApplication.shared.isActive)
|
||||
let awake = Variable<Bool>(true)
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
@@ -114,8 +163,8 @@ class TogglViewModel {
|
||||
var completions: Driver<[String]> {
|
||||
return user.asDriver().map({ user -> [String] in
|
||||
guard let user = user else { return [] }
|
||||
let projects = user.projects.map({"#\($0.name)"})
|
||||
let entries = user.time_entries.sorted(by: {$0.start > $1.start}).flatMap({$0.description})
|
||||
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}) ?? []
|
||||
return Array(NSOrderedSet(array: projects + entries)).flatMap({$0 as? String})
|
||||
}).flatMapLatest({[weak self] (completion:[String]) -> Driver<[String]> in
|
||||
guard let input = self?.input else { return .just(completion) }
|
||||
@@ -163,7 +212,7 @@ class TogglViewModel {
|
||||
})
|
||||
}
|
||||
return Driver<TimeEntry?>.just(nil)
|
||||
}).debug().drive(current).disposed(by: self.disposeBag)
|
||||
}).drive(current).disposed(by: self.disposeBag)
|
||||
|
||||
token.asDriver().flatMapLatest({[weak self] token -> Driver<User?> in
|
||||
if let token = token {
|
||||
@@ -172,55 +221,109 @@ class TogglViewModel {
|
||||
}) ?? .just(nil)
|
||||
}
|
||||
return Driver<User?>.just(nil)
|
||||
}).debug().drive(user).disposed(by: self.disposeBag)
|
||||
}).drive(user).disposed(by: self.disposeBag)
|
||||
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.rx.notification(NSWorkspace.screensDidSleepNotification)
|
||||
.subscribe(onNext: {[weak self] _ in
|
||||
self?.awake.value = false
|
||||
guard let entry = self?.current.value else { return }
|
||||
self?.timeEntryWhenSlept = entry
|
||||
self?.screenSleptAt = Date()
|
||||
self?.stopTimer()
|
||||
}).disposed(by: self.disposeBag)
|
||||
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.rx.notification(NSWorkspace.screensDidWakeNotification)
|
||||
.subscribe(onNext: {[weak self] _ in
|
||||
self?.awake.value = true
|
||||
|
||||
defer {
|
||||
self?.screenSleptAt = nil
|
||||
self?.timeEntryWhenSlept = nil
|
||||
}
|
||||
|
||||
guard
|
||||
let date = self?.screenSleptAt,
|
||||
let timer = self?.timeEntryWhenSlept,
|
||||
Date().timeIntervalSince(date) < 60 * 10
|
||||
Date().timeIntervalSince(date) > 60 * 10
|
||||
else {
|
||||
self?.screenSleptAt = nil
|
||||
self?.timeEntryWhenSlept = nil
|
||||
return
|
||||
}
|
||||
|
||||
self?.input.value = timer.description ?? ""
|
||||
self?.startTimer()
|
||||
self?.stopTimer(entry: timer, stoppedAt: date)
|
||||
}).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() {
|
||||
guard let token = self.token.value else { return }
|
||||
let projectId = self.input.value.hashKey.flatMap({ projectName in
|
||||
self.user.value?.projects.first(where: {
|
||||
let inputValue = input.value
|
||||
if let projectName = inputValue.hashKey {
|
||||
if let existingProject = self.user.value?.projects?.first(where: {
|
||||
$0.name.lowercased() == projectName.lowercased()
|
||||
})
|
||||
})?.id
|
||||
}) {
|
||||
self.startTimer(input: inputValue, projectId: existingProject.id)
|
||||
} else {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .informational
|
||||
alert.messageText = "Project \(projectName) does not exist, should I create it?"
|
||||
alert.addButton(withTitle: "Create")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.beginSheetModal(for: NSApplication.shared.keyWindow!) {[weak self] (response) in
|
||||
guard response == .alertFirstButtonReturn else {
|
||||
self?.startTimer(input: inputValue, projectId: nil)
|
||||
return
|
||||
}
|
||||
_ = self?.createProject(name: projectName).subscribe(onNext: {[weak self] project in
|
||||
self?.startTimer(input: inputValue, projectId: project.id)
|
||||
}, onError: { _ in
|
||||
self?.startTimer(input: inputValue, projectId: nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.startTimer(input: inputValue, projectId: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func createProject(name: String) -> Observable<Project> {
|
||||
guard let token = self.token.value else { return .empty() }
|
||||
return Endpoint<Project>.create(project: name).request(with: token)
|
||||
}
|
||||
|
||||
func startTimer(input: String, projectId: Int64?) {
|
||||
guard let token = self.token.value else { return }
|
||||
Endpoint<TimeEntry>.start(title: self.input.value, projectId: projectId)
|
||||
.request(with: token)
|
||||
.map(Optional.some)
|
||||
.catchErrorJustReturn(nil)
|
||||
.bind(to: current)
|
||||
.bind(to: self.current)
|
||||
.disposed(by: self.disposeBag)
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
func stopTimer(entry: TimeEntry? = nil, stoppedAt: Date? = nil) {
|
||||
guard let token = self.token.value else { return }
|
||||
guard let entryId = self.current.value?.id else { return }
|
||||
Endpoint<TimeEntry>.stop(timeEntry: entryId).request(with: token)
|
||||
guard let entry = entry ?? self.current.value else { return }
|
||||
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})
|
||||
.catchErrorJustReturn(nil)
|
||||
.bind(to: current)
|
||||
@@ -322,11 +425,8 @@ class ViewController: NSViewController {
|
||||
self.selectedRow = 0
|
||||
}).disposed(by: self.disposeBag)
|
||||
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
||||
|
||||
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
|
||||
let time: Int = Int(Date().timeIntervalSince(date))
|
||||
let hours = time / 3600
|
||||
@@ -354,6 +454,10 @@ class ViewController: NSViewController {
|
||||
self?.placeCursorAtTheEnd()
|
||||
self?.resizeWindow()
|
||||
}).disposed(by: self.disposeBag)
|
||||
|
||||
viewModel.presentReminder.subscribe(onNext: {[weak self] in
|
||||
self?.presentReminder()
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
var trackingRect: NSView.TrackingRectTag?
|
||||
@@ -431,6 +535,52 @@ class ViewController: NSViewController {
|
||||
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) {
|
||||
presentSetToken()
|
||||
}
|
||||
@@ -487,7 +637,11 @@ extension ViewController: NSTextFieldDelegate {
|
||||
case #selector(textView.insertTab(_:)):
|
||||
insertSelection()
|
||||
case #selector(textView.insertNewline(_:)):
|
||||
self.viewModel.startTimer()
|
||||
if isShowingRecentEntries {
|
||||
insertSelection()
|
||||
} else {
|
||||
self.viewModel.startTimer()
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
//: Playground - noun: a place where people can play
|
||||
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
import PlaygroundSupport
|
||||
|
||||
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
f.date(from: "2017-10-25T08:01:26Z")
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='macos'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
||||
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Toggl Bar
|
||||
|
||||
---------
|
||||
|
||||
|
||||
A Toggl Bar Mac app that will follow you wherever you go.
|
||||
|
||||
First you need to obtain your API token from:
|
||||
|
||||
https://www.toggl.com/app/profile
|
||||
|
||||
Download this app from https://github.com/zhigang1992/InTime/releases/tag/1.0.0
|
||||
|
||||
And you're all set.
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
1. Using `#project` syntax to add task to a project.
|
||||
1. Auto complete using your projects and recent entries.
|
||||
1. Automatically stop timer when screen goes to sleep.
|
||||
1. Automatically resume timer when screen goes back on within 10 min.
|
||||
1. ...
|
||||
|
||||

|
||||

|
||||