Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
Merge pull request #667 from brave/computed-theme-color
Browse files Browse the repository at this point in the history
Computed theme color
  • Loading branch information
bbondy committed Feb 8, 2016
2 parents 3d96142 + 994034d commit c28e6ea
Show file tree
Hide file tree
Showing 19 changed files with 144 additions and 105 deletions.
1 change: 1 addition & 0 deletions app/adBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const startAdBlocking = () => {
Filtering.registerFilteringCB(details => {
const firstPartyUrl = URL.parse(details.firstPartyUrl)
const shouldBlock = firstPartyUrl.protocol &&
details.resourceType !== 'mainFrame' &&
firstPartyUrl.protocol.startsWith('http') &&
mapFilterType[details.resourceType] !== undefined &&
adblock.matches(details.url, mapFilterType[details.resourceType], firstPartyUrl.host)
Expand Down
58 changes: 57 additions & 1 deletion app/content/webviewPreload.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

// hide this from the rest of the page
(function () {
'use strict'

var ipcRenderer = process.binding.v8_util.getHiddenValue(this, 'ipc')

/**
Expand Down Expand Up @@ -241,12 +243,66 @@
}
}

document.addEventListener('DOMContentLoaded', () => {
const rgbaFromStr = function (rgba) {
if (!rgba) {
return undefined
}
return rgba.split('(')[1].split(')')[0].split(',')
}
const distance = function (v1, v2) {
let d = 0
for (let i = 0; i < v2.length; i++) {
d += (v1[i] - v2[i]) * (v1[i] - v2[i])
}
return Math.sqrt(d)
}
const getElementColor = function (el) {
const currentColorRGBA = window.getComputedStyle(el).backgroundColor
const currentColor = rgbaFromStr(currentColorRGBA)
// Ensure that the selected color is not too similar to an inactive tab color
const threshold = 50
if (currentColor !== undefined &&
Number(currentColor[3]) !== 0 &&
distance(currentColor, [199, 199, 199]) > threshold) {
return currentColorRGBA
}
return undefined
}
// Determines a good tab color
const computeThemeColor = function () {
// Use y = 3 to avoid hitting a border which are often gray
const samplePoints = [[3, 3], [window.innerWidth / 2, 3], [window.innerWidth - 3, 3]]
const els = []
for (const point of samplePoints) {
const el = document.elementFromPoint(point[0], point[1])
if (el) {
els.push(el)
if (el.parentElement) {
els.push(el.parentElement)
}
}
}
els.push(document.body)
for (const el of els) {
if (el !== document.documentElement) {
const themeColor = getElementColor(el)
if (themeColor) {
return themeColor
}
}
}
return undefined
}
ipcRenderer.on('post-page-load-run', function () {
// Hide broken images
Array.from(document.querySelectorAll('img')).forEach(function (img) {
img.addEventListener('error', function () {
this.style.visibility = 'hidden'
})
})
ipcRenderer.sendToHost({
actionType: 'theme-color-computed',
themeColor: computeThemeColor()
})
})
}).apply(this)
1 change: 0 additions & 1 deletion app/index-require.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
require('babel-polyfill')
require('vibrant/dist/Vibrant.min.js')
5 changes: 4 additions & 1 deletion docs/windowActions.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ Dispatches a message to indicate that audio is playing



### setThemeColor(frameProps, themeColor)
### setThemeColor(frameProps, themeColor, computedThemeColor)

Dispatches a message to indicate that the theme color has changed for a page

Expand All @@ -430,6 +430,9 @@ Dispatches a message to indicate that the theme color has changed for a page

**themeColor**: `string`, Theme color of the frame

**computedThemeColor**: `string`, Computed theme color of the
frame which is used if no frame color is present



### setFavicon(frameProps, favicon)
Expand Down
7 changes: 5 additions & 2 deletions js/actions/windowActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,12 +582,15 @@ const WindowActions = {
*
* @param {Object} frameProps - Properties of the frame in question
* @param {string} themeColor - Theme color of the frame
* @param {string} computedThemeColor - Computed theme color of the
* frame which is used if no frame color is present
*/
setThemeColor: function (frameProps, themeColor) {
setThemeColor: function (frameProps, themeColor, computedThemeColor) {
WindowDispatcher.dispatch({
actionType: WindowConstants.WINDOW_SET_THEME_COLOR,
frameProps,
themeColor
themeColor,
computedThemeColor
})
},

Expand Down
17 changes: 16 additions & 1 deletion js/components/frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ class Frame extends ImmutableComponent {
this.insertAds(event.target.src)
}
})
const frame = this.props.frame
this.webview.addEventListener('ipc-message', (e) => {
let action = e.channel
switch (action.actionType) {
case messages.THEME_COLOR_COMPUTED:
WindowActions.setThemeColor(frame, undefined, action.themeColor || null)
break
}
})
this.webview.addEventListener('load-commit', (event) => {
if (event.isMainFrame) {
// TODO: These 3 events should be combined into one
Expand Down Expand Up @@ -214,12 +223,14 @@ class Frame extends ImmutableComponent {
WindowActions.onWebviewLoadEnd(
this.props.frame,
this.webview.getURL())
this.webview.send(messages.POST_PAGE_LOAD_RUN)
})
this.webview.addEventListener('did-frame-finish-load', (event) => {
if (event.isMainFrame) {
WindowActions.onWebviewLoadEnd(
this.props.frame,
this.webview.getURL())
this.webview.send(messages.POST_PAGE_LOAD_RUN)
}
})
this.webview.addEventListener('media-started-playing', ({title}) => {
Expand All @@ -229,7 +240,11 @@ class Frame extends ImmutableComponent {
WindowActions.setAudioPlaybackActive(this.props.frame, false)
})
this.webview.addEventListener('did-change-theme-color', ({themeColor}) => {
WindowActions.setThemeColor(this.props.frame, themeColor)
// Due to a bug in Electron, after navigating to a page with a theme color
// to a page without a theme color, the background is sent to us as black
// even know there is no background. To work around this we just ignore
// the theme color in that case and let the computed theme color take over.
WindowActions.setThemeColor(this.props.frame, themeColor !== '#000000' ? themeColor : null)
})
this.webview.addEventListener('found-in-page', (e) => {
if (e.result !== undefined && e.result.matches !== undefined) {
Expand Down
2 changes: 2 additions & 0 deletions js/constants/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const messages = {
APP_STATE_CHANGE: _,
APP_ACTION: _,
STOP_LOAD: _,
POST_PAGE_LOAD_RUN: _,
THEME_COLOR_COMPUTED: _,
// Init
INIT_WINODW: _,
// Session restore
Expand Down
7 changes: 1 addition & 6 deletions js/lib/color.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
module.exports.parseColor = (color) => {
const div = document.createElement('div')
div.style.color = color
const m = div.style.color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i)
if (m) {
return [m[1], m[2], m[3]]
} else {
return null
}
return div.style.color.split('(')[1].split(')')[0].split(',')
}

module.exports.getTextColorForBackground = (color) => {
Expand Down
30 changes: 0 additions & 30 deletions js/state/frameStateUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import Immutable from 'immutable'
import Config from '../constants/config.js'
const getFavicon = require('../lib/faviconUtil.js')

export function isFrameKeyActive (windowState, frameKey) {
return windowState.get('activeFrameKey') === frameKey
Expand Down Expand Up @@ -278,35 +277,6 @@ export function removeOtherFrames (frames, closedFrames, frameProps) {
}
}

/**
* Extracts theme-color from a favicon using vibrant.js.
*/
export function computeThemeColor (frameProps) {
return new Promise((resolve, reject) => {
const icon = getFavicon(frameProps)
const img = new window.Image()
img.src = icon

img.onload = () => {
const vibrant = new window.Vibrant(img)
const swatches = vibrant.swatches()

// Arbitrary selection ordering, which appears to give decent results.
const swatchOrder = ['Vibrant', 'DarkVibrant', 'LightVibrant', 'Muted', 'LightMuted', 'DarkMuted']
for (let i = 0; i < swatchOrder.length; i++) {
const swatch = swatchOrder[i]
if (swatches[swatch]) {
resolve(swatches[swatch].getHex())
break
}
}
}
img.onerror = () => {
reject(new Error('Could not render image from blob.'))
}
})
}

export function getFrameTabPageIndex (frames, frameProps, tabsPerTabPage) {
const index = getFramePropsIndex(frames, frameProps)
if (index === -1) {
Expand Down
23 changes: 8 additions & 15 deletions js/stores/windowStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const windowStore = new WindowStore()

// Register callback to handle all updates
const doAction = (action) => {
// console.log(action.actionType, windowState.toJS())
// console.log(action.actionType, action, windowState.toJS())
switch (action.actionType) {
case WindowConstants.WINDOW_SET_STATE:
windowState = action.windowState
Expand Down Expand Up @@ -203,19 +203,7 @@ const doAction = (action) => {
loading: false,
endLoadTime: new Date().getTime()
})
FrameStateUtil.computeThemeColor(action.frameProps).then(
color => {
windowState = windowState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps)], {
computedThemeColor: color
})
windowStore.emitChange()
},
() => {
windowState = windowState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps)], {
computedThemeColor: null
})
windowStore.emitChange()
})
windowStore.emitChange()
break
case WindowConstants.WINDOW_SET_NAVBAR_FOCUSED:
windowState = windowState.setIn(activeFrameStatePath().concat(['navbar', 'focused']), action.focused)
Expand Down Expand Up @@ -363,7 +351,12 @@ const doAction = (action) => {
windowStore.emitChange()
break
case WindowConstants.WINDOW_SET_THEME_COLOR:
windowState = windowState.setIn(frameStatePathForFrame(action.frameProps).concat(['themeColor']), action.themeColor)
if (action.themeColor !== undefined) {
windowState = windowState.setIn(frameStatePathForFrame(action.frameProps).concat(['themeColor']), action.themeColor)
}
if (action.computedThemeColor !== undefined) {
windowState = windowState.setIn(frameStatePathForFrame(action.frameProps).concat(['computedThemeColor']), action.computedThemeColor)
}
windowStore.emitChange()
break
case WindowConstants.WINDOW_SET_URL_BAR_ACTIVE:
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@
"sqlite3": "^3.1.0",
"tracking-protection": "1.0.x",
"url-loader": "^0.5.7",
"urlutil.js": "^0.1.1",
"vibrant": "jariz/vibrant.js#1.0"
"urlutil.js": "^0.1.1"
},
"devDependencies": {
"asar": "^0.8.3",
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/red_bg.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<head>
</head>
<body style='background-color: #f00'>
</body>
</html>
7 changes: 7 additions & 0 deletions test/fixtures/yellow_header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<head>
</head>
<body style="background-color: #f00; margin: 0">
<div style="height: 100px; width: 100%; position: absolute; top: 0; background-color: #ffff66;" />
</body>
</html>
3 changes: 1 addition & 2 deletions test/lib/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ module.exports = {
activeTabFavicon: '.tab.active .tabIcon',
navigator: '#navigator',
navigatorLoadTime: '#navigator .loadTime',
newFrameButtonInsideTabs: '.tabs .newFrameButton',
newFrameButtonOutsideTabs: '.tabsToolbarButtons .newFrameButton',
newFrameButton: '.tabs .newFrameButton',
tabPage: '.tabPage',
tabPage1: '[data-tab-page="0"]',
tabPage2: '[data-tab-page="1"]',
Expand Down
22 changes: 13 additions & 9 deletions test/navigationBarTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,23 +179,27 @@ describe('urlbar', function () {
})

// We need a newer electron build first
it.skip('Parses theme-color meta tag when one is present', function *() {
it('Parses theme-color meta tag when one is present', function *() {
const pageWithFavicon = Brave.server.url('theme_color.html')
yield navigate(this.app.client, pageWithFavicon)
yield this.app.client.waitUntil(() =>
this.app.client.getCssProperty(activeTab, 'background-color').then(backgroundColor =>
backgroundColor.parsed.hex === '#4d90fe'
))
})
it.skip('Obtains theme color from favicon', function *() {
const pageWithFavicon = Brave.server.url('favicon.html')
yield navigate(this.app.client, pageWithFavicon)
it('Obtains theme color from the background', function *() {
const redPage = Brave.server.url('red_bg.html')
yield navigate(this.app.client, redPage)
yield this.app.client.waitUntil(() =>
this.app.client.getCssProperty(activeTab, 'background-color').then(backgroundColor => {
console.log(backgroundColor.parsed.hex)
backgroundColor.parsed.hex === '#320f07'
}
))
this.app.client.getCssProperty(activeTab, 'background-color').then(backgroundColor =>
backgroundColor.parsed.hex === '#ff0000'))
})
it('Obtains theme color from a top header and not background', function *() {
const redPage = Brave.server.url('yellow_header.html')
yield navigate(this.app.client, redPage)
yield this.app.client.waitUntil(() =>
this.app.client.getCssProperty(activeTab, 'background-color').then(backgroundColor =>
backgroundColor.parsed.hex === '#ffff66'))
})
})

Expand Down
5 changes: 3 additions & 2 deletions test/pinnedTabTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ describe('pinnedTabs', function () {
describe('new tab signal', function () {
Brave.beforeAll(this)
before(function *() {
this.page1Url = Brave.server.url('page1.html')
yield setup(this.app.client)
})

it('creates a new pinned tab when signaled', function *() {
it.skip('creates a new pinned tab when signaled', function *() {
yield this.app.client
.ipcSend(messages.SHORTCUT_NEW_FRAME, 'http://www.brave.com', { isPinned: true })
.ipcSend(messages.SHORTCUT_NEW_FRAME, this.page1Url, { isPinned: true })
.waitForExist('.tab.isPinned[data-frame-key="2"]')
})
})
Expand Down
13 changes: 13 additions & 0 deletions test/settingsTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* global describe, it */

const settings = require('../js/constants/settings')
const appConfig = require('../js/constants/appConfig')
const assert = require('assert')

describe('settings', function () {
it('All settings have default values', function *() {
Object.keys(settings).forEach(setting => {
assert.notStrictEqual(appConfig.defaultSettings[settings[setting]], undefined)
})
})
})
Loading

0 comments on commit c28e6ea

Please sign in to comment.