Skip to content

Commit a14e06d

Browse files
committed
feat(ratings): adds rating edition mode
1 parent 60fe888 commit a14e06d

File tree

8 files changed

+213
-25
lines changed

8 files changed

+213
-25
lines changed

src/components/common/numbers/AnimatedNumber.vue

+12-1
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,23 @@ defineProps({
3030
required: false,
3131
default: false,
3232
},
33+
active: {
34+
type: Boolean,
35+
required: false,
36+
default: true,
37+
},
3338
});
3439
</script>
3540

3641
<template>
3742
<NStatistic class="statistics" :class="{ disabled }" tabular-nums>
38-
<NNumberAnimation :from="from" :to="to" :duration="duration" :precision="precision" />
43+
<NNumberAnimation
44+
:from="from"
45+
:to="to"
46+
:duration="duration"
47+
:precision="precision"
48+
:active="active"
49+
/>
3950
<span v-if="unit" class="unit">{{ unit }}</span>
4051
</NStatistic>
4152
</template>

src/components/common/numbers/ProgressNumber.vue

+122-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
<script lang="ts" setup>
22
import { NProgress, NSkeleton, NSpin } from 'naive-ui';
33
4-
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
4+
import {
5+
computed,
6+
onBeforeUnmount,
7+
onMounted,
8+
type PropType,
9+
ref,
10+
toRefs,
11+
watch,
12+
} from 'vue';
513
614
import AnimatedNumber from './AnimatedNumber.vue';
715
816
import { Rating } from '~/models/rating.model';
17+
import { debounce } from '~/utils/debounce.utils';
918
import { wait } from '~/utils/promise.utils';
1019
1120
const props = defineProps({
@@ -42,19 +51,91 @@ const props = defineProps({
4251
required: false,
4352
default: false,
4453
},
54+
editable: {
55+
type: Boolean,
56+
required: false,
57+
default: false,
58+
},
59+
container: {
60+
type: Object as PropType<HTMLElement>,
61+
required: false,
62+
},
63+
transform: {
64+
type: Function as PropType<(progress: number) => number>,
65+
required: false,
66+
},
4567
});
4668
47-
const { progress, delay } = toRefs(props);
69+
const emit = defineEmits<{
70+
(e: 'onEditing', editing: boolean): void;
71+
(e: 'onEdit', progress: number): void;
72+
(e: 'onEditProgress', progress: number): void;
73+
}>();
74+
75+
const { progress, delay, container, duration, editable, transform } = toRefs(props);
4876
4977
const _progress = ref(0);
5078
79+
const progressRef = ref<InstanceType<typeof NProgress>>();
80+
const containerRef = computed(() => container?.value ?? progressRef.value?.$el);
81+
82+
const editing = ref(false);
83+
const angleProgress = ref(0);
84+
85+
const debounceEmitProgress = debounce(
86+
() => emit('onEditProgress', angleProgress.value),
87+
500,
88+
);
89+
90+
const editProgress = computed(() => {
91+
if (!editing.value) return _progress.value;
92+
debounceEmitProgress();
93+
return angleProgress.value;
94+
});
95+
96+
const progressDuration = computed(() =>
97+
editing.value ? 100 : duration.value - delay.value,
98+
);
99+
51100
const color = computed(() => {
52-
if (_progress.value <= Rating.Bad * 10) return 'color-error';
53-
if (_progress.value <= Rating.Mediocre * 10) return 'color-warning';
54-
if (_progress.value <= Rating.Good * 10) return 'color-info';
101+
if (editProgress.value <= Rating.Bad * 10) return 'color-error';
102+
if (editProgress.value <= Rating.Mediocre * 10) return 'color-warning';
103+
if (editProgress.value <= Rating.Good * 10) return 'color-info';
55104
return 'color-primary';
56105
});
57106
107+
const emitProgress = computed(() => {
108+
if (!transform?.value || typeof transform.value !== 'function') {
109+
return angleProgress.value;
110+
}
111+
return transform.value(angleProgress.value);
112+
});
113+
114+
const onClick = () => {
115+
if (!editable.value) return;
116+
editing.value = !editing.value;
117+
emit('onEditing', editing.value);
118+
if (!editing.value) emit('onEdit', emitProgress.value);
119+
if (editing.value) progressRef.value?.$el?.focus();
120+
};
121+
122+
const listener = (event: MouseEvent) => {
123+
if (!progressRef.value?.$el) return;
124+
// Get the bounding rectangle of the tracking box
125+
const rect = progressRef.value?.$el.getBoundingClientRect();
126+
127+
// Calculate the mouse position relative to the tracking box
128+
const mouseX = event.clientX - (rect.left + rect.width / 2);
129+
const mouseY = rect.top + rect.height / 2 - event.clientY;
130+
131+
// Convert (x, y) to angle in radians then in degrees
132+
let degrees = Math.atan2(mouseX, mouseY) * (180 / Math.PI) - 180;
133+
134+
// Adjust angle to 0-360° range
135+
if (degrees < 0) degrees += 360;
136+
angleProgress.value = Math.round(degrees / 3.6);
137+
};
138+
58139
onMounted(async () => {
59140
watch(
60141
progress,
@@ -64,32 +145,50 @@ onMounted(async () => {
64145
},
65146
{ immediate: true },
66147
);
148+
watch(
149+
containerRef,
150+
(_new, _old) => {
151+
_old?.removeEventListener('mousemove', listener);
152+
if (editable.value) _new?.addEventListener('mousemove', listener);
153+
},
154+
155+
{ immediate: true },
156+
);
67157
});
68158
69-
onUnmounted(() => {
159+
onBeforeUnmount(() => {
70160
_progress.value = 0;
161+
containerRef.value?.removeEventListener('mousemove', listener);
71162
});
72163
</script>
73164

74165
<template>
75-
<NSpin v-if="loading" class="spin" size="large">
166+
<NSpin v-show="loading" class="spin" size="large">
76167
<NProgress class="progress" type="circle">
77168
<NSkeleton class="skeleton" text round />
78169
</NProgress>
79170
</NSpin>
80171
<NProgress
81-
v-else
172+
v-show="!loading"
173+
ref="progressRef"
82174
class="progress custom-color"
175+
:class="{ editing, editable }"
83176
type="circle"
84-
:percentage="_progress"
177+
:percentage="editProgress"
85178
:style="{
86-
'--duration': `${ duration - delay }ms`,
179+
'--duration': `${ progressDuration }ms`,
87180
'--custom-progress-color': `var(--${ color })`,
88181
}"
182+
:tabindex="editable ? 0 : undefined"
183+
@click="onClick"
184+
@keydown.enter="onClick"
185+
@blur="editing = false"
89186
>
187+
<span v-if="editing">{{ emitProgress }}</span>
90188
<AnimatedNumber
189+
v-else
91190
:from="from"
92-
:to="_progress"
191+
:to="editProgress"
93192
:duration="duration"
94193
:precision="precision"
95194
:disabled="!_progress"
@@ -102,10 +201,22 @@ onUnmounted(() => {
102201
.spin,
103202
.progress {
104203
--progress-size: 3.7rem !important;
204+
205+
&.editable {
206+
cursor: grab;
207+
}
208+
209+
&.editing {
210+
cursor: grabbing;
211+
}
105212
}
106213
107214
.custom-color {
108215
--n-fill-color: var(--custom-progress-color, var(--color-info)) !important;
216+
217+
&:focus-visible {
218+
outline: -webkit-focus-ring-color auto 1px;
219+
}
109220
}
110221
111222
.spin {

src/components/common/typography/TextField.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const { openTab } = useLinksStore();
7979
<template>
8080
<NFlex
8181
class="detail"
82-
:class="{ grow, array, disabled }"
82+
:class="{ grow, array, disabled, vertical }"
8383
:style="{ '--prefix-min-width': labelWidth, '--text-flex': flex }"
8484
:align="align"
8585
:wrap="wrap"
@@ -140,6 +140,10 @@ const { openTab } = useLinksStore();
140140
align-items: baseline;
141141
min-width: max-content;
142142
143+
&.vertical {
144+
width: 100%;
145+
}
146+
143147
&:hover .prefix {
144148
color: var(--white-70);
145149
}

src/components/views/panel/PanelRating.vue

+12-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ const _rating = computed(() => (rating?.value ?? 0) * 10);
7575
vertical
7676
size="small"
7777
flex="0 1 auto"
78+
align="center"
79+
style="padding-top: 1.5rem"
7880
>
7981
<NSkeleton v-if="loading" class="rating-skeleton" text round />
8082
<AnimatedNumber
@@ -89,7 +91,13 @@ const _rating = computed(() => (rating?.value ?? 0) * 10);
8991
</TextField>
9092

9193
<!-- Rating progress -->
92-
<TextField :label="i18n('label_rating')" vertical flex="0 1 auto">
94+
<TextField
95+
:label="i18n('label_rating')"
96+
vertical
97+
flex="0 1 auto"
98+
align="center"
99+
style="padding-bottom: 1.5rem"
100+
>
93101
<ButtonLinkExternal
94102
:href="url"
95103
:title="i18n('calendar', 'common', 'link')"
@@ -113,10 +121,12 @@ const _rating = computed(() => (rating?.value ?? 0) * 10);
113121
.rating-container {
114122
--duration: 1000ms;
115123
116-
gap: 1rem;
124+
flex: 1 1 auto;
125+
min-width: 6rem;
117126
padding: 0.5rem;
118127
119128
.rating-skeleton {
129+
width: 2.125rem;
120130
margin-top: 0.5rem;
121131
}
122132

src/components/views/panel/PanelScore.vue

+29-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts" setup>
22
import { NFlex, NSkeleton } from 'naive-ui';
3-
import { computed, toRefs } from 'vue';
3+
import { computed, ref, toRefs } from 'vue';
44
55
import ProgressNumber from '~/components/common/numbers/ProgressNumber.vue';
66
@@ -30,6 +30,10 @@ const props = defineProps({
3030
},
3131
});
3232
33+
const emit = defineEmits<{
34+
(e: 'onEdit', progress: number): void;
35+
}>();
36+
3337
const i18n = useI18n('panel', 'ratings');
3438
3539
const { score } = toRefs(props);
@@ -45,6 +49,11 @@ const _scoreLabel = computed(() => {
4549
return 'not_rated';
4650
}
4751
});
52+
53+
const transformProgress = (progress: number) => Math.round(progress / 10) * 10;
54+
const onEdit = (_progress: number) => emit('onEdit', _progress);
55+
56+
const containerRef = ref<InstanceType<typeof TextField>>();
4857
</script>
4958

5059
<template>
@@ -56,6 +65,9 @@ const _scoreLabel = computed(() => {
5665
vertical
5766
size="small"
5867
flex="0 1 auto"
68+
align="center"
69+
justify="center"
70+
style="padding-top: 1.5rem"
5971
>
6072
<NSkeleton v-if="loading" class="score-skeleton" text round />
6173
<span v-else class="score-label" :class="{ disabled: !score }">
@@ -64,12 +76,24 @@ const _scoreLabel = computed(() => {
6476
</TextField>
6577

6678
<!-- Score -->
67-
<TextField :label="i18n('label_score')" vertical flex="0 1 auto">
79+
<TextField
80+
ref="containerRef"
81+
:label="i18n('label_score')"
82+
:title="i18n('title_score')"
83+
vertical
84+
flex="0 1 auto"
85+
align="center"
86+
style="padding-bottom: 1.5rem"
87+
>
6888
<ProgressNumber
6989
:progress="_score"
7090
:duration="duration"
7191
:precision="precision"
7292
:loading="loading"
93+
:container="containerRef?.$el"
94+
:transform="transformProgress"
95+
editable
96+
@on-edit="onEdit"
7397
/>
7498
</TextField>
7599
</NFlex>
@@ -79,7 +103,8 @@ const _scoreLabel = computed(() => {
79103
.score-container {
80104
--duration: 1000ms;
81105
82-
gap: 1rem;
106+
flex: 1 1 auto;
107+
min-width: 6rem;
83108
padding: 0.5rem;
84109
85110
.score-label {
@@ -92,6 +117,7 @@ const _scoreLabel = computed(() => {
92117
}
93118
94119
.score-skeleton {
120+
width: 2.125rem;
95121
margin-top: 0.5rem;
96122
}
97123

src/components/views/panel/PanelStatistics.vue

+12-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ defineProps({
3232
},
3333
});
3434
35+
const emit = defineEmits<{
36+
(e: 'onScoreEdit', progress: number): void;
37+
}>();
38+
3539
const { enableRatings } = useExtensionSettingsStoreRefs();
40+
41+
const onScoreEdit = (progress: number) => emit('onScoreEdit', progress);
3642
</script>
3743

3844
<template>
@@ -45,7 +51,12 @@ const { enableRatings } = useExtensionSettingsStoreRefs();
4551
:url="url"
4652
/>
4753
<slot />
48-
<PanelScore v-if="enableRatings" :loading="loadingScore" :score="score" />
54+
<PanelScore
55+
v-if="enableRatings"
56+
:loading="loadingScore"
57+
:score="score"
58+
@on-edit="onScoreEdit"
59+
/>
4960
</NFlex>
5061
</template>
5162

0 commit comments

Comments
 (0)