Merge pull request 'develop' (#9125) from develop into main

Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9125
This commit is contained in:
Kainoa Kanter 2022-11-19 04:20:09 +00:00
commit c124d2cb25
64 changed files with 450 additions and 345 deletions

View File

@ -1,5 +1,5 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
# Calckey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
@ -38,11 +38,11 @@ db:
port: 5432
# Database name
db: misskey
db: calckey
# Auth
user: example-misskey-user
pass: example-misskey-pass
user: example-calckey-user
pass: example-calckey-pass
# Whether disable Caching queries
#disableCache: true
@ -147,7 +147,8 @@ id: 'aid'
# Managed hosting settings
# !!!!!!!!!!
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! YOU DON'T NEED THIS! <<<<<<
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
# >>>>>> YOU DON'T NEED THIS! <<<<<<
# !!!!!!!!!!
# Each category is optional, but if each item in each category is mandatory!
# If you mess this up, that's on you, you've been warned...
@ -181,4 +182,11 @@ id: 'aid'
# connnectOverProxy: false
# setPublicReadOnUpload: true
# s3ForcePathStyle: true
#summalyProxyUrl: 'https://summaly.arkjp.net'
# !!!!!!!!!!
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
# !!!!!!!!!!
# Seriously. Do NOT fill out the above settings if you're self-hosting.
# They're much better off being set from the control panel.

View File

@ -9,7 +9,6 @@
- User "choices" (recommended users) like Mastodon and Soapbox
- Option to publicize instance blocks
- Fully revamp non-logged-in screen
- Remote follow button
- Personal notes for all accounts
- Non-nyaify cat mode
- Timeline filters
@ -21,8 +20,8 @@
## Work in progress
- Better Messaging UI
- Videos can be played in DMs
- Make your password hasn't been pwned
- Remote follow button
- Admin custom CSS
- Add back time machine (jump to date)
- Improve accesibility score
@ -86,6 +85,7 @@
- Link hover effect
- Replace all `$ts` with i18n
- AVIF support
- Page drafts
- Obliteration of Ai-chan
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)

108
README.md
View File

@ -1,15 +1,15 @@
<div align="center">
<a href="https://stop.voring.me/">
<a href="https://i.calckey.cloud/">
<img src="./.github/title_float.svg" alt="Calckey logo" style="border-radius:50%" width="400"/>
</a>
**🌎 **[Calckey](https://stop.voring.me/)** is an open source, decentralized social media platform that's free forever! 🚀**
**🌎 **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! 🚀**
</div>
<div>
<img src="https://pool.jortage.com/voringme/misskey/e7cd2a17-8b23-4e1e-b5cf-709480c623e2.png" align="right" height="320px"/>
<img src="https://pool.jortage.com/voringme/misskey/e7cd2a17-8b23-4e1e-b5cf-709480c623e2.png" align="right" height="320px" alt="Calc (the Calckey mascot) smoking a fat dart"/>
# ✨ About Calckey
@ -33,6 +33,8 @@
# 🥂 Links
- 🚢 Flagship instance: <https://i.calckey.cloud>
- 📣 Official account: <https://i.calckey.cloud/@calckey>
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
- 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com>
- 📜 Instance list: <https://calckey.fediverse.observer/list>
@ -93,89 +95,17 @@ cp -r ../misskey/files . # if you don't use object storage
## 🍀 NGINX
<details>
<summary>Click to see an example NGINX config:</summary>
```nginx
# Replace example.tld with your domain
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name example.tld;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.tld;
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
# To use Let's Encrypt certificate
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL protocol settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_redirect off;
# If it's behind another reverse proxy or CDN, remove the following.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
add_header X-Cache $upstream_cache_status;
}
}
```
- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
- Edit `calckey.nginx.conf` to reflect your instance properly
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
</details>
## 🚀 Build and launch!
### 🐢 NodeJS
#### `git pull` and run these steps to update Calckey in the future!
```sh
@ -195,19 +125,21 @@ docker up -d
### 🐳 Docker Compose
```sh
sudo docker compose build
sudo docker-compose run --rm web yarn run init
sudo docker compose up -d
docker-compose build
docker-compose run --rm web yarn run init
docker-compose up -d
```
## 😉 Tips & Tricks
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
- For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker.
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
- To add another admin account:
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
- Go back to Overview > click the clipboard icon next to the ID
- Run `psql -d calckey` (or whatever the database name is)
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace 999999 with the copied ID)
- Have the new admin log out and log back in
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
- Go back to Overview > click the clipboard icon next to the ID
- Run `psql -d calckey` (or whatever the database name is)
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace `999999` with the copied ID)
- Have the new admin log out and log back in

72
calckey.nginx.conf Normal file
View 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;
}
}

View File

@ -159,7 +159,7 @@ proxyAccount: "Proxy account"
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
host: "Host"
selectUser: "Select a user"
recipient: "Recipient"
recipient: "Recipient(s)"
annotation: "Comments"
federation: "Federation"
instances: "Instances"
@ -770,8 +770,8 @@ noBotProtectionWarning: "Bot protection is not configured."
configure: "Configure"
postToGallery: "Create new gallery post"
gallery: "Gallery"
recentPosts: "Recent posts"
popularPosts: "Popular posts"
recentPosts: "Recent pages"
popularPosts: "Popular pages"
shareWithNote: "Share with note"
ads: "Advertisements"
expiration: "Deadline"
@ -1094,7 +1094,7 @@ _channel:
usersCount: "{n} Participants"
notesCount: "{n} Notes"
_messaging:
dms: "DMs"
dms: "Private"
groups: "Groups"
_menuDisplay:
sideFull: "Side"
@ -1264,10 +1264,10 @@ _permissions:
"read:reactions": "View your reactions"
"write:reactions": "Edit your reactions"
"write:votes": "Vote on a poll"
"read:pages": "View your pages"
"write:pages": "Edit or delete your pages"
"read:page-likes": "View your likes on pages"
"write:page-likes": "Edit your likes on pages"
"read:pages": "View your page"
"write:pages": "Edit or delete your page"
"read:page-likes": "View your likes on page"
"write:page-likes": "Edit your likes on page"
"read:user-groups": "View your user groups"
"write:user-groups": "Edit or delete your user groups"
"read:channels": "View your channels"
@ -1441,7 +1441,7 @@ _pages:
liked: "Liked Pages"
featured: "Popular"
inspector: "Inspector"
contents: "Contents"
contents: "Content"
content: "Page block"
variables: "Variables"
title: "Title"

View File

@ -1,12 +1,12 @@
{
"name": "calckey",
"version": "12.119.0-calc.14",
"version": "12.119.0-calc.15",
"codename": "aqua",
"repository": {
"type": "git",
"url": "https://codeberg.org/thatonecalculator/calckey.git"
},
"packageManager": "yarn@3.2.4",
"packageManager": "yarn@3.3.0",
"workspaces": [
"packages/client",
"packages/backend",
@ -39,10 +39,10 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@bull-board/api": "^4.6.3",
"@bull-board/ui": "^4.6.3",
"@bull-board/api": "^4.6.4",
"@bull-board/ui": "^4.6.4",
"@tensorflow/tfjs": "^3.21.0",
"eslint": "^8.27.0",
"eslint": "^8.28.0",
"execa": "5.1.1",
"gulp": "4.0.2",
"gulp-cssnano": "2.1.3",
@ -55,13 +55,13 @@
"seedrandom": "^3.0.5"
},
"devDependencies": {
"@types/gulp": "4.0.9",
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "5.42.1",
"@typescript-eslint/parser": "5.43.0",
"cross-env": "7.0.3",
"cypress": "10.11.0",
"start-server-and-test": "1.14.0",
"typescript": "4.8.4",
"typescript": "4.9.3",
"vue-eslint-parser": "^9.1.0"
}
}

View 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"`);
}
}

