Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
Merged dev into master
  • Loading branch information
masich committed Apr 14, 2020
2 parents 3594efe + d4cb603 commit fa3c7bb
Show file tree
Hide file tree
Showing 13 changed files with 92 additions and 80 deletions.
Binary file modified Images/Readme/Installation/Drag&Drop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/Readme/Installation/Security.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/Readme/Installation/Warning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Images/Readme/Screenshots/MenuAppScreenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
[![CodeFactor](https://www.codefactor.io/repository/github/masich/savemyeyes/badge)](https://www.codefactor.io/repository/github/masich/savemyeyes)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)

You can choose a specific time interval and break time which perfects for you. For now, there are only a limited number of options, but the app will support direct time input in the future.
You can choose a specific work and break time intervals which perfects for you.

Time interval - time in minutes to work on the computer before notification about break will be generated.
Work time - time interval in minutes to work on the computer before notification about break will be generated.

Break time - time in minutes to rest your eyes.
Break time - time interval in minutes to rest your eyes.

Features:
* Some general predefined work and break time intervals sets.
* Minimalistic application design.
* Time settings selected by the user are saved in local storage.
* Automatically pauses and resumes timer depending on user activity.
* Sends reminder notifications based on the time presets selected by the user.
Expand All @@ -20,7 +20,7 @@ The app is built using ```SwiftUI``` and requires macOS 10.15 to run.

## Installation guide
* Download the latest [release](https://github.com/masich/SaveMyEyes/releases/) ```.dmg``` image and mount it.
* Drag & Drop SaveMyEyes app to the Application folder.
* [Drag & Drop](Images/Readme/Installation/Drag&Drop.png) SaveMyEyes app to the Application folder.
* Open SaveMyEyes.
* According to the latest Catalina changes the [warning](Images/Readme/Installation/Warning.png) will appear.
* Go to the System Preferences and open it's Security & Privacy tab.
Expand Down
8 changes: 4 additions & 4 deletions SaveMyEyes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_ASSET_PATHS = "\"SaveMyEyes/Preview Content\"";
DEVELOPMENT_TEAM = 5PYHR2J538;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -369,7 +369,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.31;
MARKETING_VERSION = 0.4;
PRODUCT_BUNDLE_IDENTIFIER = com.masich.SaveMyEyes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -384,7 +384,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_ASSET_PATHS = "\"SaveMyEyes/Preview Content\"";
DEVELOPMENT_TEAM = 5PYHR2J538;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -395,7 +395,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.31;
MARKETING_VERSION = 0.4;
PRODUCT_BUNDLE_IDENTIFIER = com.masich.SaveMyEyes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand Down
8 changes: 5 additions & 3 deletions SaveMyEyes/Source/Common/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import Foundation

struct Constants {
// TODO: make it possible to take these values from the user input
public static let workIntervals = [15, 20, 30, 40, 60, 90, 120]
public static let breakIntervals = [1, 2, 5, 10, 15, 20, 30]
public static let workIntervalRange = 1...150 // minutes
public static let breakIntervalRange = 1...60 // minutes
public static let defaultWorkInterval = 15 // minutes
public static let defaultBreakInterval = 1 // minutes
public static let defaultIsSoundEnabled = true

public static let minute: TimeInterval = 60 // seconds

Expand Down
41 changes: 18 additions & 23 deletions SaveMyEyes/Source/Common/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,48 @@
import Foundation

class Preferences{
private static let selectedTimeIntervalKey = "SelectedTimeInterval";
private static let selectedBreakTimeKey = "SelectedBreakTime";
private static let isSoundEnabledKey = "IsSoundEnabled";
private static let workTimeIntervalKey = "WorkTimeInterval"
private static let breakTimeIntervalKey = "BreakTimeInterval"
private static let isSoundEnabledKey = "IsSoundEnabled"

private static let userDefaults = UserDefaults.standard

public static func registerDefaults(defaults: [String : Any]) {
userDefaults.register(defaults: defaults)
}

/**
Returns selected time interval index retrieved from the local storage
Returns selected time interval value retrieved from the local storage

returns `Int`: Localy saved user selected time interval or `0` when there is no
returns `Int`: Localy saved user selected time interval or `defaultValue` when there is no
saved time interval
*/
public static func getWorkIntervalIndexValue(_ defaultValue: Int) -> Int {
return userDefaults.value(forKey: selectedTimeIntervalKey, defaultValue: defaultValue)

public static func getWorkIntervalValue(_ defaultValue: Int = 0) -> Int {
return userDefaults.value(forKey: workTimeIntervalKey, defaultValue: defaultValue)
}

/**
Returns selected break time index retrieved from the local storage
Returns selected break interval value retrieved from the local storage

returns `Int`: Localy saved user selected break time index or `0` when there is
no saved break time index
returns `Int`: Localy saved user selected break time index or `defaultValue` when there is
no saved break time
*/
public static func getBreakIntervalIndexValue(_ defaultValue: Int) -> Int {
return userDefaults.value(forKey: selectedBreakTimeKey, defaultValue: defaultValue)
public static func getBreakIntervalValue(_ defaultValue: Int = 0) -> Int {
return userDefaults.value(forKey: breakTimeIntervalKey, defaultValue: defaultValue)
}

/**
Returns is sound enabled value retrieved from the local storage

returns `Bool`: Localy saved is sound enabled value or `false` when there is
returns `Bool`: Localy saved is sound enabled value or `defaultValue` when there is
no saved is sound enabled value
*/
public static func isSoundEnabled(_ defaultValue: Bool) -> Bool {
public static func isSoundEnabled(_ defaultValue: Bool = false) -> Bool {
return userDefaults.value(forKey: isSoundEnabledKey, defaultValue: defaultValue)
}

public static func setWorkTimeIntervalIndexValue(_ value: Int) {
userDefaults.set(value, forKey: selectedTimeIntervalKey)
public static func setWorkTimeIntervalValue(_ value: Int) {
userDefaults.set(value, forKey: workTimeIntervalKey)
}

public static func setBreakIntervalIndexValue(_ value: Int) {
userDefaults.set(value, forKey: selectedBreakTimeKey)
public static func setBreakIntervalValue(_ value: Int) {
userDefaults.set(value, forKey: breakTimeIntervalKey)
}

public static func setSoundEnabled(_ value: Bool) {
Expand Down
48 changes: 28 additions & 20 deletions SaveMyEyes/Source/Main/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,56 @@

import SwiftUI


struct MainView: View {
@ObservedObject var mainViewModel: MainViewModel

var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 20) {
HStack(spacing: 8) {
Text(mainViewModel.isBreakTimeNow ? "Time to work" : "Time to break")
Spacer()
Text("\(mainViewModel.remainingMins) \("min".localized)")
}.scaledToFill()
HStack(spacing: 4) {
Text("\(self.mainViewModel.remainingMins)").frame(width: 36, alignment: .trailing)
Text("min")
}
}
Divider()
HStack(spacing: 20) {
HStack(spacing: 8) {
Text("Run timer")
Spacer()
Toggle("Run timer toggle", isOn: self.$mainViewModel.shouldTimerRun.value).labelsHidden()
}.scaledToFill()
HStack(spacing: 20) {
}
HStack(spacing: 8) {
Text("Enable sound")
Spacer()
Toggle("Enable sound toggle", isOn: self.$mainViewModel.isSoundEnabled.value).labelsHidden()
}.scaledToFill()
HStack(spacing: 20) {
}
HStack(spacing: 8) {
Text("Work interval")
Spacer()
Picker("Work interval picker", selection: self.$mainViewModel.workIntervalIndex.value) {
ForEach(mainViewModel.workIntervals.indices, id: \.self) { index in
Text(String(self.mainViewModel.workIntervals[index])).tag(index)
Stepper(value: self.$mainViewModel.workInterval.value, in: Constants.workIntervalRange) {
HStack(spacing: 4) {
Text("\(self.mainViewModel.workInterval.value)").frame(width: 36, alignment: .trailing)
Text("min")
}
}.labelsHidden().scaledToFit().fixedSize()
}
}
HStack(spacing: 20) {
HStack(spacing: 8) {
Text("Break interval")
Spacer()
Picker("Break interval picker", selection: self.$mainViewModel.breakIntervalIndex.value) {
ForEach(mainViewModel.breakIntervals.indices, id: \.self) { index in
Text(String(self.mainViewModel.breakIntervals[index])).tag(index)
Stepper(value: self.$mainViewModel.breakInterval.value, in: Constants.breakIntervalRange) {
HStack(spacing: 4) {
Text("\(self.mainViewModel.breakInterval.value)").frame(width: 36, alignment: .trailing)
Text("min")
}
}.labelsHidden().scaledToFit().fixedSize()
}
}
Divider()
Button("Quit", action: mainViewModel.terminateApp).buttonStyle(BorderlessButtonStyle())
}.padding().fixedSize()
HStack(spacing: 8) {
Button("Quit", action: mainViewModel.terminateApp).buttonStyle(BorderlessButtonStyle())
Spacer()
Button("Reset", action: mainViewModel.resetToDefaults).buttonStyle(BorderlessButtonStyle())
}
}.padding().fixedSize().scaledToFill()
}
}
49 changes: 25 additions & 24 deletions SaveMyEyes/Source/Main/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ class MainViewModel: ObservableObject {
@Published private(set) var remainingMins: Int = 0

@Published var shouldTimerRun = Observable<Bool>(false)
@Published var isSoundEnabled = Observable<Bool>(Preferences.isSoundEnabled(true))
@Published var workIntervalIndex = Observable<Int>(Preferences.getWorkIntervalIndexValue(0))
@Published var breakIntervalIndex = Observable<Int>(Preferences.getBreakIntervalIndexValue(0))
@Published var isSoundEnabled = Observable<Bool>(Preferences.isSoundEnabled(Constants.defaultIsSoundEnabled))
@Published var workInterval = Observable<Int>(Preferences.getWorkIntervalValue(Constants.defaultWorkInterval))
@Published var breakInterval = Observable<Int>(Preferences.getBreakIntervalValue(Constants.defaultBreakInterval))

private var timerWorker: TimerWorker!
private var cancellables = [AnyCancellable]()
Expand All @@ -62,51 +62,45 @@ class MainViewModel: ObservableObject {
private let allowedUserInactivityInterval: TimeInterval
private let timerInterval: TimeInterval

let breakIntervals: [Int]
let workIntervals: [Int]
let terminateApp: () -> ()

init(
workIntervals: [Int],
breakIntervals: [Int],
timerInterval: TimeInterval,
allowedUserInactivityInterval: TimeInterval,
terminateApp: @escaping () -> ()
) {
self.workIntervals = workIntervals
self.breakIntervals = breakIntervals
self.timerInterval = timerInterval
self.allowedUserInactivityInterval = allowedUserInactivityInterval
self.terminateApp = terminateApp

remainingMins = isBreakTimeNow ? breakIntervals[breakIntervalIndex.value] : workIntervals[workIntervalIndex.value]
remainingMins = isBreakTimeNow ? breakInterval.value : workInterval.value
timerWorker = TimerWorker(timerInterval: timerInterval, timerHandler: timerHandler)

cancellables = [
shouldTimerRun.subject.sink(receiveValue: timerWorker.toggleInternalTimer),
isSoundEnabled.subject.sink(receiveValue: Preferences.setSoundEnabled),
workIntervalIndex.subject.sink(receiveValue: onWorkIntervalChanged),
workIntervalIndex.subject.sink(receiveValue: Preferences.setWorkTimeIntervalIndexValue),
breakIntervalIndex.subject.sink(receiveValue: onBreakIntervalChanged),
breakIntervalIndex.subject.sink(receiveValue: Preferences.setBreakIntervalIndexValue),
workInterval.subject.sink(receiveValue: onWorkIntervalChanged),
workInterval.subject.sink(receiveValue: Preferences.setWorkTimeIntervalValue),
breakInterval.subject.sink(receiveValue: onBreakIntervalChanged),
breakInterval.subject.sink(receiveValue: Preferences.setBreakIntervalValue),
]
}

/**
Applyes changes on the work time interval value index
Applyes changes on the work time interval value
*/
private func onWorkIntervalChanged(_ workIntervalIndex: Int) {
private func onWorkIntervalChanged(_ workInterval: Int) {
if !isBreakTimeNow {
remainingMins = workIntervals[workIntervalIndex]
remainingMins = workInterval
}
}

/**
Applyes changes on the break time interval value index
Applyes changes on the break time interval value
*/
private func onBreakIntervalChanged(_ breakIntervalIndex: Int) {
private func onBreakIntervalChanged(_ breakInterval: Int) {
if isBreakTimeNow {
remainingMins = breakIntervals[breakIntervalIndex]
remainingMins = breakInterval
}
}

Expand All @@ -118,17 +112,18 @@ class MainViewModel: ObservableObject {
public func timerHandler(timer: Timer) {
let isUserIncativeNew = System.isUserInactive(forMinutes: Constants.allowedUserInactivityMinutes)
if !isUserInactive && isUserIncativeNew {
remainingMins += Constants.allowedUserInactivityMinutes
let maxIncrementValue = workInterval.value - remainingMins
remainingMins += min(maxIncrementValue, Constants.allowedUserInactivityMinutes)
}
isUserInactive = isUserIncativeNew

if isBreakTimeNow || !isUserInactive {
remainingMins -= 1
if remainingMins <= 0 {
if isBreakTimeNow {
remainingMins = workIntervals[workIntervalIndex.value]
remainingMins = workInterval.value
} else {
remainingMins = breakIntervals[breakIntervalIndex.value]
remainingMins = breakInterval.value
}
isBreakTimeNow.toggle()
sendNotification()
Expand All @@ -145,7 +140,7 @@ class MainViewModel: ObservableObject {
let notification: AppNotification
let notificationSound = isSoundEnabled.value ? AppNotification.defaultSound : AppNotification.withoutSound
if isBreakTimeNow {
notification = AppNotification(title: "It's time for break".localized, subtitle: String(format: "Relax from your computer for %d minutes.".localized, breakIntervals[breakIntervalIndex.value]), sound: notificationSound)
notification = AppNotification(title: "It's time for break".localized, subtitle: String(format: "Relax from your computer for %d minutes.".localized, breakInterval.value), sound: notificationSound)
} else {
notification = AppNotification(title: "It's time to work".localized, subtitle: "Let's continue to do amazing things!".localized, sound: notificationSound)
}
Expand All @@ -159,4 +154,10 @@ class MainViewModel: ObservableObject {
// TODO: Remove it
remainingMins -= 0
}

public func resetToDefaults() {
isSoundEnabled.value = Constants.defaultIsSoundEnabled
workInterval.value = Constants.defaultWorkInterval
breakInterval.value = Constants.defaultBreakInterval
}
}
6 changes: 5 additions & 1 deletion SaveMyEyes/Source/Startup/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele

func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
mainViewModel = MainViewModel(workIntervals: Constants.workIntervals, breakIntervals: Constants.breakIntervals, timerInterval: Constants.minute, allowedUserInactivityInterval: Constants.allowedUserInactivityInterval, terminateApp: AppDelegate.terminateApp)
mainViewModel = MainViewModel(timerInterval: Constants.minute, allowedUserInactivityInterval: Constants.allowedUserInactivityInterval, terminateApp: AppDelegate.terminateApp)
let view = MainView(mainViewModel: mainViewModel!)

// Create the popover
Expand Down Expand Up @@ -72,6 +72,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
completionHandler()
}

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .badge, .sound])
}

private func setupNotifications() {
AppNotificationManager.requestAuthorization()
AppNotificationManager.removeAllNotifications()
Expand Down
1 change: 1 addition & 0 deletions SaveMyEyes/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
"Let's continue to do amazing things!" = "Let's continue to do amazing things!";
"Pause" = "Pause";
"Enable sound" = "Enable sound";
"Reset" = "Reset";
1 change: 1 addition & 0 deletions SaveMyEyes/uk.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
"Let's continue to do amazing things!" = "Давай продовжимо робити круті речі!";
"Pause" = "Пауза";
"Enable sound" = "Увімкнути звук";
"Reset" = "Скинути";

0 comments on commit fa3c7bb

Please sign in to comment.