|  | <!-- Copyright (C) 2017 The Android Open Source Project | 
|  |  | 
|  | Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | you may not use this file except in compliance with the License. | 
|  | You may obtain a copy of the License at | 
|  |  | 
|  | http://www.apache.org/licenses/LICENSE-2.0 | 
|  |  | 
|  | Unless required by applicable law or agreed to in writing, software | 
|  | distributed under the License is distributed on an "AS IS" BASIS, | 
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | See the License for the specific language governing permissions and | 
|  | limitations under the License. | 
|  | --> | 
|  | <template> | 
|  | <div id="app"> | 
|  | <md-app> | 
|  | <md-app-toolbar md-tag="md-toolbar"> | 
|  | <h1 class="md-title" style="flex: 1">{{title}}</h1> | 
|  | <md-button | 
|  | class="md-accent md-raised md-theme-default" | 
|  | @click="clear()" | 
|  | v-if="dataLoaded" | 
|  | >Clear</md-button> | 
|  | </md-app-toolbar> | 
|  | <md-app-content class="main-content"> | 
|  | <div class="md-layout m-2" v-if="!dataLoaded"> | 
|  | <dataadb ref="adb" :store="store" @dataReady="onDataReady" @statusChange="setStatus" /> | 
|  | <datainput ref="input" :store="store" @dataReady="onDataReady" @statusChange="setStatus" /> | 
|  | </div> | 
|  | <div class="md-layout md-alignment-left-center " v-if="dataLoaded"> | 
|  | <div class="md-layout-item video" v-if="video"> | 
|  | <videoview :file="video" :ref="video.filename" /> | 
|  | </div> | 
|  | <div class="md-layout-item"> | 
|  | <md-toolbar | 
|  | md-elevation="0" | 
|  | class="md-transparent"> | 
|  | <h2 class="md-title">Timeline </h2> | 
|  | <span> {{seekTime}} </span> | 
|  | <datafilter v-for="file in files" :key="file.filename" :store="store" :file="file" /> | 
|  | </md-toolbar> | 
|  | <md-list> | 
|  | <md-list-item v-for="(file, idx) in files" :key="file.filename"> | 
|  | <md-icon>{{file.type.icon}}</md-icon> | 
|  | <timeline | 
|  | :items="file.timeline" | 
|  | :selected-index="file.selectedIndex" | 
|  | :scale="scale" | 
|  | @item-selected="onTimelineItemSelected($event, idx)" | 
|  | class="timeline" | 
|  | /> | 
|  | </md-list-item> | 
|  | </md-list> | 
|  | </div> | 
|  | </div> | 
|  | <dataview | 
|  | v-for="file in files" | 
|  | :key="file.filename" | 
|  | :ref="file.filename" | 
|  | :store="store" | 
|  | :file="file" | 
|  | @focus="onDataViewFocus(file.filename)" | 
|  | /> | 
|  | </md-app-content> | 
|  | </md-app> | 
|  | </div> | 
|  | </template> | 
|  | <script> | 
|  | import TreeView from './TreeView.vue' | 
|  | import Timeline from './Timeline.vue' | 
|  | import Rects from './Rects.vue' | 
|  | import DataView from './DataView.vue' | 
|  | import VideoView from './VideoView.vue' | 
|  | import DataInput from './DataInput.vue' | 
|  | import LocalStore from './localstore.js' | 
|  | import DataAdb from './DataAdb.vue' | 
|  | import DataFilter from './DataFilter.vue' | 
|  | import FileType from './FileType.js' | 
|  | import { nanos_to_string } from './transform.js' | 
|  |  | 
|  | const APP_NAME = "Winscope" | 
|  |  | 
|  | // Find the index of the last element matching the predicate in a sorted array | 
|  | function findLastMatchingSorted(array, predicate) { | 
|  | var a = 0; | 
|  | var b = array.length - 1; | 
|  | while (b - a > 1) { | 
|  | var m = Math.floor((a + b) / 2); | 
|  | if (predicate(array, m)) { | 
|  | a = m; | 
|  | } else { | 
|  | b = m - 1; | 
|  | } | 
|  | } | 
|  | return predicate(array, b) ? b : a; | 
|  | } | 
|  |  | 
|  | export default { | 
|  | name: 'app', | 
|  | mixins: [FileType], | 
|  | data() { | 
|  | return { | 
|  | files: [], | 
|  | video: null, | 
|  | title: APP_NAME, | 
|  | currentTimestamp: 0, | 
|  | activeDataView: null, | 
|  | store: LocalStore('app', { | 
|  | flattened: false, | 
|  | onlyVisible: false, | 
|  | displayDefaults: true, | 
|  | }), | 
|  | } | 
|  | }, | 
|  | created() { | 
|  | window.addEventListener('keydown', this.onKeyDown); | 
|  | document.title = this.title; | 
|  | }, | 
|  | methods: { | 
|  | clear() { | 
|  | this.files.forEach(function(item) { item.destroy(); }) | 
|  | this.files = []; | 
|  | this.activeDataView = null; | 
|  | }, | 
|  | onTimelineItemSelected(index, timelineIndex) { | 
|  | this.files[timelineIndex].selectedIndex = index; | 
|  | var t = parseInt(this.files[timelineIndex].timeline[index]); | 
|  | for (var i = 0; i < this.files.length; i++) { | 
|  | if (i != timelineIndex) { | 
|  | this.files[i].selectedIndex = findLastMatchingSorted(this.files[i].timeline, function(array, idx) { | 
|  | return parseInt(array[idx]) <= t; | 
|  | }); | 
|  | } | 
|  | } | 
|  | this.currentTimestamp = t; | 
|  | }, | 
|  | advanceTimeline(direction) { | 
|  | var closestTimeline = -1; | 
|  | var timeDiff = Infinity; | 
|  | for (var idx = 0; idx < this.files.length; idx++) { | 
|  | var file = this.files[idx]; | 
|  | var cur = file.selectedIndex; | 
|  | if (cur + direction < 0 || cur + direction >= this.files[idx].timeline.length) { | 
|  | continue; | 
|  | } | 
|  | var d = Math.abs(parseInt(file.timeline[cur + direction]) - this.currentTimestamp); | 
|  | if (timeDiff > d) { | 
|  | timeDiff = d; | 
|  | closestTimeline = idx; | 
|  | } | 
|  | } | 
|  | if (closestTimeline >= 0) { | 
|  | this.files[closestTimeline].selectedIndex += direction; | 
|  | this.currentTimestamp = parseInt(this.files[closestTimeline].timeline[this.files[closestTimeline].selectedIndex]); | 
|  | } | 
|  | }, | 
|  | onDataViewFocus(view) { | 
|  | this.activeDataView = view; | 
|  | }, | 
|  | onKeyDown(event) { | 
|  | event = event || window.event; | 
|  | if (event.keyCode == 37 /* left */ ) { | 
|  | this.advanceTimeline(-1); | 
|  | } else if (event.keyCode == 39 /* right */ ) { | 
|  | this.advanceTimeline(1); | 
|  | } else if (event.keyCode == 38 /* up */ ) { | 
|  | this.$refs[this.activeView][0].arrowUp(); | 
|  | } else if (event.keyCode == 40 /* down */ ) { | 
|  | this.$refs[this.activeView][0].arrowDown(); | 
|  | } else { | 
|  | return false; | 
|  | } | 
|  | event.preventDefault(); | 
|  | return true; | 
|  | }, | 
|  | onDataReady(files) { | 
|  | for (let i = 0; i < files.length; i++) { | 
|  | const file = files[i]; | 
|  | if (this.isVideo(file)) { | 
|  | this.video = file; | 
|  | } | 
|  | this.files.push(file); | 
|  | } | 
|  | }, | 
|  | setStatus(status) { | 
|  | if (status) { | 
|  | this.title = status; | 
|  | } else { | 
|  | this.title = APP_NAME; | 
|  | } | 
|  | }, | 
|  | }, | 
|  | computed: { | 
|  | prettyDump: function() { return JSON.stringify(this.dump, null, 2); }, | 
|  | dataLoaded: function() { return this.files.length > 0 }, | 
|  | scale() { | 
|  | var mx = Math.max(...(this.files.map(f => Math.max(...f.timeline)))); | 
|  | var mi = Math.min(...(this.files.map(f => Math.min(...f.timeline)))); | 
|  | return [mi, mx]; | 
|  | }, | 
|  | activeView: function() { | 
|  | if (!this.activeDataView && this.files.length > 0) { | 
|  | this.activeDataView = this.files[0].filename; | 
|  | } | 
|  | return this.activeDataView; | 
|  | }, | 
|  | seekTime: function() { | 
|  | return nanos_to_string(this.currentTimestamp); | 
|  | } | 
|  | }, | 
|  | watch: { | 
|  | title() { | 
|  | document.title = this.title; | 
|  | } | 
|  | }, | 
|  | components: { | 
|  | timeline: Timeline, | 
|  | dataview: DataView, | 
|  | videoview: VideoView, | 
|  | datainput: DataInput, | 
|  | dataadb: DataAdb, | 
|  | datafilter: DataFilter | 
|  | } | 
|  | }; | 
|  | </script> | 
|  | <style> | 
|  | .main-content>* { | 
|  | margin: 1em; | 
|  | } | 
|  |  | 
|  | .card-toolbar { | 
|  | border-bottom: 1px solid rgba(0, 0, 0, .12); | 
|  | } | 
|  |  | 
|  | .timeline { | 
|  | margin: 16px; | 
|  | } | 
|  |  | 
|  | .container { | 
|  | display: flex; | 
|  | flex-wrap: wrap; | 
|  | } | 
|  |  | 
|  | .md-button { | 
|  | margin-top: 1em | 
|  | } | 
|  |  | 
|  | .video { | 
|  | flex-grow: 0; | 
|  | } | 
|  |  | 
|  | h1, | 
|  | h2 { | 
|  | font-weight: normal; | 
|  | } | 
|  |  | 
|  | ul { | 
|  | list-style-type: none; | 
|  | padding: 0; | 
|  | } | 
|  |  | 
|  | li { | 
|  | display: inline-block; | 
|  | margin: 0 10px; | 
|  | } | 
|  |  | 
|  | a { | 
|  | color: #42b983; | 
|  | } | 
|  | </style> |