Imagery Age to be displayed for realtime mode in Imagery View (#3308)

* fix linting errors

* removing testing units

* WIP: stubbe in age in template, adding getAge function

* WIP: stubbed in age in template, dummy function to start

* added image age for realtime mode, ready for styling

* reverting unnecesarry telemetryview file changes, not needed for this issue

* checking for age tracking conditions on mount

* Image age styling and changes

- Cleaned up code in ImageryPlugin to use const instead of var, changed
image delay time into a const

* Image age styling and changes

- WIP!
- Layout changes for Imagery control-bar;
- New animation effect, WIP;

* Image age styling and changes

- Markup and CSS updates for Imagery view;
- Final layout for age indicator;

* parsing image timestamp in case it is a string

* using moment for human readable durations above 8 hours

* UTC based timesystem check

* reset "new" css class on image age when "time" updates

* WIP: debuggin weird imagery plugin issue for first selection of image in thumbnails

* fixing pause overwriting clicked images selection

* making isImageNew a computed value

* WIP: pr updates

* WIP: tabling PR edits to focus on lower hanging PR edits for testathon

* WIP

* overhaul of imagery plugin logic for optimization PLUS imagery age

* adding next/prev functionality to refactored plugin

* added arrow left and right keys to navigate next and previous

* added arrow key scrolling and scrolling thumbnail into view and hold down scrolling

* adding in missing class

* component based key listening, PR updates

* refactor to use just imageIndex to track focused image, utilized more caching, PR comment edits

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: charlesh88 <charlesh88@gmail.com>
This commit is contained in:
Jamie V 2020-10-06 16:01:47 -07:00 committed by GitHub
parent a91179091f
commit ab76451360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 378 additions and 142 deletions

View File

@ -27,7 +27,7 @@ define([
) { ) {
function ImageryPlugin() { function ImageryPlugin() {
var IMAGE_SAMPLES = [ const IMAGE_SAMPLES = [
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg", "https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg", "https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg", "https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
@ -47,13 +47,14 @@ define([
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg", "https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg" "https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
]; ];
const IMAGE_DELAY = 20000;
function pointForTimestamp(timestamp, name) { function pointForTimestamp(timestamp, name) {
return { return {
name: name, name: name,
utc: Math.floor(timestamp / 5000) * 5000, utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / 5000) * 5000, local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length] url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
}; };
} }
@ -64,7 +65,7 @@ define([
subscribe: function (domainObject, callback) { subscribe: function (domainObject, callback) {
var interval = setInterval(function () { var interval = setInterval(function () {
callback(pointForTimestamp(Date.now(), domainObject.name)); callback(pointForTimestamp(Date.now(), domainObject.name));
}, 5000); }, IMAGE_DELAY);
return function () { return function () {
clearInterval(interval); clearInterval(interval);
@ -81,9 +82,9 @@ define([
var start = options.start; var start = options.start;
var end = Math.min(options.end, Date.now()); var end = Math.min(options.end, Date.now());
var data = []; var data = [];
while (start <= end && data.length < 5000) { while (start <= end && data.length < IMAGE_DELAY) {
data.push(pointForTimestamp(start, domainObject.name)); data.push(pointForTimestamp(start, domainObject.name));
start += 5000; start += IMAGE_DELAY;
} }
return Promise.resolve(data); return Promise.resolve(data);

View File

@ -1,5 +1,11 @@
<template> <template>
<div class="c-imagery"> <div
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div class="c-imagery__main-image-wrapper has-local-controls"> <div class="c-imagery__main-image-wrapper has-local-controls">
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc"> <div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
<span class="holder flex-elem grows c-imagery__lc__sliders"> <span class="holder flex-elem grows c-imagery__lc__sliders">
@ -22,98 +28,188 @@
></a> ></a>
</span> </span>
</div> </div>
<div class="main-image s-image-main c-imagery__main-image has-local-controls js-imageryView-image" <div class="main-image s-image-main c-imagery__main-image has-local-controls"
:class="{'paused unnsynced': paused(),'stale':false }" :class="{'paused unnsynced': isPaused,'stale':false }"
:style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none', :style="{'background-image': imageUrl ? `url(${imageUrl})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}" 'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
:data-openmct-image-timestamp="getTime()" :data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keystring" :data-openmct-object-keystring="keyString"
> >
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev" <button class="c-nav c-nav--prev"
title="Previous image" title="Previous image"
:disabled="isPrevDisabled()" :disabled="isPrevDisabled"
@click="prevImage()" @click="prevImage()"
></button> ></button>
<button class="c-nav c-nav--next" <button class="c-nav c-nav--next"
title="Next image" title="Next image"
:disabled="isNextDisabled()" :disabled="isNextDisabled"
@click="nextImage()" @click="nextImage()"
></button> ></button>
</div> </div>
</div> </div>
<div class="c-imagery__control-bar"> <div class="c-imagery__control-bar">
<div class="c-imagery__timestamp">{{ getTime() }}</div> <div class="c-imagery__time">
<div class="c-imagery__timestamp">{{ time }}</div>
<div
v-if="canTrackDuration"
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div>
</div>
<div class="h-local-controls flex-elem"> <div class="h-local-controls flex-elem">
<button <button
class="c-button icon-pause pause-play" class="c-button icon-pause pause-play"
:class="{'is-paused': paused()}" :class="{'is-paused': isPaused}"
@click="paused(!paused(), true)" @click="paused(!isPaused, 'button')"
></button> ></button>
</div> </div>
</div> </div>
</div> </div>
<div ref="thumbsWrapper" <div ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper" class="c-imagery__thumbs-wrapper"
:class="{'is-paused': paused()}" :class="{'is-paused': isPaused}"
@scroll="handleScroll" @scroll="handleScroll"
> >
<div v-for="(imageData, index) in imageHistory" <div v-for="(datum, index) in imageHistory"
:key="index" :key="datum.url"
class="c-imagery__thumb c-thumb" class="c-imagery__thumb c-thumb"
:class="{selected: imageData.selected}" :class="{ selected: focusedImageIndex === index && isPaused }"
@click="setSelectedImage(imageData)" @click="setFocusedImage(index, thumbnailClick)"
> >
<img class="c-thumb__image" <img class="c-thumb__image"
:src="getImageUrl(imageData)" :src="formatImageUrl(datum)"
> >
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div> <div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import moment from 'moment';
const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
const ARROW_SCROLL_RATE_MS = 100;
const THUMBNAIL_CLICKED = true;
const ONE_MINUTE = 60 * 1000;
const FIVE_MINUTES = 5 * ONE_MINUTE;
const ONE_HOUR = ONE_MINUTE * 60;
const EIGHT_HOURS = 8 * ONE_HOUR;
const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
let timeSystem = this.openmct.time.timeSystem();
return { return {
autoScroll: true, autoScroll: true,
durationFormatter: undefined,
filters: { filters: {
brightness: 100, brightness: 100,
contrast: 100 contrast: 100
}, },
image: {
selected: ''
},
imageFormat: '',
imageHistory: [], imageHistory: [],
imageUrl: '', thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false, isPaused: false,
metadata: {}, metadata: {},
requestCount: 0, requestCount: 0,
timeFormat: '', timeSystem: timeSystem,
keystring: '' timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
numericDuration: undefined
}; };
}, },
computed: { computed: {
bounds() { time() {
return this.openmct.time.bounds(); return this.formatTime(this.focusedImage);
},
imageUrl() {
return this.formatImageUrl(this.focusedImage);
},
isImageNew() {
let cutoff = FIVE_MINUTES;
let age = this.numericDuration;
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) {
disabled = true;
}
return disabled;
},
isPrevDisabled() {
let disabled = false;
if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {
disabled = true;
}
return disabled;
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
parsedSelectedTime() {
return this.parseTime(this.focusedImage);
},
formattedDuration() {
let result = 'N/A';
let negativeAge = -1;
if (this.numericDuration > TWENTYFOUR_HOURS) {
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
result = moment.duration(negativeAge, 'days').humanize(true);
} else if (this.numericDuration > EIGHT_HOURS) {
negativeAge *= (this.numericDuration / ONE_HOUR);
result = moment.duration(negativeAge, 'hours').humanize(true);
} else if (this.durationFormatter) {
result = this.durationFormatter.format(this.numericDuration);
}
return result;
}
},
watch: {
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
} }
}, },
mounted() { mounted() {
// set
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.timeKey = this.openmct.time.timeSystem().key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
// listen // listen
this.openmct.time.on('bounds', this.boundsChange); this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange); this.openmct.time.on('timeSystem', this.timeSystemChange);
this.openmct.time.on('clock', this.clockChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff // kickoff
this.subscribe(); this.subscribe();
this.requestHistory(); this.requestHistory();
@ -127,41 +223,55 @@ export default {
delete this.unsubscribe; delete this.unsubscribe;
} }
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange); this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange); this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
}, },
methods: { methods: {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) { datumIsNotValid(datum) {
if (this.imageHistory.length === 0) { if (this.imageHistory.length === 0) {
return false; return false;
} }
const datumTime = this.timeFormat.format(datum); const datumURL = this.formatImageUrl(datum);
const datumURL = this.imageFormat.format(datum); const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]);
const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history, // datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history // or it is before the last datum in the history
const datumTimeCheck = this.timeFormat.parse(datum); const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]); const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL); const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck; const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale; return matchesLast || isStale;
}, },
getImageUrl(datum) { formatImageUrl(datum) {
return datum if (!datum) {
? this.imageFormat.format(datum) return;
: this.imageUrl; }
return this.imageFormatter.format(datum);
}, },
getTime(datum) { formatTime(datum) {
let dateTimeStr = datum if (!datum) {
? this.timeFormat.format(datum) return;
: this.time; }
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping // Replace ISO "T" with a space to allow wrapping
return dateTimeStr ? dateTimeStr.replace("T", " ") : ""; return dateTimeStr.replace("T", " ");
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
}, },
handleScroll() { handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper; const thumbsWrapper = this.$refs.thumbsWrapper;
@ -174,26 +284,35 @@ export default {
|| (scrollHeight - scrollTop) > 2 * clientHeight; || (scrollHeight - scrollTop) > 2 * clientHeight;
this.autoScroll = !disableScroll; this.autoScroll = !disableScroll;
}, },
paused(state, button = false) { paused(state, type) {
if (arguments.length > 0 && state !== this.isPaused) {
this.unselectAllImages();
this.isPaused = state;
if (state === true && button) {
// If we are pausing, select the latest image in imageHistory
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
}
if (this.nextDatum) { this.isPaused = state;
this.updateValues(this.nextDatum);
delete this.nextDatum;
} else {
this.updateValues(this.imageHistory[this.imageHistory.length - 1]);
}
this.autoScroll = true; if (type === 'button') {
this.setFocusedImage(this.imageHistory.length - 1);
} }
return this.isPaused; if (this.nextImageIndex) {
this.setFocusedImage(this.nextImageIndex);
delete this.nextImageIndex;
}
this.autoScroll = true;
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) {
return;
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, },
scrollToRight() { scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) { if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
@ -207,22 +326,17 @@ export default {
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0); setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
}, },
setSelectedImage(image) { setFocusedImage(index, thumbnailClick = false) {
// If we are paused and the current image IS selected, unpause if (this.isPaused && !thumbnailClick) {
// Otherwise, set current image and pause this.nextImageIndex = index;
if (!image) {
return; return;
} }
if (this.isPaused && image.selected) { this.focusedImageIndex = index;
this.paused(false);
this.unselectAllImages(); if (thumbnailClick && !this.isPaused) {
} else {
this.imageUrl = this.getImageUrl(image);
this.time = this.getTime(image);
this.paused(true); this.paused(true);
this.unselectAllImages();
image.selected = true;
} }
}, },
boundsChange(bounds, isTick) { boundsChange(bounds, isTick) {
@ -230,98 +344,158 @@ export default {
this.requestHistory(); this.requestHistory();
} }
}, },
requestHistory() { async requestHistory() {
const requestId = ++this.requestCount; let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = []; this.imageHistory = [];
this.openmct.telemetry let data = await this.openmct.telemetry
.request(this.domainObject, this.bounds) .request(this.domainObject, bounds) || [];
.then((values = []) => {
if (this.requestCount === requestId) { if (this.requestCount === requestId) {
// add each image to the history data.forEach((datum, index) => {
// update values for the very last image (set current image time and url) this.updateHistory(datum, index === data.length - 1);
values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1));
}
}); });
}
}, },
timeSystemChange(system) { timeSystemChange(system) {
// reset timesystem dependent variables this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = system.key; this.timeKey = this.timeSystem.key;
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.trackDuration();
},
clockChange(clock) {
this.trackDuration();
}, },
subscribe() { subscribe() {
this.unsubscribe = this.openmct.telemetry this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => { .subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.timeFormat.parse(datum); let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) { if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum); this.updateHistory(datum);
} }
}); });
}, },
unselectAllImages() { updateHistory(datum, setFocused = true) {
this.imageHistory.forEach(image => image.selected = false);
},
updateHistory(datum, updateValues = true) {
if (this.datumIsNotValid(datum)) { if (this.datumIsNotValid(datum)) {
return; return;
} }
this.imageHistory.push(datum); this.imageHistory.push(datum);
if (updateValues) { if (setFocused) {
this.updateValues(datum); this.setFocusedImage(this.imageHistory.length - 1);
} }
}, },
updateValues(datum) { getFormatter(key) {
if (this.isPaused) { let metadataValue = this.metadata.value(key) || { format: key };
this.nextDatum = datum; let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
trackDuration() {
if (this.canTrackDuration) {
this.stopDurationTracking();
this.updateDuration();
this.durationTracker = window.setInterval(
this.updateDuration, DURATION_TRACK_MS
);
} else {
this.stopDurationTracking();
}
},
stopDurationTracking() {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
},
resetAgeCSS() {
this.refreshCSS = true;
// unable to make this work with nextTick
setTimeout(() => {
this.refreshCSS = false;
}, REFRESH_CSS_MS);
},
nextImage() {
if (this.isNextDisabled) {
return; return;
} }
this.time = this.timeFormat.format(datum); let index = this.focusedImageIndex;
this.imageUrl = this.imageFormat.format(datum);
}, this.setFocusedImage(++index, THUMBNAIL_CLICKED);
selectedImageIndex() {
return this.imageHistory.findIndex(image => image.selected);
},
setSelectedByIndex(index) {
this.setSelectedImage(this.imageHistory[index]);
},
nextImage() {
let index = this.selectedImageIndex();
this.setSelectedByIndex(++index);
if (index === this.imageHistory.length - 1) { if (index === this.imageHistory.length - 1) {
this.paused(false); this.paused(false);
} }
}, },
prevImage() { prevImage() {
let index = this.selectedImageIndex(); if (this.isPrevDisabled) {
if (index === -1) { return;
this.setSelectedByIndex(this.imageHistory.length - 2); }
let index = this.focusedImageIndex;
if (index === this.imageHistory.length - 1) {
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
} else { } else {
this.setSelectedByIndex(--index); this.setFocusedImage(--index, THUMBNAIL_CLICKED);
} }
}, },
isNextDisabled() { arrowDownHandler(event) {
let disabled = false; let key = event.keyCode;
let index = this.selectedImageIndex();
if (index === -1 || index === this.imageHistory.length - 1) { if (this.isLeftOrRightArrowKey(key)) {
disabled = true; this.arrowDown = true;
window.clearTimeout(this.arrowDownDelayTimeout);
this.arrowDownDelayTimeout = window.setTimeout(() => {
this.arrowKeyScroll(this.directionByKey(key));
}, ARROW_DOWN_DELAY_CHECK_MS);
} }
return disabled;
}, },
isPrevDisabled() { arrowUpHandler(event) {
let disabled = false; let key = event.keyCode;
let index = this.selectedImageIndex();
if (index === 0 || this.imageHistory.length < 2) { window.clearTimeout(this.arrowDownDelayTimeout);
disabled = true;
if (this.isLeftOrRightArrowKey(key)) {
this.arrowDown = false;
let direction = this.directionByKey(key);
this[direction + 'Image']();
}
},
arrowKeyScroll(direction) {
if (this.arrowDown) {
this.arrowKeyScrolling = true;
this[direction + 'Image']();
setTimeout(() => {
this.arrowKeyScroll(direction);
}, ARROW_SCROLL_RATE_MS);
} else {
window.clearTimeout(this.arrowDownDelayTimeout);
this.arrowKeyScrolling = false;
this.scrollToFocused();
}
},
directionByKey(keyCode) {
let direction;
if (keyCode === ARROW_LEFT) {
direction = 'prev';
} }
return disabled; if (keyCode === ARROW_RIGHT) {
direction = 'next';
}
return direction;
},
isLeftOrRightArrowKey(keyCode) {
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
} }
} }
}; };

