1
- // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2
- // Use of this source code is governed by a BSD-style license that can be
3
- // found in the LICENSE file.
1
+ // @ts -check
4
2
5
- /**
6
- * States that the extension can be in.
7
- */
8
- let StateEnum = {
9
- DISABLED : 'disabled' ,
10
- DISPLAY : 'display' ,
11
- SYSTEM : 'system'
12
- } ;
3
+ // Copyright 2024 Google LLC
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // https://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
13
16
14
- /**
15
- * Key used for storing the current state in {localStorage}.
16
- */
17
- let STATE_KEY = 'state' ;
17
+ import { StateEnum , getSavedMode , verifyMode } from './common.js' ;
18
+ /** @typedef {import('./common.js').KeepAwakeMode } KeepAwakeMode */
19
+
20
+ const ALARM_NAME = 'keepAwakeTimeout' ;
21
+ const HOUR_TO_MILLIS = 60 * 60 * 1000 ;
22
+ const USE_POPUP_DEFAULT = { usePopup : true } ;
18
23
19
24
/**
20
- * Loads the locally-saved state asynchronously.
21
- * @param {function } callback Callback invoked with the loaded {StateEnum}.
25
+ * Simple timestamped log function
26
+ * @param {string } msg
27
+ * @param {...* } args
22
28
*/
23
- function loadSavedState ( callback ) {
24
- chrome . storage . local . get ( STATE_KEY , function ( items ) {
25
- let savedState = items [ STATE_KEY ] ;
26
- for ( let key in StateEnum ) {
27
- if ( savedState == StateEnum [ key ] ) {
28
- callback ( savedState ) ;
29
- return ;
30
- }
31
- }
32
- callback ( StateEnum . DISABLED ) ;
33
- } ) ;
29
+ function log ( msg , ...args ) {
30
+ console . log ( new Date ( ) . toLocaleTimeString ( 'short' ) + ' ' + msg , ...args ) ;
34
31
}
35
32
36
33
/**
37
- * Switches to a new state.
38
- * @param {string } newState New {StateEnum} to use.
34
+ * Set keep awake mode, and update icon.
35
+ *
36
+ * @param {KeepAwakeMode } mode
39
37
*/
40
- function setState ( newState ) {
41
- let imagePrefix = 'night' ;
42
- let title = '' ;
38
+ function updateState ( mode ) {
39
+ let imagePrefix ;
40
+ let title ;
43
41
44
- switch ( newState ) {
42
+ switch ( mode . state ) {
45
43
case StateEnum . DISABLED :
46
44
chrome . power . releaseKeepAwake ( ) ;
47
45
imagePrefix = 'night' ;
@@ -58,42 +56,224 @@ function setState(newState) {
58
56
title = chrome . i18n . getMessage ( 'systemTitle' ) ;
59
57
break ;
60
58
default :
61
- throw 'Invalid state "' + newState + '"' ;
59
+ throw 'Invalid state "' + mode . state + '"' ;
62
60
}
63
61
64
- let items = { } ;
65
- items [ STATE_KEY ] = newState ;
66
- chrome . storage . local . set ( items ) ;
67
-
68
62
chrome . action . setIcon ( {
69
63
path : {
70
64
19 : 'images/' + imagePrefix + '-19.png' ,
71
65
38 : 'images/' + imagePrefix + '-38.png'
72
66
}
73
67
} ) ;
74
- chrome . action . setTitle ( { title : title } ) ;
68
+
69
+ if ( mode . endMillis && mode . state != StateEnum . DISABLED ) {
70
+ // a timeout is specified, update the badge and the title text
71
+ let hoursLeft = Math . ceil ( ( mode . endMillis - Date . now ( ) ) / HOUR_TO_MILLIS ) ;
72
+ chrome . action . setBadgeText ( { text : `${ hoursLeft } h` } ) ;
73
+ const endDate = new Date ( mode . endMillis ) ;
74
+ chrome . action . setTitle ( {
75
+ title : `${ title } ${ chrome . i18n . getMessage ( 'untilText' ) } ${ endDate . toLocaleTimeString ( 'short' ) } `
76
+ } ) ;
77
+ log (
78
+ `mode = ${ mode . state } for the next ${ hoursLeft } hrs until ${ endDate . toLocaleTimeString ( 'short' ) } `
79
+ ) ;
80
+ } else {
81
+ // No timeout.
82
+ chrome . action . setBadgeText ( { text : '' } ) ;
83
+ chrome . action . setTitle ( { title : title } ) ;
84
+ log ( `mode = ${ mode . state } ` ) ;
85
+ }
75
86
}
76
87
77
- chrome . action . onClicked . addListener ( function ( ) {
78
- loadSavedState ( function ( state ) {
79
- switch ( state ) {
80
- case StateEnum . DISABLED :
81
- setState ( StateEnum . DISPLAY ) ;
82
- break ;
83
- case StateEnum . DISPLAY :
84
- setState ( StateEnum . SYSTEM ) ;
85
- break ;
86
- case StateEnum . SYSTEM :
87
- setState ( StateEnum . DISABLED ) ;
88
- break ;
89
- default :
90
- throw 'Invalid state "' + state + '"' ;
91
- }
88
+ /**
89
+ * Apply a new KeepAwake mode.
90
+ *
91
+ * @param {KeepAwakeMode } newMode
92
+ */
93
+ async function setNewMode ( newMode ) {
94
+ // Clear any old alarms
95
+ await chrome . alarms . clearAll ( ) ;
96
+
97
+ // is a timeout required?
98
+ if ( newMode . defaultDurationHrs && newMode . state !== StateEnum . DISABLED ) {
99
+ // Set an alarm every 60 mins.
100
+ chrome . alarms . create ( ALARM_NAME , {
101
+ delayInMinutes : 60 ,
102
+ periodInMinutes : 60
103
+ } ) ;
104
+ newMode . endMillis =
105
+ Date . now ( ) + newMode . defaultDurationHrs * HOUR_TO_MILLIS ;
106
+ } else {
107
+ newMode . endMillis = null ;
108
+ }
109
+
110
+ // Store the new mode.
111
+ chrome . storage . local . set ( newMode ) ;
112
+ updateState ( newMode ) ;
113
+ }
114
+
115
+ /**
116
+ * Check to see if any set timeout has expired, and if so, reset the mode.
117
+ */
118
+ async function checkTimeoutAndUpdateDisplay ( ) {
119
+ const mode = await getSavedMode ( ) ;
120
+ if ( mode . endMillis && mode . endMillis < Date . now ( ) ) {
121
+ log ( `timer expired` ) ;
122
+ // reset state to disabled
123
+ mode . state = StateEnum . DISABLED ;
124
+ mode . endMillis = null ;
125
+ setNewMode ( mode ) ;
126
+ } else {
127
+ updateState ( mode ) ;
128
+ }
129
+ }
130
+
131
+ async function recreateAlarms ( ) {
132
+ const mode = await getSavedMode ( ) ;
133
+ await chrome . alarms . clearAll ( ) ;
134
+ if (
135
+ mode . state !== StateEnum . DISABLED &&
136
+ mode . endMillis &&
137
+ mode . endMillis > Date . now ( )
138
+ ) {
139
+ // previous timeout has not yet expired...
140
+ // restart alarm to be triggered at the next 1hr of the timeout
141
+ const remainingMillis = mode . endMillis - Date . now ( ) ;
142
+ const millisToNextHour = remainingMillis % HOUR_TO_MILLIS ;
143
+
144
+ log (
145
+ `recreating alarm, next = ${ new Date ( Date . now ( ) + millisToNextHour ) . toLocaleTimeString ( ) } `
146
+ ) ;
147
+ chrome . alarms . create ( ALARM_NAME , {
148
+ delayInMinutes : millisToNextHour / 60_000 ,
149
+ periodInMinutes : 60
150
+ } ) ;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Creates the context menu buttons on the action icon.
156
+ */
157
+ async function reCreateContextMenus ( ) {
158
+ chrome . contextMenus . removeAll ( ) ;
159
+
160
+ chrome . contextMenus . create ( {
161
+ type : 'normal' ,
162
+ id : 'openStateMenu' ,
163
+ title : chrome . i18n . getMessage ( 'openStateWindowMenuTitle' ) ,
164
+ contexts : [ 'action' ]
165
+ } ) ;
166
+ chrome . contextMenus . create ( {
167
+ type : 'checkbox' ,
168
+ checked : USE_POPUP_DEFAULT . usePopup ,
169
+ id : 'usePopupMenu' ,
170
+ title : chrome . i18n . getMessage ( 'usePopupMenuTitle' ) ,
171
+ contexts : [ 'action' ]
92
172
} ) ;
173
+
174
+ updateUsePopupMenu (
175
+ ( await chrome . storage . sync . get ( USE_POPUP_DEFAULT ) ) . usePopup
176
+ ) ;
177
+ }
178
+
179
+ /**
180
+ * Sets whether or not to use the popup menu when clicking on the action icon.
181
+ *
182
+ * @param {boolean } usePopup
183
+ */
184
+ function updateUsePopupMenu ( usePopup ) {
185
+ chrome . contextMenus . update ( 'usePopupMenu' , { checked : usePopup } ) ;
186
+ if ( usePopup ) {
187
+ chrome . action . setPopup ( { popup : 'popup.html' } ) ;
188
+ } else {
189
+ chrome . action . setPopup ( { popup : '' } ) ;
190
+ }
191
+ }
192
+
193
+ // Handle messages received from the popup.
194
+ chrome . runtime . onMessage . addListener ( function ( request , _ , sendResponse ) {
195
+ log (
196
+ `Got message from popup: state: %s, duration: %d` ,
197
+ request . state ,
198
+ request . duration
199
+ ) ;
200
+ sendResponse ( { } ) ;
201
+
202
+ setNewMode (
203
+ verifyMode ( {
204
+ state : request . state ,
205
+ defaultDurationHrs : request . duration ,
206
+ endMillis : null
207
+ } )
208
+ ) ;
93
209
} ) ;
94
210
95
- chrome . runtime . onStartup . addListener ( function ( ) {
96
- loadSavedState ( function ( state ) {
97
- setState ( state ) ;
98
- } ) ;
211
+ // Handle action clicks - rotates the mode to the next mode.
212
+ chrome . action . onClicked . addListener ( async ( ) => {
213
+ log ( `Action clicked` ) ;
214
+
215
+ const mode = await getSavedMode ( ) ;
216
+ switch ( mode . state ) {
217
+ case StateEnum . DISABLED :
218
+ mode . state = StateEnum . DISPLAY ;
219
+ break ;
220
+ case StateEnum . DISPLAY :
221
+ mode . state = StateEnum . SYSTEM ;
222
+ break ;
223
+ case StateEnum . SYSTEM :
224
+ mode . state = StateEnum . DISABLED ;
225
+ break ;
226
+ default :
227
+ throw 'Invalid state "' + mode . state + '"' ;
228
+ }
229
+ setNewMode ( mode ) ;
99
230
} ) ;
231
+
232
+ // Handle context menu clicks
233
+ chrome . contextMenus . onClicked . addListener ( async ( e ) => {
234
+ switch ( e . menuItemId ) {
235
+ case 'openStateMenu' :
236
+ chrome . windows . create ( {
237
+ focused : true ,
238
+ height : 220 ,
239
+ width : 240 ,
240
+ type : 'popup' ,
241
+ url : './popup.html'
242
+ } ) ;
243
+ break ;
244
+
245
+ case 'usePopupMenu' :
246
+ // e.checked is new state, after being clicked.
247
+ chrome . storage . sync . set ( { usePopup : ! ! e . checked } ) ;
248
+ updateUsePopupMenu ( ! ! e . checked ) ;
249
+ break ;
250
+ }
251
+ } ) ;
252
+
253
+ // Whenever the alarm is triggered check the timeout and update the icon.
254
+ chrome . alarms . onAlarm . addListener ( ( ) => {
255
+ log ( 'alarm!' ) ;
256
+ checkTimeoutAndUpdateDisplay ( ) ;
257
+ } ) ;
258
+
259
+ chrome . runtime . onStartup . addListener ( async ( ) => {
260
+ log ( 'onStartup' ) ;
261
+ recreateAlarms ( ) ;
262
+ reCreateContextMenus ( ) ;
263
+ } ) ;
264
+
265
+ chrome . runtime . onInstalled . addListener ( async ( ) => {
266
+ log ( 'onInstalled' ) ;
267
+ recreateAlarms ( ) ;
268
+ reCreateContextMenus ( ) ;
269
+ } ) ;
270
+
271
+ chrome . storage . sync . onChanged . addListener ( ( changes ) => {
272
+ if ( changes . usePopup != null ) {
273
+ log ( 'usePopup changed to %s' , changes . usePopup . newValue ) ;
274
+ updateUsePopupMenu ( ! ! changes . usePopup . newValue ) ;
275
+ }
276
+ } ) ;
277
+
278
+ // Whenever the service worker starts up, check the timeout and update the state
279
+ checkTimeoutAndUpdateDisplay ( ) ;
0 commit comments