diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 2c662bdb06a80..1d56cb6e150c9 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -60,11 +60,6 @@ func (s Stopwatch) Seconds() int64 { return int64(timeutil.TimeStampNow() - s.CreatedUnix) } -// Duration returns a human-readable duration string based on local server time -func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) -} - func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { sw = new(Stopwatch) exists, err = db.GetEngine(ctx). @@ -215,7 +210,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Doer: user, Issue: issue, Repo: issue.Repo, - Content: util.SecToTime(timediff), + Content: fmt.Sprintf("|%d", timediff), Type: CommentTypeStopTracking, TimeID: tt.ID, }); err != nil { diff --git a/models/user/setting_keys.go b/models/user/setting_keys.go index 72b3974eee435..0c4e1ee49f47b 100644 --- a/models/user/setting_keys.go +++ b/models/user/setting_keys.go @@ -14,4 +14,6 @@ const ( UserActivityPubPrivPem = "activitypub.priv_pem" // UserActivityPubPubPem is user's public key UserActivityPubPubPem = "activitypub.pub_pem" + // SettingsKeyTrackedTimeMaxUnit set how tracked time values are converted from seconds to sting + SettingsKeyTrackedTimeMaxUnit = "tracked_time.max_unit" ) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 235fd96b73d8f..d4fb4ef5db5d0 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -62,12 +62,13 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // time / number / format - "FileSize": base.FileSize, - "CountFmt": base.FormatNumberSI, - "TimeSince": timeutil.TimeSince, - "TimeSinceUnix": timeutil.TimeSinceUnix, - "DateTime": timeutil.DateTime, - "Sec2Time": util.SecToTime, + "FileSize": base.FileSize, + "CountFmt": base.FormatNumberSI, + "TimeSince": timeutil.TimeSince, + "TimeSinceUnix": timeutil.TimeSinceUnix, + "DateTime": timeutil.DateTime, + "Sec2Time": util.SecToTime, + "Sec2TrackedTime": util.Sec2TrackedTime, "LoadTimes": func(startTime time.Time) string { return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" }, diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index ad0fb1a68b4a7..67d6e0a6c682d 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -9,11 +9,11 @@ import ( ) // SecToTime converts an amount of seconds to a human-readable string. E.g. -// 66s -> 1 minute 6 seconds -// 52410s -> 14 hours 33 minutes -// 563418 -> 6 days 12 hours +// 66s -> 1 minute 6 seconds +// 52410s -> 14 hours 33 minutes +// 563418 -> 6 days 12 hours // 1563418 -> 2 weeks 4 days -// 3937125s -> 1 month 2 weeks +// 3937125s -> 1 month 2 weeks // 45677465s -> 1 year 6 months func SecToTime(durationVal any) string { duration, _ := ToInt64(durationVal) @@ -79,3 +79,36 @@ func formatTime(value int64, name, formattedTime string) string { return formattedTime } + +// Sec2TrackedTime converts an amount of seconds to a human-readable string. E.g. +// 66s -> 1 minute 6 seconds +// 52410s -> 14 hours 33 minutes +// 563418s -> 156 hours 30 minutes +// 1563418s -> 434 hours 16 minutes +// 3937125s -> 1093 hours 38 minutes +// 45677465s -> 12688 hours 11 minutes +func Sec2TrackedTime(durationVal any) string { + duration, _ := ToInt64(durationVal) + + formattedTime := "" + + // The following three variables are calculated without depending + // on the previous calculated variables. + hours := (duration / 3600) + minutes := (duration / 60) % 60 + seconds := duration % 60 + + // Extract only the relevant information of the time + // If the time is greater than a year, it makes no sense to display seconds. + switch { + case hours > 0: + formattedTime = formatTime(hours, "hour", formattedTime) + formattedTime = formatTime(minutes, "minute", formattedTime) + default: + formattedTime = formatTime(minutes, "minute", formattedTime) + formattedTime = formatTime(seconds, "second", formattedTime) + } + + // The formatTime() function always appends a space at the end. This will be trimmed + return strings.TrimRight(formattedTime, " ") +} diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go index 4d1213a52c05e..c9d553819c215 100644 --- a/modules/util/sec_to_time_test.go +++ b/modules/util/sec_to_time_test.go @@ -28,3 +28,23 @@ func TestSecToTime(t *testing.T) { assert.Equal(t, "11 months", SecToTime(year-25*day)) assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second)) } + +func TestSec2TrackedTime(t *testing.T) { + second := int64(1) + minute := 60 * second + hour := 60 * minute + day := 24 * hour + year := 365 * day + + assert.Equal(t, "1 minute 6 seconds", Sec2TrackedTime(minute+6*second)) + assert.Equal(t, "1 hour", Sec2TrackedTime(hour)) + assert.Equal(t, "1 hour", Sec2TrackedTime(hour+second)) + assert.Equal(t, "14 hours 33 minutes", Sec2TrackedTime(14*hour+33*minute+30*second)) + assert.Equal(t, "156 hours 30 minutes", Sec2TrackedTime(6*day+12*hour+30*minute+18*second)) + assert.Equal(t, "434 hours 16 minutes", Sec2TrackedTime((2*7+4)*day+2*hour+16*minute+58*second)) + assert.Equal(t, "672 hours", Sec2TrackedTime(4*7*day)) + assert.Equal(t, "696 hours", Sec2TrackedTime((4*7+1)*day)) + assert.Equal(t, "1093 hours 38 minutes", Sec2TrackedTime((6*7+3)*day+13*hour+38*minute+45*second)) + assert.Equal(t, "8160 hours", Sec2TrackedTime(year-25*day)) + assert.Equal(t, "12682 hours 11 minutes", Sec2TrackedTime(year+163*day+10*hour+11*minute+5*second)) +} diff --git a/package-lock.json b/package-lock.json index 5a49efee23e8c..2aab59bec1564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,6 @@ "monaco-editor": "0.44.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.2.12", - "pretty-ms": "8.0.0", "sortablejs": "1.15.0", "swagger-ui-dist": "5.10.0", "throttle-debounce": "5.0.0", @@ -8529,17 +8528,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-ms": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", - "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -9015,20 +9003,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-ms": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", - "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", - "dependencies": { - "parse-ms": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", diff --git a/package.json b/package.json index 4aee1bb04943e..d74122a56c484 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "monaco-editor": "0.44.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.2.12", - "pretty-ms": "8.0.0", "sortablejs": "1.15.0", "swagger-ui-dist": "5.10.0", "throttle-debounce": "5.0.0", diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index c9bf861b844b8..2a4283a2681f7 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -82,6 +82,6 @@ func DeleteTime(c *context.Context) { return } - c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) + c.Flash.Success(c.Tr("repo.issues.del_time_history", util.Sec2TrackedTime(t.Time))) c.Redirect(issue.Link()) } diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index d8331fef43f5a..3a04c78999e45 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -350,6 +350,12 @@ func Appearance(ctx *context.Context) { return forms.IsUserHiddenCommentTypeGroupChecked(commentTypeGroup, hiddenCommentTypes) } + ctx.Data["TrackedTimeMaxUnit"], err = user_model.GetUserSetting(ctx.Doer.ID, user_model.SettingsKeyTrackedTimeMaxUnit, "year") // TODO: make default set via system? + if err != nil { + ctx.ServerError("GetUserSetting", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsAppearance) } diff --git a/services/convert/issue.go b/services/convert/issue.go index 39d785e108332..71b5e3bc5390d 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) func ToIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue { @@ -180,7 +181,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), Seconds: sw.Seconds(), - Duration: sw.Duration(), + Duration: util.SecToTime(sw.Seconds()), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index effe4dcea9f8f..322a874822bbb 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -88,7 +88,7 @@ {{svg "octicon-issue-opened" 16 "gt-mr-3"}} {{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}} - {{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}} + {{if .ActiveStopwatch}}{{Sec2TrackedTime .ActiveStopwatch.Seconds}}{{end}}