View 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`);
}
}

View File

@ -21,9 +21,9 @@
"@tensorflow/tfjs-node": "3.21.1"
},
"dependencies": {
"@bull-board/api": "^4.6.3",
"@bull-board/koa": "^4.6.3",
"@bull-board/ui": "^4.6.3",
"@bull-board/api": "^4.6.4",
"@bull-board/koa": "^4.6.4",
"@bull-board/ui": "^4.6.4",
"@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3",
@ -32,15 +32,15 @@
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "9.1.2",
"@syuilo/aiscript": "0.11.1",
"ajv": "8.11.0",
"ajv": "8.11.2",
"archiver": "5.3.1",
"autobind-decorator": "2.4.0",
"autwh": "0.1.0",
"aws-sdk": "2.1253.0",
"aws-sdk": "2.1258.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.5",
"bull": "4.10.1",
"cacheable-lookup": "6.1.0",
"cacheable-lookup": "7.0.0",
"cbor": "8.1.0",
"chalk": "5.1.2",
"chalk-template": "0.4.0",
@ -54,10 +54,10 @@
"feed": "4.2.2",
"file-type": "17.1.6",
"fluent-ffmpeg": "2.1.2",
"got": "12.5.2",
"got": "12.5.3",
"hpagent": "0.1.2",
"ioredis": "4.28.5",
"ip-cidr": "3.0.10",
"ip-cidr": "3.0.11",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "20.0.2",
@ -83,7 +83,7 @@
"node-fetch": "3.3.0",
"nodemailer": "6.8.0",
"nsfwjs": "2.4.2",
"oauth": "^0.9.15",
"oauth": "^0.10.0",
"os-utils": "0.0.14",
"parse5": "7.1.1",
"pg": "8.8.0",
@ -111,7 +111,7 @@
"stringz": "2.1.0",
"summaly": "2.7.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.12.14",
"systeminformation": "5.13.5",
"tesseract.js": "^3.0.3",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
@ -130,7 +130,7 @@
"xev": "3.0.2"
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.112",
"@redocly/openapi-core": "1.0.0-beta.114",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.9",
"@types/cbor": "6.0.0",
@ -165,7 +165,7 @@
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2",
"@types/semver": "7.3.13",
"@types/sharp": "0.30.5",
"@types/sharp": "0.31.0",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3",
@ -174,12 +174,12 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.42.1",
"@typescript-eslint/parser": "5.42.1",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"cross-env": "7.0.3",
"eslint": "8.27.0",
"eslint": "8.28.0",
"eslint-plugin-import": "2.26.0",
"execa": "6.1.0",
"typescript": "4.8.4"
"typescript": "4.9.3"
}
}

View 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;
}
}

View File

@ -40,6 +40,9 @@ export class Page {
@Column('boolean')
public alignCenter: boolean;
@Column('boolean')
public isPublic: boolean;
@Column('boolean', {
default: false,
})

View File

@ -9,6 +9,8 @@ import { query, appendQuery } from '@/prelude/url.js';
import { Meta } from '@/models/entities/meta.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, DriveFolders } from '../index.js';
import { deepClone } from '@/misc/clone.js';
type PackOptions = {
detail?: boolean,
@ -29,9 +31,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
// TODO
//const properties = structuredClone(file.properties);
const properties = JSON.parse(JSON.stringify(file.properties));
const properties = deepClone(file.properties);
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}

View File

@ -65,6 +65,7 @@ export const PageRepository = db.getRepository(Page).extend({
content: page.content,
variables: page.variables,
title: page.title,
isPublic: page.isPublic,
name: page.name,
summary: page.summary,
hideTitleWhenPinned: page.hideTitleWhenPinned,

View File

@ -47,5 +47,9 @@ export const packedPageSchema = {
ref: 'UserLite',
optional: false, nullable: false,
},
isPublic: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -53,6 +53,7 @@ export const paramDef = {
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
alignCenter: { type: 'boolean', default: false },
isPublic: { type: 'boolean', default: true },
hideTitleWhenPinned: { type: 'boolean', default: false },
},
required: ['title', 'name', 'content', 'variables', 'script'],
@ -97,6 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font,
isPublic: ps.isPublic,
})).then(x => Pages.findOneByOrFail(x.identifiers[0]));
return await Pages.pack(page);

View File

@ -67,5 +67,9 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchPage);
}
if (!page.isPublic && (user == null || (page.userId !== user.id))) {
throw new ApiError(meta.errors.noSuchPage);
}
return await Pages.pack(page, user);
});

View File

@ -60,6 +60,7 @@ export const paramDef = {
font: { type: 'string', enum: ['serif', 'sans-serif'] },
alignCenter: { type: 'boolean' },
hideTitleWhenPinned: { type: 'boolean' },
isPublic: { type: 'boolean' },
},
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
} as const;
@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
content: ps.content,
variables: ps.variables,
script: ps.script,
isPublic: ps.isPublic,
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
font: ps.font === undefined ? page.font : ps.font,

View File

@ -34,7 +34,8 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
.andWhere('page.userId = :userId', { userId: ps.userId })
.andWhere('page.visibility = \'public\'');
.andWhere('page.visibility = \'public\'')
.andWhere('page.isPublic = true');
const pages = await query
.take(ps.limit)

View File

@ -19,8 +19,8 @@
"blurhash": "1.1.5",
"broadcast-channel": "4.18.1",
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "2.0.0",
"chart.js": "4.0.1",
"chartjs-adapter-date-fns": "2.0.1",
"chartjs-plugin-gradient": "0.5.1",
"chartjs-plugin-zoom": "1.2.1",
"compare-versions": "5.0.1",
@ -31,7 +31,7 @@
"idb-keyval": "6.2.0",
"insert-text-at-cursor": "0.3.0",
"json5": "2.2.1",
"katex": "0.15.6",
"katex": "0.16.3",
"matter-js": "0.18.0",
"mfm-js": "0.23.0",
"misskey-js": "0.0.14",
@ -48,16 +48,16 @@
"swiper": "^8.4.4",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.144.0",
"three": "0.146.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.4.2",
"tsc-alias": "1.7.1",
"tsconfig-paths": "4.1.0",
"twemoji-parser": "14.0.0",
"typescript": "4.8.4",
"typescript": "4.9.3",
"uuid": "9.0.0",
"vanilla-tilt": "1.7.3",
"vite": "^3.2.3",
"vite": "^3.2.4",
"vue": "3.2.45",
"vue-isyourpasswordsafe": "^2.0.0",
"vue-plyr": "^7.0.0",
@ -67,7 +67,7 @@
"devDependencies": {
"@types/escape-regexp": "0.0.1",
"@types/glob": "8.0.0",
"@types/gulp": "4.0.9",
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/katex": "0.14.0",
"@types/matter-js": "0.18.2",
@ -76,11 +76,11 @@
"@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.42.1",
"@typescript-eslint/parser": "5.42.1",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"cross-env": "7.0.3",
"cypress": "10.11.0",
"eslint": "8.27.0",
"eslint": "8.28.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.7.0",
"rollup": "2.79.1",

View File

@ -33,7 +33,7 @@
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
</div>
<div v-else>
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.yes }}</MkButton>
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ i18n.ts.yes }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
</div>
</div>

View File

@ -1,5 +1,6 @@
<template>
<button class="kpoogebi _button"
<button
class="kpoogebi _button"
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
:disabled="wait"
@click="onClick"
@ -8,7 +9,8 @@
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 -->
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse"></i>
</template>
<template v-else-if="isFollowing">
@ -29,16 +31,16 @@
<script lang="ts" setup>
import { onBeforeUnmount, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import type * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed,
full?: boolean,
large?: boolean,
}>(), {
user: Misskey.entities.UserDetailed,
full?: boolean,
large?: boolean,
}>(), {
full: false,
large: false,
});
@ -50,9 +52,9 @@ const connection = stream.useChannel('main');
if (props.user.isFollowing == null) {
os.api('users/show', {
userId: props.user.id
userId: props.user.id,
})
.then(onFollowChange);
.then(onFollowChange);
}
function onFollowChange(user: Misskey.entities.UserDetailed) {
@ -75,17 +77,17 @@ async function onClick() {
if (canceled) return;
await os.api('following/delete', {
userId: props.user.id
userId: props.user.id,
});
} else {
if (hasPendingFollowRequestFromYou) {
await os.api('following/requests/cancel', {
userId: props.user.id
userId: props.user.id,
});
hasPendingFollowRequestFromYou = false;
} else {
await os.api('following/create', {
userId: props.user.id
userId: props.user.id,
});
hasPendingFollowRequestFromYou = true;
}

View File

@ -49,7 +49,7 @@
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/>
<div v-else class="translated">
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
</div>
</div>
@ -104,9 +104,10 @@
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue';
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import type { Ref } from 'vue';
import type * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import XNoteHeader from '@/components/MkNoteHeader.vue';
import XNoteSimple from '@/components/MkNoteSimple.vue';
@ -134,6 +135,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { notePage } from '@/filters/note';
import { deepClone } from '@/scripts/clone';
const router = useRouter();
@ -144,12 +146,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null);
let note = $ref(JSON.parse(JSON.stringify(props.note)));
let note = $ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result = JSON.parse(JSON.stringify(note));
let result = deepClone(note);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
@ -432,7 +434,9 @@ function readPromo() {
width: 58px;
height: 58px;
position: sticky;
top: var(--stickyTop, 0px);
/* For some reason this breaks avatar
positions on notes, commenting it for now */
/* top: var(--stickyTop, 0px); */
left: 0;
}

View File

@ -60,7 +60,7 @@
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/>
<div v-else class="translated">
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
</div>
</div>
@ -143,6 +143,7 @@ import { $i } from '@/account';
import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
const router = useRouter();
@ -153,12 +154,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null);
let note = $ref(JSON.parse(JSON.stringify(props.note)));
let note = $ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result = JSON.parse(JSON.stringify(note));
let result = deepClone(note);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}

View File

@ -5,7 +5,7 @@
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
<div class="sub-icon" :class="notification.type">
<i v-if="notification.type === 'follow'" class="ph-plus-bold"></i>
<i v-if="notification.type === 'follow'" class="ph-hand-waving-bold"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock-bold"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check-bold"></i>
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i>

View File

@ -6,12 +6,12 @@
<span>
<template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template>
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
<span v-if="showResult" class="votes">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
</span>
</li>
</ul>
<p v-if="!readOnly">
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
<span> · </span>
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>

View File

@ -5,7 +5,7 @@
</p>
<ul>
<li v-for="(choice, i) in choices" :key="i">
<MkInput class="input" small :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput>
<button class="_button" @click="remove(i)">
<i class="ph-x-bold ph-lg"></i>

View File

@ -36,7 +36,7 @@
<MkAcct :user="u"/>
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
</span>
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ph-plus-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button" @click="addVisibleUser"><i class="ph-plus-bold ph-md ph-fw ph-lg"></i></button>
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
@ -89,6 +89,7 @@ import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
import { uploadFile } from '@/scripts/upload';
import { deepClone } from '@/scripts/clone';
const modal = inject('modal');
@ -458,7 +459,7 @@ async function onPaste(ev: ClipboardEvent) {
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
ev.preventDefault();
os.confirm({
os.yesno({
type: 'info',
text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => {
@ -575,7 +576,7 @@ async function post() {
// plugin
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
postData = await interruptor.handler(JSON.parse(JSON.stringify(postData)));
postData = await interruptor.handler(deepClone(postData));
}
}
@ -832,7 +833,7 @@ onMounted(() => {
padding: 6px 24px;
margin-bottom: 8px;
overflow: auto;
white-space: nowrap;
line-height: 2rem;
> .visibleUsers {
display: inline;
@ -840,15 +841,19 @@ onMounted(() => {
font-size: 14px;
> button {
padding: 4px;
padding: 2px;
border-radius: 8px;
> i {
transform: translateX(2px);
}
}
> span {
margin-right: 14px;
padding: 8px 0 8px 8px;
border-radius: 8px;
background: var(--X4);
margin: 0.3rem;
padding: 4px 0 4px 4px;
border-radius: 999px;
background: var(--X3);
> button {
padding: 4px 8px;

View File

@ -41,9 +41,9 @@
</div>
</div>
<div class="social _section">
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
</div>
</form>
</template>
@ -51,6 +51,7 @@
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue';
import MkInfo from '@/components/MkInfo.vue';
@ -58,7 +59,6 @@ import { apiUrl, host as configHost } from '@/config';
import { byteify, hexify } from '@/scripts/2fa';
import * as os from '@/os';
import { login } from '@/account';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
@ -85,7 +85,7 @@ const props = defineProps({
withAvatar: {
type: Boolean,
required: false,
default: true
default: true,
},
autoSet: {
type: Boolean,
@ -95,13 +95,13 @@ const props = defineProps({
message: {
type: String,
required: false,
default: ''
}
default: '',
},
});
function onUsernameChange() {
os.api('users/show', {
username: username
username: username,
}).then(userResponse => {
user = userResponse;
}, () => {
@ -123,10 +123,10 @@ function queryKey() {
allowCredentials: challengeData.securityKeys.map(key => ({
id: byteify(key.id, 'hex'),
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal']
transports: ['usb', 'nfc', 'ble', 'internal'],
})),
timeout: 60 * 1000
}
timeout: 60 * 1000,
},
}).catch(() => {
queryingKey = false;
return Promise.reject(null);
@ -141,7 +141,7 @@ function queryKey() {
clientDataJSON: hexify(credential.response.clientDataJSON),
credentialId: credential.id,
challengeId: challengeData.challengeId,
'hcaptcha-response': hCaptchaResponse,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
});
}).then(res => {
@ -151,7 +151,7 @@ function queryKey() {
if (err === null) return;
os.alert({
type: 'error',
text: i18n.ts.signinFailed
text: i18n.ts.signinFailed,
});
signing = false;
});
@ -165,8 +165,8 @@ function onSubmit() {
os.api('signin', {
username,
password,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
}).then(res => {
totpLogin = true;
signing = false;
@ -181,9 +181,9 @@ function onSubmit() {
os.api('signin', {
username,
password,
'hcaptcha-response': hCaptchaResponse,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
token: user && user.twoFactorEnabled ? token : undefined
token: user && user.twoFactorEnabled ? token : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
@ -197,7 +197,7 @@ function loginFailed(err) {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.noSuchUser
text: i18n.ts.noSuchUser,
});
break;
}
@ -226,7 +226,7 @@ function loginFailed(err) {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: JSON.stringify(err)
text: JSON.stringify(err),
});
}
}

View File

@ -7,7 +7,7 @@
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<div v-if="note.files.length > 0">
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
<XMediaList :media-list="note.files"/>
</div>
<div v-if="note.poll">

View File

@ -23,7 +23,7 @@
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
</div>
</XModalWindow>
</template>

View File

@ -22,7 +22,7 @@ import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton
MkButton,
},
props: {
@ -60,7 +60,7 @@ export default defineComponent({
watch(() => props.p, () => {
process();
}, {
immediate: true
immediate: true,
});
const retry = () => {
@ -73,6 +73,7 @@ export default defineComponent({
rejected,
result,
retry,
i18n,
};
}
});

View File

@ -8,7 +8,7 @@
<i v-if="relay.status === 'accepted'" class="ph-check-bold ph-lg icon accepted"></i>
<i v-else-if="relay.status === 'rejected'" class="ph-prohibit-bold ph-lg icon rejected"></i>
<i v-else class="ph-clock-bold ph-lg icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
<span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
</div>

View File

@ -1,6 +1,6 @@
<template>
<section class="_section">
<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
<div class="_title">{{ i18n.t('_auth.shareAccess', { name: app.name }) }}</div>
<div class="_content">
<h2>{{ app.name }}</h2>
<p class="id">{{ app.id }}</p>
@ -9,7 +9,7 @@
<div class="_content">
<h2>{{ i18n.ts._auth.permissionAsk }}</h2>
<ul>
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
<li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul>
</div>
<div class="_footer">

View File

@ -15,7 +15,7 @@
<h1>{{ i18n.ts._auth.denied }}</h1>
</div>
<div v-if="state == 'accepted'" class="accepted">
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : i18n.ts.allowed }}</h1>
<h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
</div>
@ -47,6 +47,7 @@ export default defineComponent({
state: null,
session: null,
fetching: true,
i18n,
};
},
mounted() {

View File

@ -202,11 +202,11 @@ definePageMetadata(computed(() => post ? {
> .like {
> .button {
--accent: rgb(241 97 132);
--X8: rgb(241 92 128);
--accent: #eb6f92;
--X8: #eb6f92;
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f;
color: #eb6f92;
::v-deep(.count) {
margin-left: 0.5em;

View File

@ -1,6 +1,6 @@
<template>
<div v-size="{ max: [400, 500] }" class="thvuemwp" :class="{ isMe }">
<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/>
<MkAvatar v-if="!isMe" class="avatar" :user="message.user" :show-indicator="true"/>
<div class="content">
<div class="balloon" :class="{ noText: message.text == null }">
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
@ -38,7 +38,6 @@
<script lang="ts" setup>
import { } from 'vue';
import * as mfm from 'mfm-js';
import VuePlyr from 'vue-plyr';
import type * as Misskey from 'misskey-js';
import XMediaList from '@/components/MkMediaList.vue';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
@ -73,10 +72,10 @@ function del(): void {
> .avatar {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
top: calc(var(--stickyTop, 0px) + 20px);
display: block;
width: 54px;
height: 54px;
width: 45px;
height: 45px;
transition: all 0.1s ease;
}
@ -91,14 +90,7 @@ function del(): void {
min-height: 38px;
border-radius: 16px;
max-width: 100%;
&:before {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 12px;
}
margin-left: 10rem;
& + * {
clear: both;
@ -222,7 +214,9 @@ function del(): void {
padding-right: 32px;
> .balloon {
$color: var(--messageBg);
$color: var(--X4);
margin-right: 10rem;
margin-left: 0rem !important;
background: $color;
&.noText {

View File

@ -18,12 +18,12 @@
</div>
</div>
<div v-else class="_section">
<div v-if="name" class="_title">{{ $t('_auth.shareAccess', { name: name }) }}</div>
<div v-if="name" class="_title">{{ i18n.t('_auth.shareAccess', { name: name }) }}</div>
<div v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div>
<div class="_content">
<p>{{ i18n.ts._auth.permissionAsk }}</p>
<ul>
<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li>
<li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul>
</div>
<div class="_footer">

View File

@ -21,7 +21,7 @@
<MkInput v-model="value.message"><template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
<MkSelect v-model="value.var">
<template #label>{{ i18n.ts._pages.blocks._button._action._pushEvent.variable }}</template>
<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
<option :value="null">{{ i18n.t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
<optgroup :label="i18n.ts._pages.script.pageVariables">
<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>

View File

@ -16,8 +16,8 @@
</button>
</div>
</header>
<p v-show="showBody" v-if="error != null" class="error">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
<p v-show="showBody" v-if="warn != null" class="warn">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
<p v-show="showBody" v-if="error != null" class="error">{{ i18n.t('_pages.script.typeError', { slot: error.arg + 1, expect: i18n.t(`script.types.${error.expect}`), actual: i18n.t(`script.types.${error.actual}`) }) }}</p>
<p v-show="showBody" v-if="warn != null" class="warn">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
<div v-show="showBody" class="body">
<slot></slot>
</div>
@ -26,34 +26,36 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { i18n } from '@/i18n';
export default defineComponent({
props: {
expanded: {
type: Boolean,
default: true
default: true,
},
removable: {
type: Boolean,
default: true
default: true,
},
draggable: {
type: Boolean,
default: false
default: false,
},
error: {
required: false,
default: null
default: null,
},
warn: {
required: false,
default: null
}
default: null,
},
},
emits: ['toggle', 'remove'],
data() {
return {
showBody: this.expanded,
i18n,
};
},
methods: {
@ -63,8 +65,8 @@ export default defineComponent({
},
remove() {
this.$emit('remove');
}
}
},
},
});
</script>

View File

@ -43,15 +43,15 @@
<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
<MkTextarea v-model="slots">
<template #label>{{ i18n.ts._pages.script.blocks._fn.slots }}</template>
<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
<template #caption>{{ i18n.t('_pages.script.blocks._fn.slots-info') }}</template>
</MkTextarea>
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="i18n.t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
</section>
<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
</section>
<section v-else class="" style="padding:16px;">
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="i18n.t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>
</section>
</XContainer>
</template>
@ -124,7 +124,7 @@ export default defineComponent({
typeText(): any {
if (this.modelValue.type === null) return null;
if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
return this.$t(`_pages.script.blocks.${this.modelValue.type}`);
return i18n.t(`_pages.script.blocks.${this.modelValue.type}`);
},
},

View File

@ -24,6 +24,7 @@
<template #label>{{ i18n.ts._pages.url }}</template>
</MkInput>
<MkSwitch v-model="isPublic" class="_formBlock">{{ i18n.ts.public }}</MkSwitch>
<MkSwitch v-model="alignCenter" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
<MkSelect v-model="font" class="_formBlock">
@ -47,7 +48,6 @@
<div v-else-if="tab === 'contents'">
<div>
<XBlocks v-model="content" class="content" :hpml="hpml"/>
<MkButton v-if="!readonly" @click="add()"><i class="ph-plus-bold ph-lg"></i></MkButton>
</div>
</div>
@ -130,6 +130,7 @@ let eyeCatchingImageId = $ref(null);
let font = $ref('sans-serif');
let content = $ref([]);
let alignCenter = $ref(false);
let isPublic = $ref(true);
let hideTitleWhenPinned = $ref(false);
let variables = $ref([]);
let hpml = $ref(null);
@ -158,6 +159,7 @@ function getSaveOptions() {
script: script,
hideTitleWhenPinned: hideTitleWhenPinned,
alignCenter: alignCenter,
isPublic: isPublic,
content: content,
variables: variables,
eyeCatchingImageId: eyeCatchingImageId,
@ -393,6 +395,7 @@ async function init() {
script = page.script;
hideTitleWhenPinned = page.hideTitleWhenPinned;
alignCenter = page.alignCenter;
isPublic = page.isPublic;
content = page.content;
variables = page.variables;
eyeCatchingImageId = page.eyeCatchingImageId;
@ -401,7 +404,7 @@ async function init() {
content = [{
id,
type: 'text',
text: 'Hello World!',
text: '',
}];
}
}
@ -439,7 +442,7 @@ definePageMetadata(computed(() => {
return {
title: title,
icon: 'ph-pencil-bold ph-lg',
};
};
}));
</script>
@ -447,7 +450,7 @@ definePageMetadata(computed(() => {
.jqqmcavi {
> .button {
& + .button {
margin-left: 8px;
margin: 4px;
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<MkSpacer :content-max="800">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
<div class="_block main">
@ -25,14 +25,14 @@
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
</div>
</div>
<div class="user">
<MkAvatar :user="page.user" class="avatar"/>
<div class="name">
<MkUserName :user="page.user" style="display: block;"/>
<MkAcct :user="page.user"/>
<div class="user">
<MkAvatar :user="page.user" class="avatar"/>
<div class="name">
<MkUserName :user="page.user" style="display: block;"/>
<MkAcct :user="page.user"/>
</div>
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
<div class="links">
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
@ -176,6 +176,10 @@ definePageMetadata(computed(() => page ? {
.xcukqgmh {
> .main {
> * {
margin: 1rem;
}
> .header {
padding: 16px;
@ -185,6 +189,8 @@ definePageMetadata(computed(() => page ? {
}
> .banner {
margin: 0rem !important;
> img {
// TODO:
display: block;
@ -195,7 +201,6 @@ definePageMetadata(computed(() => page ? {
}
> .content {
margin: 1rem;
padding: 16px 0 0 0;
}
@ -208,11 +213,11 @@ definePageMetadata(computed(() => page ? {
> .like {
> .button {
--accent: rgb(241 97 132);
--X8: rgb(241 92 128);
--accent: #eb6f92;
--X8: #eb6f92;
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f;
color: #eb6f92;
::v-deep(.count) {
margin-left: 0.5em;
@ -221,8 +226,6 @@ definePageMetadata(computed(() => page ? {
}
> .other {
margin-left: auto;
> button {
padding: 8px;
margin: 0 8px;
@ -232,37 +235,36 @@ definePageMetadata(computed(() => page ? {
}
}
}
}
> .user {
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex;
align-items: center;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
> .user {
margin-left: auto;
display: flex;
align-items: center;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto;
margin: 1rem;
}
}
}
> .links {
margin-top: 16px;
padding: 24px 0 0 0;
padding: 14px 0;
border-top: solid 0.5px var(--divider);
> .link {
margin-right: 0.75em;
margin-right: 2em;
}
}
}

View File

@ -27,7 +27,7 @@
<details>
<summary>{{ i18n.ts.details }}</summary>
<ul>
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul>
</details>
</div>

View File

@ -66,8 +66,9 @@ import * as os from '@/os';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { deepClone } from '@/scripts/clone';
let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions)));
let reactions = $ref(deepClone(defaultStore.state.reactions));
const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
@ -101,7 +102,7 @@ async function setDefault() {
});
if (canceled) return;
reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
reactions = deepClone(defaultStore.def.reactions.default);
}
function chooseEmoji(ev: MouseEvent) {

View File

@ -7,7 +7,7 @@
<FormSection>
<template #label>{{ i18n.ts.sounds }}</template>
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
{{ $t('_sfx.' + type) }}
{{ i18n.t('_sfx.' + type) }}
<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
<template #suffixIcon><i class="ph-caret-down-bold ph-lg"></i></template>
</FormLink>

View File

@ -91,13 +91,14 @@ import FormRange from '@/components/form/range.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { deepClone } from '@/scripts/clone';
const props = defineProps<{
_id: string;
userLists: any[] | null;
}>();
const statusbar = reactive(JSON.parse(JSON.stringify(defaultStore.state.statusbars.find(x => x.id === props._id))));
const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
watch(() => statusbar.type, () => {
if (statusbar.type === 'rss') {
@ -128,8 +129,8 @@ watch(statusbar, save);
async function save() {
const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
statusbars[i] = JSON.parse(JSON.stringify(statusbar));
const statusbars = deepClone(defaultStore.state.statusbars);
statusbars[i] = deepClone(statusbar);
defaultStore.set('statusbars', statusbars);
}

View File

@ -86,9 +86,7 @@ if (defaultStore.reactiveState.tutorial.value !== -1) {
const isLocalTimelineAvailable =
!instance.disableLocalTimeline ||
($i != null && ($i.isModerator || $i.isAdmin));
const isRecommendedTimelineAvailable =
!instance.disableRecommendedTimeline ||
($i != null && ($i.isModerator || $i.isAdmin));
const isRecommendedTimelineAvailable = !instance.disableRecommendedTimeline;
const isGlobalTimelineAvailable =
!instance.disableGlobalTimeline ||
($i != null && ($i.isModerator || $i.isAdmin));

View File

@ -24,9 +24,10 @@
</div>
</div>
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
<div v-if="$i" class="actions">
<div class="actions">
<button class="menu _button" @click="menu"><i class="ph-dots-three-outline-bold ph-lg"></i></button>
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
<MkFollowButton v-if="$i != null && $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
<MkFollowButton v-else :user="user" :remote="true" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
</div>
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
@ -51,7 +52,7 @@
</dl>
<dl v-if="user.birthday" class="field">
<dt class="name"><i class="ph-cake-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.birthday }}</dt>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.t('yearsOld', { age }) }})</dd>
</dl>
<dl class="field">
<dt class="name"><i class="ph-calendar-blank-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.registeredDate }}</dt>

View File

@ -83,26 +83,26 @@ const headerTabs = $computed(() =>
{
key: 'home',
title: i18n.ts.overview,
icon: 'ph-user-bold ph-large',
icon: 'ph-user-bold ph-lg',
},
...(($i && $i.id === user.id) || user.publicReactions
? [{
key: 'reactions',
title: i18n.ts.reaction,
icon: 'ph-smiley-bold ph-large',
icon: 'ph-smiley-bold ph-lg',
}] : []),
...(user.instance == null ? [{
key: 'clips',
title: i18n.ts.clips,
icon: 'ph-paperclip-bold ph-large',
icon: 'ph-paperclip-bold ph-lg',
}, {
key: 'pages',
title: i18n.ts.pages,
icon: 'ph-file-text-bold ph-large',
icon: 'ph-file-text-bold ph-lg',
}, {
key: 'gallery',
title: i18n.ts.gallery,
icon: 'ph-image-square-bold ph-large',
icon: 'ph-image-square-bold ph-lg',
}] : []),
]
: null,

View File

@ -105,7 +105,7 @@ export default defineComponent({
showMenu(ev) {
os.popupMenu([{
text: this.$t('aboutX', { x: instanceName }),
text: i18n.t('aboutX', { x: instanceName }),
icon: 'ph-info-bold ph-lg',
action: () => {
os.pageWindow('/about');

View File

@ -125,7 +125,7 @@ export default defineComponent({
showMenu(ev) {
os.popupMenu([{
text: this.$t('aboutX', { x: instanceName }),
text: i18n.t('aboutX', { x: instanceName }),
icon: 'ph-info-bold ph-lg',
action: () => {
os.pageWindow('/about');

View 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;
}
}

View File

@ -13,6 +13,7 @@ export type Theme = {
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
import { deepClone } from './clone';
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@ -63,7 +64,7 @@ export function applyTheme(theme: Theme, persist = true) {
const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));
const _theme = deepClone(theme);
if (_theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);

View File

@ -38,7 +38,7 @@
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="ph-trash-bold ph-lg"></i></button>
</div>
<div class="middle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="ph-plus-bold ph-lg"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button new" @click="addColumn"><i class="ph-plus-bold ph-lg"></i></button>
</div>
<div class="bottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="ph-gear-six-bold ph-lg"></i></button>
@ -322,7 +322,7 @@ async function deleteProfile() {
display: flex;
flex-direction: column;
justify-content: center;
width: 32px;
width: 44px;
> .top, > .middle, > .bottom {
> .button {
@ -339,6 +339,11 @@ async function deleteProfile() {
> .middle {
margin-top: auto;
margin-bottom: auto;
> .new {
font-size: 20px;
background-color: var(--accentedBg);
}
}
> .bottom {

View File

@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
import { Storage } from '../../pizzax';
import { i18n } from '@/i18n';
import { api } from '@/os';
import { deepClone } from '@/scripts/clone';
type ColumnWidget = {
name: string;
@ -25,10 +26,6 @@ export type Column = {
tl?: 'home' | 'local' | 'social' | 'global';
};
function copy<T>(x: T): T {
return JSON.parse(JSON.stringify(x));
}
export const deckStore = markRaw(new Storage('deck', {
profile: {
where: 'deviceAccount',
@ -128,7 +125,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
const layout = copy(deckStore.state.layout);
const layout = deepClone(deckStore.state.layout);
layout[aX][aY] = b;
layout[bX][bY] = a;
deckStore.set('layout', layout);
@ -136,7 +133,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
}
export function swapLeftColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
const layout = deepClone(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) {
const left = deckStore.state.layout[i - 1];
@ -152,7 +149,7 @@ export function swapLeftColumn(id: Column['id']) {
}
export function swapRightColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
const layout = deepClone(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) {
const right = deckStore.state.layout[i + 1];
@ -168,9 +165,9 @@ export function swapRightColumn(id: Column['id']) {
}
export function swapUpColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
const layout = deepClone(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
const ids = copy(deckStore.state.layout[idsIndex]);
const ids = deepClone(deckStore.state.layout[idsIndex]);
ids.some((x, i) => {
if (x === id) {
const up = ids[i - 1];
@ -188,9 +185,9 @@ export function swapUpColumn(id: Column['id']) {
}
export function swapDownColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
const layout = deepClone(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
const ids = copy(deckStore.state.layout[idsIndex]);
const ids = deepClone(deckStore.state.layout[idsIndex]);
ids.some((x, i) => {
if (x === id) {
const down = ids[i + 1];
@ -208,7 +205,7 @@ export function swapDownColumn(id: Column['id']) {
}
export function stackLeftColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout);
let layout = deepClone(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
layout = layout.map(ids => ids.filter(_id => _id !== id));
layout[i - 1].push(id);
@ -218,7 +215,7 @@ export function stackLeftColumn(id: Column['id']) {
}
export function popRightColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout);
let layout = deepClone(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
const affected = layout[i];
layout = layout.map(ids => ids.filter(_id => _id !== id));
@ -226,7 +223,7 @@ export function popRightColumn(id: Column['id']) {
layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout);
const columns = copy(deckStore.state.columns);
const columns = deepClone(deckStore.state.columns);
for (const column of columns) {
if (affected.includes(column.id)) {
column.active = true;
@ -238,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
}
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = copy(deckStore.state.columns);
const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return;
if (column.widgets == null) column.widgets = [];
column.widgets.unshift(widget);
@ -250,9 +247,9 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
}
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = copy(deckStore.state.columns);
const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column;
@ -261,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
}
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
const columns = copy(deckStore.state.columns);
const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = widgets;
columns[columnIndex] = column;
@ -272,9 +269,9 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
}
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
const columns = copy(deckStore.state.columns);
const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = column.widgets.map(w => w.id === widgetId ? {
...w,
@ -286,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
}
export function updateColumn(id: Column['id'], column: Partial<Column>) {
const columns = copy(deckStore.state.columns);
const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const currentColumn = copy(deckStore.state.columns[columnIndex]);
const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
if (currentColumn == null) return;
for (const [k, v] of Object.entries(column)) {
currentColumn[k] = v;

View File

@ -11,9 +11,9 @@
<div v-if="disabled" class="iwaalbte">
<p>
<i class="ph-minus-circle-bold ph-lg"></i>
{{ $t('disabled-timeline.title') }}
{{ i18n.t('disabled-timeline.title') }}
</p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
<p class="desc">{{ i18n.t('disabled-timeline.description') }}</p>
</div>
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
</XColumn>

View File

@ -2,11 +2,11 @@
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
<div class="calendar" :class="{ isHoliday }">
<p class="month-and-year">
<span class="year">{{ $t('yearX', { year }) }}</span>
<span class="month">{{ $t('monthX', { month }) }}</span>
<span class="year">{{ i18n.t('yearX', { year }) }}</span>
<span class="month">{{ i18n.t('monthX', { month }) }}</span>
</p>
<p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
<p v-else class="day">{{ $t('dayX', { day }) }}</p>
<p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
<p v-else class="day">{{ i18n.t('dayX', { day }) }}</p>
<p class="week-day">{{ weekDay }}</p>
</div>
<div class="info">

View File

@ -47,12 +47,13 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { GetFormResultType } from '@/scripts/form';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { GetFormResultType } from '@/scripts/form';
import { stream } from '@/stream';
import number from '@/filters/number';
import * as sound from '@/scripts/sound';
import * as os from '@/os';
import { deepClone } from '@/scripts/clone';
const name = 'jobQueue';
@ -100,12 +101,12 @@ const prev = reactive({} as typeof current);
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
for (const domain of ['inbox', 'deliver']) {
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
prev[domain] = deepClone(current[domain]);
}
const onStats = (stats) => {
for (const domain of ['inbox', 'deliver']) {
prev[domain] = JSON.parse(JSON.stringify(current[domain]));
prev[domain] = deepClone(current[domain]);
current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
current[domain].active = stats[domain].active;
current[domain].waiting = stats[domain].waiting;

View File

@ -4,7 +4,7 @@
<p v-if="widgetProps.folderId == null">
{{ i18n.ts.folder }}
</p>
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p>
<div ref="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div>
</div>

View File

@ -8,7 +8,7 @@
<i v-else-if="widgetProps.src === 'global'" class="ph-planet-bold ph-lg"></i>
<i v-else-if="widgetProps.src === 'list'" class="ph-list-bullets-bold ph-lg"></i>
<i v-else-if="widgetProps.src === 'antenna'" class="ph-television-bold ph-lg"></i>
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span>
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span>
<i :class="menuOpened ? 'ph-caret-up-bold ph-lg' : 'ph-caret-down-bold ph-lg'" style="margin-left: 8px;"></i>
</button>
</template>

View File

@ -8,7 +8,7 @@
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
<p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p>
</div>
<MkMiniChart class="chart" :src="stat.chart"/>
</div>

View File

@ -2,6 +2,7 @@ import { reactive, watch } from 'vue';
import { throttle } from 'throttle-debounce';
import { Form, GetFormResultType } from '@/scripts/form';
import * as os from '@/os';
import { deepClone } from '@/scripts/clone';
export type Widget<P extends Record<string, unknown>> = {
id: string;
@ -32,7 +33,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
save: () => void;
configure: () => void;
} => {
const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {});
const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {});
const mergeProps = () => {
for (const prop of Object.keys(propsDef)) {
@ -43,14 +44,14 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
};
watch(widgetProps, () => {
mergeProps();
}, { deep: true, immediate: true, });
}, { deep: true, immediate: true });
const save = throttle(3000, () => {
emit('updateProps', widgetProps);
});
const configure = async () => {
const form = JSON.parse(JSON.stringify(propsDef));
const form = deepClone(propsDef);
for (const item of Object.keys(form)) {
form[item].default = widgetProps[item];
}

View File

@ -7,11 +7,11 @@
"lint": "eslint --quiet src/**/*.{ts}"
},
"dependencies": {
"esbuild": "^0.14.54",
"esbuild": "^0.15.14",
"idb-keyval": "^6.2.0",
"misskey-js": "0.0.14"
},
"devDependencies": {
"eslint": "^8.27.0"
"eslint": "^8.28.0"
}
}