Merge pull request 'develop' (#9125) from develop into main
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9125
This commit is contained in:
commit
34a646f478
64 changed files with 450 additions and 345 deletions
|
@ -1,5 +1,5 @@
|
|||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Misskey configuration
|
||||
# Calckey configuration
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────┐
|
||||
|
@ -38,11 +38,11 @@ db:
|
|||
port: 5432
|
||||
|
||||
# Database name
|
||||
db: misskey
|
||||
db: calckey
|
||||
|
||||
# Auth
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
user: example-calckey-user
|
||||
pass: example-calckey-pass
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
@ -147,7 +147,8 @@ id: 'aid'
|
|||
|
||||
# Managed hosting settings
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! YOU DON'T NEED THIS! <<<<<<
|
||||
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
# Each category is optional, but if each item in each category is mandatory!
|
||||
# If you mess this up, that's on you, you've been warned...
|
||||
|
@ -181,4 +182,11 @@ id: 'aid'
|
|||
# connnectOverProxy: false
|
||||
# setPublicReadOnUpload: true
|
||||
# s3ForcePathStyle: true
|
||||
#summalyProxyUrl: 'https://summaly.arkjp.net'
|
||||
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
|
||||
# Seriously. Do NOT fill out the above settings if you're self-hosting.
|
||||
# They're much better off being set from the control panel.
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
- User "choices" (recommended users) like Mastodon and Soapbox
|
||||
- Option to publicize instance blocks
|
||||
- Fully revamp non-logged-in screen
|
||||
- Remote follow button
|
||||
- Personal notes for all accounts
|
||||
- Non-nyaify cat mode
|
||||
- Timeline filters
|
||||
|
@ -21,8 +20,8 @@
|
|||
## Work in progress
|
||||
|
||||
- Better Messaging UI
|
||||
- Videos can be played in DMs
|
||||
- Make your password hasn't been pwned
|
||||
- Remote follow button
|
||||
- Admin custom CSS
|
||||
- Add back time machine (jump to date)
|
||||
- Improve accesibility score
|
||||
|
@ -86,6 +85,7 @@
|
|||
- Link hover effect
|
||||
- Replace all `$ts` with i18n
|
||||
- AVIF support
|
||||
- Page drafts
|
||||
- Obliteration of Ai-chan
|
||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
||||
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
||||
|
|
110
README.md
110
README.md
|
@ -1,15 +1,15 @@
|
|||
<div align="center">
|
||||
<a href="https://stop.voring.me/">
|
||||
<a href="https://i.calckey.cloud/">
|
||||
<img src="./.github/title_float.svg" alt="Calckey logo" style="border-radius:50%" width="400"/>
|
||||
</a>
|
||||
|
||||
**🌎 **[Calckey](https://stop.voring.me/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
**🌎 **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<img src="https://pool.jortage.com/voringme/misskey/e7cd2a17-8b23-4e1e-b5cf-709480c623e2.png" align="right" height="320px"/>
|
||||
<img src="https://pool.jortage.com/voringme/misskey/e7cd2a17-8b23-4e1e-b5cf-709480c623e2.png" align="right" height="320px" alt="Calc (the Calckey mascot) smoking a fat dart"/>
|
||||
|
||||
# ✨ About Calckey
|
||||
|
||||
|
@ -33,6 +33,8 @@
|
|||
|
||||
# 🥂 Links
|
||||
|
||||
- 🚢 Flagship instance: <https://i.calckey.cloud>
|
||||
- 📣 Official account: <https://i.calckey.cloud/@calckey>
|
||||
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
|
||||
- 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com>
|
||||
- 📜 Instance list: <https://calckey.fediverse.observer/list>
|
||||
|
@ -93,89 +95,17 @@ cp -r ../misskey/files . # if you don't use object storage
|
|||
|
||||
## 🍀 NGINX
|
||||
|
||||
<details>
|
||||
<summary>Click to see an example NGINX config:</summary>
|
||||
|
||||
```nginx
|
||||
# Replace example.tld with your domain
|
||||
|
||||
# For WebSocket
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.tld;
|
||||
|
||||
# For SSL domain validation
|
||||
root /var/www/html;
|
||||
location /.well-known/acme-challenge/ { allow all; }
|
||||
location /.well-known/pki-validation/ { allow all; }
|
||||
location / { return 301 https://$server_name$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name example.tld;
|
||||
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:ssl_session_cache:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# To use Let's Encrypt certificate
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
|
||||
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
|
||||
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||
|
||||
# SSL protocol settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Change to your upload limit
|
||||
client_max_body_size 80m;
|
||||
|
||||
# Proxy to Node
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
|
||||
# If it's behind another reverse proxy or CDN, remove the following.
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# For WebSocket
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Cache settings
|
||||
proxy_cache cache1;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_use_stale updating;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
}
|
||||
}
|
||||
```
|
||||
- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
|
||||
- Edit `calckey.nginx.conf` to reflect your instance properly
|
||||
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
|
||||
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
|
||||
|
||||
</details>
|
||||
|
||||
## 🚀 Build and launch!
|
||||
|
||||
### 🐢 NodeJS
|
||||
|
||||
#### `git pull` and run these steps to update Calckey in the future!
|
||||
|
||||
```sh
|
||||
|
@ -195,19 +125,21 @@ docker up -d
|
|||
### 🐳 Docker Compose
|
||||
|
||||
```sh
|
||||
sudo docker compose build
|
||||
sudo docker-compose run --rm web yarn run init
|
||||
sudo docker compose up -d
|
||||
docker-compose build
|
||||
docker-compose run --rm web yarn run init
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 😉 Tips & Tricks
|
||||
|
||||
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
|
||||
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
|
||||
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
|
||||
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
|
||||
- For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker.
|
||||
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
|
||||
- To add another admin account:
|
||||
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
|
||||
- Go back to Overview > click the clipboard icon next to the ID
|
||||
- Run `psql -d calckey` (or whatever the database name is)
|
||||
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace 999999 with the copied ID)
|
||||
- Have the new admin log out and log back in
|
||||
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
|
||||
- Go back to Overview > click the clipboard icon next to the ID
|
||||
- Run `psql -d calckey` (or whatever the database name is)
|
||||
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace `999999` with the copied ID)
|
||||
- Have the new admin log out and log back in
|
||||
|
|
72
calckey.nginx.conf
Normal file
72
calckey.nginx.conf
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Replace example.tld with your domain
|
||||
|
||||
# For WebSocket
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.tld;
|
||||
|
||||
# For SSL domain validation
|
||||
root /var/www/html;
|
||||
location /.well-known/acme-challenge/ { allow all; }
|
||||
location /.well-known/pki-validation/ { allow all; }
|
||||
location / { return 301 https://$server_name$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name example.tld;
|
||||
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:ssl_session_cache:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# To use Let's Encrypt certificate
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
|
||||
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
|
||||
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||
|
||||
# SSL protocol settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Change to your upload limit
|
||||
client_max_body_size 80m;
|
||||
|
||||
# Proxy to Node
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
|
||||
# If it's behind another reverse proxy or CDN, remove the following.
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# For WebSocket
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Cache settings
|
||||
proxy_cache cache1;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_use_stale updating;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
}
|
||||
}
|
|
@ -159,7 +159,7 @@ proxyAccount: "Proxy account"
|
|||
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
|
||||
host: "Host"
|
||||
selectUser: "Select a user"
|
||||
recipient: "Recipient"
|
||||
recipient: "Recipient(s)"
|
||||
annotation: "Comments"
|
||||
federation: "Federation"
|
||||
instances: "Instances"
|
||||
|
@ -770,8 +770,8 @@ noBotProtectionWarning: "Bot protection is not configured."
|
|||
configure: "Configure"
|
||||
postToGallery: "Create new gallery post"
|
||||
gallery: "Gallery"
|
||||
recentPosts: "Recent posts"
|
||||
popularPosts: "Popular posts"
|
||||
recentPosts: "Recent pages"
|
||||
popularPosts: "Popular pages"
|
||||
shareWithNote: "Share with note"
|
||||
ads: "Advertisements"
|
||||
expiration: "Deadline"
|
||||
|
@ -1094,7 +1094,7 @@ _channel:
|
|||
usersCount: "{n} Participants"
|
||||
notesCount: "{n} Notes"
|
||||
_messaging:
|
||||
dms: "DMs"
|
||||
dms: "Private"
|
||||
groups: "Groups"
|
||||
_menuDisplay:
|
||||
sideFull: "Side"
|
||||
|
@ -1264,10 +1264,10 @@ _permissions:
|
|||
"read:reactions": "View your reactions"
|
||||
"write:reactions": "Edit your reactions"
|
||||
"write:votes": "Vote on a poll"
|
||||
"read:pages": "View your pages"
|
||||
"write:pages": "Edit or delete your pages"
|
||||
"read:page-likes": "View your likes on pages"
|
||||
"write:page-likes": "Edit your likes on pages"
|
||||
"read:pages": "View your page"
|
||||
"write:pages": "Edit or delete your page"
|
||||
"read:page-likes": "View your likes on page"
|
||||
"write:page-likes": "Edit your likes on page"
|
||||
"read:user-groups": "View your user groups"
|
||||
"write:user-groups": "Edit or delete your user groups"
|
||||
"read:channels": "View your channels"
|
||||
|
@ -1441,7 +1441,7 @@ _pages:
|
|||
liked: "Liked Pages"
|
||||
featured: "Popular"
|
||||
inspector: "Inspector"
|
||||
contents: "Contents"
|
||||
contents: "Content"
|
||||
content: "Page block"
|
||||
variables: "Variables"
|
||||
title: "Title"
|
||||
|
|
16
package.json
16
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "12.119.0-calc.14",
|
||||
"version": "12.119.0-calc.15",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/thatonecalculator/calckey.git"
|
||||
},
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"packageManager": "yarn@3.3.0",
|
||||
"workspaces": [
|
||||
"packages/client",
|
||||
"packages/backend",
|
||||
|
@ -39,10 +39,10 @@
|
|||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.6.3",
|
||||
"@bull-board/ui": "^4.6.3",
|
||||
"@bull-board/api": "^4.6.4",
|
||||
"@bull-board/ui": "^4.6.4",
|
||||
"@tensorflow/tfjs": "^3.21.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint": "^8.28.0",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
|
@ -55,13 +55,13 @@
|
|||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/parser": "5.42.1",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"typescript": "4.8.4",
|
||||
"typescript": "4.9.3",
|
||||
"vue-eslint-parser": "^9.1.0"
|
||||
}
|
||||
}
|
||||
|
|
8
packages/backend/migration/1668828368510PageDraft.js
Normal file
8
packages/backend/migration/1668828368510PageDraft.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export class Page1668828368510 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "page" ADD "isPublic" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "isPublic"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1668831378728FixCalckeyAgain.js
Normal file
11
packages/backend/migration/1668831378728FixCalckeyAgain.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export class FixCalckeyAgain1668831378728 {
|
||||
name = 'FixCalckeyAgain1668831378728'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`UPDATE "meta" SET "useStarForReactionFallback" = TRUE`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`UPDATE "meta" SET "useStarForReactionFallback" = FALSE`);
|
||||
}
|
||||
}
|
|
@ -21,9 +21,9 @@
|
|||
"@tensorflow/tfjs-node": "3.21.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.6.3",
|
||||
"@bull-board/koa": "^4.6.3",
|
||||
"@bull-board/ui": "^4.6.3",
|
||||
"@bull-board/api": "^4.6.4",
|
||||
"@bull-board/koa": "^4.6.4",
|
||||
"@bull-board/ui": "^4.6.4",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@elastic/elasticsearch": "7.17.0",
|
||||
"@koa/cors": "3.4.3",
|
||||
|
@ -32,15 +32,15 @@
|
|||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"ajv": "8.11.0",
|
||||
"ajv": "8.11.2",
|
||||
"archiver": "5.3.1",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1253.0",
|
||||
"aws-sdk": "2.1258.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"bull": "4.10.1",
|
||||
"cacheable-lookup": "6.1.0",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.1.2",
|
||||
"chalk-template": "0.4.0",
|
||||
|
@ -54,10 +54,10 @@
|
|||
"feed": "4.2.2",
|
||||
"file-type": "17.1.6",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"got": "12.5.2",
|
||||
"got": "12.5.3",
|
||||
"hpagent": "0.1.2",
|
||||
"ioredis": "4.28.5",
|
||||
"ip-cidr": "3.0.10",
|
||||
"ip-cidr": "3.0.11",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "20.0.2",
|
||||
|
@ -83,7 +83,7 @@
|
|||
"node-fetch": "3.3.0",
|
||||
"nodemailer": "6.8.0",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.9.15",
|
||||
"oauth": "^0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "7.1.1",
|
||||
"pg": "8.8.0",
|
||||
|
@ -111,7 +111,7 @@
|
|||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.12.14",
|
||||
"systeminformation": "5.13.5",
|
||||
"tesseract.js": "^3.0.3",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
|
@ -130,7 +130,7 @@
|
|||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.112",
|
||||
"@redocly/openapi-core": "1.0.0-beta.114",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.9",
|
||||
"@types/cbor": "6.0.0",
|
||||
|
@ -165,7 +165,7 @@
|
|||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.6.2",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sharp": "0.30.5",
|
||||
"@types/sharp": "0.31.0",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/speakeasy": "2.0.7",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
|
@ -174,12 +174,12 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||
"@typescript-eslint/parser": "5.42.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"execa": "6.1.0",
|
||||
"typescript": "4.8.4"
|
||||
"typescript": "4.9.3"
|
||||
}
|
||||
}
|
||||
|
|
18
packages/backend/src/misc/clone.ts
Normal file
18
packages/backend/src/misc/clone.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// structredCloneが遅いため
|
||||
// SEE: http://var.blog.jp/archives/86038606.html
|
||||
|
||||
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
||||
|
||||
export function deepClone<T extends Cloneable>(x: T): T {
|
||||
if (typeof x === 'object') {
|
||||
if (x === null) return x;
|
||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||
const obj = {} as Record<string, Cloneable>;
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
obj[k] = deepClone(v);
|
||||
}
|
||||
return obj as T;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
|
@ -40,6 +40,9 @@ export class Page {
|
|||
@Column('boolean')
|
||||
public alignCenter: boolean;
|
||||
|
||||
@Column('boolean')
|
||||
public isPublic: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -9,6 +9,8 @@ import { query, appendQuery } from '@/prelude/url.js';
|
|||
import { Meta } from '@/models/entities/meta.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, DriveFolders } from '../index.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
|
||||
|
||||
type PackOptions = {
|
||||
detail?: boolean,
|
||||
|
@ -29,9 +31,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
|||
|
||||
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||
if (file.properties.orientation != null) {
|
||||
// TODO
|
||||
//const properties = structuredClone(file.properties);
|
||||
const properties = JSON.parse(JSON.stringify(file.properties));
|
||||
const properties = deepClone(file.properties);
|
||||
if (file.properties.orientation >= 5) {
|
||||
[properties.width, properties.height] = [properties.height, properties.width];
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ export const PageRepository = db.getRepository(Page).extend({
|
|||
content: page.content,
|
||||
variables: page.variables,
|
||||
title: page.title,
|
||||
isPublic: page.isPublic,
|
||||
name: page.name,
|
||||
summary: page.summary,
|
||||
hideTitleWhenPinned: page.hideTitleWhenPinned,
|
||||
|
|
|
@ -47,5 +47,9 @@ export const packedPageSchema = {
|
|||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -53,6 +53,7 @@ export const paramDef = {
|
|||
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
|
||||
alignCenter: { type: 'boolean', default: false },
|
||||
isPublic: { type: 'boolean', default: true },
|
||||
hideTitleWhenPinned: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['title', 'name', 'content', 'variables', 'script'],
|
||||
|
@ -97,6 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
alignCenter: ps.alignCenter,
|
||||
hideTitleWhenPinned: ps.hideTitleWhenPinned,
|
||||
font: ps.font,
|
||||
isPublic: ps.isPublic,
|
||||
})).then(x => Pages.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await Pages.pack(page);
|
||||
|
|
|
@ -67,5 +67,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw new ApiError(meta.errors.noSuchPage);
|
||||
}
|
||||
|
||||
if (!page.isPublic && (user == null || (page.userId !== user.id))) {
|
||||
throw new ApiError(meta.errors.noSuchPage);
|
||||
}
|
||||
|
||||
return await Pages.pack(page, user);
|
||||
});
|
||||
|
|
|
@ -60,6 +60,7 @@ export const paramDef = {
|
|||
font: { type: 'string', enum: ['serif', 'sans-serif'] },
|
||||
alignCenter: { type: 'boolean' },
|
||||
hideTitleWhenPinned: { type: 'boolean' },
|
||||
isPublic: { type: 'boolean' },
|
||||
},
|
||||
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
|
||||
} as const;
|
||||
|
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
content: ps.content,
|
||||
variables: ps.variables,
|
||||
script: ps.script,
|
||||
isPublic: ps.isPublic,
|
||||
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
|
||||
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
|
||||
font: ps.font === undefined ? page.font : ps.font,
|
||||
|
|
|
@ -34,7 +34,8 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
||||
.andWhere('page.userId = :userId', { userId: ps.userId })
|
||||
.andWhere('page.visibility = \'public\'');
|
||||
.andWhere('page.visibility = \'public\'')
|
||||
.andWhere('page.isPublic = true');
|
||||
|
||||
const pages = await query
|
||||
.take(ps.limit)
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.18.1",
|
||||
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
|
||||
"chart.js": "3.9.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chart.js": "4.0.1",
|
||||
"chartjs-adapter-date-fns": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.5.1",
|
||||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"compare-versions": "5.0.1",
|
||||
|
@ -31,7 +31,7 @@
|
|||
"idb-keyval": "6.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.1",
|
||||
"katex": "0.15.6",
|
||||
"katex": "0.16.3",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.0",
|
||||
"misskey-js": "0.0.14",
|
||||
|
@ -48,16 +48,16 @@
|
|||
"swiper": "^8.4.4",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.144.0",
|
||||
"three": "0.146.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.7.1",
|
||||
"tsconfig-paths": "4.1.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "4.8.4",
|
||||
"typescript": "4.9.3",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.7.3",
|
||||
"vite": "^3.2.3",
|
||||
"vite": "^3.2.4",
|
||||
"vue": "3.2.45",
|
||||
"vue-isyourpasswordsafe": "^2.0.0",
|
||||
"vue-plyr": "^7.0.0",
|
||||
|
@ -67,7 +67,7 @@
|
|||
"devDependencies": {
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "8.0.0",
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/katex": "0.14.0",
|
||||
"@types/matter-js": "0.18.2",
|
||||
|
@ -76,11 +76,11 @@
|
|||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||
"@typescript-eslint/parser": "5.42.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"eslint": "8.27.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-vue": "9.7.0",
|
||||
"rollup": "2.79.1",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.yes }}</MkButton>
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ i18n.ts.yes }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<button class="kpoogebi _button"
|
||||
<button
|
||||
class="kpoogebi _button"
|
||||
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
|
@ -8,7 +9,8 @@
|
|||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
|
||||
</template>
|
||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
|
||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
|
||||
<!-- つまりリモートフォローの場合。 -->
|
||||
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse"></i>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
|
@ -29,16 +31,16 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
}>(), {
|
||||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
}>(), {
|
||||
full: false,
|
||||
large: false,
|
||||
});
|
||||
|
@ -50,9 +52,9 @@ const connection = stream.useChannel('main');
|
|||
|
||||
if (props.user.isFollowing == null) {
|
||||
os.api('users/show', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
})
|
||||
.then(onFollowChange);
|
||||
.then(onFollowChange);
|
||||
}
|
||||
|
||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||
|
@ -75,17 +77,17 @@ async function onClick() {
|
|||
if (canceled) return;
|
||||
|
||||
await os.api('following/delete', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
});
|
||||
} else {
|
||||
if (hasPendingFollowRequestFromYou) {
|
||||
await os.api('following/requests/cancel', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
});
|
||||
hasPendingFollowRequestFromYou = false;
|
||||
} else {
|
||||
await os.api('following/create', {
|
||||
userId: props.user.id
|
||||
userId: props.user.id,
|
||||
});
|
||||
hasPendingFollowRequestFromYou = true;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -104,9 +104,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue';
|
||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as misskey from 'misskey-js';
|
||||
import type { Ref } from 'vue';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
|
@ -134,6 +135,7 @@ import { i18n } from '@/i18n';
|
|||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -144,12 +146,12 @@ const props = defineProps<{
|
|||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = JSON.parse(JSON.stringify(note));
|
||||
let result = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
}
|
||||
|
@ -432,7 +434,9 @@ function readPromo() {
|
|||
width: 58px;
|
||||
height: 58px;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0px);
|
||||
/* For some reason this breaks avatar
|
||||
positions on notes, commenting it for now */
|
||||
/* top: var(--stickyTop, 0px); */
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -143,6 +143,7 @@ import { $i } from '@/account';
|
|||
import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -153,12 +154,12 @@ const props = defineProps<{
|
|||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = JSON.parse(JSON.stringify(note));
|
||||
let result = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
||||
<div class="sub-icon" :class="notification.type">
|
||||
<i v-if="notification.type === 'follow'" class="ph-plus-bold"></i>
|
||||
<i v-if="notification.type === 'follow'" class="ph-hand-waving-bold"></i>
|
||||
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock-bold"></i>
|
||||
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check-bold"></i>
|
||||
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i>
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
<span>
|
||||
<template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template>
|
||||
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
|
||||
<span v-if="showResult" class="votes">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!readOnly">
|
||||
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</p>
|
||||
<ul>
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<MkInput class="input" small :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
</MkInput>
|
||||
<button class="_button" @click="remove(i)">
|
||||
<i class="ph-x-bold ph-lg"></i>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<MkAcct :user="u"/>
|
||||
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
|
||||
</span>
|
||||
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ph-plus-bold ph-lg ph-fw ph-lg"></i></button>
|
||||
<button class="_button" @click="addVisibleUser"><i class="ph-plus-bold ph-md ph-fw ph-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
||||
|
@ -89,6 +89,7 @@ import { i18n } from '@/i18n';
|
|||
import { instance } from '@/instance';
|
||||
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
|
@ -458,7 +459,7 @@ async function onPaste(ev: ClipboardEvent) {
|
|||
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
|
||||
ev.preventDefault();
|
||||
|
||||
os.confirm({
|
||||
os.yesno({
|
||||
type: 'info',
|
||||
text: i18n.ts.quoteQuestion,
|
||||
}).then(({ canceled }) => {
|
||||
|
@ -575,7 +576,7 @@ async function post() {
|
|||
// plugin
|
||||
if (notePostInterruptors.length > 0) {
|
||||
for (const interruptor of notePostInterruptors) {
|
||||
postData = await interruptor.handler(JSON.parse(JSON.stringify(postData)));
|
||||
postData = await interruptor.handler(deepClone(postData));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -761,7 +762,7 @@ onMounted(() => {
|
|||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> .local-only {
|
||||
margin: 0 0 0 12px;
|
||||
opacity: 0.7;
|
||||
|
@ -832,7 +833,7 @@ onMounted(() => {
|
|||
padding: 6px 24px;
|
||||
margin-bottom: 8px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
line-height: 2rem;
|
||||
|
||||
> .visibleUsers {
|
||||
display: inline;
|
||||
|
@ -840,15 +841,19 @@ onMounted(() => {
|
|||
font-size: 14px;
|
||||
|
||||
> button {
|
||||
padding: 4px;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
|
||||
> i {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-right: 14px;
|
||||
padding: 8px 0 8px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--X4);
|
||||
margin: 0.3rem;
|
||||
padding: 4px 0 4px 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--X3);
|
||||
|
||||
> button {
|
||||
padding: 4px 8px;
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="social _section">
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
@ -51,6 +51,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
@ -58,7 +59,6 @@ import { apiUrl, host as configHost } from '@/config';
|
|||
import { byteify, hexify } from '@/scripts/2fa';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
|
@ -85,7 +85,7 @@ const props = defineProps({
|
|||
withAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
|
@ -95,13 +95,13 @@ const props = defineProps({
|
|||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
function onUsernameChange() {
|
||||
os.api('users/show', {
|
||||
username: username
|
||||
username: username,
|
||||
}).then(userResponse => {
|
||||
user = userResponse;
|
||||
}, () => {
|
||||
|
@ -123,10 +123,10 @@ function queryKey() {
|
|||
allowCredentials: challengeData.securityKeys.map(key => ({
|
||||
id: byteify(key.id, 'hex'),
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'nfc', 'ble', 'internal']
|
||||
transports: ['usb', 'nfc', 'ble', 'internal'],
|
||||
})),
|
||||
timeout: 60 * 1000
|
||||
}
|
||||
timeout: 60 * 1000,
|
||||
},
|
||||
}).catch(() => {
|
||||
queryingKey = false;
|
||||
return Promise.reject(null);
|
||||
|
@ -141,7 +141,7 @@ function queryKey() {
|
|||
clientDataJSON: hexify(credential.response.clientDataJSON),
|
||||
credentialId: credential.id,
|
||||
challengeId: challengeData.challengeId,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
});
|
||||
}).then(res => {
|
||||
|
@ -151,7 +151,7 @@ function queryKey() {
|
|||
if (err === null) return;
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.signinFailed
|
||||
text: i18n.ts.signinFailed,
|
||||
});
|
||||
signing = false;
|
||||
});
|
||||
|
@ -165,8 +165,8 @@ function onSubmit() {
|
|||
os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
}).then(res => {
|
||||
totpLogin = true;
|
||||
signing = false;
|
||||
|
@ -181,9 +181,9 @@ function onSubmit() {
|
|||
os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
token: user && user.twoFactorEnabled ? token : undefined
|
||||
token: user && user.twoFactorEnabled ? token : undefined,
|
||||
}).then(res => {
|
||||
emit('login', res);
|
||||
onLogin(res);
|
||||
|
@ -197,7 +197,7 @@ function loginFailed(err) {
|
|||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.noSuchUser
|
||||
text: i18n.ts.noSuchUser,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ function loginFailed(err) {
|
|||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: JSON.stringify(err)
|
||||
text: JSON.stringify(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<div v-if="note.files.length > 0">
|
||||
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<XMediaList :media-list="note.files"/>
|
||||
</div>
|
||||
<div v-if="note.poll">
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
|
||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
</div>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
|
|
@ -22,7 +22,7 @@ import { i18n } from '@/i18n';
|
|||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -60,7 +60,7 @@ export default defineComponent({
|
|||
watch(() => props.p, () => {
|
||||
process();
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const retry = () => {
|
||||
|
@ -73,6 +73,7 @@ export default defineComponent({
|
|||
rejected,
|
||||
result,
|
||||
retry,
|
||||
i18n,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<i v-if="relay.status === 'accepted'" class="ph-check-bold ph-lg icon accepted"></i>
|
||||
<i v-else-if="relay.status === 'rejected'" class="ph-prohibit-bold ph-lg icon rejected"></i>
|
||||
<i v-else class="ph-clock-bold ph-lg icon requesting"></i>
|
||||
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
|
||||
<span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
|
||||
</div>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<section class="_section">
|
||||
<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
|
||||
<div class="_title">{{ i18n.t('_auth.shareAccess', { name: app.name }) }}</div>
|
||||
<div class="_content">
|
||||
<h2>{{ app.name }}</h2>
|
||||
<p class="id">{{ app.id }}</p>
|
||||
|
@ -9,7 +9,7 @@
|
|||
<div class="_content">
|
||||
<h2>{{ i18n.ts._auth.permissionAsk }}</h2>
|
||||
<ul>
|
||||
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<h1>{{ i18n.ts._auth.denied }}</h1>
|
||||
</div>
|
||||
<div v-if="state == 'accepted'" class="accepted">
|
||||
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : i18n.ts.allowed }}</h1>
|
||||
<h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
|
@ -47,6 +47,7 @@ export default defineComponent({
|
|||
state: null,
|
||||
session: null,
|
||||
fetching: true,
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -48,7 +48,7 @@ watch(() => props.clipId, async () => {
|
|||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
});
|
||||
|
||||
provide('currentClipPage', $$(clip));
|
||||
|
||||
|
|
|
@ -202,11 +202,11 @@ definePageMetadata(computed(() => post ? {
|
|||
|
||||
> .like {
|
||||
> .button {
|
||||
--accent: rgb(241 97 132);
|
||||
--X8: rgb(241 92 128);
|
||||
--accent: #eb6f92;
|
||||
--X8: #eb6f92;
|
||||
--buttonBg: rgb(216 71 106 / 5%);
|
||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||
color: #ff002f;
|
||||
color: #eb6f92;
|
||||
|
||||
::v-deep(.count) {
|
||||
margin-left: 0.5em;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div v-size="{ max: [400, 500] }" class="thvuemwp" :class="{ isMe }">
|
||||
<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/>
|
||||
<MkAvatar v-if="!isMe" class="avatar" :user="message.user" :show-indicator="true"/>
|
||||
<div class="content">
|
||||
<div class="balloon" :class="{ noText: message.text == null }">
|
||||
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
|
||||
|
@ -38,7 +38,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import XMediaList from '@/components/MkMediaList.vue';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||
|
@ -73,10 +72,10 @@ function del(): void {
|
|||
|
||||
> .avatar {
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 16px);
|
||||
top: calc(var(--stickyTop, 0px) + 20px);
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
|
@ -91,14 +90,7 @@ function del(): void {
|
|||
min-height: 38px;
|
||||
border-radius: 16px;
|
||||
max-width: 100%;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
}
|
||||
margin-left: 10rem;
|
||||
|
||||
& + * {
|
||||
clear: both;
|
||||
|
@ -222,7 +214,9 @@ function del(): void {
|
|||
padding-right: 32px;
|
||||
|
||||
> .balloon {
|
||||
$color: var(--messageBg);
|
||||
$color: var(--X4);
|
||||
margin-right: 10rem;
|
||||
margin-left: 0rem !important;
|
||||
background: $color;
|
||||
|
||||
&.noText {
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="_section">
|
||||
<div v-if="name" class="_title">{{ $t('_auth.shareAccess', { name: name }) }}</div>
|
||||
<div v-if="name" class="_title">{{ i18n.t('_auth.shareAccess', { name: name }) }}</div>
|
||||
<div v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<div class="_content">
|
||||
<p>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<MkInput v-model="value.message"><template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
|
||||
<MkSelect v-model="value.var">
|
||||
<template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.variable }}</template>
|
||||
<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
|
||||
<option :value="null">{{ i18n.t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
|
||||
<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
|
||||
<optgroup :label="i18n.ts._pages.script.pageVariables">
|
||||
<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<p v-show="showBody" v-if="error != null" class="error">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
|
||||
<p v-show="showBody" v-if="warn != null" class="warn">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
|
||||
<p v-show="showBody" v-if="error != null" class="error">{{ i18n.t('_pages.script.typeError', { slot: error.arg + 1, expect: i18n.t(`script.types.${error.expect}`), actual: i18n.t(`script.types.${error.actual}`) }) }}</p>
|
||||
<p v-show="showBody" v-if="warn != null" class="warn">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
|
||||
<div v-show="showBody" class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
@ -26,34 +26,36 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
removable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
required: false,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
warn: {
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['toggle', 'remove'],
|
||||
data() {
|
||||
return {
|
||||
showBody: this.expanded,
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -63,8 +65,8 @@ export default defineComponent({
|
|||
},
|
||||
remove() {
|
||||
this.$emit('remove');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -43,15 +43,15 @@
|
|||
<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
|
||||
<MkTextarea v-model="slots">
|
||||
<template #label>{{ i18n.ts._pages.script.blocks._fn.slots }}</template>
|
||||
<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
|
||||
<template #caption>{{ i18n.t('_pages.script.blocks._fn.slots-info') }}</template>
|
||||
</MkTextarea>
|
||||
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
|
||||
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="i18n.t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
|
||||
</section>
|
||||
<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
|
||||
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
|
||||
</section>
|
||||
<section v-else class="" style="padding:16px;">
|
||||
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>
|
||||
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="i18n.t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
@ -124,7 +124,7 @@ export default defineComponent({
|
|||
typeText(): any {
|
||||
if (this.modelValue.type === null) return null;
|
||||
if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
|
||||
return this.$t(`_pages.script.blocks.${this.modelValue.type}`);
|
||||
return i18n.t(`_pages.script.blocks.${this.modelValue.type}`);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<template #label>{{ i18n.ts._pages.url }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="isPublic" class="_formBlock">{{ i18n.ts.public }}</MkSwitch>
|
||||
<MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
|
||||
|
||||
<MkSelect v-model="font" class="_formBlock">
|
||||
|
@ -47,7 +48,6 @@
|
|||
<div v-else-if="tab === 'contents'">
|
||||
<div>
|
||||
<XBlocks v-model="content" class="content" :hpml="hpml"/>
|
||||
|
||||
<MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -130,6 +130,7 @@ let eyeCatchingImageId = $ref(null);
|
|||
let font = $ref('sans-serif');
|
||||
let content = $ref([]);
|
||||
let alignCenter = $ref(false);
|
||||
let isPublic = $ref(true);
|
||||
let hideTitleWhenPinned = $ref(false);
|
||||
let variables = $ref([]);
|
||||
let hpml = $ref(null);
|
||||
|
@ -158,6 +159,7 @@ function getSaveOptions() {
|
|||
script: script,
|
||||
hideTitleWhenPinned: hideTitleWhenPinned,
|
||||
alignCenter: alignCenter,
|
||||
isPublic: isPublic,
|
||||
content: content,
|
||||
variables: variables,
|
||||
eyeCatchingImageId: eyeCatchingImageId,
|
||||
|
@ -393,6 +395,7 @@ async function init() {
|
|||
script = page.script;
|
||||
hideTitleWhenPinned = page.hideTitleWhenPinned;
|
||||
alignCenter = page.alignCenter;
|
||||
isPublic = page.isPublic;
|
||||
content = page.content;
|
||||
variables = page.variables;
|
||||
eyeCatchingImageId = page.eyeCatchingImageId;
|
||||
|
@ -401,7 +404,7 @@ async function init() {
|
|||
content = [{
|
||||
id,
|
||||
type: 'text',
|
||||
text: 'Hello World!',
|
||||
text: '',
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
@ -439,7 +442,7 @@ definePageMetadata(computed(() => {
|
|||
return {
|
||||
title: title,
|
||||
icon: 'ph-pencil-bold ph-lg',
|
||||
};
|
||||
};
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
@ -447,7 +450,7 @@ definePageMetadata(computed(() => {
|
|||
.jqqmcavi {
|
||||
> .button {
|
||||
& + .button {
|
||||
margin-left: 8px;
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<MkSpacer :content-max="800">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
|
||||
<div class="_block main">
|
||||
|
@ -25,14 +25,14 @@
|
|||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="page.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="page.user" style="display: block;"/>
|
||||
<MkAcct :user="page.user"/>
|
||||
<div class="user">
|
||||
<MkAvatar :user="page.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="page.user" style="display: block;"/>
|
||||
<MkAcct :user="page.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
<div class="links">
|
||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||
|
@ -176,6 +176,10 @@ definePageMetadata(computed(() => page ? {
|
|||
.xcukqgmh {
|
||||
> .main {
|
||||
|
||||
> * {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
> .header {
|
||||
padding: 16px;
|
||||
|
||||
|
@ -185,6 +189,8 @@ definePageMetadata(computed(() => page ? {
|
|||
}
|
||||
|
||||
> .banner {
|
||||
margin: 0rem !important;
|
||||
|
||||
> img {
|
||||
// TODO: 良い感じのアスペクト比で表示
|
||||
display: block;
|
||||
|
@ -195,7 +201,6 @@ definePageMetadata(computed(() => page ? {
|
|||
}
|
||||
|
||||
> .content {
|
||||
margin: 1rem;
|
||||
padding: 16px 0 0 0;
|
||||
}
|
||||
|
||||
|
@ -208,11 +213,11 @@ definePageMetadata(computed(() => page ? {
|
|||
|
||||
> .like {
|
||||
> .button {
|
||||
--accent: rgb(241 97 132);
|
||||
--X8: rgb(241 92 128);
|
||||
--accent: #eb6f92;
|
||||
--X8: #eb6f92;
|
||||
--buttonBg: rgb(216 71 106 / 5%);
|
||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||
color: #ff002f;
|
||||
color: #eb6f92;
|
||||
|
||||
::v-deep(.count) {
|
||||
margin-left: 0.5em;
|
||||
|
@ -221,8 +226,6 @@ definePageMetadata(computed(() => page ? {
|
|||
}
|
||||
|
||||
> .other {
|
||||
margin-left: auto;
|
||||
|
||||
> button {
|
||||
padding: 8px;
|
||||
margin: 0 8px;
|
||||
|
@ -232,37 +235,36 @@ definePageMetadata(computed(() => page ? {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .user {
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
> .user {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
margin-left: auto;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .links {
|
||||
margin-top: 16px;
|
||||
padding: 24px 0 0 0;
|
||||
padding: 14px 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .link {
|
||||
margin-right: 0.75em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<details>
|
||||
<summary>{{ i18n.ts.details }}</summary>
|
||||
<ul>
|
||||
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
|
|
@ -66,8 +66,9 @@ import * as os from '@/os';
|
|||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions)));
|
||||
let reactions = $ref(deepClone(defaultStore.state.reactions));
|
||||
|
||||
const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
|
||||
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
|
||||
|
@ -101,7 +102,7 @@ async function setDefault() {
|
|||
});
|
||||
if (canceled) return;
|
||||
|
||||
reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
|
||||
reactions = deepClone(defaultStore.def.reactions.default);
|
||||
}
|
||||
|
||||
function chooseEmoji(ev: MouseEvent) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<FormSection>
|
||||
<template #label>{{ i18n.ts.sounds }}</template>
|
||||
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
|
||||
{{ $t('_sfx.' + type) }}
|
||||
{{ i18n.t('_sfx.' + type) }}
|
||||
<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
|
||||
<template #suffixIcon><i class="ph-caret-down-bold ph-lg"></i></template>
|
||||
</FormLink>
|
||||
|
|
|
@ -91,13 +91,14 @@ import FormRange from '@/components/form/range.vue';
|
|||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const props = defineProps<{
|
||||
_id: string;
|
||||
userLists: any[] | null;
|
||||
}>();
|
||||
|
||||
const statusbar = reactive(JSON.parse(JSON.stringify(defaultStore.state.statusbars.find(x => x.id === props._id))));
|
||||
const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
|
||||
|
||||
watch(() => statusbar.type, () => {
|
||||
if (statusbar.type === 'rss') {
|
||||
|
@ -128,8 +129,8 @@ watch(statusbar, save);
|
|||
|
||||
async function save() {
|
||||
const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
|
||||
const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
|
||||
statusbars[i] = JSON.parse(JSON.stringify(statusbar));
|
||||
const statusbars = deepClone(defaultStore.state.statusbars);
|
||||
statusbars[i] = deepClone(statusbar);
|
||||
defaultStore.set('statusbars', statusbars);
|
||||
}
|
||||
|
||||
|
|
|
@ -86,9 +86,7 @@ if (defaultStore.reactiveState.tutorial.value !== -1) {
|
|||
const isLocalTimelineAvailable =
|
||||
!instance.disableLocalTimeline ||
|
||||
($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const isRecommendedTimelineAvailable =
|
||||
!instance.disableRecommendedTimeline ||
|
||||
($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const isRecommendedTimelineAvailable = !instance.disableRecommendedTimeline;
|
||||
const isGlobalTimelineAvailable =
|
||||
!instance.disableGlobalTimeline ||
|
||||
($i != null && ($i.isModerator || $i.isAdmin));
|
||||
|
|
|
@ -24,9 +24,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||
<div v-if="$i" class="actions">
|
||||
<div class="actions">
|
||||
<button class="menu _button" @click="menu"><i class="ph-dots-three-outline-bold ph-lg"></i></button>
|
||||
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
<MkFollowButton v-if="$i != null && $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
<MkFollowButton v-else :user="user" :remote="true" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
|
||||
|
@ -51,7 +52,7 @@
|
|||
</dl>
|
||||
<dl v-if="user.birthday" class="field">
|
||||
<dt class="name"><i class="ph-cake-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.birthday }}</dt>
|
||||
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
|
||||
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.t('yearsOld', { age }) }})</dd>
|
||||
</dl>
|
||||
<dl class="field">
|
||||
<dt class="name"><i class="ph-calendar-blank-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.registeredDate }}</dt>
|
||||
|
|
|
@ -83,26 +83,26 @@ const headerTabs = $computed(() =>
|
|||
{
|
||||
key: 'home',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ph-user-bold ph-large',
|
||||
icon: 'ph-user-bold ph-lg',
|
||||
},
|
||||
...(($i && $i.id === user.id) || user.publicReactions
|
||||
? [{
|
||||
key: 'reactions',
|
||||
title: i18n.ts.reaction,
|
||||
icon: 'ph-smiley-bold ph-large',
|
||||
icon: 'ph-smiley-bold ph-lg',
|
||||
}] : []),
|
||||
...(user.instance == null ? [{
|
||||
key: 'clips',
|
||||
title: i18n.ts.clips,
|
||||
icon: 'ph-paperclip-bold ph-large',
|
||||
icon: 'ph-paperclip-bold ph-lg',
|
||||
}, {
|
||||
key: 'pages',
|
||||
title: i18n.ts.pages,
|
||||
icon: 'ph-file-text-bold ph-large',
|
||||
icon: 'ph-file-text-bold ph-lg',
|
||||
}, {
|
||||
key: 'gallery',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'ph-image-square-bold ph-large',
|
||||
icon: 'ph-image-square-bold ph-lg',
|
||||
}] : []),
|
||||
]
|
||||
: null,
|
||||
|
|
|
@ -105,7 +105,7 @@ export default defineComponent({
|
|||
|
||||
showMenu(ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$t('aboutX', { x: instanceName }),
|
||||
text: i18n.t('aboutX', { x: instanceName }),
|
||||
icon: 'ph-info-bold ph-lg',
|
||||
action: () => {
|
||||
os.pageWindow('/about');
|
||||
|
|
|
@ -125,7 +125,7 @@ export default defineComponent({
|
|||
|
||||
showMenu(ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$t('aboutX', { x: instanceName }),
|
||||
text: i18n.t('aboutX', { x: instanceName }),
|
||||
icon: 'ph-info-bold ph-lg',
|
||||
action: () => {
|
||||
os.pageWindow('/about');
|
||||
|
|
18
packages/client/src/scripts/clone.ts
Normal file
18
packages/client/src/scripts/clone.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// structredCloneが遅いため
|
||||
// SEE: http://var.blog.jp/archives/86038606.html
|
||||
|
||||
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
||||
|
||||
export function deepClone<T extends Cloneable>(x: T): T {
|
||||
if (typeof x === 'object') {
|
||||
if (x === null) return x;
|
||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||
const obj = {} as Record<string, Cloneable>;
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
obj[k] = deepClone(v);
|
||||
}
|
||||
return obj as T;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ export type Theme = {
|
|||
|
||||
import lightTheme from '@/themes/_light.json5';
|
||||
import darkTheme from '@/themes/_dark.json5';
|
||||
import { deepClone } from './clone';
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
|
||||
|
@ -63,7 +64,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||
const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
// Deep copy
|
||||
const _theme = JSON.parse(JSON.stringify(theme));
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="ph-trash-bold ph-lg"></i></button>
|
||||
</div>
|
||||
<div class="middle">
|
||||
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="ph-plus-bold ph-lg"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button new" @click="addColumn"><i class="ph-plus-bold ph-lg"></i></button>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="ph-gear-six-bold ph-lg"></i></button>
|
||||
|
@ -322,7 +322,7 @@ async function deleteProfile() {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
width: 44px;
|
||||
|
||||
> .top, > .middle, > .bottom {
|
||||
> .button {
|
||||
|
@ -339,6 +339,11 @@ async function deleteProfile() {
|
|||
> .middle {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
> .new {
|
||||
font-size: 20px;
|
||||
background-color: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
|
||||
> .bottom {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
|
|||
import { Storage } from '../../pizzax';
|
||||
import { i18n } from '@/i18n';
|
||||
import { api } from '@/os';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
type ColumnWidget = {
|
||||
name: string;
|
||||
|
@ -25,10 +26,6 @@ export type Column = {
|
|||
tl?: 'home' | 'local' | 'social' | 'global';
|
||||
};
|
||||
|
||||
function copy<T>(x: T): T {
|
||||
return JSON.parse(JSON.stringify(x));
|
||||
}
|
||||
|
||||
export const deckStore = markRaw(new Storage('deck', {
|
||||
profile: {
|
||||
where: 'deviceAccount',
|
||||
|
@ -128,7 +125,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
|||
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
||||
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const layout = deepClone(deckStore.state.layout);
|
||||
layout[aX][aY] = b;
|
||||
layout[bX][bY] = a;
|
||||
deckStore.set('layout', layout);
|
||||
|
@ -136,7 +133,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
|||
}
|
||||
|
||||
export function swapLeftColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const layout = deepClone(deckStore.state.layout);
|
||||
deckStore.state.layout.some((ids, i) => {
|
||||
if (ids.includes(id)) {
|
||||
const left = deckStore.state.layout[i - 1];
|
||||
|
@ -152,7 +149,7 @@ export function swapLeftColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function swapRightColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const layout = deepClone(deckStore.state.layout);
|
||||
deckStore.state.layout.some((ids, i) => {
|
||||
if (ids.includes(id)) {
|
||||
const right = deckStore.state.layout[i + 1];
|
||||
|
@ -168,9 +165,9 @@ export function swapRightColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function swapUpColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const layout = deepClone(deckStore.state.layout);
|
||||
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
const ids = copy(deckStore.state.layout[idsIndex]);
|
||||
const ids = deepClone(deckStore.state.layout[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const up = ids[i - 1];
|
||||
|
@ -188,9 +185,9 @@ export function swapUpColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function swapDownColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const layout = deepClone(deckStore.state.layout);
|
||||
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
const ids = copy(deckStore.state.layout[idsIndex]);
|
||||
const ids = deepClone(deckStore.state.layout[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const down = ids[i + 1];
|
||||
|
@ -208,7 +205,7 @@ export function swapDownColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function stackLeftColumn(id: Column['id']) {
|
||||
let layout = copy(deckStore.state.layout);
|
||||
let layout = deepClone(deckStore.state.layout);
|
||||
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
layout[i - 1].push(id);
|
||||
|
@ -218,7 +215,7 @@ export function stackLeftColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function popRightColumn(id: Column['id']) {
|
||||
let layout = copy(deckStore.state.layout);
|
||||
let layout = deepClone(deckStore.state.layout);
|
||||
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
const affected = layout[i];
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
|
@ -226,7 +223,7 @@ export function popRightColumn(id: Column['id']) {
|
|||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
for (const column of columns) {
|
||||
if (affected.includes(column.id)) {
|
||||
column.active = true;
|
||||
|
@ -238,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets.unshift(widget);
|
||||
|
@ -250,9 +247,9 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||
}
|
||||
|
||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||
columns[columnIndex] = column;
|
||||
|
@ -261,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||
}
|
||||
|
||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = widgets;
|
||||
columns[columnIndex] = column;
|
||||
|
@ -272,9 +269,9 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
|||
}
|
||||
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||
...w,
|
||||
|
@ -286,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
|
|||
}
|
||||
|
||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const currentColumn = copy(deckStore.state.columns[columnIndex]);
|
||||
const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (currentColumn == null) return;
|
||||
for (const [k, v] of Object.entries(column)) {
|
||||
currentColumn[k] = v;
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
<div v-if="disabled" class="iwaalbte">
|
||||
<p>
|
||||
<i class="ph-minus-circle-bold ph-lg"></i>
|
||||
{{ $t('disabled-timeline.title') }}
|
||||
{{ i18n.t('disabled-timeline.title') }}
|
||||
</p>
|
||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||
<p class="desc">{{ i18n.t('disabled-timeline.description') }}</p>
|
||||
</div>
|
||||
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
||||
</XColumn>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
|
||||
<div class="calendar" :class="{ isHoliday }">
|
||||
<p class="month-and-year">
|
||||
<span class="year">{{ $t('yearX', { year }) }}</span>
|
||||
<span class="month">{{ $t('monthX', { month }) }}</span>
|
||||
<span class="year">{{ i18n.t('yearX', { year }) }}</span>
|
||||
<span class="month">{{ i18n.t('monthX', { month }) }}</span>
|
||||
</p>
|
||||
<p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
|
||||
<p v-else class="day">{{ $t('dayX', { day }) }}</p>
|
||||
<p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
|
||||
<p v-else class="day">{{ i18n.t('dayX', { day }) }}</p>
|
||||
<p class="week-day">{{ weekDay }}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
|
|
|
@ -47,12 +47,13 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { stream } from '@/stream';
|
||||
import number from '@/filters/number';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import * as os from '@/os';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
const name = 'jobQueue';
|
||||
|
||||
|
@ -100,12 +101,12 @@ const prev = reactive({} as typeof current);
|
|||
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
|
||||
|
||||
for (const domain of ['inbox', 'deliver']) {
|
||||
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
|
||||
prev[domain] = deepClone(current[domain]);
|
||||
}
|
||||
|
||||
const onStats = (stats) => {
|
||||
for (const domain of ['inbox', 'deliver']) {
|
||||
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
|
||||
prev[domain] = deepClone(current[domain]);
|
||||
current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
|
||||
current[domain].active = stats[domain].active;
|
||||
current[domain].waiting = stats[domain].waiting;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<p v-if="widgetProps.folderId == null">
|
||||
{{ i18n.ts.folder }}
|
||||
</p>
|
||||
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
|
||||
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p>
|
||||
<div ref="slideA" class="slide a"></div>
|
||||
<div ref="slideB" class="slide b"></div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<i v-else-if="widgetProps.src === 'global'" class="ph-planet-bold ph-lg"></i>
|
||||
<i v-else-if="widgetProps.src === 'list'" class="ph-list-bullets-bold ph-lg"></i>
|
||||
<i v-else-if="widgetProps.src === 'antenna'" class="ph-television-bold ph-lg"></i>
|
||||
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span>
|
||||
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span>
|
||||
<i :class="menuOpened ? 'ph-caret-up-bold ph-lg' : 'ph-caret-down-bold ph-lg'" style="margin-left: 8px;"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
|
||||
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
<p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
</div>
|
||||
<MkMiniChart class="chart" :src="stat.chart"/>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { reactive, watch } from 'vue';
|
|||
import { throttle } from 'throttle-debounce';
|
||||
import { Form, GetFormResultType } from '@/scripts/form';
|
||||
import * as os from '@/os';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
export type Widget<P extends Record<string, unknown>> = {
|
||||
id: string;
|
||||
|
@ -32,7 +33,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
|||
save: () => void;
|
||||
configure: () => void;
|
||||
} => {
|
||||
const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {});
|
||||
const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {});
|
||||
|
||||
const mergeProps = () => {
|
||||
for (const prop of Object.keys(propsDef)) {
|
||||
|
@ -43,14 +44,14 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
|||
};
|
||||
watch(widgetProps, () => {
|
||||
mergeProps();
|
||||
}, { deep: true, immediate: true, });
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
const save = throttle(3000, () => {
|
||||
emit('updateProps', widgetProps);
|
||||
});
|
||||
|
||||
const configure = async () => {
|
||||
const form = JSON.parse(JSON.stringify(propsDef));
|
||||
const form = deepClone(propsDef);
|
||||
for (const item of Object.keys(form)) {
|
||||
form[item].default = widgetProps[item];
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
"lint": "eslint --quiet src/**/*.{ts}"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "^0.14.54",
|
||||
"esbuild": "^0.15.14",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"misskey-js": "0.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.27.0"
|
||||
"eslint": "^8.28.0"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue