Implement options page

This commit is contained in:
hensm
2018-07-21 01:38:07 +01:00
parent 9f7bf780e5
commit c4ed13fb0b
8 changed files with 284 additions and 14 deletions

View File

@@ -16,4 +16,20 @@
, "context_media_cast": {
"message": "Cast..."
}
, "options_category_localMedia": {
"message": "Local media casting"
}
, "options_category_localMedia_description": {
"message": "HTTP server started by the bridge app to stream local media files to the cast receiver."
}
, "options_option_localMediaEnabled": {
"message": "Enabled"
}
, "options_option_localMediaServerPort": {
"message": "HTTP server port"
}
, "options_submit": {
"message": "Submit"
}
}

View File

@@ -5,6 +5,46 @@ import messageRouter from "./messageRouter";
const _ = browser.i18n.getMessage;
browser.runtime.onInstalled.addListener(async details => {
const initialOptions = {
option_localMediaEnabled: true
, option_localMediaServerPort: 9555
};
switch (details.reason) {
// Set initial options
case "install":
browser.storage.sync.set({
options: initialOptions
});
break;
// Set newly added options
case "update":
const { options } = await browser.storage.sync.get("options");
const newOptions = {};
// Find options not already in storage
for (const [ key, val ] of Object.entries(initialOptions)) {
if (!options.hasOwnProperty(key)) {
newOptions[key] = val;
}
}
// Update storage with default values of new options
browser.storage.sync.set({
options: {
...options
, ...newOptions
}
});
break;
}
});
// Google-hosted API loader script
const SENDER_SCRIPT_URL =
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";

View File

