Compare commits

..

13 Commits

Author SHA1 Message Date
syuilo
961badd01d New translations ja-jp.yml (German) 2025-03-23 03:19:14 +09:00
syuilo
1e4c49a9ab New translations ja-jp.yml (English) 2025-03-23 03:19:12 +09:00
syuilo
cc9d5bc2b5 New translations ja-jp.yml (Czech) 2025-03-21 02:23:37 +09:00
syuilo
144fabdead New translations ja-jp.yml (Korean) 2025-03-20 23:11:04 +09:00
syuilo
dfab6b1b8d lint(frontend): force window prefix 2025-03-20 15:44:06 +09:00
syuilo
fac59d75e2 lint(frontend): relax id-denylist rule 2025-03-20 15:43:35 +09:00
syuilo
fd3e28812e clean up console 2025-03-20 15:15:46 +09:00
syuilo
6a90b7e04b add todo 2025-03-20 15:09:50 +09:00
syuilo
8d8414687a enhance(frontend): improve preference manager stability 2025-03-20 14:57:14 +09:00
syuilo
0c682dd027 🎨 2025-03-20 13:34:57 +09:00
syuilo
3399c786a8 refactor(frontend): refactor components 2025-03-20 13:33:01 +09:00
syuilo
64cf101fe7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-20 13:16:23 +09:00
syuilo
8b6d90b7a4 🎨 2025-03-20 13:16:08 +09:00
17 changed files with 132 additions and 64 deletions

View File

@@ -9,6 +9,8 @@ reset: "Obnovit"
notifications: "Oznámení"
username: "Uživatelské jméno"
password: "Heslo"
initialPasswordForSetup: "Počáteční heslo pro nastavení"
initialPasswordIsIncorrect: "Počáteční heslo pro nastavení je nesprávné"
forgotPassword: "Zapomenuté heslo"
fetchingAsApObject: "Načítám data z Fediversu..."
ok: "Potvrdit"
@@ -478,6 +480,8 @@ uiLanguage: "Jazyk uživatelského rozhraní"
aboutX: "O {x}"
emojiStyle: "Styl emoji"
native: "Výchozí"
style: "Vzhled"
popup: "Vyskakovací okno"
showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši"
noHistory: "Žádná historie"
signinHistory: "Historie přihlášení"

View File

@@ -1144,7 +1144,7 @@ preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive
preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich."
options: "Optionen"
specifyUser: "Spezifischer Benutzer"
lookupConfirm: "Zustimmen?"
lookupConfirm: "Bist du sicher, dass du das nachschlagen möchtest?"
openTagPageConfirm: "Hashtag Seite wirklich öffnen?"
specifyHost: "Host"
failedToPreviewUrl: "Vorschau nicht anzeigbar"

View File

@@ -1392,7 +1392,7 @@ _abuseUserReport:
resolve: "Resolve"
accept: "Accept"
reject: "Reject"
resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
resolveTutorial: "If the report's content is legitimate, select \"Accept\" to mark it as resolved.\nIf the report's content is illegitimate, select \"Reject\" to ignore it."
_delivery:
status: "Delivery status"
stop: "Suspended"
@@ -2598,7 +2598,7 @@ _webhookSettings:
testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
_abuseReport:
_notificationRecipient:
createRecipient: "Add a recipient for reports"
createRecipient: "Add recipient for reports"
modifyRecipient: "Edit a recipient for reports"
recipientType: "Notification type"
_recipientType:
@@ -2828,7 +2828,7 @@ _customEmojisManager:
confirmImportEmojisTitle: "Import Emojis"
confirmImportEmojisDescription: "Import {count} Emoji(s) received from the remote server. Please pay close attention to the license of the Emoji. Are you sure to continue?"
_local:
tabTitleList: "List of registered Emojis"
tabTitleList: "Registered emojis"
tabTitleRegister: "Emoji registration"
_list:
emojisNothing: "There are no registered Emojis."

View File

