mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Implement options page
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
44
ext/src/options/index.css
Normal 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;
|
||||
}
|
||||
11
ext/src/options/index.html
Normal file
11
ext/src/options/index.html
Normal 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
141
ext/src/options/index.jsx
Normal 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"));
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user