Skip to content

Commit 19cf409

Browse files
authored
Merge pull request #225 from LCOGT/feature/grid-cards
Feature/grid cards
2 parents 13ea59b + 92d4391 commit 19cf409

9 files changed

Lines changed: 123 additions & 90 deletions

File tree

src/assets/css/ptr-extra.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ p {
8484
color: var(--text);
8585
}
8686

87+
.prevent-select {
88+
-webkit-touch-callout: none;
89+
-webkit-user-select: none;
90+
-khtml-user-select: none;
91+
-moz-user-select: none;
92+
-ms-user-select: none;
93+
user-select: none;
94+
}
95+
96+
.single-line-text {
97+
overflow: hidden;
98+
text-overflow: ellipsis;
99+
white-space: nowrap;
100+
}
101+
87102
.datalab-site-menu {
88103
font-family: var(--font-headers);
89104
text-transform: uppercase;

src/components/DataSession/DataSession.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function addCompletedOperation(operation) {
5353
operation.output.output_files.forEach(outputFile => {
5454
outputFile.operation = operation.id
5555
outputFile.operationIndex = operation.index
56+
outputFile.operationName = operation.name
5657
if (!imagesContainsFile(outputFile)) {
5758
images.value.push(outputFile)
5859
}

src/components/DataSession/OperationWizard.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ function selectOperation(name) {
287287
:selected-images="operationInputs[inputKey].map(image => image.basename)"
288288
:column-span="calculateColumnSpan(images.length, IMAGES_PER_ROW)"
289289
:allow-selection="true"
290+
:enable-image-cards="false"
290291
@select-image="selectImage(inputKey, $event)"
291292
/>
292293
</div>

src/components/Global/FilterBadge.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const props = defineProps({
1111
</script>
1212
<template>
1313
<p
14-
class="filter-badge rounded d-flex pa-2 justify-center align-center"
14+
class="filter-badge rounded d-flex pa-2 justify-center align-center prevent-select"
1515
:style="{ backgroundColor: filterToColor(props.filter) }"
1616
>
1717
{{ props.filter }}
@@ -20,7 +20,6 @@ const props = defineProps({
2020
<style scoped>
2121
.filter-badge {
2222
color: var(--text);
23-
user-select: none;
2423
font-size: smaller;
2524
font-weight: bolder;
2625
max-width: 2.2rem;

src/components/Analysis/ImageDownloadMenu.vue renamed to src/components/Global/ImageDownloadMenu.vue

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script setup>
22
import { useAlertsStore } from '@/stores/alerts'
33
import { useAnalysisStore } from '@/stores/analysis'
4+
import { useThumbnailsStore } from '@/stores/thumbnails'
5+
import { useConfigurationStore } from '@/stores/configuration'
46
57
const props = defineProps({
68
imageName: {
79
type: String,
810
required: true,
9-
default: null,
1011
},
1112
fitsUrl: {
1213
type: String,
@@ -18,17 +19,34 @@ const props = defineProps({
1819
required: false,
1920
default: null,
2021
},
22+
enableScaledDownload: {
23+
type: Boolean,
24+
required: false,
25+
default: true,
26+
},
27+
speedDialLocation: {
28+
type: String,
29+
required: false,
30+
default: 'left center',
31+
}
2132
})
2233
2334
defineEmits(['analysisAction'])
2435
2536
const alertStore = useAlertsStore()
2637
const analysisStore = useAnalysisStore()
38+
const thumbnailsStore = useThumbnailsStore()
39+
const configurationStore = useConfigurationStore()
2740
2841
function downloadBase64File(base64Data, filename, fileType='file'){
2942
downloadFile('data:image/jpeg;base64,' + base64Data, filename, fileType)
3043
}
3144
45+
async function downloadJpg(jpgUrl, filename, fileType='file'){
46+
const urlToDownload = jpgUrl || await thumbnailsStore.cacheImage('large', configurationStore.archiveType, jpgUrl, props.imageName)
47+
downloadFile(urlToDownload, filename, fileType)
48+
}
49+
3250
function downloadFile(file, filename, fileType='file'){
3351
try{
3452
const a = document.createElement('a')
@@ -44,13 +62,14 @@ function downloadFile(file, filename, fileType='file'){
4462
<template>
4563
<v-speed-dial
4664
variant="text"
47-
location="left center"
65+
:location="props.speedDialLocation"
4866
transition="fade-transition"
4967
>
5068
<template #activator="{ props: activatorProps }">
51-
<v-btn
69+
<v-icon
5270
v-bind="activatorProps"
5371
icon="mdi-download"
72+
color="var(--secondary-interactive)"
5473
/>
5574
</template>
5675
<v-btn
@@ -63,22 +82,23 @@ function downloadFile(file, filename, fileType='file'){
6382
<v-btn
6483
key="2"
6584
class="file-download"
66-
text=".TIF"
67-
@click="$emit('analysisAction', 'get-tif', {'basename': props.imageName}, downloadFile)"
68-
/>
69-
<v-btn
70-
v-if="props.jpgUrl"
71-
key="3"
72-
class="file-download"
73-
text="Small .JPG"
74-
@click="downloadFile(props.jpgUrl, props.imageName, 'Small JPG')"
75-
/>
76-
<v-btn
77-
key="4"
78-
class="file-download"
79-
text="Scaled .JPG"
80-
@click="$emit('analysisAction', 'get-jpg', {'basename': props.imageName, 'zmin': analysisStore.zmin, 'zmax': analysisStore.zmax}, downloadBase64File)"
85+
text=".JPG"
86+
@click="downloadJpg(props.jpgUrl, props.imageName, 'JPG')"
8187
/>
88+
<template v-if="props.enableScaledDownload">
89+
<v-btn
90+
key="3"
91+
class="file-download"
92+
text=".TIF"
93+
@click="$emit('analysisAction', 'get-tif', {'basename': props.imageName}, downloadFile)"
94+
/>
95+
<v-btn
96+
key="4"
97+
class="file-download"
98+
text="Scaled .JPG"
99+
@click="$emit('analysisAction', 'get-jpg', {'basename': props.imageName, 'zmin': analysisStore.zmin, 'zmax': analysisStore.zmax}, downloadBase64File)"
100+
/>
101+
</template>
82102
</v-speed-dial>
83103
</template>
84104
<style scoped>

src/components/Global/ImageGrid.vue

Lines changed: 61 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { ref, watch } from 'vue'
33
import { useThumbnailsStore } from '@/stores/thumbnails'
44
import { useConfigurationStore } from '@/stores/configuration'
55
import { useAlertsStore } from '@/stores/alerts'
6+
import ImageDownloadMenu from '@/components/Global/ImageDownloadMenu.vue'
67
import FilterBadge from './FilterBadge.vue'
78
import AnalysisView from '../../views/AnalysisView.vue'
89
910
const props = defineProps({
1011
images: {
11-
type: [Array, Boolean],
12-
default: false
12+
type: Array,
13+
default: () => []
1314
},
1415
selectedImages: {
1516
type: Array,
@@ -22,6 +23,10 @@ const props = defineProps({
2223
allowSelection: {
2324
type: Boolean,
2425
default: false
26+
},
27+
enableImageCards: {
28+
type: Boolean,
29+
default: true
2530
}
2631
})
2732
@@ -32,38 +37,21 @@ const emit = defineEmits(['selectImage'])
3237
const showAnalysisDialog = ref(false)
3338
const imageDetails = ref({})
3439
const analysisImage = ref({})
35-
var doubleClickTimer = 0
3640
37-
const handleClick = (basename) => {
38-
clearTimeout(doubleClickTimer)
39-
// timeout length indicates how long to wait for a second click before treating as a single click
40-
doubleClickTimer = setTimeout(() => {
41-
emit('selectImage', basename)
42-
doubleClickTimer = 0
43-
}, 250)
41+
async function ensureLargeCachedUrl(image) {
42+
if (!image.largeCachedUrl) {
43+
const url = image.large_url || image.largeThumbUrl || ''
44+
image.largeCachedUrl = await thumbnailsStore.cacheImage('large', configurationStore.archiveType, url, image.basename)
45+
}
46+
return image.largeCachedUrl
4447
}
4548
46-
const handleDoubleClick = (image) => {
47-
clearTimeout(doubleClickTimer)
49+
const launchAnalysis = async (image) => {
4850
alertsStore.setAlert('info', `Opening ${image?.basename} for analysis`)
49-
launchAnalysis(image)
50-
}
51-
52-
const launchAnalysis = (image) => {
5351
try {
54-
if (!image.largeCachedUrl) {
55-
image.largeCachedUrl = ref('')
56-
const url = image.large_url || image.largeThumbUrl || ''
57-
thumbnailsStore.cacheImage('large', configurationStore.archiveType, url, image.basename).then((cachedUrl) => {
58-
image.largeCachedUrl = cachedUrl
59-
analysisImage.value = image
60-
showAnalysisDialog.value = true
61-
})
62-
}
63-
else {
64-
analysisImage.value = image
65-
showAnalysisDialog.value = true
66-
}
52+
await ensureLargeCachedUrl(image)
53+
analysisImage.value = image
54+
showAnalysisDialog.value = true
6755
} catch {
6856
alertsStore.setAlert('error', `Failed to open ${image?.basename}`)
6957
}
@@ -74,7 +62,6 @@ const isSelected = (basename) => {
7462
}
7563
7664
watch(() => props.images, () => {
77-
if (!props.images) return
7865
props.images.forEach(image => {
7966
if (image.basename && !(image.basename in imageDetails.value)) {
8067
imageDetails.value[image.basename] = ref('')
@@ -90,22 +77,27 @@ watch(() => props.images, () => {
9077
9178
<template>
9279
<v-row>
93-
<template v-if="props.images">
94-
<v-col
95-
v-for="(image, index) in props.images"
96-
:key="index"
97-
:cols="columnSpan"
98-
class="image-grid-col"
80+
<v-col
81+
v-for="(image, index) in props.images"
82+
:key="index"
83+
:cols="columnSpan"
84+
class="image-grid-col"
85+
>
86+
<v-sheet
87+
v-if="image.basename in imageDetails && imageDetails[image.basename]"
88+
class="pa-2"
89+
color="var(--secondary-background)"
90+
:elevation="2"
91+
rounded
92+
:class="{ 'selected-image': isSelected(image.basename) }"
93+
@click="emit('selectImage', image.basename)"
9994
>
10095
<v-img
101-
v-if="image.basename in imageDetails && imageDetails[image.basename]"
10296
:src="imageDetails[image.basename]"
10397
:alt="image.basename"
98+
rounded
10499
cover
105-
:class="{ 'selected-image': isSelected(image.basename) }"
106100
aspect-ratio="1"
107-
@click="handleClick(image.basename)"
108-
@dblclick="handleDoubleClick(image)"
109101
>
110102
<filter-badge
111103
v-if="image.filter || image.FILTER"
@@ -116,33 +108,37 @@ watch(() => props.images, () => {
116108
class="image-text-overlay"
117109
>{{ image.operationIndex }}</span>
118110
</v-img>
119-
<v-skeleton-loader
120-
v-else
121-
type="card"
122-
color="var(--secondary-background)"
123-
bg-color="var(--primary-background)"
124-
/>
125-
</v-col>
126-
</template>
127-
<template v-else>
128-
<v-col
129-
v-for="n in 10"
130-
:key="n"
131-
:cols="columnSpan"
132-
class="image-grid-col"
133-
>
134-
<v-skeleton-loader
135-
type="card"
136-
class="ma-1"
137-
color="var(--secondary-background)"
138-
bg-color="var(--primary-background)"
139-
/>
140-
</v-col>
141-
</template>
111+
<div
112+
v-if="props.enableImageCards"
113+
class="d-flex flex-row ga-2 align-center mt-2"
114+
>
115+
<p class="text-subtitle-2 mr-auto prevent-select single-line-text">
116+
{{ image.target_name || image.operationName }}
117+
</p>
118+
<v-icon
119+
icon="mdi-eye"
120+
color="var(--primary-interactive)"
121+
@click.stop="launchAnalysis(image)"
122+
/>
123+
<image-download-menu
124+
:fits-url="image.url || image.fits_url || ''"
125+
:jpg-url="image.largeCachedUrl || image.large_url || ''"
126+
:image-name="image.basename"
127+
speed-dial-location="top right"
128+
:enable-scaled-download="false"
129+
/>
130+
</div>
131+
</v-sheet>
132+
<v-skeleton-loader
133+
v-else
134+
type="card"
135+
color="var(--secondary-background)"
136+
bg-color="var(--primary-background)"
137+
/>
138+
</v-col>
142139
</v-row>
143140
<v-dialog
144141
v-model="showAnalysisDialog"
145-
persistent
146142
fullscreen
147143
>
148144
<analysis-view
@@ -154,9 +150,8 @@ watch(() => props.images, () => {
154150
155151
<style scoped>
156152
.selected-image {
157-
border: 0.3rem solid var(--primary-interactive);
153+
outline: 0.3rem solid var(--primary-interactive);
158154
}
159-
160155
.image-text-overlay {
161156
color: var(--text);
162157
font-weight: bold;

src/components/Project/CreateSessionDialog.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ async function addImagesToExistingSession(session){
4141
'basename': image.basename.replace('-small', '') || image.basename.replace('-large', ''),
4242
'id': image.id,
4343
'url': image.url,
44-
'filter': image.FILTER
44+
'filter': image.FILTER,
45+
'target_name': image.target_name
4546
}))]
4647
const requestBody = {
4748
'name': session.name,
@@ -61,7 +62,8 @@ async function createNewDataSession(){
6162
'basename': image.basename.replace('-small', '') || image.basename.replace('-large', ''),
6263
'id': image.id,
6364
'url': image.url,
64-
'filter': image.FILTER
65+
'filter': image.FILTER,
66+
'target_name': image.target_name
6567
}))
6668
const requestBody = {
6769
'name': newSessionName.value,

src/views/AnalysisView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useUserDataStore } from '@/stores/userData'
77
import FilterBadge from '@/components/Global/FilterBadge.vue'
88
import NonLinearSlider from '@/components/Global/NonLinearSlider.vue'
99
import HistogramSlider from '@/components/Global/Scaling/HistogramSlider.vue'
10-
import ImageDownloadMenu from '@/components/Analysis/ImageDownloadMenu.vue'
10+
import ImageDownloadMenu from '@/components/Global/ImageDownloadMenu.vue'
1111
import FitsHeaderTable from '@/components/Analysis/FitsHeaderTable.vue'
1212
import ImageViewer from '@/components/Analysis/ImageViewer.vue'
1313
import LinePlot from '@/components/Analysis/LinePlot.vue'

src/views/ProjectView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ onMounted(() => {
349349
@select-image="selectImage(proposal.id, $event)"
350350
/>
351351
<div
352-
v-if="imagesByProposal[proposal.id]?.length == 0"
352+
v-if="imagesByProposal[proposal.id]?.length == 0 && !loadingProposals"
353353
class="mt-4 d-flex flex-column justify-center align-center"
354354
>
355355
<v-icon

0 commit comments

Comments
 (0)