summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2025-09-19 21:02:30 +0900
committerGitHub <noreply@github.com>2025-09-19 21:02:30 +0900
commit42b2aea53364c57c39ebb953359ece4b7b0017a5 (patch)
tree8abe7de01705ed3fb6324c6a174162c7bb9288f2 /packages/frontend/src/components
parent🎨 (diff)
downloadmisskey-42b2aea53364c57c39ebb953359ece4b7b0017a5.tar.gz
misskey-42b2aea53364c57c39ebb953359ece4b7b0017a5.tar.bz2
misskey-42b2aea53364c57c39ebb953359ece4b7b0017a5.zip
feat(frontend): 自分のプロフィールページの二次元コード(QRコード)を表示し、他の人のコードを読み取りするページを追加 (#16456)
* wip (qr.show.vue) * added to navbar * qr.show.vue * fix * added to navbar * fix size * :art: * :art: * fix div warn * fix * use * 0.25 * fix?? * fix lint * clean up * ??? * ? * fix * :art: * :art: * refactor * :art: * :art: * :ar:t * :art: * iphone flip * no lazy import * :art: * :art: * :art: * ユーザー全部flipでいいや * :v: * fix * fix * fix lint * :art: * fix type * fix: local user profile url cannot be resolved with ap/show * fix: local user url with hostname could not be resolved * chore: use common utility for checking self host * wip * :art: * :art: * fix imports * fix * fix * fix * :art: * fix... * set spacer-w * :v: * 全画面でQRを読むように * fix * :art: * modify navbar.ts * start/stop on vue activation * display raw content read from qr * 端末のQRをスキャンするボタンを追加 * chore * やっぱりmfmを先に表示する * :art: * fix 18n * QRの内容は/users/:userIdにする * add spdx * use cqh * `defineProps` is a compiler macro and no longer needs to be imported. * use MkUserName * 🎨 * 🎨 * refactor * clean up * refactor * 🎨 * Update qr.show.vue * Misskeyロゴにdrop-shadowを追加 * clean up: do not use empty css * fix os.select usage * Update qr.vue * Update qr.show.vue * Update qr.show.vue * Update get-user-menu.ts * ✌️ * Update show.ts * Update ja-JP.yml * watermark * Update CHANGELOG.md * Update qr.read.vue * Update qr.read.vue * wip * Update MkWatermarkEditorDialog.Layer.vue --------- Co-authored-by: anatawa12 <anatawa12@icloud.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkPolkadots.vue15
-rw-r--r--packages/frontend/src/components/MkPositionSelector.vue18
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue73
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.vue58
4 files changed, 114 insertions, 50 deletions
diff --git a/packages/frontend/src/components/MkPolkadots.vue b/packages/frontend/src/components/MkPolkadots.vue
index 285c4d0b79..4f1346b685 100644
--- a/packages/frontend/src/components/MkPolkadots.vue
+++ b/packages/frontend/src/components/MkPolkadots.vue
@@ -4,14 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.root, accented ? $style.accented : null]"></div>
+<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
accented?: boolean;
+ revered?: boolean;
+ height?: number;
}>(), {
accented: false,
+ revered: false,
+ height: 200,
});
</script>
@@ -27,14 +31,17 @@ const props = withDefaults(defineProps<{
--dot-size: 2px;
--gap-size: 40px;
--offset: calc(var(--gap-size) / 2);
+ --height: v-bind('props.height + "px"');
- height: 200px;
- margin-bottom: -200px;
-
+ height: var(--height);
background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size));
background-position: 0 0, 0 0, var(--offset) var(--offset);
background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size);
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
pointer-events: none;
+
+ &.revered {
+ mask-image: linear-gradient(to top, black 0%, transparent 100%);
+ }
}
</style>
diff --git a/packages/frontend/src/components/MkPositionSelector.vue b/packages/frontend/src/components/MkPositionSelector.vue
index 739f55125b..6f12aada30 100644
--- a/packages/frontend/src/components/MkPositionSelector.vue
+++ b/packages/frontend/src/components/MkPositionSelector.vue
@@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root]">
<div :class="$style.items">
- <button class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-align-box-left-top"></i></button>
- <button class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-align-box-center-top"></i></button>
- <button class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-align-box-right-top"></i></button>
- <button class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-align-box-left-middle"></i></button>
- <button class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-align-box-center-middle"></i></button>
- <button class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-align-box-right-middle"></i></button>
- <button class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-align-box-left-bottom"></i></button>
- <button class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-align-box-center-bottom"></i></button>
- <button class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-align-box-right-bottom"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-arrow-up-left"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-arrow-up"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-arrow-up-right"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-arrow-left"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-focus-2"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-arrow-right"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-arrow-down-left"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-arrow-down"></i></button>
+ <button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-arrow-down-right"></i></button>
</div>
</div>
</template>
diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue
index 11ae091d90..288293db3f 100644
--- a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue
+++ b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue
@@ -19,6 +19,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSlot>
<MkRange
+ :modelValue="layer.align.margin ?? 0"
+ :min="0"
+ :max="0.25"
+ :step="0.01"
+ :textConverter="(v) => (v * 100).toFixed(1) + '%'"
+ continuousUpdate
+ @update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'text' }>).align.margin = v"
+ >
+ <template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
+ </MkRange>
+
+ <MkRange
v-model="layer.scale"
:min="0"
:max="1"
@@ -67,6 +79,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSlot>
<MkRange
+ :modelValue="layer.align.margin ?? 0"
+ :min="0"
+ :max="0.25"
+ :step="0.01"
+ :textConverter="(v) => (v * 100).toFixed(1) + '%'"
+ continuousUpdate
+ @update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'image' }>).align.margin = v"
+ >
+ <template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
+ </MkRange>
+
+ <MkRange
v-model="layer.scale"
:min="0"
:max="1"
@@ -107,6 +131,55 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</template>
+ <template v-else-if="layer.type === 'qr'">
+ <MkInput v-model="layer.data" debounce>
+ <template #label>{{ i18n.ts._watermarkEditor.text }}</template>
+ <template #caption>{{ i18n.ts._watermarkEditor.leaveBlankToAccountUrl }}</template>
+ </MkInput>
+
+ <FormSlot>
+ <template #label>{{ i18n.ts._watermarkEditor.position }}</template>
+ <MkPositionSelector
+ v-model:x="layer.align.x"
+ v-model:y="layer.align.y"
+ ></MkPositionSelector>
+ </FormSlot>
+
+ <MkRange
+ :modelValue="layer.align.margin ?? 0"
+ :min="0"
+ :max="0.25"
+ :step="0.01"
+ :textConverter="(v) => (v * 100).toFixed(1) + '%'"
+ continuousUpdate
+ @update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'qr' }>).align.margin = v"
+ >
+ <template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
+ </MkRange>
+
+ <MkRange
+ v-model="layer.scale"
+ :min="0"
+ :max="1"
+ :step="0.01"
+ :textConverter="(v) => (v * 100).toFixed(1) + '%'"
+ continuousUpdate
+ >
+ <template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
+ </MkRange>
+
+ <MkRange
+ v-model="layer.opacity"
+ :min="0"
+ :max="1"
+ :step="0.01"
+ :textConverter="(v) => (v * 100).toFixed(1) + '%'"
+ continuousUpdate
+ >
+ <template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
+ </MkRange>
+ </template>
+
<template v-else-if="layer.type === 'stripe'">
<MkRange
v-model="layer.frequency"
diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue
index 75a45548fd..0d0488d9bc 100644
--- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue
+++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue
@@ -30,22 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.controls">
<div class="_spacer _gaps">
- <MkSelect v-model="type" :items="typeDef">
- <template #label>{{ i18n.ts._watermarkEditor.type }}</template>
- </MkSelect>
-
- <div v-if="type === 'text' || type === 'image'">
- <XLayer
- v-for="(layer, i) in preset.layers"
- :key="layer.id"
- v-model:layer="preset.layers[i]"
- ></XLayer>
- </div>
- <div v-else-if="type === 'advanced'" class="_gaps_s">
+ <div class="_gaps_s">
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
<template #label>
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
+ <div v-if="layer.type === 'qr'">{{ i18n.ts._watermarkEditor.qr }}</div>
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
@@ -95,7 +85,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
id: genId(),
type: 'text',
text: `(c) @${$i.username}`,
- align: { x: 'right', y: 'bottom' },
+ align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
angle: 0,
opacity: 0.75,
@@ -109,7 +99,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
type: 'image',
imageId: null,
imageUrl: null,
- align: { x: 'right', y: 'bottom' },
+ align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
angle: 0,
opacity: 0.75,
@@ -118,6 +108,17 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
};
}
+function createQrLayer(): WatermarkPreset['layers'][number] {
+ return {
+ id: genId(),
+ type: 'qr',
+ data: '',
+ align: { x: 'right', y: 'bottom', margin: 0 },
+ scale: 0.3,
+ opacity: 1,
+ };
+}
+
function createStripeLayer(): WatermarkPreset['layers'][number] {
return {
id: genId(),
@@ -165,7 +166,7 @@ const props = defineProps<{
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
id: genId(),
name: '',
- layers: [createTextLayer()],
+ layers: [],
});
const emit = defineEmits<{
@@ -187,28 +188,6 @@ async function cancel() {
dialog.value?.close();
}
-const {
- model: type,
- def: typeDef,
-} = useMkSelect({
- items: [
- { label: i18n.ts._watermarkEditor.text, value: 'text' },
- { label: i18n.ts._watermarkEditor.image, value: 'image' },
- { label: i18n.ts._watermarkEditor.advanced, value: 'advanced' },
- ],
- initialValue: preset.layers.length > 1 ? 'advanced' : preset.layers[0].type,
-});
-
-watch(type, () => {
- if (type.value === 'text') {
- preset.layers = [createTextLayer()];
- } else if (type.value === 'image') {
- preset.layers = [createImageLayer()];
- } else if (type.value === 'advanced') {
- // nop
- }
-});
-
watch(preset, async (newValue, oldValue) => {
if (renderer != null) {
renderer.setLayers(preset.layers);
@@ -339,6 +318,11 @@ function addLayer(ev: MouseEvent) {
preset.layers.push(createImageLayer());
},
}, {
+ text: i18n.ts._watermarkEditor.qr,
+ action: () => {
+ preset.layers.push(createQrLayer());
+ },
+ }, {
text: i18n.ts._watermarkEditor.stripe,
action: () => {
preset.layers.push(createStripeLayer());