diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index c53b1eb..4027e20 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -18,6 +18,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var username = UserDefaults.standard.string(forKey: Consts.usernameDefaultKey) ?? "" private var friendUsername = UserDefaults.standard.string(forKey: Consts.friendUsernameDefaultKey) ?? "" private var goal = UserDefaults.standard.integer(forKey: Consts.goalDefaultKey) + private var selfCompareOffset = UserDefaults.standard.integer(forKey: Consts.selfCompareOffsetDefaultKey) private let menu = NSMenu().then { $0.title = "" } @@ -73,6 +74,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { $0.tag = 6 $0.keyEquivalent = "d" } + + private let pastSelfMenuItem = NSMenuItem().then { + $0.title = Localized.setPastSelf + $0.action = #selector(onChangePastSelfClick) + $0.tag = 11 + $0.keyEquivalent = "p" + } + + private let removePastSelfMenuItem = NSMenuItem().then { + $0.title = Localized.removePastSelf + $0.action = #selector(onRemovePastSelfClick) + $0.tag = 11 + $0.keyEquivalent = "" + } private let settingMenuItem = NSMenuItem().then { $0.title = Localized.setting @@ -125,6 +140,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(.separator()) menu.addItem(friendMenuItem) menu.addItem(RemoveFriendMenuItem) + menu.addItem(pastSelfMenuItem) + menu.addItem(removePastSelfMenuItem) menu.addItem(.separator()) menu.addItem(goalMenuItem) menu.addItem(.separator()) @@ -140,19 +157,29 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private func updateUI() { - + var withFriend = "" if !friendUsername.isEmpty { withFriend = Localized.withFriend.replacingOccurrences(of: "${username}", with: friendUsername) + } else if selfCompareOffset > 0 { + withFriend = Localized.withPastSelf.replacingOccurrences(of: "${day}", with: String(selfCompareOffset)) } - + let userMenuItemTitle = Localized.hello.replacingOccurrences(of: "${username}", with: username).replacingOccurrences(of: "${withFriend}", with: withFriend) userMenuItem.attributedTitle = NSAttributedString(string: userMenuItemTitle) - + let friendMenuItemTitle = self.friendUsername.isEmpty ? Localized.setFriendUsername : Localized.changeFriendUsername friendMenuItem.title = friendMenuItemTitle - - RemoveFriendMenuItem.isHidden = self.friendUsername.isEmpty + + let pastSelfActive = self.selfCompareOffset > 0 + let friendActive = !self.friendUsername.isEmpty + + friendMenuItem.isHidden = pastSelfActive + RemoveFriendMenuItem.isHidden = !friendActive || pastSelfActive + + pastSelfMenuItem.title = pastSelfActive ? Localized.changePastSelf : Localized.setPastSelf + pastSelfMenuItem.isHidden = friendActive + removePastSelfMenuItem.isHidden = !pastSelfActive || friendActive } private func showSettingAlert() { @@ -224,6 +251,37 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + private func showChangePastSelfAlert() { + let alert = NSAlert() + let offsetTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 20)) + let formatter = IntegerValueFormatter() + offsetTextField.formatter = formatter + offsetTextField.placeholderString = self.selfCompareOffset > 0 ? String(self.selfCompareOffset) : "1" + + alert.messageText = Localized.setPastSelf + alert.informativeText = Localized.pastSelfInformation + alert.alertStyle = .informational + alert.accessoryView = offsetTextField + alert.addButton(withTitle: Localized.ok) + alert.addButton(withTitle: Localized.cancel) + alert.window.initialFirstResponder = alert.accessoryView + + if alert.runModal() == .alertFirstButtonReturn { + let raw = offsetTextField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + let offset: Int + if raw.isEmpty { + offset = self.selfCompareOffset > 0 ? self.selfCompareOffset : 1 + } else { + offset = offsetTextField.integerValue + } + if offset <= 0 { + removeSelfCompareInfo() + } else { + changeSelfCompareOffset(with: offset) + } + } + } + private func showChangeGoalAlert() { let alert = NSAlert() let goalTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 20)) @@ -303,6 +361,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { showChangeGoalAlert() } + @objc func onChangePastSelfClick() { + showChangePastSelfAlert() + } + + @objc func onRemovePastSelfClick() { + removeSelfCompareInfo() + } + private func changeUsername(withUsername username: String) { UserDefaults.standard.setValue(username, forKey: Consts.usernameDefaultKey) self.username = UserDefaults.standard.string(forKey: Consts.usernameDefaultKey)! @@ -313,15 +379,37 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func changeFriendUsername(withUsername username: String) { UserDefaults.standard.setValue(username, forKey: Consts.friendUsernameDefaultKey) self.friendUsername = UserDefaults.standard.string(forKey: Consts.friendUsernameDefaultKey)! - + + UserDefaults.standard.setValue(0, forKey: Consts.selfCompareOffsetDefaultKey) + self.selfCompareOffset = 0 + refresh() } - + private func removeFriendinfo(){ UserDefaults.standard.setValue("", forKey: Consts.friendUsernameDefaultKey) self.friendUsername = "" self.friendContributes = [] - + + refresh() + } + + private func changeSelfCompareOffset(with offset: Int) { + UserDefaults.standard.setValue(offset, forKey: Consts.selfCompareOffsetDefaultKey) + self.selfCompareOffset = UserDefaults.standard.integer(forKey: Consts.selfCompareOffsetDefaultKey) + + UserDefaults.standard.setValue("", forKey: Consts.friendUsernameDefaultKey) + self.friendUsername = "" + self.friendContributes = [] + + refresh() + } + + private func removeSelfCompareInfo() { + UserDefaults.standard.setValue(0, forKey: Consts.selfCompareOffsetDefaultKey) + self.selfCompareOffset = 0 + self.friendContributes = [] + refresh() } @@ -478,12 +566,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { } - let contributeDataList = self.parseHtmltoData(html: html) + let sortedDays = self.parseSortedDays(html: html) + let contributeDataList = self.parseHtmltoData(days: sortedDays) if isFriend { self.friendContributes = contributeDataList } else{ self.myContributes = contributeDataList - self.mystreaks = self.parseHtmltoDataForCount(html: html) + self.mystreaks = self.parseHtmltoDataForCount(days: sortedDays) + if self.friendUsername.isEmpty && self.selfCompareOffset > 0 { + self.friendContributes = self.parseHtmltoData(days: sortedDays, daysBack: self.selfCompareOffset) + } } if group != nil { @@ -531,41 +623,43 @@ class AppDelegate: NSObject, NSApplicationDelegate { return integerValue } - private func parseHtmltoData(html: String) -> [ContributeData] { + private func parseSortedDays(html: String) -> [Element] { let isoDateFormatter = ISO8601DateFormatter() isoDateFormatter.formatOptions = [.withFullDate] - + do { let doc: Document = try SwiftSoup.parse(html) let rects: Elements = try doc.getElementsByTag(ParseKeys.rect) let tooltips: Elements = try doc.getElementsByTag(ParseKeys.tooltip) let days: [Element] = rects.array().filter { $0.hasAttr(ParseKeys.date) } let sortedDays = sortDaysByDate(days, with: isoDateFormatter) - let weekend = sortedDays.suffix(Consts.fetchCount) - + var tooltipsTextById = [String: String]() for tooltip in tooltips.array() { let id = try tooltip.attr("for") let text = try tooltip.text() tooltipsTextById[id] = text } - - let updatedWeekend = weekend.map { element -> Element in + + for element in sortedDays { let id = element.id() if let tooltipText = tooltipsTextById[id] { _ = try? element.text(tooltipText) } - return element } - - let contributeDataList = updatedWeekend.map(mapFunction) - return contributeDataList - + + return sortedDays } catch { return [] } } - + + private func parseHtmltoData(days: [Element], daysBack: Int = 0) -> [ContributeData] { + let trimmedDays = daysBack > 0 ? days.dropLast(daysBack) : ArraySlice(days) + let weekend = trimmedDays.suffix(Consts.fetchCount) + return weekend.map(mapFunction) + } + private func sortDaysByDate(_ days: [Element], with dateFormatter: ISO8601DateFormatter) -> [Element] { return days.sorted { (element1, element2) -> Bool in guard let date1 = try? element1.attr(ParseKeys.date), @@ -577,69 +671,37 @@ class AppDelegate: NSObject, NSApplicationDelegate { return date1Value < date2Value } } - - private func parseHtmltoDataForCount(html: String) -> ContributeData { - do { - let doc: Document = try SwiftSoup.parse(html) - let rects: Elements = try doc.getElementsByTag(ParseKeys.rect) - let tooltips: Elements = try doc.getElementsByTag(ParseKeys.tooltip) - let days: [Element] = rects.array().filter { $0.hasAttr(ParseKeys.date) } - - var tooltipsTextById = [String: String]() - for tooltip in tooltips.array() { - let id = try tooltip.attr("for") - let text = try tooltip.text() - tooltipsTextById[id] = text - } - - let sortedDays = days.sorted { (element1, element2) -> Bool in - guard let date1 = try? element1.attr(ParseKeys.date), - let date2 = try? element2.attr(ParseKeys.date) else { - return false - } - return date1 < date2 - } - - let updatedDays = sortedDays.map { element -> Element in - let id = element.id() - if let tooltipText = tooltipsTextById[id] { - _ = try? element.text(tooltipText) - } - return element - } - - var currentStreak = 0 - let today = Calendar.current.startOfDay(for: Date()) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - - for day in updatedDays.reversed() { - let contribute = mapFunction(ele: day) - guard let contributionDate = dateFormatter.date(from: contribute.date) else { continue } - - let dayDifference = Calendar.current.dateComponents([.day], from: contributionDate, to: today).day ?? 0 - - if currentStreak == 0 && dayDifference > 1 { - break - } - - if contribute.count > 0 { - currentStreak += 1 - } else { - break - } + + private func parseHtmltoDataForCount(days: [Element]) -> ContributeData { + var currentStreak = 0 + let today = Calendar.current.startOfDay(for: Date()) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + for day in days.reversed() { + let contribute = mapFunction(ele: day) + guard let contributionDate = dateFormatter.date(from: contribute.date) else { continue } + + let dayDifference = Calendar.current.dateComponents([.day], from: contributionDate, to: today).day ?? 0 + + if currentStreak == 0 && dayDifference > 1 { + break } - - if let lastDay = updatedDays.last { - let lastContribute = mapFunction(ele: lastDay) - return ContributeData( - count: currentStreak, - weekend: lastContribute.weekend, - date: lastContribute.date - ) + + if contribute.count > 0 { + currentStreak += 1 + } else { + break } - } catch { - print("Failed to parse streaks data: \(error)") + } + + if let lastDay = days.last { + let lastContribute = mapFunction(ele: lastDay) + return ContributeData( + count: currentStreak, + weekend: lastContribute.weekend, + date: lastContribute.date + ) } return ContributeData(count: 0, weekend: "", date: "") } diff --git a/Sources/Consts/Consts.swift b/Sources/Consts/Consts.swift index 54067b8..e37c74a 100644 --- a/Sources/Consts/Consts.swift +++ b/Sources/Consts/Consts.swift @@ -16,4 +16,5 @@ enum Consts { static let usernameDefaultKey = "username" static let friendUsernameDefaultKey = "friend_username" static let goalDefaultKey = "goal" + static let selfCompareOffsetDefaultKey = "self_compare_offset" } diff --git a/Sources/Consts/Localized.swift b/Sources/Consts/Localized.swift index c4e9bc0..1ae8309 100644 --- a/Sources/Consts/Localized.swift +++ b/Sources/Consts/Localized.swift @@ -16,6 +16,11 @@ enum Localized { static let changeFriendUsername = NSLocalizedString("change_friend_username", comment: "Change friend username") static let setFriendUsername = NSLocalizedString("set_friend_username", comment: "Set friend username") static let removeFriendUsername = NSLocalizedString("remove_friend_username", comment: "Remove friend username") + static let setPastSelf = NSLocalizedString("set_past_self", comment: "Set past self comparison") + static let changePastSelf = NSLocalizedString("change_past_self", comment: "Change past self offset") + static let removePastSelf = NSLocalizedString("remove_past_self", comment: "Stop comparing with past self") + static let pastSelfInformation = NSLocalizedString("past_self_information", comment: "How many days ago to compare with") + static let withPastSelf = NSLocalizedString("with_past_self", comment: "\nvs me ${day} day(s) ago") static let setGoal = NSLocalizedString("set_goal", comment: "Set goal") static let help = NSLocalizedString("help", comment: "help") static let textFieldPlaceholder = NSLocalizedString("textfield_placeholder", comment: "Github username") diff --git a/Supporting Files/en.lproj/Localizable.strings b/Supporting Files/en.lproj/Localizable.strings index 8525c1a..a23606d 100644 --- a/Supporting Files/en.lproj/Localizable.strings +++ b/Supporting Files/en.lproj/Localizable.strings @@ -14,6 +14,11 @@ "change_friend_username" = "🀝 Change Friend username"; "set_friend_username" = "🀝 Set Friend username"; "remove_friend_username" = "πŸ™‹β€β™‚οΈ Remove Friend username"; +"set_past_self" = "βͺ Compare with past me"; +"change_past_self" = "βͺ Change past me offset"; +"remove_past_self" = "πŸ™‹β€β™‚οΈ Stop comparing with past me"; +"past_self_information" = "How many days ago should we compare with?\n(1 = yesterday)"; +"with_past_self" = "\nvs me ${day} day(s) ago"; "set_goal" = "🏁 Set Goal"; "help" = "πŸ› Bug Report"; "auto_launch" = "Automatically start Jandi at login"; diff --git a/Supporting Files/ko.lproj/Localizable.strings b/Supporting Files/ko.lproj/Localizable.strings index 69b499d..0571da8 100644 --- a/Supporting Files/ko.lproj/Localizable.strings +++ b/Supporting Files/ko.lproj/Localizable.strings @@ -14,6 +14,11 @@ "change_friend_username" = "🀝 친ꡬ 아이디 λ³€κ²½"; "set_friend_username" = "🀝 친ꡬ 아이디 μ„€μ •"; "remove_friend_username" = "πŸ™‹β€β™‚οΈ 친ꡬ 아이디 제거"; +"set_past_self" = "βͺ 과거의 λ‚˜μ™€ 비ꡐ"; +"change_past_self" = "βͺ 과거의 λ‚˜ 일수 λ³€κ²½"; +"remove_past_self" = "πŸ™‹β€β™‚οΈ 과거의 λ‚˜ 비ꡐ 쀑지"; +"past_self_information" = "λ©°μΉ  μ „μ˜ λ‚˜μ™€ λΉ„κ΅ν• κΉŒμš”?\n(1 = μ–΄μ œ)"; +"with_past_self" = "\n${day}일 μ „μ˜ λ‚˜μ™€ 비ꡐ"; "set_goal" = "🏁 λͺ©ν‘œ μ„€μ •"; "help" = "πŸ› 버그 μ œλ³΄ν•˜κΈ°"; "auto_launch" = "μž¬μ‹œμž‘μ‹œ μžλ™ μ‹€ν–‰";