Skip to content

Commit

Permalink
Add play stats page (#660)
Browse files Browse the repository at this point in the history
  • Loading branch information
robbevp authored Dec 4, 2021
1 parent fc02bc4 commit 619effb
Show file tree
Hide file tree
Showing 14 changed files with 566 additions and 4 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@mdi/font": "^6.5.95",
"@mdi/svg": "^6.5.95",
"fetch-retry": "^5.0.1",
"gsap": "^3.8.0",
"localforage": "^1.10.0",
"lodash.debounce": "^4.0.8",
"roboto-fontface": "^0.10.0",
Expand Down
4 changes: 4 additions & 0 deletions src/Main.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ a {
}
// Additional utility classes
.break-word {
word-break: break-word;
}
.white-space-nowrap {
white-space: nowrap;
}
Expand Down
139 changes: 139 additions & 0 deletions src/components/DateRangeSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<template>
<div>
<VSelect
:items="periodPresets"
:label="$t('components.dateRangeSelect.label')"
v-model="selectedPreset"
>
<template v-slot:item="{ item, on, attrs }">
<VListItem
v-on="on"
v-bind="attrs"
@click="showCustomRangeModal = item.value === 'customRange'"
>
{{ item.text }}
</VListItem>
</template>
<template v-slot:selection="{ item }">
<span>
{{ item.value === "customRange" ? customRangeText : item.text }}
</span>
</template>
</VSelect>
<VDialog persistent width="290px" v-model="showCustomRangeModal">
<VDatePicker
v-model="customRange"
scrollable
range
:first-day-of-week="1"
:locale="locale"
>
<VSpacer></VSpacer>
<VBtn
text
color="primary"
class="ma-2"
@click="showCustomRangeModal = false"
>
{{ $t("common.cancel") }}
</VBtn>
<VBtn text color="primary" class="ma-2" @click="emitSelection">
{{ $t("common.ok") }}
</VBtn>
</VDatePicker>
</VDialog>
</div>
</template>

<script>
import { mapState } from "vuex";
export default {
name: "DateRangeSelect",
data() {
return {
periodPresets: [
{
value: "last7Days",
text: this.$t("components.dateRangeSelect.options.last7Days"),
},
{
value: "lastMonth",
text: this.$t("components.dateRangeSelect.options.lastMonth"),
},
{
value: "thisYear",
text: this.$t("components.dateRangeSelect.options.thisYear"),
},
{
value: "allTime",
text: this.$t("components.dateRangeSelect.options.allTime"),
},
{
value: "customRange",
text: this.$t("components.dateRangeSelect.options.customRange"),
},
],
selectedPreset: "last7Days",
showCustomRangeModal: false,
customRange: [],
};
},
computed: {
...mapState("userSettings", ["locale"]),
customRangeText() {
return this.customRange.join(" - ");
},
period() {
let end = new Date();
let start = new Date();
switch (this.selectedPreset) {
case "last7Days":
start.setDate(start.getDate() - 7);
break;
case "lastMonth":
start.setMonth(start.getMonth() - 1);
break;
case "thisYear":
start.setMonth(0, 1);
break;
case "allTime":
start = new Date(0);
break;
case "customRange": {
const range = this.customRange
.map((d) => new Date(d))
.sort((d1, d2) => d1 > d2);
if (range.length) {
start = range[0];
end = range[1] || new Date(range[0]);
}
break;
}
}
// Set start and end to beginning and end of day
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
return { start, end };
},
},
watch: {
selectedPreset: {
handler(newValue) {
if (newValue !== "customRange") {
this.emitSelection();
}
},
immediate: true,
},
},
methods: {
emitSelection() {
this.$emit("input", this.period);
this.showCustomRangeModal = false;
},
},
};
</script>

<style></style>
109 changes: 109 additions & 0 deletions src/components/PercentagePlayedCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<template>
<VCard class="pa-2">
<VCardTitle class="break-word">{{ title }}</VCardTitle>
<div class="ma-2">
<svg viewBox="0 0 250 250">
<circle
cx="125"
cy="125"
r="100"
fill="transparent"
stroke="currentColor"
stroke-width="30"
:stroke-dasharray="circumference"
class="grey--text text--lighten-2"
></circle>
<circle
cx="125"
cy="125"
r="100"
fill="transparent"
stroke="currentColor"
class="primary--text"
stroke-width="30"
:stroke-dasharray="circumference"
:stroke-dashoffset="strokeDashOffset"
transform="rotate(-90, 125, 125)"
></circle>
<text x="130" y="125" text-anchor="middle" class="text-h6">
{{ Math.round(animatedPercentage * 1000) / 10.0 }}%
</text>
</svg>
</div>
</VCard>
</template>

<script>
import { gsap } from "gsap";
export default {
name: "PercentagePlayedCard",
props: {
plays: {
type: Array,
required: true,
},
tracks: {
type: Array,
required: true,
},
title: {
type: String,
required: true,
},
useTrackLength: {
type: Boolean,
default: false,
},
},
data() {
return {
animatedPercentage: 0,
};
},
computed: {
playedTracksInPeriod() {
const trackIds = this.plays.map((p) => p.track_id);
return [...new Set(trackIds)];
},
playedTracksLength() {
return this.playedTracksInPeriod.reduce((acc, cur) => {
return acc + (this.tracks.find((t) => t.id === cur)?.length || 0);
}, 0);
},
totalTracksLength() {
return this.tracks.reduce((acc, cur) => {
return acc + cur.length;
}, 0);
},
circumference() {
// 2πr
return 2 * Math.PI * 100;
},
percentage() {
if (!this.tracks.length) {
return 0;
}
const playsCount = this.useTrackLength
? this.playedTracksLength
: this.playedTracksInPeriod.length;
const tracksCount = this.useTrackLength
? this.totalTracksLength
: this.tracks.length;
return (1.0 * playsCount) / tracksCount;
},
strokeDashOffset() {
const strokeDiff = this.animatedPercentage * this.circumference;
return this.circumference - strokeDiff;
},
},
watch: {
percentage(newValue) {
gsap.to(this.$data, { duration: 0.8, animatedPercentage: newValue });
},
},
};
</script>
<style></style>
Loading

0 comments on commit 619effb

Please sign in to comment.