mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 19:38:23 +00:00 
			
		
		
		
	Display current stopwatch in navbar (#14122)
* add notification about running stopwatch to header * serialize seconds, duration in stopwatches api * ajax update stopwatch i should get my testenv working locally... * new variant: hover dialog * noscript compatibility * js: live-update stopwatch time * js live update robustness
This commit is contained in:
		| @@ -7,7 +7,6 @@ package integrations | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| @@ -31,14 +30,11 @@ func TestAPIListStopWatches(t *testing.T) { | |||||||
| 	issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) | 	issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) | ||||||
| 	if assert.Len(t, apiWatches, 1) { | 	if assert.Len(t, apiWatches, 1) { | ||||||
| 		assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) | 		assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) | ||||||
| 		apiWatches[0].Created = time.Time{} | 		assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) | ||||||
| 		assert.EqualValues(t, api.StopWatch{ | 		assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) | ||||||
| 			Created:       time.Time{}, | 		assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) | ||||||
| 			IssueIndex:    issue.Index, | 		assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) | ||||||
| 			IssueTitle:    issue.Title, | 		assert.Greater(t, int64(apiWatches[0].Seconds), int64(0)) | ||||||
| 			RepoName:      repo.Name, |  | ||||||
| 			RepoOwnerName: repo.OwnerName, |  | ||||||
| 		}, *apiWatches[0]) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ func TestCreateIssueAttachment(t *testing.T) { | |||||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||||
| 	htmlDoc := NewHTMLParser(t, resp.Body) | 	htmlDoc := NewHTMLParser(t, resp.Body) | ||||||
|  |  | ||||||
| 	link, exists := htmlDoc.doc.Find("form").Attr("action") | 	link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action") | ||||||
| 	assert.True(t, exists, "The template has changed") | 	assert.True(t, exists, "The template has changed") | ||||||
|  |  | ||||||
| 	postData := map[string]string{ | 	postData := map[string]string{ | ||||||
|   | |||||||
| @@ -19,6 +19,16 @@ type Stopwatch struct { | |||||||
| 	CreatedUnix timeutil.TimeStamp `xorm:"created"` | 	CreatedUnix timeutil.TimeStamp `xorm:"created"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Seconds returns the amount of time passed since creation, based on local server time | ||||||
|  | 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 SecToTime(s.Seconds()) | ||||||
|  | } | ||||||
|  |  | ||||||
| func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { | func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { | ||||||
| 	sw = new(Stopwatch) | 	sw = new(Stopwatch) | ||||||
| 	exists, err = e. | 	exists, err = e. | ||||||
|   | |||||||
| @@ -147,6 +147,8 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) { | |||||||
|  |  | ||||||
| 		result = append(result, api.StopWatch{ | 		result = append(result, api.StopWatch{ | ||||||
| 			Created:       sw.CreatedUnix.AsTime(), | 			Created:       sw.CreatedUnix.AsTime(), | ||||||
|  | 			Seconds:       sw.Seconds(), | ||||||
|  | 			Duration:      sw.Duration(), | ||||||
| 			IssueIndex:    issue.Index, | 			IssueIndex:    issue.Index, | ||||||
| 			IssueTitle:    issue.Title, | 			IssueTitle:    issue.Title, | ||||||
| 			RepoOwnerName: repo.OwnerName, | 			RepoOwnerName: repo.OwnerName, | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import ( | |||||||
| type StopWatch struct { | type StopWatch struct { | ||||||
| 	// swagger:strfmt date-time | 	// swagger:strfmt date-time | ||||||
| 	Created       time.Time `json:"created"` | 	Created       time.Time `json:"created"` | ||||||
|  | 	Seconds       int64     `json:"seconds"` | ||||||
|  | 	Duration      string    `json:"duration"` | ||||||
| 	IssueIndex    int64     `json:"issue_index"` | 	IssueIndex    int64     `json:"issue_index"` | ||||||
| 	IssueTitle    string    `json:"issue_title"` | 	IssueTitle    string    `json:"issue_title"` | ||||||
| 	RepoOwnerName string    `json:"repo_owner_name"` | 	RepoOwnerName string    `json:"repo_owner_name"` | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ page = Page | |||||||
| template = Template | template = Template | ||||||
| language = Language | language = Language | ||||||
| notifications = Notifications | notifications = Notifications | ||||||
|  | active_stopwatch = Active Time Tracker | ||||||
| create_new = Create… | create_new = Create… | ||||||
| user_profile_and_more = Profile and Settings… | user_profile_and_more = Profile and Settings… | ||||||
| signed_in_as = Signed in as | signed_in_as = Signed in as | ||||||
| @@ -1139,13 +1140,15 @@ issues.lock.title = Lock conversation on this issue. | |||||||
| issues.unlock.title = Unlock conversation on this issue. | issues.unlock.title = Unlock conversation on this issue. | ||||||
| issues.comment_on_locked = You cannot comment on a locked issue. | issues.comment_on_locked = You cannot comment on a locked issue. | ||||||
| issues.tracker = Time Tracker | issues.tracker = Time Tracker | ||||||
| issues.start_tracking_short = Start | issues.start_tracking_short = Start Timer | ||||||
| issues.start_tracking = Start Time Tracking | issues.start_tracking = Start Time Tracking | ||||||
| issues.start_tracking_history = `started working %s` | issues.start_tracking_history = `started working %s` | ||||||
| issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed | issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed | ||||||
| issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!` | issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!` | ||||||
| issues.stop_tracking = Stop | issues.stop_tracking = Stop Timer | ||||||
| issues.stop_tracking_history = `stopped working %s` | issues.stop_tracking_history = `stopped working %s` | ||||||
|  | issues.cancel_tracking = Discard | ||||||
|  | issues.cancel_tracking_history = `cancelled time tracking %s` | ||||||
| issues.add_time = Manually Add Time | issues.add_time = Manually Add Time | ||||||
| issues.add_time_short = Add Time | issues.add_time_short = Add Time | ||||||
| issues.add_time_cancel = Cancel | issues.add_time_cancel = Cancel | ||||||
| @@ -1154,8 +1157,6 @@ issues.del_time_history= `deleted spent time %s` | |||||||
| issues.add_time_hours = Hours | issues.add_time_hours = Hours | ||||||
| issues.add_time_minutes = Minutes | issues.add_time_minutes = Minutes | ||||||
| issues.add_time_sum_to_small = No time was entered. | issues.add_time_sum_to_small = No time was entered. | ||||||
| issues.cancel_tracking = Cancel |  | ||||||
| issues.cancel_tracking_history = `cancelled time tracking %s` |  | ||||||
| issues.time_spent_total = Total Time Spent | issues.time_spent_total = Total Time Spent | ||||||
| issues.time_spent_from_all_authors = `Total Time Spent: %s` | issues.time_spent_from_all_authors = `Total Time Spent: %s` | ||||||
| issues.due_date = Due Date | issues.due_date = Due Date | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -5293,6 +5293,11 @@ | |||||||
|         "json-parse-better-errors": "^1.0.1" |         "json-parse-better-errors": "^1.0.1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "parse-ms": { | ||||||
|  |       "version": "2.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", | ||||||
|  |       "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" | ||||||
|  |     }, | ||||||
|     "parse-node-version": { |     "parse-node-version": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", | ||||||
| @@ -6702,6 +6707,14 @@ | |||||||
|       "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", |       "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", | ||||||
|       "optional": true |       "optional": true | ||||||
|     }, |     }, | ||||||
|  |     "pretty-ms": { | ||||||
|  |       "version": "7.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", | ||||||
|  |       "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", | ||||||
|  |       "requires": { | ||||||
|  |         "parse-ms": "^2.1.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "progress": { |     "progress": { | ||||||
|       "version": "2.0.3", |       "version": "2.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", |       "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ | |||||||
|     "monaco-editor": "0.21.2", |     "monaco-editor": "0.21.2", | ||||||
|     "monaco-editor-webpack-plugin": "2.1.0", |     "monaco-editor-webpack-plugin": "2.1.0", | ||||||
|     "postcss": "8.2.1", |     "postcss": "8.2.1", | ||||||
|  |     "pretty-ms": "7.0.1", | ||||||
|     "raw-loader": "4.0.2", |     "raw-loader": "4.0.2", | ||||||
|     "sortablejs": "1.12.0", |     "sortablejs": "1.12.0", | ||||||
|     "swagger-ui-dist": "3.38.0", |     "swagger-ui-dist": "3.38.0", | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package repo | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| @@ -61,3 +62,47 @@ func CancelStopwatch(c *context.Context) { | |||||||
| 	url := issue.HTMLURL() | 	url := issue.HTMLURL() | ||||||
| 	c.Redirect(url, http.StatusSeeOther) | 	c.Redirect(url, http.StatusSeeOther) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context | ||||||
|  | func GetActiveStopwatch(c *context.Context) { | ||||||
|  | 	if strings.HasPrefix(c.Req.URL.Path, "/api") { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !c.IsSigned { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, sw, err := models.HasUserStopwatch(c.User.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.ServerError("HasUserStopwatch", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if sw == nil || sw.ID == 0 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	issue, err := models.GetIssueByID(sw.IssueID) | ||||||
|  | 	if err != nil || issue == nil { | ||||||
|  | 		c.ServerError("GetIssueByID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if err = issue.LoadRepo(); err != nil { | ||||||
|  | 		c.ServerError("LoadRepo", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c.Data["ActiveStopwatch"] = StopwatchTmplInfo{ | ||||||
|  | 		issue.Repo.FullName(), | ||||||
|  | 		issue.Index, | ||||||
|  | 		sw.Seconds() + 1, // ensure time is never zero in ui | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StopwatchTmplInfo is a view on a stopwatch specifically for template rendering | ||||||
|  | type StopwatchTmplInfo struct { | ||||||
|  | 	RepoSlug   string | ||||||
|  | 	IssueIndex int64 | ||||||
|  | 	Seconds    int64 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -176,6 +176,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	m.Use(user.GetNotificationCount) | 	m.Use(user.GetNotificationCount) | ||||||
|  | 	m.Use(repo.GetActiveStopwatch) | ||||||
| 	m.Use(func(ctx *context.Context) { | 	m.Use(func(ctx *context.Context) { | ||||||
| 		ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled() | 		ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled() | ||||||
| 		ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled() | 		ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled() | ||||||
|   | |||||||
| @@ -67,6 +67,44 @@ | |||||||
| 		</div> | 		</div> | ||||||
| 	{{else if .IsSigned}} | 	{{else if .IsSigned}} | ||||||
| 		<div class="right stackable menu"> | 		<div class="right stackable menu"> | ||||||
|  | 			{{$issueURL := Printf "%s/%s/issues/%d" AppSubUrl .ActiveStopwatch.RepoSlug .ActiveStopwatch.IssueIndex}} | ||||||
|  | 			<a class="active-stopwatch-trigger item ui label {{if not .ActiveStopwatch}}hidden{{end}}" href="{{$issueURL}}"> | ||||||
|  | 				<span class="text"> | ||||||
|  | 					<span class="fitted item"> | ||||||
|  | 						{{svg "octicon-stopwatch"}} | ||||||
|  | 						<span class="red" style="position:absolute; right:-0.6em; top:-0.6em;">{{svg "octicon-dot-fill"}}</span> | ||||||
|  | 					</span> | ||||||
|  | 					<span class="sr-mobile-only">{{.i18n.Tr "active_stopwatch"}}</span> | ||||||
|  | 				</span> | ||||||
|  | 			</a> | ||||||
|  | 			<div class="ui popup very wide"> | ||||||
|  | 				<div class="df ac"> | ||||||
|  | 					<a class="stopwatch-link df ac" href="{{$issueURL}}"> | ||||||
|  | 						{{svg "octicon-issue-opened"}} | ||||||
|  | 						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span> | ||||||
|  | 						<span class="ui label blue stopwatch-time my-0 mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}"> | ||||||
|  | 							{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}} | ||||||
|  | 						</span> | ||||||
|  | 					</a> | ||||||
|  | 					<form class="stopwatch-commit" method="POST" action="{{$issueURL}}/times/stopwatch/toggle"> | ||||||
|  | 						{{.CsrfTokenHtml}} | ||||||
|  | 						<button | ||||||
|  | 							class="ui button mini compact basic icon fitted poping up" | ||||||
|  | 							data-content="{{.i18n.Tr "repo.issues.stop_tracking"}}" | ||||||
|  | 							data-position="top right" data-variation="small inverted" | ||||||
|  | 						>{{svg "octicon-square-fill"}}</button> | ||||||
|  | 					</form> | ||||||
|  | 					<form class="stopwatch-cancel" method="POST" action="{{$issueURL}}/times/stopwatch/cancel"> | ||||||
|  | 						{{.CsrfTokenHtml}} | ||||||
|  | 						<button | ||||||
|  | 							class="ui button mini compact basic icon fitted poping up" | ||||||
|  | 							data-content="{{.i18n.Tr "repo.issues.cancel_tracking"}}" | ||||||
|  | 							data-position="top right" data-variation="small inverted" | ||||||
|  | 						>{{svg "octicon-trashcan"}}</button> | ||||||
|  | 					</form> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
| 			<a href="{{AppSubUrl}}/notifications" class="item poping up" data-content='{{.i18n.Tr "notifications"}}' data-variation="tiny inverted"> | 			<a href="{{AppSubUrl}}/notifications" class="item poping up" data-content='{{.i18n.Tr "notifications"}}' data-variation="tiny inverted"> | ||||||
| 				<span class="text"> | 				<span class="text"> | ||||||
| 					<span class="fitted">{{svg "octicon-bell"}}</span> | 					<span class="fitted">{{svg "octicon-bell"}}</span> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <form class="ui comment form stackable grid" action="{{.Link}}" method="post"> | <form class="ui comment form stackable grid" id="new-issue" action="{{.Link}}" method="post"> | ||||||
| 	{{.CsrfTokenHtml}} | 	{{.CsrfTokenHtml}} | ||||||
| 	{{if .Flash}} | 	{{if .Flash}} | ||||||
| 		<div class="sixteen wide column"> | 		<div class="sixteen wide column"> | ||||||
|   | |||||||
| @@ -15473,6 +15473,10 @@ | |||||||
|           "format": "date-time", |           "format": "date-time", | ||||||
|           "x-go-name": "Created" |           "x-go-name": "Created" | ||||||
|         }, |         }, | ||||||
|  |         "duration": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Duration" | ||||||
|  |         }, | ||||||
|         "issue_index": { |         "issue_index": { | ||||||
|           "type": "integer", |           "type": "integer", | ||||||
|           "format": "int64", |           "format": "int64", | ||||||
| @@ -15489,6 +15493,11 @@ | |||||||
|         "repo_owner_name": { |         "repo_owner_name": { | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "RepoOwnerName" |           "x-go-name": "RepoOwnerName" | ||||||
|  |         }, | ||||||
|  |         "seconds": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "Seconds" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|   | |||||||
							
								
								
									
										91
									
								
								web_src/js/features/stopwatch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								web_src/js/features/stopwatch.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | import prettyMilliseconds from 'pretty-ms'; | ||||||
|  | const {AppSubUrl, csrf, NotificationSettings} = window.config; | ||||||
|  |  | ||||||
|  | let updateTimeInterval = null; // holds setInterval id when active | ||||||
|  |  | ||||||
|  | export async function initStopwatch() { | ||||||
|  |   const stopwatchEl = $('.active-stopwatch-trigger'); | ||||||
|  |  | ||||||
|  |   stopwatchEl.removeAttr('href'); // intended for noscript mode only | ||||||
|  |   stopwatchEl.popup({ | ||||||
|  |     position: 'bottom right', | ||||||
|  |     hoverable: true, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // form handlers | ||||||
|  |   $('form > button', stopwatchEl).on('click', function () { | ||||||
|  |     $(this).parent().trigger('submit'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   if (!stopwatchEl || NotificationSettings.MinTimeout <= 0) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const fn = (timeout) => { | ||||||
|  |     setTimeout(async () => { | ||||||
|  |       await updateStopwatchWithCallback(fn, timeout); | ||||||
|  |     }, timeout); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   fn(NotificationSettings.MinTimeout); | ||||||
|  |  | ||||||
|  |   const currSeconds = $('.stopwatch-time').data('seconds'); | ||||||
|  |   if (currSeconds) { | ||||||
|  |     updateTimeInterval = updateStopwatchTime(currSeconds); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateStopwatchWithCallback(callback, timeout) { | ||||||
|  |   const isSet = await updateStopwatch(); | ||||||
|  |  | ||||||
|  |   if (!isSet) { | ||||||
|  |     timeout = NotificationSettings.MinTimeout; | ||||||
|  |   } else if (timeout < NotificationSettings.MaxTimeout) { | ||||||
|  |     timeout += NotificationSettings.TimeoutStep; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   callback(timeout); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateStopwatch() { | ||||||
|  |   const data = await $.ajax({ | ||||||
|  |     type: 'GET', | ||||||
|  |     url: `${AppSubUrl}/api/v1/user/stopwatches`, | ||||||
|  |     headers: {'X-Csrf-Token': csrf}, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   if (updateTimeInterval) { | ||||||
|  |     clearInterval(updateTimeInterval); | ||||||
|  |     updateTimeInterval = null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const watch = data[0]; | ||||||
|  |   const btnEl = $('.active-stopwatch-trigger'); | ||||||
|  |   if (!watch) { | ||||||
|  |     btnEl.addClass('hidden'); | ||||||
|  |   } else { | ||||||
|  |     const {repo_owner_name, repo_name, issue_index, seconds} = watch; | ||||||
|  |     const issueUrl = `${AppSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`; | ||||||
|  |     $('.stopwatch-link').attr('href', issueUrl); | ||||||
|  |     $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`); | ||||||
|  |     $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`); | ||||||
|  |     $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`); | ||||||
|  |     $('.stopwatch-time').text(prettyMilliseconds(seconds * 1000)); | ||||||
|  |     updateStopwatchTime(seconds); | ||||||
|  |     btnEl.removeClass('hidden'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return !!data.length; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateStopwatchTime(seconds) { | ||||||
|  |   const secs = parseInt(seconds); | ||||||
|  |   if (!Number.isFinite(secs)) return; | ||||||
|  |  | ||||||
|  |   const start = Date.now(); | ||||||
|  |   updateTimeInterval = setInterval(() => { | ||||||
|  |     const delta = Date.now() - start; | ||||||
|  |     const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true}); | ||||||
|  |     $('.stopwatch-time').text(dur); | ||||||
|  |   }, 1000); | ||||||
|  | } | ||||||
| @@ -22,6 +22,7 @@ import createDropzone from './features/dropzone.js'; | |||||||
| import initTableSort from './features/tablesort.js'; | import initTableSort from './features/tablesort.js'; | ||||||
| import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | ||||||
| import {initNotificationsTable, initNotificationCount} from './features/notification.js'; | import {initNotificationsTable, initNotificationCount} from './features/notification.js'; | ||||||
|  | import {initStopwatch} from './features/stopwatch.js'; | ||||||
| import {createCodeEditor, createMonaco} from './features/codeeditor.js'; | import {createCodeEditor, createMonaco} from './features/codeeditor.js'; | ||||||
| import {svg, svgs} from './svg.js'; | import {svg, svgs} from './svg.js'; | ||||||
| import {stripTags} from './utils.js'; | import {stripTags} from './utils.js'; | ||||||
| @@ -2626,6 +2627,7 @@ $(document).ready(async () => { | |||||||
|     initProject(), |     initProject(), | ||||||
|     initServiceWorker(), |     initServiceWorker(), | ||||||
|     initNotificationCount(), |     initNotificationCount(), | ||||||
|  |     initStopwatch(), | ||||||
|     renderMarkdownContent(), |     renderMarkdownContent(), | ||||||
|     initGithook(), |     initGithook(), | ||||||
|   ]); |   ]); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user