@@ -35,7 +34,7 @@ import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Checkbox from 'primevue/checkbox';
import ddb from 'ddb';
-import { MeasurementStorage } from '@/libs/map/measurementStorage';
+import { discoverSidecars } from '@/libs/sidecarUtils';
export default {
components: {
@@ -44,16 +43,16 @@ export default {
props: {
file: { type: Object, required: true },
- busy: { type: Boolean, default: false }
+ busy: { type: Boolean, default: false },
+ allEntries: { type: Array, default: () => [] }
},
emits: ['onClose'],
data: function () {
return {
renameText: null,
- hasMeasurementsFile: false,
- renameMeasurements: true, // Checked by default
- checkingMeasurements: false
+ sidecars: [],
+ renameSidecars: true // Checked by default
};
},
mounted: function () {
@@ -64,8 +63,7 @@ export default {
// completed and PrimeVue's own focus handling can no longer steal it.
this.focusInput();
- // Check if measurements file exists
- this.checkForMeasurementsFile();
+ this.sidecars = discoverSidecars(this.allEntries, this.file.entry.path);
},
methods: {
focusInput() {
@@ -81,37 +79,6 @@ export default {
}
});
},
- async checkForMeasurementsFile() {
- // Check only for point clouds
- if (this.file.entry.type !== ddb.entry.type.POINTCLOUD) {
- this.hasMeasurementsFile = false;
- return;
- }
-
- this.checkingMeasurements = true;
-
- try {
- // Use dataset from parent to create MeasurementStorage
- const dataset = this.$parent.dataset || this.$root.dataset;
- if (!dataset) {
- console.warn('Dataset not available for measurements check');
- this.hasMeasurementsFile = false;
- return;
- }
-
- const storage = new MeasurementStorage(dataset, this.file.entry);
- this.hasMeasurementsFile = await storage.exists();
-
- if (this.hasMeasurementsFile) {
- console.log('Measurements file found:', storage.measurementPath);
- }
- } catch (e) {
- console.error('Error checking for measurements file:', e);
- this.hasMeasurementsFile = false;
- } finally {
- this.checkingMeasurements = false;
- }
- },
close: function (buttonId) {
if (this.busy && buttonId !== 'rename' && buttonId !== 'renameddb') return;
this.$emit('onClose', buttonId);
@@ -146,39 +113,21 @@ export default {
? basePath + '/' + this.renameText
: this.renameText;
- // If it's a point cloud with measurements, handle rename for that too
- if (this.file.entry.type === ddb.entry.type.POINTCLOUD &&
- this.hasMeasurementsFile &&
- this.renameMeasurements) {
-
- try {
- // Calculate the new path for the measurements file
- const dataset = this.$parent.dataset || this.$root.dataset;
- if (dataset) {
- const oldStorage = new MeasurementStorage(dataset, this.file.entry);
- const oldMeasurementsPath = oldStorage.measurementPath;
-
- // Calculate new measurements path based on new name
- const newPathWithoutExt = newPath.substring(0, newPath.lastIndexOf('.'));
- const newMeasurementsPath = `${newPathWithoutExt}_measurements.geojson`;
-
- console.log(`Will rename measurements: ${oldMeasurementsPath} -> ${newMeasurementsPath}`);
-
- // Emit event with both paths
- this.$emit('onClose', "rename", newPath, this.file.entry, {
- renameMeasurements: true,
- oldMeasurementsPath: oldMeasurementsPath,
- newMeasurementsPath: newMeasurementsPath
- });
- return;
- }
- } catch (e) {
- console.error('Error preparing measurements rename:', e);
- // Continue anyway with normal rename
- }
+ // If sidecar files were found and the checkbox is checked, rename them too
+ if (this.renameSidecars && this.sidecars.length > 0) {
+ const oldBase = this.file.entry.path.replace(/\.[^./\\]+$/, '');
+ const newBase = newPath.replace(/\.[^./\\]+$/, '');
+
+ const sidecarsInfo = this.sidecars.map(sc => ({
+ oldPath: sc.path,
+ newPath: newBase + sc.path.substring(oldBase.length)
+ }));
+
+ this.$emit('onClose', "rename", newPath, this.file.entry, { sidecars: sidecarsInfo });
+ return;
}
- // Normal emit for files without measurements or with unchecked checkbox
+ // Normal emit for files without sidecars or with unchecked checkbox
this.$emit('onClose', "rename", newPath, this.file.entry);
}
}
@@ -191,7 +140,7 @@ export default {
width: 100%;
}
-.measurements-rename-option {
+.sidecar-rename-option {
margin-top: var(--ddb-spacing-lg);
padding: var(--ddb-spacing-md);
background: rgba(var(--ddb-primary-rgb), 0.1);
@@ -199,21 +148,20 @@ export default {
border-left: 0.25rem solid var(--ddb-primary);
}
-.measurements-rename-option label {
+.sidecar-rename-option label {
color: var(--ddb-text);
font-weight: 500;
}
-.measurements-rename-option .hint {
+.sidecar-list {
+ margin: 0;
+ padding-left: 1.25rem;
font-size: 0.75rem;
color: var(--ddb-text-muted);
- margin-top: 0.25rem;
- display: flex;
- align-items: center;
- gap: 0.25rem;
+ list-style: disc;
}
-.measurements-rename-option .hint i {
- font-size: var(--ddb-font-size-base);
+.sidecar-list li {
+ margin-bottom: 0.15rem;
}
diff --git a/webapp/js/features/dataset/explorer/TableView.vue b/webapp/js/features/dataset/explorer/TableView.vue
index a2f22c4..ccb4229 100644
--- a/webapp/js/features/dataset/explorer/TableView.vue
+++ b/webapp/js/features/dataset/explorer/TableView.vue
@@ -30,8 +30,12 @@
@row-contextmenu="onRowContextMenu">
-
-
+
+
+
+
+
+
@@ -76,7 +80,10 @@
- {{ getModifiedDate(data) }}
+
+
{{ getModifiedDate(data) }}
+
{{ getFileModifiedDateRel(data) }}
+
@@ -97,16 +104,15 @@
import Toolbar from '@/components/Toolbar.vue';
import Keyboard from '@/libs/keyboard';
import Mouse from '@/libs/mouse';
-import { clone } from '@/libs/utils';
+import { clone, formatTimeAgo, bytesToSize } from '@/libs/utils';
import { dragDropMixin } from '@/libs/dragDropUtils';
import emitter from '@/libs/eventBus';
import { buildStandardContextMenu } from '@/libs/contextMenuItems';
import { isBuildableFile, hasActiveBuild, isBuildLoading, getBuildBadge, getBuildBadgeTooltip, buildFile } from '@/libs/build/buildHelpers';
import { getTypeDisplayName } from '@/libs/entryTypes';
-import { bytesToSize } from '@/libs/utils';
import ddb from 'ddb';
-const { pathutils, entry } = ddb;
+const { pathutils, entry, thumbs } = ddb;
import ContextMenu from '@/components/ContextMenu';
import Breadcrumb from 'primevue/breadcrumb';
@@ -125,7 +131,7 @@ export default {
Toolbar, ContextMenu, Breadcrumb, DataTable, Column
},
emits: ['openItem', 'openAsText', 'moveSelectedItems', 'openProperties', 'shareEmbed', 'downloadItems', 'transferSelectedItems', 'setAsCover', 'createFolder', 'deleteSelecteditems', 'selectionChanged', 'buildStarted', 'buildError', 'mergeMultispectral', 'maskBorders', 'extractItem', 'copySelectedItems', 'cutSelectedItems', 'pasteFromClipboard', 'downloadBuildArtifact'],
- props: ['files', 'currentPath', 'tools', 'dataset', 'viewMode', 'canWrite', 'isLoadingFiles'],
+ props: ['files', 'currentPath', 'tools', 'dataset', 'viewMode', 'expandedGrid', 'canWrite', 'isLoadingFiles'],
inject: { showBuildConfirm: { default: null } },
data: function () {
const self = this;
@@ -153,7 +159,8 @@ export default {
contextMenu,
sortColumn: 'name',
sortDirection: 'asc',
- rangeStartIdx: null
+ rangeStartIdx: null,
+ thumbnailCache: new Map() // path -> thumbnail URL, or null if unsupported/unavailable
};
},
watch: {
@@ -161,7 +168,15 @@ export default {
handler() {
// Reset loading state when files change (folder opened)
this.loading = false;
+ if (this.expandedGrid) this.prepareThumbnails();
}
+ },
+ expandedGrid: {
+ handler(enabled) {
+ if (enabled) this.prepareThumbnails();
+ else this.thumbnailCache.clear();
+ },
+ immediate: true
}
},
computed: {
@@ -415,6 +430,38 @@ export default {
return date.toLocaleString();
},
+ getFileModifiedDateRel: function(file) {
+ if (!file.entry.mtime) return '';
+
+ return formatTimeAgo(file.entry.mtime * 1000);
+ },
+
+ // Returns the cached thumbnail URL for a file, or null if unsupported/not yet available
+ getThumbnail: function(file) {
+ const cached = this.thumbnailCache.get(file.entry.path);
+ return cached || null;
+ },
+
+ // Thumbnail failed to load (e.g. build not ready) - fall back to icon
+ onThumbError: function(file) {
+ this.thumbnailCache.delete(file.entry.path);
+ },
+
+ // Populate thumbnail URLs for the current file list (synchronous URL construction for ddb:// paths)
+ prepareThumbnails: function() {
+ this.thumbnailCache.clear();
+ if (!this.files) return;
+
+ for (const file of this.files) {
+ if (!thumbs.supportedForType(file.entry.type)) continue;
+ try {
+ this.thumbnailCache.set(file.entry.path, thumbs.fetch(file.path, 48));
+ } catch (e) {
+ // Unsupported URI scheme or fetch error - fall back to icon
+ }
+ }
+ },
+
// Build management methods (delegated to shared helpers)
isBuildableFile: function(file) {
return isBuildableFile(this.dataset, file);
@@ -561,6 +608,15 @@ export default {
color: var(--ddb-text-muted);
}
+ .thumb-preview {
+ width: 48px;
+ height: 48px;
+ object-fit: cover;
+ border-radius: 4px;
+ display: block;
+ margin: 0 auto;
+ }
+
.file-name {
word-break: break-all;
}
@@ -573,5 +629,11 @@ export default {
.file-date {
white-space: nowrap;
}
+
+ .file-date .relative-time {
+ font-size: 0.75rem;
+ color: var(--ddb-text-muted);
+ display: block;
+ }
}
diff --git a/webapp/js/features/datasets/Datasets.vue b/webapp/js/features/datasets/Datasets.vue
index c2968ed..fc470d6 100644
--- a/webapp/js/features/datasets/Datasets.vue
+++ b/webapp/js/features/datasets/Datasets.vue
@@ -100,7 +100,12 @@
- {{ formatDate(slotProps.data.creationDate) }}
+
+
+
{{ formatDate(slotProps.data.creationDate) }}
+
{{ formatCreationDateRel(slotProps.data.creationDate) }}
+
+
@@ -172,7 +177,7 @@