+
+
+
@@ -30,19 +36,22 @@ import { computed, ref } from 'vue'
import { t } from '@nextcloud/l10n'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcRichText from '@nextcloud/vue/components/NcRichText'
import DeleteIcon from '@icons/Delete.vue'
import { contrastColor } from './noteColors'
import type { Note } from '@/api/types'
-const props = withDefaults(defineProps<{ note: Note; draggableEnabled?: boolean }>(), {
- draggableEnabled: true,
-})
+const props = withDefaults(
+ defineProps<{ note: Note; draggableEnabled?: boolean; selected?: boolean }>(),
+ { draggableEnabled: true, selected: false },
+)
const emit = defineEmits<{
edit: [note: Note]
delete: [note: Note]
'drag-start': [noteId: number]
'reorder-over': [noteId: number, event: MouseEvent]
+ select: [noteId: number]
}>()
const isDragging = ref(false)
@@ -89,8 +98,9 @@ const strings = {
color: var(--note-fg, inherit);
border: 1px solid var(--color-border);
transition:
- box-shadow 0.15s ease,
- transform 0.15s ease;
+ box-shadow 0.2s ease,
+ transform 0.2s ease,
+ outline-color 0.2s ease;
min-height: 80px;
max-height: 240px;
display: flex;
@@ -112,13 +122,53 @@ const strings = {
cursor: grabbing;
}
+ &--selected {
+ outline: 3px solid var(--color-primary-element);
+ outline-offset: -3px;
+ }
+
+ &__select {
+ position: absolute;
+ top: 0.25rem;
+ inset-inline-start: 0.25rem;
+ z-index: 2;
+ opacity: 0;
+ transition:
+ opacity 0.2s ease,
+ background 0.2s ease;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 99px;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.45);
+ }
+
+ :deep(.checkbox-radio-switch__content) {
+ transition:
+ color 0.2s ease,
+ opacity 0.2s ease,
+ border-radius 0.2s ease;
+ }
+ }
+
+ &--selected &__select,
+ &:hover &__select {
+ opacity: 1;
+ }
+
+ @media (hover: none) {
+ .note-card__select {
+ opacity: 1;
+ }
+ }
+
&__actions {
position: absolute;
top: 0.25rem;
inset-inline-end: 0.25rem;
z-index: 1;
opacity: 0;
- transition: opacity 0.15s ease;
+ transition: opacity 0.2s ease;
background: rgba(0, 0, 0, 0.3);
border-radius: 99px;
}
diff --git a/src/components/Photos/PhotoCard.test.ts b/src/components/Photos/PhotoCard.test.ts
index 0391c96..1b799d9 100644
--- a/src/components/Photos/PhotoCard.test.ts
+++ b/src/components/Photos/PhotoCard.test.ts
@@ -32,6 +32,14 @@ vi.mock('@nextcloud/vue/components/NcActionButton', () => ({
template: '',
},
}))
+vi.mock('@nextcloud/vue/components/NcCheckboxRadioSwitch', () => ({
+ default: {
+ name: 'NcCheckboxRadioSwitch',
+ template:
+ '',
+ props: ['modelValue'],
+ },
+}))
import PhotoCard from './PhotoCard.vue'
diff --git a/src/components/Photos/PhotoCard.vue b/src/components/Photos/PhotoCard.vue
index 68402a0..2a02228 100644
--- a/src/components/Photos/PhotoCard.vue
+++ b/src/components/Photos/PhotoCard.vue
@@ -1,7 +1,7 @@
![]()
+
+
+
@@ -42,14 +48,15 @@ import { t } from '@nextcloud/l10n'
import { photoPreviewUrl } from '@/api/images'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import PencilIcon from '@icons/Pencil.vue'
import DeleteIcon from '@icons/Delete.vue'
import ArrowUpIcon from '@icons/ArrowUp.vue'
import type { Photo } from '@/api/types'
const props = withDefaults(
- defineProps<{ photo: Photo; houseId: number; reorderEnabled?: boolean }>(),
- { reorderEnabled: true },
+ defineProps<{ photo: Photo; houseId: number; reorderEnabled?: boolean; selected?: boolean }>(),
+ { reorderEnabled: true, selected: false },
)
const emit = defineEmits<{
preview: [photo: Photo]
@@ -58,6 +65,7 @@ const emit = defineEmits<{
'move-to-root': [photo: Photo]
'drag-start': [photoId: number]
'reorder-over': [photoId: number, event: MouseEvent]
+ select: [photoId: number]
}>()
const isDragging = ref(false)
@@ -105,8 +113,9 @@ const strings = {
cursor: grab;
aspect-ratio: 1;
transition:
- box-shadow 0.15s ease,
- transform 0.15s ease;
+ box-shadow 0.2s ease,
+ transform 0.2s ease,
+ outline-color 0.2s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
@@ -133,13 +142,53 @@ const strings = {
cursor: inherit;
}
+ &--selected {
+ outline: 3px solid var(--color-primary-element);
+ outline-offset: -3px;
+ }
+
+ &__select {
+ position: absolute;
+ top: 0.25rem;
+ inset-inline-start: 0.25rem;
+ z-index: 2;
+ opacity: 0;
+ transition:
+ opacity 0.2s ease,
+ background 0.2s ease;
+ background: rgba(0, 0, 0, 0.45);
+ border-radius: 99px;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.6);
+ }
+
+ :deep(.checkbox-radio-switch__content) {
+ transition:
+ color 0.2s ease,
+ opacity 0.2s ease,
+ border-radius 0.2s ease;
+ }
+ }
+
+ &--selected &__select,
+ &:hover &__select {
+ opacity: 1;
+ }
+
+ @media (hover: none) {
+ .photo-card__select {
+ opacity: 1;
+ }
+ }
+
&__actions {
position: absolute;
top: 0.25rem;
inset-inline-end: 0.25rem;
z-index: 1;
opacity: 0;
- transition: opacity 0.15s ease;
+ transition: opacity 0.2s ease;
background: rgba(0, 0, 0, 0.45);
border-radius: 99px;
}
diff --git a/src/views/NotesView.vue b/src/views/NotesView.vue
index 4da826a..a2e9676 100644
--- a/src/views/NotesView.vue
+++ b/src/views/NotesView.vue
@@ -46,25 +46,39 @@
-
+
+
+
+ {{ selectionLabel }}
+
+
+ {{ strings.delete }}
+
+ {{ strings.clearSelection }}
+
+
+
+
@@ -90,12 +104,27 @@
{{ strings.delete }}
+
+
+
!v && (bulkDeleting = false)"
+ >
+ {{ bulkDeleteBody }}
+
+ {{ strings.cancel }}
+ {{ strings.delete }}
+
+
@@ -375,6 +447,21 @@ const strings = {
padding: 2rem;
}
+.pantry-selection-bar {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 1rem;
+ margin: 0 1rem 0.75rem;
+ background: var(--color-primary-element-light);
+ border-radius: var(--border-radius-large, 12px);
+ font-weight: 500;
+
+ span:first-child {
+ flex: 1;
+ }
+}
+
.pantry-sort-active {
font-weight: 600;
}
diff --git a/src/views/PhotosView.vue b/src/views/PhotosView.vue
index 6260df5..a9f7792 100644
--- a/src/views/PhotosView.vue
+++ b/src/views/PhotosView.vue
@@ -59,6 +59,20 @@
{{ strings.dropToUpload }}
+
+
+ {{ photoSelectionLabel }}
+
+
+ {{ strings.moveToFolder }}
+
+
+
+ {{ strings.delete }}
+
+ {{ strings.clearSelection }}
+
+
@@ -124,12 +138,14 @@
:photo="item.photo"
:house-id="houseIdNum"
:reorder-enabled="isCustomSort"
+ :selected="selectedPhotoIds.has(item.photo.id)"
@preview="openPreview"
@edit="startEditPhoto"
@delete="confirmDeletePhoto"
@move-to-root="movePhotoToRoot"
@drag-start="onPhotoDragStart"
@reorder-over="(id, e) => onReorderOver(id, rootPhotos, e)"
+ @select="togglePhotoSelection"
/>
@@ -169,12 +185,14 @@
:photo="item.photo"
:house-id="houseIdNum"
:reorder-enabled="isCustomSort"
+ :selected="selectedPhotoIds.has(item.photo.id)"
@preview="openPreview"
@edit="startEditPhoto"
@delete="confirmDeletePhoto"
@move-to-root="movePhotoToRoot"
@drag-start="onPhotoDragStart"
@reorder-over="(id, e) => onReorderOver(id, activeFolderPhotos, e)"
+ @select="togglePhotoSelection"
/>