mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-16 15:07:36 -07:00
Merge pull request 'develop' (#9125) from develop into main
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9125
This commit is contained in:
commit
c124d2cb25
64 changed files with 450 additions and 345 deletions
|
@ -1,5 +1,5 @@
|
||||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
# Misskey configuration
|
# Calckey configuration
|
||||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
# ┌─────┐
|
# ┌─────┐
|
||||||
|
@ -38,11 +38,11 @@ db:
|
||||||
port: 5432
|
port: 5432
|
||||||
|
|
||||||
# Database name
|
# Database name
|
||||||
db: misskey
|
db: calckey
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
user: example-misskey-user
|
user: example-calckey-user
|
||||||
pass: example-misskey-pass
|
pass: example-calckey-pass
|
||||||
|
|
||||||
# Whether disable Caching queries
|
# Whether disable Caching queries
|
||||||
#disableCache: true
|
#disableCache: true
|
||||||
|
@ -147,7 +147,8 @@ id: 'aid'
|
||||||
|
|
||||||
# Managed hosting settings
|
# 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!
|
# 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...
|
# If you mess this up, that's on you, you've been warned...
|
||||||
|
@ -181,4 +182,11 @@ id: 'aid'
|
||||||
# connnectOverProxy: false
|
# connnectOverProxy: false
|
||||||
# setPublicReadOnUpload: true
|
# setPublicReadOnUpload: true
|
||||||
# s3ForcePathStyle: 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
|
- User "choices" (recommended users) like Mastodon and Soapbox
|
||||||
- Option to publicize instance blocks
|
- Option to publicize instance blocks
|
||||||
- Fully revamp non-logged-in screen
|
- Fully revamp non-logged-in screen
|
||||||
- Remote follow button
|
|
||||||
- Personal notes for all accounts
|
- Personal notes for all accounts
|
||||||
- Non-nyaify cat mode
|
- Non-nyaify cat mode
|
||||||
- Timeline filters
|
- Timeline filters
|
||||||
|
@ -21,8 +20,8 @@
|
||||||
## Work in progress
|
## Work in progress
|
||||||
|
|
||||||
- Better Messaging UI
|
- Better Messaging UI
|
||||||
- Videos can be played in DMs
|
|
||||||
- Make your password hasn't been pwned
|
- Make your password hasn't been pwned
|
||||||
|
- Remote follow button
|
||||||
- Admin custom CSS
|
- Admin custom CSS
|
||||||
- Add back time machine (jump to date)
|
- Add back time machine (jump to date)
|
||||||
- Improve accesibility score
|
- Improve accesibility score
|
||||||
|
@ -86,6 +85,7 @@
|
||||||
- Link hover effect
|
- Link hover effect
|
||||||
- Replace all `$ts` with i18n
|
- Replace all `$ts` with i18n
|
||||||
- AVIF support
|
- AVIF support
|
||||||
|
- Page drafts
|
||||||
- Obliteration of Ai-chan
|
- Obliteration of Ai-chan
|
||||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
- [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)
|
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
||||||
|
|
100
README.md
100
README.md
|
@ -1,15 +1,15 @@
|
||||||
<div align="center">
|
<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"/>
|
<img src="./.github/title_float.svg" alt="Calckey logo" style="border-radius:50%" width="400"/>
|
||||||
</a>
|
</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>
|
||||||
|
|
||||||
<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
|
# ✨ About Calckey
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@
|
||||||
|
|
||||||
# 🥂 Links
|
# 🥂 Links
|
||||||
|
|
||||||
|
- 🚢 Flagship instance: <https://i.calckey.cloud>
|
||||||
|
- 📣 Official account: <https://i.calckey.cloud/@calckey>
|
||||||
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
|
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
|
||||||
- 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com>
|
- 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com>
|
||||||
- 📜 Instance list: <https://calckey.fediverse.observer/list>
|
- 📜 Instance list: <https://calckey.fediverse.observer/list>
|
||||||
|
@ -93,89 +95,17 @@ cp -r ../misskey/files . # if you don't use object storage
|
||||||
|
|
||||||
## 🍀 NGINX
|
## 🍀 NGINX
|
||||||
|
|
||||||
<details>
|
- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
|
||||||
<summary>Click to see an example NGINX config:</summary>
|
- Edit `calckey.nginx.conf` to reflect your instance properly
|
||||||
|
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
|
||||||
```nginx
|
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
|
||||||
# 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## 🚀 Build and launch!
|
## 🚀 Build and launch!
|
||||||
|
|
||||||
### 🐢 NodeJS
|
### 🐢 NodeJS
|
||||||
|
|
||||||
#### `git pull` and run these steps to update Calckey in the future!
|
#### `git pull` and run these steps to update Calckey in the future!
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
@ -195,13 +125,15 @@ docker up -d
|
||||||
### 🐳 Docker Compose
|
### 🐳 Docker Compose
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo docker compose build
|
docker-compose build
|
||||||
sudo docker-compose run --rm web yarn run init
|
docker-compose run --rm web yarn run init
|
||||||
sudo docker compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## 😉 Tips & Tricks
|
## 😉 Tips & Tricks
|
||||||
|
|
||||||
|
- 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.
|
- 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 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.
|
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
|
||||||
|
@ -209,5 +141,5 @@ sudo docker compose up -d
|
||||||
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
|
- 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
|
- Go back to Overview > click the clipboard icon next to the ID
|
||||||
- Run `psql -d calckey` (or whatever the database name is)
|
- 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)
|
- 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
|
- 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."
|
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"
|
host: "Host"
|
||||||
selectUser: "Select a user"
|
selectUser: "Select a user"
|
||||||
recipient: "Recipient"
|
recipient: "Recipient(s)"
|
||||||
annotation: "Comments"
|
annotation: "Comments"
|
||||||
federation: "Federation"
|
federation: "Federation"
|
||||||
instances: "Instances"
|
instances: "Instances"
|
||||||
|
@ -770,8 +770,8 @@ noBotProtectionWarning: "Bot protection is not configured."
|
||||||
configure: "Configure"
|
configure: "Configure"
|
||||||
postToGallery: "Create new gallery post"
|
postToGallery: "Create new gallery post"
|
||||||
gallery: "Gallery"
|
gallery: "Gallery"
|
||||||
recentPosts: "Recent posts"
|
recentPosts: "Recent pages"
|
||||||
popularPosts: "Popular posts"
|
popularPosts: "Popular pages"
|
||||||
shareWithNote: "Share with note"
|
shareWithNote: "Share with note"
|
||||||
ads: "Advertisements"
|
ads: "Advertisements"
|
||||||
expiration: "Deadline"
|
expiration: "Deadline"
|
||||||
|
@ -1094,7 +1094,7 @@ _channel:
|
||||||
usersCount: "{n} Participants"
|
usersCount: "{n} Participants"
|
||||||
notesCount: "{n} Notes"
|
notesCount: "{n} Notes"
|
||||||
_messaging:
|
_messaging:
|
||||||
dms: "DMs"
|
dms: "Private"
|
||||||
groups: "Groups"
|
groups: "Groups"
|
||||||
_menuDisplay:
|
_menuDisplay:
|
||||||
sideFull: "Side"
|
sideFull: "Side"
|
||||||
|
@ -1264,10 +1264,10 @@ _permissions:
|
||||||
"read:reactions": "View your reactions"
|
"read:reactions": "View your reactions"
|
||||||
"write:reactions": "Edit your reactions"
|
"write:reactions": "Edit your reactions"
|
||||||
"write:votes": "Vote on a poll"
|
"write:votes": "Vote on a poll"
|
||||||
"read:pages": "View your pages"
|
"read:pages": "View your page"
|
||||||
"write:pages": "Edit or delete your pages"
|
"write:pages": "Edit or delete your page"
|
||||||
"read:page-likes": "View your likes on pages"
|
"read:page-likes": "View your likes on page"
|
||||||
"write:page-likes": "Edit your likes on pages"
|
"write:page-likes": "Edit your likes on page"
|
||||||
"read:user-groups": "View your user groups"
|
"read:user-groups": "View your user groups"
|
||||||
"write:user-groups": "Edit or delete your user groups"
|
"write:user-groups": "Edit or delete your user groups"
|
||||||
"read:channels": "View your channels"
|
"read:channels": "View your channels"
|
||||||
|
@ -1441,7 +1441,7 @@ _pages:
|
||||||
liked: "Liked Pages"
|
liked: "Liked Pages"
|
||||||
featured: "Popular"
|
featured: "Popular"
|
||||||
inspector: "Inspector"
|
inspector: "Inspector"
|
||||||
contents: "Contents"
|
contents: "Content"
|
||||||
content: "Page block"
|
content: "Page block"
|
||||||
variables: "Variables"
|
variables: "Variables"
|
||||||
title: "Title"
|
title: "Title"
|
||||||
|
|
16
package.json
16
package.json
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "12.119.0-calc.14",
|
"version": "12.119.0-calc.15",
|
||||||
"codename": "aqua",
|
"codename": "aqua",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://codeberg.org/thatonecalculator/calckey.git"
|
"url": "https://codeberg.org/thatonecalculator/calckey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.2.4",
|
"packageManager": "yarn@3.3.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/client",
|
"packages/client",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
|
@ -39,10 +39,10 @@
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.6.3",
|
"@bull-board/api": "^4.6.4",
|
||||||
"@bull-board/ui": "^4.6.3",
|
"@bull-board/ui": "^4.6.4",
|
||||||
"@tensorflow/tfjs": "^3.21.0",
|
"@tensorflow/tfjs": "^3.21.0",
|
||||||
"eslint": "^8.27.0",
|
"eslint": "^8.28.0",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-cssnano": "2.1.3",
|
"gulp-cssnano": "2.1.3",
|
||||||
|
@ -55,13 +55,13 @@
|
||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.9",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/parser": "5.42.1",
|
"@typescript-eslint/parser": "5.43.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "10.11.0",
|
"cypress": "10.11.0",
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"typescript": "4.8.4",
|
"typescript": "4.9.3",
|
||||||
"vue-eslint-parser": "^9.1.0"
|
"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"
|
"@tensorflow/tfjs-node": "3.21.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.6.3",
|
"@bull-board/api": "^4.6.4",
|
||||||
"@bull-board/koa": "^4.6.3",
|
"@bull-board/koa": "^4.6.4",
|
||||||
"@bull-board/ui": "^4.6.3",
|
"@bull-board/ui": "^4.6.4",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@elastic/elasticsearch": "7.17.0",
|
"@elastic/elasticsearch": "7.17.0",
|
||||||
"@koa/cors": "3.4.3",
|
"@koa/cors": "3.4.3",
|
||||||
|
@ -32,15 +32,15 @@
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "9.1.2",
|
"@sinonjs/fake-timers": "9.1.2",
|
||||||
"@syuilo/aiscript": "0.11.1",
|
"@syuilo/aiscript": "0.11.1",
|
||||||
"ajv": "8.11.0",
|
"ajv": "8.11.2",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1253.0",
|
"aws-sdk": "2.1258.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"bull": "4.10.1",
|
"bull": "4.10.1",
|
||||||
"cacheable-lookup": "6.1.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "8.1.0",
|
"cbor": "8.1.0",
|
||||||
"chalk": "5.1.2",
|
"chalk": "5.1.2",
|
||||||
"chalk-template": "0.4.0",
|
"chalk-template": "0.4.0",
|
||||||
|
@ -54,10 +54,10 @@
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "17.1.6",
|
"file-type": "17.1.6",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"got": "12.5.2",
|
"got": "12.5.3",
|
||||||
"hpagent": "0.1.2",
|
"hpagent": "0.1.2",
|
||||||
"ioredis": "4.28.5",
|
"ioredis": "4.28.5",
|
||||||
"ip-cidr": "3.0.10",
|
"ip-cidr": "3.0.11",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "20.0.2",
|
"jsdom": "20.0.2",
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
"node-fetch": "3.3.0",
|
"node-fetch": "3.3.0",
|
||||||
"nodemailer": "6.8.0",
|
"nodemailer": "6.8.0",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "^0.9.15",
|
"oauth": "^0.10.0",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"parse5": "7.1.1",
|
"parse5": "7.1.1",
|
||||||
"pg": "8.8.0",
|
"pg": "8.8.0",
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "2.7.0",
|
"summaly": "2.7.0",
|
||||||
"syslog-pro": "1.0.0",
|
"syslog-pro": "1.0.0",
|
||||||
"systeminformation": "5.12.14",
|
"systeminformation": "5.13.5",
|
||||||
"tesseract.js": "^3.0.3",
|
"tesseract.js": "^3.0.3",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/openapi-core": "1.0.0-beta.112",
|
"@redocly/openapi-core": "1.0.0-beta.114",
|
||||||
"@types/bcryptjs": "2.4.2",
|
"@types/bcryptjs": "2.4.2",
|
||||||
"@types/bull": "3.15.9",
|
"@types/bull": "3.15.9",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
|
@ -165,7 +165,7 @@
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.4",
|
||||||
"@types/sanitize-html": "2.6.2",
|
"@types/sanitize-html": "2.6.2",
|
||||||
"@types/semver": "7.3.13",
|
"@types/semver": "7.3.13",
|
||||||
"@types/sharp": "0.30.5",
|
"@types/sharp": "0.31.0",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.2",
|
||||||
"@types/speakeasy": "2.0.7",
|
"@types/speakeasy": "2.0.7",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
|
@ -174,12 +174,12 @@
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||||
"@typescript-eslint/parser": "5.42.1",
|
"@typescript-eslint/parser": "5.43.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.27.0",
|
"eslint": "8.28.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"execa": "6.1.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')
|
@Column('boolean')
|
||||||
public alignCenter: boolean;
|
public alignCenter: boolean;
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
public isPublic: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { query, appendQuery } from '@/prelude/url.js';
|
||||||
import { Meta } from '@/models/entities/meta.js';
|
import { Meta } from '@/models/entities/meta.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Users, DriveFolders } from '../index.js';
|
import { Users, DriveFolders } from '../index.js';
|
||||||
|
import { deepClone } from '@/misc/clone.js';
|
||||||
|
|
||||||
|
|
||||||
type PackOptions = {
|
type PackOptions = {
|
||||||
detail?: boolean,
|
detail?: boolean,
|
||||||
|
@ -29,9 +31,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||||
|
|
||||||
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||||
if (file.properties.orientation != null) {
|
if (file.properties.orientation != null) {
|
||||||
// TODO
|
const properties = deepClone(file.properties);
|
||||||
//const properties = structuredClone(file.properties);
|
|
||||||
const properties = JSON.parse(JSON.stringify(file.properties));
|
|
||||||
if (file.properties.orientation >= 5) {
|
if (file.properties.orientation >= 5) {
|
||||||
[properties.width, properties.height] = [properties.height, properties.width];
|
[properties.width, properties.height] = [properties.height, properties.width];
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const PageRepository = db.getRepository(Page).extend({
|
||||||
content: page.content,
|
content: page.content,
|
||||||
variables: page.variables,
|
variables: page.variables,
|
||||||
title: page.title,
|
title: page.title,
|
||||||
|
isPublic: page.isPublic,
|
||||||
name: page.name,
|
name: page.name,
|
||||||
summary: page.summary,
|
summary: page.summary,
|
||||||
hideTitleWhenPinned: page.hideTitleWhenPinned,
|
hideTitleWhenPinned: page.hideTitleWhenPinned,
|
||||||
|
|
|
@ -47,5 +47,9 @@ export const packedPageSchema = {
|
||||||
ref: 'UserLite',
|
ref: 'UserLite',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isPublic: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -53,6 +53,7 @@ export const paramDef = {
|
||||||
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
|
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
|
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
|
||||||
alignCenter: { type: 'boolean', default: false },
|
alignCenter: { type: 'boolean', default: false },
|
||||||
|
isPublic: { type: 'boolean', default: true },
|
||||||
hideTitleWhenPinned: { type: 'boolean', default: false },
|
hideTitleWhenPinned: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: ['title', 'name', 'content', 'variables', 'script'],
|
required: ['title', 'name', 'content', 'variables', 'script'],
|
||||||
|
@ -97,6 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
alignCenter: ps.alignCenter,
|
alignCenter: ps.alignCenter,
|
||||||
hideTitleWhenPinned: ps.hideTitleWhenPinned,
|
hideTitleWhenPinned: ps.hideTitleWhenPinned,
|
||||||
font: ps.font,
|
font: ps.font,
|
||||||
|
isPublic: ps.isPublic,
|
||||||
})).then(x => Pages.findOneByOrFail(x.identifiers[0]));
|
})).then(x => Pages.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
return await Pages.pack(page);
|
return await Pages.pack(page);
|
||||||
|
|
|
@ -67,5 +67,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw new ApiError(meta.errors.noSuchPage);
|
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);
|
return await Pages.pack(page, user);
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,6 +60,7 @@ export const paramDef = {
|
||||||
font: { type: 'string', enum: ['serif', 'sans-serif'] },
|
font: { type: 'string', enum: ['serif', 'sans-serif'] },
|
||||||
alignCenter: { type: 'boolean' },
|
alignCenter: { type: 'boolean' },
|
||||||
hideTitleWhenPinned: { type: 'boolean' },
|
hideTitleWhenPinned: { type: 'boolean' },
|
||||||
|
isPublic: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
|
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
content: ps.content,
|
content: ps.content,
|
||||||
variables: ps.variables,
|
variables: ps.variables,
|
||||||
script: ps.script,
|
script: ps.script,
|
||||||
|
isPublic: ps.isPublic,
|
||||||
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
|
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
|
||||||
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
|
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
|
||||||
font: ps.font === undefined ? page.font : ps.font,
|
font: ps.font === undefined ? page.font : ps.font,
|
||||||
|
|
|
@ -34,7 +34,8 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('page.userId = :userId', { userId: ps.userId })
|
.andWhere('page.userId = :userId', { userId: ps.userId })
|
||||||
.andWhere('page.visibility = \'public\'');
|
.andWhere('page.visibility = \'public\'')
|
||||||
|
.andWhere('page.isPublic = true');
|
||||||
|
|
||||||
const pages = await query
|
const pages = await query
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"broadcast-channel": "4.18.1",
|
"broadcast-channel": "4.18.1",
|
||||||
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
|
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
|
||||||
"chart.js": "3.9.1",
|
"chart.js": "4.0.1",
|
||||||
"chartjs-adapter-date-fns": "2.0.0",
|
"chartjs-adapter-date-fns": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.5.1",
|
"chartjs-plugin-gradient": "0.5.1",
|
||||||
"chartjs-plugin-zoom": "1.2.1",
|
"chartjs-plugin-zoom": "1.2.1",
|
||||||
"compare-versions": "5.0.1",
|
"compare-versions": "5.0.1",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"idb-keyval": "6.2.0",
|
"idb-keyval": "6.2.0",
|
||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
"json5": "2.2.1",
|
"json5": "2.2.1",
|
||||||
"katex": "0.15.6",
|
"katex": "0.16.3",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.0",
|
||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
|
@ -48,16 +48,16 @@
|
||||||
"swiper": "^8.4.4",
|
"swiper": "^8.4.4",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.144.0",
|
"three": "0.146.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
"tsc-alias": "1.7.1",
|
"tsc-alias": "1.7.1",
|
||||||
"tsconfig-paths": "4.1.0",
|
"tsconfig-paths": "4.1.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typescript": "4.8.4",
|
"typescript": "4.9.3",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vanilla-tilt": "1.7.3",
|
"vanilla-tilt": "1.7.3",
|
||||||
"vite": "^3.2.3",
|
"vite": "^3.2.4",
|
||||||
"vue": "3.2.45",
|
"vue": "3.2.45",
|
||||||
"vue-isyourpasswordsafe": "^2.0.0",
|
"vue-isyourpasswordsafe": "^2.0.0",
|
||||||
"vue-plyr": "^7.0.0",
|
"vue-plyr": "^7.0.0",
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/glob": "8.0.0",
|
"@types/glob": "8.0.0",
|
||||||
"@types/gulp": "4.0.9",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@types/katex": "0.14.0",
|
"@types/katex": "0.14.0",
|
||||||
"@types/matter-js": "0.18.2",
|
"@types/matter-js": "0.18.2",
|
||||||
|
@ -76,11 +76,11 @@
|
||||||
"@types/throttle-debounce": "5.0.0",
|
"@types/throttle-debounce": "5.0.0",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||||
"@typescript-eslint/parser": "5.42.1",
|
"@typescript-eslint/parser": "5.43.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "10.11.0",
|
"cypress": "10.11.0",
|
||||||
"eslint": "8.27.0",
|
"eslint": "8.28.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-vue": "9.7.0",
|
"eslint-plugin-vue": "9.7.0",
|
||||||
"rollup": "2.79.1",
|
"rollup": "2.79.1",
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<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>
|
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="kpoogebi _button"
|
<button
|
||||||
|
class="kpoogebi _button"
|
||||||
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
|
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
|
||||||
:disabled="wait"
|
:disabled="wait"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
@ -8,7 +9,8 @@
|
||||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||||
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
|
||||||
</template>
|
</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>
|
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isFollowing">
|
<template v-else-if="isFollowing">
|
||||||
|
@ -29,7 +31,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted } from 'vue';
|
import { onBeforeUnmount, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import type * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -38,7 +40,7 @@ const props = withDefaults(defineProps<{
|
||||||
user: Misskey.entities.UserDetailed,
|
user: Misskey.entities.UserDetailed,
|
||||||
full?: boolean,
|
full?: boolean,
|
||||||
large?: boolean,
|
large?: boolean,
|
||||||
}>(), {
|
}>(), {
|
||||||
full: false,
|
full: false,
|
||||||
large: false,
|
large: false,
|
||||||
});
|
});
|
||||||
|
@ -50,7 +52,7 @@ const connection = stream.useChannel('main');
|
||||||
|
|
||||||
if (props.user.isFollowing == null) {
|
if (props.user.isFollowing == null) {
|
||||||
os.api('users/show', {
|
os.api('users/show', {
|
||||||
userId: props.user.id
|
userId: props.user.id,
|
||||||
})
|
})
|
||||||
.then(onFollowChange);
|
.then(onFollowChange);
|
||||||
}
|
}
|
||||||
|
@ -75,17 +77,17 @@ async function onClick() {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
await os.api('following/delete', {
|
await os.api('following/delete', {
|
||||||
userId: props.user.id
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (hasPendingFollowRequestFromYou) {
|
if (hasPendingFollowRequestFromYou) {
|
||||||
await os.api('following/requests/cancel', {
|
await os.api('following/requests/cancel', {
|
||||||
userId: props.user.id
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
hasPendingFollowRequestFromYou = false;
|
hasPendingFollowRequestFromYou = false;
|
||||||
} else {
|
} else {
|
||||||
await os.api('following/create', {
|
await os.api('following/create', {
|
||||||
userId: props.user.id
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
hasPendingFollowRequestFromYou = true;
|
hasPendingFollowRequestFromYou = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else class="translated">
|
<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"/>
|
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,9 +104,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 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 MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
|
@ -134,6 +135,7 @@ import { i18n } from '@/i18n';
|
||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from '@/filters/note';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -144,12 +146,12 @@ const props = defineProps<{
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let result = JSON.parse(JSON.stringify(note));
|
let result = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
result = await interruptor.handler(result);
|
result = await interruptor.handler(result);
|
||||||
}
|
}
|
||||||
|
@ -432,7 +434,9 @@ function readPromo() {
|
||||||
width: 58px;
|
width: 58px;
|
||||||
height: 58px;
|
height: 58px;
|
||||||
position: sticky;
|
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;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else class="translated">
|
<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"/>
|
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -143,6 +143,7 @@ import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -153,12 +154,12 @@ const props = defineProps<{
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let result = JSON.parse(JSON.stringify(note));
|
let result = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
result = await interruptor.handler(result);
|
result = await interruptor.handler(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
||||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
||||||
<div class="sub-icon" :class="notification.type">
|
<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 === 'receiveFollowRequest'" class="ph-clock-bold"></i>
|
||||||
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check-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>
|
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i>
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
<span>
|
<span>
|
||||||
<template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template>
|
<template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template>
|
||||||
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-if="!readOnly">
|
<p v-if="!readOnly">
|
||||||
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
|
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
<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>
|
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(choice, i) in choices" :key="i">
|
<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>
|
</MkInput>
|
||||||
<button class="_button" @click="remove(i)">
|
<button class="_button" @click="remove(i)">
|
||||||
<i class="ph-x-bold ph-lg"></i>
|
<i class="ph-x-bold ph-lg"></i>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<MkAcct :user="u"/>
|
<MkAcct :user="u"/>
|
||||||
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
|
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
<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 { instance } from '@/instance';
|
||||||
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
import { uploadFile } from '@/scripts/upload';
|
import { uploadFile } from '@/scripts/upload';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
const modal = inject('modal');
|
const modal = inject('modal');
|
||||||
|
|
||||||
|
@ -458,7 +459,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
|
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
os.confirm({
|
os.yesno({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
text: i18n.ts.quoteQuestion,
|
text: i18n.ts.quoteQuestion,
|
||||||
}).then(({ canceled }) => {
|
}).then(({ canceled }) => {
|
||||||
|
@ -575,7 +576,7 @@ async function post() {
|
||||||
// plugin
|
// plugin
|
||||||
if (notePostInterruptors.length > 0) {
|
if (notePostInterruptors.length > 0) {
|
||||||
for (const interruptor of notePostInterruptors) {
|
for (const interruptor of notePostInterruptors) {
|
||||||
postData = await interruptor.handler(JSON.parse(JSON.stringify(postData)));
|
postData = await interruptor.handler(deepClone(postData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -832,7 +833,7 @@ onMounted(() => {
|
||||||
padding: 6px 24px;
|
padding: 6px 24px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
white-space: nowrap;
|
line-height: 2rem;
|
||||||
|
|
||||||
> .visibleUsers {
|
> .visibleUsers {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
@ -840,15 +841,19 @@ onMounted(() => {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
padding: 4px;
|
padding: 2px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
margin-right: 14px;
|
margin: 0.3rem;
|
||||||
padding: 8px 0 8px 8px;
|
padding: 4px 0 4px 4px;
|
||||||
border-radius: 8px;
|
border-radius: 999px;
|
||||||
background: var(--X4);
|
background: var(--X3);
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
|
|
@ -41,9 +41,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="social _section">
|
<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.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>{{ $t('signinWith', { x: 'GitHub' }) }}</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>{{ $t('signinWith', { x: 'Discord' }) }}</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -51,6 +51,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
|
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
import MkInfo from '@/components/MkInfo.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 { byteify, hexify } from '@/scripts/2fa';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { login } from '@/account';
|
import { login } from '@/account';
|
||||||
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
|
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ const props = defineProps({
|
||||||
withAvatar: {
|
withAvatar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
autoSet: {
|
autoSet: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -95,13 +95,13 @@ const props = defineProps({
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function onUsernameChange() {
|
function onUsernameChange() {
|
||||||
os.api('users/show', {
|
os.api('users/show', {
|
||||||
username: username
|
username: username,
|
||||||
}).then(userResponse => {
|
}).then(userResponse => {
|
||||||
user = userResponse;
|
user = userResponse;
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -123,10 +123,10 @@ function queryKey() {
|
||||||
allowCredentials: challengeData.securityKeys.map(key => ({
|
allowCredentials: challengeData.securityKeys.map(key => ({
|
||||||
id: byteify(key.id, 'hex'),
|
id: byteify(key.id, 'hex'),
|
||||||
type: 'public-key',
|
type: 'public-key',
|
||||||
transports: ['usb', 'nfc', 'ble', 'internal']
|
transports: ['usb', 'nfc', 'ble', 'internal'],
|
||||||
})),
|
})),
|
||||||
timeout: 60 * 1000
|
timeout: 60 * 1000,
|
||||||
}
|
},
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
queryingKey = false;
|
queryingKey = false;
|
||||||
return Promise.reject(null);
|
return Promise.reject(null);
|
||||||
|
@ -151,7 +151,7 @@ function queryKey() {
|
||||||
if (err === null) return;
|
if (err === null) return;
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: i18n.ts.signinFailed
|
text: i18n.ts.signinFailed,
|
||||||
});
|
});
|
||||||
signing = false;
|
signing = false;
|
||||||
});
|
});
|
||||||
|
@ -183,7 +183,7 @@ function onSubmit() {
|
||||||
password,
|
password,
|
||||||
'hcaptcha-response': hCaptchaResponse,
|
'hcaptcha-response': hCaptchaResponse,
|
||||||
'g-recaptcha-response': reCaptchaResponse,
|
'g-recaptcha-response': reCaptchaResponse,
|
||||||
token: user && user.twoFactorEnabled ? token : undefined
|
token: user && user.twoFactorEnabled ? token : undefined,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
emit('login', res);
|
emit('login', res);
|
||||||
onLogin(res);
|
onLogin(res);
|
||||||
|
@ -197,7 +197,7 @@ function loginFailed(err) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: i18n.ts.loginFailed,
|
title: i18n.ts.loginFailed,
|
||||||
text: i18n.ts.noSuchUser
|
text: i18n.ts.noSuchUser,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -226,7 +226,7 @@ function loginFailed(err) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: i18n.ts.loginFailed,
|
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>
|
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.files.length > 0">
|
<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"/>
|
<XMediaList :media-list="note.files"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.poll">
|
<div v-if="note.poll">
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
|
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
|
||||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</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>
|
</div>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkButton
|
MkButton,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -60,7 +60,7 @@ export default defineComponent({
|
||||||
watch(() => props.p, () => {
|
watch(() => props.p, () => {
|
||||||
process();
|
process();
|
||||||
}, {
|
}, {
|
||||||
immediate: true
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
|
@ -73,6 +73,7 @@ export default defineComponent({
|
||||||
rejected,
|
rejected,
|
||||||
result,
|
result,
|
||||||
retry,
|
retry,
|
||||||
|
i18n,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<i v-if="relay.status === 'accepted'" class="ph-check-bold ph-lg icon accepted"></i>
|
<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-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>
|
<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>
|
</div>
|
||||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
|
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="_section">
|
<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">
|
<div class="_content">
|
||||||
<h2>{{ app.name }}</h2>
|
<h2>{{ app.name }}</h2>
|
||||||
<p class="id">{{ app.id }}</p>
|
<p class="id">{{ app.id }}</p>
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
<h2>{{ i18n.ts._auth.permissionAsk }}</h2>
|
<h2>{{ i18n.ts._auth.permissionAsk }}</h2>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="_footer">
|
<div class="_footer">
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<h1>{{ i18n.ts._auth.denied }}</h1>
|
<h1>{{ i18n.ts._auth.denied }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="state == 'accepted'" class="accepted">
|
<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.callback }}<MkEllipsis/></p>
|
||||||
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
|
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,6 +47,7 @@ export default defineComponent({
|
||||||
state: null,
|
state: null,
|
||||||
session: null,
|
session: null,
|
||||||
fetching: true,
|
fetching: true,
|
||||||
|
i18n,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -202,11 +202,11 @@ definePageMetadata(computed(() => post ? {
|
||||||
|
|
||||||
> .like {
|
> .like {
|
||||||
> .button {
|
> .button {
|
||||||
--accent: rgb(241 97 132);
|
--accent: #eb6f92;
|
||||||
--X8: rgb(241 92 128);
|
--X8: #eb6f92;
|
||||||
--buttonBg: rgb(216 71 106 / 5%);
|
--buttonBg: rgb(216 71 106 / 5%);
|
||||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||||
color: #ff002f;
|
color: #eb6f92;
|
||||||
|
|
||||||
::v-deep(.count) {
|
::v-deep(.count) {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ max: [400, 500] }" class="thvuemwp" :class="{ isMe }">
|
<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="content">
|
||||||
<div class="balloon" :class="{ noText: message.text == null }">
|
<div class="balloon" :class="{ noText: message.text == null }">
|
||||||
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
|
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
|
||||||
|
@ -38,7 +38,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import VuePlyr from 'vue-plyr';
|
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'misskey-js';
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
import XMediaList from '@/components/MkMediaList.vue';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||||
|
@ -73,10 +72,10 @@ function del(): void {
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: calc(var(--stickyTop, 0px) + 16px);
|
top: calc(var(--stickyTop, 0px) + 20px);
|
||||||
display: block;
|
display: block;
|
||||||
width: 54px;
|
width: 45px;
|
||||||
height: 54px;
|
height: 45px;
|
||||||
transition: all 0.1s ease;
|
transition: all 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,14 +90,7 @@ function del(): void {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin-left: 10rem;
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + * {
|
& + * {
|
||||||
clear: both;
|
clear: both;
|
||||||
|
@ -222,7 +214,9 @@ function del(): void {
|
||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
|
|
||||||
> .balloon {
|
> .balloon {
|
||||||
$color: var(--messageBg);
|
$color: var(--X4);
|
||||||
|
margin-right: 10rem;
|
||||||
|
margin-left: 0rem !important;
|
||||||
background: $color;
|
background: $color;
|
||||||
|
|
||||||
&.noText {
|
&.noText {
|
||||||
|
|
|
@ -18,12 +18,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_section">
|
<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 v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
<p>{{ i18n.ts._auth.permissionAsk }}</p>
|
<p>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="_footer">
|
<div class="_footer">
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<MkInput v-model="value.message"><template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
|
<MkInput v-model="value.message"><template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
|
||||||
<MkSelect v-model="value.var">
|
<MkSelect v-model="value.var">
|
||||||
<template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.variable }}</template>
|
<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>
|
<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
|
||||||
<optgroup :label="i18n.ts._pages.script.pageVariables">
|
<optgroup :label="i18n.ts._pages.script.pageVariables">
|
||||||
<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>
|
<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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="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">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</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">
|
<div v-show="showBody" class="body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,34 +26,36 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
expanded: {
|
expanded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
removable: {
|
removable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
draggable: {
|
draggable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
warn: {
|
warn: {
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ['toggle', 'remove'],
|
emits: ['toggle', 'remove'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showBody: this.expanded,
|
showBody: this.expanded,
|
||||||
|
i18n,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -63,8 +65,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
remove() {
|
remove() {
|
||||||
this.$emit('remove');
|
this.$emit('remove');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -43,15 +43,15 @@
|
||||||
<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
|
<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
|
||||||
<MkTextarea v-model="slots">
|
<MkTextarea v-model="slots">
|
||||||
<template #label>{{ i18n.ts._pages.script.blocks._fn.slots }}</template>
|
<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>
|
</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>
|
||||||
<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
|
<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"/>
|
<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>
|
||||||
<section v-else class="" style="padding:16px;">
|
<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>
|
</section>
|
||||||
</XContainer>
|
</XContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -124,7 +124,7 @@ export default defineComponent({
|
||||||
typeText(): any {
|
typeText(): any {
|
||||||
if (this.modelValue.type === null) return null;
|
if (this.modelValue.type === null) return null;
|
||||||
if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
|
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>
|
<template #label>{{ i18n.ts._pages.url }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
<MkSwitch v-model="isPublic" class="_formBlock">{{ i18n.ts.public }}</MkSwitch>
|
||||||
<MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
|
<MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
|
||||||
|
|
||||||
<MkSelect v-model="font" class="_formBlock">
|
<MkSelect v-model="font" class="_formBlock">
|
||||||
|
@ -47,7 +48,6 @@
|
||||||
<div v-else-if="tab === 'contents'">
|
<div v-else-if="tab === 'contents'">
|
||||||
<div>
|
<div>
|
||||||
<XBlocks v-model="content" class="content" :hpml="hpml"/>
|
<XBlocks v-model="content" class="content" :hpml="hpml"/>
|
||||||
|
|
||||||
<MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton>
|
<MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -130,6 +130,7 @@ let eyeCatchingImageId = $ref(null);
|
||||||
let font = $ref('sans-serif');
|
let font = $ref('sans-serif');
|
||||||
let content = $ref([]);
|
let content = $ref([]);
|
||||||
let alignCenter = $ref(false);
|
let alignCenter = $ref(false);
|
||||||
|
let isPublic = $ref(true);
|
||||||
let hideTitleWhenPinned = $ref(false);
|
let hideTitleWhenPinned = $ref(false);
|
||||||
let variables = $ref([]);
|
let variables = $ref([]);
|
||||||
let hpml = $ref(null);
|
let hpml = $ref(null);
|
||||||
|
@ -158,6 +159,7 @@ function getSaveOptions() {
|
||||||
script: script,
|
script: script,
|
||||||
hideTitleWhenPinned: hideTitleWhenPinned,
|
hideTitleWhenPinned: hideTitleWhenPinned,
|
||||||
alignCenter: alignCenter,
|
alignCenter: alignCenter,
|
||||||
|
isPublic: isPublic,
|
||||||
content: content,
|
content: content,
|
||||||
variables: variables,
|
variables: variables,
|
||||||
eyeCatchingImageId: eyeCatchingImageId,
|
eyeCatchingImageId: eyeCatchingImageId,
|
||||||
|
@ -393,6 +395,7 @@ async function init() {
|
||||||
script = page.script;
|
script = page.script;
|
||||||
hideTitleWhenPinned = page.hideTitleWhenPinned;
|
hideTitleWhenPinned = page.hideTitleWhenPinned;
|
||||||
alignCenter = page.alignCenter;
|
alignCenter = page.alignCenter;
|
||||||
|
isPublic = page.isPublic;
|
||||||
content = page.content;
|
content = page.content;
|
||||||
variables = page.variables;
|
variables = page.variables;
|
||||||
eyeCatchingImageId = page.eyeCatchingImageId;
|
eyeCatchingImageId = page.eyeCatchingImageId;
|
||||||
|
@ -401,7 +404,7 @@ async function init() {
|
||||||
content = [{
|
content = [{
|
||||||
id,
|
id,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Hello World!',
|
text: '',
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -447,7 +450,7 @@ definePageMetadata(computed(() => {
|
||||||
.jqqmcavi {
|
.jqqmcavi {
|
||||||
> .button {
|
> .button {
|
||||||
& + .button {
|
& + .button {
|
||||||
margin-left: 8px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<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">
|
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||||
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
|
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
|
||||||
<div class="_block main">
|
<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.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>
|
<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>
|
|
||||||
<div class="user">
|
<div class="user">
|
||||||
<MkAvatar :user="page.user" class="avatar"/>
|
<MkAvatar :user="page.user" class="avatar"/>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<MkUserName :user="page.user" style="display: block;"/>
|
<MkUserName :user="page.user" style="display: block;"/>
|
||||||
<MkAcct :user="page.user"/>
|
<MkAcct :user="page.user"/>
|
||||||
</div>
|
</div>
|
||||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||||
|
@ -176,6 +176,10 @@ definePageMetadata(computed(() => page ? {
|
||||||
.xcukqgmh {
|
.xcukqgmh {
|
||||||
> .main {
|
> .main {
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
|
@ -185,6 +189,8 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
|
margin: 0rem !important;
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
// TODO: 良い感じのアスペクト比で表示
|
// TODO: 良い感じのアスペクト比で表示
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -195,7 +201,6 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
margin: 1rem;
|
|
||||||
padding: 16px 0 0 0;
|
padding: 16px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,11 +213,11 @@ definePageMetadata(computed(() => page ? {
|
||||||
|
|
||||||
> .like {
|
> .like {
|
||||||
> .button {
|
> .button {
|
||||||
--accent: rgb(241 97 132);
|
--accent: #eb6f92;
|
||||||
--X8: rgb(241 92 128);
|
--X8: #eb6f92;
|
||||||
--buttonBg: rgb(216 71 106 / 5%);
|
--buttonBg: rgb(216 71 106 / 5%);
|
||||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||||
color: #ff002f;
|
color: #eb6f92;
|
||||||
|
|
||||||
::v-deep(.count) {
|
::v-deep(.count) {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
|
@ -221,8 +226,6 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .other {
|
> .other {
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
|
@ -232,12 +235,9 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
> .user {
|
> .user {
|
||||||
margin-top: 16px;
|
margin-left: auto;
|
||||||
padding: 16px 0 0 0;
|
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@ -253,16 +253,18 @@ definePageMetadata(computed(() => page ? {
|
||||||
|
|
||||||
> .koudoku {
|
> .koudoku {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .links {
|
> .links {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 24px 0 0 0;
|
padding: 14px 0;
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
|
||||||
> .link {
|
> .link {
|
||||||
margin-right: 0.75em;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ i18n.ts.details }}</summary>
|
<summary>{{ i18n.ts.details }}</summary>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -66,8 +66,9 @@ import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
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 reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
|
||||||
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
|
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
|
||||||
|
@ -101,7 +102,7 @@ async function setDefault() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
|
reactions = deepClone(defaultStore.def.reactions.default);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseEmoji(ev: MouseEvent) {
|
function chooseEmoji(ev: MouseEvent) {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.sounds }}</template>
|
<template #label>{{ i18n.ts.sounds }}</template>
|
||||||
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
|
<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 #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
|
||||||
<template #suffixIcon><i class="ph-caret-down-bold ph-lg"></i></template>
|
<template #suffixIcon><i class="ph-caret-down-bold ph-lg"></i></template>
|
||||||
</FormLink>
|
</FormLink>
|
||||||
|
|
|
@ -91,13 +91,14 @@ import FormRange from '@/components/form/range.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
_id: string;
|
_id: string;
|
||||||
userLists: any[] | null;
|
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, () => {
|
watch(() => statusbar.type, () => {
|
||||||
if (statusbar.type === 'rss') {
|
if (statusbar.type === 'rss') {
|
||||||
|
@ -128,8 +129,8 @@ watch(statusbar, save);
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
|
const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
|
||||||
const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
|
const statusbars = deepClone(defaultStore.state.statusbars);
|
||||||
statusbars[i] = JSON.parse(JSON.stringify(statusbar));
|
statusbars[i] = deepClone(statusbar);
|
||||||
defaultStore.set('statusbars', statusbars);
|
defaultStore.set('statusbars', statusbars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,9 +86,7 @@ if (defaultStore.reactiveState.tutorial.value !== -1) {
|
||||||
const isLocalTimelineAvailable =
|
const isLocalTimelineAvailable =
|
||||||
!instance.disableLocalTimeline ||
|
!instance.disableLocalTimeline ||
|
||||||
($i != null && ($i.isModerator || $i.isAdmin));
|
($i != null && ($i.isModerator || $i.isAdmin));
|
||||||
const isRecommendedTimelineAvailable =
|
const isRecommendedTimelineAvailable = !instance.disableRecommendedTimeline;
|
||||||
!instance.disableRecommendedTimeline ||
|
|
||||||
($i != null && ($i.isModerator || $i.isAdmin));
|
|
||||||
const isGlobalTimelineAvailable =
|
const isGlobalTimelineAvailable =
|
||||||
!instance.disableGlobalTimeline ||
|
!instance.disableGlobalTimeline ||
|
||||||
($i != null && ($i.isModerator || $i.isAdmin));
|
($i != null && ($i.isModerator || $i.isAdmin));
|
||||||
|
|
|
@ -24,9 +24,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
|
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
</dl>
|
</dl>
|
||||||
<dl v-if="user.birthday" class="field">
|
<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>
|
<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>
|
||||||
<dl class="field">
|
<dl class="field">
|
||||||
<dt class="name"><i class="ph-calendar-blank-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.registeredDate }}</dt>
|
<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',
|
key: 'home',
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
icon: 'ph-user-bold ph-large',
|
icon: 'ph-user-bold ph-lg',
|
||||||
},
|
},
|
||||||
...(($i && $i.id === user.id) || user.publicReactions
|
...(($i && $i.id === user.id) || user.publicReactions
|
||||||
? [{
|
? [{
|
||||||
key: 'reactions',
|
key: 'reactions',
|
||||||
title: i18n.ts.reaction,
|
title: i18n.ts.reaction,
|
||||||
icon: 'ph-smiley-bold ph-large',
|
icon: 'ph-smiley-bold ph-lg',
|
||||||
}] : []),
|
}] : []),
|
||||||
...(user.instance == null ? [{
|
...(user.instance == null ? [{
|
||||||
key: 'clips',
|
key: 'clips',
|
||||||
title: i18n.ts.clips,
|
title: i18n.ts.clips,
|
||||||
icon: 'ph-paperclip-bold ph-large',
|
icon: 'ph-paperclip-bold ph-lg',
|
||||||
}, {
|
}, {
|
||||||
key: 'pages',
|
key: 'pages',
|
||||||
title: i18n.ts.pages,
|
title: i18n.ts.pages,
|
||||||
icon: 'ph-file-text-bold ph-large',
|
icon: 'ph-file-text-bold ph-lg',
|
||||||
}, {
|
}, {
|
||||||
key: 'gallery',
|
key: 'gallery',
|
||||||
title: i18n.ts.gallery,
|
title: i18n.ts.gallery,
|
||||||
icon: 'ph-image-square-bold ph-large',
|
icon: 'ph-image-square-bold ph-lg',
|
||||||
}] : []),
|
}] : []),
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default defineComponent({
|
||||||
|
|
||||||
showMenu(ev) {
|
showMenu(ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: this.$t('aboutX', { x: instanceName }),
|
text: i18n.t('aboutX', { x: instanceName }),
|
||||||
icon: 'ph-info-bold ph-lg',
|
icon: 'ph-info-bold ph-lg',
|
||||||
action: () => {
|
action: () => {
|
||||||
os.pageWindow('/about');
|
os.pageWindow('/about');
|
||||||
|
|
|
@ -125,7 +125,7 @@ export default defineComponent({
|
||||||
|
|
||||||
showMenu(ev) {
|
showMenu(ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: this.$t('aboutX', { x: instanceName }),
|
text: i18n.t('aboutX', { x: instanceName }),
|
||||||
icon: 'ph-info-bold ph-lg',
|
icon: 'ph-info-bold ph-lg',
|
||||||
action: () => {
|
action: () => {
|
||||||
os.pageWindow('/about');
|
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 lightTheme from '@/themes/_light.json5';
|
||||||
import darkTheme from '@/themes/_dark.json5';
|
import darkTheme from '@/themes/_dark.json5';
|
||||||
|
import { deepClone } from './clone';
|
||||||
|
|
||||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
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';
|
const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
|
||||||
|
|
||||||
// Deep copy
|
// Deep copy
|
||||||
const _theme = JSON.parse(JSON.stringify(theme));
|
const _theme = deepClone(theme);
|
||||||
|
|
||||||
if (_theme.base) {
|
if (_theme.base) {
|
||||||
const base = [lightTheme, darkTheme].find(x => x.id === _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>
|
<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>
|
||||||
<div class="middle">
|
<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>
|
||||||
<div class="bottom">
|
<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>
|
<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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
width: 44px;
|
||||||
|
|
||||||
> .top, > .middle, > .bottom {
|
> .top, > .middle, > .bottom {
|
||||||
> .button {
|
> .button {
|
||||||
|
@ -339,6 +339,11 @@ async function deleteProfile() {
|
||||||
> .middle {
|
> .middle {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
> .new {
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: var(--accentedBg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .bottom {
|
> .bottom {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
|
||||||
import { Storage } from '../../pizzax';
|
import { Storage } from '../../pizzax';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { api } from '@/os';
|
import { api } from '@/os';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
type ColumnWidget = {
|
type ColumnWidget = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -25,10 +26,6 @@ export type Column = {
|
||||||
tl?: 'home' | 'local' | 'social' | 'global';
|
tl?: 'home' | 'local' | 'social' | 'global';
|
||||||
};
|
};
|
||||||
|
|
||||||
function copy<T>(x: T): T {
|
|
||||||
return JSON.parse(JSON.stringify(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deckStore = markRaw(new Storage('deck', {
|
export const deckStore = markRaw(new Storage('deck', {
|
||||||
profile: {
|
profile: {
|
||||||
where: 'deviceAccount',
|
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 aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
||||||
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||||
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
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[aX][aY] = b;
|
||||||
layout[bX][bY] = a;
|
layout[bX][bY] = a;
|
||||||
deckStore.set('layout', layout);
|
deckStore.set('layout', layout);
|
||||||
|
@ -136,7 +133,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swapLeftColumn(id: 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) => {
|
deckStore.state.layout.some((ids, i) => {
|
||||||
if (ids.includes(id)) {
|
if (ids.includes(id)) {
|
||||||
const left = deckStore.state.layout[i - 1];
|
const left = deckStore.state.layout[i - 1];
|
||||||
|
@ -152,7 +149,7 @@ export function swapLeftColumn(id: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swapRightColumn(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) => {
|
deckStore.state.layout.some((ids, i) => {
|
||||||
if (ids.includes(id)) {
|
if (ids.includes(id)) {
|
||||||
const right = deckStore.state.layout[i + 1];
|
const right = deckStore.state.layout[i + 1];
|
||||||
|
@ -168,9 +165,9 @@ export function swapRightColumn(id: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swapUpColumn(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 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) => {
|
ids.some((x, i) => {
|
||||||
if (x === id) {
|
if (x === id) {
|
||||||
const up = ids[i - 1];
|
const up = ids[i - 1];
|
||||||
|
@ -188,9 +185,9 @@ export function swapUpColumn(id: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swapDownColumn(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 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) => {
|
ids.some((x, i) => {
|
||||||
if (x === id) {
|
if (x === id) {
|
||||||
const down = ids[i + 1];
|
const down = ids[i + 1];
|
||||||
|
@ -208,7 +205,7 @@ export function swapDownColumn(id: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stackLeftColumn(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));
|
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||||
layout[i - 1].push(id);
|
layout[i - 1].push(id);
|
||||||
|
@ -218,7 +215,7 @@ export function stackLeftColumn(id: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function popRightColumn(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 i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
const affected = layout[i];
|
const affected = layout[i];
|
||||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
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);
|
layout = layout.filter(ids => ids.length > 0);
|
||||||
deckStore.set('layout', layout);
|
deckStore.set('layout', layout);
|
||||||
|
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
if (affected.includes(column.id)) {
|
if (affected.includes(column.id)) {
|
||||||
column.active = true;
|
column.active = true;
|
||||||
|
@ -238,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
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 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 == null) return;
|
||||||
if (column.widgets == null) column.widgets = [];
|
if (column.widgets == null) column.widgets = [];
|
||||||
column.widgets.unshift(widget);
|
column.widgets.unshift(widget);
|
||||||
|
@ -250,9 +247,9 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeColumnWidget(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 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 == null) return;
|
||||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||||
columns[columnIndex] = column;
|
columns[columnIndex] = column;
|
||||||
|
@ -261,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setColumnWidgets(id: Column['id'], widgets: 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 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 == null) return;
|
||||||
column.widgets = widgets;
|
column.widgets = widgets;
|
||||||
columns[columnIndex] = column;
|
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) {
|
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 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 == null) return;
|
||||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||||
...w,
|
...w,
|
||||||
|
@ -286,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
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 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;
|
if (currentColumn == null) return;
|
||||||
for (const [k, v] of Object.entries(column)) {
|
for (const [k, v] of Object.entries(column)) {
|
||||||
currentColumn[k] = v;
|
currentColumn[k] = v;
|
||||||
|
|
|
@ -11,9 +11,9 @@
|
||||||
<div v-if="disabled" class="iwaalbte">
|
<div v-if="disabled" class="iwaalbte">
|
||||||
<p>
|
<p>
|
||||||
<i class="ph-minus-circle-bold ph-lg"></i>
|
<i class="ph-minus-circle-bold ph-lg"></i>
|
||||||
{{ $t('disabled-timeline.title') }}
|
{{ i18n.t('disabled-timeline.title') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
<p class="desc">{{ i18n.t('disabled-timeline.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
|
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
|
||||||
<div class="calendar" :class="{ isHoliday }">
|
<div class="calendar" :class="{ isHoliday }">
|
||||||
<p class="month-and-year">
|
<p class="month-and-year">
|
||||||
<span class="year">{{ $t('yearX', { year }) }}</span>
|
<span class="year">{{ i18n.t('yearX', { year }) }}</span>
|
||||||
<span class="month">{{ $t('monthX', { month }) }}</span>
|
<span class="month">{{ i18n.t('monthX', { month }) }}</span>
|
||||||
</p>
|
</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-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">{{ $t('dayX', { day }) }}</p>
|
<p v-else class="day">{{ i18n.t('dayX', { day }) }}</p>
|
||||||
<p class="week-day">{{ weekDay }}</p>
|
<p class="week-day">{{ weekDay }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
|
|
@ -47,12 +47,13 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||||
import { GetFormResultType } from '@/scripts/form';
|
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||||
|
import { GetFormResultType } from '@/scripts/form';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
import * as sound from '@/scripts/sound';
|
import * as sound from '@/scripts/sound';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
const name = 'jobQueue';
|
const name = 'jobQueue';
|
||||||
|
|
||||||
|
@ -100,12 +101,12 @@ const prev = reactive({} as typeof current);
|
||||||
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
|
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
|
||||||
|
|
||||||
for (const domain of ['inbox', 'deliver']) {
|
for (const domain of ['inbox', 'deliver']) {
|
||||||
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
|
prev[domain] = deepClone(current[domain]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onStats = (stats) => {
|
const onStats = (stats) => {
|
||||||
for (const domain of ['inbox', 'deliver']) {
|
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].activeSincePrevTick = stats[domain].activeSincePrevTick;
|
||||||
current[domain].active = stats[domain].active;
|
current[domain].active = stats[domain].active;
|
||||||
current[domain].waiting = stats[domain].waiting;
|
current[domain].waiting = stats[domain].waiting;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<p v-if="widgetProps.folderId == null">
|
<p v-if="widgetProps.folderId == null">
|
||||||
{{ i18n.ts.folder }}
|
{{ i18n.ts.folder }}
|
||||||
</p>
|
</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="slideA" class="slide a"></div>
|
||||||
<div ref="slideB" class="slide b"></div>
|
<div ref="slideB" class="slide b"></div>
|
||||||
</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 === '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 === 'list'" class="ph-list-bullets-bold ph-lg"></i>
|
||||||
<i v-else-if="widgetProps.src === 'antenna'" class="ph-television-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>
|
<i :class="menuOpened ? 'ph-caret-up-bold ph-lg' : 'ph-caret-down-bold ph-lg'" style="margin-left: 8px;"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div v-for="stat in stats" :key="stat.tag">
|
<div v-for="stat in stats" :key="stat.tag">
|
||||||
<div class="tag">
|
<div class="tag">
|
||||||
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
|
<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>
|
</div>
|
||||||
<MkMiniChart class="chart" :src="stat.chart"/>
|
<MkMiniChart class="chart" :src="stat.chart"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { reactive, watch } from 'vue';
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
import { Form, GetFormResultType } from '@/scripts/form';
|
import { Form, GetFormResultType } from '@/scripts/form';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
export type Widget<P extends Record<string, unknown>> = {
|
export type Widget<P extends Record<string, unknown>> = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -32,7 +33,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
||||||
save: () => void;
|
save: () => void;
|
||||||
configure: () => 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 = () => {
|
const mergeProps = () => {
|
||||||
for (const prop of Object.keys(propsDef)) {
|
for (const prop of Object.keys(propsDef)) {
|
||||||
|
@ -43,14 +44,14 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
||||||
};
|
};
|
||||||
watch(widgetProps, () => {
|
watch(widgetProps, () => {
|
||||||
mergeProps();
|
mergeProps();
|
||||||
}, { deep: true, immediate: true, });
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
const save = throttle(3000, () => {
|
const save = throttle(3000, () => {
|
||||||
emit('updateProps', widgetProps);
|
emit('updateProps', widgetProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
const configure = async () => {
|
const configure = async () => {
|
||||||
const form = JSON.parse(JSON.stringify(propsDef));
|
const form = deepClone(propsDef);
|
||||||
for (const item of Object.keys(form)) {
|
for (const item of Object.keys(form)) {
|
||||||
form[item].default = widgetProps[item];
|
form[item].default = widgetProps[item];
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
"lint": "eslint --quiet src/**/*.{ts}"
|
"lint": "eslint --quiet src/**/*.{ts}"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.14.54",
|
"esbuild": "^0.15.14",
|
||||||
"idb-keyval": "^6.2.0",
|
"idb-keyval": "^6.2.0",
|
||||||
"misskey-js": "0.0.14"
|
"misskey-js": "0.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.27.0"
|
"eslint": "^8.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue