diff --git a/CHANGELOG.md b/CHANGELOG.md index 749bcae81..b5ec1548b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added + * Save app state when closing to tray ([#968](https://github.com/lbryio/lbry-app/issues/968)) * Added startup-troubleshooting FAQ URL to daemon error ([#1039](https://github.com/lbryio/lbry-app/pull/1039)) * @@ -16,7 +17,9 @@ Web UI version numbers should always match the corresponding version of LBRY App * ### Fixed - * + * Fixed sort by date of published content ([#986](https://github.com/lbryio/lbry-app/issues/986)) + * Fix night mode start time, set to 9PM (#1050) + * Fix night mode start time, set to 9PM ([#1050](https://github.com/lbryio/lbry-app/issues/1050)) * ### Deprecated diff --git a/src/main/Tray.js b/src/main/Tray.js deleted file mode 100644 index 6937dda42..000000000 --- a/src/main/Tray.js +++ /dev/null @@ -1,63 +0,0 @@ -import { app, Menu, Tray as ElectronTray } from 'electron'; -import path from 'path'; -import createWindow from './createWindow'; - -export default class Tray { - window; - updateAttachedWindow; - tray; - - constructor(window, updateAttachedWindow) { - this.window = window; - this.updateAttachedWindow = updateAttachedWindow; - } - - create() { - let iconPath; - switch (process.platform) { - case 'darwin': { - iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png'); - break; - } - case 'win32': { - iconPath = path.join(__static, '/img/tray/windows/tray.ico'); - break; - } - default: { - iconPath = path.join(__static, '/img/tray/default/tray.png'); - } - } - - this.tray = new ElectronTray(iconPath); - - this.tray.on('double-click', () => { - if (!this.window || this.window.isDestroyed()) { - this.window = createWindow(); - this.updateAttachedWindow(this.window); - } else { - this.window.show(); - this.window.focus(); - } - }); - - this.tray.setToolTip('LBRY App'); - - const template = [ - { - label: `Open ${app.getName()}`, - click: () => { - if (!this.window || this.window.isDestroyed()) { - this.window = createWindow(); - this.updateAttachedWindow(this.window); - } else { - this.window.show(); - this.window.focus(); - } - }, - }, - { role: 'quit' }, - ]; - const contextMenu = Menu.buildFromTemplate(template); - this.tray.setContextMenu(contextMenu); - } -} diff --git a/src/main/createTray.js b/src/main/createTray.js new file mode 100644 index 000000000..c8ed9ba95 --- /dev/null +++ b/src/main/createTray.js @@ -0,0 +1,41 @@ +import { app, Menu, Tray } from 'electron'; +import path from 'path'; + +export default window => { + let iconPath; + switch (process.platform) { + case 'darwin': { + iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png'); + break; + } + case 'win32': { + iconPath = path.join(__static, '/img/tray/windows/tray.ico'); + break; + } + default: { + iconPath = path.join(__static, '/img/tray/default/tray.png'); + } + } + + const tray = new Tray(iconPath); + + tray.on('double-click', () => { + window.show(); + }); + + tray.setToolTip('LBRY App'); + + const template = [ + { + label: `Open ${app.getName()}`, + click: () => { + window.show(); + }, + }, + { role: 'quit' }, + ]; + const contextMenu = Menu.buildFromTemplate(template); + tray.setContextMenu(contextMenu); + + return tray; +}; diff --git a/src/main/createWindow.js b/src/main/createWindow.js index 6809761f3..8521b3207 100644 --- a/src/main/createWindow.js +++ b/src/main/createWindow.js @@ -2,7 +2,7 @@ import { app, BrowserWindow, dialog } from 'electron'; import setupBarMenu from './menu/setupBarMenu'; import setupContextMenu from './menu/setupContextMenu'; -export default deepLinkingURIArg => { +export default appState => { let windowConfiguration = { backgroundColor: '#155B4A', minWidth: 800, @@ -35,11 +35,7 @@ export default deepLinkingURIArg => { let deepLinkingURI; // Protocol handler for win32 - if ( - !deepLinkingURIArg && - process.platform === 'win32' && - String(process.argv[1]).startsWith('lbry') - ) { + if (process.platform === 'win32' && String(process.argv[1]).startsWith('lbry')) { // Keep only command line / deep linked arguments // Windows normalizes URIs when they're passed in from other apps. On Windows, this tries to // restore the original URI that was typed. @@ -48,15 +44,16 @@ export default deepLinkingURIArg => { // - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as // an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well. deepLinkingURI = process.argv[1].replace(/\/$/, '').replace('/#', '#'); - } else { - deepLinkingURI = deepLinkingURIArg; } setupBarMenu(); setupContextMenu(window); - window.on('closed', () => { - window = null; + window.on('close', event => { + if (!appState.isQuitting) { + event.preventDefault(); + window.hide(); + } }); window.on('focus', () => { diff --git a/src/main/index.js b/src/main/index.js index 0ecd8d884..d068e87e0 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -8,7 +8,7 @@ import https from 'https'; import { shell, app, ipcMain, dialog } from 'electron'; import { autoUpdater } from 'electron-updater'; import Daemon from './Daemon'; -import Tray from './Tray'; +import createTray from './createTray'; import createWindow from './createWindow'; autoUpdater.autoDownload = true; @@ -32,11 +32,7 @@ let rendererWindow; let tray; let daemon; -let isQuitting; - -const updateRendererWindow = window => { - rendererWindow = window; -}; +const appState = {}; const installExtensions = async () => { // eslint-disable-next-line import/no-extraneous-dependencies,global-require @@ -64,7 +60,7 @@ app.on('ready', async () => { daemon = new Daemon(); daemon.on('exit', () => { daemon = null; - if (!isQuitting) { + if (!appState.isQuitting) { dialog.showErrorBox( 'Daemon has Exited', 'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' + @@ -79,15 +75,12 @@ app.on('ready', async () => { if (process.env.NODE_ENV === 'development') { await installExtensions(); } - rendererWindow = createWindow(); - tray = new Tray(rendererWindow, updateRendererWindow); - tray.create(); + rendererWindow = createWindow(appState); + tray = createTray(rendererWindow); }); app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (!rendererWindow) rendererWindow = createWindow(); + rendererWindow.show(); }); app.on('will-quit', event => { @@ -119,7 +112,7 @@ app.on('will-quit', event => { return; } - isQuitting = true; + appState.isQuitting = true; if (daemon) daemon.quit(); }); @@ -128,18 +121,13 @@ app.on('will-finish-launching', () => { // Protocol handler for macOS app.on('open-url', (event, URL) => { event.preventDefault(); - if (rendererWindow && !rendererWindow.isDestroyed()) { - rendererWindow.webContents.send('open-uri-requested', URL); - rendererWindow.show(); - rendererWindow.focus(); - } else { - rendererWindow = createWindow(URL); - } + rendererWindow.webContents.send('open-uri-requested', URL); + rendererWindow.show(); }); }); -app.on('window-all-closed', () => { - // Subscribe to event so the app doesn't quit when closing the window. +app.on('before-quit', () => { + appState.isQuitting = true; }); ipcMain.on('upgrade', (event, installerPath) => { @@ -226,7 +214,7 @@ ipcMain.on('set-auth-token', (event, token) => { process.on('uncaughtException', error => { dialog.showErrorBox('Error Encountered', `Caught error: ${error}`); - isQuitting = true; + appState.isQuitting = true; if (daemon) daemon.quit(); app.exit(1); }); @@ -242,14 +230,8 @@ const isSecondInstance = app.makeSingleInstance(argv => { URI = argv[1].replace(/\/$/, '').replace('/#', '#'); } - if (rendererWindow && !rendererWindow.isDestroyed()) { - rendererWindow.webContents.send('open-uri-requested', URI); - - rendererWindow.show(); - rendererWindow.focus(); - } else { - rendererWindow = createWindow(URI); - } + rendererWindow.webContents.send('open-uri-requested', URI); + rendererWindow.show(); }); if (isSecondInstance) { diff --git a/src/renderer/component/fileList/view.jsx b/src/renderer/component/fileList/view.jsx index 8dcccadc3..afb446323 100644 --- a/src/renderer/component/fileList/view.jsx +++ b/src/renderer/component/fileList/view.jsx @@ -9,12 +9,33 @@ class FileList extends React.PureComponent { super(props); this.state = { - sortBy: 'date', + sortBy: 'dateNew', }; this._sortFunctions = { - date(fileInfos) { - return fileInfos.slice().reverse(); + dateNew(fileInfos) { + return fileInfos.slice().sort((fileInfo1, fileInfo2) => { + const height1 = fileInfo1.height + const height2 = fileInfo2.height + if (height1 > height2) { + return -1; + } else if (height1 < height2) { + return 1; + } + return 0; + }); + }, + dateOld(fileInfos) { + return fileInfos.slice().sort((fileInfo1, fileInfo2) => { + const height1 = fileInfo1.height + const height2 = fileInfo2.height + if (height1 < height2) { + return -1; + } else if (height1 > height2) { + return 1; + } + return 0; + }); }, title(fileInfos) { return fileInfos.slice().sort((fileInfo1, fileInfo2) => { @@ -95,7 +116,8 @@ class FileList extends React.PureComponent { {__('Sort by')}{' '} - + + diff --git a/src/renderer/component/video/internal/player.jsx b/src/renderer/component/video/internal/player.jsx index e938a856f..8bbf2692f 100644 --- a/src/renderer/component/video/internal/player.jsx +++ b/src/renderer/component/video/internal/player.jsx @@ -1,5 +1,4 @@ -const { remote } = require('electron'); - +import { remote } from 'electron'; import React from 'react'; import { Thumbnail } from 'component/common'; import player from 'render-media'; @@ -21,33 +20,21 @@ class VideoPlayer extends React.PureComponent { this.togglePlayListener = this.togglePlay.bind(this); } - componentWillReceiveProps(next) { - const el = this.refs.media.children[0]; - if (!this.props.paused && next.paused && !el.paused) el.pause(); - } - componentDidMount() { - const container = this.refs.media; - const { - contentType, - downloadPath, - mediaType, - changeVolume, - volume, - position, - claim, - uri, - } = this.props; + const container = this.media; + const { contentType, changeVolume, volume, position, claim } = this.props; - const loadedMetadata = e => { + const loadedMetadata = () => { this.setState({ hasMetadata: true, startedPlaying: true }); - this.refs.media.children[0].play(); + this.media.children[0].play(); }; - const renderMediaCallback = err => { - if (err) this.setState({ unplayable: true }); + + const renderMediaCallback = error => { + if (error) this.setState({ unplayable: true }); }; + // Handle fullscreen change for the Windows platform - const win32FullScreenChange = e => { + const win32FullScreenChange = () => { const win = remote.BrowserWindow.getFocusedWindow(); if (process.platform === 'win32') { win.setMenu(document.webkitIsFullScreen ? null : remote.Menu.getApplicationMenu()); @@ -61,13 +48,13 @@ class VideoPlayer extends React.PureComponent { player.append( this.file(), container, - { autoplay: false, controls: true }, + { autoplay: true, controls: true }, renderMediaCallback.bind(this) ); } document.addEventListener('keydown', this.togglePlayListener); - const mediaElement = this.refs.media.children[0]; + const mediaElement = this.media.children[0]; if (mediaElement) { mediaElement.currentTime = position || 0; mediaElement.addEventListener('play', () => this.props.doPlay()); @@ -87,29 +74,38 @@ class VideoPlayer extends React.PureComponent { } } + componentWillReceiveProps(next) { + const el = this.media.children[0]; + if (!this.props.paused && next.paused && !el.paused) el.pause(); + } + + componentDidUpdate() { + const { contentType, downloadCompleted } = this.props; + const { startedPlaying } = this.state; + + if (this.playableType() && !startedPlaying && downloadCompleted) { + const container = this.media.children[0]; + + if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) { + this.renderAudio(this.media, true); + } else { + player.render(this.file(), container, { + autoplay: true, + controls: true, + }); + } + } + } + componentWillUnmount() { document.removeEventListener('keydown', this.togglePlayListener); - const mediaElement = this.refs.media.children[0]; + const mediaElement = this.media.children[0]; if (mediaElement) { mediaElement.removeEventListener('click', this.togglePlayListener); } this.props.doPause(); } - renderAudio(container, autoplay) { - if (container.firstChild) { - container.firstChild.remove(); - } - - // clear the container - const { downloadPath } = this.props; - const audio = document.createElement('audio'); - audio.autoplay = autoplay; - audio.controls = true; - audio.src = downloadPath; - container.appendChild(audio); - } - togglePlay(event) { // ignore all events except click and spacebar keydown, or input events in a form control if ( @@ -119,7 +115,7 @@ class VideoPlayer extends React.PureComponent { return; } event.preventDefault(); - const mediaElement = this.refs.media.children[0]; + const mediaElement = this.media.children[0]; if (mediaElement) { if (!mediaElement.paused) { mediaElement.pause(); @@ -129,24 +125,6 @@ class VideoPlayer extends React.PureComponent { } } - componentDidUpdate() { - const { contentType, downloadCompleted } = this.props; - const { startedPlaying } = this.state; - - if (this.playableType() && !startedPlaying && downloadCompleted) { - const container = this.refs.media.children[0]; - - if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) { - this.renderAudio(this.refs.media, true); - } else { - player.render(this.file(), container, { - autoplay: true, - controls: true, - }); - } - } - } - file() { const { downloadPath, filename } = this.props; @@ -162,14 +140,26 @@ class VideoPlayer extends React.PureComponent { return ['audio', 'video'].indexOf(mediaType) !== -1; } + renderAudio(container, autoplay) { + if (container.firstChild) { + container.firstChild.remove(); + } + + // clear the container + const { downloadPath } = this.props; + const audio = document.createElement('audio'); + audio.autoplay = autoplay; + audio.controls = true; + audio.src = downloadPath; + container.appendChild(audio); + } + render() { const { mediaType, poster } = this.props; const { hasMetadata, unplayable } = this.state; const noMetadataMessage = 'Waiting for metadata.'; const unplayableMessage = "Sorry, looks like we can't play this file."; - const needsMetadata = this.playableType(); - return (
{['audio', 'application'].indexOf(mediaType) !== -1 && @@ -179,7 +169,12 @@ class VideoPlayer extends React.PureComponent { !hasMetadata && !unplayable && } {unplayable && } -
+
{ + this.media = container; + }} + className="media" + />
); } diff --git a/src/renderer/jsonrpc.js b/src/renderer/jsonrpc.js index 8e11c4f12..184065122 100644 --- a/src/renderer/jsonrpc.js +++ b/src/renderer/jsonrpc.js @@ -15,7 +15,7 @@ jsonrpc.call = ( return response.json().then(json => { let error; if (json.error) { - error = new Error(json.error); + error = new Error(json.error.message); } else { error = new Error('Protocol error with unknown response signature'); } diff --git a/src/renderer/page/settings/view.jsx b/src/renderer/page/settings/view.jsx index 0b061e87a..639d9195e 100644 --- a/src/renderer/page/settings/view.jsx +++ b/src/renderer/page/settings/view.jsx @@ -14,6 +14,8 @@ class SettingsPage extends React.PureComponent { this.state = { clearingCache: false, }; + + this.onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this); } clearCache() { @@ -62,11 +64,16 @@ class SettingsPage extends React.PureComponent { onThemeChange(event) { const { value } = event.target; + + if (value === 'dark') { + this.onAutomaticDarkModeChange(false); + } + this.props.setClientSetting(settings.THEME, value); } - onAutomaticDarkModeChange(event) { - this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, event.target.checked); + onAutomaticDarkModeChange(value) { + this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, value); } onInstantPurchaseEnabledChange(enabled) { @@ -143,6 +150,7 @@ class SettingsPage extends React.PureComponent { ); } + return (
@@ -325,8 +333,9 @@ class SettingsPage extends React.PureComponent { this.onAutomaticDarkModeChange(e.target.checked)} + checked={automaticDarkModeEnabled} label={__('Automatic dark mode (9pm to 8am)')} />
diff --git a/src/renderer/redux/actions/settings.js b/src/renderer/redux/actions/settings.js index 9196dca44..40365b7f8 100644 --- a/src/renderer/redux/actions/settings.js +++ b/src/renderer/redux/actions/settings.js @@ -71,7 +71,7 @@ export function doUpdateIsNight() { return { type: ACTIONS.UPDATE_IS_NIGHT, data: { isNight: (() => { - const startNightMoment = moment('19:00', 'HH:mm'); + const startNightMoment = moment('21:00', 'HH:mm'); const endNightMoment = moment('8:00', 'HH:mm'); return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment)); })() diff --git a/src/renderer/util/setBadge.js b/src/renderer/util/setBadge.js index ca4c9d358..77e5ee2f2 100644 --- a/src/renderer/util/setBadge.js +++ b/src/renderer/util/setBadge.js @@ -2,10 +2,10 @@ import { remote } from 'electron'; const application = remote.app; const { dock } = application; -const win = remote.BrowserWindow.getFocusedWindow(); +const browserWindow = remote.getCurrentWindow(); const setBadge = text => { if (!dock) return; - if (win.isFocused()) return; + if (browserWindow.isFocused()) return; dock.setBadge(text); };