mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-07 08:45:33 +02:00
Compare commits
18 Commits
2026.5.1-a
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b73ac26612 | ||
|
|
b528ff9c59 | ||
|
|
a82ba0d775 | ||
|
|
b78e0168b0 | ||
|
|
33f59b3469 | ||
|
|
5b478dda9d | ||
|
|
90725d6a8c | ||
|
|
86542f07d3 | ||
|
|
45022bc766 | ||
|
|
35711fc8e1 | ||
|
|
45f140aa86 | ||
|
|
22ce7b58ca | ||
|
|
37107c9818 | ||
|
|
a5a43c8c06 | ||
|
|
723d8add2f | ||
|
|
9d20152e05 | ||
|
|
37412f0e1b | ||
|
|
712b51c142 |
4
.github/workflows/api-misskey-js.yml
vendored
4
.github/workflows/api-misskey-js.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
2
.github/workflows/changelog-check.yml
vendored
2
.github/workflows/changelog-check.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
|
||||
12
.github/workflows/check-misskey-js-autogen.yml
vendored
12
.github/workflows/check-misskey-js-autogen.yml
vendored
@@ -25,11 +25,11 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
# packages/misskey-js/generator/built/autogen
|
||||
- name: Upload Generated
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: generated-misskey-js
|
||||
path: packages/misskey-js/generator/built/autogen
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- name: Upload From Merged
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: actual-misskey-js
|
||||
path: packages/misskey-js/src/autogen
|
||||
@@ -86,13 +86,13 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: download generated-misskey-js
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: generated-misskey-js
|
||||
path: misskey-js-generated
|
||||
|
||||
- name: download actual-misskey-js
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: actual-misskey-js
|
||||
path: misskey-js-actual
|
||||
|
||||
14
.github/workflows/docker-develop.yml
vendored
14
.github/workflows/docker-develop.yml
vendored
@@ -29,15 +29,15 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -66,15 +66,15 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -34,21 +34,21 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: ${{ env.TAGS }}
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push to Docker Hub
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -77,21 +77,21 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: ${{ env.TAGS }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
8
.github/workflows/get-api-diff.yml
vendored
8
.github/workflows/get-api-diff.yml
vendored
@@ -30,9 +30,9 @@ jobs:
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Copy API.json
|
||||
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: api-artifact-${{ matrix.api-json-name }}
|
||||
path: ${{ matrix.api-json-name }}
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
echo "$PR_NUMBER" > ./pr_number
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: api-artifact-pr-number
|
||||
path: pr_number
|
||||
|
||||
10
.github/workflows/get-backend-memory.yml
vendored
10
.github/workflows/get-backend-memory.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@@ -45,9 +45,9 @@ jobs:
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
# Start the server and measure memory usage
|
||||
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: memory-artifact-${{ matrix.memory-json-name }}
|
||||
path: ${{ matrix.memory-json-name }}
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
echo "$PR_NUMBER" > ./pr_number
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: memory-artifact-pr-number
|
||||
path: pr_number
|
||||
|
||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -41,8 +41,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -74,14 +74,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.3.0
|
||||
uses: actions/cache@v5.0.5
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
@@ -105,8 +105,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/locale.yml
vendored
4
.github/workflows/locale.yml
vendored
@@ -21,8 +21,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
- uses: actions/setup-node@v6.3.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
cache: "pnpm"
|
||||
|
||||
4
.github/workflows/on-release-created.yml
vendored
4
.github/workflows/on-release-created.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/report-api-diff.yml
vendored
4
.github/workflows/report-api-diff.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
# api-artifact
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v8.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Echo full diff
|
||||
run: cat ./api-full.json.diff
|
||||
- name: Upload full diff to Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: api-artifact
|
||||
path: |
|
||||
|
||||
2
.github/workflows/report-backend-memory.yml
vendored
2
.github/workflows/report-backend-memory.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v8.0.0
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
2
.github/workflows/request-release-review.yml
vendored
2
.github/workflows/request-release-review.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Reply
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const body = `To dev team (@misskey-dev/dev):
|
||||
|
||||
8
.github/workflows/storybook.yml
vendored
8
.github/workflows/storybook.yml
vendored
@@ -37,9 +37,9 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
env:
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
- name: Notify that Chromatic detects changes
|
||||
uses: actions/github-script@v8.0.0
|
||||
uses: actions/github-script@v9
|
||||
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
|
||||
})
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: storybook
|
||||
path: packages/frontend/storybook-static
|
||||
|
||||
24
.github/workflows/test-backend.yml
vendored
24
.github/workflows/test-backend.yml
vendored
@@ -45,11 +45,11 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.38.2
|
||||
image: getmeili/meilisearch:v1.42.1
|
||||
ports:
|
||||
- 57712:7700
|
||||
env:
|
||||
@@ -61,13 +61,13 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Get current date
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Setup and Restore ffmpeg/ffprobe Cache
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
/usr/local/bin/ffmpeg
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@@ -140,9 +140,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage:e2e
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -184,12 +184,12 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Get current date
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
6
.github/workflows/test-federation.yml
vendored
6
.github/workflows/test-federation.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Get current date
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Setup and Restore ffmpeg/ffprobe Cache
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
/usr/local/bin/ffmpeg
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
18
.github/workflows/test-frontend.yml
vendored
18
.github/workflows/test-frontend.yml
vendored
@@ -32,9 +32,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter frontend test-and-coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/frontend/coverage/coverage-final.json
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
image: redis:8
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@@ -86,9 +86,9 @@ jobs:
|
||||
#- uses: browser-actions/setup-firefox@latest
|
||||
# if: ${{ matrix.browser == 'firefox' }}
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
- name: Cypress install
|
||||
run: pnpm exec cypress install
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
uses: cypress-io/github-action@v7.1.9
|
||||
timeout-minutes: 15
|
||||
with:
|
||||
install: false
|
||||
@@ -113,12 +113,12 @@ jobs:
|
||||
wait-on: 'http://localhost:61812'
|
||||
headed: true
|
||||
browser: ${{ matrix.browser }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: ${{ matrix.browser }}-cypress-screenshots
|
||||
path: cypress/screenshots
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ matrix.browser }}-cypress-videos
|
||||
|
||||
6
.github/workflows/test-misskey-js.yml
vendored
6
.github/workflows/test-misskey-js.yml
vendored
@@ -25,10 +25,10 @@ jobs:
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
CI: true
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/misskey-js/coverage/coverage-final.json
|
||||
|
||||
4
.github/workflows/test-production.yml
vendored
4
.github/workflows/test-production.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/validate-api-json.yml
vendored
4
.github/workflows/validate-api-json.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.4.0
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.3.0
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,10 +1,28 @@
|
||||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように
|
||||
- Fix: テーマエディター使用時に、最初の変更のみ適用される問題を修正
|
||||
- Fix: テーマのプレビュー時、既存のテーマとIDが被っている場合にプレビューできない問題を修正
|
||||
- Fix: テーマのインストールエラーの表示を改善
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
|
||||
## 2026.5.1
|
||||
|
||||
### General
|
||||
- Enhance: チャンネルの作成の可否をロールポリシーで制御できるように
|
||||
- Fix: `.devcontainer/compose.yml`のvolumeのマウントパスを修正
|
||||
|
||||
### Client
|
||||
-
|
||||
- Enhance: ノートの詳細表示での公開範囲の表示を改善
|
||||
(Cherry-picked from https://github.com/kokonect-link/cherrypick/commit/ecc75563f4e428b66adccc379bf317b5b21ed8e6)
|
||||
- Fix: ロール設定画面でロールをアサイン/アサイン解除した際、リロードしなくても画面に反映されるよう修正
|
||||
|
||||
### Server
|
||||
- Fix: ID生成アルゴリズムにULIDを使用している場合に通知が約10秒遅延する問題を修正
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.21
|
||||
# syntax = docker/dockerfile:1.23
|
||||
|
||||
ARG NODE_VERSION=22.22.0-bookworm
|
||||
ARG NODE_VERSION=22.22.2-bookworm
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
|
||||
@@ -1408,6 +1408,7 @@ frame: "Frame"
|
||||
presets: "Preset"
|
||||
zeroPadding: "Zero padding"
|
||||
nothingToConfigure: "No configurable options available"
|
||||
viewRenotedChannel: "Show renoted channel"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "File caption"
|
||||
@@ -3401,6 +3402,8 @@ _imageEffector:
|
||||
threshold: "Threshold"
|
||||
centerX: "Center X"
|
||||
centerY: "Center Y"
|
||||
density: "Density"
|
||||
zoomLinesOutlineThickness: "Outline shadow thickness"
|
||||
zoomLinesMaskSize: "Center diameter"
|
||||
circle: "Circular"
|
||||
drafts: "Drafts"
|
||||
|
||||
@@ -1408,6 +1408,7 @@ frame: "Marco"
|
||||
presets: "Predefinido"
|
||||
zeroPadding: "Relleno cero"
|
||||
nothingToConfigure: "No hay nada que configurar"
|
||||
viewRenotedChannel: "Ver el canal al que te has suscrito"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Título del archivo"
|
||||
|
||||
@@ -1409,6 +1409,8 @@ presets: "プリセット"
|
||||
zeroPadding: "ゼロ埋め"
|
||||
nothingToConfigure: "設定項目はありません"
|
||||
viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -2122,6 +2124,7 @@ _role:
|
||||
canSearchNotes: "ノート検索の利用"
|
||||
canSearchUsers: "ユーザー検索の利用"
|
||||
canUseTranslator: "翻訳機能の利用"
|
||||
canCreateChannel: "チャンネルの作成"
|
||||
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
|
||||
canImportAntennas: "アンテナのインポートを許可"
|
||||
canImportBlocking: "ブロックのインポートを許可"
|
||||
|
||||
@@ -2098,6 +2098,7 @@ _role:
|
||||
canSearchNotes: "노트 검색 이용 가능 여부"
|
||||
canSearchUsers: "유저 검색 이용"
|
||||
canUseTranslator: "번역 기능의 사용"
|
||||
canCreateChannel: "패널 생성"
|
||||
avatarDecorationLimit: "아바타 장식의 최대 붙임 개수"
|
||||
canImportAntennas: "안테나 가져오기 허용"
|
||||
canImportBlocking: "차단 목록 가져오기 허용"
|
||||
|
||||
@@ -1332,7 +1332,9 @@ overrideByAccount: "Переопределить этим аккаунтом"
|
||||
untitled: "Без названия"
|
||||
noName: "Имя не указано"
|
||||
skip: "Пропустить"
|
||||
restore: "Восстановить"
|
||||
syncBetweenDevices: "Синхронизировать между устройствами"
|
||||
paste: "вставить"
|
||||
postForm: "Форма отправки"
|
||||
textCount: "Количество символов"
|
||||
information: "Описание"
|
||||
@@ -2395,6 +2397,8 @@ _imageEffector:
|
||||
opacity: "Непрозрачность"
|
||||
lightness: "Осветление"
|
||||
drafts: "Черновик"
|
||||
_drafts:
|
||||
restore: "Восстановить"
|
||||
_qr:
|
||||
showTabTitle: "Отображение"
|
||||
raw: "Текст"
|
||||
|
||||
@@ -1335,7 +1335,7 @@ markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อ
|
||||
unmarkAsSensitiveConfirm: "ต้องการยกเลิกการระบุว่าสื่อนี้มีเนื้อหาละเอียดอ่อนหรือไม่?"
|
||||
preferences: "การตั้งค่าสภาพแวดล้อม"
|
||||
accessibility: "การช่วยการเข้าถึง"
|
||||
preferencesProfile: "โปรไฟล์การกำหนดค่า"
|
||||
preferencesProfile: "โปรไฟล์ของการตั้งค่า"
|
||||
copyPreferenceId: "คัดลือก ID การตั้งค่า"
|
||||
resetToDefaultValue: "คืนค่าเป็นค่าเริ่มต้น"
|
||||
overrideByAccount: "เขียนทับด้วยบัญชี"
|
||||
@@ -1345,10 +1345,10 @@ skip: "ข้าม"
|
||||
restore: "กู้คืน"
|
||||
syncBetweenDevices: "ซิงค์ระหว่างอุปกรณ์"
|
||||
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
|
||||
preferenceSyncConflictText: "การตั้งค่าที่เปิดใช้งานการซิงค์จะบันทึกค่าลงในเซิร์ฟเวอร์ อย่างไรก็ดี พบว่ามีค่าการตั้งค่านี้ที่เคยบันทึกไว้ในเซิร์ฟเวอร์แล้ว ต้องการดำเนินการอย่างไร?"
|
||||
preferenceSyncConflictText: "รายการตั้งค่าที่เปิดการซิงก์จะถูกบันทึกลงเซิร์ฟเวอร์ แต่รายการตั้งค่านี้ได้ถูกบันทึกลงเซิร์ฟเวอร์ไว้อยู่แล้ว ต้องการดำเนินการอย่างไร?"
|
||||
preferenceSyncConflictChoiceMerge: "รวมเข้าด้วยกัน"
|
||||
preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าเซิร์ฟเวอร์"
|
||||
preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าอุปกรณ์"
|
||||
preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าของเซิร์ฟเวอร์"
|
||||
preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าของอุปกรณ์"
|
||||
preferenceSyncConflictChoiceCancel: "ยกเลิกการเปิดใช้งานการซิงค์"
|
||||
paste: "วาง"
|
||||
emojiPalette: "จานสีเอโมจิ"
|
||||
@@ -1408,6 +1408,7 @@ frame: "เฟรม"
|
||||
presets: "พรีเซ็ต"
|
||||
zeroPadding: "ห่างเป็น 0"
|
||||
nothingToConfigure: "ไม่มีอะไรให้ต้ังค่า"
|
||||
viewRenotedChannel: "แสดงช่องที่ถูกรีโน้ต"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "แคปชั่นของไฟล์"
|
||||
@@ -2097,6 +2098,7 @@ _role:
|
||||
canSearchNotes: "การใช้การค้นหาโน้ต"
|
||||
canSearchUsers: "ค้นหาผู้ใช้"
|
||||
canUseTranslator: "การใช้งานแปล"
|
||||
canCreateChannel: "สร้างช่องใหม่"
|
||||
avatarDecorationLimit: "จำนวนของตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้"
|
||||
canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ"
|
||||
canImportBlocking: "อนุญาตให้นำเข้าการบล็อก"
|
||||
@@ -2136,7 +2138,7 @@ _sensitiveMediaDetection:
|
||||
sensitivityDescription: "เมื่อความไวต่ำ Misdetection (ผลบวกลวง) จะลดลง, เมื่อความไวสูง Missed detection (ผลลบลวง) จะลดลง"
|
||||
setSensitiveFlagAutomatically: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน"
|
||||
setSensitiveFlagAutomaticallyDescription: "ผลลัพธ์ของการตรวจจับภายในนั้นจะยังคงอยู่ ถึงแม้ว่าจะปิดตัวเลือกนี้"
|
||||
analyzeVideos: "เปิดใช้งานวิเคราะห์ของวิดีโอ"
|
||||
analyzeVideos: "เปิดใช้งานวิเคราะห์วิดีโอ"
|
||||
analyzeVideosDescription: "การวิเคราะห์วิดีโอนอกเหนือจากรูปภาพนั้น การทำสิ่งนี้จะทำให้เพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
||||
_emailUnavailable:
|
||||
used: "ที่อยู่อีเมลนี้ได้ถูกใช้ไปแล้ว"
|
||||
@@ -2586,7 +2588,7 @@ _widgetOptions:
|
||||
period: "ระยะเวลา"
|
||||
_cw:
|
||||
hide: "ซ่อน"
|
||||
show: "โหลดเพิ่มเติม"
|
||||
show: "ดูเพิ่มเติม"
|
||||
chars: "{count} ตัวอักษร"
|
||||
files: "{count} ไฟล์"
|
||||
_poll:
|
||||
@@ -3029,7 +3031,7 @@ _externalResourceInstaller:
|
||||
description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
|
||||
_dataSaver:
|
||||
_media:
|
||||
title: "โหลดสื่อ"
|
||||
title: "ปิดใช้งานการโหลดสื่ออัตโนมัติ"
|
||||
description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
|
||||
_avatar:
|
||||
title: "ปิดใช้งานภาพเคลื่อนไหวของไอคอนประจำตัว"
|
||||
@@ -3111,7 +3113,7 @@ _urlPreviewSetting:
|
||||
summaryProxyDescription: "สร้างการแสดงตัวอย่างด้วย summary Proxy แทนที่จะใช้เนื้อหา Misskey"
|
||||
summaryProxyDescription2: "พารามิเตอร์ต่อไปนี้จะถูกใช้เป็นสตริงการสืบค้นเพื่อเชื่อมต่อกับพร็อกซี หากฝั่งพร็อกซีไม่รองรับการตั้งค่าเหล่านี้จะถูกละเว้น"
|
||||
_mediaControls:
|
||||
pip: "รูปภาพในรูปภาม"
|
||||
pip: "ภาพซ้อนภาพ (PiP)"
|
||||
playbackRate: "ความเร็วในการเล่น"
|
||||
loop: "เล่นวนซ้ำ"
|
||||
_contextMenu:
|
||||
|
||||
@@ -1408,6 +1408,7 @@ frame: "Çerçeve"
|
||||
presets: "Ön ayar"
|
||||
zeroPadding: "Sıfır doldurma"
|
||||
nothingToConfigure: "Ayarlar seçeneği bulunmamaktadır."
|
||||
viewRenotedChannel: "Show renoted channel"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Dosya başlığı"
|
||||
|
||||
@@ -63,7 +63,7 @@ copyUserId: "复制用户 ID"
|
||||
copyNoteId: "复制帖子 ID"
|
||||
copyFileId: "复制文件ID"
|
||||
copyFolderId: "复制文件夹ID"
|
||||
copyProfileUrl: "复制个人资料URL"
|
||||
copyProfileUrl: "复制个人资料链接"
|
||||
searchUser: "搜索用户"
|
||||
searchThisUsersNotes: "搜索用户帖子"
|
||||
reply: "回复"
|
||||
@@ -101,12 +101,12 @@ somethingHappened: "出错了"
|
||||
retry: "重试"
|
||||
pageLoadError: "页面加载失败。"
|
||||
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
||||
serverIsDead: "没有服务器响应。 请稍后再试。"
|
||||
serverIsDead: "服务器未响应。 请稍后再试。"
|
||||
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
||||
enterListName: "输入列表名称"
|
||||
privacy: "隐私"
|
||||
makeFollowManuallyApprove: "关注请求需要批准"
|
||||
defaultNoteVisibility: "默认可见性"
|
||||
defaultNoteVisibility: "默认可见范围"
|
||||
follow: "关注"
|
||||
followRequest: "申请关注"
|
||||
followRequests: "关注请求"
|
||||
@@ -137,10 +137,10 @@ pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显
|
||||
pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号"
|
||||
emojiPickerDisplay: "选择器显示设置"
|
||||
overwriteFromPinnedEmojisForReaction: "使用「置顶(回应)」设置覆盖"
|
||||
overwriteFromPinnedEmojis: "从全局设置覆盖"
|
||||
overwriteFromPinnedEmojis: "使用全局设置覆盖"
|
||||
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
||||
rememberNoteVisibility: "保存上次设置的可见性"
|
||||
attachCancel: "取消添加附件"
|
||||
attachCancel: "移除附件"
|
||||
deleteFile: "删除文件"
|
||||
markAsSensitive: "标记为敏感内容"
|
||||
unmarkAsSensitive: "取消标记为敏感内容"
|
||||
@@ -149,12 +149,12 @@ mute: "屏蔽"
|
||||
unmute: "取消隐藏"
|
||||
renoteMute: "隐藏转帖"
|
||||
renoteUnmute: "取消隐藏转帖"
|
||||
block: "屏蔽"
|
||||
unblock: "取消屏蔽"
|
||||
block: "禁止对方与我互动"
|
||||
unblock: "允许对方与我互动"
|
||||
suspend: "冻结"
|
||||
unsuspend: "解除冻结"
|
||||
blockConfirm: "确定要屏蔽吗?"
|
||||
unblockConfirm: "确定要取消屏蔽吗?"
|
||||
blockConfirm: "确定要禁止对方与我互动吗?"
|
||||
unblockConfirm: "确定要允许对方与我互动吗?"
|
||||
suspendConfirm: "要冻结吗?"
|
||||
unsuspendConfirm: "要解除冻结吗?"
|
||||
selectList: "选择列表"
|
||||
@@ -184,7 +184,7 @@ flagAsCat: "喵!!!!!!!!!!!!"
|
||||
flagAsCatDescription: "喵喵喵??"
|
||||
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||
autoAcceptFollowed: "自动允许回关请求"
|
||||
autoAcceptFollowed: "自动允许我关注的人的关注请求"
|
||||
addAccount: "添加账户"
|
||||
reloadAccountsList: "更新账户列表"
|
||||
loginFailed: "登录失败"
|
||||
@@ -207,7 +207,7 @@ selectSelf: "选择自己"
|
||||
selectUser: "选择用户"
|
||||
recipient: "收件人"
|
||||
annotation: "注解"
|
||||
federation: "联合"
|
||||
federation: "联邦"
|
||||
instances: "服务器"
|
||||
registeredAt: "初次观测"
|
||||
latestRequestReceivedAt: "上次收到的请求"
|
||||
@@ -244,11 +244,11 @@ silencedInstances: "被静音的服务器"
|
||||
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。已被屏蔽的实例不受影响。"
|
||||
mediaSilencedInstances: "已隐藏媒体文件的服务器"
|
||||
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。已被屏蔽的实例不受影响。"
|
||||
federationAllowedHosts: "允许联合的服务器"
|
||||
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
|
||||
muteAndBlock: "隐藏/屏蔽"
|
||||
mutedUsers: "已隐藏的用户"
|
||||
blockedUsers: "已屏蔽的用户"
|
||||
federationAllowedHosts: "允许联邦交互的服务器"
|
||||
federationAllowedHostsDescription: "设定允许联邦通信的服务器,以换行分隔。"
|
||||
muteAndBlock: "屏蔽用户/禁止用户与我互动"
|
||||
mutedUsers: "已屏蔽的用户"
|
||||
blockedUsers: "禁止与我互动的用户"
|
||||
noUsers: "无用户"
|
||||
editProfile: "编辑资料"
|
||||
noteDeleteConfirm: "确定要删除该帖子吗?"
|
||||
@@ -261,7 +261,7 @@ default: "默认"
|
||||
defaultValueIs: "默认值: {value}"
|
||||
noCustomEmojis: "没有自定义表情符号"
|
||||
noJobs: "没有任务"
|
||||
federating: "联合中"
|
||||
federating: "联邦通信中"
|
||||
blocked: "已屏蔽"
|
||||
suspended: "停止投递"
|
||||
all: "全部"
|
||||
@@ -277,7 +277,7 @@ retypedNotMatch: "两次输入不一致!"
|
||||
currentPassword: "现在的密码"
|
||||
newPassword: "新密码"
|
||||
newPasswordRetype: "重新输入密码:"
|
||||
attachFile: "插入附件"
|
||||
attachFile: "添加附件"
|
||||
more: "更多!"
|
||||
featured: "热门"
|
||||
usernameOrUserId: "用户名或用户 ID"
|
||||
@@ -342,7 +342,7 @@ selectFolders: "选择多个文件夹"
|
||||
fileNotSelected: "未选择文件"
|
||||
renameFile: "重命名文件"
|
||||
folderName: "文件夹名称"
|
||||
createFolder: "创建文件夹"
|
||||
createFolder: "新建文件夹"
|
||||
renameFolder: "重命名文件夹"
|
||||
deleteFolder: "删除文件夹"
|
||||
folder: "文件夹"
|
||||
@@ -353,7 +353,7 @@ emptyFolder: "此文件夹中无文件"
|
||||
dropHereToUpload: "将文件拖动到这里来上传"
|
||||
unableToDelete: "无法删除"
|
||||
inputNewFileName: "请输入新文件名"
|
||||
inputNewDescription: "请输入新标题"
|
||||
inputNewDescription: "请输入新的描述文本"
|
||||
inputNewFolderName: "请输入新文件夹名"
|
||||
circularReferenceFolder: "目标文件夹是要移动的文件夹的子文件夹。"
|
||||
hasChildFilesOrFolders: "此文件夹中有文件,无法删除。"
|
||||
@@ -396,10 +396,10 @@ driveCapacityPerLocalAccount: "每个用户的网盘容量"
|
||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||
inMb: "以兆字节(MegaByte)为单位"
|
||||
bannerUrl: "横幅 URL"
|
||||
backgroundImageUrl: "背景图 URL"
|
||||
backgroundImageUrl: "背景图片的链接"
|
||||
basicInfo: "基本信息"
|
||||
pinnedUsers: "置顶用户"
|
||||
pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。"
|
||||
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
|
||||
pinnedPages: "固定页面"
|
||||
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
|
||||
pinnedClipId: "置顶的便签 ID"
|
||||
@@ -432,7 +432,7 @@ antennaExcludeBots: "排除机器人账户"
|
||||
antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||
notifyAntenna: "开启通知"
|
||||
withFileAntenna: "仅带有附件的帖子"
|
||||
excludeNotesInSensitiveChannel: "排除敏感频道内的帖子"
|
||||
excludeNotesInSensitiveChannel: "排除敏感频道的帖子"
|
||||
enableServiceworker: "启用 ServiceWorker"
|
||||
antennaUsersDescription: "指定用户名,用换行符进行分隔"
|
||||
caseSensitive: "区分大小写"
|
||||
@@ -476,7 +476,7 @@ passwordLessLogin: "无密码登录"
|
||||
passwordLessLoginDescription: "不使用密码,仅使用安全密钥或 Passkey 登录"
|
||||
resetPassword: "重置密码"
|
||||
newPasswordIs: "新的密码是「{password}」"
|
||||
reduceUiAnimation: "减少 UI 动画"
|
||||
reduceUiAnimation: "减少 UI 动效"
|
||||
share: "分享"
|
||||
notFound: "未找到"
|
||||
notFoundDescription: "没有与指定 URL 对应的页面。"
|
||||
@@ -543,7 +543,7 @@ regenerate: "重新生成"
|
||||
fontSize: "字体大小"
|
||||
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
|
||||
limitTo: "上限为 {x}"
|
||||
showMediaListByGridInWideArea: "在大屏幕上并排显示媒体列表"
|
||||
showMediaListByGridInWideArea: "在宽屏上并排显示媒体列表"
|
||||
noFollowRequests: "没有关注请求"
|
||||
openImageInNewTab: "在新标签页中打开图片"
|
||||
dashboard: "管理面板"
|
||||
@@ -581,7 +581,7 @@ s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定
|
||||
serverLogs: "服务器日志"
|
||||
deleteAll: "全部删除"
|
||||
showFixedPostForm: "在时间线顶部显示发帖框"
|
||||
showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)"
|
||||
showFixedPostFormInChannel: "在时间线顶部显示发帖框(频道)"
|
||||
withRepliesByDefaultForNewlyFollowed: "在时间线中默认包含新关注用户的回复"
|
||||
newNoteRecived: "有新的帖子"
|
||||
newNote: "新帖子"
|
||||
@@ -656,7 +656,7 @@ expandTweet: "展开帖子"
|
||||
themeEditor: "主题编辑器"
|
||||
description: "描述"
|
||||
describeFile: "添加描述"
|
||||
enterFileDescription: "输入标题"
|
||||
enterFileDescription: "输入描述文本"
|
||||
author: "作者"
|
||||
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
|
||||
manage: "管理"
|
||||
@@ -664,7 +664,7 @@ plugins: "插件"
|
||||
preferencesBackups: "备份设置"
|
||||
deck: "Deck"
|
||||
undeck: "取消 Deck"
|
||||
useBlurEffectForModal: "对话框使用模糊效果"
|
||||
useBlurEffectForModal: "发帖背景使用模糊效果"
|
||||
useFullReactionPicker: "使用全功能的回应工具栏"
|
||||
width: "宽度"
|
||||
height: "高度"
|
||||
@@ -773,13 +773,13 @@ yes: "是"
|
||||
no: "否"
|
||||
driveFilesCount: "网盘的文件数"
|
||||
driveUsage: "网盘的空间用量"
|
||||
noCrawle: "要求搜索引擎不索引该用户"
|
||||
noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。"
|
||||
noCrawle: "拒绝搜索引擎的索引"
|
||||
noCrawleDescription: "拒绝搜索引擎收录(索引)您的个人资料,帖子,页面等。"
|
||||
lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅关注者」,任何人都可以看到您的帖子。"
|
||||
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
|
||||
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
|
||||
disableShowingAnimatedImages: "不播放动画"
|
||||
disableShowingAnimatedImages_caption: "如果即使关闭了此设置但动画仍无法播放,则可能是浏览器或操作系统的辅助功能设置,又或者是省电设置等产生了干扰。"
|
||||
disableShowingAnimatedImages: "不播放动态图像"
|
||||
disableShowingAnimatedImages_caption: "如果即使禁用了此设置,动态图像仍无法播放,可能是由于浏览器或操作系统的辅助功能设置、省电设置或其他因素所致。"
|
||||
highlightSensitiveMedia: "高亮显示敏感媒体"
|
||||
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
|
||||
notSet: "未设置"
|
||||
@@ -851,7 +851,7 @@ fullView: "全屏"
|
||||
quitFullView: "退出全屏"
|
||||
addDescription: "添加描述"
|
||||
userPagePinTip: "在帖子的菜单中选择“置顶”,即可显示该条帖子。"
|
||||
notSpecifiedMentionWarning: "有未指定的提及"
|
||||
notSpecifiedMentionWarning: "有未添加到收件人的提及"
|
||||
info: "关于"
|
||||
userInfo: "用户信息"
|
||||
unknown: "未知"
|
||||
@@ -877,9 +877,9 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
|
||||
noInquiryUrlWarning: "尚未设置联络地址。"
|
||||
noBotProtectionWarning: "尚未设置 Bot 防御。"
|
||||
configure: "设置"
|
||||
postToGallery: "创建新图集"
|
||||
postToGallery: "发表相册"
|
||||
postToHashtag: "发布至该话题"
|
||||
gallery: "图集"
|
||||
gallery: "相册"
|
||||
recentPosts: "最新发布"
|
||||
popularPosts: "热门投稿"
|
||||
shareWithNote: "分享到帖文"
|
||||
@@ -1032,7 +1032,7 @@ browserPushNotificationDisabledDescription: "{serverName}无权限发送通知
|
||||
windowMaximize: "最大化"
|
||||
windowMinimize: "最小化"
|
||||
windowRestore: "还原"
|
||||
caption: "标题"
|
||||
caption: "描述文本"
|
||||
loggedInAsBot: "以 Bot 账户登录"
|
||||
tools: "工具"
|
||||
cannotLoad: "无法加载"
|
||||
@@ -1073,7 +1073,7 @@ thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。"
|
||||
thisPostMayBeAnnoyingHome: "发到首页"
|
||||
thisPostMayBeAnnoyingCancel: "取消"
|
||||
thisPostMayBeAnnoyingIgnore: "就这样发布"
|
||||
collapseRenotes: "省略显示已经看过的转发内容"
|
||||
collapseRenotes: "折叠已经看过的转贴"
|
||||
collapseRenotesDescription: "折叠显示回应或转发过的帖文。"
|
||||
internalServerError: "内部服务器错误"
|
||||
internalServerErrorDescription: "内部服务器发生了预期外的错误"
|
||||
@@ -1081,9 +1081,9 @@ copyErrorInfo: "复制错误信息"
|
||||
joinThisServer: "在本服务器上注册"
|
||||
exploreOtherServers: "探索其他服务器"
|
||||
letsLookAtTimeline: "看看时间线"
|
||||
disableFederationConfirm: "确定要禁用联合?"
|
||||
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
|
||||
disableFederationOk: "联合禁用"
|
||||
disableFederationConfirm: "确定要禁用联邦交互?"
|
||||
disableFederationConfirmWarn: "即使禁用联邦交互,也不会将帖子设为私有。在大多数情况下,没有必要禁用联邦交互。"
|
||||
disableFederationOk: "禁用联邦"
|
||||
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
|
||||
emailNotSupported: "此服务器不支持发送邮件"
|
||||
postToTheChannel: "发布到频道"
|
||||
@@ -1093,7 +1093,7 @@ likeOnly: "仅点赞"
|
||||
likeOnlyForRemote: "全部(远程仅点赞)"
|
||||
nonSensitiveOnly: "仅限非敏感内容"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
|
||||
rolesAssignedToMe: "指派给自己的角色"
|
||||
rolesAssignedToMe: "我的角色"
|
||||
resetPasswordConfirm: "确定重置密码?"
|
||||
sensitiveWords: "敏感词"
|
||||
sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。"
|
||||
@@ -1148,7 +1148,7 @@ pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。"
|
||||
continue: "继续"
|
||||
preservedUsernames: "保留的用户名"
|
||||
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
|
||||
createNoteFromTheFile: "从文件创建帖子"
|
||||
createNoteFromTheFile: "使用该文件发帖"
|
||||
archive: "归档"
|
||||
archived: "已归档"
|
||||
unarchive: "取消归档"
|
||||
@@ -1158,7 +1158,7 @@ thisChannelArchived: "该频道已被归档。"
|
||||
displayOfNote: "显示帖子"
|
||||
initialAccountSetting: "初始设定"
|
||||
youFollowing: "正在关注"
|
||||
preventAiLearning: "拒绝接受生成式 AI 的学习"
|
||||
preventAiLearning: "拒绝用于训练生成式 AI"
|
||||
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
|
||||
options: "选项"
|
||||
specifyUser: "指定用户"
|
||||
@@ -1226,8 +1226,8 @@ notificationRecieveConfig: "通知接收设置"
|
||||
mutualFollow: "互相关注"
|
||||
followingOrFollower: "关注中或关注者"
|
||||
fileAttachedOnly: "仅限媒体"
|
||||
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
|
||||
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
|
||||
showRepliesToOthersInTimeline: "在时间线中显示对他人的回复"
|
||||
hideRepliesToOthersInTimeline: "在时间线中隐藏对他人的回复"
|
||||
showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
|
||||
hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
|
||||
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
|
||||
@@ -1325,22 +1325,22 @@ lockdown: "锁定"
|
||||
pleaseSelectAccount: "请选择帐户"
|
||||
availableRoles: "可用角色"
|
||||
acknowledgeNotesAndEnable: "理解注意事项后再开启。"
|
||||
federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。"
|
||||
federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
|
||||
federationSpecified: "此服务器已开启联邦白名单模式。只能与管理员指定的服务器通信。"
|
||||
federationDisabled: "此服务器已禁用联邦功能。无法与其它服务器上的用户通信。"
|
||||
draft: "草稿"
|
||||
draftsAndScheduledNotes: "草稿和定时发送"
|
||||
confirmOnReact: "发送回应前需要确认"
|
||||
reactAreYouSure: "要用「{emoji}」进行回应吗?"
|
||||
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
|
||||
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
|
||||
markAsSensitiveConfirm: "确定标记此媒体为敏感内容吗?"
|
||||
unmarkAsSensitiveConfirm: "确定取消标记为敏感内容吗?"
|
||||
preferences: "偏好设置"
|
||||
accessibility: "辅助功能"
|
||||
preferencesProfile: "设置的配置文件"
|
||||
copyPreferenceId: "复制设置 ID"
|
||||
resetToDefaultValue: "重置为默认值"
|
||||
overrideByAccount: "覆盖账号"
|
||||
overrideByAccount: "使用账户设置"
|
||||
untitled: "未命名"
|
||||
noName: "没有名字"
|
||||
noName: "未命名"
|
||||
skip: "跳过"
|
||||
restore: "恢复"
|
||||
syncBetweenDevices: "设备间同步"
|
||||
@@ -1351,7 +1351,7 @@ preferenceSyncConflictChoiceServer: "服务器上的设定值"
|
||||
preferenceSyncConflictChoiceDevice: "设备上的设定值"
|
||||
preferenceSyncConflictChoiceCancel: "取消同步"
|
||||
paste: "粘贴"
|
||||
emojiPalette: "表情符号调色板"
|
||||
emojiPalette: "表情符号选择器"
|
||||
postForm: "发帖窗口"
|
||||
textCount: "字数"
|
||||
information: "关于"
|
||||
@@ -1368,7 +1368,7 @@ embed: "嵌入"
|
||||
settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)"
|
||||
readonly: "只读"
|
||||
goToDeck: "返回至 Deck"
|
||||
federationJobs: "联合作业"
|
||||
federationJobs: "联邦作业"
|
||||
driveAboutTip: "网盘可以显示以前上传的文件。<br>\n也可以在发布帖子时重复使用文件,或在发布帖子前预先上传文件。<br>\n<b>删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。</b><br>\n也可以新建文件夹来整理文件。"
|
||||
scrollToClose: "滑动并关闭"
|
||||
advice: "建议"
|
||||
@@ -1382,7 +1382,7 @@ unmuteX: "取消对{x}的隐藏"
|
||||
abort: "中止"
|
||||
tip: "提示和技巧"
|
||||
redisplayAllTips: "重新显示所有的提示和技巧"
|
||||
hideAllTips: "隐藏所有的提示和技巧"
|
||||
hideAllTips: "隐藏所有的 “提示与技巧”"
|
||||
defaultImageCompressionLevel: "默认图像压缩等级"
|
||||
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
|
||||
defaultCompressionLevel: "默认压缩等级"
|
||||
@@ -1394,7 +1394,7 @@ pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均
|
||||
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
|
||||
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
|
||||
thankYouForTestingBeta: "感谢您协助测试 beta 版!"
|
||||
createUserSpecifiedNote: "创建指定用户的帖子"
|
||||
createUserSpecifiedNote: "提及该用户并发帖"
|
||||
schedulePost: "定时发布"
|
||||
scheduleToPostOnX: "预定在 {x} 发出"
|
||||
scheduledToPostOnX: "已预定在 {x} 发出"
|
||||
@@ -1511,10 +1511,10 @@ _chat:
|
||||
mutual: "仅相互关注"
|
||||
none: "没有人"
|
||||
_emojiPalette:
|
||||
palettes: "调色板"
|
||||
enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
|
||||
paletteForMain: "主调色板"
|
||||
paletteForReaction: "回应用调色板"
|
||||
palettes: "表情符号托盘"
|
||||
enableSyncBetweenDevicesForPalettes: "在设备间同步表情符号托盘"
|
||||
paletteForMain: "主表情符号托盘"
|
||||
paletteForReaction: "回应时的表情符号托盘"
|
||||
_settings:
|
||||
driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。"
|
||||
pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。"
|
||||
@@ -1535,9 +1535,9 @@ _settings:
|
||||
timelineAndNote: "时间线和帖子"
|
||||
makeEveryTextElementsSelectable: "使所有的文字均可选择"
|
||||
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
|
||||
useStickyIcons: "使图标跟随滚动"
|
||||
useStickyIcons: "用户头像跟随页面滚动"
|
||||
enableHighQualityImagePlaceholders: "显示高质量图像的占位符"
|
||||
uiAnimations: "UI 动画"
|
||||
uiAnimations: "UI 动效"
|
||||
showNavbarSubButtons: "在导航栏中显示副按钮"
|
||||
ifOn: "启用时"
|
||||
ifOff: "关闭时"
|
||||
@@ -1552,7 +1552,7 @@ _settings:
|
||||
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
|
||||
showPageTabBarBottom: "在下方显示页面标签栏"
|
||||
emojiPaletteBanner: "可以将固定显示在表情符号选择器中的预设注册为调色板,也可以自定义表情符号选择器的显示方式。"
|
||||
enableAnimatedImages: "启用动画图像"
|
||||
enableAnimatedImages: "启用动态图像"
|
||||
settingsPersistence_title: "设置持久化"
|
||||
settingsPersistence_description1: "启用设置持久化可防止设置信息丢失。"
|
||||
settingsPersistence_description2: "根据环境不同,有可能无法开启。"
|
||||
@@ -1580,12 +1580,12 @@ _accountSettings:
|
||||
requireSigninToViewContents: "需要登录才能显示内容"
|
||||
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
|
||||
requireSigninToViewContentsDescription2: "没有 URL 预览(OGP)、内嵌网页、引用帖子的功能的服务器也将无法显示。"
|
||||
requireSigninToViewContentsDescription3: "这些限制可能不适用于联合到远程服务器的内容。"
|
||||
requireSigninToViewContentsDescription3: "对于已通过联邦分发到远程服务器的内容,这些限制可能不适用。"
|
||||
makeNotesFollowersOnlyBefore: "可将过去的帖子设为仅关注者可见"
|
||||
makeNotesFollowersOnlyBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅关注者可见。关闭后帖子的公开状态将恢复成原本的设定。"
|
||||
makeNotesHiddenBefore: "将过去的帖子设为私密"
|
||||
makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。"
|
||||
mayNotEffectForFederatedNotes: "与远程服务器联合的帖子在远端可能会没有效果。"
|
||||
mayNotEffectForFederatedNotes: "对于已通过联邦投递到远程服务器的帖子,此操作在远端可能无法生效。"
|
||||
mayNotEffectSomeSituations: "此限制功能非常简单,在与远程服务器联合等情形时可能不适用。"
|
||||
notesHavePassedSpecifiedPeriod: "超过指定时间的帖子"
|
||||
notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子"
|
||||
@@ -1637,7 +1637,7 @@ _announcement:
|
||||
_initialAccountSetting:
|
||||
accountCreated: "账户创建完成了!"
|
||||
letsStartAccountSetup: "马上来进行账户的初始设定吧。"
|
||||
letsFillYourProfile: "首先,来设定你的个人档案吧!"
|
||||
letsFillYourProfile: "首先,设置一下您的个人资料吧!"
|
||||
profileSetting: "个人资料设置"
|
||||
privacySetting: "隐私设置"
|
||||
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
|
||||
@@ -1689,10 +1689,10 @@ _initialTutorial:
|
||||
public: "向所有用户公开。\n"
|
||||
home: "仅在首页时间线上发布。 关注者、从个人资料页查看过来的用户、以及通过转帖也能被别的用户看见。"
|
||||
followers: "仅对关注者可见。 除了您自己之外,没有人可以转贴,并且只有您的关注者可以查看它。\n"
|
||||
direct: "它将仅向指定用户公开,并且他们也会收到通知。 您可以使用它来代替私信。\n"
|
||||
direct: "仅对指定用户公开,且收件人将收到通知。"
|
||||
doNotSendConfidencialOnDirect1: "发送敏感信息时请注意。\n"
|
||||
doNotSendConfidencialOnDirect2: "目标服务器的管理员可以看到发布的内容,因此如果您向不受信任的服务器上的用户发送私信,则在处理敏感信息时需要小心。"
|
||||
localOnly: "不将帖子推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n"
|
||||
localOnly: "不将帖子通过联邦推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n"
|
||||
_cw:
|
||||
title: "隐藏内容 (CW)\n"
|
||||
description: "显示「注解」里的内容而不是正文。点击「查看更多」将会把正文显示出来。"
|
||||
@@ -1701,7 +1701,7 @@ _initialTutorial:
|
||||
note: "茨了带巧克力的甜甜圈🍩😋"
|
||||
useCases: "用于服务器条款所规定的帖子,或对剧透内容和敏感内容进行自主规制。"
|
||||
_howToMakeAttachmentsSensitive:
|
||||
title: "如何将附件标注为敏感内容?"
|
||||
title: "如何标记附件为敏感内容?"
|
||||
description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n"
|
||||
tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!"
|
||||
_exampleNote:
|
||||
@@ -1746,7 +1746,7 @@ _serverSettings:
|
||||
singleUserMode: "单用户模式"
|
||||
singleUserMode_description: "若此服务器只有自己使用,开启此模式将最佳化性能。"
|
||||
signToActivityPubGet: "对 GET 请求签名"
|
||||
signToActivityPubGet_description: "通常情况下请保持启用。若遇到联合通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。"
|
||||
signToActivityPubGet_description: "通常情况下请保持启用。若遇到联邦通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。"
|
||||
proxyRemoteFiles: "代理远程文件"
|
||||
proxyRemoteFiles_description: "如果启用,远程服务器的文件将由代理提供。可有效保护图像预览缩略图的生成与用户隐私。"
|
||||
allowExternalApRedirect: "允许通过 ActivityPub 重定向查询"
|
||||
@@ -1771,7 +1771,7 @@ _accountMigration:
|
||||
moveTo: "把这个账户迁移到新的账户"
|
||||
moveToLabel: "迁移后的账户"
|
||||
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
|
||||
moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n列表、隐藏、屏蔽也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)"
|
||||
moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n列表、屏蔽列表、禁止与我互动的列表也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)"
|
||||
moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com"
|
||||
startMigration: "迁移"
|
||||
migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。"
|
||||
@@ -1889,7 +1889,7 @@ _achievements:
|
||||
description: "第一次将帖子加入收藏"
|
||||
_myNoteFavorited1:
|
||||
title: "想要星星"
|
||||
description: "自己的帖子被其他人加入收藏了"
|
||||
description: "自己的帖子被其他人收藏了"
|
||||
_profileFilled:
|
||||
title: "整装待发"
|
||||
description: "设置了个人资料"
|
||||
@@ -2083,7 +2083,7 @@ _role:
|
||||
maxFileSize: "可上传的最大文件大小"
|
||||
maxFileSize_caption: "可能在反向代理或 CDN 等前端存在其它设定值。"
|
||||
alwaysMarkNsfw: "总是将文件标记为 NSFW"
|
||||
canUpdateBioMedia: "可以更新头像和横幅"
|
||||
canUpdateBioMedia: "允许更新头像和横幅"
|
||||
pinMax: "帖子置顶数量限制"
|
||||
antennaMax: "可创建的最大天线数量"
|
||||
wordMuteMax: "折叠词的字数限制"
|
||||
@@ -2098,9 +2098,10 @@ _role:
|
||||
canSearchNotes: "是否可以搜索帖子"
|
||||
canSearchUsers: "使用用户检索"
|
||||
canUseTranslator: "使用翻译功能"
|
||||
canCreateChannel: "创建频道"
|
||||
avatarDecorationLimit: "可添加头像挂件的最大个数"
|
||||
canImportAntennas: "允许导入天线"
|
||||
canImportBlocking: "允许导入屏蔽列表"
|
||||
canImportBlocking: "允许导入禁止与我互动的列表"
|
||||
canImportFollowing: "允许导入关注列表"
|
||||
canImportMuting: "允许导入隐藏列表"
|
||||
canImportUserLists: "允许导入用户列表"
|
||||
@@ -2108,7 +2109,7 @@ _role:
|
||||
uploadableFileTypes: "可上传的文件类型"
|
||||
uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)"
|
||||
uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。"
|
||||
noteDraftLimit: "可在服务器上创建多少草稿"
|
||||
noteDraftLimit: "可在服务器上创建的草稿数量"
|
||||
scheduledNoteLimit: "可同时创建的定时帖子数量"
|
||||
watermarkAvailable: "能否使用水印功能"
|
||||
_condition:
|
||||
@@ -2165,7 +2166,7 @@ _ad:
|
||||
back: "返回"
|
||||
reduceFrequencyOfThisAd: "减少此广告的频率"
|
||||
hide: "不显示"
|
||||
timezoneinfo: "星期几是由服务器的时区所指定的。"
|
||||
timezoneinfo: "星期几是根据服务器的时区确定的。"
|
||||
adsSettings: "广告设置"
|
||||
notesPerOneAd: "在实时更新时间线中插入广告的间隔(帖子个数)"
|
||||
setZeroToDisable: "设为 0 将不在实时更新时间线中投放广告"
|
||||
@@ -2229,7 +2230,7 @@ _aboutMisskey:
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "隐藏敏感媒体"
|
||||
ignore: "显示敏感媒体"
|
||||
force: "隐藏所有内容"
|
||||
force: "隐藏所有媒体"
|
||||
_instanceTicker:
|
||||
none: "不显示"
|
||||
remote: "仅远程用户"
|
||||
@@ -2253,7 +2254,7 @@ _channel:
|
||||
allowRenoteToExternal: "允许转发到频道外和引用"
|
||||
_menuDisplay:
|
||||
sideFull: "横向"
|
||||
sideIcon: "横向(图标)"
|
||||
sideIcon: "横向(图标)"
|
||||
top: "顶部"
|
||||
hide: "隐藏"
|
||||
_wordMute:
|
||||
@@ -2315,7 +2316,7 @@ _theme:
|
||||
mention: "提及"
|
||||
mentionMe: "提及"
|
||||
renote: "转发"
|
||||
modalBg: "对话框背景"
|
||||
modalBg: "发帖背景"
|
||||
divider: "分割线"
|
||||
scrollbarHandle: "滚动条"
|
||||
scrollbarHandleHover: "滚动条(悬停)"
|
||||
@@ -2334,7 +2335,7 @@ _theme:
|
||||
fgHighlighted: "高亮显示文本"
|
||||
_sfx:
|
||||
note: "帖子"
|
||||
noteMy: "我的帖子"
|
||||
noteMy: "发帖"
|
||||
notification: "通知"
|
||||
reaction: "选择回应时"
|
||||
chatMessage: "私信"
|
||||
@@ -2403,8 +2404,8 @@ _2fa:
|
||||
_permissions:
|
||||
"read:account": "查看账户信息"
|
||||
"write:account": "更改帐户信息"
|
||||
"read:blocks": "查看屏蔽列表"
|
||||
"write:blocks": "编辑屏蔽列表"
|
||||
"read:blocks": "查看禁止与我互动的列表"
|
||||
"write:blocks": "编辑禁止与我互动的列表"
|
||||
"read:drive": "查看网盘"
|
||||
"write:drive": "管理网盘文件"
|
||||
"read:favorites": "查看收藏夹"
|
||||
@@ -2429,10 +2430,10 @@ _permissions:
|
||||
"write:user-groups": "编辑用户组"
|
||||
"read:channels": "查看频道"
|
||||
"write:channels": "管理频道"
|
||||
"read:gallery": "浏览图集"
|
||||
"write:gallery": "编辑图集"
|
||||
"read:gallery-likes": "浏览喜欢的图集"
|
||||
"write:gallery-likes": "管理喜欢的图集"
|
||||
"read:gallery": "浏览相册"
|
||||
"write:gallery": "管理相册"
|
||||
"read:gallery-likes": "浏览喜欢的相册"
|
||||
"write:gallery-likes": "管理喜欢的相册"
|
||||
"read:flash": "查看 Play"
|
||||
"write:flash": "编辑 Play"
|
||||
"read:flash-likes": "查看 Play 的点赞"
|
||||
@@ -2456,7 +2457,7 @@ _permissions:
|
||||
"write:admin:unsuspend-user": "解除用户冻结"
|
||||
"write:admin:meta": "编辑实例元数据"
|
||||
"write:admin:user-note": "编辑管理笔记"
|
||||
"write:admin:roles": "编辑角色"
|
||||
"write:admin:roles": "管理角色"
|
||||
"read:admin:roles": "查看角色"
|
||||
"write:admin:relays": "编辑中继"
|
||||
"read:admin:relays": "查看中继"
|
||||
@@ -2466,8 +2467,8 @@ _permissions:
|
||||
"read:admin:announcements": "查看公告"
|
||||
"write:admin:avatar-decorations": "编辑头像挂件"
|
||||
"read:admin:avatar-decorations": "查看头像挂件"
|
||||
"write:admin:federation": "编辑联合相关信息"
|
||||
"write:admin:account": "编辑用户账户"
|
||||
"write:admin:federation": "编辑联邦相关信息"
|
||||
"write:admin:account": "管理用户账户"
|
||||
"read:admin:account": "查看用户相关情报"
|
||||
"write:admin:emoji": "编辑表情符号"
|
||||
"read:admin:emoji": "查看表情符号"
|
||||
@@ -2483,7 +2484,7 @@ _permissions:
|
||||
"read:invite-codes": "获取已发行的邀请码"
|
||||
"write:clip-favorite": "管理喜欢的便签"
|
||||
"read:clip-favorite": "查看便签的点赞"
|
||||
"read:federation": "查看联合相关信息"
|
||||
"read:federation": "查看联邦相关信息"
|
||||
"write:report-abuse": "举报用户"
|
||||
"write:chat": "撰写或删除消息"
|
||||
"read:chat": "查看私信"
|
||||
@@ -2530,7 +2531,7 @@ _widgets:
|
||||
photos: "照片"
|
||||
digitalClock: "数字时钟"
|
||||
unixClock: "UNIX 时钟"
|
||||
federation: "联合"
|
||||
federation: "联邦"
|
||||
instanceCloud: "服务器球状列表"
|
||||
postForm: "发帖窗口"
|
||||
slideshow: "幻灯片展示"
|
||||
@@ -2563,7 +2564,7 @@ _widgetOptions:
|
||||
graduationDots: "点"
|
||||
graduationArabic: "阿拉伯数字"
|
||||
fadeGraduations: "淡化表盘"
|
||||
sAnimation: "秒针动画"
|
||||
sAnimation: "秒针动效"
|
||||
sAnimationElastic: "跳动"
|
||||
sAnimationEaseOut: "平滑"
|
||||
twentyFour: "24 小时制"
|
||||
@@ -2621,11 +2622,11 @@ _visibility:
|
||||
followersDescription: "仅发送至关注者"
|
||||
specified: "指定用户"
|
||||
specifiedDescription: "仅发送至指定用户"
|
||||
disableFederation: "不参与联合"
|
||||
disableFederation: "仅限本地"
|
||||
disableFederationDescription: "不发送到其他服务器"
|
||||
_postForm:
|
||||
quitInspiteOfThereAreUnuploadedFilesConfirm: "还有未上传的文件,要丢弃并关闭窗口吗?"
|
||||
uploaderTip: "文件还未上传。可以在文件菜单中进行重命名、裁剪、添加水印、设置是否压缩等操作。文件将在发帖时自动上传。"
|
||||
uploaderTip: "文件尚未上传。您可以在文件菜单中设置重命名、裁剪图片、添加水印以及是否压缩等功能。文件将在帖子发布时自动上传。"
|
||||
replyPlaceholder: "回复这个帖子..."
|
||||
quotePlaceholder: "引用这个帖子..."
|
||||
channelPlaceholder: "发布到频道…"
|
||||
@@ -2660,12 +2661,12 @@ _profile:
|
||||
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
|
||||
metadataLabel: "标签"
|
||||
metadataContent: "内容"
|
||||
changeAvatar: "修改头像"
|
||||
changeBanner: "修改横幅"
|
||||
changeAvatar: "更换头像"
|
||||
changeBanner: "更换横幅"
|
||||
verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。"
|
||||
avatarDecorationMax: "最多可添加 {max} 个挂件"
|
||||
followedMessage: "被关注时显示的消息"
|
||||
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
|
||||
followedMessage: "被关注时的信息"
|
||||
followedMessageDescription: "被关注时,可设置向关注者显示的信息。"
|
||||
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息会在请求被批准后显示。"
|
||||
_exportOrImport:
|
||||
allNotes: "所有帖子"
|
||||
@@ -2673,13 +2674,13 @@ _exportOrImport:
|
||||
clips: "便签"
|
||||
followingList: "关注中"
|
||||
muteList: "隐藏"
|
||||
blockingList: "屏蔽"
|
||||
blockingList: "禁止与我互动的列表"
|
||||
userLists: "列表"
|
||||
excludeMutingUsers: "排除已隐藏用户"
|
||||
excludeInactiveUsers: "排除不活跃用户"
|
||||
withReplies: "在时间线中包含导入用户的回复"
|
||||
_charts:
|
||||
federation: "联合"
|
||||
federation: "联邦"
|
||||
apRequest: "请求"
|
||||
usersIncDec: "用户数量:增加/减少"
|
||||
usersTotal: "用户总数"
|
||||
@@ -2977,7 +2978,7 @@ _moderationLogTypes:
|
||||
deleteAccount: "删除帐户"
|
||||
deletePage: "删除页面"
|
||||
deleteFlash: "删除 Play"
|
||||
deleteGalleryPost: "删除图集内容"
|
||||
deleteGalleryPost: "删除相册内容"
|
||||
deleteChatRoom: "删除群聊"
|
||||
updateProxyAccountDescription: "更新代理账户的简介"
|
||||
_fileViewer:
|
||||
@@ -3034,7 +3035,7 @@ _dataSaver:
|
||||
description: "防止自动加载图像和视频。 点击隐藏的图像/视频即可加载它们。\n"
|
||||
_avatar:
|
||||
title: "头像"
|
||||
description: "停止播放头像的动画。 由于动画图片的文件大小可能比普通图像大,这可以进一步减少数据流量。"
|
||||
description: "不播放头像的动画。 由于动态图像的文件大小远大于一般图像,停止播放能够进一步节省数据流量。"
|
||||
_urlPreviewThumbnail:
|
||||
title: "不显示 URL预览缩略图"
|
||||
description: "将不再加载 URL 预览缩略图。"
|
||||
@@ -3053,7 +3054,7 @@ _reversi:
|
||||
gameSettings: "对局设置"
|
||||
chooseBoard: "选择棋盘"
|
||||
blackOrWhite: "先手/后手"
|
||||
blackIs: "{name}执黑(先手)"
|
||||
blackIs: "{name}执黑(先手)"
|
||||
rules: "规则"
|
||||
thisGameIsStartedSoon: "对局即将开始"
|
||||
waitingForOther: "等待对手准备"
|
||||
@@ -3069,7 +3070,7 @@ _reversi:
|
||||
surrendered: "已认输"
|
||||
timeout: "超时"
|
||||
drawn: "平局"
|
||||
won: "{name}获胜"
|
||||
won: "{name} 获胜"
|
||||
black: "黑"
|
||||
white: "白"
|
||||
total: "总计"
|
||||
@@ -3078,7 +3079,7 @@ _reversi:
|
||||
allGames: "所有对局"
|
||||
ended: "结束"
|
||||
playing: "对局中"
|
||||
isLlotheo: "落子少的一方获胜(又名奥赛罗)"
|
||||
isLlotheo: "落子少的一方获胜(黑白棋规则)"
|
||||
loopedMap: "循环棋盘"
|
||||
canPutEverywhere: "无限制放置模式"
|
||||
timeLimitForEachTurn: "1回合的时间限制"
|
||||
@@ -3088,8 +3089,8 @@ _reversi:
|
||||
shareToTlTheGameWhenStart: "开始时在时间线发布对局"
|
||||
iStartedAGame: "对局开始!#MisskeyReversi"
|
||||
opponentHasSettingsChanged: "对手更改了设定"
|
||||
allowIrregularRules: "允许非常规规则(完全自由)"
|
||||
disallowIrregularRules: "禁止非常规规则"
|
||||
allowIrregularRules: "允许特殊规则(完全自由)"
|
||||
disallowIrregularRules: "禁止特殊规则"
|
||||
showBoardLabels: "显示行号和列号"
|
||||
useAvatarAsStone: "用头像作为棋子"
|
||||
_offlineScreen:
|
||||
@@ -3246,7 +3247,7 @@ _search:
|
||||
searchScopeLocal: "本地"
|
||||
searchScopeServer: "指定服务器"
|
||||
searchScopeUser: "指定用户"
|
||||
pleaseEnterServerHost: "请填写服务器主机名"
|
||||
pleaseEnterServerHost: "请填写服务器的主机名称"
|
||||
pleaseSelectUser: "请选择用户"
|
||||
serverHostPlaceholder: "如:misskey.example.com"
|
||||
_serverSetupWizard:
|
||||
@@ -3275,13 +3276,13 @@ _serverSetupWizard:
|
||||
largeScaleServerAdvice: "运营大规模服务器可能需要高级基础设施知识,如负载均衡和数据库复制。"
|
||||
doYouConnectToFediverse: "要加入 Fediverse 吗?"
|
||||
doYouConnectToFediverse_description1: "若加入由分散性服务器所构成的网络(Fediverse),将能与其它服务器交换内容。"
|
||||
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联合」。"
|
||||
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
|
||||
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联邦」。"
|
||||
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器允许进行联邦交互等高级设置。"
|
||||
remoteContentsCleaning: "自动清理传入内容"
|
||||
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
|
||||
remoteContentsCleaning_description: "开启联邦互通后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
|
||||
adminInfo: "管理员信息"
|
||||
adminInfo_description: "设置用于接受询问的管理员信息。"
|
||||
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"
|
||||
adminInfo_mustBeFilled: "开放服务器或启用了联邦的情况下必须输入。"
|
||||
followingSettingsAreRecommended: "推荐以下设置"
|
||||
applyTheseSettings: "使用此设置"
|
||||
skipSettings: "跳过设置"
|
||||
@@ -3301,7 +3302,7 @@ _uploader:
|
||||
doneConfirm: "部分文件尚未上传,是否继续?"
|
||||
maxFileSizeIsX: "可上传最大 {x} 的文件。"
|
||||
allowedTypes: "可上传的文件类型"
|
||||
tip: "文件还没有被上传。可在此对话框中进行上传前确认、重命名、压缩、裁剪等操作。准备完成后,点击「上传」即可开始上传。"
|
||||
tip: "文件尚未上传。在此对话框中,您可以进行上传前的确认、重命名、压缩和裁剪等操作。准备就绪后,点击“上传”按钮即可开始上传。"
|
||||
_clientPerformanceIssueTip:
|
||||
title: "如果觉得电池耗电过高"
|
||||
makeSureDisabledAdBlocker: "请关闭广告拦截器"
|
||||
|
||||
20
package.json
20
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.5.1-alpha.0",
|
||||
"version": "2026.5.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
@@ -53,29 +53,29 @@
|
||||
"cleanall": "pnpm clean-all"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssnano": "7.1.5",
|
||||
"cssnano": "7.1.7",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "8.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"postcss": "8.5.9",
|
||||
"postcss": "8.5.10",
|
||||
"tar": "7.5.13",
|
||||
"terser": "5.46.1"
|
||||
"terser": "5.46.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.4",
|
||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260421.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260426.1",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.13.1",
|
||||
"cypress": "15.14.1",
|
||||
"eslint": "9.39.4",
|
||||
"globals": "17.5.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.33.0",
|
||||
"pnpm": "10.33.2",
|
||||
"start-server-and-test": "3.0.2",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
"utf-8-validate": "6.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1030.0",
|
||||
"@aws-sdk/lib-storage": "3.1030.0",
|
||||
"@aws-sdk/client-s3": "3.1037.0",
|
||||
"@aws-sdk/lib-storage": "3.1037.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.2.0",
|
||||
@@ -65,26 +65,26 @@
|
||||
"@kitajs/html": "4.2.13",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.3.0",
|
||||
"@napi-rs/canvas": "0.1.97",
|
||||
"@napi-rs/canvas": "0.1.100",
|
||||
"@nestjs/common": "11.1.19",
|
||||
"@nestjs/core": "11.1.19",
|
||||
"@nestjs/testing": "11.1.19",
|
||||
"@oxc-project/runtime": "0.125.0",
|
||||
"@oxc-project/runtime": "0.127.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.48.0",
|
||||
"@sentry/profiling-node": "10.48.0",
|
||||
"@sentry/node": "10.50.0",
|
||||
"@sentry/profiling-node": "10.50.0",
|
||||
"@simplewebauthn/server": "13.3.0",
|
||||
"@sinonjs/fake-timers": "15.3.2",
|
||||
"@smithy/node-http-handler": "4.5.2",
|
||||
"@smithy/node-http-handler": "4.6.1",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.18.0",
|
||||
"ajv": "8.20.0",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "2.2.2",
|
||||
"bullmq": "5.73.5",
|
||||
"bullmq": "5.76.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
@@ -92,14 +92,14 @@
|
||||
"color-convert": "3.1.3",
|
||||
"content-disposition": "1.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"deep-email-validator": "0.1.27",
|
||||
"fastify": "5.8.5",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "5.2.0",
|
||||
"feed": "5.2.1",
|
||||
"file-type": "22.0.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.5",
|
||||
"got": "14.6.6",
|
||||
"got": "15.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
@@ -116,16 +116,16 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.7",
|
||||
"nanoid": "5.1.9",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.1.0",
|
||||
"nodemailer": "8.0.5",
|
||||
"nodemailer": "8.0.6",
|
||||
"nsfwjs": "4.3.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.5.0",
|
||||
"otpauth": "9.5.1",
|
||||
"pg": "8.20.0",
|
||||
"pkce-challenge": "6.0.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
@@ -160,8 +160,7 @@
|
||||
"@kitajs/ts-html-plugin": "4.1.4",
|
||||
"@nestjs/platform-express": "11.1.19",
|
||||
"@rollup/plugin-esm-shim": "0.1.8",
|
||||
"@sentry/vue": "10.48.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@sentry/vue": "10.50.0",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "7.0.0",
|
||||
"@types/body-parser": "1.19.6",
|
||||
@@ -192,9 +191,9 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@vitest/coverage-v8": "4.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor": "10.0.12",
|
||||
"cross-env": "10.1.0",
|
||||
@@ -206,8 +205,8 @@
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.2.2",
|
||||
"vite": "8.0.8",
|
||||
"vitest": "4.1.4",
|
||||
"vite": "8.0.10",
|
||||
"vitest": "4.1.5",
|
||||
"vitest-mock-extended": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export type RolePolicies = {
|
||||
canSearchUsers: boolean;
|
||||
canUseTranslator: boolean;
|
||||
canHideAds: boolean;
|
||||
canCreateChannel: boolean;
|
||||
driveCapacityMb: number;
|
||||
maxFileSizeMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
@@ -88,6 +89,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canSearchUsers: true,
|
||||
canUseTranslator: true,
|
||||
canHideAds: false,
|
||||
canCreateChannel: true,
|
||||
driveCapacityMb: 100,
|
||||
maxFileSizeMb: 30,
|
||||
alwaysMarkNsfw: false,
|
||||
@@ -410,6 +412,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
canSearchUsers: calc('canSearchUsers', vs => vs.some(v => v === true)),
|
||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
canCreateChannel: calc('canCreateChannel', vs => vs.some(v => v === true)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)),
|
||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/types';
|
||||
} from '@simplewebauthn/server';
|
||||
|
||||
@Injectable()
|
||||
export class WebAuthnService {
|
||||
|
||||
@@ -313,7 +313,8 @@ export class ApInboxService {
|
||||
// アナウンス先が許可されているかチェック
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) return;
|
||||
|
||||
const unlock = await acquireApObjectLock(this.redisClient, uri);
|
||||
const activityUri = getApId(activity);
|
||||
const unlock = await acquireApObjectLock(this.redisClient, activityUri);
|
||||
|
||||
try {
|
||||
// 既に同じURIを持つものが登録されていないかチェック
|
||||
|
||||
@@ -224,6 +224,10 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canCreateChannel: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
driveCapacityMb: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import cors from '@fastify/cors';
|
||||
import multipart from '@fastify/multipart';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
@@ -28,7 +28,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -23,7 +23,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -22,6 +22,8 @@ export const meta = {
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
requiredRolePolicy: 'canCreateChannel',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 10,
|
||||
|
||||
@@ -39,137 +39,6 @@ export const meta = {
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
nullable: false,
|
||||
optional: false,
|
||||
properties: {
|
||||
rp: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
challenge: {
|
||||
type: 'string',
|
||||
},
|
||||
pubKeyCredParams: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
},
|
||||
alg: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
excludeCredentials: {
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
},
|
||||
transports: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'ble',
|
||||
'cable',
|
||||
'hybrid',
|
||||
'internal',
|
||||
'nfc',
|
||||
'smart-card',
|
||||
'usb',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authenticatorSelection: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
authenticatorAttachment: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'cross-platform',
|
||||
'platform',
|
||||
],
|
||||
},
|
||||
requireResidentKey: {
|
||||
type: 'boolean',
|
||||
},
|
||||
userVerification: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'discouraged',
|
||||
'preferred',
|
||||
'required',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
attestation: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
enum: [
|
||||
'direct',
|
||||
'enterprise',
|
||||
'indirect',
|
||||
'none',
|
||||
null,
|
||||
],
|
||||
},
|
||||
extensions: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
appid: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
credProps: {
|
||||
type: 'boolean',
|
||||
nullable: true,
|
||||
},
|
||||
hmacCreateSecret: {
|
||||
type: 'boolean',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.29
|
||||
image: nginx:1.30
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./certificates/rootCA.crt
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/types';
|
||||
} from '@simplewebauthn/server';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { describe, beforeAll, test } from 'vitest';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
|
||||
import { mockDeep } from 'vitest-mock-extended';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
|
||||
import { HttpHeader } from 'fastify/types/utils.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/node": "24.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"rollup": "4.60.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"rollup": "4.60.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"estree-walker": "3.0.3",
|
||||
"i18n": "workspace:*",
|
||||
"magic-string": "0.30.21",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"vite": "8.0.8"
|
||||
"vite": "8.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.60.1",
|
||||
"rollup": "4.60.2",
|
||||
"shiki": "4.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"uuid": "13.0.0",
|
||||
"vue": "3.5.32"
|
||||
"uuid": "14.0.0",
|
||||
"vue": "3.5.33"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.3.0",
|
||||
@@ -40,26 +40,26 @@
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@vitest/coverage-v8": "4.1.4",
|
||||
"@vue/runtime-core": "3.5.32",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"@vue/runtime-core": "3.5.33",
|
||||
"acorn": "8.16.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.8.0",
|
||||
"eslint-plugin-vue": "10.9.0",
|
||||
"happy-dom": "20.9.0",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.13.3",
|
||||
"msw": "2.13.6",
|
||||
"prettier": "3.8.3",
|
||||
"sass-embedded": "1.99.0",
|
||||
"start-server-and-test": "3.0.2",
|
||||
"tsx": "4.21.0",
|
||||
"vite": "8.0.8",
|
||||
"vite": "8.0.10",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.2.6",
|
||||
"vue-component-type-helpers": "3.2.7",
|
||||
"vue-eslint-parser": "10.4.0",
|
||||
"vue-tsc": "3.2.6"
|
||||
"vue-tsc": "3.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
||||
import { serverContext } from '@/server-context.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
|
||||
console.log('Misskey Embed');
|
||||
|
||||
|
||||
@@ -5,26 +5,10 @@
|
||||
|
||||
// TODO: (可能な部分を)sharedに抽出して frontend と共通化
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
props: Record<string, string>;
|
||||
codeHighlighter?: {
|
||||
base: BundledTheme;
|
||||
overrides?: Record<string, any>;
|
||||
} | {
|
||||
base: '_none_';
|
||||
overrides: Record<string, any>;
|
||||
};
|
||||
};
|
||||
import { compile } from '@@/js/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
|
||||
let timeout: number | null = null;
|
||||
|
||||
@@ -32,7 +16,7 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
||||
return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme;
|
||||
}
|
||||
|
||||
export function applyTheme(theme: Theme, persist = true) {
|
||||
export function applyTheme(theme: Theme) {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
@@ -68,48 +52,3 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
|
||||
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
||||
}
|
||||
|
||||
function compile(theme: Theme): Record<string, string> {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getColor(theme.props[val.substring(1)]);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
const props = {};
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
return c.toRgbString();
|
||||
}
|
||||
|
||||
13
packages/frontend-shared/@types/theme.d.ts
vendored
Normal file
13
packages/frontend-shared/@types/theme.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare module '@@/themes/*.json5' {
|
||||
import { Theme } from '@@/js/theme.js';
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default theme;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as esbuild from 'esbuild';
|
||||
import { build } from 'esbuild';
|
||||
import { execa } from 'execa';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
|
||||
|
||||
const entryPoints = fs.globSync('./js/**/**.{ts,tsx}');
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const options = {
|
||||
entryPoints,
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
outdir: './js-built',
|
||||
target: 'es2022',
|
||||
platform: 'browser',
|
||||
format: 'esm',
|
||||
sourcemap: 'linked',
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
|
||||
|
||||
// js-built配下をすべて削除する
|
||||
if (!args.includes('--no-clean')) {
|
||||
fs.rmSync('./js-built', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (args.includes('--watch')) {
|
||||
await watchSrc();
|
||||
} else {
|
||||
await buildSrc();
|
||||
}
|
||||
|
||||
async function buildSrc() {
|
||||
console.log(`[${_package.name}] start building...`);
|
||||
|
||||
await build(options)
|
||||
.then(() => {
|
||||
console.log(`[${_package.name}] build succeeded.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(err.stderr);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
|
||||
} else {
|
||||
await buildDts();
|
||||
}
|
||||
|
||||
fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json');
|
||||
|
||||
console.log(`[${_package.name}] finish building.`);
|
||||
}
|
||||
|
||||
function buildDts() {
|
||||
return execa(
|
||||
'tsgo',
|
||||
[
|
||||
'--project', 'tsconfig.json',
|
||||
'--outDir', 'js-built',
|
||||
'--declaration', 'true',
|
||||
'--emitDeclarationOnly', 'true',
|
||||
],
|
||||
{
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function watchSrc() {
|
||||
const plugins = [{
|
||||
name: 'gen-dts',
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log(`[${_package.name}] detect changed...`);
|
||||
});
|
||||
build.onEnd(async result => {
|
||||
if (result.errors.length > 0) {
|
||||
console.error(`[${_package.name}] watch build failed:`, result);
|
||||
return;
|
||||
}
|
||||
await buildDts();
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
console.log(`[${_package.name}] start watching...`);
|
||||
|
||||
const context = await esbuild.context({ ...options, plugins });
|
||||
await context.watch();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
process.on('SIGHUP', resolve);
|
||||
process.on('SIGINT', resolve);
|
||||
process.on('SIGTERM', resolve);
|
||||
process.on('uncaughtException', reject);
|
||||
process.on('exit', resolve);
|
||||
}).finally(async () => {
|
||||
await context.dispose();
|
||||
console.log(`[${_package.name}] finish watching.`);
|
||||
});
|
||||
}
|
||||
126
packages/frontend-shared/js/theme.ts
Normal file
126
packages/frontend-shared/js/theme.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import JSON5 from 'json5';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
kind?: 'dark' | 'light'; // legacy
|
||||
props: Record<string, string>;
|
||||
codeHighlighter?: {
|
||||
base: BundledTheme;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overrides?: Record<string, any>;
|
||||
} | {
|
||||
base: '_none_';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overrides: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
export type CompiledTheme = Record<string, string>;
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
|
||||
export const getBuiltinThemes = () => Promise.all(
|
||||
[
|
||||
'l-light',
|
||||
'l-coffee',
|
||||
'l-apricot',
|
||||
'l-rainy',
|
||||
'l-botanical',
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
'l-u0',
|
||||
|
||||
'd-dark',
|
||||
'd-persimmon',
|
||||
'd-astro',
|
||||
'd-future',
|
||||
'd-botanical',
|
||||
'd-green-lime',
|
||||
'd-green-orange',
|
||||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-u0',
|
||||
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
export function compile(theme: Theme): CompiledTheme {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getColor(theme.props[val.substring(1)]);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
const props = {} as CompiledTheme;
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
return c.toRgbString();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function validateTheme(theme: Record<string, any>): boolean {
|
||||
if (theme.id == null || typeof theme.id !== 'string') return false;
|
||||
if (theme.name == null || typeof theme.name !== 'string') return false;
|
||||
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
|
||||
if (theme.props == null || typeof theme.props !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseThemeCode(code: string): Theme {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (_) {
|
||||
throw new Error('Failed to parse theme json');
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
@@ -1,32 +1,18 @@
|
||||
{
|
||||
"name": "frontend-shared",
|
||||
"type": "module",
|
||||
"main": "./js-built/index.js",
|
||||
"types": "./js-built/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./js-built/index.js",
|
||||
"types": "./js-built/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./js-built/*",
|
||||
"types": "./js-built/*"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node ./build.js",
|
||||
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"esbuild": "0.28.0",
|
||||
"eslint-plugin-vue": "10.8.0",
|
||||
"nodemon": "3.1.14",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"eslint-plugin-vue": "10.9.0",
|
||||
"vue-eslint-parser": "10.4.0"
|
||||
},
|
||||
"files": [
|
||||
@@ -34,7 +20,10 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"json5": "2.2.3",
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.32"
|
||||
"shiki": "4.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"vue": "3.5.33"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ let moduleInitialized = false;
|
||||
let unobserve = () => {};
|
||||
let misskeyOS = null;
|
||||
|
||||
function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
|
||||
function loadTheme(themeMaganer: typeof import('../src/theme')['themeManager']) {
|
||||
unobserve();
|
||||
const theme = themes[window.document.documentElement.dataset.misskeyTheme];
|
||||
if (theme) {
|
||||
applyTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
|
||||
themeMaganer.updateTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
|
||||
} else {
|
||||
applyTheme(themes['l-light']);
|
||||
themeMaganer.updateTheme(themes['l-light']);
|
||||
}
|
||||
const observer = new MutationObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
@@ -34,7 +34,7 @@ function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
|
||||
const target = entry.target as HTMLElement;
|
||||
const theme = themes[target.dataset.misskeyTheme];
|
||||
if (theme) {
|
||||
applyTheme(themes[target.dataset.misskeyTheme]);
|
||||
themeMaganer.updateTheme(themes[target.dataset.misskeyTheme]);
|
||||
} else {
|
||||
target.removeAttribute('style');
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
"dependencies": {
|
||||
"@analytics/google-analytics": "1.1.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/core-glue": "0.1.0-alpha-5",
|
||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@sentry/vue": "10.48.0",
|
||||
"@sentry/vue": "10.50.0",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@syuilo/aiscript": "1.2.1",
|
||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"chartjs-chart-matrix": "3.0.0",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "15.3.1",
|
||||
"chromatic": "16.6.0",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.40.1",
|
||||
"mediabunny": "1.41.0",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -64,11 +64,11 @@
|
||||
"sanitize-html": "2.17.3",
|
||||
"shiki": "4.0.2",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.183.2",
|
||||
"three": "0.184.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vue": "3.5.32",
|
||||
"vue": "3.5.33",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -106,22 +106,22 @@
|
||||
"@types/textarea-caret": "3.0.4",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@vitest/coverage-v8": "4.1.4",
|
||||
"@vue/compiler-core": "3.5.32",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"@vue/compiler-core": "3.5.33",
|
||||
"acorn": "8.16.0",
|
||||
"astring": "1.9.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.13.1",
|
||||
"cypress": "15.14.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.8.0",
|
||||
"eslint-plugin-vue": "10.9.0",
|
||||
"estree-walker": "3.0.3",
|
||||
"happy-dom": "20.9.0",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.2.5",
|
||||
"msw": "2.13.3",
|
||||
"msw": "2.13.6",
|
||||
"msw-storybook-addon": "2.0.7",
|
||||
"nodemon": "3.1.14",
|
||||
"prettier": "3.8.3",
|
||||
@@ -134,13 +134,13 @@
|
||||
"storybook": "10.3.5",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.21.0",
|
||||
"vite": "8.0.8",
|
||||
"vite": "8.0.10",
|
||||
"vite-plugin-glsl": "1.6.0",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "4.1.4",
|
||||
"vitest": "4.1.5",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.2.6",
|
||||
"vue-component-type-helpers": "3.2.7",
|
||||
"vue-eslint-parser": "10.4.0",
|
||||
"vue-tsc": "3.2.6"
|
||||
"vue-tsc": "3.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { App } from 'vue';
|
||||
import widgets from '@/widgets/index.js';
|
||||
import directives from '@/directives/index.js';
|
||||
import components from '@/components/index.js';
|
||||
import { applyTheme } from '@/theme.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { refreshCurrentAccount, login } from '@/accounts.js';
|
||||
@@ -161,7 +161,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
|
||||
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため)
|
||||
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重発火を防ぐため)
|
||||
// see: https://github.com/misskey-dev/misskey/issues/16562
|
||||
watch(store.r.darkMode, (darkMode) => {
|
||||
const theme = (() => {
|
||||
@@ -172,7 +172,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||
}
|
||||
})();
|
||||
|
||||
applyTheme(theme);
|
||||
themeManager.updateTheme(theme);
|
||||
}, { immediate: true });
|
||||
|
||||
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
|
||||
@@ -180,13 +180,13 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||
if (!isSafeMode) {
|
||||
watch(prefer.r.darkTheme, (theme) => {
|
||||
if (store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultDarkTheme);
|
||||
themeManager.updateTheme(theme ?? defaultDarkTheme);
|
||||
}
|
||||
});
|
||||
|
||||
watch(prefer.r.lightTheme, (theme) => {
|
||||
if (!store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultLightTheme);
|
||||
themeManager.updateTheme(theme ?? defaultLightTheme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
|
||||
|
||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
||||
@@ -192,13 +192,13 @@ function tick() {
|
||||
tick();
|
||||
|
||||
function calcColors() {
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
|
||||
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
const themeValue = themeManager.currentCompiledTheme!;
|
||||
const dark = tinycolor(themeValue.bg).isDark();
|
||||
const accent = tinycolor(themeValue.accent).toHexString();
|
||||
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
|
||||
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
|
||||
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
|
||||
mHandColor.value = tinycolor(themeValue.fg).toHexString();
|
||||
hHandColor.value = accent;
|
||||
nowColor.value = accent;
|
||||
}
|
||||
@@ -207,13 +207,13 @@ calcColors();
|
||||
|
||||
onMounted(() => {
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
globalEvents.on('themeChanged', calcColors);
|
||||
themeManager.on('themeChanged', calcColors);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
enabled = false;
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
globalEvents.off('themeChanged', calcColors);
|
||||
themeManager.off('themeChanged', calcColors);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import * as Misskey from 'misskey-js';
|
||||
import Cropper from 'cropperjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -105,10 +105,10 @@ onMounted(() => {
|
||||
cropper = new Cropper(imgEl.value, {
|
||||
});
|
||||
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const themeValue = themeManager.currentCompiledTheme!;
|
||||
|
||||
const selection = cropper.getCropperSelection()!;
|
||||
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
selection.themeColor = tinycolor(themeValue.accent).toHexString();
|
||||
if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio;
|
||||
selection.initialAspectRatio = props.aspectRatio ?? 1;
|
||||
selection.outlined = true;
|
||||
|
||||
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
|
||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||
@@ -92,11 +92,11 @@ function updateBgColor() {
|
||||
|
||||
onMounted(() => {
|
||||
updateBgColor();
|
||||
globalEvents.on('themeChanging', updateBgColor);
|
||||
themeManager.on('themeChanging', updateBgColor);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
globalEvents.off('themeChanging', updateBgColor);
|
||||
themeManager.off('themeChanging', updateBgColor);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ import { nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
import { pageFolderTeleportCount, popup } from '@/os.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import MkFolderPage from '@/components/MkFolderPage.vue';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
|
||||
@@ -192,9 +193,9 @@ async function toggle(ev: PointerEvent) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const themeValue = themeManager.currentCompiledTheme!;
|
||||
const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
|
||||
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
|
||||
const myBg = themeValue.panel;
|
||||
bgSame.value = parentBg === myBg;
|
||||
});
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
|
||||
initChart();
|
||||
|
||||
@@ -186,7 +187,7 @@ function createDoughnut(chartEl: HTMLCanvasElement, tooltip: ReturnType<typeof u
|
||||
labels: data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: data.map(x => x.color),
|
||||
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||
borderColor: themeManager.currentCompiledTheme!.panel,
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: data.map(x => x.value),
|
||||
|
||||
@@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
||||
@@ -47,8 +48,7 @@ const polylinePoints = ref('');
|
||||
const polygonPoints = ref('');
|
||||
const headX = ref<number | null>(null);
|
||||
const headY = ref<number | null>(null);
|
||||
const clock = ref<number | null>(null);
|
||||
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
|
||||
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
|
||||
const color = accent.toRgbString();
|
||||
|
||||
function draw(): void {
|
||||
|
||||
@@ -135,6 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkA :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
<span style="margin-left: 0.5em;">
|
||||
<span style="border: 1px solid var(--MI_THEME-divider); margin-right: 0.5em;"></span>
|
||||
<i v-if="appearNote.visibility === 'public'" class="ti ti-world"></i>
|
||||
<i v-else-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
<span style="margin-left: 0.3em;">{{ i18n.ts._visibility[appearNote.visibility] }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<MkReactionsViewer
|
||||
v-if="appearNote.reactionAcceptance !== 'likeOnly'"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Chart } from 'chart.js';
|
||||
import type { ScatterDataPoint } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { store } from '@/store.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
@@ -51,7 +52,7 @@ onMounted(async () => {
|
||||
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
|
||||
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
|
||||
const color = accent.toHex();
|
||||
|
||||
if (chartEl.value == null) return;
|
||||
|
||||
@@ -22,21 +22,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
|
||||
import type { PublicKeyCredentialRequestOptionsJSON, AuthenticationResponseJSON } from '@simplewebauthn/browser';
|
||||
|
||||
const props = defineProps<{
|
||||
credentialRequest: CredentialRequestOptions;
|
||||
credentialRequest: PublicKeyCredentialRequestOptionsJSON;
|
||||
isPerformingPasswordlessLogin?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', credential: AuthenticationPublicKeyCredential): void;
|
||||
(ev: 'done', credential: AuthenticationResponseJSON): void;
|
||||
(ev: 'useTotp'): void;
|
||||
}>();
|
||||
|
||||
@@ -44,7 +44,7 @@ const queryingKey = ref(true);
|
||||
|
||||
async function queryKey() {
|
||||
queryingKey.value = true;
|
||||
await webAuthnRequest(props.credentialRequest)
|
||||
await startAuthentication({ optionsJSON: props.credentialRequest })
|
||||
.catch(() => {
|
||||
return Promise.reject(null);
|
||||
})
|
||||
|
||||
@@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
|
||||
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
||||
import type { PublicKeyCredentialRequestOptionsJSON, AuthenticationResponseJSON } from '@simplewebauthn/browser';
|
||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||
import type { PwResponse } from '@/components/MkSignin.password.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
@@ -108,21 +108,18 @@ const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
|
||||
const password = ref('');
|
||||
|
||||
//#region Passkey Passwordless
|
||||
const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
|
||||
const credentialRequest = shallowRef<PublicKeyCredentialRequestOptionsJSON | null>(null);
|
||||
const passkeyContext = ref('');
|
||||
const doingPasskeyFromInputPage = ref(false);
|
||||
|
||||
function onPasskeyLogin(): void {
|
||||
if (webAuthnSupported()) {
|
||||
if (browserSupportsWebAuthn()) {
|
||||
doingPasskeyFromInputPage.value = true;
|
||||
waiting.value = true;
|
||||
misskeyApi('signin-with-passkey', {})
|
||||
.then((res) => {
|
||||
passkeyContext.value = res.context ?? '';
|
||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||
// @ts-expect-error TODO: misskey-js由来の型(@simplewebauthn/types)とフロントエンド由来の型(@github/webauthn-json)が合わない
|
||||
publicKey: res.option,
|
||||
});
|
||||
credentialRequest.value = res.option;
|
||||
|
||||
page.value = 'passkey';
|
||||
waiting.value = false;
|
||||
@@ -131,12 +128,12 @@ function onPasskeyLogin(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
|
||||
function onPasskeyDone(credential: AuthenticationResponseJSON): void {
|
||||
waiting.value = true;
|
||||
|
||||
if (doingPasskeyFromInputPage.value) {
|
||||
misskeyApi<Misskey.entities.SigninWithPasskeyResponse>('signin-with-passkey', {
|
||||
credential: credential.toJSON(),
|
||||
misskeyApi('signin-with-passkey', {
|
||||
credential: credential,
|
||||
context: passkeyContext.value,
|
||||
}).then((res) => {
|
||||
if (res.signinResponse == null) {
|
||||
@@ -150,8 +147,7 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
|
||||
tryLogin({
|
||||
username: userInfo.value.username,
|
||||
password: password.value,
|
||||
// @ts-expect-error TODO: misskey-js由来の型(@simplewebauthn/types)とフロントエンド由来の型(@github/webauthn-json)が合わない
|
||||
credential: credential.toJSON(),
|
||||
credential: credential,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -253,11 +249,8 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi
|
||||
break;
|
||||
}
|
||||
case 'passkey': {
|
||||
if (webAuthnSupported()) {
|
||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||
// @ts-expect-error TODO: misskey-js由来の型(@simplewebauthn/types)とフロントエンド由来の型(@github/webauthn-json)が合わない
|
||||
publicKey: res.authRequest,
|
||||
});
|
||||
if (browserSupportsWebAuthn()) {
|
||||
credentialRequest.value = res.authRequest;
|
||||
page.value = 'passkey';
|
||||
} else {
|
||||
page.value = 'totp';
|
||||
|
||||
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
const loaded = !!window.TagCanvas;
|
||||
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||
const available = ref(false);
|
||||
@@ -33,7 +33,7 @@ watch(available, () => {
|
||||
try {
|
||||
window.TagCanvas.Start(idForCanvas, idForTags, {
|
||||
textColour: '#ffffff',
|
||||
outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
|
||||
outlineColour: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
|
||||
outlineRadius: 10,
|
||||
initial: [-0.030, -0.010],
|
||||
frontSelect: true,
|
||||
|
||||
@@ -43,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, watch } from 'vue';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { compile } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import { compile } from '@@/js/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef, ref, nextTick } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
@@ -61,8 +61,7 @@ async function renderChart() {
|
||||
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
const accent = tinycolor(themeManager.currentCompiledTheme!.accent).toHexString();
|
||||
|
||||
const colorRead = accent;
|
||||
const colorWrite = '#2ecc71';
|
||||
|
||||
@@ -324,8 +324,9 @@ function onTopHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionY(evt);
|
||||
const height = parseInt(getComputedStyle(main, '').height, 10);
|
||||
const top = parseInt(getComputedStyle(main, '').top, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const height = parseInt(computedStyle.height, 10);
|
||||
const top = parseInt(computedStyle.top, 10);
|
||||
|
||||
// 動かした時
|
||||
dragListen(me => {
|
||||
@@ -353,8 +354,9 @@ function onRightHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionX(evt);
|
||||
const width = parseInt(getComputedStyle(main, '').width, 10);
|
||||
const left = parseInt(getComputedStyle(main, '').left, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const width = parseInt(computedStyle.width, 10);
|
||||
const left = parseInt(computedStyle.left, 10);
|
||||
const browserWidth = window.innerWidth;
|
||||
|
||||
// 動かした時
|
||||
@@ -380,8 +382,9 @@ function onBottomHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionY(evt);
|
||||
const height = parseInt(getComputedStyle(main, '').height, 10);
|
||||
const top = parseInt(getComputedStyle(main, '').top, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const height = parseInt(computedStyle.height, 10);
|
||||
const top = parseInt(computedStyle.top, 10);
|
||||
const browserHeight = window.innerHeight;
|
||||
|
||||
// 動かした時
|
||||
@@ -407,8 +410,9 @@ function onLeftHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionX(evt);
|
||||
const width = parseInt(getComputedStyle(main, '').width, 10);
|
||||
const left = parseInt(getComputedStyle(main, '').left, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const width = parseInt(computedStyle.width, 10);
|
||||
const left = parseInt(computedStyle.left, 10);
|
||||
|
||||
// 動かした時
|
||||
dragListen(me => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Directive } from 'vue';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
|
||||
const handlerMap = new WeakMap<HTMLElement, () => void>();
|
||||
|
||||
@@ -27,10 +27,10 @@ export const adaptiveBorderDirective = {
|
||||
|
||||
calc();
|
||||
|
||||
globalEvents.on('themeChanged', calc);
|
||||
themeManager.on('themeChanged', calc);
|
||||
},
|
||||
|
||||
unmounted(src) {
|
||||
globalEvents.off('themeChanged', handlerMap.get(src));
|
||||
themeManager.off('themeChanged', handlerMap.get(src));
|
||||
},
|
||||
} as Directive<HTMLElement>;
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import type { Directive } from 'vue';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
|
||||
export const panelDirective = {
|
||||
mounted(src) {
|
||||
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
|
||||
|
||||
const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
|
||||
const myBg = themeManager.currentCompiledTheme!.panel;
|
||||
|
||||
if (parentBg === myBg) {
|
||||
src.style.backgroundColor = 'var(--MI_THEME-bg)';
|
||||
|
||||
@@ -8,8 +8,6 @@ import * as Misskey from 'misskey-js';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
|
||||
type Events = {
|
||||
themeChanging: () => void;
|
||||
themeChanged: () => void;
|
||||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||
notePosted: (note: Misskey.entities.Note) => void;
|
||||
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
|
||||
|
||||
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
@@ -43,7 +44,7 @@ onMounted(() => {
|
||||
labels: props.data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: props.data.map(x => x.color ?? '#000'),
|
||||
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||
borderColor: themeManager.currentCompiledTheme!.panel,
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: props.data.map(x => x.value),
|
||||
|
||||
@@ -161,6 +161,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canCreateChannel, 'canCreateChannel'])" v-model:policyMeta="policyMetaModel.canCreateChannel" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canCreateChannel }}</template>
|
||||
<template #suffix>{{ valuesModel.canCreateChannel ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canCreateChannel" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])" v-model:policyMeta="policyMetaModel.driveCapacityMb" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #valueText>{{ valuesModel.driveCapacityMb }}MB</template>
|
||||
|
||||
@@ -139,6 +139,7 @@ async function assign() {
|
||||
|
||||
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
|
||||
//role.users.push(user);
|
||||
usersPaginator.reload();
|
||||
}
|
||||
|
||||
async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) {
|
||||
@@ -149,6 +150,7 @@ async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) {
|
||||
action: async () => {
|
||||
await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: userId });
|
||||
//role.users = role.users.filter(u => u.id !== userId);
|
||||
usersPaginator.reload();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'owned'" class="_gaps">
|
||||
<MkButton type="routerLink" primary rounded to="/channels/new"><i class="ti ti-plus"></i> {{ i18n.ts.createNew }}</MkButton>
|
||||
<MkButton v-if="$i?.policies.canCreateChannel" type="routerLink" primary rounded to="/channels/new"><i class="ti ti-plus"></i> {{ i18n.ts.createNew }}</MkButton>
|
||||
<MkPagination v-slot="{items}" :paginator="ownedPaginator">
|
||||
<div :class="$style.root">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
|
||||
@@ -74,6 +74,7 @@ import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { parsePluginMeta, installPlugin } from '@/plugin.js';
|
||||
import { parseThemeCode, installTheme } from '@/theme.js';
|
||||
import { installTheme } from '@/theme.js';
|
||||
import { parseThemeCode } from '@@/js/theme.js';
|
||||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
@@ -48,11 +48,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
{{ i18n.ts._2fa.securityKeyInfo }}
|
||||
</MkInfo>
|
||||
|
||||
<MkInfo v-if="!webAuthnSupported()" warn>
|
||||
<MkInfo v-if="!browserSupportsWebAuthn()" warn>
|
||||
{{ i18n.ts._2fa.securityKeyNotSupported }}
|
||||
</MkInfo>
|
||||
|
||||
<MkInfo v-else-if="webAuthnSupported() && !$i.twoFactorEnabled" warn>
|
||||
<MkInfo v-else-if="browserSupportsWebAuthn() && !$i.twoFactorEnabled" warn>
|
||||
{{ i18n.ts._2fa.registerTOTPBeforeKey }}
|
||||
</MkInfo>
|
||||
|
||||
@@ -83,8 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed } from 'vue';
|
||||
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||
import { computed } from 'vue';
|
||||
import { browserSupportsWebAuthn, startRegistration } from '@simplewebauthn/browser';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
@@ -196,12 +196,9 @@ async function addSecurityKey() {
|
||||
const auth = await os.authenticateDialog();
|
||||
if (auth.canceled) return;
|
||||
|
||||
const registrationOptions = parseCreationOptionsFromJSON({
|
||||
// @ts-expect-error misskey-js側に型がない
|
||||
publicKey: await os.apiWithDialog('i/2fa/register-key', {
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
}),
|
||||
const registrationOptions = await os.apiWithDialog('i/2fa/register-key', {
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
});
|
||||
|
||||
const name = await os.inputText({
|
||||
@@ -214,7 +211,7 @@ async function addSecurityKey() {
|
||||
if (name.canceled) return;
|
||||
|
||||
const credential = await os.promiseDialog(
|
||||
webAuthnCreate(registrationOptions),
|
||||
startRegistration({ optionsJSON: registrationOptions }),
|
||||
null,
|
||||
() => {}, // ユーザーのキャンセルはrejectなのでエラーダイアログを出さない
|
||||
i18n.ts._2fa.tapSecurityKey,
|
||||
@@ -228,8 +225,7 @@ async function addSecurityKey() {
|
||||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
name: name.result,
|
||||
// @ts-expect-error misskey-js側に型がない
|
||||
credential: credential.toJSON(),
|
||||
credential: credential,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, computed } from 'vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
|
||||
import { themeManager, installTheme, handleThemeInstallError } from '@/theme.js';
|
||||
import { parseThemeCode } from '@@/js/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
@@ -29,6 +30,19 @@ import { useRouter } from '@/router.js';
|
||||
const router = useRouter();
|
||||
const installThemeCode = ref<string | null>(null);
|
||||
|
||||
function previewTheme(code: string): void {
|
||||
try {
|
||||
const theme = parseThemeCode(code);
|
||||
themeManager.previewTheme(theme);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function install(code: string): Promise<void> {
|
||||
try {
|
||||
const theme = parseThemeCode(code);
|
||||
@@ -40,22 +54,7 @@ async function install(code: string): Promise<void> {
|
||||
installThemeCode.value = null;
|
||||
router.push('/settings/theme');
|
||||
} catch (err: any) {
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts._theme.alreadyInstalled,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
break;
|
||||
}
|
||||
console.error(err);
|
||||
handleThemeInstallError(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,21 +27,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
|
||||
import { removeTheme } from '@/theme.js';
|
||||
import { getBuiltinThemes } from '@@/js/theme.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import { prefer } from '@/preferences';
|
||||
|
||||
const installedThemes = prefer.r.themes;
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => {
|
||||
builtinThemes.value = themes;
|
||||
});
|
||||
|
||||
const installedThemes = getThemesRef();
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
const {
|
||||
model: selectedThemeId,
|
||||
def: selectedThemeIdDef,
|
||||
|
||||
@@ -210,7 +210,7 @@ import JSON5 from 'json5';
|
||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||
import { isSafeMode } from '@@/js/config.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
@@ -218,7 +218,8 @@ import FormLink from '@/components/form/link.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkThemePreview from '@/components/MkThemePreview.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { getBuiltinThemesRef, getThemesRef, installTheme, parseThemeCode, removeTheme } from '@/theme.js';
|
||||
import { handleThemeInstallError, installTheme, removeTheme } from '@/theme.js';
|
||||
import { getBuiltinThemes } from '@@/js/theme.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -229,8 +230,11 @@ import { prefer } from '@/preferences.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { checkDragDataType, getDragData, getPlainDragData, setDragData, setPlainDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const installedThemes = getThemesRef();
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
const installedThemes = prefer.r.themes;
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => {
|
||||
builtinThemes.value = themes;
|
||||
});
|
||||
|
||||
const instanceDarkTheme = computed<Theme | null>(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
|
||||
const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||
@@ -353,7 +357,7 @@ async function onDrop(ev: DragEvent) {
|
||||
try {
|
||||
await installTheme(code);
|
||||
} catch (err) {
|
||||
// nop
|
||||
handleThemeInstallError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +79,15 @@ import JSON5 from 'json5';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import { host } from '@@/js/config.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { addTheme, applyTheme } from '@/theme.js';
|
||||
import { addTheme, themeManager } from '@/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import * as os from '@/os.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -130,7 +131,7 @@ const theme = ref<Theme>({
|
||||
name: 'untitled',
|
||||
author: `@${$i.username}@${toUnicode(host)}`,
|
||||
base: 'light',
|
||||
props: lightTheme.props,
|
||||
props: deepClone(lightTheme.props),
|
||||
});
|
||||
const description = ref<string | null>(null);
|
||||
const themeCode = ref<string>('');
|
||||
@@ -170,7 +171,7 @@ function setFgColor(color: typeof fgColors[number]) {
|
||||
|
||||
function apply() {
|
||||
themeCode.value = JSON5.stringify(theme.value, null, '\t');
|
||||
applyTheme(theme.value, false);
|
||||
themeManager.previewTheme(theme.value);
|
||||
changed.value = true;
|
||||
}
|
||||
|
||||
@@ -201,7 +202,7 @@ async function saveAs() {
|
||||
theme.value.name = name;
|
||||
if (description.value) theme.value.desc = description.value;
|
||||
await addTheme(theme.value);
|
||||
applyTheme(theme.value);
|
||||
themeManager.updateTheme(theme.value);
|
||||
if (store.s.darkMode) {
|
||||
prefer.commit('darkTheme', theme.value);
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { hemisphere } from '@@/js/intl-const.js';
|
||||
import { DEFAULT_EMOJIS } from '@@/js/const.js';
|
||||
import { prefersReducedMotion } from '@@/js/config.js';
|
||||
import { definePreferences } from './manager.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import type { SoundType } from '@/utility/sound.js';
|
||||
import type { Plugin } from '@/plugin.js';
|
||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
|
||||
@@ -6,73 +6,183 @@
|
||||
// TODO: (可能な部分を)sharedに抽出して frontend-embed と共通化
|
||||
|
||||
import { ref, nextTick } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import JSON5 from 'json5';
|
||||
import { version } from '@@/js/config.js';
|
||||
import type { Ref } from 'vue';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
import { getBuiltinThemes, parseThemeCode, themeProps, compile } from '@@/js/theme.js';
|
||||
import type { Theme, CompiledTheme } from '@@/js/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { deepEqual } from '@/utility/deep-equal.js';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
kind?: 'dark' | 'light'; // legacy
|
||||
props: Record<string, string>;
|
||||
codeHighlighter?: {
|
||||
base: BundledTheme;
|
||||
overrides?: Record<string, any>;
|
||||
} | {
|
||||
base: '_none_';
|
||||
overrides: Record<string, any>;
|
||||
};
|
||||
type ThemeManagerEvents = {
|
||||
'themeChanging': () => void;
|
||||
'themeChanged': () => void;
|
||||
'previewStateChanged': (isPreview: boolean) => void;
|
||||
'requestUpdateThemeCache': (theme: Theme, compiled: CompiledTheme) => void;
|
||||
};
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
class ThemeManager extends EventEmitter<ThemeManagerEvents> {
|
||||
/** 現在常用しているテーマ */
|
||||
private _theme: Theme | null = null;
|
||||
get theme() { return this._theme; }
|
||||
private _compiledTheme: CompiledTheme | null = null;
|
||||
get compiledTheme() { return this._compiledTheme; }
|
||||
|
||||
export const getBuiltinThemes = () => Promise.all(
|
||||
[
|
||||
'l-light',
|
||||
'l-coffee',
|
||||
'l-apricot',
|
||||
'l-rainy',
|
||||
'l-botanical',
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
'l-u0',
|
||||
/** 現在適用中のテーマ */
|
||||
private _currentTheme: Theme | null = null;
|
||||
get currentTheme() { return this._currentTheme; }
|
||||
get currentThemeId() { return this._currentTheme?.id; }
|
||||
private _currentCompiledTheme: CompiledTheme | null = null;
|
||||
get currentCompiledTheme() { return this._currentCompiledTheme; }
|
||||
|
||||
'd-dark',
|
||||
'd-persimmon',
|
||||
'd-astro',
|
||||
'd-future',
|
||||
'd-botanical',
|
||||
'd-green-lime',
|
||||
'd-green-orange',
|
||||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-u0',
|
||||
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
/** プレビュー中かどうか */
|
||||
private _isPreviewMode = false;
|
||||
get isPreviewMode() { return this._isPreviewMode; }
|
||||
set isPreviewMode(value: boolean) {
|
||||
if (this._isPreviewMode !== value) {
|
||||
this._isPreviewMode = value;
|
||||
this.emit('previewStateChanged', value);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuiltinThemesRef() {
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => builtinThemes.value = themes);
|
||||
return builtinThemes;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/** テーマを更新し、同時に適用します。 */
|
||||
public updateTheme(newTheme: Theme) {
|
||||
if (newTheme.id === this.theme?.id && version === miLocalStorage.getItem('themeCachedVersion')) return; // 変更なし
|
||||
|
||||
this.isPreviewMode = false;
|
||||
|
||||
// テーマを更新
|
||||
this._theme = deepClone(newTheme);
|
||||
const compiled = this.compile(newTheme);
|
||||
this._compiledTheme = compiled;
|
||||
|
||||
// 適用中のテーマも更新
|
||||
this._currentTheme = deepClone(this.theme);
|
||||
this._currentCompiledTheme = deepClone(compiled);
|
||||
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
/** プレビュー用のテーマを適用します。 */
|
||||
public previewTheme(theme: Theme) {
|
||||
this.isPreviewMode = true;
|
||||
|
||||
// 適用中のテーマを更新
|
||||
this._currentTheme = deepClone(theme);
|
||||
this._currentCompiledTheme = this.compile(theme);
|
||||
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
/** プレビュー状態を解除し、適用中のテーマを常用しているテーマに戻します。 */
|
||||
public clearPreview() {
|
||||
this.isPreviewMode = false;
|
||||
|
||||
// 適用中のテーマを常用しているテーマに戻す
|
||||
this._currentTheme = deepClone(this.theme);
|
||||
this._currentCompiledTheme = deepClone(this.compiledTheme);
|
||||
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
/** 通常のテーマのコンパイルに加え、ベースとなるテーマの値を解決し代入します。 */
|
||||
private compile(theme: Theme) {
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base != null) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
|
||||
}
|
||||
|
||||
return compile(_theme);
|
||||
}
|
||||
|
||||
/** currentThemeを適用します。 */
|
||||
private applyTheme() {
|
||||
if (this.currentTheme == null || this.currentCompiledTheme == null) return;
|
||||
|
||||
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
|
||||
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
|
||||
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
try {
|
||||
window.document.startViewTransition(async () => {
|
||||
this.updateAttributes();
|
||||
await nextTick();
|
||||
}).finished.then(() => {
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
this.emit('themeChanged');
|
||||
});
|
||||
} catch (err) {
|
||||
// 様々な理由により startViewTransition は失敗することがある
|
||||
// ref. https://github.com/misskey-dev/misskey/issues/16562
|
||||
|
||||
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
|
||||
console.error(err);
|
||||
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
this.updateAttributes();
|
||||
this.emit('themeChanged');
|
||||
}
|
||||
} else {
|
||||
this.updateAttributes();
|
||||
this.emit('themeChanged');
|
||||
}
|
||||
|
||||
if (!this.isPreviewMode) {
|
||||
this.emit('requestUpdateThemeCache', this.currentTheme, this.currentCompiledTheme);
|
||||
}
|
||||
}
|
||||
|
||||
private updateAttributes() {
|
||||
if (!this.currentTheme || !this.currentCompiledTheme) return;
|
||||
|
||||
const colorScheme = this.currentTheme.base === 'dark' ? 'dark' : 'light';
|
||||
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||
|
||||
for (const tag of window.document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', this.currentCompiledTheme['htmlThemeColor']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of themeProps) {
|
||||
const value = this.currentCompiledTheme[key];
|
||||
if (value) {
|
||||
window.document.documentElement.style.setProperty(`--MI_THEME-${key}`, value.toString());
|
||||
} else {
|
||||
window.document.documentElement.style.removeProperty(`--MI_THEME-${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
|
||||
this.emit('themeChanging');
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemesRef(): Ref<Theme[]> {
|
||||
return prefer.r.themes;
|
||||
}
|
||||
export const themeManager = new ThemeManager();
|
||||
export const isPreviewMode = ref(false);
|
||||
|
||||
themeManager.on('requestUpdateThemeCache', (theme, props) => {
|
||||
miLocalStorage.setItem('theme', JSON.stringify(props));
|
||||
miLocalStorage.setItem('themeId', theme.id);
|
||||
miLocalStorage.setItem('themeCachedVersion', version);
|
||||
});
|
||||
|
||||
themeManager.on('previewStateChanged', (preview) => {
|
||||
isPreviewMode.value = preview;
|
||||
});
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
@@ -93,163 +203,6 @@ export async function removeTheme(theme: Theme): Promise<void> {
|
||||
prefer.commit('themes', themes);
|
||||
}
|
||||
|
||||
function applyThemeInternal(theme: Theme, persist: boolean) {
|
||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||
|
||||
// Deep copy
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
|
||||
}
|
||||
|
||||
const props = compile(_theme);
|
||||
|
||||
for (const tag of window.document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', props['htmlThemeColor']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||
}
|
||||
|
||||
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
|
||||
if (persist) {
|
||||
miLocalStorage.setItem('theme', JSON.stringify(props));
|
||||
miLocalStorage.setItem('themeId', theme.id);
|
||||
miLocalStorage.setItem('themeCachedVersion', version);
|
||||
miLocalStorage.setItem('colorScheme', colorScheme);
|
||||
}
|
||||
|
||||
// 色計算など再度行えるようにクライアント全体に通知
|
||||
globalEvents.emit('themeChanging');
|
||||
}
|
||||
|
||||
let timeout: number | null = null;
|
||||
let currentThemeId = miLocalStorage.getItem('themeId');
|
||||
|
||||
export function applyTheme(theme: Theme, persist = true) {
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
|
||||
currentThemeId = theme.id;
|
||||
|
||||
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
|
||||
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
|
||||
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
try {
|
||||
window.document.startViewTransition(async () => {
|
||||
applyThemeInternal(theme, persist);
|
||||
await nextTick();
|
||||
}).finished.then(() => {
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
globalEvents.emit('themeChanged');
|
||||
});
|
||||
} catch (err) {
|
||||
// 様々な理由により startViewTransition は失敗することがある
|
||||
// ref. https://github.com/misskey-dev/misskey/issues/16562
|
||||
|
||||
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
|
||||
|
||||
console.error(err);
|
||||
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
applyThemeInternal(theme, persist);
|
||||
globalEvents.emit('themeChanged');
|
||||
}
|
||||
} else {
|
||||
applyThemeInternal(theme, persist);
|
||||
globalEvents.emit('themeChanged');
|
||||
}
|
||||
}
|
||||
|
||||
export function compile(theme: Theme): Record<string, string> {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getColor(theme.props[val.substring(1)]);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
const props = {} as Record<string, string>;
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
return c.toRgbString();
|
||||
}
|
||||
|
||||
export function validateTheme(theme: Record<string, any>): boolean {
|
||||
if (theme.id == null || typeof theme.id !== 'string') return false;
|
||||
if (theme.name == null || typeof theme.name !== 'string') return false;
|
||||
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
|
||||
if (theme.props == null || typeof theme.props !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseThemeCode(code: string): Theme {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (_) {
|
||||
throw new Error('Failed to parse theme json');
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
if (prefer.s.themes.some(t => t.id === theme.id)) {
|
||||
throw new Error('This theme is already installed');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function previewTheme(code: string): void {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme != null) applyTheme(theme, false);
|
||||
}
|
||||
|
||||
export async function installTheme(code: string): Promise<void> {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme == null) return;
|
||||
@@ -261,3 +214,26 @@ export function clearAppliedThemeCache() {
|
||||
miLocalStorage.removeItem('themeId');
|
||||
miLocalStorage.removeItem('themeCachedVersion');
|
||||
}
|
||||
|
||||
export function handleThemeInstallError(err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
let message = '';
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
case 'already exists':
|
||||
case 'builtin theme':
|
||||
message = i18n.ts._theme.alreadyInstalled;
|
||||
break;
|
||||
default:
|
||||
message = i18n.ts._theme.invalid;
|
||||
break;
|
||||
}
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
57
packages/frontend/src/ui/_common_/ThemePreviewing.vue
Normal file
57
packages/frontend/src/ui/_common_/ThemePreviewing.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<span :class="$style.icon">
|
||||
<i class="ti ti-info-circle"></i>
|
||||
</span>
|
||||
<span :class="$style.title">{{ i18n.ts.previewingTheme }}</span>
|
||||
<span :class="$style.body"><button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="restore">{{ i18n.ts.previewingThemeRestore }}</button> | <MkA class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" to="/settings/theme">{{ i18n.ts.settings }}</MkA></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
|
||||
function restore() {
|
||||
themeManager.clearPreview();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
--height: 24px;
|
||||
font-size: 0.85em;
|
||||
display: flex;
|
||||
vertical-align: bottom;
|
||||
width: 100%;
|
||||
line-height: var(--height);
|
||||
height: var(--height);
|
||||
overflow: clip;
|
||||
contain: strict;
|
||||
background: var(--MI_THEME-accent);
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10px;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: clip;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<XReloadSuggestion v-if="shouldSuggestReload"/>
|
||||
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
|
||||
<XThemePreviewing v-if="isThemePreviewMode"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
<XStatusBars/>
|
||||
<div :class="$style.columnsWrapper">
|
||||
@@ -94,12 +95,14 @@ import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
||||
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
||||
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
||||
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
|
||||
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
|
||||
import XMainColumn from '@/ui/deck/main-column.vue';
|
||||
import XTlColumn from '@/ui/deck/tl-column.vue';
|
||||
import XAntennaColumn from '@/ui/deck/antenna-column.vue';
|
||||
|
||||
@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>
|
||||
<XReloadSuggestion v-if="shouldSuggestReload"/>
|
||||
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
|
||||
<XThemePreviewing v-if="isThemePreviewMode"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
<XStatusBars :class="$style.statusbars"/>
|
||||
</div>
|
||||
@@ -40,8 +41,10 @@ import type { PageMetadata } from '@/page.js';
|
||||
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
||||
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
||||
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
|
||||
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
|
||||
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
||||
import XSidebar from '@/ui/_common_/navbar.vue';
|
||||
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
|
||||
_res.type = mode;
|
||||
|
||||
if (getName) {
|
||||
return _res.name;
|
||||
return _res.name!;
|
||||
}
|
||||
return _res;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { store } from '@/store.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
@@ -50,7 +51,7 @@ export function initChart() {
|
||||
);
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-fg');
|
||||
Chart.defaults.color = themeManager.currentCompiledTheme!.fg;
|
||||
|
||||
Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ export function popout(path: string, w?: HTMLElement) {
|
||||
url = appendQuery(url, 'zen');
|
||||
if (w) {
|
||||
const position = w.getBoundingClientRect();
|
||||
const width = parseInt(getComputedStyle(w, '').width, 10);
|
||||
const height = parseInt(getComputedStyle(w, '').height, 10);
|
||||
const computedStyle = getComputedStyle(w, '');
|
||||
const width = parseInt(computedStyle.width, 10);
|
||||
const height = parseInt(computedStyle.height, 10);
|
||||
const x = window.screenX + position.left;
|
||||
const y = window.screenY + position.top;
|
||||
window.open(url, url,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { themeProps } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import { themeProps } from '@@/js/theme.js';
|
||||
|
||||
export type Default = null;
|
||||
export type Color = string;
|
||||
|
||||
188
packages/frontend/test/theme.test.ts
Normal file
188
packages/frontend/test/theme.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { afterEach, assert, beforeEach, describe, test, vi } from 'vitest';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import './init';
|
||||
|
||||
vi.mock('@/i18n.js', () => ({
|
||||
i18n: {
|
||||
ts: {
|
||||
_theme: {
|
||||
alreadyInstalled: 'already installed',
|
||||
invalid: 'invalid',
|
||||
},
|
||||
},
|
||||
},
|
||||
updateI18n: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/os.js', () => ({
|
||||
alert: vi.fn(),
|
||||
}));
|
||||
|
||||
const cloneTheme = <T>(value: T): T => structuredClone(value);
|
||||
|
||||
const createTheme = (base: 'light' | 'dark', options: {
|
||||
id: string;
|
||||
name: string;
|
||||
accent: string;
|
||||
bg: string;
|
||||
fg: string;
|
||||
}): Theme => {
|
||||
const builtin = base === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
return {
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
author: 'tester',
|
||||
base,
|
||||
props: {
|
||||
...cloneTheme(builtin.props),
|
||||
accent: options.accent,
|
||||
bg: options.bg,
|
||||
fg: options.fg,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const primaryTheme = createTheme('light', {
|
||||
id: 'primary-theme',
|
||||
name: 'Primary Theme',
|
||||
accent: '#224488',
|
||||
bg: '#faf7f2',
|
||||
fg: '#1a1a1a',
|
||||
});
|
||||
|
||||
const previewTheme = createTheme('dark', {
|
||||
id: 'preview-theme',
|
||||
name: 'Preview Theme',
|
||||
accent: '#55aa33',
|
||||
bg: '#101820',
|
||||
fg: '#f4f4f4',
|
||||
});
|
||||
|
||||
const replacementTheme = createTheme('dark', {
|
||||
id: 'replacement-theme',
|
||||
name: 'Replacement Theme',
|
||||
accent: '#bb5500',
|
||||
bg: '#18110f',
|
||||
fg: '#f6e7df',
|
||||
});
|
||||
|
||||
const loadThemeModule = async () => {
|
||||
vi.resetModules();
|
||||
return await import('@/theme.js');
|
||||
};
|
||||
|
||||
const resetDocument = () => {
|
||||
window.localStorage.clear();
|
||||
document.head.innerHTML = '<meta name="theme-color" content="#000000">';
|
||||
document.documentElement.className = '';
|
||||
document.documentElement.removeAttribute('data-color-scheme');
|
||||
document.documentElement.style.cssText = '';
|
||||
Reflect.deleteProperty(document, 'startViewTransition');
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
configurable: true,
|
||||
value: 'visible',
|
||||
});
|
||||
};
|
||||
|
||||
describe('ThemeManager', () => {
|
||||
beforeEach(() => {
|
||||
resetDocument();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test('通常テーマ適用後のプレビューは現在テーマのみを切り替え、キャッシュは保持する', async () => {
|
||||
const { themeManager, isPreviewMode } = await loadThemeModule();
|
||||
|
||||
themeManager.updateTheme(primaryTheme);
|
||||
const cachedTheme = window.localStorage.getItem('theme');
|
||||
const cachedThemeId = window.localStorage.getItem('themeId');
|
||||
|
||||
themeManager.previewTheme(previewTheme);
|
||||
|
||||
assert.strictEqual(themeManager.theme?.id, primaryTheme.id);
|
||||
assert.strictEqual(themeManager.currentTheme?.id, previewTheme.id);
|
||||
assert.strictEqual(themeManager.currentThemeId, previewTheme.id);
|
||||
assert.strictEqual(themeManager.isPreviewMode, true);
|
||||
assert.strictEqual(isPreviewMode.value, true);
|
||||
assert.strictEqual(document.documentElement.dataset.colorScheme, 'dark');
|
||||
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
|
||||
assert.strictEqual(window.localStorage.getItem('theme'), cachedTheme);
|
||||
assert.strictEqual(window.localStorage.getItem('themeId'), cachedThemeId);
|
||||
});
|
||||
|
||||
test('プレビュー解除で元のテーマと DOM 状態が復元される', async () => {
|
||||
const { themeManager, isPreviewMode } = await loadThemeModule();
|
||||
|
||||
themeManager.updateTheme(primaryTheme);
|
||||
const originalCompiledThemeColor = themeManager.currentCompiledTheme?.htmlThemeColor;
|
||||
|
||||
themeManager.previewTheme(previewTheme);
|
||||
const previewCompiledThemeColor = themeManager.currentCompiledTheme?.htmlThemeColor;
|
||||
assert.strictEqual(themeManager.currentTheme?.id, previewTheme.id);
|
||||
assert.notStrictEqual(previewCompiledThemeColor, originalCompiledThemeColor);
|
||||
|
||||
themeManager.clearPreview();
|
||||
|
||||
assert.strictEqual(themeManager.theme?.id, primaryTheme.id);
|
||||
assert.strictEqual(themeManager.currentTheme?.id, primaryTheme.id);
|
||||
assert.strictEqual(themeManager.currentCompiledTheme?.htmlThemeColor, originalCompiledThemeColor);
|
||||
assert.strictEqual(themeManager.isPreviewMode, false);
|
||||
assert.strictEqual(isPreviewMode.value, false);
|
||||
assert.strictEqual(document.documentElement.dataset.colorScheme, 'light');
|
||||
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
|
||||
assert.strictEqual(document.head.querySelector('meta[name="theme-color"]')?.getAttribute('content'), originalCompiledThemeColor);
|
||||
assert.strictEqual(window.localStorage.getItem('themeId'), primaryTheme.id);
|
||||
});
|
||||
|
||||
test('プレビュー中に通常テーマを更新するとプレビューを抜けて新しい通常テーマが適用される', async () => {
|
||||
const { themeManager, isPreviewMode } = await loadThemeModule();
|
||||
|
||||
themeManager.updateTheme(primaryTheme);
|
||||
themeManager.previewTheme(previewTheme);
|
||||
themeManager.updateTheme(replacementTheme);
|
||||
|
||||
assert.strictEqual(themeManager.theme?.id, replacementTheme.id);
|
||||
assert.strictEqual(themeManager.currentTheme?.id, replacementTheme.id);
|
||||
assert.strictEqual(themeManager.isPreviewMode, false);
|
||||
assert.strictEqual(isPreviewMode.value, false);
|
||||
assert.strictEqual(document.documentElement.dataset.colorScheme, 'dark');
|
||||
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
|
||||
assert.strictEqual(window.localStorage.getItem('themeId'), replacementTheme.id);
|
||||
});
|
||||
|
||||
test('themeChanging と themeChanged はプレビュー適用と復帰のたびに発火する', async () => {
|
||||
const { themeManager } = await loadThemeModule();
|
||||
const events: string[] = [];
|
||||
|
||||
themeManager.on('themeChanging', () => {
|
||||
events.push('themeChanging');
|
||||
});
|
||||
themeManager.on('themeChanged', () => {
|
||||
events.push('themeChanged');
|
||||
});
|
||||
|
||||
themeManager.updateTheme(primaryTheme);
|
||||
themeManager.previewTheme(previewTheme);
|
||||
themeManager.clearPreview();
|
||||
|
||||
assert.deepStrictEqual(events, [
|
||||
'themeChanging',
|
||||
'themeChanged',
|
||||
'themeChanging',
|
||||
'themeChanged',
|
||||
'themeChanging',
|
||||
'themeChanged',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
"@/*": ["../src/*"],
|
||||
"@@/*": ["../../frontend-shared/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"../node_modules/@types"
|
||||
@@ -37,6 +38,7 @@
|
||||
"compileOnSave": false,
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"../src/**/*.vue"
|
||||
"../src/**/*.vue",
|
||||
"../@types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"chokidar": "5.0.0",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
|
||||
@@ -5651,6 +5651,14 @@ export interface Locale extends ILocale {
|
||||
* リノート先のチャンネルを見る
|
||||
*/
|
||||
"viewRenotedChannel": string;
|
||||
/**
|
||||
* テーマのプレビュー中
|
||||
*/
|
||||
"previewingTheme": string;
|
||||
/**
|
||||
* 元に戻す
|
||||
*/
|
||||
"previewingThemeRestore": string;
|
||||
"_imageEditing": {
|
||||
"_vars": {
|
||||
/**
|
||||
@@ -8177,6 +8185,10 @@ export interface Locale extends ILocale {
|
||||
* 翻訳機能の利用
|
||||
*/
|
||||
"canUseTranslator": string;
|
||||
/**
|
||||
* チャンネルの作成
|
||||
*/
|
||||
"canCreateChannel": string;
|
||||
/**
|
||||
* アイコンデコレーションの最大取付個数
|
||||
*/
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "24.12.2",
|
||||
"@types/wawoff2": "1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2"
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
"@types/matter-js": "0.20.2",
|
||||
"@types/node": "24.12.2",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.14"
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
```ts
|
||||
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/browser';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { Options } from 'reconnecting-websocket';
|
||||
import type { PublicKeyCredentialRequestOptionsJSON as PublicKeyCredentialRequestOptionsJSON_2 } from '@simplewebauthn/types';
|
||||
import type { PublicKeyCredentialCreationOptionsJSON as PublicKeyCredentialCreationOptionsJSON_2 } from '@simplewebauthn/browser';
|
||||
import type { PublicKeyCredentialRequestOptionsJSON as PublicKeyCredentialRequestOptionsJSON_2 } from '@simplewebauthn/browser';
|
||||
import _ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import type { RegistrationResponseJSON } from '@simplewebauthn/browser';
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
@@ -1471,6 +1473,14 @@ export type Endpoints = Overwrite<Endpoints_2, {
|
||||
};
|
||||
};
|
||||
};
|
||||
'i/2fa/register-key': {
|
||||
req: I2faRegisterKeyRequest;
|
||||
res: I2faRegisterKeyResponse_2;
|
||||
};
|
||||
'i/2fa/key-done': {
|
||||
req: I2faKeyDoneRequest_2;
|
||||
res: I2faKeyDoneResponse;
|
||||
};
|
||||
'admin/roles/create': {
|
||||
req: Overwrite<AdminRolesCreateRequest, {
|
||||
policies: PartialRolePolicyOverride;
|
||||
@@ -1510,6 +1520,8 @@ declare namespace entities {
|
||||
SigninWithPasskeyRequest,
|
||||
SigninWithPasskeyInitResponse,
|
||||
SigninWithPasskeyResponse,
|
||||
I2faRegisterKeyResponse_2 as I2faRegisterKeyResponse,
|
||||
I2faKeyDoneRequest_2 as I2faKeyDoneRequest,
|
||||
PartialRolePolicyOverride,
|
||||
EmptyRequest,
|
||||
EmptyResponse,
|
||||
@@ -1911,13 +1923,11 @@ declare namespace entities {
|
||||
IResponse,
|
||||
I2faDoneRequest,
|
||||
I2faDoneResponse,
|
||||
I2faKeyDoneRequest,
|
||||
I2faKeyDoneResponse,
|
||||
I2faPasswordLessRequest,
|
||||
I2faRegisterRequest,
|
||||
I2faRegisterResponse,
|
||||
I2faRegisterKeyRequest,
|
||||
I2faRegisterKeyResponse,
|
||||
I2faRemoveKeyRequest,
|
||||
I2faUnregisterRequest,
|
||||
I2faUpdateKeyRequest,
|
||||
@@ -2515,7 +2525,12 @@ type I2faDoneRequest = operations['i___2fa___done']['requestBody']['content']['a
|
||||
type I2faDoneResponse = operations['i___2fa___done']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type I2faKeyDoneRequest = operations['i___2fa___key-done']['requestBody']['content']['application/json'];
|
||||
type I2faKeyDoneRequest_2 = {
|
||||
password: string;
|
||||
token?: string | null;
|
||||
name: string;
|
||||
credential: RegistrationResponseJSON;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
type I2faKeyDoneResponse = operations['i___2fa___key-done']['responses']['200']['content']['application/json'];
|
||||
@@ -2527,7 +2542,7 @@ type I2faPasswordLessRequest = operations['i___2fa___password-less']['requestBod
|
||||
type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json'];
|
||||
type I2faRegisterKeyResponse_2 = PublicKeyCredentialCreationOptionsJSON_2;
|
||||
|
||||
// @public (undocumented)
|
||||
type I2faRegisterRequest = operations['i___2fa___register']['requestBody']['content']['application/json'];
|
||||
@@ -3466,7 +3481,7 @@ type RoleLite = components['schemas']['RoleLite'];
|
||||
type RolePolicies = components['schemas']['RolePolicies'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"];
|
||||
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "canCreateChannel", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"];
|
||||
|
||||
// @public (undocumented)
|
||||
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];
|
||||
@@ -3880,7 +3895,7 @@ type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/entities.ts:55:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:60:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:226:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:241:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"devDependencies": {
|
||||
"@readme/openapi-parser": "6.0.1",
|
||||
"@types/node": "24.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"openapi-types": "12.1.3",
|
||||
"openapi-typescript": "7.13.0",
|
||||
"ts-case-convert": "2.1.0",
|
||||
|
||||
@@ -414,7 +414,7 @@ async function main() {
|
||||
await generateEndpoints(openApiDocs, typeFileName, entitiesFileName, endpointFileName);
|
||||
|
||||
const apiClientWarningFileName = `${generatePath}/apiClientJSDoc.ts`;
|
||||
await generateApiClientJSDoc(openApiDocs, '../api.ts', endpointFileName, apiClientWarningFileName);
|
||||
await generateApiClientJSDoc(openApiDocs, '../api.ts', '../api.types.ts', apiClientWarningFileName);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user