@@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name}님이 \"{word}\"를 언급했습니다."
makeActive: "활성화"
display: "보기"
copy: "복사"
copiedToClipboard: "클립보드에 복사되었습니다."
metrics: "통계"
overview: "요약"
logs: "로그"
@@ -1294,7 +1295,7 @@ thereAreNChanges: "{n}건 변경이 있습니다."
signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "입력된 패스키는 정상적이나, 비밀번호 없이 로그인 하는 기능이 비활성화 되어있습니다."
messageToFollower: "팔로워에게 보낼 메시지"
target: "대상"
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
@@ -1325,21 +1326,40 @@ skip: "건너뛰기"
restore: "복원"
syncBetweenDevices: "장치간 동기화"
preferenceSyncConflictTitle: "서버에 설정값이 존재합니다."
preferenceSyncConflictText: "동기화를 활성화 한 항목의 설정 값은 서버에 저장되지만, 해당 항목은 이미 서버에 설정 값이 저장되어져 있습니다. 어느 쪽의 설정 값을 덮어씌울까요?"
preferenceSyncConflictChoiceServer: "서버 설정값"
preferenceSyncConflictChoiceDevice: "장치 설정값"
preferenceSyncConflictChoiceCancel: "동기화 취소"
paste: "붙여넣기"
emojiPalette: "이모지 팔레트"
postForm: "글 입력란"
textCount: "문자 수"
information: "정보"
_emojiPalette:
palettes: "팔레트"
enableSyncBetweenDevicesForPalettes: "팔레트의 디바이스 간 동기화를 활성화"
paletteForMain: "메인으로 사용할 팔레트"
paletteForReaction: "리액션으로 사용할 팔레트"
_settings:
driveBanner: "드라이브 관리, 사용량 확인, 파일 업로드에 관한 설정을 합니다."
pluginBanner: "플러그인을 사용하면 클라이언트 기능을 확장할 수 있습니다. 플러그인 설치와 개별적인 설정을 합니다."
notificationsBanner: "서버에서 받는 알림의 종류 및 범위, 푸시 알림 설정을 합니다."
api: "API"
webhook: "Webhook"
serviceConnection: "서비스 연동"
serviceConnectionBanner: "외부 앱, 서비스와 연결하기 위한 액세스 토큰과 웹 훅 관리 설정을 합니다."
accountData: "계정 데이터"
accountDataBanner: "계정 데이터의 아카이브를 추출하기/가져오기 하여 관리할 수 있습니다."
muteAndBlockBanner: "숨길 컨텐츠의 설정과, 특정 유저의 리액션을 제한하는 설정을 관리합니다."
accessibilityBanner: "좀 더 쾌적하게 사용할 수 있도록 클라이언트의 시각 및 움직임에 관한 개인화 설정을 합니다."
privacyBanner: "컨텐츠, 계정의 발견 범위, 팔로우 승인제 등의 계정의 프라이버시에 관한 설정을 합니다."
securityBanner: "비밀번호, 로그인 방법, OTP, 패스 키 등의 계정의 보안에 관련된 설정을 합니다."
preferencesBanner: "취향에 알맞는 클라이언트의 전체적인 동작을 설정합니다."
appearanceBanner: "취향에 알맞는 클라이언트의 디스플레이, 표시 방법에 관한 설정을 합니다."
soundsBanner: "클라이언트에서 재생할 소리에 대한 설정을 합니다."
timelineAndNote: "타임라인과 노트"
makeEveryTextElementsSelectable: "모든 텍스트 요소를 선택할 수 있도록 함"
makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 사용자의 접근성이 나빠질 수도 있습니다."
_preferencesProfile:
profileName: "프로필 이름"
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
@@ -1363,6 +1383,7 @@ _accountSettings:
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
mayNotEffectSomeSituations: "여기서 설정하는 제한은 모더레이션이나 리모트 서버에서 볼 때 등 일부 환경에서는 적용되지 않을 수도 있습니다."
notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트"
notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트"
_abuseUserReport:
@@ -2503,6 +2524,7 @@ _notification:
achievementEarned: "도전 과제 획득"
exportCompleted: "추출을 성공함"
login: "로그인"
createToken: "액세스 토큰 만들기"
test: "알림 테스트"
app: "연동된 앱을 통한 알림"
_actions:
@@ -2530,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기"
usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다"
flexible: "폭 자동 조정"
enableSyncBetweenDevicesForProfiles: "프로파일 정보의 디바이스 간 동기화를 활성화"
_columns:
main: "메인"
widgets: "위젯"

View File

@@ -33,13 +33,11 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
Math.min(navigator.hardwareConcurrency - 1, 4),
);
resolve(workers);
if (_DEV_) console.log('WebGL2 in worker is supported!');
} else {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
if (_DEV_) console.log('WebGL2 in worker is not supported...');
}
testWorker.terminate();
});

View File

@@ -50,13 +50,36 @@ export default [
// defineExposeが誤検知されてしまう
'@typescript-eslint/no-unused-expressions': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
// close ... window.closeと衝突 or 紛らわしい
// open ... window.openと衝突 or 紛らわしい
// fetch ... window.fetchと衝突 or 紛らわしい
// location ... window.locationと衝突 or 紛らわしい
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location'],
'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location'],
'no-restricted-globals': [
'error',
{
'name': 'open',
'message': 'Use `window.open`.',
},
{
'name': 'close',
'message': 'Use `window.close`.',
},
{
'name': 'fetch',
'message': 'Use `window.fetch`.',
},
{
'name': 'location',
'message': 'Use `window.location`.',
},
{
'name': 'history',
'message': 'Use `window.history`.',
},
],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,

View File

