Merge branch 'develop' into mkjs-n
This commit is contained in:
commit
7d8fa48d47
14 changed files with 217 additions and 298 deletions
10
.github/workflows/storybook.yml
vendored
10
.github/workflows/storybook.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Checkout HEAD
|
- name: Checkout HEAD
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request_target'
|
||||||
run: git checkout ${{ github.head_ref }}
|
run: git checkout ${{ github.head_ref }}
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
|
@ -41,12 +41,12 @@ jobs:
|
||||||
- name: Build storybook
|
- name: Build storybook
|
||||||
run: pnpm --filter frontend build-storybook
|
run: pnpm --filter frontend build-storybook
|
||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
|
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master'
|
||||||
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
|
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
|
||||||
env:
|
env:
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
if: github.event_name != 'pull_request' && github.ref != 'refs/heads/master'
|
if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master'
|
||||||
id: chromatic_push
|
id: chromatic_push
|
||||||
run: |
|
run: |
|
||||||
DIFF="${{ github.event.before }} HEAD"
|
DIFF="${{ github.event.before }} HEAD"
|
||||||
|
@ -61,7 +61,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request_target'
|
||||||
id: chromatic_pull_request
|
id: chromatic_pull_request
|
||||||
run: |
|
run: |
|
||||||
DIFF="${{ github.base_ref }} HEAD"
|
DIFF="${{ github.base_ref }} HEAD"
|
||||||
|
@ -77,7 +77,7 @@ jobs:
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
- name: Notify that Chromatic will skip testing
|
- name: Notify that Chromatic will skip testing
|
||||||
uses: actions/github-script@v6.4.0
|
uses: actions/github-script@v6.4.0
|
||||||
if: github.event_name == 'pull_request' && steps.chromatic_pull_request.outputs.skip == 'true'
|
if: github.event_name == 'pull_request_target' && steps.chromatic_pull_request.outputs.skip == 'true'
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 13.x.x (unreleased)
|
## 13.12.2
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- 投稿したコンテンツのAIによる学習を軽減するオプションを追加
|
- 投稿したコンテンツのAIによる学習を軽減するオプションを追加
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
- Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正
|
- Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- センシティブワードの登録にAnd、正規表現が使用できるようになりました。
|
||||||
|
|
||||||
## 13.12.1
|
## 13.12.1
|
||||||
|
|
||||||
|
|
|
@ -1038,6 +1038,8 @@ thisChannelArchived: "Dieser Kanal wurde archiviert."
|
||||||
displayOfNote: "Anzeige von Notizen"
|
displayOfNote: "Anzeige von Notizen"
|
||||||
initialAccountSetting: "Kontoeinrichtung"
|
initialAccountSetting: "Kontoeinrichtung"
|
||||||
youFollowing: "Gefolgt"
|
youFollowing: "Gefolgt"
|
||||||
|
preventAiLarning: "Verwendung in machinellem Lernen (AI/KI) ablehnen"
|
||||||
|
preventAiLarningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (AI/KI) zu verwenden. Dies wird durch das Hinzufügen eines \"noai\"-HTML-Tags an den jeweiligen Inhalt erreicht. Da dieser Tag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich."
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
||||||
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
||||||
|
|
|
@ -1038,6 +1038,8 @@ thisChannelArchived: "This channel has been archived."
|
||||||
displayOfNote: "Note display"
|
displayOfNote: "Note display"
|
||||||
initialAccountSetting: "Profile setup"
|
initialAccountSetting: "Profile setup"
|
||||||
youFollowing: "Followed"
|
youFollowing: "Followed"
|
||||||
|
preventAiLarning: "Reject usage in Machine Learning (AI)"
|
||||||
|
preventAiLarningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (AI) data sets. This is achieved by adding a \"noai\" HTML-Tag to the respective content. A complete prevention can however not be achieved through this tag, as it may simply be ignored."
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Your account was successfully created!"
|
accountCreated: "Your account was successfully created!"
|
||||||
letsStartAccountSetup: "For starters, let's set up your profile."
|
letsStartAccountSetup: "For starters, let's set up your profile."
|
||||||
|
|
|
@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール"
|
||||||
resetPasswordConfirm: "パスワードリセットしますか?"
|
resetPasswordConfirm: "パスワードリセットしますか?"
|
||||||
sensitiveWords: "センシティブワード"
|
sensitiveWords: "センシティブワード"
|
||||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||||
|
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.12.1",
|
"version": "13.12.2",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource } from 'typeorm';
|
import { In, DataSource } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import RE2 from 're2';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
|
@ -238,7 +239,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.channel != null) data.localOnly = true;
|
if (data.channel != null) data.localOnly = true;
|
||||||
|
|
||||||
if (data.visibility === 'public' && data.channel == null) {
|
if (data.visibility === 'public' && data.channel == null) {
|
||||||
if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) {
|
const sensitiveWords = (await this.metaService.fetch()).sensitiveWords;
|
||||||
|
if (this.isSensitive(data, sensitiveWords)) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
|
@ -670,6 +672,31 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
// Register to search database
|
// Register to search database
|
||||||
this.index(note);
|
this.index(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
|
||||||
|
if (sensitiveWord.length > 0) {
|
||||||
|
const text = note.cw ?? note.text ?? '';
|
||||||
|
if (text === '') return false;
|
||||||
|
const matched = sensitiveWord.some(filter => {
|
||||||
|
// represents RegExp
|
||||||
|
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
if (!regexp) {
|
||||||
|
const words = filter.split(' ');
|
||||||
|
return words.every(keyword => text.includes(keyword));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new RE2(regexp[1], regexp[2]).test(text);
|
||||||
|
} catch (err) {
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (matched) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private incRenoteCount(renote: Note) {
|
private incRenoteCount(renote: Note) {
|
||||||
|
|
|
@ -541,6 +541,61 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('センシティブな投稿はhomeになる (単語指定)', async () => {
|
||||||
|
const sensitive = await api('admin/update-meta', {
|
||||||
|
sensitiveWords: [
|
||||||
|
"test",
|
||||||
|
]
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(sensitive.status, 204);
|
||||||
|
|
||||||
|
await new Promise(x => setTimeout(x, 2));
|
||||||
|
|
||||||
|
const note1 = await api('/notes/create', {
|
||||||
|
text: 'hogetesthuge',
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(note1.status, 200);
|
||||||
|
assert.strictEqual(note1.body.createdNote.visibility, 'home');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('センシティブな投稿はhomeになる (正規表現)', async () => {
|
||||||
|
const sensitive = await api('admin/update-meta', {
|
||||||
|
sensitiveWords: [
|
||||||
|
"/Test/i",
|
||||||
|
]
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(sensitive.status, 204);
|
||||||
|
|
||||||
|
const note2 = await api('/notes/create', {
|
||||||
|
text: 'hogetesthuge',
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(note2.status, 200);
|
||||||
|
assert.strictEqual(note2.body.createdNote.visibility, 'home');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('センシティブな投稿はhomeになる (スペースアンド)', async () => {
|
||||||
|
const sensitive = await api('admin/update-meta', {
|
||||||
|
sensitiveWords: [
|
||||||
|
"Test hoge"
|
||||||
|
]
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(sensitive.status, 204);
|
||||||
|
|
||||||
|
const note2 = await api('/notes/create', {
|
||||||
|
text: 'hogeTesthuge',
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(note2.status, 200);
|
||||||
|
assert.strictEqual(note2.body.createdNote.visibility, 'home');
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('notes/delete', () => {
|
describe('notes/delete', () => {
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-adaptive-border
|
v-adaptive-border
|
||||||
class="novjtctn"
|
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
||||||
:class="{ disabled, checked }"
|
|
||||||
:aria-checked="checked"
|
:aria-checked="checked"
|
||||||
:aria-disabled="disabled"
|
:aria-disabled="disabled"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
|
@ -10,11 +9,12 @@
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:class="$style.input"
|
||||||
>
|
>
|
||||||
<span class="button">
|
<span :class="$style.button">
|
||||||
<span></span>
|
<span></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="label"><slot></slot></span>
|
<span :class="$style.label"><slot></slot></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ function toggle(): void {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.novjtctn {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -53,17 +53,11 @@ function toggle(): void {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
> * {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed !important;
|
||||||
&, * {
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -74,10 +68,7 @@ function toggle(): void {
|
||||||
background-color: var(--accentedBg) !important;
|
background-color: var(--accentedBg) !important;
|
||||||
border-color: var(--accentedBg) !important;
|
border-color: var(--accentedBg) !important;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
cursor: default !important;
|
||||||
&, * {
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .button {
|
> .button {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
@ -89,44 +80,44 @@ function toggle(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> input {
|
.input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .button {
|
.button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background: none;
|
background: none;
|
||||||
border: solid 2px var(--inputBorder);
|
border: solid 2px var(--inputBorder);
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
transition: inherit;
|
transition: inherit;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .label {
|
|
||||||
margin-left: 28px;
|
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 20px;
|
position: absolute;
|
||||||
cursor: pointer;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-left: 28px;
|
||||||
|
display: block;
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
|
||||||
class="ziffeomt"
|
|
||||||
:class="{ disabled, checked }"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:class="$style.input"
|
||||||
@keydown.enter="toggle"
|
@keydown.enter="toggle"
|
||||||
>
|
>
|
||||||
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle">
|
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
|
||||||
<div class="knob"></div>
|
<div :class="$style.knob"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="label">
|
<span :class="$style.body">
|
||||||
<!-- TODO: 無名slotの方は廃止 -->
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
|
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
|
||||||
<p class="caption"><slot name="caption"></slot></p>
|
<p :class="$style.caption"><slot name="caption"></slot></p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -45,52 +43,12 @@ const toggle = () => {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.ziffeomt {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
> * {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
> .button {
|
> .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 {
|
&.disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
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>
|
</style>
|
||||||
|
|
|
@ -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)"/>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export type Widget = {
|
export type Widget = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -42,6 +43,7 @@ export type DefaultStoredWidget = {
|
||||||
place: string | null;
|
place: string | null;
|
||||||
} & Widget;
|
} & Widget;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref } from 'vue';
|
import { defineAsyncComponent, ref } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
<MkTextarea v-model="sensitiveWords">
|
<MkTextarea v-model="sensitiveWords">
|
||||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
|
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
|
|
|
@ -28,9 +28,9 @@
|
||||||
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
|
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
|
||||||
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
||||||
|
|
||||||
<div class="vxjfqztj">
|
<div>
|
||||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA>
|
||||||
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
|
|
||||||
|
@ -132,15 +132,3 @@ os.api('hashtags/list', {
|
||||||
tagsRemote = tags;
|
tagsRemote = tags;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.vxjfqztj {
|
|
||||||
> * {
|
|
||||||
margin-right: 16px;
|
|
||||||
|
|
||||||
&.local {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
Loading…
Reference in a new issue