Compare commits
17 Commits
e714b3fb51
...
5a1d4a59e8
| Author | SHA1 | Date |
|---|---|---|
|
|
5a1d4a59e8 | |
|
|
fc7655c808 | |
|
|
ae2ac9d50f | |
|
|
8932492fd3 | |
|
|
a168e7b648 | |
|
|
1adcb03b93 | |
|
|
b6e737dc76 | |
|
|
2fa6ecc7ef | |
|
|
f744b5711f | |
|
|
2b3d72bb73 | |
|
|
3205eb6925 | |
|
|
d4fcc694a6 | |
|
|
389861f1da | |
|
|
ec683f04d1 | |
|
|
bd81a6c8ad | |
|
|
d8318c02a1 | |
|
|
b941c896aa |
|
|
@ -54,55 +54,110 @@ jobs:
|
||||||
BASE_MEMORY=$(cat ./artifacts/memory-base.json)
|
BASE_MEMORY=$(cat ./artifacts/memory-base.json)
|
||||||
HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
|
HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
|
||||||
|
|
||||||
BASE_RSS=$(echo "$BASE_MEMORY" | jq -r '.memory.rss // 0')
|
variation() {
|
||||||
HEAD_RSS=$(echo "$HEAD_MEMORY" | jq -r '.memory.rss // 0')
|
calc() {
|
||||||
|
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||||
|
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||||
|
|
||||||
# Calculate difference
|
DIFF=$((HEAD - BASE))
|
||||||
if [ "$BASE_RSS" -gt 0 ] && [ "$HEAD_RSS" -gt 0 ]; then
|
if [ "$BASE" -gt 0 ]; then
|
||||||
DIFF=$((HEAD_RSS - BASE_RSS))
|
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
|
||||||
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE_RSS" | bc)
|
|
||||||
|
|
||||||
# Convert to MB for readability
|
|
||||||
BASE_MB=$(echo "scale=2; $BASE_RSS / 1048576" | bc)
|
|
||||||
HEAD_MB=$(echo "scale=2; $HEAD_RSS / 1048576" | bc)
|
|
||||||
DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc)
|
|
||||||
|
|
||||||
echo "base_mb=$BASE_MB" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "head_mb=$HEAD_MB" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "diff_mb=$DIFF_MB" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "diff_percent=$DIFF_PERCENT" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_data=true" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Determine if this is a significant change (more than 5% increase)
|
|
||||||
if [ "$(echo "$DIFF_PERCENT > 5" | bc)" -eq 1 ]; then
|
|
||||||
echo "significant_increase=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
else
|
||||||
echo "significant_increase=false" >> "$GITHUB_OUTPUT"
|
DIFF_PERCENT=0
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_data=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Convert KB to MB for readability
|
||||||
|
BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
|
||||||
|
HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
|
||||||
|
DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
|
||||||
|
|
||||||
|
JSON=$(jq -c -n \
|
||||||
|
--argjson base "$BASE_MB" \
|
||||||
|
--argjson head "$HEAD_MB" \
|
||||||
|
--argjson diff "$DIFF_MB" \
|
||||||
|
--argjson diff_percent "$DIFF_PERCENT" \
|
||||||
|
'{base: $base, head: $head, diff: $diff, diff_percent: $diff_percent}')
|
||||||
|
|
||||||
|
echo "$JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON=$(jq -c -n \
|
||||||
|
--argjson VmRSS "$(calc $1 VmRSS)" \
|
||||||
|
--argjson VmHWM "$(calc $1 VmHWM)" \
|
||||||
|
--argjson VmSize "$(calc $1 VmSize)" \
|
||||||
|
--argjson VmData "$(calc $1 VmData)" \
|
||||||
|
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
|
||||||
|
|
||||||
|
echo "$JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON=$(jq -c -n \
|
||||||
|
--argjson beforeGc "$(variation beforeGc)" \
|
||||||
|
--argjson afterGc "$(variation afterGc)" \
|
||||||
|
--argjson afterRequest "$(variation afterRequest)" \
|
||||||
|
'{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
|
||||||
|
|
||||||
|
echo "res=$JSON" >> "$GITHUB_OUTPUT"
|
||||||
- id: build-comment
|
- id: build-comment
|
||||||
name: Build memory comment
|
name: Build memory comment
|
||||||
|
env:
|
||||||
|
RES: ${{ steps.compare.outputs.res }}
|
||||||
run: |
|
run: |
|
||||||
HEADER="## Backend Memory Usage Comparison"
|
HEADER="## Backend memory usage comparison"
|
||||||
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
|
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
|
||||||
|
|
||||||
echo "$HEADER" > ./output.md
|
echo "$HEADER" > ./output.md
|
||||||
echo >> ./output.md
|
echo >> ./output.md
|
||||||
|
|
||||||
if [ "${{ steps.compare.outputs.has_data }}" == "true" ]; then
|
table() {
|
||||||
echo "| Metric | base | head | Diff |" >> ./output.md
|
echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
|
||||||
echo "|--------|------|------|------|" >> ./output.md
|
echo "|--------|------:|------:|------:|------:|" >> ./output.md
|
||||||
echo "| RSS | ${{ steps.compare.outputs.base_mb }} MB | ${{ steps.compare.outputs.head_mb }} MB | ${{ steps.compare.outputs.diff_mb }} MB (${{ steps.compare.outputs.diff_percent }}%) |" >> ./output.md
|
|
||||||
|
line() {
|
||||||
|
METRIC=$2
|
||||||
|
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
|
||||||
|
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
|
||||||
|
DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
|
||||||
|
DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
|
||||||
|
|
||||||
|
if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
|
||||||
|
DIFF="+$DIFF"
|
||||||
|
DIFF_PERCENT="+$DIFF_PERCENT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# highlight VmRSS
|
||||||
|
if [ "$2" = "VmRSS" ]; then
|
||||||
|
METRIC="**${METRIC}**"
|
||||||
|
BASE="**${BASE}**"
|
||||||
|
HEAD="**${HEAD}**"
|
||||||
|
DIFF="**${DIFF}**"
|
||||||
|
DIFF_PERCENT="**${DIFF_PERCENT}**"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
|
||||||
|
}
|
||||||
|
|
||||||
|
line $1 VmRSS
|
||||||
|
line $1 VmHWM
|
||||||
|
line $1 VmSize
|
||||||
|
line $1 VmData
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "### Before GC" >> ./output.md
|
||||||
|
table beforeGc
|
||||||
echo >> ./output.md
|
echo >> ./output.md
|
||||||
|
|
||||||
if [ "${{ steps.compare.outputs.significant_increase }}" == "true" ]; then
|
echo "### After GC" >> ./output.md
|
||||||
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
table afterGc
|
||||||
echo >> ./output.md
|
echo >> ./output.md
|
||||||
fi
|
|
||||||
else
|
echo "### After Request" >> ./output.md
|
||||||
echo "Could not retrieve memory usage data." >> ./output.md
|
table afterRequest
|
||||||
|
echo >> ./output.md
|
||||||
|
|
||||||
|
# Determine if this is a significant change (more than 5% increase)
|
||||||
|
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
|
||||||
|
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||||
echo >> ./output.md
|
echo >> ./output.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
26
package.json
26
package.json
|
|
@ -6,7 +6,7 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.27.0",
|
"packageManager": "pnpm@10.28.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/misskey-js",
|
"packages/misskey-js",
|
||||||
"packages/i18n",
|
"packages/i18n",
|
||||||
|
|
@ -52,10 +52,6 @@
|
||||||
"clean-all": "node scripts/clean-all.mjs",
|
"clean-all": "node scripts/clean-all.mjs",
|
||||||
"cleanall": "pnpm clean-all"
|
"cleanall": "pnpm clean-all"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"chokidar": "5.0.0",
|
|
||||||
"lodash": "4.17.21"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano": "7.1.2",
|
"cssnano": "7.1.2",
|
||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.2",
|
||||||
|
|
@ -63,23 +59,23 @@
|
||||||
"ignore-walk": "8.0.0",
|
"ignore-walk": "8.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"tar": "7.5.2",
|
"tar": "7.5.6",
|
||||||
"terser": "5.44.1"
|
"terser": "5.46.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.39.2",
|
"@eslint/js": "9.39.2",
|
||||||
"@misskey-dev/eslint-plugin": "2.2.0",
|
"@misskey-dev/eslint-plugin": "2.2.0",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251226.1",
|
"@typescript/native-preview": "7.0.0-dev.20260116.1",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"cypress": "15.8.1",
|
"cypress": "15.9.0",
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.2",
|
||||||
"globals": "16.5.0",
|
"globals": "16.5.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"pnpm": "10.27.0",
|
"pnpm": "10.28.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"start-server-and-test": "2.1.3"
|
"start-server-and-test": "2.1.3"
|
||||||
},
|
},
|
||||||
|
|
@ -88,7 +84,9 @@
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@aiscript-dev/aiscript-languageserver": "-"
|
"@aiscript-dev/aiscript-languageserver": "-",
|
||||||
|
"chokidar": "5.0.0",
|
||||||
|
"lodash": "4.17.23"
|
||||||
},
|
},
|
||||||
"ignoredBuiltDependencies": [
|
"ignoredBuiltDependencies": [
|
||||||
"@sentry-internal/node-cpu-profiler",
|
"@sentry-internal/node-cpu-profiler",
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,17 @@
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
"@swc/core-darwin-arm64": "1.15.7",
|
"@swc/core-darwin-arm64": "1.15.8",
|
||||||
"@swc/core-darwin-x64": "1.15.7",
|
"@swc/core-darwin-x64": "1.15.8",
|
||||||
"@swc/core-freebsd-x64": "1.3.11",
|
"@swc/core-freebsd-x64": "1.3.11",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.15.7",
|
"@swc/core-linux-arm-gnueabihf": "1.15.8",
|
||||||
"@swc/core-linux-arm64-gnu": "1.15.7",
|
"@swc/core-linux-arm64-gnu": "1.15.8",
|
||||||
"@swc/core-linux-arm64-musl": "1.15.7",
|
"@swc/core-linux-arm64-musl": "1.15.8",
|
||||||
"@swc/core-linux-x64-gnu": "1.15.7",
|
"@swc/core-linux-x64-gnu": "1.15.8",
|
||||||
"@swc/core-linux-x64-musl": "1.15.7",
|
"@swc/core-linux-x64-musl": "1.15.8",
|
||||||
"@swc/core-win32-arm64-msvc": "1.15.7",
|
"@swc/core-win32-arm64-msvc": "1.15.8",
|
||||||
"@swc/core-win32-ia32-msvc": "1.15.7",
|
"@swc/core-win32-ia32-msvc": "1.15.8",
|
||||||
"@swc/core-win32-x64-msvc": "1.15.7",
|
"@swc/core-win32-x64-msvc": "1.15.8",
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"@tensorflow/tfjs": "4.22.0",
|
||||||
"@tensorflow/tfjs-node": "4.22.0",
|
"@tensorflow/tfjs-node": "4.22.0",
|
||||||
"bufferutil": "4.1.0",
|
"bufferutil": "4.1.0",
|
||||||
|
|
@ -71,40 +71,39 @@
|
||||||
"utf-8-validate": "6.0.6"
|
"utf-8-validate": "6.0.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.958.0",
|
"@aws-sdk/client-s3": "3.970.0",
|
||||||
"@aws-sdk/lib-storage": "3.958.0",
|
"@aws-sdk/lib-storage": "3.970.0",
|
||||||
"@discordapp/twemoji": "16.0.1",
|
"@discordapp/twemoji": "16.0.1",
|
||||||
"@fastify/accepts": "5.0.4",
|
"@fastify/accepts": "5.0.4",
|
||||||
"@fastify/cors": "11.2.0",
|
"@fastify/cors": "11.2.0",
|
||||||
"@fastify/express": "4.0.2",
|
"@fastify/express": "4.0.4",
|
||||||
"@fastify/http-proxy": "11.4.1",
|
"@fastify/http-proxy": "11.4.1",
|
||||||
"@fastify/multipart": "9.3.0",
|
"@fastify/multipart": "9.3.0",
|
||||||
"@fastify/static": "8.3.0",
|
"@fastify/static": "8.3.0",
|
||||||
"@kitajs/html": "4.2.11",
|
"@kitajs/html": "4.2.11",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.2.5",
|
"@misskey-dev/summaly": "5.2.5",
|
||||||
"@napi-rs/canvas": "0.1.87",
|
"@napi-rs/canvas": "0.1.88",
|
||||||
"@nestjs/common": "11.1.10",
|
"@nestjs/common": "11.1.12",
|
||||||
"@nestjs/core": "11.1.10",
|
"@nestjs/core": "11.1.12",
|
||||||
"@nestjs/testing": "11.1.10",
|
"@nestjs/testing": "11.1.12",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "10.32.1",
|
"@sentry/node": "10.34.0",
|
||||||
"@sentry/profiling-node": "10.32.1",
|
"@sentry/profiling-node": "10.34.0",
|
||||||
"@simplewebauthn/server": "13.2.2",
|
"@simplewebauthn/server": "13.2.2",
|
||||||
"@sinonjs/fake-timers": "15.1.0",
|
"@sinonjs/fake-timers": "15.1.0",
|
||||||
"@smithy/node-http-handler": "4.4.7",
|
"@smithy/node-http-handler": "4.4.8",
|
||||||
"@swc/cli": "0.7.9",
|
"@swc/cli": "0.7.10",
|
||||||
"@swc/core": "1.15.7",
|
"@swc/core": "1.15.8",
|
||||||
"@twemoji/parser": "16.0.0",
|
"@twemoji/parser": "16.0.0",
|
||||||
"@types/redis-info": "3.0.3",
|
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bcryptjs": "3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "2.2.1",
|
"body-parser": "2.2.2",
|
||||||
"bullmq": "5.66.3",
|
"bullmq": "5.66.5",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"chalk-template": "1.1.2",
|
"chalk-template": "1.1.2",
|
||||||
|
|
@ -113,24 +112,24 @@
|
||||||
"content-disposition": "1.0.1",
|
"content-disposition": "1.0.1",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fastify": "5.6.2",
|
"fastify": "5.7.1",
|
||||||
"fastify-raw-body": "5.0.0",
|
"fastify-raw-body": "5.0.0",
|
||||||
"feed": "5.1.0",
|
"feed": "5.2.0",
|
||||||
"file-type": "21.2.0",
|
"file-type": "21.3.0",
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.5",
|
"form-data": "4.0.5",
|
||||||
"got": "14.6.5",
|
"got": "14.6.6",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"http-link-header": "1.1.3",
|
"http-link-header": "1.1.3",
|
||||||
"i18n": "workspace:*",
|
"i18n": "workspace:*",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.9.2",
|
||||||
"ip-cidr": "4.0.2",
|
"ip-cidr": "4.0.2",
|
||||||
"ipaddr.js": "2.3.0",
|
"ipaddr.js": "2.3.0",
|
||||||
"is-svg": "6.1.0",
|
"is-svg": "6.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "9.0.0",
|
"jsonld": "9.0.0",
|
||||||
"juice": "11.0.3",
|
"juice": "11.1.0",
|
||||||
"meilisearch": "0.54.0",
|
"meilisearch": "0.55.0",
|
||||||
"mfm-js": "0.25.0",
|
"mfm-js": "0.25.0",
|
||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
|
|
@ -139,14 +138,14 @@
|
||||||
"nanoid": "5.1.6",
|
"nanoid": "5.1.6",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-html-parser": "7.0.1",
|
"node-html-parser": "7.0.2",
|
||||||
"nodemailer": "7.0.12",
|
"nodemailer": "7.0.12",
|
||||||
"nsfwjs": "4.2.0",
|
"nsfwjs": "4.2.0",
|
||||||
"oauth2orize": "1.12.0",
|
"oauth2orize": "1.12.0",
|
||||||
"oauth2orize-pkce": "0.1.2",
|
"oauth2orize-pkce": "0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.4.1",
|
"otpauth": "9.4.1",
|
||||||
"pg": "8.16.3",
|
"pg": "8.17.1",
|
||||||
"pkce-challenge": "5.0.1",
|
"pkce-challenge": "5.0.1",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
|
|
@ -154,7 +153,6 @@
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.23.0",
|
"re2": "1.23.0",
|
||||||
"redis-info": "3.1.0",
|
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
"rss-parser": "3.13.0",
|
"rss-parser": "3.13.0",
|
||||||
|
|
@ -166,7 +164,7 @@
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"systeminformation": "5.28.1",
|
"systeminformation": "5.30.5",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.5",
|
"tmp": "0.2.5",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
|
|
@ -174,14 +172,14 @@
|
||||||
"ulid": "3.0.2",
|
"ulid": "3.0.2",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.7",
|
"web-push": "3.6.7",
|
||||||
"ws": "8.18.3",
|
"ws": "8.19.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@kitajs/ts-html-plugin": "4.1.3",
|
"@kitajs/ts-html-plugin": "4.1.3",
|
||||||
"@nestjs/platform-express": "11.1.10",
|
"@nestjs/platform-express": "11.1.12",
|
||||||
"@sentry/vue": "10.32.1",
|
"@sentry/vue": "10.34.0",
|
||||||
"@simplewebauthn/types": "12.0.0",
|
"@simplewebauthn/types": "12.0.0",
|
||||||
"@swc/jest": "0.2.39",
|
"@swc/jest": "0.2.39",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
|
|
@ -195,8 +193,8 @@
|
||||||
"@types/jsonld": "1.5.15",
|
"@types/jsonld": "1.5.15",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/ms": "2.1.0",
|
"@types/ms": "2.1.0",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@types/nodemailer": "7.0.4",
|
"@types/nodemailer": "7.0.5",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.16.0",
|
"@types/pg": "8.16.0",
|
||||||
|
|
@ -214,22 +212,22 @@
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.4",
|
"@types/web-push": "3.6.4",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"aws-sdk-client-mock": "4.1.0",
|
"aws-sdk-client-mock": "4.1.0",
|
||||||
"cbor": "10.0.11",
|
"cbor": "10.0.11",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"esbuild-plugin-swc": "1.0.1",
|
"esbuild-plugin-swc": "1.0.1",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"execa": "9.6.1",
|
"execa": "9.6.1",
|
||||||
"fkill": "10.0.1",
|
"fkill": "10.0.3",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-mock": "29.7.0",
|
"jest-mock": "29.7.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"nodemon": "3.1.11",
|
"nodemon": "3.1.11",
|
||||||
"pid-port": "2.0.0",
|
"pid-port": "2.0.1",
|
||||||
"simple-oauth2": "5.1.0",
|
"simple-oauth2": "5.1.0",
|
||||||
"supertest": "7.1.4",
|
"supertest": "7.2.2",
|
||||||
"vite": "7.3.0"
|
"vite": "7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,56 @@ import { fork } from 'node:child_process';
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const SAMPLE_COUNT = 3; // Number of samples to measure
|
||||||
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
||||||
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
||||||
|
|
||||||
async function measureMemory() {
|
const keys = {
|
||||||
const startTime = Date.now();
|
VmPeak: 0,
|
||||||
|
VmSize: 0,
|
||||||
|
VmHWM: 0,
|
||||||
|
VmRSS: 0,
|
||||||
|
VmData: 0,
|
||||||
|
VmStk: 0,
|
||||||
|
VmExe: 0,
|
||||||
|
VmLib: 0,
|
||||||
|
VmPTE: 0,
|
||||||
|
VmSwap: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getMemoryUsage(pid) {
|
||||||
|
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const key of Object.keys(keys)) {
|
||||||
|
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
|
||||||
|
if (match) {
|
||||||
|
result[key] = parseInt(match[1], 10);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureMemory() {
|
||||||
// Start the Misskey backend server using fork to enable IPC
|
// Start the Misskey backend server using fork to enable IPC
|
||||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
|
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
|
||||||
cwd: join(__dirname, '..'),
|
cwd: join(__dirname, '..'),
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
NODE_ENV: 'test',
|
NODE_ENV: 'production',
|
||||||
|
MK_DISABLE_CLUSTERING: '1',
|
||||||
},
|
},
|
||||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||||
|
execArgv: [...process.execArgv, '--expose-gc'],
|
||||||
});
|
});
|
||||||
|
|
||||||
let serverReady = false;
|
let serverReady = false;
|
||||||
|
|
@ -57,6 +89,40 @@ async function measureMemory() {
|
||||||
process.stderr.write(`[server error] ${err}\n`);
|
process.stderr.write(`[server error] ${err}\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function triggerGc() {
|
||||||
|
const ok = new Promise((resolve) => {
|
||||||
|
serverProcess.once('message', (message) => {
|
||||||
|
if (message === 'gc ok') resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.send('gc');
|
||||||
|
|
||||||
|
await ok;
|
||||||
|
|
||||||
|
await setTimeout(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRequest() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({
|
||||||
|
host: 'localhost',
|
||||||
|
port: 61812,
|
||||||
|
path: '/api/meta',
|
||||||
|
method: 'POST',
|
||||||
|
}, (res) => {
|
||||||
|
res.on('data', () => { });
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for server to be ready or timeout
|
// Wait for server to be ready or timeout
|
||||||
const startupStartTime = Date.now();
|
const startupStartTime = Date.now();
|
||||||
while (!serverReady) {
|
while (!serverReady) {
|
||||||
|
|
@ -73,46 +139,23 @@ async function measureMemory() {
|
||||||
// Wait for memory to settle
|
// Wait for memory to settle
|
||||||
await setTimeout(MEMORY_SETTLE_TIME);
|
await setTimeout(MEMORY_SETTLE_TIME);
|
||||||
|
|
||||||
// Get memory usage from the server process via /proc
|
|
||||||
const pid = serverProcess.pid;
|
const pid = serverProcess.pid;
|
||||||
let memoryInfo;
|
|
||||||
|
|
||||||
try {
|
const beforeGc = await getMemoryUsage(pid);
|
||||||
const fs = await import('node:fs/promises');
|
|
||||||
|
|
||||||
// Read /proc/[pid]/status for detailed memory info
|
await triggerGc();
|
||||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
|
||||||
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
||||||
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
|
|
||||||
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
|
|
||||||
|
|
||||||
memoryInfo = {
|
const afterGc = await getMemoryUsage(pid);
|
||||||
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
|
|
||||||
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
|
|
||||||
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
// Fallback: use ps command
|
|
||||||
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
|
|
||||||
|
|
||||||
const { execSync } = await import('node:child_process');
|
// create some http requests to simulate load
|
||||||
try {
|
const REQUEST_COUNT = 10;
|
||||||
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' });
|
await Promise.all(
|
||||||
const rssKb = parseInt(ps.trim(), 10);
|
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
|
||||||
memoryInfo = {
|
);
|
||||||
rss: rssKb * 1024,
|
|
||||||
heapUsed: null,
|
await triggerGc();
|
||||||
vmSize: null,
|
|
||||||
};
|
const afterRequest = await getMemoryUsage(pid);
|
||||||
} catch {
|
|
||||||
memoryInfo = {
|
|
||||||
rss: null,
|
|
||||||
heapUsed: null,
|
|
||||||
vmSize: null,
|
|
||||||
error: 'Could not measure memory',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the server
|
// Stop the server
|
||||||
serverProcess.kill('SIGTERM');
|
serverProcess.kill('SIGTERM');
|
||||||
|
|
@ -135,15 +178,51 @@ async function measureMemory() {
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
startupTimeMs: startupTime,
|
beforeGc,
|
||||||
memory: memoryInfo,
|
afterGc,
|
||||||
|
afterRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 直列の方が時間的に分散されて正確そうだから直列でやる
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
||||||
|
const res = await measureMemory();
|
||||||
|
results.push(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate averages
|
||||||
|
const beforeGc = structuredClone(keys);
|
||||||
|
const afterGc = structuredClone(keys);
|
||||||
|
const afterRequest = structuredClone(keys);
|
||||||
|
for (const res of results) {
|
||||||
|
for (const key of Object.keys(keys)) {
|
||||||
|
beforeGc[key] += res.beforeGc[key];
|
||||||
|
afterGc[key] += res.afterGc[key];
|
||||||
|
afterRequest[key] += res.afterRequest[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(keys)) {
|
||||||
|
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
|
||||||
|
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
|
||||||
|
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
beforeGc,
|
||||||
|
afterGc,
|
||||||
|
afterRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Output as JSON to stdout
|
// Output as JSON to stdout
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
measureMemory().catch((err) => {
|
main().catch((err) => {
|
||||||
console.error(JSON.stringify({
|
console.error(JSON.stringify({
|
||||||
error: err.message,
|
error: err.message,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,18 @@ if (!envOption.disableClustering) {
|
||||||
ev.mount();
|
ev.mount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.on('message', msg => {
|
||||||
|
if (msg === 'gc') {
|
||||||
|
if (global.gc != null) {
|
||||||
|
logger.info('Manual GC triggered');
|
||||||
|
global.gc();
|
||||||
|
if (process.send != null) process.send('gc ok');
|
||||||
|
} else {
|
||||||
|
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
readyRef.value = true;
|
readyRef.value = true;
|
||||||
|
|
||||||
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
|
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MetricsTime, type JobType } from 'bullmq';
|
import { MetricsTime, type JobType } from 'bullmq';
|
||||||
import { parse as parseRedisInfo } from 'redis-info';
|
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
|
@ -86,6 +85,19 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
|
||||||
pattern: '0 4 * * *',
|
pattern: '0 4 * * *',
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
function parseRedisInfo(infoText: string): Record<string, string> {
|
||||||
|
const fields = infoText
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.length > 0 && !line.startsWith('#'))
|
||||||
|
.map(line => line.trim().split(':'));
|
||||||
|
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const [key, value] of fields) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -890,7 +902,7 @@ export class QueueService {
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
version: db.redis_version,
|
version: db.redis_version,
|
||||||
mode: db.redis_mode,
|
mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
|
||||||
runId: db.run_id,
|
runId: db.run_id,
|
||||||
processId: db.process_id,
|
processId: db.process_id,
|
||||||
port: parseInt(db.tcp_port),
|
port: parseInt(db.tcp_port),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
|
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
|
||||||
|
return ['random', '1', '2'].includes(bw);
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReversiGameEntityService {
|
export class ReversiGameEntityService {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -58,7 +62,7 @@ export class ReversiGameEntityService {
|
||||||
surrenderedUserId: game.surrenderedUserId,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
timeoutUserId: game.timeoutUserId,
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
bw: game.bw,
|
bw: assertBw(game.bw) ? game.bw : 'random',
|
||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: game.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: game.loopedBoard,
|
||||||
|
|
@ -116,7 +120,7 @@ export class ReversiGameEntityService {
|
||||||
surrenderedUserId: game.surrenderedUserId,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
timeoutUserId: game.timeoutUserId,
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
bw: game.bw,
|
bw: assertBw(game.bw) ? game.bw : 'random',
|
||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: game.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: game.loopedBoard,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import {
|
||||||
packedMetaDetailedOnlySchema,
|
packedMetaDetailedOnlySchema,
|
||||||
packedMetaDetailedSchema,
|
packedMetaDetailedSchema,
|
||||||
packedMetaLiteSchema,
|
packedMetaLiteSchema,
|
||||||
|
packedMetaClientOptionsSchema,
|
||||||
} from '@/models/json-schema/meta.js';
|
} from '@/models/json-schema/meta.js';
|
||||||
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
|
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
|
||||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||||
|
|
@ -135,6 +136,7 @@ export const refs = {
|
||||||
MetaLite: packedMetaLiteSchema,
|
MetaLite: packedMetaLiteSchema,
|
||||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||||
MetaDetailed: packedMetaDetailedSchema,
|
MetaDetailed: packedMetaDetailedSchema,
|
||||||
|
MetaClientOptions: packedMetaClientOptionsSchema,
|
||||||
UserWebhook: packedUserWebhookSchema,
|
UserWebhook: packedUserWebhookSchema,
|
||||||
SystemWebhook: packedSystemWebhookSchema,
|
SystemWebhook: packedSystemWebhookSchema,
|
||||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||||
|
|
|
||||||
|
|
@ -725,7 +725,11 @@ export class MiMeta {
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: { },
|
default: { },
|
||||||
})
|
})
|
||||||
public clientOptions: Record<string, any>;
|
public clientOptions: {
|
||||||
|
entrancePageStyle: 'classic' | 'simple';
|
||||||
|
showTimelineForVisitor: boolean;
|
||||||
|
showActivitiesForVisitor: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SoftwareSuspension = {
|
export type SoftwareSuspension = {
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,7 @@ export const packedMetaLiteSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
clientOptions: {
|
clientOptions: {
|
||||||
type: 'object',
|
ref: 'MetaClientOptions',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
disableRegistration: {
|
disableRegistration: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
@ -397,3 +396,23 @@ export const packedMetaDetailedSchema = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const packedMetaClientOptionsSchema = {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
entrancePageStyle: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['classic', 'simple'],
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
showTimelineForVisitor: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
showActivitiesForVisitor: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
|
||||||
bw: {
|
bw: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
enum: ['random', '1', '2'],
|
||||||
},
|
},
|
||||||
noIrregularRules: {
|
noIrregularRules: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
|
||||||
bw: {
|
bw: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
enum: ['random', '1', '2'],
|
||||||
},
|
},
|
||||||
noIrregularRules: {
|
noIrregularRules: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -428,8 +428,7 @@ export const meta = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
clientOptions: {
|
clientOptions: {
|
||||||
type: 'object',
|
ref: 'MetaClientOptions',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
|
@ -67,7 +68,14 @@ export const paramDef = {
|
||||||
description: { type: 'string', nullable: true },
|
description: { type: 'string', nullable: true },
|
||||||
defaultLightTheme: { type: 'string', nullable: true },
|
defaultLightTheme: { type: 'string', nullable: true },
|
||||||
defaultDarkTheme: { type: 'string', nullable: true },
|
defaultDarkTheme: { type: 'string', nullable: true },
|
||||||
clientOptions: { type: 'object', nullable: false },
|
clientOptions: {
|
||||||
|
type: 'object', nullable: false,
|
||||||
|
properties: {
|
||||||
|
entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
|
||||||
|
showTimelineForVisitor: { type: 'boolean', nullable: false },
|
||||||
|
showActivitiesForVisitor: { type: 'boolean', nullable: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
cacheRemoteFiles: { type: 'boolean' },
|
cacheRemoteFiles: { type: 'boolean' },
|
||||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||||
emailRequiredForSignup: { type: 'boolean' },
|
emailRequiredForSignup: { type: 'boolean' },
|
||||||
|
|
@ -217,6 +225,9 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private serverSettings: MiMeta,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
|
|
@ -329,7 +340,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.clientOptions !== undefined) {
|
if (ps.clientOptions !== undefined) {
|
||||||
set.clientOptions = ps.clientOptions;
|
set.clientOptions = {
|
||||||
|
...serverSettings.clientOptions,
|
||||||
|
...ps.clientOptions,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.cacheRemoteFiles !== undefined) {
|
if (ps.cacheRemoteFiles !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,15 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/estree": "1.0.8",
|
"@types/estree": "1.0.8",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"rollup": "4.54.0"
|
"rollup": "4.55.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"i18n": "workspace:*",
|
"i18n": "workspace:*",
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"magic-string": "0.30.21",
|
"magic-string": "0.30.21",
|
||||||
"vite": "7.3.0"
|
"vite": "7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,15 @@ export default [
|
||||||
'vue/return-in-computed-property': 'warn',
|
'vue/return-in-computed-property': 'warn',
|
||||||
'vue/no-setup-props-reactivity-loss': 'warn',
|
'vue/no-setup-props-reactivity-loss': 'warn',
|
||||||
'vue/max-attributes-per-line': 'off',
|
'vue/max-attributes-per-line': 'off',
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': ['error', {
|
||||||
|
html: {
|
||||||
|
void: 'any',
|
||||||
|
normal: 'never',
|
||||||
|
component: 'any',
|
||||||
|
},
|
||||||
|
svg: 'any',
|
||||||
|
math: 'any',
|
||||||
|
}],
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
'vue/v-on-event-hyphenation': ['error', 'never', {
|
'vue/v-on-event-hyphenation': ['error', 'never', {
|
||||||
autofix: true,
|
autofix: true,
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@
|
||||||
"mfm-js": "0.25.0",
|
"mfm-js": "0.25.0",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"rollup": "4.54.0",
|
"rollup": "4.55.1",
|
||||||
"sass": "1.97.1",
|
"sass": "1.97.2",
|
||||||
"shiki": "3.20.0",
|
"shiki": "3.21.0",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"uuid": "13.0.0",
|
"uuid": "13.0.0",
|
||||||
"vite": "7.3.0",
|
"vite": "7.3.1",
|
||||||
"vue": "3.5.26"
|
"vue": "3.5.26"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -39,29 +39,29 @@
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/estree": "1.0.8",
|
"@types/estree": "1.0.8",
|
||||||
"@types/micromatch": "4.0.10",
|
"@types/micromatch": "4.0.10",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"@vitest/coverage-v8": "4.0.16",
|
"@vitest/coverage-v8": "4.0.17",
|
||||||
"@vue/runtime-core": "3.5.26",
|
"@vue/runtime-core": "3.5.26",
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-vue": "10.6.2",
|
"eslint-plugin-vue": "10.7.0",
|
||||||
"happy-dom": "20.0.11",
|
"happy-dom": "20.3.1",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"msw": "2.12.6",
|
"msw": "2.12.7",
|
||||||
"nodemon": "3.1.11",
|
"nodemon": "3.1.11",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.0",
|
||||||
"start-server-and-test": "2.1.3",
|
"start-server-and-test": "2.1.3",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vue-component-type-helpers": "3.2.1",
|
"vue-component-type-helpers": "3.2.2",
|
||||||
"vue-eslint-parser": "10.2.0",
|
"vue-eslint-parser": "10.2.0",
|
||||||
"vue-tsc": "3.2.1"
|
"vue-tsc": "3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="user.isCat" :class="[$style.ears]">
|
<div v-if="user.isCat" :class="[$style.ears]">
|
||||||
<div :class="$style.earLeft">
|
<div :class="$style.earLeft">
|
||||||
<div v-if="false" :class="$style.layer">
|
<div v-if="false" :class="$style.layer">
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.earRight">
|
<div :class="$style.earRight">
|
||||||
<div v-if="false" :class="$style.layer">
|
<div v-if="false" :class="$style.layer">
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
|
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"></canvas>
|
||||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.2",
|
||||||
"eslint-plugin-vue": "10.6.2",
|
"eslint-plugin-vue": "10.7.0",
|
||||||
"nodemon": "3.1.11",
|
"nodemon": "3.1.11",
|
||||||
"vue-eslint-parser": "10.2.0"
|
"vue-eslint-parser": "10.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,15 @@ export default [
|
||||||
'vue/return-in-computed-property': 'warn',
|
'vue/return-in-computed-property': 'warn',
|
||||||
'vue/no-setup-props-reactivity-loss': 'warn',
|
'vue/no-setup-props-reactivity-loss': 'warn',
|
||||||
'vue/max-attributes-per-line': 'off',
|
'vue/max-attributes-per-line': 'off',
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': ['error', {
|
||||||
|
html: {
|
||||||
|
void: 'any',
|
||||||
|
normal: 'never',
|
||||||
|
component: 'any',
|
||||||
|
},
|
||||||
|
svg: 'any',
|
||||||
|
math: 'any',
|
||||||
|
}],
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
'vue/v-on-event-hyphenation': ['error', 'never', {
|
'vue/v-on-event-hyphenation': ['error', 'never', {
|
||||||
autofix: true,
|
autofix: true,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "6.0.3",
|
"@rollup/plugin-replace": "6.0.3",
|
||||||
"@rollup/pluginutils": "5.3.0",
|
"@rollup/pluginutils": "5.3.0",
|
||||||
"@sentry/vue": "10.32.1",
|
"@sentry/vue": "10.34.0",
|
||||||
"@syuilo/aiscript": "1.2.1",
|
"@syuilo/aiscript": "1.2.1",
|
||||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||||
"@twemoji/parser": "16.0.0",
|
"@twemoji/parser": "16.0.0",
|
||||||
|
|
@ -39,13 +39,13 @@
|
||||||
"chartjs-chart-matrix": "3.0.0",
|
"chartjs-chart-matrix": "3.0.0",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.2.0",
|
"chartjs-plugin-zoom": "2.2.0",
|
||||||
"chromatic": "13.3.4",
|
"chromatic": "13.3.5",
|
||||||
"compare-versions": "6.1.1",
|
"compare-versions": "6.1.1",
|
||||||
"cropperjs": "2.1.0",
|
"cropperjs": "2.1.0",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
"execa": "9.6.1",
|
"execa": "9.6.1",
|
||||||
"exifreader": "4.33.1",
|
"exifreader": "4.36.0",
|
||||||
"frontend-shared": "workspace:*",
|
"frontend-shared": "workspace:*",
|
||||||
"i18n": "workspace:*",
|
"i18n": "workspace:*",
|
||||||
"icons-subsetter": "workspace:*",
|
"icons-subsetter": "workspace:*",
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
"is-file-animated": "1.0.2",
|
"is-file-animated": "1.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"matter-js": "0.20.0",
|
"matter-js": "0.20.0",
|
||||||
"mediabunny": "1.27.2",
|
"mediabunny": "1.28.0",
|
||||||
"mfm-js": "0.25.0",
|
"mfm-js": "0.25.0",
|
||||||
"misskey-bubble-game": "workspace:*",
|
"misskey-bubble-game": "workspace:*",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
|
|
@ -64,16 +64,16 @@
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"qr-code-styling": "1.9.2",
|
"qr-code-styling": "1.9.2",
|
||||||
"qr-scanner": "1.4.2",
|
"qr-scanner": "1.4.2",
|
||||||
"rollup": "4.54.0",
|
"rollup": "4.55.1",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"sass": "1.97.1",
|
"sass": "1.97.2",
|
||||||
"shiki": "3.20.0",
|
"shiki": "3.21.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.182.0",
|
"three": "0.182.0",
|
||||||
"throttle-debounce": "5.0.2",
|
"throttle-debounce": "5.0.2",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "7.3.0",
|
"vite": "7.3.1",
|
||||||
"vue": "3.5.26",
|
"vue": "3.5.26",
|
||||||
"wanakana": "5.3.1"
|
"wanakana": "5.3.1"
|
||||||
},
|
},
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
"@misskey-dev/summaly": "5.2.5",
|
"@misskey-dev/summaly": "5.2.5",
|
||||||
"@storybook/addon-essentials": "8.6.15",
|
"@storybook/addon-essentials": "8.6.15",
|
||||||
"@storybook/addon-interactions": "8.6.15",
|
"@storybook/addon-interactions": "8.6.15",
|
||||||
"@storybook/addon-links": "10.1.10",
|
"@storybook/addon-links": "10.1.11",
|
||||||
"@storybook/addon-mdx-gfm": "8.6.15",
|
"@storybook/addon-mdx-gfm": "8.6.15",
|
||||||
"@storybook/addon-storysource": "8.6.15",
|
"@storybook/addon-storysource": "8.6.15",
|
||||||
"@storybook/blocks": "8.6.15",
|
"@storybook/blocks": "8.6.15",
|
||||||
|
|
@ -89,13 +89,13 @@
|
||||||
"@storybook/core-events": "8.6.15",
|
"@storybook/core-events": "8.6.15",
|
||||||
"@storybook/manager-api": "8.6.15",
|
"@storybook/manager-api": "8.6.15",
|
||||||
"@storybook/preview-api": "8.6.15",
|
"@storybook/preview-api": "8.6.15",
|
||||||
"@storybook/react": "10.1.10",
|
"@storybook/react": "10.1.11",
|
||||||
"@storybook/react-vite": "10.1.10",
|
"@storybook/react-vite": "10.1.11",
|
||||||
"@storybook/test": "8.6.15",
|
"@storybook/test": "8.6.15",
|
||||||
"@storybook/theming": "8.6.15",
|
"@storybook/theming": "8.6.15",
|
||||||
"@storybook/types": "8.6.15",
|
"@storybook/types": "8.6.15",
|
||||||
"@storybook/vue3": "10.1.10",
|
"@storybook/vue3": "10.1.11",
|
||||||
"@storybook/vue3-vite": "10.1.10",
|
"@storybook/vue3-vite": "10.1.11",
|
||||||
"@tabler/icons-webfont": "3.35.0",
|
"@tabler/icons-webfont": "3.35.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/canvas-confetti": "1.9.0",
|
"@types/canvas-confetti": "1.9.0",
|
||||||
|
|
@ -103,46 +103,46 @@
|
||||||
"@types/insert-text-at-cursor": "0.3.2",
|
"@types/insert-text-at-cursor": "0.3.2",
|
||||||
"@types/matter-js": "0.20.2",
|
"@types/matter-js": "0.20.2",
|
||||||
"@types/micromatch": "4.0.10",
|
"@types/micromatch": "4.0.10",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/textarea-caret": "3.0.4",
|
"@types/textarea-caret": "3.0.4",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"@vitest/coverage-v8": "4.0.16",
|
"@vitest/coverage-v8": "4.0.17",
|
||||||
"@vue/compiler-core": "3.5.26",
|
"@vue/compiler-core": "3.5.26",
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"astring": "1.9.0",
|
"astring": "1.9.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"cypress": "15.8.1",
|
"cypress": "15.9.0",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-vue": "10.6.2",
|
"eslint-plugin-vue": "10.7.0",
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"happy-dom": "20.0.11",
|
"happy-dom": "20.3.1",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"magic-string": "0.30.21",
|
"magic-string": "0.30.21",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"minimatch": "10.1.1",
|
"minimatch": "10.1.1",
|
||||||
"msw": "2.12.6",
|
"msw": "2.12.7",
|
||||||
"msw-storybook-addon": "2.0.6",
|
"msw-storybook-addon": "2.0.6",
|
||||||
"nodemon": "3.1.11",
|
"nodemon": "3.1.11",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"start-server-and-test": "2.1.3",
|
"start-server-and-test": "2.1.3",
|
||||||
"storybook": "10.1.10",
|
"storybook": "10.1.11",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"vite-plugin-glsl": "1.5.5",
|
"vite-plugin-glsl": "1.5.5",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "4.0.16",
|
"vitest": "4.0.17",
|
||||||
"vitest-fetch-mock": "0.4.5",
|
"vitest-fetch-mock": "0.4.5",
|
||||||
"vue-component-type-helpers": "3.2.1",
|
"vue-component-type-helpers": "3.2.2",
|
||||||
"vue-eslint-parser": "10.2.0",
|
"vue-eslint-parser": "10.2.0",
|
||||||
"vue-tsc": "3.2.1"
|
"vue-tsc": "3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<span :class="$style.title">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].title }}</span>
|
<span :class="$style.title">{{ i18n.ts._achievements._types[`_${achievement.name}`].title }}</span>
|
||||||
<span :class="$style.time">
|
<span :class="$style.time">
|
||||||
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
|
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.description">{{ withDescription ? (i18n.ts._achievements._types as any)['_' + achievement.name].description : '???' }}</div>
|
<div :class="$style.description">{{ withDescription ? i18n.ts._achievements._types[`_${achievement.name}`].description : '???' }}</div>
|
||||||
<div v-if="(i18n.ts._achievements._types as any)['_' + achievement.name].flavor && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].flavor }}</div>
|
<div v-if="'flavor' in i18n.ts._achievements._types[`_${achievement.name}`] && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types[`_${achievement.name}`] as { flavor: string; }).flavor }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="withLocked">
|
<template v-if="withLocked">
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
|
||||||
|
|
@ -63,7 +64,7 @@ import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
export type CompleteInfo = {
|
export type CompleteInfo = {
|
||||||
user: {
|
user: {
|
||||||
payload: any;
|
payload: Misskey.entities.User;
|
||||||
query: string | null;
|
query: string | null;
|
||||||
},
|
},
|
||||||
hashtag: {
|
hashtag: {
|
||||||
|
|
@ -185,9 +186,9 @@ const suggests = ref<Element>();
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const users = ref<any[]>([]);
|
const users = ref<Misskey.entities.User[]>([]);
|
||||||
const hashtags = ref<any[]>([]);
|
const hashtags = ref<string[]>([]);
|
||||||
const emojis = ref<(EmojiDef)[]>([]);
|
const emojis = ref<EmojiDef[]>([]);
|
||||||
const items = ref<Element[] | HTMLCollection>([]);
|
const items = ref<Element[] | HTMLCollection>([]);
|
||||||
const mfmTags = ref<string[]>([]);
|
const mfmTags = ref<string[]>([]);
|
||||||
const mfmParams = ref<string[]>([]);
|
const mfmParams = ref<string[]>([]);
|
||||||
|
|
@ -204,8 +205,8 @@ function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T][
|
||||||
emit('closed');
|
emit('closed');
|
||||||
if (type === 'emoji' || type === 'emojiComplete') {
|
if (type === 'emoji' || type === 'emojiComplete') {
|
||||||
let recents = store.s.recentlyUsedEmojis;
|
let recents = store.s.recentlyUsedEmojis;
|
||||||
recents = recents.filter((emoji: any) => emoji !== value);
|
recents = recents.filter((emoji) => emoji !== value);
|
||||||
recents.unshift(value);
|
recents.unshift(value as string);
|
||||||
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +255,7 @@ function exec() {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
detail: false,
|
detail: false,
|
||||||
}).then(searchedUsers => {
|
}).then(searchedUsers => {
|
||||||
users.value = searchedUsers as any[];
|
users.value = searchedUsers;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
// キャッシュ
|
// キャッシュ
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
||||||
|
|
@ -276,7 +277,7 @@ function exec() {
|
||||||
query: props.q,
|
query: props.q,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
}).then(searchedHashtags => {
|
}).then(searchedHashtags => {
|
||||||
hashtags.value = searchedHashtags as any[];
|
hashtags.value = searchedHashtags;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
// キャッシュ
|
// キャッシュ
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
||||||
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
|
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"></span>
|
||||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"></span>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
||||||
|
|
@ -52,7 +52,8 @@ import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
borderWidth ? { borderWidth: borderWidth } : {},
|
borderWidth ? { borderWidth: borderWidth } : {},
|
||||||
borderColor ? { borderColor: borderColor } : {},
|
borderColor ? { borderColor: borderColor } : {},
|
||||||
]"
|
]"
|
||||||
/>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #key>{{ i18n.ts.permission }}</template>
|
<template #key>{{ i18n.ts.permission }}</template>
|
||||||
<template #value>
|
<template #value>
|
||||||
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
|
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
|
||||||
<li v-for="permission in extension.meta.permissions" :key="permission">{{ (i18n.ts._permissions as any)[permission] ?? permission }}</li>
|
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<template v-else>{{ i18n.ts.none }}</template>
|
<template v-else>{{ i18n.ts.none }}</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot name="additionalInfo"/>
|
<slot name="additionalInfo"></slot>
|
||||||
|
|
||||||
<div class="_buttonsCenter">
|
<div class="_buttonsCenter">
|
||||||
<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||||
|
|
@ -101,6 +101,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
export type Extension = {
|
export type Extension = {
|
||||||
type: 'plugin';
|
type: 'plugin';
|
||||||
raw: string;
|
raw: string;
|
||||||
|
|
@ -109,7 +111,7 @@ export type Extension = {
|
||||||
version: string;
|
version: string;
|
||||||
author: string;
|
author: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions?: string[];
|
permissions?: (typeof Misskey.permissions)[number][];
|
||||||
config?: Record<string, unknown>;
|
config?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
|
|
@ -125,7 +127,6 @@ export type Extension = {
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import MkCode from '@/components/MkCode.vue';
|
import MkCode from '@/components/MkCode.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
|
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(v)">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
|
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
|
|
@ -60,6 +59,7 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
|
import type { MkRadiosOption } from '@/components/MkRadios.vue';
|
||||||
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
|
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -113,7 +113,13 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRadioKey(e: RadioFormItem['options'][number]) {
|
function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] {
|
||||||
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
|
return def.options.map<MkRadiosOption>((v) => {
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
return { value: v, label: v };
|
||||||
|
} else {
|
||||||
|
return { value: v.value, label: v.label };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ v.label ?? k }}</template>
|
<template #label>{{ v.label ?? k }}</template>
|
||||||
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]">
|
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum">
|
||||||
<template #label>{{ v.label ?? k }}</template>
|
<template #label>{{ v.label ?? k }}</template>
|
||||||
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
||||||
<option v-for="item in v.enum" :value="item.value">
|
|
||||||
<i v-if="item.icon" :class="item.icon"></i>
|
|
||||||
<template v-else>{{ item.label }}</template>
|
|
||||||
</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<div v-else-if="v.type === 'seed'">
|
<div v-else-if="v.type === 'seed'">
|
||||||
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
|
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
draggable="false"
|
draggable="false"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
style="-webkit-user-drag: none;"
|
style="-webkit-user-drag: none;"
|
||||||
/>
|
></canvas>
|
||||||
<img
|
<img
|
||||||
v-show="!hide"
|
v-show="!hide"
|
||||||
key="img"
|
key="img"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
>
|
>
|
||||||
<datalist v-if="datalist" :id="id">
|
<datalist v-if="datalist" :id="id">
|
||||||
<option v-for="data in datalist" :key="data" :value="data"/>
|
<option v-for="data in datalist" :key="data" :value="data"></option>
|
||||||
</datalist>
|
</datalist>
|
||||||
<div ref="suffixEl" :class="$style.suffix"><slot name="suffix"></slot></div>
|
<div ref="suffixEl" :class="$style.suffix"><slot name="suffix"></slot></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal">
|
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal" @contextmenu.stop="onContextmenu">
|
||||||
<component
|
<component
|
||||||
:is="disableImageLink ? 'div' : 'a'"
|
:is="disableImageLink ? 'div' : 'a'"
|
||||||
v-bind="disableImageLink ? {
|
v-bind="disableImageLink ? {
|
||||||
|
|
@ -123,7 +123,7 @@ watch(() => props.image, (newImage) => {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
function showMenu(ev: PointerEvent) {
|
function getMenu() {
|
||||||
const menuItems: MenuItem[] = [];
|
const menuItems: MenuItem[] = [];
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
|
|
@ -188,9 +188,16 @@ function showMenu(ev: PointerEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
return menuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showMenu(ev: PointerEvent) {
|
||||||
|
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextmenu(ev: PointerEvent) {
|
||||||
|
os.contextMenu(getMenu(), ev);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
||||||
|
|
@ -323,9 +323,20 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent |
|
||||||
type: 'radioOption',
|
type: 'radioOption',
|
||||||
text: key,
|
text: key,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
if ('value' in item.ref) {
|
||||||
|
item.ref.value = value;
|
||||||
|
} else {
|
||||||
|
// @ts-expect-error リアクティビティは保たれる
|
||||||
item.ref = value;
|
item.ref = value;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
active: computed(() => item.ref === value),
|
active: computed(() => {
|
||||||
|
if ('value' in item.ref) {
|
||||||
|
return item.ref.value === value;
|
||||||
|
} else {
|
||||||
|
return item.ref === value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ notification.invitation.room.name }}
|
{{ notification.invitation.room.name }}
|
||||||
</div>
|
</div>
|
||||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||||
{{ (i18n.ts._achievements._types as any)['_' + notification.achievement].title }}
|
{{ i18n.ts._achievements._types[`_${notification.achievement}`].title }}
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
|
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
|
||||||
{{ i18n.ts.showFile }}
|
{{ i18n.ts.showFile }}
|
||||||
|
|
@ -143,8 +143,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
||||||
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
||||||
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"></i> {{ i18n.ts.accept }}</MkButton>
|
||||||
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"></i> {{ i18n.ts.reject }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
|
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.buttons">
|
<div :class="$style.buttons">
|
||||||
<div v-if="prevDotVisible" :class="$style.headTailButtons">
|
<div v-if="prevDotVisible" :class="$style.headTailButtons">
|
||||||
<MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton>
|
<MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton>
|
||||||
<span class="ti ti-dots"/>
|
<span class="ti ti-dots"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkButton
|
<MkButton
|
||||||
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkButton>
|
</MkButton>
|
||||||
|
|
||||||
<div v-if="nextDotVisible" :class="$style.headTailButtons">
|
<div v-if="nextDotVisible" :class="$style.headTailButtons">
|
||||||
<span class="ti ti-dots"/>
|
<span class="ti ti-dots"></span>
|
||||||
<MkButton @click="onToTailButtonClicked">{{ max }}</MkButton>
|
<MkButton @click="onToTailButtonClicked">{{ max }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
|
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
||||||
<div v-if="targetChannel" :class="$style.colorBar" :style="{ background: targetChannel.color }"></div>
|
<div v-if="targetChannel" :class="$style.colorBar" :style="{ background: targetChannel.color }"></div>
|
||||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"></textarea>
|
||||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
|
|
@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<datalist id="hashtags">
|
<datalist id="hashtags">
|
||||||
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
|
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"></option>
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="showFileMenu(item, $event)"
|
@click="showFileMenu(item, $event)"
|
||||||
@keydown.space.enter="showFileMenu(item, $event)"
|
@keydown.space.enter="showFileMenu(item, $event)"
|
||||||
@contextmenu.prevent="showFileMenu(item, $event)"
|
@contextmenu.prevent.stop="showFileMenu(item, $event)"
|
||||||
>
|
>
|
||||||
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||||
<MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/>
|
<MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/>
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="flag" :class="$style.preview__content1__switch_button">
|
<MkSwitch v-model="flag" :class="$style.preview__content1__switch_button">
|
||||||
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
|
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<div :class="$style.preview__content1__input">
|
|
||||||
<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
|
|
||||||
<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
|
|
||||||
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.preview__content1__button">
|
<div :class="$style.preview__content1__button">
|
||||||
<MkButton inline>This is</MkButton>
|
<MkButton inline>This is</MkButton>
|
||||||
<MkButton inline primary>the button</MkButton>
|
<MkButton inline primary>the button</MkButton>
|
||||||
|
|
@ -40,15 +35,12 @@ import * as config from '@@/js/config.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
|
||||||
import MkRadio from '@/components/MkRadio.vue';
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { chooseDriveFile } from '@/utility/drive.js';
|
import { chooseDriveFile } from '@/utility/drive.js';
|
||||||
|
|
||||||
const text = ref('');
|
const text = ref('');
|
||||||
const flag = ref(true);
|
const flag = ref(true);
|
||||||
const radio = ref('misskey');
|
|
||||||
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
|
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
|
||||||
|
|
||||||
const openDialog = async () => {
|
const openDialog = async () => {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot/>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ async function unsubscribe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function encode(buffer: ArrayBuffer | null) {
|
function encode(buffer: ArrayBuffer | null) {
|
||||||
return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
|
return btoa(String.fromCharCode(...(buffer != null ? new Uint8Array(buffer) : [])));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-adaptive-border
|
|
||||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
|
||||||
:aria-checked="checked"
|
|
||||||
:aria-disabled="disabled"
|
|
||||||
role="checkbox"
|
|
||||||
@click="toggle"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="$style.input"
|
|
||||||
>
|
|
||||||
<span :class="$style.button">
|
|
||||||
<span></span>
|
|
||||||
</span>
|
|
||||||
<span :class="$style.label"><slot></slot></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup generic="T extends unknown">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: T;
|
|
||||||
value: T;
|
|
||||||
disabled?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'update:modelValue', value: T): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const checked = computed(() => props.modelValue === props.value);
|
|
||||||
|
|
||||||
function toggle(): void {
|
|
||||||
if (props.disabled) return;
|
|
||||||
emit('update:modelValue', props.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.root {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 7px 10px;
|
|
||||||
min-width: 60px;
|
|
||||||
background-color: var(--MI_THEME-panel);
|
|
||||||
background-clip: padding-box !important;
|
|
||||||
border: solid 1px var(--MI_THEME-panel);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 90%;
|
|
||||||
transition: all 0.2s;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--MI_THEME-inputBorderHover) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
background-color: var(--MI_THEME-accentedBg) !important;
|
|
||||||
border-color: var(--MI_THEME-accentedBg) !important;
|
|
||||||
color: var(--MI_THEME-accent);
|
|
||||||
cursor: default !important;
|
|
||||||
|
|
||||||
> .button {
|
|
||||||
border-color: var(--MI_THEME-accent);
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
background-color: var(--MI_THEME-accent);
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
background: none;
|
|
||||||
border: solid 2px var(--MI_THEME-inputBorder);
|
|
||||||
border-radius: 100%;
|
|
||||||
transition: inherit;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 3px;
|
|
||||||
right: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
left: 3px;
|
|
||||||
border-radius: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0);
|
|
||||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-left: 8px;
|
|
||||||
display: block;
|
|
||||||
line-height: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -3,70 +3,96 @@ SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="{ [$style.vertical]: vertical }">
|
||||||
|
<div :class="$style.label">
|
||||||
|
<slot name="label"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.body">
|
||||||
|
<div
|
||||||
|
v-for="option in options"
|
||||||
|
:key="getKey(option.value)"
|
||||||
|
v-adaptive-border
|
||||||
|
:class="[$style.optionRoot, { [$style.disabled]: option.disabled, [$style.checked]: model === option.value }]"
|
||||||
|
:aria-checked="model === option.value"
|
||||||
|
:aria-disabled="option.disabled"
|
||||||
|
role="checkbox"
|
||||||
|
@click="toggle(option)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
:class="$style.optionInput"
|
||||||
|
>
|
||||||
|
<span :class="$style.optionButton">
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
<div :class="$style.optionContent">
|
||||||
|
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
|
||||||
|
<div>
|
||||||
|
<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
|
||||||
|
<template v-else>
|
||||||
|
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
|
||||||
|
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.caption">
|
||||||
|
<slot name="caption"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Comment, defineComponent, h, ref, watch } from 'vue';
|
import type { StyleValue } from 'vue';
|
||||||
import MkRadio from './MkRadio.vue';
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
import type { VNode } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export type MkRadiosOption<T = OptionValue, S = string> = {
|
||||||
props: {
|
value: T;
|
||||||
modelValue: {
|
slotId?: S;
|
||||||
required: false,
|
label?: string;
|
||||||
},
|
labelStyle?: StyleValue;
|
||||||
vertical: {
|
icon?: string;
|
||||||
type: Boolean,
|
iconStyle?: StyleValue;
|
||||||
default: false,
|
caption?: string;
|
||||||
},
|
disabled?: boolean;
|
||||||
},
|
};
|
||||||
setup(props, context) {
|
|
||||||
const value = ref(props.modelValue);
|
|
||||||
watch(value, () => {
|
|
||||||
context.emit('update:modelValue', value.value);
|
|
||||||
});
|
|
||||||
watch(() => props.modelValue, v => {
|
|
||||||
value.value = v;
|
|
||||||
});
|
|
||||||
if (!context.slots.default) return null;
|
|
||||||
let options = context.slots.default();
|
|
||||||
const label = context.slots.label && context.slots.label();
|
|
||||||
const caption = context.slots.caption && context.slots.caption();
|
|
||||||
|
|
||||||
// なぜかFragmentになることがあるため
|
|
||||||
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
|
|
||||||
|
|
||||||
// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
|
|
||||||
options = options.filter(vnode => vnode.type !== Comment);
|
|
||||||
|
|
||||||
return () => h('div', {
|
|
||||||
class: [
|
|
||||||
'novjtcto',
|
|
||||||
...(props.vertical ? ['vertical'] : []),
|
|
||||||
],
|
|
||||||
}, [
|
|
||||||
...(label ? [h('div', {
|
|
||||||
class: 'label',
|
|
||||||
}, label)] : []),
|
|
||||||
h('div', {
|
|
||||||
class: 'body',
|
|
||||||
}, options.map(option => h(MkRadio, {
|
|
||||||
key: option.key as string,
|
|
||||||
value: option.props?.value,
|
|
||||||
disabled: option.props?.disabled,
|
|
||||||
modelValue: value.value,
|
|
||||||
'onUpdate:modelValue': _v => value.value = _v,
|
|
||||||
}, () => option.children)),
|
|
||||||
),
|
|
||||||
...(caption ? [h('div', {
|
|
||||||
class: 'caption',
|
|
||||||
}, caption)] : []),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<script setup lang="ts" generic="const T extends MkRadiosOption">
|
||||||
.novjtcto {
|
defineProps<{
|
||||||
> .label {
|
options: T[];
|
||||||
|
vertical?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
label?: () => void;
|
||||||
|
caption?: () => void;
|
||||||
|
} & {
|
||||||
|
[K in `option-${SlotNames}`]: () => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<T['value']>({ required: true });
|
||||||
|
|
||||||
|
function getKey(value: OptionValue): PropertyKey {
|
||||||
|
if (value === null) return '___null___';
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(o: MkRadiosOption): void {
|
||||||
|
if (o.disabled) return;
|
||||||
|
model.value = o.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.label {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
padding: 0 0 8px 0;
|
padding: 0 0 8px 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
@ -76,13 +102,13 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .caption {
|
.caption {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
padding: 8px 0 0 0;
|
padding: 8px 0 0 0;
|
||||||
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||||
|
|
@ -92,10 +118,110 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.vertical {
|
.vertical > .body {
|
||||||
> .body {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.optionRoot {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 10px;
|
||||||
|
min-width: 60px;
|
||||||
|
background-color: var(--MI_THEME-panel);
|
||||||
|
background-clip: padding-box !important;
|
||||||
|
border: solid 1px var(--MI_THEME-panel);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 90%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--MI_THEME-inputBorderHover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
background-color: var(--MI_THEME-accentedBg) !important;
|
||||||
|
border-color: var(--MI_THEME-accentedBg) !important;
|
||||||
|
color: var(--MI_THEME-accent);
|
||||||
|
cursor: default !important;
|
||||||
|
|
||||||
|
.optionButton {
|
||||||
|
border-color: var(--MI_THEME-accent);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: var(--MI_THEME-accent);
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCaption {
|
||||||
|
color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionInput {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionButton {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: none;
|
||||||
|
border: solid 2px var(--MI_THEME-inputBorder);
|
||||||
|
border-radius: 100%;
|
||||||
|
transition: inherit;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
left: 3px;
|
||||||
|
border-radius: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCaption {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 2px 0 0 0;
|
||||||
|
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionIcon {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:myReaction="props.myReaction"
|
:myReaction="props.myReaction"
|
||||||
@reactionToggled="onMockToggleReaction"
|
@reactionToggled="onMockToggleReaction"
|
||||||
/>
|
/>
|
||||||
<slot v-if="hasMoreReactions" name="more"/>
|
<slot v-if="hasMoreReactions" name="more"></slot>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, useTemplateRef } from 'vue';
|
import { onMounted, useTemplateRef } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
|
import type { ScatterDataPoint } from 'chart.js';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||||
|
|
@ -18,6 +19,12 @@ import { alpha } from '@/utility/color.js';
|
||||||
import { initChart } from '@/utility/init-chart.js';
|
import { initChart } from '@/utility/init-chart.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
|
||||||
|
interface RetentionPoint extends ScatterDataPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
d: string;
|
||||||
|
}
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const chartEl = useTemplateRef('chartEl');
|
const chartEl = useTemplateRef('chartEl');
|
||||||
|
|
@ -62,14 +69,14 @@ onMounted(async () => {
|
||||||
fill: false,
|
fill: false,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
data: [{
|
data: [{
|
||||||
x: '0',
|
x: 0,
|
||||||
y: 100,
|
y: 100,
|
||||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||||
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
|
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
|
||||||
x: (i + 1).toString(),
|
x: i + 1,
|
||||||
y: (v / record.users) * 100,
|
y: (v / record.users) * 100,
|
||||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||||
}))] as any,
|
}))],
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -111,11 +118,11 @@ onMounted(async () => {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title(context) {
|
title(context) {
|
||||||
const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
|
const v = context[0].dataset.data[context[0].dataIndex] as RetentionPoint;
|
||||||
return `${v.x} days later`;
|
return `${v.x} days later`;
|
||||||
},
|
},
|
||||||
label(context) {
|
label(context) {
|
||||||
const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
|
const v = context.dataset.data[context.dataIndex] as RetentionPoint;
|
||||||
const p = Math.round(v.y) + '%';
|
const p = Math.round(v.y) + '%';
|
||||||
return `${v.d} ${p}`;
|
return `${v.d} ${p}`;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export type OptionValue = string | number | null;
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
|
|
||||||
export type ItemOption<T extends OptionValue = OptionValue> = {
|
export type ItemOption<T extends OptionValue = OptionValue> = {
|
||||||
type?: 'option';
|
type?: 'option';
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #icon><i class="ti ti-settings-question"></i></template>
|
<template #icon><i class="ti ti-settings-question"></i></template>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkRadios v-model="q_use" :vertical="true">
|
<MkRadios
|
||||||
<option value="single">
|
v-model="q_use"
|
||||||
<div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div>
|
:options="[
|
||||||
<div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div>
|
{ value: 'single', label: i18n.ts._serverSetupWizard._use.single, icon: 'ti ti-user', caption: i18n.ts._serverSetupWizard._use.single_description },
|
||||||
</option>
|
{ value: 'group', label: i18n.ts._serverSetupWizard._use.group, icon: 'ti ti-lock', caption: i18n.ts._serverSetupWizard._use.group_description },
|
||||||
<option value="group">
|
{ value: 'open', label: i18n.ts._serverSetupWizard._use.open, icon: 'ti ti-world', caption: i18n.ts._serverSetupWizard._use.open_description },
|
||||||
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div>
|
]"
|
||||||
<div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div>
|
vertical
|
||||||
</option>
|
>
|
||||||
<option value="open">
|
|
||||||
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div>
|
|
||||||
<div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div>
|
|
||||||
</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
|
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
|
||||||
|
|
@ -40,10 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #icon><i class="ti ti-users"></i></template>
|
<template #icon><i class="ti ti-users"></i></template>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkRadios v-model="q_scale" :vertical="true">
|
<MkRadios
|
||||||
<option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option>
|
v-model="q_scale"
|
||||||
<option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option>
|
:options="[
|
||||||
<option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option>
|
{ value: 'small', label: i18n.ts._serverSetupWizard._scale.small, icon: 'ti ti-user' },
|
||||||
|
{ value: 'medium', label: i18n.ts._serverSetupWizard._scale.medium, icon: 'ti ti-users' },
|
||||||
|
{ value: 'large', label: i18n.ts._serverSetupWizard._scale.large, icon: 'ti ti-users-group' },
|
||||||
|
]"
|
||||||
|
vertical
|
||||||
|
>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
|
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
|
||||||
|
|
@ -57,9 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
|
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
|
||||||
|
|
||||||
<MkRadios v-model="q_federation" :vertical="true">
|
<MkRadios
|
||||||
<option value="yes">{{ i18n.ts.yes }}</option>
|
v-model="q_federation"
|
||||||
<option value="no">{{ i18n.ts.no }}</option>
|
:options="[
|
||||||
|
{ value: 'yes', label: i18n.ts.yes },
|
||||||
|
{ value: 'no', label: i18n.ts.no },
|
||||||
|
]"
|
||||||
|
vertical
|
||||||
|
>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
|
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
|
||||||
|
|
@ -212,9 +218,9 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const q_name = ref('');
|
const q_name = ref('');
|
||||||
const q_use = ref('single');
|
const q_use = ref<'single' | 'group' | 'open'>('single');
|
||||||
const q_scale = ref('small');
|
const q_scale = ref<'small' | 'medium' | 'large'>('small');
|
||||||
const q_federation = ref('yes');
|
const q_federation = ref<'yes' | 'no'>('no');
|
||||||
const q_remoteContentsCleaning = ref(true);
|
const q_remoteContentsCleaning = ref(true);
|
||||||
const q_adminName = ref('');
|
const q_adminName = ref('');
|
||||||
const q_adminEmail = ref('');
|
const q_adminEmail = ref('');
|
||||||
|
|
@ -239,7 +245,7 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
|
||||||
enableReactionsBuffering,
|
enableReactionsBuffering,
|
||||||
clientOptions: {
|
clientOptions: {
|
||||||
entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple',
|
entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple',
|
||||||
} as any,
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkTextarea v-model="text">
|
<MkTextarea v-model="text">
|
||||||
<template #label>{{ i18n.ts.text }}</template>
|
<template #label>{{ i18n.ts.text }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkRadios v-model="icon">
|
<MkRadios
|
||||||
|
v-model="icon"
|
||||||
|
:options="[
|
||||||
|
{ value: 'info', icon: 'ti ti-info-circle' },
|
||||||
|
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
|
||||||
|
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
|
||||||
|
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.icon }}</template>
|
<template #label>{{ i18n.ts.icon }}</template>
|
||||||
<option value="info"><i class="ti ti-info-circle"></i></option>
|
|
||||||
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
|
|
||||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
|
|
||||||
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkRadios v-model="display">
|
<MkRadios
|
||||||
|
v-model="display"
|
||||||
|
:options="[
|
||||||
|
{ value: 'normal', label: i18n.ts.normal },
|
||||||
|
{ value: 'banner', label: i18n.ts.banner },
|
||||||
|
{ value: 'dialog', label: i18n.ts.dialog },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.display }}</template>
|
<template #label>{{ i18n.ts.display }}</template>
|
||||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
|
||||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
|
||||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkSwitch v-model="needConfirmationToRead">
|
<MkSwitch v-model="needConfirmationToRead">
|
||||||
{{ i18n.ts._announcement.needConfirmationToRead }}
|
{{ i18n.ts._announcement.needConfirmationToRead }}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@ok="save()"
|
@ok="save()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header><i class="ti ti-icons"></i> {{ (i18n.ts._widgets as any)[widgetName] ?? widgetName }}</template>
|
<template #header><i class="ti ti-icons"></i> {{ i18n.ts._widgets[widgetName] ?? widgetName }}</template>
|
||||||
|
|
||||||
<MkPreviewWithControls>
|
<MkPreviewWithControls>
|
||||||
<template #preview>
|
<template #preview>
|
||||||
|
|
@ -50,13 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
|
import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
|
||||||
import MkPreviewWithControls from './MkPreviewWithControls.vue';
|
import MkPreviewWithControls from './MkPreviewWithControls.vue';
|
||||||
import type { Form } from '@/utility/form.js';
|
import type { Form } from '@/utility/form.js';
|
||||||
|
import type { WidgetName } from '@/widgets/index.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import MkForm from '@/components/MkForm.vue';
|
import MkForm from '@/components/MkForm.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widgetName: string;
|
widgetName: WidgetName;
|
||||||
form: Form;
|
form: Form;
|
||||||
currentSettings: Record<string, any>;
|
currentSettings: Record<string, any>;
|
||||||
}>();
|
}>();
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="user.isCat" :class="[$style.ears]">
|
<div v-if="user.isCat" :class="[$style.ears]">
|
||||||
<div :class="$style.earLeft">
|
<div :class="$style.earLeft">
|
||||||
<div v-if="false" :class="$style.layer">
|
<div v-if="false" :class="$style.layer">
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.earRight">
|
<div :class="$style.earRight">
|
||||||
<div v-if="false" :class="$style.layer">
|
<div v-if="false" :class="$style.layer">
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<span :class="$style.container">
|
<span :class="$style.container">
|
||||||
<span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
|
<span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
|
||||||
<slot/>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import type { Ref, MaybeRefOrGetter } from 'vue';
|
import type { Ref, MaybeRefOrGetter } from 'vue';
|
||||||
import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
|
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
|
||||||
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
|
|
||||||
type UnwrapReadonlyItems<T> = T extends readonly (infer U)[] ? U[] : T;
|
type UnwrapReadonlyItems<T> = T extends readonly (infer U)[] ? U[] : T;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,3 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientOptions = {
|
|
||||||
entrancePageStyle: 'classic' | 'simple';
|
|
||||||
showTimelineForVisitor: boolean;
|
|
||||||
showActivitiesForVisitor: boolean;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import type { Form, GetFormResultType } from '@/utility/form.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import type { PostFormProps } from '@/types/post-form.js';
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
||||||
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
import type { MkDialogReturnType } from '@/components/MkDialog.vue';
|
import type { MkDialogReturnType } from '@/components/MkDialog.vue';
|
||||||
import type { OverloadToUnion } from '@/types/overload-to-union.js';
|
import type { OverloadToUnion } from '@/types/overload-to-union.js';
|
||||||
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
|
<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
|
||||||
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
|
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"></span>
|
||||||
<span v-else class="ti ti-settings" style="line-height: normal"/>
|
<span v-else class="ti ti-settings" style="line-height: normal"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root" class="_panel _gaps_s">
|
<div :class="$style.root" class="_panel _gaps_s">
|
||||||
<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div>
|
<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"></span> {{ methodName }}</div>
|
||||||
<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
|
<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
|
||||||
<div :class="$style.rightDivider" style="flex: 1">
|
<div :class="$style.rightDivider" style="flex: 1">
|
||||||
<div v-if="method === 'email' && user">
|
<div v-if="method === 'email' && user">
|
||||||
|
|
@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.recipientButtons" style="margin-left: auto">
|
<div :class="$style.recipientButtons" style="margin-left: auto">
|
||||||
<button :class="$style.recipientButton" @click="onEditButtonClicked()">
|
<button :class="$style.recipientButton" @click="onEditButtonClicked()">
|
||||||
<span class="ti ti-settings"/>
|
<span class="ti ti-settings"></span>
|
||||||
</button>
|
</button>
|
||||||
<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
|
<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
|
||||||
<span class="ti ti-trash"/>
|
<span class="ti ti-trash"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root" class="_gaps_m">
|
<div :class="$style.root" class="_gaps_m">
|
||||||
<div :class="$style.addButton">
|
<div :class="$style.addButton">
|
||||||
<MkButton primary @click="onAddButtonClicked">
|
<MkButton primary @click="onAddButtonClicked">
|
||||||
<span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
|
<span class="ti ti-plus"></span> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.subMenus" class="_gaps_s">
|
<div :class="$style.subMenus" class="_gaps_s">
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkRadios v-model="ad.place">
|
<MkRadios
|
||||||
|
v-model="ad.place"
|
||||||
|
:options="[
|
||||||
|
{ value: 'square' },
|
||||||
|
{ value: 'horizontal' },
|
||||||
|
{ value: 'horizontal-big' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>Form</template>
|
<template #label>Form</template>
|
||||||
<option value="square">square</option>
|
|
||||||
<option value="horizontal">horizontal</option>
|
|
||||||
<option value="horizontal-big">horizontal-big</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<!--
|
|
||||||
<div style="margin: 32px 0;">
|
|
||||||
{{ i18n.ts.priority }}
|
|
||||||
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
|
|
||||||
<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
|
|
||||||
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<FormSplit>
|
<FormSplit>
|
||||||
<MkInput v-model="ad.ratio" type="number">
|
<MkInput v-model="ad.ratio" type="number">
|
||||||
<template #label>{{ i18n.ts.ratio }}</template>
|
<template #label>{{ i18n.ts.ratio }}</template>
|
||||||
|
|
@ -109,7 +104,11 @@ import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const ads = ref<Misskey.entities.Ad[]>([]);
|
type Ad = Misskey.entities.Ad & {
|
||||||
|
place: 'square' | 'horizontal' | 'horizontal-big';
|
||||||
|
};
|
||||||
|
|
||||||
|
const ads = ref<Ad[]>([]);
|
||||||
|
|
||||||
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
|
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
|
||||||
const localTime = new Date();
|
const localTime = new Date();
|
||||||
|
|
@ -136,7 +135,7 @@ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||||
return {
|
return {
|
||||||
...r,
|
...(r as Ad),
|
||||||
expiresAt: exdate.toISOString().slice(0, 16),
|
expiresAt: exdate.toISOString().slice(0, 16),
|
||||||
startsAt: stdate.toISOString().slice(0, 16),
|
startsAt: stdate.toISOString().slice(0, 16),
|
||||||
};
|
};
|
||||||
|
|
@ -239,7 +238,7 @@ function more() {
|
||||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||||
return {
|
return {
|
||||||
...r,
|
...(r as Ad),
|
||||||
expiresAt: exdate.toISOString().slice(0, 16),
|
expiresAt: exdate.toISOString().slice(0, 16),
|
||||||
startsAt: stdate.toISOString().slice(0, 16),
|
startsAt: stdate.toISOString().slice(0, 16),
|
||||||
};
|
};
|
||||||
|
|
@ -256,7 +255,7 @@ function refresh() {
|
||||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||||
return {
|
return {
|
||||||
...r,
|
...(r as Ad),
|
||||||
expiresAt: exdate.toISOString().slice(0, 16),
|
expiresAt: exdate.toISOString().slice(0, 16),
|
||||||
startsAt: stdate.toISOString().slice(0, 16),
|
startsAt: stdate.toISOString().slice(0, 16),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,18 +45,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="announcement.imageUrl" type="url">
|
<MkInput v-model="announcement.imageUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-model="announcement.icon">
|
<MkRadios
|
||||||
|
v-model="announcement.icon"
|
||||||
|
:options="[
|
||||||
|
{ value: 'info', icon: 'ti ti-info-circle' },
|
||||||
|
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
|
||||||
|
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
|
||||||
|
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.icon }}</template>
|
<template #label>{{ i18n.ts.icon }}</template>
|
||||||
<option value="info"><i class="ti ti-info-circle"></i></option>
|
|
||||||
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
|
|
||||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
|
|
||||||
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkRadios v-model="announcement.display">
|
<MkRadios
|
||||||
|
v-model="announcement.display"
|
||||||
|
:options="[
|
||||||
|
{ value: 'normal', label: i18n.ts.normal },
|
||||||
|
{ value: 'banner', label: i18n.ts.banner },
|
||||||
|
{ value: 'dialog', label: i18n.ts.dialog },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.display }}</template>
|
<template #label>{{ i18n.ts.display }}</template>
|
||||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
|
||||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
|
||||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
|
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
|
||||||
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
|
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkRadios v-model="botProtectionForm.state.provider">
|
<MkRadios
|
||||||
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
v-model="botProtectionForm.state.provider"
|
||||||
<option value="hcaptcha">hCaptcha</option>
|
:options="[
|
||||||
<option value="mcaptcha">mCaptcha</option>
|
{ value: 'none', label: `${i18n.ts.none} (${i18n.ts.notRecommended})` },
|
||||||
<option value="recaptcha">reCAPTCHA</option>
|
{ value: 'hcaptcha', label: 'hCaptcha' },
|
||||||
<option value="turnstile">Turnstile</option>
|
{ value: 'mcaptcha', label: 'mCaptcha' },
|
||||||
<option value="testcaptcha">testCaptcha</option>
|
{ value: 'recaptcha', label: 'reCAPTCHA' },
|
||||||
|
{ value: 'turnstile', label: 'Turnstile' },
|
||||||
|
{ value: 'testcaptcha', label: 'testCaptcha' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
|
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
|
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<SearchMarker :keywords="['entrance', 'welcome', 'landing', 'front', 'home', 'page', 'style']">
|
<SearchMarker :keywords="['entrance', 'welcome', 'landing', 'front', 'home', 'page', 'style']">
|
||||||
<MkRadios v-model="entrancePageStyle">
|
<MkRadios
|
||||||
|
v-model="entrancePageStyle"
|
||||||
|
:options="[
|
||||||
|
{ value: 'classic' },
|
||||||
|
{ value: 'simple' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings.entrancePageStyle }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts._serverSettings.entrancePageStyle }}</SearchLabel></template>
|
||||||
<option value="classic">Classic</option>
|
|
||||||
<option value="simple">Simple</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
|
|
@ -151,8 +155,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import type { ClientOptions } from '@/instance.js';
|
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
@ -168,11 +172,11 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
const entrancePageStyle = ref<ClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
|
const entrancePageStyle = ref<Misskey.entities.MetaClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
const showTimelineForVisitor = ref<ClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
|
const showTimelineForVisitor = ref<Misskey.entities.MetaClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
const showActivitiesForVisitor = ref<ClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
|
const showActivitiesForVisitor = ref<Misskey.entities.MetaClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
|
||||||
|
|
||||||
const iconUrl = ref(meta.iconUrl);
|
const iconUrl = ref(meta.iconUrl);
|
||||||
const app192IconUrl = ref(meta.app192IconUrl);
|
const app192IconUrl = ref(meta.app192IconUrl);
|
||||||
|
|
@ -191,11 +195,11 @@ const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
clientOptions: ({
|
clientOptions: {
|
||||||
entrancePageStyle: entrancePageStyle.value,
|
entrancePageStyle: entrancePageStyle.value,
|
||||||
showTimelineForVisitor: showTimelineForVisitor.value,
|
showTimelineForVisitor: showTimelineForVisitor.value,
|
||||||
showActivitiesForVisitor: showActivitiesForVisitor.value,
|
showActivitiesForVisitor: showActivitiesForVisitor.value,
|
||||||
} as ClientOptions) as any,
|
},
|
||||||
iconUrl: iconUrl.value,
|
iconUrl: iconUrl.value,
|
||||||
app192IconUrl: app192IconUrl.value,
|
app192IconUrl: app192IconUrl.value,
|
||||||
app512IconUrl: app512IconUrl.value,
|
app512IconUrl: app512IconUrl.value,
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
|
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
|
||||||
|
|
||||||
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
|
<MkRadios
|
||||||
<option value="none">{{ i18n.ts.none }}</option>
|
v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection"
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
:options="[
|
||||||
<option value="local">{{ i18n.ts.localOnly }}</option>
|
{ value: 'none', label: i18n.ts.none },
|
||||||
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
|
{ value: 'all', label: i18n.ts.all },
|
||||||
|
{ value: 'local', label: i18n.ts.localOnly },
|
||||||
|
{ value: 'remote', label: i18n.ts.remoteOnly },
|
||||||
|
]"
|
||||||
|
>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<SearchMarker :keywords="['sensitivity']">
|
<SearchMarker :keywords="['sensitivity']">
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #default="{ item, index, dragStart }">
|
<template #default="{ item, index, dragStart }">
|
||||||
<div :class="$style.item">
|
<div :class="$style.item">
|
||||||
<div :class="$style.itemHeader">
|
<div :class="$style.itemHeader">
|
||||||
<div :class="$style.itemNumber" v-text="String(index + 1)"/>
|
<div :class="$style.itemNumber">{{ index + 1 }}</div>
|
||||||
<span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"/></span>
|
<span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></span>
|
||||||
<button class="_button" :class="$style.itemRemove" @click="remove(item.id)"><i class="ti ti-x"></i></button>
|
<button class="_button" :class="$style.itemRemove" @click="remove(item.id)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<MkInput :modelValue="item.text" @update:modelValue="serverRules[index].text = $event"/>
|
<MkInput :modelValue="item.text" @update:modelValue="serverRules[index].text = $event"/>
|
||||||
|
|
|
||||||
|
|
@ -258,11 +258,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<SearchMarker>
|
<SearchMarker>
|
||||||
<MkRadios v-model="federationForm.state.federation">
|
<MkRadios
|
||||||
|
v-model="federationForm.state.federation"
|
||||||
|
:options="[
|
||||||
|
{ value: 'all', label: i18n.ts.all },
|
||||||
|
{ value: 'specified', label: i18n.ts.specifyHost },
|
||||||
|
{ value: 'none', label: i18n.ts.none },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
|
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="specified">{{ i18n.ts.specifyHost }}</option>
|
|
||||||
<option value="none">{{ i18n.ts.none }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ entity.name || entity.url }}</template>
|
<template #label>{{ entity.name || entity.url }}</template>
|
||||||
<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
|
<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
|
<i v-if="!entity.isActive" class="ti ti-player-pause"></i>
|
||||||
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
|
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"></i>
|
||||||
<i
|
<i
|
||||||
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
|
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
|
||||||
class="ti ti-check"
|
class="ti ti-check"
|
||||||
:style="{ color: 'var(--MI_THEME-success)' }"
|
:style="{ color: 'var(--MI_THEME-success)' }"
|
||||||
/>
|
></i>
|
||||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/>
|
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i>
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
|
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<div v-if="app.permission.length > 0">
|
<div v-if="permissions.length > 0">
|
||||||
<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
|
<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="p in app.permission" :key="p">{{ (i18n.ts._permissions as any)[p] ?? p }}</li>
|
<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] ?? p }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
|
<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
|
||||||
|
|
@ -37,6 +37,10 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const app = computed(() => props.session.app);
|
const app = computed(() => props.session.app);
|
||||||
|
|
||||||
|
const permissions = computed(() => {
|
||||||
|
return props.session.app.permission.filter((p): p is typeof Misskey.permissions[number] => typeof p === 'string');
|
||||||
|
});
|
||||||
|
|
||||||
const name = computed(() => {
|
const name = computed(() => {
|
||||||
const el = window.document.createElement('div');
|
const el = window.document.createElement('div');
|
||||||
el.textContent = app.value.name;
|
el.textContent = app.value.name;
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
|
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
|
||||||
<template #prefix><i class="ti ti-search"></i></template>
|
<template #prefix><i class="ti ti-search"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-model="searchType" @update:modelValue="search()">
|
<MkRadios
|
||||||
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
|
v-model="searchType"
|
||||||
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
|
:options="[
|
||||||
|
{ value: 'nameAndDescription', label: i18n.ts._channel.nameAndDescription },
|
||||||
|
{ value: 'nameOnly', label: i18n.ts._channel.nameOnly },
|
||||||
|
]"
|
||||||
|
@update:modelValue="search()"
|
||||||
|
>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,15 +77,17 @@ import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
type SearchType = 'nameAndDescription' | 'nameOnly';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
query: string;
|
query: string;
|
||||||
type?: string;
|
type?: SearchType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const key = ref('');
|
const key = ref('');
|
||||||
const tab = ref('featured');
|
const tab = ref('featured');
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const searchType = ref('nameAndDescription');
|
const searchType = ref<SearchType>('nameAndDescription');
|
||||||
const channelPaginator = shallowRef();
|
const channelPaginator = shallowRef();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
||||||
<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
||||||
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
||||||
<canvas ref="canvasEl" :class="$style.canvas"/>
|
<canvas ref="canvasEl" :class="$style.canvas"></canvas>
|
||||||
<Transition
|
<Transition
|
||||||
:enterActiveClass="$style.transition_combo_enterActive"
|
:enterActiveClass="$style.transition_combo_enterActive"
|
||||||
:leaveActiveClass="$style.transition_combo_leaveActive"
|
:leaveActiveClass="$style.transition_combo_leaveActive"
|
||||||
|
|
@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</Transition>
|
</Transition>
|
||||||
<template v-if="dropReady && currentPick">
|
<template v-if="dropReady && currentPick">
|
||||||
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/>
|
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/>
|
||||||
<div :class="$style.dropGuide"/>
|
<div :class="$style.dropGuide"></div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
|
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ async function run() {
|
||||||
const version = utils.getLangVersion(flash.value.script);
|
const version = utils.getLangVersion(flash.value.script);
|
||||||
const isLegacy = getIsLegacy(version);
|
const isLegacy = getIsLegacy(version);
|
||||||
|
|
||||||
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
|
const { Interpreter, Parser, values } = (isLegacy ? (await import('@syuilo/aiscript-0-19-0')) : await import('@syuilo/aiscript')) as typeof import('@syuilo/aiscript');
|
||||||
|
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<p class="acct">@{{ acct(displayUser(req)) }}</p>
|
<p class="acct">@{{ acct(displayUser(req)) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tab === 'list'" class="commands">
|
<div v-if="tab === 'list'" class="commands">
|
||||||
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"></i> {{ i18n.ts.accept }}</MkButton>
|
||||||
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"></i> {{ i18n.ts.reject }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="commands">
|
<div v-else class="commands">
|
||||||
<MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton>
|
<MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,22 +35,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFolder :defaultOpen="true">
|
<MkFolder :defaultOpen="true">
|
||||||
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
||||||
|
|
||||||
<MkRadios v-model="game.bw">
|
<MkRadios
|
||||||
<option value="random">{{ i18n.ts.random }}</option>
|
v-model="game.bw"
|
||||||
<option :value="'1'">
|
:options="[
|
||||||
|
{ value: 'random', label: i18n.ts.random },
|
||||||
|
{ value: '1', slotId: 'user1' },
|
||||||
|
{ value: '2', slotId: 'user2' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template #option-user1>
|
||||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||||
<template #name>
|
<template #name>
|
||||||
<b><MkUserName :user="game.user1"/></b>
|
<b><MkUserName :user="game.user1"/></b>
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
</option>
|
</template>
|
||||||
<option :value="'2'">
|
<template #option-user2>
|
||||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||||
<template #name>
|
<template #name>
|
||||||
<b><MkUserName :user="game.user2"/></b>
|
<b><MkUserName :user="game.user2"/></b>
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
</option>
|
</template>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
|
@ -58,15 +64,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
||||||
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
||||||
|
|
||||||
<MkRadios v-model="game.timeLimitForEachTurn">
|
<MkRadios
|
||||||
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
v-model="game.timeLimitForEachTurn"
|
||||||
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
:options="gameTurnOptionsDef"
|
||||||
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
>
|
||||||
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
|
@ -110,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
|
import { computed, watch, ref, onUnmounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import * as Reversi from 'misskey-reversi';
|
import * as Reversi from 'misskey-reversi';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
|
@ -122,6 +123,7 @@ import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import type { MkRadiosOption } from '@/components/MkRadios.vue';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
@ -139,6 +141,17 @@ const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false }
|
||||||
|
|
||||||
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
||||||
|
|
||||||
|
const gameTurnOptionsDef = [
|
||||||
|
{ value: 5, label: '5' + i18n.ts._time.second },
|
||||||
|
{ value: 10, label: '10' + i18n.ts._time.second },
|
||||||
|
{ value: 30, label: '30' + i18n.ts._time.second },
|
||||||
|
{ value: 60, label: '60' + i18n.ts._time.second },
|
||||||
|
{ value: 90, label: '90' + i18n.ts._time.second },
|
||||||
|
{ value: 120, label: '120' + i18n.ts._time.second },
|
||||||
|
{ value: 180, label: '180' + i18n.ts._time.second },
|
||||||
|
{ value: 3600, label: '3600' + i18n.ts._time.second },
|
||||||
|
] as MkRadiosOption<number>[];
|
||||||
|
|
||||||
const mapName = computed(() => {
|
const mapName = computed(() => {
|
||||||
if (game.value.map == null) return 'Random';
|
if (game.value.map == null) return 'Random';
|
||||||
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
|
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header>{{ i18n.ts.options }}</template>
|
<template #header>{{ i18n.ts.options }}</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkRadios v-model="searchScope">
|
<MkRadios
|
||||||
<option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="all">{{ i18n.ts._search.searchScopeAll }}</option>
|
v-model="searchScope"
|
||||||
<option value="local">{{ instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal }}</option>
|
:options="searchScopeDef"
|
||||||
<option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="server">{{ i18n.ts._search.searchScopeServer }}</option>
|
>
|
||||||
<option value="user">{{ i18n.ts._search.searchScopeUser }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot">
|
<div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot">
|
||||||
|
|
@ -127,6 +126,7 @@ import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
import type { MkRadiosOption } from '@/components/MkRadios.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
query?: string;
|
query?: string;
|
||||||
|
|
@ -183,6 +183,24 @@ const searchScope = ref<'all' | 'local' | 'server' | 'user'>((() => {
|
||||||
return 'all';
|
return 'all';
|
||||||
})());
|
})());
|
||||||
|
|
||||||
|
const searchScopeDef = computed<MkRadiosOption[]>(() => {
|
||||||
|
const options: MkRadiosOption[] = [];
|
||||||
|
|
||||||
|
if (instance.federation !== 'none' && noteSearchableScope === 'global') {
|
||||||
|
options.push({ value: 'all', label: i18n.ts._search.searchScopeAll });
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ value: 'local', label: instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal });
|
||||||
|
|
||||||
|
if (instance.federation !== 'none' && noteSearchableScope === 'global') {
|
||||||
|
options.push({ value: 'server', label: i18n.ts._search.searchScopeServer });
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ value: 'user', label: i18n.ts._search.searchScopeUser });
|
||||||
|
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
type SearchParams = {
|
type SearchParams = {
|
||||||
readonly query: string;
|
readonly query: string;
|
||||||
readonly host?: string;
|
readonly host?: string;
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
|
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
|
||||||
<template #prefix><i class="ti ti-search"></i></template>
|
<template #prefix><i class="ti ti-search"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-if="instance.federation !== 'none'" v-model="searchOrigin" @update:modelValue="search()">
|
<MkRadios
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
v-if="instance.federation !== 'none'"
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
v-model="searchOrigin"
|
||||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
:options="[
|
||||||
|
{ value: 'combined', label: i18n.ts.all },
|
||||||
|
{ value: 'local', label: i18n.ts.local },
|
||||||
|
{ value: 'remote', label: i18n.ts.remote },
|
||||||
|
]"
|
||||||
|
@update:modelValue="search()"
|
||||||
|
>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
|
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
|
||||||
|
|
||||||
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
|
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
|
||||||
<div v-text="i18n.ts._2fa.alreadyRegistered"/>
|
<div>{{ i18n.ts._2fa.alreadyRegistered }}</div>
|
||||||
<template v-if="$i.securityKeysList!.length > 0">
|
<template v-if="$i.securityKeysList!.length > 0">
|
||||||
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
|
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
|
||||||
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
|
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
|
||||||
|
|
|
||||||
|
|
@ -40,31 +40,43 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<SearchMarker :keywords="['column', 'align']">
|
<SearchMarker :keywords="['column', 'align']">
|
||||||
<MkPreferenceContainer k="deck.columnAlign">
|
<MkPreferenceContainer k="deck.columnAlign">
|
||||||
<MkRadios v-model="columnAlign">
|
<MkRadios
|
||||||
|
v-model="columnAlign"
|
||||||
|
:options="[
|
||||||
|
{ value: 'left', label: i18n.ts.left },
|
||||||
|
{ value: 'center', label: i18n.ts.center },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts._deck.columnAlign }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts._deck.columnAlign }}</SearchLabel></template>
|
||||||
<option value="left">{{ i18n.ts.left }}</option>
|
|
||||||
<option value="center">{{ i18n.ts.center }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['menu', 'position']">
|
<SearchMarker :keywords="['menu', 'position']">
|
||||||
<MkPreferenceContainer k="deck.menuPosition">
|
<MkPreferenceContainer k="deck.menuPosition">
|
||||||
<MkRadios v-model="menuPosition">
|
<MkRadios
|
||||||
|
v-model="menuPosition"
|
||||||
|
:options="[
|
||||||
|
{ value: 'right', label: i18n.ts.right },
|
||||||
|
{ value: 'bottom', label: i18n.ts.bottom },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts._deck.deckMenuPosition }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts._deck.deckMenuPosition }}</SearchLabel></template>
|
||||||
<option value="right">{{ i18n.ts.right }}</option>
|
|
||||||
<option value="bottom">{{ i18n.ts.bottom }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['navbar', 'position']">
|
<SearchMarker :keywords="['navbar', 'position']">
|
||||||
<MkPreferenceContainer k="deck.navbarPosition">
|
<MkPreferenceContainer k="deck.navbarPosition">
|
||||||
<MkRadios v-model="navbarPosition">
|
<MkRadios
|
||||||
|
v-model="navbarPosition"
|
||||||
|
:options="[
|
||||||
|
{ value: 'left', label: i18n.ts.left },
|
||||||
|
{ value: 'top', label: i18n.ts.top },
|
||||||
|
{ value: 'bottom', label: i18n.ts.bottom },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts._deck.navbarPosition }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts._deck.navbarPosition }}</SearchLabel></template>
|
||||||
<option value="left">{{ i18n.ts.left }}</option>
|
|
||||||
<option value="top">{{ i18n.ts.top }}</option>
|
|
||||||
<option value="bottom">{{ i18n.ts.bottom }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
|
||||||
|
|
@ -63,38 +63,33 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<SearchMarker :keywords="['emoji', 'picker', 'scale', 'size']">
|
<SearchMarker :keywords="['emoji', 'picker', 'scale', 'size']">
|
||||||
<MkPreferenceContainer k="emojiPickerScale">
|
<MkPreferenceContainer k="emojiPickerScale">
|
||||||
<MkRadios v-model="emojiPickerScale">
|
<MkRadios
|
||||||
|
v-model="emojiPickerScale"
|
||||||
|
:options="emojiPickerScaleDef"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.size }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.size }}</SearchLabel></template>
|
||||||
<option :value="1">{{ i18n.ts.small }}</option>
|
|
||||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
|
||||||
<option :value="3">{{ i18n.ts.large }}</option>
|
|
||||||
<option :value="4">{{ i18n.ts.large }}+</option>
|
|
||||||
<option :value="5">{{ i18n.ts.large }}++</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['emoji', 'picker', 'width', 'column', 'size']">
|
<SearchMarker :keywords="['emoji', 'picker', 'width', 'column', 'size']">
|
||||||
<MkPreferenceContainer k="emojiPickerWidth">
|
<MkPreferenceContainer k="emojiPickerWidth">
|
||||||
<MkRadios v-model="emojiPickerWidth">
|
<MkRadios
|
||||||
|
v-model="emojiPickerWidth"
|
||||||
|
:options="emojiPickerWidthDef"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.numberOfColumn }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.numberOfColumn }}</SearchLabel></template>
|
||||||
<option :value="1">5</option>
|
|
||||||
<option :value="2">6</option>
|
|
||||||
<option :value="3">7</option>
|
|
||||||
<option :value="4">8</option>
|
|
||||||
<option :value="5">9</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['emoji', 'picker', 'height', 'size']">
|
<SearchMarker :keywords="['emoji', 'picker', 'height', 'size']">
|
||||||
<MkPreferenceContainer k="emojiPickerHeight">
|
<MkPreferenceContainer k="emojiPickerHeight">
|
||||||
<MkRadios v-model="emojiPickerHeight">
|
<MkRadios
|
||||||
|
v-model="emojiPickerHeight"
|
||||||
|
:options="emojiPickerHeightDef"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.height }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.height }}</SearchLabel></template>
|
||||||
<option :value="1">{{ i18n.ts.small }}</option>
|
|
||||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
|
||||||
<option :value="3">{{ i18n.ts.large }}</option>
|
|
||||||
<option :value="4">{{ i18n.ts.large }}+</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
@ -126,6 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import XPalette from './emoji-palette.palette.vue';
|
import XPalette from './emoji-palette.palette.vue';
|
||||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
|
import type { MkRadiosOption } from '@/components/MkRadios.vue';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
|
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
|
|
@ -158,8 +154,31 @@ const emojiPaletteForMainDef = computed<MkSelectItem[]>(() => [
|
||||||
})),
|
})),
|
||||||
]);
|
]);
|
||||||
const emojiPickerScale = prefer.model('emojiPickerScale');
|
const emojiPickerScale = prefer.model('emojiPickerScale');
|
||||||
|
const emojiPickerScaleDef = [
|
||||||
|
{ label: i18n.ts.small, value: 1 },
|
||||||
|
{ label: i18n.ts.medium, value: 2 },
|
||||||
|
{ label: i18n.ts.large, value: 3 },
|
||||||
|
{ label: i18n.ts.large + '+', value: 4 },
|
||||||
|
{ label: i18n.ts.large + '++', value: 5 },
|
||||||
|
] as MkRadiosOption<number>[];
|
||||||
|
|
||||||
const emojiPickerWidth = prefer.model('emojiPickerWidth');
|
const emojiPickerWidth = prefer.model('emojiPickerWidth');
|
||||||
|
const emojiPickerWidthDef = [
|
||||||
|
{ label: '5', value: 1 },
|
||||||
|
{ label: '6', value: 2 },
|
||||||
|
{ label: '7', value: 3 },
|
||||||
|
{ label: '8', value: 4 },
|
||||||
|
{ label: '9', value: 5 },
|
||||||
|
] as MkRadiosOption<number>[];
|
||||||
|
|
||||||
const emojiPickerHeight = prefer.model('emojiPickerHeight');
|
const emojiPickerHeight = prefer.model('emojiPickerHeight');
|
||||||
|
const emojiPickerHeightDef = [
|
||||||
|
{ label: i18n.ts.small, value: 1 },
|
||||||
|
{ label: i18n.ts.medium, value: 2 },
|
||||||
|
{ label: i18n.ts.large, value: 3 },
|
||||||
|
{ label: i18n.ts.large + '+', value: 4 },
|
||||||
|
] as MkRadiosOption<number>[];
|
||||||
|
|
||||||
const emojiPickerStyle = prefer.model('emojiPickerStyle');
|
const emojiPickerStyle = prefer.model('emojiPickerStyle');
|
||||||
|
|
||||||
const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));
|
const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkRadios v-model="menuDisplay">
|
<MkRadios
|
||||||
|
v-model="menuDisplay"
|
||||||
|
:options="[
|
||||||
|
{ value: 'sideFull', label: i18n.ts._menuDisplay.sideFull },
|
||||||
|
{ value: 'sideIcon', label: i18n.ts._menuDisplay.sideIcon },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.display }}</template>
|
<template #label>{{ i18n.ts.display }}</template>
|
||||||
<option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
|
|
||||||
<option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']">
|
<SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']">
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #key>{{ i18n.ts.permission }}</template>
|
<template #key>{{ i18n.ts.permission }}</template>
|
||||||
<template #value>
|
<template #value>
|
||||||
<ul style="margin-top: 0; margin-bottom: 0;">
|
<ul style="margin-top: 0; margin-bottom: 0;">
|
||||||
<li v-for="permission in plugin.permissions" :key="permission">{{ (i18n.ts._permissions as any)[permission] ?? permission }}</li>
|
<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
|
||||||
<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
|
<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
|
<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
|
||||||
<MkRadios v-model="overridedDeviceKind">
|
<MkRadios
|
||||||
|
v-model="overridedDeviceKind"
|
||||||
|
:options="[
|
||||||
|
{ value: null, label: i18n.ts.auto },
|
||||||
|
{ value: 'smartphone', label: i18n.ts.smartphone, icon: 'ti ti-device-mobile' },
|
||||||
|
{ value: 'tablet', label: i18n.ts.tablet, icon: 'ti ti-device-tablet' },
|
||||||
|
{ value: 'desktop', label: i18n.ts.desktop, icon: 'ti ti-device-desktop' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
|
||||||
<option :value="null">{{ i18n.ts.auto }}</option>
|
|
||||||
<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
|
|
||||||
<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
|
|
||||||
<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
|
|
@ -121,11 +125,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
|
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
|
||||||
<MkPreferenceContainer k="emojiStyle">
|
<MkPreferenceContainer k="emojiStyle">
|
||||||
<div>
|
<div>
|
||||||
<MkRadios v-model="emojiStyle">
|
<MkRadios
|
||||||
|
v-model="emojiStyle"
|
||||||
|
:options="[
|
||||||
|
{ value: 'native', label: i18n.ts.native },
|
||||||
|
{ value: 'fluentEmoji', label: 'Fluent Emoji' },
|
||||||
|
{ value: 'twemoji', label: 'Twemoji' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
|
||||||
<option value="native">{{ i18n.ts.native }}</option>
|
|
||||||
<option value="fluentEmoji">Fluent Emoji</option>
|
|
||||||
<option value="twemoji">Twemoji</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
|
<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -240,11 +248,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
|
<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
|
||||||
<MkPreferenceContainer k="reactionsDisplaySize">
|
<MkPreferenceContainer k="reactionsDisplaySize">
|
||||||
<MkRadios v-model="reactionsDisplaySize">
|
<MkRadios
|
||||||
|
v-model="reactionsDisplaySize"
|
||||||
|
:options="[
|
||||||
|
{ value: 'small', label: i18n.ts.small },
|
||||||
|
{ value: 'medium', label: i18n.ts.medium },
|
||||||
|
{ value: 'large', label: i18n.ts.large },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
|
||||||
<option value="small">{{ i18n.ts.small }}</option>
|
|
||||||
<option value="medium">{{ i18n.ts.medium }}</option>
|
|
||||||
<option value="large">{{ i18n.ts.large }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
@ -259,12 +271,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
|
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
|
||||||
<MkPreferenceContainer k="mediaListWithOneImageAppearance">
|
<MkPreferenceContainer k="mediaListWithOneImageAppearance">
|
||||||
<MkRadios v-model="mediaListWithOneImageAppearance">
|
<MkRadios
|
||||||
|
v-model="mediaListWithOneImageAppearance"
|
||||||
|
:options="[
|
||||||
|
{ value: 'expand', label: i18n.ts.default },
|
||||||
|
{ value: '16_9', label: i18n.tsx.limitTo({ x: '16:9' }) },
|
||||||
|
{ value: '1_1', label: i18n.tsx.limitTo({ x: '1:1' }) },
|
||||||
|
{ value: '2_3', label: i18n.tsx.limitTo({ x: '2:3' }) },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
|
||||||
<option value="expand">{{ i18n.ts.default }}</option>
|
|
||||||
<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
|
|
||||||
<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
|
|
||||||
<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
@ -394,22 +410,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<SearchMarker :keywords="['position']">
|
<SearchMarker :keywords="['position']">
|
||||||
<MkPreferenceContainer k="notificationPosition">
|
<MkPreferenceContainer k="notificationPosition">
|
||||||
<MkRadios v-model="notificationPosition">
|
<MkRadios
|
||||||
|
v-model="notificationPosition"
|
||||||
|
:options="[
|
||||||
|
{ value: 'leftTop', label: i18n.ts.leftTop, icon: 'ti ti-align-box-left-top' },
|
||||||
|
{ value: 'rightTop', label: i18n.ts.rightTop, icon: 'ti ti-align-box-right-top' },
|
||||||
|
{ value: 'leftBottom', label: i18n.ts.leftBottom, icon: 'ti ti-align-box-left-bottom' },
|
||||||
|
{ value: 'rightBottom', label: i18n.ts.rightBottom, icon: 'ti ti-align-box-right-bottom' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
|
||||||
<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
|
|
||||||
<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
|
|
||||||
<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
|
|
||||||
<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['stack', 'axis', 'direction']">
|
<SearchMarker :keywords="['stack', 'axis', 'direction']">
|
||||||
<MkPreferenceContainer k="notificationStackAxis">
|
<MkPreferenceContainer k="notificationStackAxis">
|
||||||
<MkRadios v-model="notificationStackAxis">
|
<MkRadios
|
||||||
|
v-model="notificationStackAxis"
|
||||||
|
:options="[
|
||||||
|
{ value: 'vertical', label: i18n.ts.vertical, icon: 'ti ti-carousel-vertical' },
|
||||||
|
{ value: 'horizontal', label: i18n.ts.horizontal, icon: 'ti ti-carousel-horizontal' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
|
||||||
<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
|
|
||||||
<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
@ -578,12 +602,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['font', 'size']">
|
<SearchMarker :keywords="['font', 'size']">
|
||||||
<MkRadios v-model="fontSize">
|
<MkRadios
|
||||||
|
v-model="fontSize"
|
||||||
|
:options="[
|
||||||
|
{ value: null, label: 'Aa', labelStyle: 'font-size: 14px;' },
|
||||||
|
{ value: '1', label: 'Aa', labelStyle: 'font-size: 15px;' },
|
||||||
|
{ value: '2', label: 'Aa', labelStyle: 'font-size: 16px;' },
|
||||||
|
{ value: '3', label: 'Aa', labelStyle: 'font-size: 17px;' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
|
||||||
<option :value="null"><span style="font-size: 14px;">Aa</span></option>
|
|
||||||
<option value="1"><span style="font-size: 15px;">Aa</span></option>
|
|
||||||
<option value="2"><span style="font-size: 16px;">Aa</span></option>
|
|
||||||
<option value="3"><span style="font-size: 17px;">Aa</span></option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
|
|
@ -792,10 +820,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<SearchMarker>
|
<SearchMarker>
|
||||||
<MkPreferenceContainer k="hemisphere">
|
<MkPreferenceContainer k="hemisphere">
|
||||||
<MkRadios v-model="hemisphere">
|
<MkRadios
|
||||||
|
v-model="hemisphere"
|
||||||
|
:options="[
|
||||||
|
{ value: 'N', label: i18n.ts._hemisphere.N },
|
||||||
|
{ value: 'S', label: i18n.ts._hemisphere.S },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
|
||||||
<option value="N">{{ i18n.ts._hemisphere.N }}</option>
|
|
||||||
<option value="S">{{ i18n.ts._hemisphere.S }}</option>
|
|
||||||
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
|
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
|
|
@ -925,7 +957,7 @@ const contextMenu = prefer.model('contextMenu');
|
||||||
const menuStyle = prefer.model('menuStyle');
|
const menuStyle = prefer.model('menuStyle');
|
||||||
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
|
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
|
||||||
|
|
||||||
const fontSize = ref(miLocalStorage.getItem('fontSize'));
|
const fontSize = ref(miLocalStorage.getItem('fontSize') as '1' | '2' | '3' | null);
|
||||||
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
|
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
|
||||||
|
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
|
|
@ -1051,7 +1083,7 @@ function removePinnedList() {
|
||||||
function enableAllDataSaver() {
|
function enableAllDataSaver() {
|
||||||
const g = { ...prefer.s.dataSaver };
|
const g = { ...prefer.s.dataSaver };
|
||||||
|
|
||||||
Object.keys(g).forEach((key) => { (g as any)[key] = true; });
|
(Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = true; });
|
||||||
|
|
||||||
dataSaver.value = g;
|
dataSaver.value = g;
|
||||||
}
|
}
|
||||||
|
|
@ -1059,7 +1091,7 @@ function enableAllDataSaver() {
|
||||||
function disableAllDataSaver() {
|
function disableAllDataSaver() {
|
||||||
const g = { ...prefer.s.dataSaver };
|
const g = { ...prefer.s.dataSaver };
|
||||||
|
|
||||||
Object.keys(g).forEach((key) => { (g as any)[key] = false; });
|
(Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = false; });
|
||||||
|
|
||||||
dataSaver.value = g;
|
dataSaver.value = g;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>Black</template>
|
<template #label>Black</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkRadios v-model="statusbar.size">
|
<MkRadios
|
||||||
|
v-model="statusbar.size"
|
||||||
|
:options="[
|
||||||
|
{ value: 'verySmall', label: i18n.ts.small + '+' },
|
||||||
|
{ value: 'small', label: i18n.ts.small },
|
||||||
|
{ value: 'medium', label: i18n.ts.medium },
|
||||||
|
{ value: 'large', label: i18n.ts.large },
|
||||||
|
{ value: 'veryLarge', label: i18n.ts.large + '+' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.size }}</template>
|
<template #label>{{ i18n.ts.size }}</template>
|
||||||
<option value="verySmall">{{ i18n.ts.small }}+</option>
|
|
||||||
<option value="small">{{ i18n.ts.small }}</option>
|
|
||||||
<option value="medium">{{ i18n.ts.medium }}</option>
|
|
||||||
<option value="large">{{ i18n.ts.large }}</option>
|
|
||||||
<option value="veryLarge">{{ i18n.ts.large }}+</option>
|
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<template v-if="statusbar.type === 'rss'">
|
<template v-if="statusbar.type === 'rss'">
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="user.isLocked"><i class="ti ti-lock"></i></span>
|
<span v-if="user.isLocked"><i class="ti ti-lock"></i></span>
|
||||||
<span v-if="user.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isBot"><i class="ti ti-robot"></i></span>
|
||||||
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
||||||
<i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
|
<i class="ti ti-edit"></i> {{ i18n.ts.addMemo }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
|
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
|
||||||
<div class="heading" v-text="i18n.ts.memo"/>
|
<div class="heading">{{ i18n.ts.memo }}</div>
|
||||||
<textarea
|
<textarea
|
||||||
ref="memoTextareaEl"
|
ref="memoTextareaEl"
|
||||||
v-model="memoDraft"
|
v-model="memoDraft"
|
||||||
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@focus="isEditingMemo = true"
|
@focus="isEditingMemo = true"
|
||||||
@blur="updateMemo"
|
@blur="updateMemo"
|
||||||
@input="adjustMemoTextarea"
|
@input="adjustMemoTextarea"
|
||||||
/>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<MkOmit>
|
<MkOmit>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export type Plugin = {
|
||||||
version: string;
|
version: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions?: string[];
|
permissions?: (typeof Misskey.permissions)[number][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AiScriptPluginMeta = {
|
export type AiScriptPluginMeta = {
|
||||||
|
|
@ -34,7 +34,7 @@ export type AiScriptPluginMeta = {
|
||||||
version: string;
|
version: string;
|
||||||
author: string;
|
author: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions?: string[];
|
permissions?: (typeof Misskey.permissions)[number][];
|
||||||
config?: Record<string, any>;
|
config?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
|
||||||
version: version as string,
|
version: version as string,
|
||||||
author: author as string,
|
author: author as string,
|
||||||
description: description as string | undefined,
|
description: description as string | undefined,
|
||||||
permissions: permissions as string[] | undefined,
|
permissions: permissions as (typeof Misskey.permissions)[number][] | undefined,
|
||||||
config: config as Record<string, any> | undefined,
|
config: config as Record<string, any> | undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { unisonReload } from '@/utility/unison-reload.js';
|
import { unisonReload } from '@/utility/unison-reload.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import type { SoundStore } from '@/preferences/def.js';
|
||||||
|
|
||||||
// TODO: そのうち消す
|
// TODO: そのうち消す
|
||||||
export function migrateOldSettings() {
|
export function migrateOldSettings() {
|
||||||
|
|
@ -126,10 +127,10 @@ export function migrateOldSettings() {
|
||||||
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
|
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
|
||||||
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
|
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
|
||||||
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
|
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
|
||||||
prefer.commit('sound.on.note', store.s.sound_note as any);
|
prefer.commit('sound.on.note', store.s.sound_note as SoundStore);
|
||||||
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
|
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as SoundStore);
|
||||||
prefer.commit('sound.on.notification', store.s.sound_notification as any);
|
prefer.commit('sound.on.notification', store.s.sound_notification as SoundStore);
|
||||||
prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
|
prefer.commit('sound.on.reaction', store.s.sound_reaction as SoundStore);
|
||||||
prefer.commit('defaultNoteVisibility', store.s.defaultNoteVisibility);
|
prefer.commit('defaultNoteVisibility', store.s.defaultNoteVisibility);
|
||||||
prefer.commit('defaultNoteLocalOnly', store.s.defaultNoteLocalOnly);
|
prefer.commit('defaultNoteLocalOnly', store.s.defaultNoteLocalOnly);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ export const PREF_DEF = definePreferences({
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
emojiStyle: {
|
emojiStyle: {
|
||||||
default: 'twemoji', // twemoji / fluentEmoji / native
|
default: 'twemoji' as 'native' | 'fluentEmoji' | 'twemoji',
|
||||||
},
|
},
|
||||||
menuStyle: {
|
menuStyle: {
|
||||||
default: 'auto' as 'auto' | 'popup' | 'drawer',
|
default: 'auto' as 'auto' | 'popup' | 'drawer',
|
||||||
|
|
@ -503,7 +503,7 @@ export const PREF_DEF = definePreferences({
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
'deck.columnAlign': {
|
'deck.columnAlign': {
|
||||||
default: 'center' as 'left' | 'right' | 'center',
|
default: 'center' as 'left' | 'center',
|
||||||
},
|
},
|
||||||
'deck.columnGap': {
|
'deck.columnGap': {
|
||||||
default: 6,
|
default: 6,
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { markRaw, ref } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import lightTheme from '@@/themes/l-light.json5';
|
|
||||||
import darkTheme from '@@/themes/d-green-lime.json5';
|
|
||||||
import { prefersReducedMotion } from '@@/js/config.js';
|
import { prefersReducedMotion } from '@@/js/config.js';
|
||||||
import { hemisphere } from '@@/js/intl-const.js';
|
import { hemisphere } from '@@/js/intl-const.js';
|
||||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||||
import type { Plugin } from '@/plugin.js';
|
|
||||||
import type { TIPS } from '@/tips.js';
|
import type { TIPS } from '@/tips.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
|
||||||
import { Pizzax } from '@/lib/pizzax.js';
|
import { Pizzax } from '@/lib/pizzax.js';
|
||||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||||
|
|
||||||
|
|
@ -83,7 +79,7 @@ export const store = markRaw(new Pizzax('base', {
|
||||||
},
|
},
|
||||||
menuDisplay: {
|
menuDisplay: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
|
default: 'sideFull' as 'sideFull' | 'sideIcon'/* | 'top' */,
|
||||||
},
|
},
|
||||||
postFormWithHashtags: {
|
postFormWithHashtags: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
|
@ -257,7 +253,7 @@ export const store = markRaw(new Pizzax('base', {
|
||||||
},
|
},
|
||||||
emojiStyle: {
|
emojiStyle: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 'twemoji', // twemoji / fluentEmoji / native
|
default: 'twemoji' as 'twemoji' | 'fluentEmoji' | 'native',
|
||||||
},
|
},
|
||||||
menuStyle: {
|
menuStyle: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { Component, ComputedRef, Ref, MaybeRef } from 'vue';
|
import type { Component, ComputedRef, Ref, MaybeRef } from 'vue';
|
||||||
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||||
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
|
|
||||||
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: MaybeRef<CP<T>[K]> };
|
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: MaybeRef<CP<T>[K]> };
|
||||||
|
|
||||||
type MenuRadioOptionsDef = Record<string, any>;
|
type MenuRadioOptionsDef = Record<string, OptionValue>;
|
||||||
|
|
||||||
type Text = string | ComputedRef<string>;
|
type Text = string | ComputedRef<string>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type OptionValue = string | number | null;
|
||||||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/>
|
<i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"></i>
|
||||||
<span style="margin-left: 8px;">{{ column.name || (column.tl ? i18n.ts._timelines[column.tl] : null) || i18n.ts._deck._columns.tl }}</span>
|
<span style="margin-left: 8px;">{{ column.name || (column.tl ? i18n.ts._timelines[column.tl] : null) || i18n.ts._deck._columns.tl }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,15 @@ import { popup } from '@/os.js';
|
||||||
|
|
||||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
|
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
|
||||||
|
|
||||||
|
type CompleteProps<T extends keyof CompleteInfo> = {
|
||||||
|
type: T;
|
||||||
|
value: CompleteInfo[T]['payload'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function isCompleteType<T extends keyof CompleteInfo>(expectedType: T, props: CompleteProps<keyof CompleteInfo>): props is CompleteProps<T> {
|
||||||
|
return props.type === expectedType;
|
||||||
|
}
|
||||||
|
|
||||||
export class Autocomplete {
|
export class Autocomplete {
|
||||||
private suggestion: {
|
private suggestion: {
|
||||||
x: Ref<number>;
|
x: Ref<number>;
|
||||||
|
|
@ -253,19 +262,19 @@ export class Autocomplete {
|
||||||
/**
|
/**
|
||||||
* オートコンプリートする
|
* オートコンプリートする
|
||||||
*/
|
*/
|
||||||
private complete<T extends keyof CompleteInfo>({ type, value }: { type: T; value: CompleteInfo[T]['payload'] }) {
|
private complete<T extends keyof CompleteInfo>(props: CompleteProps<T>) {
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
const caret = Number(this.textarea.selectionStart);
|
const caret = Number(this.textarea.selectionStart);
|
||||||
|
|
||||||
if (type === 'user') {
|
if (isCompleteType('user', props)) {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
||||||
const before = source.substring(0, caret);
|
const before = source.substring(0, caret);
|
||||||
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
|
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
|
||||||
const after = source.substring(caret);
|
const after = source.substring(caret);
|
||||||
|
|
||||||
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
|
const acct = props.value.host === null ? props.value.username : `${props.value.username}@${toASCII(props.value.host)}`;
|
||||||
|
|
||||||
// 挿入
|
// 挿入
|
||||||
this.text = `${trimmedBefore}@${acct} ${after}`;
|
this.text = `${trimmedBefore}@${acct} ${after}`;
|
||||||
|
|
@ -276,7 +285,7 @@ export class Autocomplete {
|
||||||
const pos = trimmedBefore.length + (acct.length + 2);
|
const pos = trimmedBefore.length + (acct.length + 2);
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
} else if (type === 'hashtag') {
|
} else if (isCompleteType('hashtag', props)) {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
||||||
const before = source.substring(0, caret);
|
const before = source.substring(0, caret);
|
||||||
|
|
@ -284,15 +293,15 @@ export class Autocomplete {
|
||||||
const after = source.substring(caret);
|
const after = source.substring(caret);
|
||||||
|
|
||||||
// 挿入
|
// 挿入
|
||||||
this.text = `${trimmedBefore}#${value} ${after}`;
|
this.text = `${trimmedBefore}#${props.value} ${after}`;
|
||||||
|
|
||||||
// キャレットを戻す
|
// キャレットを戻す
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
const pos = trimmedBefore.length + (value.length + 2);
|
const pos = trimmedBefore.length + (props.value.length + 2);
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
} else if (type === 'emoji') {
|
} else if (isCompleteType('emoji', props)) {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
||||||
const before = source.substring(0, caret);
|
const before = source.substring(0, caret);
|
||||||
|
|
@ -300,15 +309,15 @@ export class Autocomplete {
|
||||||
const after = source.substring(caret);
|
const after = source.substring(caret);
|
||||||
|
|
||||||
// 挿入
|
// 挿入
|
||||||
this.text = trimmedBefore + value + after;
|
this.text = trimmedBefore + props.value + after;
|
||||||
|
|
||||||
// キャレットを戻す
|
// キャレットを戻す
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
const pos = trimmedBefore.length + value.length;
|
const pos = trimmedBefore.length + props.value.length;
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
} else if (type === 'emojiComplete') {
|
} else if (isCompleteType('emojiComplete', props)) {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
||||||
const before = source.substring(0, caret);
|
const before = source.substring(0, caret);
|
||||||
|
|
@ -316,15 +325,15 @@ export class Autocomplete {
|
||||||
const after = source.substring(caret);
|
const after = source.substring(caret);
|
||||||
|
|
||||||
// 挿入
|
// 挿入
|
||||||
this.text = trimmedBefore + value + after;
|
this.text = trimmedBefore + props.value + after;
|
||||||
|
|
||||||
// キャレットを戻す
|
// キャレットを戻す
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
const pos = trimmedBefore.length + value.length;
|
const pos = trimmedBefore.length + props.value.length;
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
} else if (type === 'mfmTag') {
|
} else if (isCompleteType('mfmTag', props)) {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
||||||
const before = source.substring(0, caret);
|
const before = source.substring(0, caret);
|
||||||
|
|
@ -332,15 +341,15 @@ export class Autocomplete {
|
||||||
const after = source.substring(caret);
|
const after = source.substring(caret);
|
||||||
|
|
||||||
// 挿入
|
// 挿入
|
||||||
this.text = `${trimmedBefore}$[${value} ]${after}`;
|
this.text = `${trimmedBefore}$[${props.value} ]${after}`;
|
||||||
|
|
||||||
// キャレットを戻す
|
// キャレットを戻す
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
const pos = trimmedBefore.length + (value.length + 3);
|
const pos = trimmedBefore.length + (props.value.length + 3);
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
} else if (type === 'mfmParam') {
|
} else if (isCompleteType('mfmParam', props)) {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
||||||
const before = source.substring(0, caret);
|
const before = source.substring(0, caret);
|
||||||
|
|
@ -348,12 +357,12 @@ export class Autocomplete {
|
||||||
const after = source.substring(caret);
|
const after = source.substring(caret);
|
||||||
|
|
||||||
// 挿入
|
// 挿入
|
||||||
this.text = `${trimmedBefore}.${value}${after}`;
|
this.text = `${trimmedBefore}.${props.value}${after}`;
|
||||||
|
|
||||||
// キャレットを戻す
|
// キャレットを戻す
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
const pos = trimmedBefore.length + (value.length + 1);
|
const pos = trimmedBefore.length + (props.value.length + 1);
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { OptionValue } from '@/components/MkSelect.vue';
|
import type { OptionValue } from '@/types/option-value.js';
|
||||||
|
|
||||||
export type EnumItem = string | {
|
export type EnumItem = string | {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -45,18 +45,18 @@ export interface BooleanFormItem extends FormItemBase {
|
||||||
|
|
||||||
export interface EnumFormItem extends FormItemBase {
|
export interface EnumFormItem extends FormItemBase {
|
||||||
type: 'enum';
|
type: 'enum';
|
||||||
default?: string | null;
|
default?: OptionValue | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
enum: EnumItem[];
|
enum: EnumItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadioFormItem extends FormItemBase {
|
export interface RadioFormItem extends FormItemBase {
|
||||||
type: 'radio';
|
type: 'radio';
|
||||||
default?: unknown | null;
|
default?: OptionValue | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
options: {
|
options: {
|
||||||
label: string;
|
label: string;
|
||||||
value: unknown;
|
value: OptionValue;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
||||||
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
|
|
||||||
const name = 'ai';
|
const name = 'aichan';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
|
|
|
||||||
|
|
@ -93,12 +93,12 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
|
||||||
const menuOpened = ref(false);
|
const menuOpened = ref(false);
|
||||||
|
|
||||||
const headerTitle = computed<string>(() => {
|
const headerTitle = computed<string>(() => {
|
||||||
if (widgetProps.src === 'list' && widgetProps.list != null) {
|
if (widgetProps.src === 'list') {
|
||||||
return widgetProps.list.name;
|
return widgetProps.list != null ? widgetProps.list.name : '?';
|
||||||
} else if (widgetProps.src === 'antenna' && widgetProps.antenna != null) {
|
} else if (widgetProps.src === 'antenna') {
|
||||||
return widgetProps.antenna.name;
|
return widgetProps.antenna != null ? widgetProps.antenna.name : '?';
|
||||||
} else {
|
} else {
|
||||||
return (i18n.ts._timelines as any)[widgetProps.src] ?? '?';
|
return i18n.ts._timelines[widgetProps.src] ?? '?';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
const name = 'hashtags';
|
const name = 'trends';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,5 @@ export const widgets = [
|
||||||
|
|
||||||
...federationWidgets,
|
...federationWidgets,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export type WidgetName = typeof widgets[number];
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { getDefaultFormValues } from '@/utility/form.js';
|
import { getDefaultFormValues } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
import type { WidgetName } from './index.js';
|
||||||
|
|
||||||
export type Widget<P extends Record<string, unknown>> = {
|
export type Widget<P extends Record<string, unknown>> = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -31,7 +32,7 @@ export type WidgetComponentExpose = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWidgetPropsManager = <F extends FormWithDefault>(
|
export const useWidgetPropsManager = <F extends FormWithDefault>(
|
||||||
name: string,
|
name: WidgetName,
|
||||||
propsDef: F,
|
propsDef: F,
|
||||||
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
|
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
|
||||||
emit: WidgetComponentEmits<GetFormResultType<F>>,
|
emit: WidgetComponentEmits<GetFormResultType<F>>,
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"chokidar": "5.0.0",
|
"chokidar": "5.0.0",
|
||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.2",
|
||||||
"execa": "9.6.1",
|
"execa": "9.6.1",
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@types/wawoff2": "1.0.2",
|
"@types/wawoff2": "1.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1"
|
"@typescript-eslint/parser": "8.53.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tabler/icons-webfont": "3.35.0",
|
"@tabler/icons-webfont": "3.35.0",
|
||||||
"harfbuzzjs": "0.4.14",
|
"harfbuzzjs": "0.4.15",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"wawoff2": "2.0.1"
|
"wawoff2": "2.0.1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/matter-js": "0.20.2",
|
"@types/matter-js": "0.20.2",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.2",
|
||||||
"execa": "9.6.1",
|
"execa": "9.6.1",
|
||||||
"nodemon": "3.1.11"
|
"nodemon": "3.1.11"
|
||||||
|
|
|
||||||
|
|
@ -2230,6 +2230,7 @@ declare namespace entities {
|
||||||
MetaLite,
|
MetaLite,
|
||||||
MetaDetailedOnly,
|
MetaDetailedOnly,
|
||||||
MetaDetailed,
|
MetaDetailed,
|
||||||
|
MetaClientOptions,
|
||||||
UserWebhook,
|
UserWebhook,
|
||||||
SystemWebhook,
|
SystemWebhook,
|
||||||
AbuseReportNotificationRecipient,
|
AbuseReportNotificationRecipient,
|
||||||
|
|
@ -2820,6 +2821,9 @@ type MeDetailed = components['schemas']['MeDetailed'];
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type MeDetailedOnly = components['schemas']['MeDetailedOnly'];
|
type MeDetailedOnly = components['schemas']['MeDetailedOnly'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type MetaClientOptions = components['schemas']['MetaClientOptions'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type MetaDetailed = components['schemas']['MetaDetailed'];
|
type MetaDetailed = components['schemas']['MetaDetailed'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
|
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@readme/openapi-parser": "5.4.0",
|
"@readme/openapi-parser": "5.5.0",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"openapi-types": "12.1.3",
|
"openapi-types": "12.1.3",
|
||||||
"openapi-typescript": "7.10.1",
|
"openapi-typescript": "7.10.1",
|
||||||
"ts-case-convert": "2.1.0",
|
"ts-case-convert": "2.1.0",
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,17 @@
|
||||||
"directory": "packages/misskey-js"
|
"directory": "packages/misskey-js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/api-extractor": "7.55.2",
|
"@microsoft/api-extractor": "7.55.5",
|
||||||
"@types/node": "24.10.4",
|
"@types/node": "24.10.9",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
"@vitest/coverage-v8": "4.0.16",
|
"@vitest/coverage-v8": "4.0.17",
|
||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.2",
|
||||||
"execa": "9.6.1",
|
"execa": "9.6.1",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"nodemon": "3.1.11",
|
"nodemon": "3.1.11",
|
||||||
"tsd": "0.33.0",
|
"tsd": "0.33.0",
|
||||||
"vitest": "4.0.16",
|
"vitest": "4.0.17",
|
||||||
"vitest-websocket-mock": "0.5.0"
|
"vitest-websocket-mock": "0.5.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue