Skip to content

Commit c697327

Browse files
committed
Add popup to select which mode, and configurable timeout
Keep backward compatible behavior, and add a context menu option to show the popup.
1 parent e4d5661 commit c697327

File tree

10 files changed

+821
-2590
lines changed

10 files changed

+821
-2590
lines changed

api-samples/power/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,22 @@ This extension demonstrates the `chrome.power` API by allowing users to override
44

55
## Overview
66

7-
The extension adds a popup that cycles different states when clicked. It will go though a mode that prevents the display from dimming or going to sleep, a mode that keeps the system awake but allows the screen to dim/go to sleep, and a mode that uses the system's default.
7+
The extension adds an icon that allows the user to choose different power management states when clicked:
8+
9+
- System Default
10+
- Screen stays awake
11+
- System stays awake, but screen can sleep
12+
13+
There is also a context menu popup where the user can also optionally specify an automatic timeout for the chosen state.
814

915
## Running this extension
1016

17+
Either install it from the Chrome Web Store:
18+
19+
- [Keep Awake Extension](https://chrome.google.com/webstore/detail/keep-awake/bijihlabcfdnabacffofojgmehjdielb)
20+
21+
Or load it as an upacked extension:
22+
1123
1. Clone this repository.
1224
2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked).
1325
3. Pin the extension and click the action button.

api-samples/power/_locales/en/messages.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,37 @@
1818
"systemTitle": {
1919
"message": "System will stay awake",
2020
"description": "Browser action title when preventing system sleep."
21+
},
22+
"untilText": {
23+
"message": " until: ",
24+
"description": "Suffix to append to above Titles to append an end time"
25+
},
26+
"autoDisableText": {
27+
"message": "Automatically disable after:",
28+
"description": "Text labelling a slider allowing setting a timeout for disabling the power saving state."
29+
},
30+
"autoDisableHoursSuffix": {
31+
"message": "h",
32+
"description": "Text to append after a number indicating a quantity of hours"
33+
},
34+
"disabledLabel": {
35+
"message": "Disabled",
36+
"description": "Button label to indicated keep awake is disabled."
37+
},
38+
"displayLabel": {
39+
"message": "Screen on",
40+
"description": "Button label to indicated keep awake is preventing screen-off."
41+
},
42+
"systemLabel": {
43+
"message": "System on",
44+
"description": "Button label to indicated keep awake is preventing system sleep."
45+
},
46+
"usePopupMenuTitle": {
47+
"message": "Always show State Popup",
48+
"description": "Checkbox item indicating that the popup menu should always be shown."
49+
},
50+
"openStateWindowMenuTitle": {
51+
"message": "Change State...",
52+
"description": "Menu item opening a popup window to change the state."
2153
}
2254
}

api-samples/power/background.js

Lines changed: 239 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,45 @@
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
42

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.
1316

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 };
1823

1924
/**
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
2228
*/
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);
3431
}
3532

3633
/**
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
3937
*/
40-
function setState(newState) {
41-
let imagePrefix = 'night';
42-
let title = '';
38+
function updateState(mode) {
39+
let imagePrefix;
40+
let title;
4341

44-
switch (newState) {
42+
switch (mode.state) {
4543
case StateEnum.DISABLED:
4644
chrome.power.releaseKeepAwake();
4745
imagePrefix = 'night';
@@ -58,42 +56,224 @@ function setState(newState) {
5856
title = chrome.i18n.getMessage('systemTitle');
5957
break;
6058
default:
61-
throw 'Invalid state "' + newState + '"';
59+
throw 'Invalid state "' + mode.state + '"';
6260
}
6361

64-
let items = {};
65-
items[STATE_KEY] = newState;
66-
chrome.storage.local.set(items);
67-
6862
chrome.action.setIcon({
6963
path: {
7064
19: 'images/' + imagePrefix + '-19.png',
7165
38: 'images/' + imagePrefix + '-38.png'
7266
}
7367
});
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+
}
7586
}
7687

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']
92172
});
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+
);
93209
});
94210

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);
99230
});
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

Comments
 (0)