Merge branch 'develop' into mkjs-n
This commit is contained in:
commit
64f4bb6a90
63 changed files with 1694 additions and 1197 deletions
|
|
@ -52,9 +52,12 @@
|
|||
|
||||
<MkFoldableSection class="item">
|
||||
<template #header>Retention rate</template>
|
||||
<div class="_panel" :class="$style.retention">
|
||||
<div class="_panel" :class="$style.retentionHeatmap">
|
||||
<MkRetentionHeatmap/>
|
||||
</div>
|
||||
<div class="_panel" :class="$style.retentionLine">
|
||||
<MkRetentionLineChart/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkFoldableSection class="item">
|
||||
|
|
@ -86,6 +89,7 @@ import { i18n } from '@/i18n';
|
|||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
import { initChart } from '@/scripts/init-chart';
|
||||
|
||||
initChart();
|
||||
|
|
@ -202,7 +206,12 @@ onMounted(() => {
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.retention {
|
||||
.retentionHeatmap {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.retentionLine {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div :class="[$style.root, { [$style.children]: depth > 1 }]">
|
||||
<div :class="$style.main">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.body">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
|
|
@ -62,6 +63,7 @@ if (props.detail) {
|
|||
.root {
|
||||
padding: 16px 32px;
|
||||
font-size: 0.9em;
|
||||
position: relative;
|
||||
|
||||
&.children {
|
||||
padding: 10px 0 0 16px;
|
||||
|
|
@ -73,6 +75,16 @@ if (props.detail) {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.colorBar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 5px;
|
||||
height: calc(100% - 8px);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async function renderChart() {
|
|||
|
||||
let raw = await os.api('retention', { });
|
||||
|
||||
raw = raw.slice(0, maxDays);
|
||||
raw = raw.slice(0, maxDays + 1);
|
||||
|
||||
const data = [];
|
||||
for (const record of raw) {
|
||||
|
|
@ -90,8 +90,13 @@ async function renderChart() {
|
|||
borderRadius: 3,
|
||||
backgroundColor(c) {
|
||||
const value = c.dataset.data[c.dataIndex].v;
|
||||
const a = value / max(c.dataset.data[c.dataIndex].y);
|
||||
return alpha(color, a);
|
||||
const m = max(c.dataset.data[c.dataIndex].y);
|
||||
if (m === 0) {
|
||||
return alpha(color, 0);
|
||||
} else {
|
||||
const a = value / m;
|
||||
return alpha(color, a);
|
||||
}
|
||||
},
|
||||
fill: true,
|
||||
width(c) {
|
||||
|
|
@ -129,6 +134,10 @@ async function renderChart() {
|
|||
autoSkip: false,
|
||||
callback: (value, index, values) => value,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Days later',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: 'time',
|
||||
|
|
@ -166,7 +175,12 @@ async function renderChart() {
|
|||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex];
|
||||
return [`Active: ${v.v} (${Math.round((v.v / max(v.y)) * 100)}%)`];
|
||||
const m = max(v.y);
|
||||
if (m === 0) {
|
||||
return [`Active: ${v.v} (-%)`];
|
||||
} else {
|
||||
return [`Active: ${v.v} (${Math.round((v.v / m) * 100)}%)`];
|
||||
}
|
||||
},
|
||||
},
|
||||
//mode: 'index',
|
||||
|
|
|
|||
130
packages/frontend/src/components/MkRetentionLineChart.vue
Normal file
130
packages/frontend/src/components/MkRetentionLineChart.vue
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import { chartVLine } from '@/scripts/chart-vline';
|
||||
import { alpha } from '@/scripts/color';
|
||||
import { initChart } from '@/scripts/init-chart';
|
||||
import * as os from '@/os';
|
||||
|
||||
initChart();
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
let chartInstance: Chart;
|
||||
|
||||
const getYYYYMMDD = (date: Date) => {
|
||||
const y = date.getFullYear().toString().padStart(2, '0');
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const d = date.getDate().toString().padStart(2, '0');
|
||||
return `${y}/${m}/${d}`;
|
||||
};
|
||||
|
||||
const getDate = (ymd: string) => {
|
||||
const [y, m, d] = ymd.split('-').map(x => parseInt(x, 10));
|
||||
const date = new Date(y, m + 1, d, 0, 0, 0, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
let raw = await os.api('retention', { });
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
||||
const color = accent.toHex();
|
||||
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: raw.map((record, i) => ({
|
||||
label: getYYYYMMDD(new Date(record.createdAt)),
|
||||
pointRadius: 0,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: alpha(color, Math.min(1, (raw.length - (i - 1)) / raw.length)),
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
data: [{
|
||||
x: '0',
|
||||
y: 100,
|
||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
|
||||
x: (i + 1).toString(),
|
||||
y: (v / record.users) * 100,
|
||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||
}))],
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Days later',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Rate (%)',
|
||||
},
|
||||
ticks: {
|
||||
callback: (value, index, values) => value + '%',
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const v = context[0].dataset.data[context[0].dataIndex];
|
||||
return `${v.x} days later`;
|
||||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex];
|
||||
const p = Math.round(v.y) + '%';
|
||||
return `${v.d} ${p}`;
|
||||
},
|
||||
},
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [chartVLine(vLineColor)],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -40,10 +40,6 @@ import * as os from '@/os';
|
|||
import { $i } from '@/account';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
||||
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Privacy,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Privacy v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
|
||||
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
|
||||
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.noCrawle }}</template>
|
||||
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.preventAiLearning }}</template>
|
||||
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
let isLocked = ref(false);
|
||||
let hideOnlineStatus = ref(false);
|
||||
let noCrawle = ref(false);
|
||||
let preventAiLearning = ref(true);
|
||||
|
||||
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
|
||||
os.api('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||
noCrawle: !!noCrawle.value,
|
||||
preventAiLearning: !!preventAiLearning.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
||||
|
|
@ -37,10 +37,6 @@ import { chooseFileFromPc } from '@/scripts/select-file';
|
|||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,17 @@
|
|||
@close="close(true)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
<template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
|
||||
<template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
|
||||
<template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
|
||||
<template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
|
||||
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
|
||||
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
|
||||
<div style="overflow-x: clip;">
|
||||
<div :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
|
||||
</div>
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enter-active-class="$style.transition_x_enterActive"
|
||||
|
|
@ -40,12 +48,22 @@
|
|||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XFollow/>
|
||||
<XPrivacy/>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XFollow/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
|
|
@ -58,7 +76,7 @@
|
|||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<template v-else-if="page === 5">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
|
|
@ -87,6 +105,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
|
||||
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
|
||||
import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { host } from '@/config';
|
||||
|
|
@ -134,6 +153,21 @@ async function close(skip: boolean) {
|
|||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.progressBarValue {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -142,4 +176,14 @@ async function close(skip: boolean) {
|
|||
padding-bottom: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="lzyxtsnt">
|
||||
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
|
||||
<div>
|
||||
<ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -17,11 +17,3 @@ const props = defineProps<{
|
|||
|
||||
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lzyxtsnt {
|
||||
> img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue