diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index ed939ea..9377bb7 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -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" + } } \ No newline at end of file diff --git a/ext/src/main.js b/ext/src/main.js index 84034af..0d3e027 100755 --- a/ext/src/main.js +++ b/ext/src/main.js @@ -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"; diff --git a/ext/src/manifest.json b/ext/src/manifest.json index b483e66..408dbe8 100755 --- a/ext/src/manifest.json +++ b/ext/src/manifest.json @@ -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" , "" ] + , "web_accessible_resources": [ "shim/bundle.js" , "dm.js" diff --git a/ext/src/mediaCast.js b/ext/src/mediaCast.js index e214d53..df4164a 100644 --- a/ext/src/mediaCast.js +++ b/ext/src/mediaCast.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); diff --git a/ext/src/options/index.css b/ext/src/options/index.css new file mode 100644 index 0000000..1bd3c32 --- /dev/null +++ b/ext/src/options/index.css @@ -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; +} diff --git a/ext/src/options/index.html b/ext/src/options/index.html new file mode 100644 index 0000000..4754571 --- /dev/null +++ b/ext/src/options/index.html @@ -0,0 +1,11 @@ + + + + + + + + +
+ + diff --git a/ext/src/options/index.jsx b/ext/src/options/index.jsx new file mode 100644 index 0000000..ee3e973 --- /dev/null +++ b/ext/src/options/index.jsx @@ -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 ( +
{ this.form = form; }} + onSubmit={ this.handleFormSubmit } + onChange={ this.handleFormChange }> +
+ + { _("options_category_localMedia") } + +

+ { _("options_category_localMedia_description") } +

+ + + + +
+ +
+ +
+
+ ); + } +} + + +ReactDOM.render( + + , document.querySelector("#root")); diff --git a/ext/webpack.config.js b/ext/webpack.config.js index c20f1bc..5768c1f 100755 --- a/ext/webpack.config.js +++ b/ext/webpack.config.js @@ -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: {