View File

@ -4,6 +4,10 @@
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
&:focus {
outline: none;
}
> * + * { > * + * {
margin-top: $interiorMargin; margin-top: $interiorMargin;
} }
@ -25,14 +29,57 @@
} }
} }
&__control-bar { &__control-bar,
padding: 5px 0 0 0; &__time {
display: flex; display: flex;
align-items: center; align-items: baseline;
> * + * {
margin-left: $interiorMarginSm;
}
}
&__control-bar {
margin-top: 2px;
padding: $interiorMarginSm 0;
justify-content: space-between;
}
&__time {
flex: 0 1 auto;
overflow: hidden;
}
&__timestamp,
&__age {
@include ellipsize();
flex: 0 1 auto;
} }
&__timestamp { &__timestamp {
flex: 1 1 auto; flex-shrink: 10;
}
&__age {
border-radius: $controlCr;
display: flex;
flex-shrink: 0;
align-items: baseline;
padding: 1px $interiorMarginSm;
&:before {
opacity: 0.5;
margin-right: $interiorMarginSm;
}
}
&--new {
// New imagery
$bgColor: $colorOk;
background: rgba($bgColor, 0.5);
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
} }
&__thumbs-wrapper { &__thumbs-wrapper {
@ -151,6 +198,8 @@
/*************************************** BUTTONS */ /*************************************** BUTTONS */
.c-button.pause-play { .c-button.pause-play {
// Pause icon set by default in markup // Pause icon set by default in markup
justify-self: end;
&.is-paused { &.is-paused {
background: $colorPausedBg !important; background: $colorPausedBg !important;
color: $colorPausedFg; color: $colorPausedFg;

View File

@ -50,6 +50,18 @@
} }
/************************** EFFECTS */ /************************** EFFECTS */
@mixin flash($animName: flash, $dur: 500ms, $dir: alternate, $iter: 20, $prop: background, $valStart: rgba($colorOk, 1), $valEnd: rgba($colorOk, 0)) {
@keyframes #{$animName} {
0% { #{$prop}: $valStart; }
100% { #{$prop}: $valEnd; }
}
animation-name: $animName;
animation-duration: $dur;
animation-direction: $dir;
animation-iteration-count: $iter;
animation-timing-function: ease-out;
}
@mixin mixedBg() { @mixin mixedBg() {
$c1: nth($mixedSettingBg, 1); $c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2); $c2: nth($mixedSettingBg, 2);