@@ -9,6 +9,7 @@
, "strict_min_version": "57.0"
}
}
, "browser_action": {
"default_popup": "popup/index.html"
}
@@ -16,17 +17,25 @@
, "background": {
"scripts": [ "main.js" ]
}
, "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
, "default_locale": "en"
, "manifest_version": 2
, "options_ui": {
"page": "options/index.html"
}
, "permissions": [
"menus"
, "nativeMessaging"
, "storage"
, "webNavigation"
, "webRequest"
, "webRequestBlocking"
, "<all_urls>"
]
, "web_accessible_resources": [
"shim/bundle.js"
, "dm.js"

View File

@@ -1,5 +1,7 @@
"use strict";
let options;
let chrome;
let logMessage;
@@ -122,10 +124,7 @@ async function onRequestSessionSuccess (session_) {
session = session_;
let mediaUrl = new URL(srcUrl);
// TODO: Get from extension settings
const port = 9555;
const port = options.option_localMediaServerPort;
if (isLocalFile) {
await new Promise((resolve, reject) => {
@@ -284,7 +283,7 @@ function onMediaSeekError (err) {
}
window.__onGCastApiAvailable = function (loaded, errorInfo) {
window.__onGCastApiAvailable = async function (loaded, errorInfo) {
if (!loaded) {
logMessage("__onGCastApiAvailable error");
return;
@@ -295,6 +294,15 @@ window.__onGCastApiAvailable = function (loaded, errorInfo) {
logMessage("__onGCastApiAvailable success");
options = (await browser.storage.sync.get("options")).options;
if (isLocalFile && !options.option_localMediaEnabled) {
logMessage("Local media casting not enabled");
return;
}
const sessionRequest = new chrome.cast.SessionRequest(
chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);

44
ext/src/options/index.css Normal file
View File

@@ -0,0 +1,44 @@
.category {
display: grid;
grid-template-columns: min-content min-content;
grid-column-gap: 20px;
grid-row-gap: 5px;
}
.category-name {}
.category-description {
color: graytext;
grid-column: span 2;
}
.option {
display: contents;
}
.option-label {
text-align: right;
white-space: nowrap;
display: inline-block;
}
.option > input {
align-self: center;
justify-self: flex-start;
}
#form {
display: flex;
flex-direction: column;
}
#buttons {
align-self: flex-end;
margin-block-start: 5px;
}
#buttons > :not(:last-child) {
margin-inline-end: 5px;
}
*:invalid {
box-shadow: 0 0 1.5px 1px red;
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
<script src="bundle.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

141
ext/src/options/index.jsx Normal file
View File

@@ -0,0 +1,141 @@
"use strict";
import React, { Component } from "react";
import ReactDOM from "react-dom";
const _ = browser.i18n.getMessage;
class OptionsApp extends Component {
constructor (props) {
super(props);
this.state = {
isFormValid: true
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleFormChange = this.handleFormChange.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
/**
* Set stored option values to current state
*/
setStorage () {
return browser.storage.sync.set({
options: {
option_localMediaEnabled: this.state.option_localMediaEnabled
, option_localMediaServerPort: this.state.option_localMediaServerPort
}
});
}
/**
* Get current options state from storage and set initial
*/
async componentDidMount () {
const { options } = await browser.storage.sync.get("options");
if (options) {
this.setState({
...options
, isFormValid: this.form.checkValidity()
});
} else {
try {
await this.setStorage();
} catch (err) {
// TODO
}
}
}
async handleFormSubmit (ev) {
ev.preventDefault();
this.form.reportValidity();
try {
await this.setStorage();
} catch (err) {}
}
handleFormChange () {
this.setState({
isFormValid: this.form.checkValidity()
});
}
handleInputChange (ev) {
const val = do {
if (ev.target.type === "checkbox") {
ev.target.checked;
} else if (ev.target.type === "number") {
parseInt(ev.target.value);
} else {
ev.target.value;
}
};
console.log(ev.target.name);
this.setState({
[ ev.target.name ]: val
});
}
render () {
return (
<form id="form" ref={ form => { this.form = form; }}
onSubmit={ this.handleFormSubmit }
onChange={ this.handleFormChange }>
<fieldset className="category">
<legend className="category-name">
{ _("options_category_localMedia") }
</legend>
<p className="category-description">
{ _("options_category_localMedia_description") }
</p>
<label className="option">
<div className="option-label">
{ _("options_option_localMediaEnabled") }
</div>
<input name="option_localMediaEnabled"
type="checkbox"
checked={ this.state.option_localMediaEnabled }
onChange={ this.handleInputChange } />
</label>
<label className="option">
<div className="option-label">
{ _("options_option_localMediaServerPort") }
</div>
<input name="option_localMediaServerPort"
type="number"
required
min="1025"
max="65535"
value={ this.state.option_localMediaServerPort }
onChange={ this.handleInputChange } />
</label>
</fieldset>
<div id="buttons">
<button type="submit"
disabled={ !this.state.isFormValid }>
{ _("options_submit") }
</button>
</div>
</form>
);
}
}
ReactDOM.render(
<OptionsApp />
, document.querySelector("#root"));

View File

@@ -10,14 +10,15 @@ const output_path = path.resolve(__dirname, "../dist/unpacked");
module.exports = {
entry: {
"main" : `${include_path}/main.js`
, "popup/bundle" : `${include_path}/popup/index.js`
, "shim/bundle" : `${include_path}/shim/index.js`
, "content" : `${include_path}/content.js`
, "contentSetup" : `${include_path}/contentSetup.js`
, "mediaCast" : `${include_path}/mediaCast.js`
, "mirroringCast" : `${include_path}/mirroringCast.js`
, "messageRouter" : `${include_path}/messageRouter.js`
"main" : `${include_path}/main.js`
, "popup/bundle" : `${include_path}/popup/index.js`
, "options/bundle" : `${include_path}/options/index.jsx`
, "shim/bundle" : `${include_path}/shim/index.js`
, "content" : `${include_path}/content.js`
, "contentSetup" : `${include_path}/contentSetup.js`
, "mediaCast" : `${include_path}/mediaCast.js`
, "mirroringCast" : `${include_path}/mirroringCast.js`
, "messageRouter" : `${include_path}/messageRouter.js`
}
, output: {
filename: "[name].js"
@@ -40,7 +41,7 @@ module.exports = {
, module: {
loaders: [
{
test: /\.js/
test: /\.jsx?$/
, include: `${include_path}`
, loader: "babel-loader"
, options: {