@@ -300,6 +300,7 @@ export async function common(createVue: () => App<Element>) {
removeSplash();
//#region Self-XSS 対策メッセージ
if (!_DEV_) {
console.log(
`%c${i18n.ts._selfXssPrevention.warning}`,
'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
@@ -318,6 +319,7 @@ export async function common(createVue: () => App<Element>) {
'font-size: 20px; font-weight: 700; color: #f00;',
);
console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
}
//#endregion
return {

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
</template>
<script lang="ts" setup>

View File

@@ -69,13 +69,11 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
Math.min(navigator.hardwareConcurrency - 1, 4),
);
resolve(workers);
if (_DEV_) console.log('WebGL2 in worker is supported!');
} else {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
if (_DEV_) console.log('WebGL2 in worker is not supported...');
}
testWorker.terminate();
});

View File

@@ -0,0 +1,21 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<MkAnimBg style="position: absolute;"/>
<div class="_pageScrollable" style="position: absolute; top: 0; width: 100%; height: 100%;">
<slot></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import MkAnimBg from '@/components/MkAnimBg.vue';
</script>
<style lang="scss" module>
</style>

View File

@@ -4,9 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="550">
<MkPageWithAnimBg>
<MkSpacer :contentMax="550" :marginMax="50">
<MkLoading v-if="uiPhase === 'fetching'"/>
<MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()" @cancel="close_()">
<template #additionalInfo>
@@ -38,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</MkPageWithAnimBg>
</template>
<script lang="ts" setup>
@@ -58,6 +57,7 @@ import { parseThemeCode, installTheme } from '@/theme.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkPageWithAnimBg from '@/components/MkPageWithAnimBg.vue';
const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching');
const errorKV = ref<{
@@ -232,10 +232,6 @@ url.value = urlParams.get('url');
hash.value = urlParams.get('hash');
fetch();
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts._externalResourceInstaller.title,
icon: 'ti ti-download',

View File

@@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<MkPageWithAnimBg>
<div :class="$style.formContainer">
<div :class="$style.form">
<MkAuthConfirm
@@ -25,16 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkAuthConfirm>
</div>
</div>
</div>
</MkPageWithAnimBg>
</template>
<script lang="ts" setup>
import { computed, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkAnimBg from '@/components/MkAnimBg.vue';
import MkPageWithAnimBg from '@/components/MkPageWithAnimBg.vue';
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';

View File

@@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<MkPageWithAnimBg>
<div :class="$style.formContainer">
<div :class="$style.form">
<MkAuthConfirm
@@ -19,12 +18,12 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
</div>
</div>
</div>
</MkPageWithAnimBg>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import MkAnimBg from '@/components/MkAnimBg.vue';
import MkPageWithAnimBg from '@/components/MkPageWithAnimBg.vue';
import { definePage } from '@/page.js';
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';

View File

@@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<MkPageWithAnimBg>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()">
<div :class="$style.banner">
@@ -21,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</form>
</div>
</div>
</MkPageWithAnimBg>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import MkPageWithAnimBg from '@/components/MkPageWithAnimBg.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -64,8 +63,8 @@ function submit() {
min-height: 100svh;
padding: 32px 32px 64px 32px;
box-sizing: border-box;
display: grid;
place-content: center;
display: grid;
place-content: center;
}
.form {

View File

@@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<MkPageWithAnimBg>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()">
<div :class="$style.title">
@@ -35,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</form>
</div>
</div>
</MkPageWithAnimBg>
</template>
<script lang="ts" setup>
@@ -46,7 +45,7 @@ import MkInput from '@/components/MkInput.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkAnimBg from '@/components/MkAnimBg.vue';
import MkPageWithAnimBg from '@/components/MkPageWithAnimBg.vue';
import { login } from '@/accounts.js';
const username = ref('');

View File

@@ -32,6 +32,8 @@ export type SoundStore = {
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
export const PREF_DEF = {
// TODO: 持つのはホストやユーザーID、ユーザー名など最低限にしといて、その他のプロフィール情報はpreferences外で管理した方が綺麗そう
// 現状だと、updateCurrentAccount/updateCurrentAccountPartialが呼ばれるたびに「設定」へのcommitが行われて不自然(明らかに設定の更新とは捉えにくい)だし
accounts: {
default: [] as [host: string, user: Misskey.entities.User][],
},

View File

@@ -139,9 +139,16 @@ export class PreferencesManager {
}
public commit<K extends keyof PREF>(key: K, value: ValueOf<K>) {
console.log('prefer:commit', key, value);
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.rewriteRawState(key, value);
if (deepEqual(this.s[key], v)) {
console.log('(skip) prefer:commit', key, v);
return;
}
console.log('prefer:commit', key, v);
this.rewriteRawState(key, v);
const record = this.getMatchedRecordOf(key);
@@ -149,7 +156,7 @@ export class PreferencesManager {
this.profile.preferences[key].push([makeScope({
server: host,
account: $i!.id,
}), value, {}]);
}), v, {}]);
this.save();
return;
}
@@ -157,12 +164,12 @@ export class PreferencesManager {
if (parseScope(record[0]).server == null && this.isServerDependentKey(key)) {
this.profile.preferences[key].push([makeScope({
server: host,
}), value, {}]);
}), v, {}]);
this.save();
return;
}
record[1] = value;
record[1] = v;
this.save();
if (record[2].sync) {