diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-05-12 12:41:53 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-12 12:41:53 +0900 |
| commit | de6348e8a00a72c2410a8907d59a9bd29142a200 (patch) | |
| tree | f163329a67ac5c6dc1c33845b4fc131a8c59a39b /packages/frontend/src/components | |
| parent | Merge pull request #10814 from misskey-dev/develop (diff) | |
| parent | fix(frontend): fix retention rate heatmap rendering (diff) | |
| download | misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.tar.gz misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.tar.bz2 misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.zip | |
Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules
* feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加
Resolve #10819
* enhance(backend): publicReactionsをデフォルトtrueに
* 念のためnoimageaiもつける
* add X-Robots-Tag: noai
* Update ja-JP.yml
* fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正
* enhance(backend): graceful shutdown for job queue and refactor
* fix(backend): テスト時は一部のサービスを停止
* fix test
* New Crowdin updates (#10815)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (Korean)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (Chinese Traditional)
* refactor
* bump
* refactor(frontend): use css module
* refactor(frontend): use css module
* delete unused component
* センシティブワードを正規表現、CWにも適用するように (#10688)
* cwにセンシティブが効いてない
* CWが無いときにTextを見るように
* 比較演算子間違えた
* とりあえずチェック
* 正規表現対応
* /test/giにも対応
* matchでしなくてもいいのでは感
* レビュー修正
* Update packages/backend/src/core/NoteCreateService.ts
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
* Update packages/backend/src/core/NoteCreateService.ts
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
* 修正
* wipかも
* wordsでスペース区切りのものできたかも
* なんか動いたかも
* test作成
* 文言の修正
* 修正
* note参照
---------
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
* Update CHANGELOG.md
* New Crowdin updates (#10823)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (German)
* ci: fix typo
* fix(frontend): より明確な説明にしたのとtypo修正
* fix typo
* fix(frontend): カラーバーがリプライには表示されないのを修正
* fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正
Fix #10793
* enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように
今後ハッシュタグ検索とか実装するときのため
* feat(frontend): ユーザー指定ノート検索
* fix(frontend): fix retention chart rendering
* Update about-misskey.vue
* meta: Remove @rinsuki from reviewer-lottery (#10830)
* New Crowdin updates (#10824)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (French)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (Japanese, Kansai)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (Spanish)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (Italian)
* New translations ja-JP.yml (Korean)
* New translations ja-JP.yml (Norwegian)
* New translations ja-JP.yml (Russian)
* New translations ja-JP.yml (Chinese Simplified)
* New translations ja-JP.yml (Indonesian)
* New translations ja-JP.yml (Thai)
* enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加
* Update CHANGELOG.md
* fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正
* fix MkUserSetupDialog.Privacy.vue
* ci: skip non-Japanese locale on TurboSnap
* ci: notify on changes for push events
* ci: fix missing branch
* Update basic.cy.js
* [ci skip] New Crowdin updates (#10834)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (Arabic)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (Chinese Simplified)
* New translations ja-JP.yml (Japanese, Kansai)
* New translations ja-JP.yml (Arabic)
* :art:
* :art:
* enhance(frontend): add retention line chart
* update deps
* refactor
* fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正
Fix #10837
---------
Co-authored-by: nenohi <kimutipartylove@gmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
Diffstat (limited to 'packages/frontend/src/components')
17 files changed, 577 insertions, 467 deletions
diff --git a/packages/frontend/src/components/MkCheckbox.vue b/packages/frontend/src/components/MkCheckbox.vue deleted file mode 100644 index a8e24dd839..0000000000 --- a/packages/frontend/src/components/MkCheckbox.vue +++ /dev/null @@ -1,144 +0,0 @@ -<template> -<div - class="ziffeoms" - :class="{ disabled, checked }" -> - <input - ref="input" - type="checkbox" - :disabled="disabled" - @keydown.enter="toggle" - > - <span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> - <i class="check ti ti-check"></i> - </span> - <span class="label"> - <!-- TODO: 無名slotの方は廃止 --> - <span @click="toggle"><slot name="label"></slot><slot></slot></span> - <p class="caption"><slot name="caption"></slot></p> - </span> -</div> -</template> - -<script lang="ts" setup> -import { toRefs, Ref } from 'vue'; -import * as os from '@/os'; -import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { i18n } from '@/i18n'; - -const props = defineProps<{ - modelValue: boolean | Ref<boolean>; - disabled?: boolean; -}>(); - -const emit = defineEmits<{ - (ev: 'update:modelValue', v: boolean): void; -}>(); - -let button = $shallowRef<HTMLElement>(); -const checked = toRefs(props).modelValue; -const toggle = () => { - if (props.disabled) return; - emit('update:modelValue', !checked.value); - - if (!checked.value) { - const rect = button.getBoundingClientRect(); - const x = rect.left + (button.offsetWidth / 2); - const y = rect.top + (button.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end'); - } -}; -</script> - -<style lang="scss" scoped> -.ziffeoms { - position: relative; - display: flex; - transition: all 0.2s ease; - - > * { - user-select: none; - } - - > input { - position: absolute; - width: 0; - height: 0; - opacity: 0; - margin: 0; - } - - > .button { - position: relative; - display: inline-flex; - flex-shrink: 0; - margin: 0; - box-sizing: border-box; - width: 23px; - height: 23px; - outline: none; - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 4px; - cursor: pointer; - transition: inherit; - - > .check { - margin: auto; - opacity: 0; - color: var(--fgOnAccent); - font-size: 13px; - transform: scale(0.5); - transition: all 0.2s ease; - } - } - - &:hover { - > .button { - border-color: var(--inputBorderHover) !important; - } - } - - > .label { - margin-left: 12px; - margin-top: 2px; - display: block; - transition: inherit; - color: var(--fg); - - > span { - display: block; - line-height: 20px; - cursor: pointer; - transition: inherit; - } - - > .caption { - margin: 8px 0 0 0; - color: var(--fgTransparentWeak); - font-size: 0.85em; - - &:empty { - display: none; - } - } - } - - &.disabled { - opacity: 0.6; - cursor: not-allowed; - } - - &.checked { - > .button { - background-color: var(--accent) !important; - border-color: var(--accent) !important; - - > .check { - opacity: 1; - transform: scale(1); - } - } - } -} -</style> diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 0f87fef6b1..6fcd8f7811 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -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; } diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index ad7dc4da11..63c55b904a 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -1,15 +1,15 @@ <template> <MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> - <div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown"> - <div ref="headerEl" class="header"> - <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> - <span class="title"> + <div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown"> + <div ref="headerEl" :class="$style.header"> + <button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> + <span :class="$style.title"> <slot name="header"></slot> </span> - <button v-if="!withOkButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button> - <button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button> + <button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button> + <button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button> </div> - <div class="body"> + <div :class="$style.body"> <slot :width="bodyWidth" :height="bodyHeight"></slot> </div> </div> @@ -81,8 +81,8 @@ defineExpose({ }); </script> -<style lang="scss" scoped> -.ebkgoccj { +<style lang="scss" module> +.root { margin: auto; overflow: hidden; display: flex; @@ -96,51 +96,52 @@ defineExpose({ --root-margin: 16px; } - > .header { - $height: 46px; - $height-narrow: 42px; - display: flex; - flex-shrink: 0; - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + --headerHeight: 46px; + --headerHeightNarrow: 42px; +} - > button { - height: $height; - width: $height; +.header { + display: flex; + flex-shrink: 0; + background: var(--windowHeader); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} - @media (max-width: 500px) { - height: $height-narrow; - width: $height-narrow; - } - } +.headerButton { + height: var(--headerHeight); + width: var(--headerHeight); - > .title { - flex: 1; - line-height: $height; - padding-left: 32px; - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - pointer-events: none; + @media (max-width: 500px) { + height: var(--headerHeightNarrow); + width: var(--headerHeightNarrow); + } +} - @media (max-width: 500px) { - line-height: $height-narrow; - padding-left: 16px; - } - } +.title { + flex: 1; + line-height: var(--headerHeight); + padding-left: 32px; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; - > button + .title { - padding-left: 0; - } + @media (max-width: 500px) { + line-height: var(--headerHeightNarrow); + padding-left: 16px; } +} - > .body { - flex: 1; - overflow: auto; - background: var(--panel); - container-type: size; - } +.headerButton + .title { + padding-left: 0; +} + +.body { + flex: 1; + overflow: auto; + background: var(--panel); + container-type: size; } </style> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index c293641355..9ac0b7858f 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -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; diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index 5db2f5ee6d..eea94d4692 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -1,8 +1,7 @@ <template> <div v-adaptive-border - class="novjtctn" - :class="{ disabled, checked }" + :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" :aria-checked="checked" :aria-disabled="disabled" @click="toggle" @@ -10,11 +9,12 @@ <input type="radio" :disabled="disabled" + :class="$style.input" > - <span class="button"> + <span :class="$style.button"> <span></span> </span> - <span class="label"><slot></slot></span> + <span :class="$style.label"><slot></slot></span> </div> </template> @@ -39,8 +39,8 @@ function toggle(): void { } </script> -<style lang="scss" scoped> -.novjtctn { +<style lang="scss" module> +.root { position: relative; display: inline-block; text-align: left; @@ -53,17 +53,11 @@ function toggle(): void { border-radius: 6px; font-size: 90%; transition: all 0.2s; - - > * { - user-select: none; - } + user-select: none; &.disabled { opacity: 0.6; - - &, * { - cursor: not-allowed !important; - } + cursor: not-allowed !important; } &:hover { @@ -74,10 +68,7 @@ function toggle(): void { background-color: var(--accentedBg) !important; border-color: var(--accentedBg) !important; color: var(--accent); - - &, * { - cursor: default !important; - } + cursor: default !important; > .button { border-color: var(--accent); @@ -89,44 +80,44 @@ function toggle(): void { } } } +} - > input { - position: absolute; - width: 0; - height: 0; - opacity: 0; - margin: 0; - } +.input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; +} + +.button { + position: absolute; + width: 14px; + height: 14px; + background: none; + border: solid 2px var(--inputBorder); + border-radius: 100%; + transition: inherit; - > .button { + &:after { + content: ''; + display: block; position: absolute; - width: 14px; - height: 14px; - background: none; - border: solid 2px var(--inputBorder); + top: 3px; + right: 3px; + bottom: 3px; + left: 3px; border-radius: 100%; - transition: inherit; - - &:after { - content: ''; - display: block; - position: absolute; - top: 3px; - right: 3px; - bottom: 3px; - left: 3px; - border-radius: 100%; - opacity: 0; - transform: scale(0); - transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); - } + opacity: 0; + transform: scale(0); + transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } +} - > .label { - margin-left: 28px; - display: block; - line-height: 20px; - cursor: pointer; - } +.label { + margin-left: 28px; + display: block; + line-height: 20px; + cursor: pointer; } </style> diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index f33f68cab7..311d5c425c 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -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', diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue new file mode 100644 index 0000000000..8bd0279806 --- /dev/null +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -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> diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 2de890186a..4efb65c287 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -1,13 +1,13 @@ <template> -<div class="vblkjoeq"> - <div class="label" @click="focus"><slot name="label"></slot></div> - <div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show"> - <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> +<div> + <div :class="$style.label" @click="focus"><slot name="label"></slot></div> + <div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show"> + <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> <select ref="inputEl" v-model="v" v-adaptive-border - class="select" + :class="$style.inputCore" :disabled="disabled" :required="required" :readonly="readonly" @@ -18,9 +18,9 @@ > <slot></slot> </select> - <div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> + <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> </div> - <div class="caption"><slot name="caption"></slot></div> + <div :class="$style.caption"><slot name="caption"></slot></div> <MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> @@ -169,121 +169,116 @@ function show(ev: MouseEvent) { } </script> -<style lang="scss" scoped> -.vblkjoeq { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; +<style lang="scss" module> +.label { + font-size: 0.85em; + padding: 0 0 8px 0; + user-select: none; - &:empty { - display: none; - } + &:empty { + display: none; } +} - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); +.caption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); - &:empty { - display: none; - } + &:empty { + display: none; } +} - > .input { - position: relative; - cursor: pointer; +.input { + position: relative; + cursor: pointer; - &:hover { - > .select { - border-color: var(--inputBorderHover) !important; - } - } + &.inline { + display: inline-block; + margin: 0; + } - > .select { - appearance: none; - -webkit-appearance: none; - display: block; - height: v-bind("height + 'px'"); - width: 100%; - margin: 0; - padding: 0 12px; - font: inherit; - font-weight: normal; - font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 6px; - outline: none; - box-shadow: none; - box-sizing: border-box; - cursor: pointer; - transition: border-color 0.1s ease-out; - pointer-events: none; - user-select: none; + &.focused { + > .inputCore { + border-color: var(--accent) !important; + //box-shadow: 0 0 0 4px var(--focus); } + } - > .prefix, - > .suffix { - display: flex; - align-items: center; - position: absolute; - z-index: 1; - top: 0; - padding: 0 12px; - font-size: 1em; - height: v-bind("height + 'px'"); - pointer-events: none; - - &:empty { - display: none; - } + &.disabled { + opacity: 0.7; - > * { - display: inline-block; - min-width: 16px; - max-width: 150px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } + &, + > .inputCore { + cursor: not-allowed !important; } + } - > .prefix { - left: 0; - padding-right: 6px; + &:hover { + > .inputCore { + border-color: var(--inputBorderHover) !important; } + } +} - > .suffix { - right: 0; - padding-left: 6px; - } +.inputCore { + appearance: none; + -webkit-appearance: none; + display: block; + height: v-bind("height + 'px'"); + width: 100%; + margin: 0; + padding: 0 12px; + font: inherit; + font-weight: normal; + font-size: 1em; + color: var(--fg); + background: var(--panel); + border: solid 1px var(--panel); + border-radius: 6px; + outline: none; + box-shadow: none; + box-sizing: border-box; + transition: border-color 0.1s ease-out; + cursor: pointer; + pointer-events: none; + user-select: none; +} - &.inline { - display: inline-block; - margin: 0; - } +.prefix, +.suffix { + display: flex; + align-items: center; + position: absolute; + z-index: 1; + top: 0; + padding: 0 12px; + font-size: 1em; + height: v-bind("height + 'px'"); + min-width: 16px; + max-width: 150px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + box-sizing: border-box; + pointer-events: none; - &.focused { - > select { - border-color: var(--accent) !important; - } - } + &:empty { + display: none; + } +} - &.disabled { - opacity: 0.7; +.prefix { + left: 0; + padding-right: 6px; +} - &, * { - cursor: not-allowed !important; - } - } - } +.suffix { + right: 0; + padding-left: 6px; } -</style> -<style lang="scss" module> .chevron { transition: transform 0.1s ease-out; } diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index d9f6716f92..63738b6a44 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -1,21 +1,19 @@ <template> -<div - class="ziffeomt" - :class="{ disabled, checked }" -> +<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"> <input ref="input" type="checkbox" :disabled="disabled" + :class="$style.input" @keydown.enter="toggle" > - <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle"> - <div class="knob"></div> + <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle"> + <div :class="$style.knob"></div> </span> - <span class="label"> + <span :class="$style.body"> <!-- TODO: 無名slotの方は廃止 --> - <span @click="toggle"><slot name="label"></slot><slot></slot></span> - <p class="caption"><slot name="caption"></slot></p> + <span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span> + <p :class="$style.caption"><slot name="caption"></slot></p> </span> </div> </template> @@ -45,52 +43,12 @@ const toggle = () => { }; </script> -<style lang="scss" scoped> -.ziffeomt { +<style lang="scss" module> +.root { position: relative; display: flex; transition: all 0.2s ease; - - > * { - user-select: none; - } - - > input { - position: absolute; - width: 0; - height: 0; - opacity: 0; - margin: 0; - } - - > .button { - position: relative; - display: inline-flex; - flex-shrink: 0; - margin: 0; - box-sizing: border-box; - width: 32px; - height: 23px; - outline: none; - background: var(--switchOffBg); - background-clip: content-box; - border: solid 1px var(--switchOffBg); - border-radius: 999px; - cursor: pointer; - transition: inherit; - user-select: none; - - > .knob { - position: absolute; - top: 3px; - left: 3px; - width: 15px; - height: 15px; - background: var(--switchOffFg); - border-radius: 999px; - transition: all 0.2s ease; - } - } + user-select: none; &:hover { > .button { @@ -98,31 +56,6 @@ const toggle = () => { } } - > .label { - margin-left: 12px; - margin-top: 2px; - display: block; - transition: inherit; - color: var(--fg); - - > span { - display: block; - line-height: 20px; - cursor: pointer; - transition: inherit; - } - - > .caption { - margin: 8px 0 0 0; - color: var(--fgTransparentWeak); - font-size: 0.85em; - - &:empty { - display: none; - } - } - } - &.disabled { opacity: 0.6; cursor: not-allowed; @@ -140,4 +73,66 @@ const toggle = () => { } } } + +.input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; +} + +.button { + position: relative; + display: inline-flex; + flex-shrink: 0; + margin: 0; + box-sizing: border-box; + width: 32px; + height: 23px; + outline: none; + background: var(--switchOffBg); + background-clip: content-box; + border: solid 1px var(--switchOffBg); + border-radius: 999px; + cursor: pointer; + transition: inherit; + user-select: none; +} + +.knob { + position: absolute; + top: 3px; + left: 3px; + width: 15px; + height: 15px; + background: var(--switchOffFg); + border-radius: 999px; + transition: all 0.2s ease; +} + +.body { + margin-left: 12px; + margin-top: 2px; + display: block; + transition: inherit; + color: var(--fg); +} + +.label { + display: block; + line-height: 20px; + cursor: pointer; + transition: inherit; +} + +.caption { + margin: 8px 0 0 0; + color: var(--fgTransparentWeak); + font-size: 0.85em; + + &:empty { + display: none; + } +} </style> diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue index b89e3e4c9d..a2a195cb09 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue @@ -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: { diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts new file mode 100644 index 0000000000..70817d83c3 --- /dev/null +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts @@ -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>; diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue new file mode 100644 index 0000000000..e9f4f68df8 --- /dev/null +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -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> diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index adb8d43349..f26ea11214 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -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(''); diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 096b88c309..4e80a5c0fb 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -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> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 33e594acd8..ad1c02a488 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -32,6 +32,7 @@ <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> </div> </template> + <script lang="ts"> export type Widget = { name: string; @@ -42,6 +43,7 @@ export type DefaultStoredWidget = { place: string | null; } & Widget; </script> + <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; import { v4 as uuid } from 'uuid'; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index ad36dcabe4..42abdcbdcc 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -2,7 +2,7 @@ <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> <img :class="$style.inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> - <div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]"> + <div v-if="user.isCat" :class="[$style.ears]"> <div :class="$style.earLeft"> <div v-if="false" :class="$style.layer"> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> @@ -154,24 +154,6 @@ watch(() => props.user.avatarBlurhash, () => { padding: 50%; pointer-events: none; - &.mask { - -webkit-mask: - url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%, - linear-gradient(#fff, #fff); - -webkit-mask-composite: destination-out, source-over; - mask: - url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%, - linear-gradient(#fff, #fff); // polyfill of `image(#fff)` - - > .earLeft { - animation: eartightleft 6s infinite; - } - - > .earRight { - animation: eartightright 6s infinite; - } - } - > .earLeft, > .earRight { contain: strict; diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue index 0237644d29..6ea81d257f 100644 --- a/packages/frontend/src/components/page/page.image.vue +++ b/packages/frontend/src/components/page/page.image.vue @@ -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> |