mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-29 05:17:39 -07:00
Merge pull request 'develop' (#9178) from develop into account_migration
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9178
This commit is contained in:
commit
29c1b81f12
77 changed files with 21821 additions and 505 deletions
|
@ -1,4 +1,4 @@
|
||||||
# db settings
|
# db settings
|
||||||
POSTGRES_PASSWORD=example-misskey-pass
|
POSTGRES_PASSWORD=example-calckey-pass
|
||||||
POSTGRES_USER=example-misskey-user
|
POSTGRES_USER=example-calckey-user
|
||||||
POSTGRES_DB=misskey
|
POSTGRES_DB=calckey
|
||||||
|
|
10
CALCKEY.md
10
CALCKEY.md
|
@ -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
|
- Better API Documentation
|
||||||
- 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,7 +85,12 @@
|
||||||
- Link hover effect
|
- Link hover effect
|
||||||
- Replace all `$ts` with i18n
|
- Replace all `$ts` with i18n
|
||||||
- AVIF support
|
- AVIF support
|
||||||
|
- Page drafts
|
||||||
|
- Patron list
|
||||||
|
- Animations respect reduced motion
|
||||||
- Obliteration of Ai-chan
|
- Obliteration of Ai-chan
|
||||||
|
- Undo renote button inside original note
|
||||||
|
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
||||||
- [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)
|
||||||
- [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021)
|
- [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021)
|
||||||
|
|
54
README.md
54
README.md
|
@ -1,9 +1,9 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
||||||
- Improved UI/UX (especially on mobile)
|
- Improved UI/UX (especially on mobile)
|
||||||
- Improved notifications
|
- Improved notifications
|
||||||
- Improved instance security
|
- Improved instance security
|
||||||
|
- Improved accessibility
|
||||||
- Recommended Instances timeline
|
- Recommended Instances timeline
|
||||||
- OCR image captioning
|
- OCR image captioning
|
||||||
- New and improved Groups
|
- New and improved Groups
|
||||||
|
@ -34,6 +35,9 @@
|
||||||
# 🥂 Links
|
# 🥂 Links
|
||||||
|
|
||||||
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
|
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
|
||||||
|
- Donate publicly to get your name on the Patron list!
|
||||||
|
- 🚢 Flagship instance: <https://i.calckey.cloud>
|
||||||
|
- 📣 Official account: <https://i.calckey.cloud/@calckey>
|
||||||
- 💁 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>
|
||||||
- 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F>
|
- 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F>
|
||||||
|
@ -45,15 +49,27 @@ This guide will work for both **starting from scratch** and **migrating from Mis
|
||||||
|
|
||||||
## 📦 Dependencies
|
## 📦 Dependencies
|
||||||
|
|
||||||
- At least 🐢 [NodeJS](https://nodejs.org/en/) v16.15.0 (v18.12.1 recommended)
|
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19.1.0 recommended)
|
||||||
|
- Install with [nvm](https://github.com/nvm-sh/nvm)
|
||||||
> ⚠️ NodeJS v19 is not supported as of right now because of [this issue](https://github.com/nodejs/node-gyp/issues/2757).
|
|
||||||
|
|
||||||
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
|
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
|
||||||
|
|
||||||
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
|
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
|
||||||
|
|
||||||
- 🛰️ (Optional, for non-Docker) [pm2](https://pm2.io/)
|
### 😗 Optional dependencies
|
||||||
|
|
||||||
|
- 📗 [FFmpeg](https://ffmpeg.org/) for video transcoding
|
||||||
|
- 🔍 [ElasticSearch](https://www.elastic.co/elasticsearch/) for full-text search
|
||||||
|
- OpenSearch/Sonic are not supported as of right now
|
||||||
|
- 🥡 Management (choose one of the following)
|
||||||
|
- 🛰️ [pm2](https://pm2.io/)
|
||||||
|
- 🐳 [Docker](https://docker.com)
|
||||||
|
- 📐 Service manager (systemd, openrc, etc)
|
||||||
|
|
||||||
|
### 🏗️ Build dependencies
|
||||||
|
|
||||||
|
- 🦬 C/C++ compiler & build tools
|
||||||
|
- `build-essential` on Debian/Ubuntu Linux
|
||||||
|
- `base-devel` on Arch Linux
|
||||||
|
- 🐍 [Python 3](https://www.python.org/)
|
||||||
|
|
||||||
## 👀 Get folder ready
|
## 👀 Get folder ready
|
||||||
|
|
||||||
|
@ -70,10 +86,19 @@ cd calckey/
|
||||||
corepack enable
|
corepack enable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🐘 Create database
|
||||||
|
|
||||||
|
Assuming you set up PostgreSQL correctly, all you have to run is:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
psql postgres -c "create database calckey with encoding = 'UTF8';"
|
||||||
|
```
|
||||||
|
|
||||||
## 💅 Customize
|
## 💅 Customize
|
||||||
|
|
||||||
- To add custom CSS for all users, edit `./custom/instance.css`.
|
- To add custom CSS for all users, edit `./custom/assets/instance.css`.
|
||||||
- To add static assets (such as images for the splash screen), place them in the `./custom/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`.
|
- To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`.
|
||||||
|
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
|
||||||
- To update custom assets without rebuilding, just run `yarn run gulp`.
|
- To update custom assets without rebuilding, just run `yarn run gulp`.
|
||||||
|
|
||||||
## 🧑🔬 Configuring a new instance
|
## 🧑🔬 Configuring a new instance
|
||||||
|
@ -93,7 +118,7 @@ cp -r ../misskey/files . # if you don't use object storage
|
||||||
|
|
||||||
## 🍀 NGINX
|
## 🍀 NGINX
|
||||||
|
|
||||||
- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-avaliable/ && cd /etc/nginx/sites-avaliable/`
|
- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
|
||||||
- Edit `calckey.nginx.conf` to reflect your instance properly
|
- Edit `calckey.nginx.conf` to reflect your instance properly
|
||||||
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
|
- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/`
|
||||||
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
|
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
|
||||||
|
@ -102,7 +127,7 @@ cp -r ../misskey/files . # if you don't use object storage
|
||||||
|
|
||||||
## 🚀 Build and launch!
|
## 🚀 Build and launch!
|
||||||
|
|
||||||
### 🐢 NodeJS
|
### 🐢 NodeJS + pm2
|
||||||
|
|
||||||
#### `git pull` and run these steps to update Calckey in the future!
|
#### `git pull` and run these steps to update Calckey in the future!
|
||||||
|
|
||||||
|
@ -123,15 +148,16 @@ docker up -d
|
||||||
### 🐳 Docker Compose
|
### 🐳 Docker Compose
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose build
|
docker-compose build
|
||||||
docker-compose run --rm web yarn run init
|
docker-compose run --rm web yarn run init
|
||||||
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.
|
- 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`
|
- 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 recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker.
|
||||||
- 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.
|
||||||
|
|
0
custom/locales/.gitkeep
Normal file
0
custom/locales/.gitkeep
Normal file
|
@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () =>
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('copy:backend:custom', () =>
|
gulp.task('copy:backend:custom', () =>
|
||||||
gulp.src('./custom/*').pipe(gulp.dest('./packages/backend/assets/'))
|
gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/'))
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('copy:client:fonts', () =>
|
gulp.task('copy:client:fonts', () =>
|
||||||
|
@ -24,7 +24,7 @@ gulp.task('copy:client:fonts', () =>
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('copy:client:phosphor', () =>
|
gulp.task('copy:client:phosphor', () =>
|
||||||
gulp.src('./node_modules/phosphor-icons/src/css/*').pipe(gulp.dest('./built/_client_dist_/phosphor/'))
|
gulp.src('./node_modules/phosphor-icons/src/fonts/*').pipe(gulp.dest('./built/_client_dist_/phosphor/'))
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('copy:client:locales', cb => {
|
gulp.task('copy:client:locales', cb => {
|
||||||
|
|
|
@ -32,12 +32,12 @@ uploading: "Uploading..."
|
||||||
save: "Save"
|
save: "Save"
|
||||||
users: "Users"
|
users: "Users"
|
||||||
addUser: "Add a user"
|
addUser: "Add a user"
|
||||||
favorite: "Add to favorites"
|
favorite: "Add to bookmarks"
|
||||||
favorites: "Favorites"
|
favorites: "Bookmarks"
|
||||||
unfavorite: "Remove from favorites"
|
unfavorite: "Remove from bookmarks"
|
||||||
favorited: "Added to favorites."
|
favorited: "Added to bookmarks."
|
||||||
alreadyFavorited: "Already added to favorites."
|
alreadyFavorited: "Already added to bookmarks."
|
||||||
cantFavorite: "Couldn't add to favorites."
|
cantFavorite: "Couldn't add to bookmarks."
|
||||||
pin: "Pin to profile"
|
pin: "Pin to profile"
|
||||||
unpin: "Unpin from profile"
|
unpin: "Unpin from profile"
|
||||||
copyContent: "Copy contents"
|
copyContent: "Copy contents"
|
||||||
|
@ -160,7 +160,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"
|
||||||
|
@ -680,7 +680,7 @@ disableShowingAnimatedImages: "Don't play animated images"
|
||||||
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
||||||
notSet: "Not set"
|
notSet: "Not set"
|
||||||
emailVerified: "Email has been verified"
|
emailVerified: "Email has been verified"
|
||||||
noteFavoritesCount: "Number of favorite notes"
|
noteFavoritesCount: "Number of bookmarked notes"
|
||||||
pageLikesCount: "Number of liked Pages"
|
pageLikesCount: "Number of liked Pages"
|
||||||
pageLikedCount: "Number of received Page likes"
|
pageLikedCount: "Number of received Page likes"
|
||||||
contact: "Contact"
|
contact: "Contact"
|
||||||
|
@ -771,8 +771,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"
|
||||||
|
@ -1002,9 +1002,9 @@ _aboutMisskey:
|
||||||
allContributors: "All contributors"
|
allContributors: "All contributors"
|
||||||
source: "Source code"
|
source: "Source code"
|
||||||
translation: "Translate Misskey"
|
translation: "Translate Misskey"
|
||||||
donate: "Donate to Misskey"
|
donate: "Donate to Calckey"
|
||||||
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
|
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
|
||||||
patrons: "Misskey patrons"
|
patrons: "Calckey patrons"
|
||||||
_nsfw:
|
_nsfw:
|
||||||
respect: "Hide NSFW media"
|
respect: "Hide NSFW media"
|
||||||
ignore: "Don't hide NSFW media"
|
ignore: "Don't hide NSFW media"
|
||||||
|
@ -1095,7 +1095,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"
|
||||||
|
@ -1228,7 +1228,7 @@ _tutorial:
|
||||||
step5_3: "The Home {icon} timeline is where you can see posts from your followers."
|
step5_3: "The Home {icon} timeline is where you can see posts from your followers."
|
||||||
step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance."
|
step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance."
|
||||||
step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend."
|
step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend."
|
||||||
step5_6: "The Social {icon} timeline is where you can see posts from friends of your followers."
|
step5_6: "The Social {icon} timeline is your home + local."
|
||||||
step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance."
|
step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance."
|
||||||
step6_1: "So, what is this place?"
|
step6_1: "So, what is this place?"
|
||||||
step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"."
|
step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"."
|
||||||
|
@ -1251,8 +1251,8 @@ _permissions:
|
||||||
"write:blocks": "Edit your list of blocked users"
|
"write:blocks": "Edit your list of blocked users"
|
||||||
"read:drive": "Access your Drive files and folders"
|
"read:drive": "Access your Drive files and folders"
|
||||||
"write:drive": "Edit or delete your Drive files and folders"
|
"write:drive": "Edit or delete your Drive files and folders"
|
||||||
"read:favorites": "View your list of favorites"
|
"read:favorites": "View your list of bookmarks"
|
||||||
"write:favorites": "Edit your list of favorites"
|
"write:favorites": "Edit your list of bookmarks"
|
||||||
"read:following": "View information on who you follow"
|
"read:following": "View information on who you follow"
|
||||||
"write:following": "Follow or unfollow other accounts"
|
"write:following": "Follow or unfollow other accounts"
|
||||||
"read:messaging": "View your chats"
|
"read:messaging": "View your chats"
|
||||||
|
@ -1265,10 +1265,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"
|
||||||
|
@ -1442,7 +1442,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"
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
|
let languages = []
|
||||||
|
let languages_custom = []
|
||||||
|
|
||||||
const merge = (...args) => args.reduce((a, c) => ({
|
const merge = (...args) => args.reduce((a, c) => ({
|
||||||
...a,
|
...a,
|
||||||
|
@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({
|
||||||
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
|
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
|
||||||
}), {});
|
}), {});
|
||||||
|
|
||||||
const languages = [
|
|
||||||
'ar-SA',
|
fs.readdirSync(__dirname).forEach((file) => {
|
||||||
'cs-CZ',
|
if (file.includes('.yml')){
|
||||||
'da-DK',
|
file = file.slice(0, file.indexOf('.'))
|
||||||
'de-DE',
|
languages.push(file);
|
||||||
'en-US',
|
}
|
||||||
'es-ES',
|
})
|
||||||
'fr-FR',
|
|
||||||
'id-ID',
|
fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => {
|
||||||
'it-IT',
|
if (file.includes('.yml')){
|
||||||
'ja-JP',
|
file = file.slice(0, file.indexOf('.'))
|
||||||
'ja-KS',
|
languages_custom.push(file);
|
||||||
'kab-KAB',
|
}
|
||||||
'kn-IN',
|
})
|
||||||
'ko-KR',
|
|
||||||
'nl-NL',
|
|
||||||
'no-NO',
|
|
||||||
'pl-PL',
|
|
||||||
'pt-PT',
|
|
||||||
'ru-RU',
|
|
||||||
'sk-SK',
|
|
||||||
'ug-CN',
|
|
||||||
'uk-UA',
|
|
||||||
'vi-VN',
|
|
||||||
'zh-CN',
|
|
||||||
'zh-TW',
|
|
||||||
];
|
|
||||||
|
|
||||||
const primaries = {
|
const primaries = {
|
||||||
'en': 'US',
|
'en': 'US',
|
||||||
|
@ -51,6 +40,8 @@ const primaries = {
|
||||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||||
|
|
||||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
|
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
|
||||||
|
const locales_custom = languages_custom.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, 'utf-8'))) || {}, a), {});
|
||||||
|
Object.assign(locales, locales_custom)
|
||||||
|
|
||||||
module.exports = Object.entries(locales)
|
module.exports = Object.entries(locales)
|
||||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "12.119.0-calc.14.6",
|
"version": "12.119.0-calc.18",
|
||||||
"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",
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
"@bull-board/api": "^4.6.4",
|
"@bull-board/api": "^4.6.4",
|
||||||
"@bull-board/ui": "^4.6.4",
|
"@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",
|
||||||
|
|
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`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --experimental-json-modules ./built/index.js",
|
"start": "node ./built/index.js",
|
||||||
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||||
"migrate": "typeorm migration:run -d ormconfig.js",
|
"migrate": "typeorm migration:run -d ormconfig.js",
|
||||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||||
"watch": "node watch.mjs",
|
"watch": "node watch.mjs",
|
||||||
|
@ -36,11 +36,11 @@
|
||||||
"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.1255.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",
|
||||||
|
@ -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",
|
||||||
|
@ -177,7 +177,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||||
"@typescript-eslint/parser": "5.43.0",
|
"@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.9.3"
|
"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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { In, Repository } from 'typeorm';
|
import { In, Repository } from 'typeorm';
|
||||||
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
|
|
||||||
import { Notification } from '@/models/entities/notification.js';
|
import { Notification } from '@/models/entities/notification.js';
|
||||||
import { awaitAll } from '@/prelude/await-all.js';
|
import { awaitAll } from '@/prelude/await-all.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import type { Packed } from '@/misc/schema.js';
|
||||||
import { Note } from '@/models/entities/note.js';
|
import type { Note } from '@/models/entities/note.js';
|
||||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
import type { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import type { User } from '@/models/entities/user.js';
|
||||||
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
||||||
import { notificationTypes } from '@/types.js';
|
import { notificationTypes } from '@/types.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
|
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
|
||||||
|
|
||||||
export const NotificationRepository = db.getRepository(Notification).extend({
|
export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
async pack(
|
async pack(
|
||||||
|
@ -17,7 +17,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
_hintForEachNotes_?: {
|
_hintForEachNotes_?: {
|
||||||
myReactions: Map<Note['id'], NoteReaction | null>;
|
myReactions: Map<Note['id'], NoteReaction | null>;
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
): Promise<Packed<'Notification'>> {
|
): Promise<Packed<'Notification'>> {
|
||||||
const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||||
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
||||||
|
@ -86,7 +86,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
|
|
||||||
async packMany(
|
async packMany(
|
||||||
notifications: Notification[],
|
notifications: Notification[],
|
||||||
meId: User['id']
|
meId: User['id'],
|
||||||
) {
|
) {
|
||||||
if (notifications.length === 0) return [];
|
if (notifications.length === 0) return [];
|
||||||
|
|
||||||
|
@ -106,10 +106,15 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
|
|
||||||
await prefetchEmojis(aggregateNoteEmojis(notes));
|
await prefetchEmojis(aggregateNoteEmojis(notes));
|
||||||
|
|
||||||
return await Promise.all(notifications.map(x => this.pack(x, {
|
const results = await Promise.all(notifications
|
||||||
|
.map(x =>
|
||||||
|
this.pack(x, {
|
||||||
_hintForEachNotes_: {
|
_hintForEachNotes_: {
|
||||||
myReactions: myReactionsMap,
|
myReactions: myReactionsMap,
|
||||||
},
|
},
|
||||||
})));
|
}).catch(e => null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return results.filter(x => x != null);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<st
|
||||||
await updatePerson(actor.uri!, resolver, object);
|
await updatePerson(actor.uri!, resolver, object);
|
||||||
return `ok: Person updated`;
|
return `ok: Person updated`;
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await updateQuestion(object).catch(e => console.log(e));
|
await updateQuestion(object, resolver).catch(e => console.log(e));
|
||||||
return `ok: Question updated`;
|
return `ok: Question updated`;
|
||||||
} else {
|
} else {
|
||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unknown type: ${getApType(object)}`;
|
||||||
|
|
|
@ -272,7 +272,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
await updateFeatured(user!.id).catch(err => logger.error(err));
|
await updateFeatured(user!.id, resolver).catch(err => logger.error(err));
|
||||||
|
|
||||||
return user!;
|
return user!;
|
||||||
}
|
}
|
||||||
|
@ -386,7 +386,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateFeatured(exist.id).catch(err => logger.error(err));
|
await updateFeatured(exist.id, resolver).catch(err => logger.error(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -464,14 +464,14 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined)
|
||||||
return { fields, services };
|
return { fields, services };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateFeatured(userId: User['id']) {
|
export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
|
||||||
const user = await Users.findOneByOrFail({ id: userId });
|
const user = await Users.findOneByOrFail({ id: userId });
|
||||||
if (!Users.isRemoteUser(user)) return;
|
if (!Users.isRemoteUser(user)) return;
|
||||||
if (!user.featured) return;
|
if (!user.featured) return;
|
||||||
|
|
||||||
logger.info(`Updating the featured: ${user.uri}`);
|
logger.info(`Updating the featured: ${user.uri}`);
|
||||||
|
|
||||||
const resolver = new Resolver();
|
if (resolver == null) resolver = new Resolver();
|
||||||
|
|
||||||
// Resolve to (Ordered)Collection Object
|
// Resolve to (Ordered)Collection Object
|
||||||
const collection = await resolver.resolveCollection(user.featured);
|
const collection = await resolver.resolveCollection(user.featured);
|
||||||
|
|
|
@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
|
||||||
* @param uri URI of AP Question object
|
* @param uri URI of AP Question object
|
||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
export async function updateQuestion(value: any) {
|
export async function updateQuestion(value: any, resolver?: Resolver) {
|
||||||
const uri = typeof value === 'string' ? value : value.id;
|
const uri = typeof value === 'string' ? value : value.id;
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
|
@ -55,7 +55,7 @@ export async function updateQuestion(value: any) {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
const resolver = new Resolver();
|
if (resolver == null) resolver = new Resolver();
|
||||||
const question = await resolver.resolve(value) as IQuestion;
|
const question = await resolver.resolve(value) as IQuestion;
|
||||||
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||||
export default class Resolver {
|
export default class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
private user?: ILocalUser;
|
private user?: ILocalUser;
|
||||||
|
private recursionLimit?: number;
|
||||||
|
|
||||||
constructor() {
|
constructor(recursionLimit = 100) {
|
||||||
this.history = new Set();
|
this.history = new Set();
|
||||||
|
this.recursionLimit = recursionLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getHistory(): string[] {
|
public getHistory(): string[] {
|
||||||
|
@ -59,7 +61,9 @@ export default class Resolver {
|
||||||
if (this.history.has(value)) {
|
if (this.history.has(value)) {
|
||||||
throw new Error('cannot resolve already resolved one');
|
throw new Error('cannot resolve already resolved one');
|
||||||
}
|
}
|
||||||
|
if (this.recursionLimit && this.history.size > this.recursionLimit) {
|
||||||
|
throw new Error('hit recursion limit');
|
||||||
|
}
|
||||||
this.history.add(value);
|
this.history.add(value);
|
||||||
|
|
||||||
const host = extractDbHost(value);
|
const host = extractDbHost(value);
|
||||||
|
|
|
@ -275,6 +275,7 @@ import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
||||||
import * as ep___customMOTD from './endpoints/custom-motd.js';
|
import * as ep___customMOTD from './endpoints/custom-motd.js';
|
||||||
import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js';
|
import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js';
|
||||||
import * as ep___latestVersion from './endpoints/latest-version.js';
|
import * as ep___latestVersion from './endpoints/latest-version.js';
|
||||||
|
import * as ep___patrons from './endpoints/patrons.js';
|
||||||
import * as ep___promo_read from './endpoints/promo/read.js';
|
import * as ep___promo_read from './endpoints/promo/read.js';
|
||||||
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
|
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
|
||||||
import * as ep___resetDb from './endpoints/reset-db.js';
|
import * as ep___resetDb from './endpoints/reset-db.js';
|
||||||
|
@ -599,6 +600,7 @@ const eps = [
|
||||||
['custom-motd', ep___customMOTD],
|
['custom-motd', ep___customMOTD],
|
||||||
['custom-splash-icons', ep___customSplashIcons],
|
['custom-splash-icons', ep___customSplashIcons],
|
||||||
['latest-version', ep___latestVersion],
|
['latest-version', ep___latestVersion],
|
||||||
|
['patrons', ep___patrons],
|
||||||
['promo/read', ep___promo_read],
|
['promo/read', ep___promo_read],
|
||||||
['request-reset-password', ep___requestResetPassword],
|
['request-reset-password', ep___requestResetPassword],
|
||||||
['reset-db', ep___resetDb],
|
['reset-db', ep___resetDb],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
27
packages/backend/src/server/api/endpoints/patrons.ts
Normal file
27
packages/backend/src/server/api/endpoints/patrons.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import define from '../define.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['meta'],
|
||||||
|
description: 'Get list of Calckey patrons from Codeberg',
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
requireCredentialPrivateMode: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async () => {
|
||||||
|
let patrons;
|
||||||
|
await fetch('https://codeberg.org/thatonecalculator/calckey/raw/branch/develop/patrons.json')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
patrons = data['patrons'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return patrons;
|
||||||
|
});
|
|
@ -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)
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function genOpenapiSpec() {
|
||||||
|
|
||||||
info: {
|
info: {
|
||||||
version: 'v1',
|
version: 'v1',
|
||||||
title: 'Misskey API',
|
title: 'Calckey API',
|
||||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
'x-logo': { url: '/static-assets/api-doc.png' },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -232,8 +232,43 @@ const getFeed = async (acct: string) => {
|
||||||
return user && await packFeed(user);
|
return user && await packFeed(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
|
||||||
|
const reUser = new RegExp(`^/@(?<user>[^/]+?)(?:\.(?<feed>json|rss|atom))?(?:/(?<sub>[^/]+))?$`);
|
||||||
|
router.get(reUser, async (ctx, next) => {
|
||||||
|
const groups = reUser.exec(ctx.originalUrl)?.groups;
|
||||||
|
if (!groups) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.params = groups;
|
||||||
|
|
||||||
|
console.log(ctx, ctx.params)
|
||||||
|
if (groups.feed) {
|
||||||
|
if (groups.sub) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (groups.feed) {
|
||||||
|
case 'json':
|
||||||
|
await jsonFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
case 'rss':
|
||||||
|
await rssFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
case 'atom':
|
||||||
|
await atomFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await userPage(ctx, next);
|
||||||
|
});
|
||||||
|
|
||||||
// Atom
|
// Atom
|
||||||
router.get('/@:user.atom', async ctx => {
|
const atomFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// RSS
|
// RSS
|
||||||
router.get('/@:user.rss', async ctx => {
|
const rssFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
router.get('/@:user.json', async ctx => {
|
const jsonFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -266,19 +301,26 @@ router.get('/@:user.json', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
//#region SSR (for crawlers)
|
//#region SSR (for crawlers)
|
||||||
// User
|
// User
|
||||||
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
const userPage: Router.Middleware = async (ctx, next) => {
|
||||||
const { username, host } = Acct.parse(ctx.params.user);
|
const userParam = ctx.params.user;
|
||||||
|
const subParam = ctx.params.sub;
|
||||||
|
const { username, host } = Acct.parse(userParam);
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user != null) {
|
if (user === null) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
const me = profile.fields
|
const me = profile.fields
|
||||||
|
@ -287,22 +329,19 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||||
.map(field => field.value)
|
.map(field => field.value)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
await ctx.render('user', {
|
const userDetail = {
|
||||||
user, profile, me,
|
user, profile, me,
|
||||||
avatarUrl: await Users.getAvatarUrl(user),
|
avatarUrl: await Users.getAvatarUrl(user),
|
||||||
sub: ctx.params.sub,
|
sub: subParam,
|
||||||
instanceName: meta.name || 'Calckey',
|
instanceName: meta.name || 'Calckey',
|
||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
privateMode: meta.privateMode,
|
privateMode: meta.privateMode,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
await ctx.render('user', userDetail);
|
||||||
ctx.set('Cache-Control', 'public, max-age=15');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
} else {
|
};
|
||||||
// リモートユーザーなので
|
|
||||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/users/:user', async ctx => {
|
router.get('/users/:user', async ctx => {
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
|
|
|
@ -42,7 +42,7 @@ html {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
transform: translateY(110px);
|
transform: translateY(110px);
|
||||||
display: none !important;
|
display: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
#splashSpinner > .spinner {
|
#splashSpinner > .spinner {
|
||||||
|
@ -101,6 +101,16 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media(prefers-reduced-motion) {
|
||||||
|
#splashSpinner {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splashIcon {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#splashText {
|
#splashText {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -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,7 +48,7 @@
|
||||||
"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",
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
"@typescript-eslint/parser": "5.43.0",
|
"@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",
|
||||||
|
|
|
@ -81,9 +81,12 @@ const bannerStyle = computed(() => {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .status {
|
> .status {
|
||||||
|
@ -93,7 +96,9 @@ const bannerStyle = computed(() => {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,7 @@ export default defineComponent({
|
||||||
|
|
||||||
> ::v-deep(i) {
|
> ::v-deep(i) {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
transform: translateY(0.1em);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ const bg = {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
text-overflow: clip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||||
<div class="header" @contextmenu="onContextmenu">
|
<div class="header" @contextmenu="onContextmenu">
|
||||||
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph--left-bold ph-lg"></i></button>
|
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph-caret-left-bold ph-lg"></i></button>
|
||||||
<span v-else style="display: inline-block; width: 20px"></span>
|
<span v-else style="display: inline-block; width: 20px"></span>
|
||||||
<span v-if="pageMetadata?.value" class="title">
|
<span v-if="pageMetadata?.value" class="title">
|
||||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
||||||
|
|
|
@ -71,21 +71,21 @@
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||||
<button class="button _button" @click="reply()">
|
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
||||||
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
|
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
|
||||||
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
|
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
||||||
<i class="ph-smiley-bold ph-lg"></i>
|
<i class="ph-smiley-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||||
<i class="ph-minus-bold ph-lg"></i>
|
<i class="ph-minus-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote"/>
|
<XQuoteButton class="button" :note="appearNote"/>
|
||||||
<button ref="menuButton" class="button _button" @click="menu()">
|
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
||||||
<i class="ph-dots-three-outline-bold ph-lg"></i>
|
<i class="ph-dots-three-outline-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -135,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();
|
||||||
|
|
||||||
|
@ -145,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);
|
||||||
}
|
}
|
||||||
|
@ -425,13 +426,18 @@ function readPromo() {
|
||||||
> .article {
|
> .article {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 28px 32px 18px;
|
padding: 28px 32px 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 14px 8px 0;
|
margin: 0 14px 8px 0;
|
||||||
width: 58px;
|
width: 52px;
|
||||||
height: 58px;
|
height: 52px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
/* For some reason this breaks avatar
|
/* For some reason this breaks avatar
|
||||||
positions on notes, commenting it for now */
|
positions on notes, commenting it for now */
|
||||||
|
@ -612,7 +618,7 @@ function readPromo() {
|
||||||
margin: 0 10px 8px 0;
|
margin: 0 10px 8px 0;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
top: calc(14px + var(--stickyTop, 0px));
|
// top: calc(14px + var(--stickyTop, 0px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,21 +81,21 @@
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||||
<button class="button _button" @click="reply()">
|
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
||||||
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
|
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
|
||||||
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
|
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
||||||
<i class="ph-smiley-bold ph-lg"></i>
|
<i class="ph-smiley-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||||
<i class="ph-minus-bold ph-lg"></i>
|
<i class="ph-minus-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote"/>
|
<XQuoteButton class="button" :note="appearNote"/>
|
||||||
<button ref="menuButton" class="button _button" @click="menu()">
|
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
||||||
<i class="ph-dots-three-outline-bold ph-lg"></i>
|
<i class="ph-dots-three-outline-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, 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 * as misskey from 'misskey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -345,6 +346,11 @@ if (appearNote.replyId) {
|
||||||
|
|
||||||
> .reply-to-more {
|
> .reply-to-more {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .renote {
|
> .renote {
|
||||||
|
@ -410,8 +416,8 @@ if (appearNote.replyId) {
|
||||||
> .avatar {
|
> .avatar {
|
||||||
display: block;
|
display: block;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 58px;
|
width: 52px;
|
||||||
height: 58px;
|
height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
@ -542,6 +548,11 @@ if (appearNote.replyId) {
|
||||||
|
|
||||||
> .reply {
|
> .reply {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .reply, .reply-to, .reply-to-more {
|
> .reply, .reply-to, .reply-to-more {
|
||||||
|
|
|
@ -65,6 +65,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
&.children {
|
&.children {
|
||||||
padding: 10px 0 0 16px;
|
padding: 10px 0 0 16px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
&.max-width_450px {
|
&.max-width_450px {
|
||||||
padding: 10px 0 0 8px;
|
padding: 10px 0 0 8px;
|
||||||
|
@ -86,9 +87,15 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
> .body {
|
> .body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -57,7 +57,7 @@ const buttonsLeft = $computed(() => {
|
||||||
|
|
||||||
if (history.length > 1) {
|
if (history.length > 1) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'ph--left-bold ph-lg',
|
icon: 'ph-caret-left-bold ph-lg',
|
||||||
onClick: back,
|
onClick: back,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote && $store.state.seperateRenoteQuote"
|
v-if="canRenote && $store.state.seperateRenoteQuote"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.quote"
|
||||||
class="eddddedb _button"
|
class="eddddedb _button"
|
||||||
@click="quote()"
|
@click="quote()"
|
||||||
>
|
>
|
||||||
|
@ -14,6 +15,7 @@ import type { Note } from 'misskey-js/built/entities';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: Note;
|
note: Note;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<button
|
<button
|
||||||
v-if="canRenote"
|
v-if="canRenote"
|
||||||
ref="buttonRef"
|
ref="buttonRef"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.renote"
|
||||||
class="eddddedb _button canRenote"
|
class="eddddedb _button canRenote"
|
||||||
@click="renote(false, $event)"
|
@click="renote(false, $event)"
|
||||||
>
|
>
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
import Ripple from '@/components/MkRipple.vue';
|
import Ripple from '@/components/MkRipple.vue';
|
||||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
|
@ -23,7 +24,7 @@ import * as os from '@/os';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -52,9 +53,22 @@ useTooltip(buttonRef, async (showing) => {
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
if (defaultStore.state.seperateRenoteQuote) {
|
|
||||||
|
const renotes = await os.api('notes/renotes', {
|
||||||
|
noteId: props.note.id,
|
||||||
|
limit: 11,
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = renotes.map(x => x.user.id);
|
||||||
|
const hasRenotedBefore = users.includes($i.id);
|
||||||
|
|
||||||
|
let buttonActions = [{
|
||||||
|
text: i18n.ts.renote,
|
||||||
|
icon: 'ph-repeat-bold ph-lg',
|
||||||
|
danger: false,
|
||||||
|
action: () => {
|
||||||
os.api('notes/create', {
|
os.api('notes/create', {
|
||||||
renoteId: props.note.id,
|
renoteId: props.note.id,
|
||||||
visibility: props.note.visibility,
|
visibility: props.note.visibility,
|
||||||
|
@ -66,28 +80,35 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
os.popup(Ripple, { x, y }, {}, 'end');
|
os.popup(Ripple, { x, y }, {}, 'end');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
os.popupMenu([{
|
|
||||||
text: i18n.ts.renote,
|
|
||||||
icon: 'ph-repeat-bold ph-lg',
|
|
||||||
action: () => {
|
|
||||||
os.api('notes/create', {
|
|
||||||
renoteId: props.note.id,
|
|
||||||
visibility: props.note.visibility,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}, {
|
}];
|
||||||
|
|
||||||
|
if (!defaultStore.state.seperateRenoteQuote) {
|
||||||
|
buttonActions.push({
|
||||||
text: i18n.ts.quote,
|
text: i18n.ts.quote,
|
||||||
icon: 'ph-quotes-bold ph-lg',
|
icon: 'ph-quotes-bold ph-lg',
|
||||||
|
danger: false,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.post({
|
os.post({
|
||||||
renote: props.note,
|
renote: props.note,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], buttonRef.value, {
|
|
||||||
viaKeyboard,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasRenotedBefore) {
|
||||||
|
buttonActions.push({
|
||||||
|
text: i18n.ts.unrenote,
|
||||||
|
icon: 'ph-trash-bold ph-lg',
|
||||||
|
danger: true,
|
||||||
|
action: () => {
|
||||||
|
os.api('notes/unrenote', {
|
||||||
|
noteId: props.note.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { reducedMotion } from '@/scripts/reduced-motion';
|
||||||
|
|
||||||
const particles = ref([]);
|
const particles = ref([]);
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
|
@ -75,6 +76,7 @@ let stop = false;
|
||||||
let ro: ResizeObserver | undefined;
|
let ro: ResizeObserver | undefined;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (!reducedMotion()) {
|
||||||
ro = new ResizeObserver((entries, observer) => {
|
ro = new ResizeObserver((entries, observer) => {
|
||||||
width.value = el.value?.offsetWidth + 64;
|
width.value = el.value?.offsetWidth + 64;
|
||||||
height.value = el.value?.offsetHeight + 64;
|
height.value = el.value?.offsetHeight + 64;
|
||||||
|
@ -103,6 +105,7 @@ onMounted(() => {
|
||||||
}, 500 + (Math.random() * 500));
|
}, 500 + (Math.random() * 500));
|
||||||
};
|
};
|
||||||
add();
|
add();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="skdfgljsdkf _button" @click="star($event)">
|
<button v-tooltip.noDelay.bottom="i18n.ts._gallery.like" class="skdfgljsdkf _button" @click="star($event)">
|
||||||
<i class="ph-star-bold ph-lg"></i>
|
<i class="ph-star-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -9,6 +9,7 @@ import type { Note } from 'misskey-js/built/entities';
|
||||||
import Ripple from '@/components/MkRipple.vue';
|
import Ripple from '@/components/MkRipple.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: Note;
|
note: Note;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { VNode, defineComponent, h } from 'vue';
|
import { defineComponent, h } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
import MkUrl from '@/components/global/MkUrl.vue';
|
import MkUrl from '@/components/global/MkUrl.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
import MkMention from '@/components/MkMention.vue';
|
import MkMention from '@/components/MkMention.vue';
|
||||||
|
@ -12,6 +13,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
|
||||||
import MkA from '@/components/global/MkA.vue';
|
import MkA from '@/components/global/MkA.vue';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { MFM_TAGS } from '@/scripts/mfm-tags';
|
import { MFM_TAGS } from '@/scripts/mfm-tags';
|
||||||
|
import { reducedMotion } from '@/scripts/reduced-motion';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -97,17 +99,17 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
case 'jelly': {
|
case 'jelly': {
|
||||||
const speed = validTime(token.props.args.speed) || '1s';
|
const speed = validTime(token.props.args.speed) || '1s';
|
||||||
style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
|
style = (this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'twitch': {
|
case 'twitch': {
|
||||||
const speed = validTime(token.props.args.speed) || '0.5s';
|
const speed = validTime(token.props.args.speed) || '0.5s';
|
||||||
style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
|
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-twitch ${speed} ease infinite;` : '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'shake': {
|
case 'shake': {
|
||||||
const speed = validTime(token.props.args.speed) || '0.5s';
|
const speed = validTime(token.props.args.speed) || '0.5s';
|
||||||
style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
|
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-shake ${speed} ease infinite;` : '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'spin': {
|
case 'spin': {
|
||||||
|
@ -120,19 +122,30 @@ export default defineComponent({
|
||||||
token.props.args.y ? 'mfm-spinY' :
|
token.props.args.y ? 'mfm-spinY' :
|
||||||
'mfm-spin';
|
'mfm-spin';
|
||||||
const speed = validTime(token.props.args.speed) || '1.5s';
|
const speed = validTime(token.props.args.speed) || '1.5s';
|
||||||
style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'jump': {
|
case 'jump': {
|
||||||
const speed = validTime(token.props.args.speed) || '0.75s';
|
const speed = validTime(token.props.args.speed) || '0.75s';
|
||||||
style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : '';
|
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-jump ${speed} linear infinite;` : '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'bounce': {
|
case 'bounce': {
|
||||||
const speed = validTime(token.props.args.speed) || '0.75s';
|
const speed = validTime(token.props.args.speed) || '0.75s';
|
||||||
style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
|
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'rainbow': {
|
||||||
|
const speed = validTime(token.props.args.speed) || '1s';
|
||||||
|
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'sparkle': {
|
||||||
|
if (!this.$store.state.animatedMfm && !reducedMotion()) {
|
||||||
|
return genEl(token.children);
|
||||||
|
}
|
||||||
|
return h(MkSparkle, {}, genEl(token.children));
|
||||||
|
}
|
||||||
case 'flip': {
|
case 'flip': {
|
||||||
const transform =
|
const transform =
|
||||||
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
|
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
|
||||||
|
@ -173,17 +186,6 @@ export default defineComponent({
|
||||||
class: '_mfm_blur_',
|
class: '_mfm_blur_',
|
||||||
}, genEl(token.children));
|
}, genEl(token.children));
|
||||||
}
|
}
|
||||||
case 'rainbow': {
|
|
||||||
const speed = validTime(token.props.args.speed) || '1s';
|
|
||||||
style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'sparkle': {
|
|
||||||
if (!this.$store.state.animatedMfm) {
|
|
||||||
return genEl(token.children);
|
|
||||||
}
|
|
||||||
return h(MkSparkle, {}, genEl(token.children));
|
|
||||||
}
|
|
||||||
case 'rotate': {
|
case 'rotate': {
|
||||||
const rotate =
|
const rotate =
|
||||||
token.props.args.x ? 'perspective(128px) rotateX' :
|
token.props.args.x ? 'perspective(128px) rotateX' :
|
||||||
|
|
21027
packages/client/src/icons.scss
Normal file
21027
packages/client/src/icons.scss
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@/style.scss';
|
import '@/style.scss';
|
||||||
|
import '@/icons.scss';
|
||||||
|
|
||||||
//#region account indexedDB migration
|
//#region account indexedDB migration
|
||||||
import { set } from '@/scripts/idb-proxy';
|
import { set } from '@/scripts/idb-proxy';
|
||||||
|
@ -295,7 +296,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
||||||
if (v) {
|
if (v && deviceKind !== 'smartphone') {
|
||||||
document.documentElement.style.removeProperty('--blur');
|
document.documentElement.style.removeProperty('--blur');
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.style.setProperty('--blur', 'none');
|
document.documentElement.style.setProperty('--blur', 'none');
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const navbarItemDef = reactive({
|
||||||
},
|
},
|
||||||
favorites: {
|
favorites: {
|
||||||
title: 'favorites',
|
title: 'favorites',
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||||
show: computed(() => $i != null),
|
show: computed(() => $i != null),
|
||||||
to: '/my/favorites',
|
to: '/my/favorites',
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,20 +24,29 @@
|
||||||
{{ i18n.ts._aboutMisskey.source }}
|
{{ i18n.ts._aboutMisskey.source }}
|
||||||
<template #suffix>Codeberg</template>
|
<template #suffix>Codeberg</template>
|
||||||
</FormLink>
|
</FormLink>
|
||||||
|
<FormLink to="https://liberapay.com/ThatOneCalculator" external>
|
||||||
|
<template #icon><i class="ph-money-bold ph-lg"></i></template>
|
||||||
|
{{ i18n.ts._aboutMisskey.donate }}
|
||||||
|
<template #suffix>Donate</template>
|
||||||
|
</FormLink>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||||
<div class="_formLinks">
|
<div class="_formLinks">
|
||||||
<FormLink to="https://codeberg.org/thatonecalculator" external>ThatOneCalculator (fork developer)</FormLink>
|
<FormLink to="/@t1c@i.calckey.cloud"><Mfm :text="'$[sparkle @t1c@i.calckey.cloud] (Main fork developer)'"/></FormLink>
|
||||||
<FormLink to="https://github.com/syuilo" external>Syuilo (Misskey developer)</FormLink>
|
<FormLink to="/@syuilo@misskey.io"><Mfm :text="'@syuilo@misskey.io (Misskey developer)'"/></FormLink>
|
||||||
<FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink>
|
<FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink>
|
||||||
</div>
|
</div>
|
||||||
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||||
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
|
<MkSparkle>
|
||||||
|
<div v-for="patron in patrons" :key="patron" style="margin-bottom: 0.5rem">
|
||||||
|
<Mfm :text="`${patron}`"/>
|
||||||
|
</div>
|
||||||
|
</MkSparkle>
|
||||||
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
|
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,92 +62,14 @@ import FormLink from '@/components/form/link.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
|
import MkSparkle from '@/components/MkSparkle.vue';
|
||||||
import { physics } from '@/scripts/physics';
|
import { physics } from '@/scripts/physics';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
const patrons = [
|
const patrons = await os.api('patrons');
|
||||||
'まっちゃとーにゅ',
|
|
||||||
'mametsuko',
|
|
||||||
'noellabo',
|
|
||||||
'AureoleArk',
|
|
||||||
'Gargron',
|
|
||||||
'Nokotaro Takeda',
|
|
||||||
'Suji Yan',
|
|
||||||
'oi_yekssim',
|
|
||||||
'regtan',
|
|
||||||
'Hekovic',
|
|
||||||
'nenohi',
|
|
||||||
'Gitmo Life Services',
|
|
||||||
'naga_rus',
|
|
||||||
'Efertone',
|
|
||||||
'Melilot',
|
|
||||||
'motcha',
|
|
||||||
'nanami kan',
|
|
||||||
'sevvie Rose',
|
|
||||||
'Hayato Ishikawa',
|
|
||||||
'Puniko',
|
|
||||||
'skehmatics',
|
|
||||||
'Quinton Macejkovic',
|
|
||||||
'YUKIMOCHI',
|
|
||||||
'dansup',
|
|
||||||
'mewl hayabusa',
|
|
||||||
'Emilis',
|
|
||||||
'Fristi',
|
|
||||||
'makokunsan',
|
|
||||||
'chidori ninokura',
|
|
||||||
'Peter G.',
|
|
||||||
'見当かなみ',
|
|
||||||
'natalie',
|
|
||||||
'Maronu',
|
|
||||||
'Steffen K9',
|
|
||||||
'takimura',
|
|
||||||
'sikyosyounin',
|
|
||||||
'Nesakko',
|
|
||||||
'YuzuRyo61',
|
|
||||||
'blackskye',
|
|
||||||
'sheeta.s',
|
|
||||||
'osapon',
|
|
||||||
'public_yusuke',
|
|
||||||
'CG',
|
|
||||||
'吴浥',
|
|
||||||
't_w',
|
|
||||||
'Jerry',
|
|
||||||
'nafuchoco',
|
|
||||||
'Takumi Sugita',
|
|
||||||
'GLaTAN',
|
|
||||||
'mkatze',
|
|
||||||
'kabo2468y',
|
|
||||||
'mydarkstar',
|
|
||||||
'Roujo',
|
|
||||||
'DignifiedSilence',
|
|
||||||
'uroco @99',
|
|
||||||
'totokoro',
|
|
||||||
'うし',
|
|
||||||
'kiritan',
|
|
||||||
'weepjp',
|
|
||||||
'Liaizon Wakest',
|
|
||||||
'Duponin',
|
|
||||||
'Blue',
|
|
||||||
'Naoki Hirayama',
|
|
||||||
'wara',
|
|
||||||
'Wataru Manji (manji0)',
|
|
||||||
'みなしま',
|
|
||||||
'kanoy',
|
|
||||||
'xianon',
|
|
||||||
'Denshi',
|
|
||||||
'Osushimaru',
|
|
||||||
'にょんへら',
|
|
||||||
'おのだい',
|
|
||||||
'Leni',
|
|
||||||
'oss',
|
|
||||||
'Weeble',
|
|
||||||
'蝉暮せせせ',
|
|
||||||
'ThatOneCalculator',
|
|
||||||
'pixeldesu',
|
|
||||||
];
|
|
||||||
|
|
||||||
let easterEggReady = false;
|
let easterEggReady = false;
|
||||||
let easterEggEmojis = $ref([]);
|
let easterEggEmojis = $ref([]);
|
||||||
|
|
|
@ -1,35 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader/></template>
|
<template #header><MkPageHeader /></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
<img
|
||||||
|
src="/static-assets/badges/info.png"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Info"
|
||||||
|
/>
|
||||||
<div>{{ i18n.ts.noNotes }}</div>
|
<div>{{ i18n.ts.noNotes }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items }">
|
<template #default="{ items }">
|
||||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
|
<XList
|
||||||
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
|
v-slot="{ item }"
|
||||||
|
:items="items"
|
||||||
|
:direction="'down'"
|
||||||
|
:no-gap="false"
|
||||||
|
:ad="false"
|
||||||
|
>
|
||||||
|
<XNote :key="item.id" :note="item.note" :class="$style.note" />
|
||||||
</XList>
|
</XList>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import XNote from '@/components/MkNote.vue';
|
import XNote from "@/components/MkNote.vue";
|
||||||
import XList from '@/components/MkDateSeparatedList.vue';
|
import XList from "@/components/MkDateSeparatedList.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'i/favorites' as const,
|
endpoint: "i/favorites" as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,7 +47,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.favorites,
|
title: i18n.ts.favorites,
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: "ph-bookmark-simple-bold ph-lg",
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="other">
|
<div class="other">
|
||||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-bold ph-lg ph-fw ph-lg"></i></button>
|
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-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.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-if="shareAvailable()" 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>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
|
@ -67,6 +67,7 @@ import { url } from '@/config';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { shareAvailable } from '@/scripts/share-available';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,14 +91,6 @@ function del(): void {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + * {
|
& + * {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
@ -222,7 +213,7 @@ function del(): void {
|
||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
|
|
||||||
> .balloon {
|
> .balloon {
|
||||||
$color: var(--messageBg);
|
$color: var(--X4);
|
||||||
background: $color;
|
background: $color;
|
||||||
|
|
||||||
&.noText {
|
&.noText {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,25 @@
|
||||||
<MkSpacer :content-max="800">
|
<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="footer">
|
||||||
|
<div><i class="ph-alarm-bold"/> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||||
|
<div v-if="page.createdAt != page.updatedAt"><i class="ph-alarm-bold"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||||
|
</div>
|
||||||
<div class="_block main">
|
<div class="_block main">
|
||||||
<!--
|
<div class="banner">
|
||||||
|
<div class="banner-image">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>{{ page.title }}</h1>
|
<h1>{{ page.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
-->
|
<div class="menu-actions">
|
||||||
<div class="banner">
|
<MkA v-tooltip="i18n.ts._pages.viewSource" :to="`/@${username}/pages/${pageName}/view-source`" class="menu _button"><i class="ph-code-bold ph-lg"/></MkA>
|
||||||
<img v-if="page.eyeCatchingImageId" :src="page.eyeCatchingImage.url"/>
|
<template v-if="$i && $i.id === page.userId">
|
||||||
|
<MkA v-tooltip="i18n.ts._pages.editPage" class="menu _button" :to="`/pages/edit/${page.id}`"><i class="ph-pencil-bold ph-lg"/></MkA>
|
||||||
|
<button v-if="$i.pinnedPageId === page.id" v-tooltip="i18n.ts.unpin" class="menu _button" @click="pin(false)"><i class="ph-push-pin-slash-bold ph-lg"/></button>
|
||||||
|
<button v-else v-tooltip="i18n.ts.pin" class="menu _button" @click="pin(true)"><i class="ph-push-pin-bold ph-lg"/></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<XPage :page="page"/>
|
<XPage :page="page"/>
|
||||||
|
@ -23,8 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="other">
|
<div class="other">
|
||||||
<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-if="shareAvailable()" 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"/>
|
||||||
|
@ -32,20 +42,17 @@
|
||||||
<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 class="links">
|
</div>
|
||||||
|
<!-- <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>
|
||||||
<template v-if="$i && $i.id === page.userId">
|
<template v-if="$i && $i.id === page.userId">
|
||||||
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
|
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
|
||||||
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
|
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
|
||||||
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
|
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<div><i class="ph-alarm-bold"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
|
||||||
<div v-if="page.createdAt != page.updatedAt"><i class="ph-alarm-bold"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
|
||||||
</div>
|
</div>
|
||||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||||
|
@ -74,6 +81,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkPagePreview from '@/components/MkPagePreview.vue';
|
import MkPagePreview from '@/components/MkPagePreview.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { shareAvailable } from '@/scripts/share-available';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
pageName: string;
|
pageName: string;
|
||||||
|
@ -81,6 +89,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let page = $ref(null);
|
let page = $ref(null);
|
||||||
|
let bgImg = $ref(null);
|
||||||
let error = $ref(null);
|
let error = $ref(null);
|
||||||
const otherPostsPagination = {
|
const otherPostsPagination = {
|
||||||
endpoint: 'users/pages' as const,
|
endpoint: 'users/pages' as const,
|
||||||
|
@ -98,11 +107,21 @@ function fetchPage() {
|
||||||
username: props.username,
|
username: props.username,
|
||||||
}).then(_page => {
|
}).then(_page => {
|
||||||
page = _page;
|
page = _page;
|
||||||
|
bgImg = getBgImg();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
error = err;
|
error = err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBgImg(): string {
|
||||||
|
if (page.eyeCatchingImage != null) {
|
||||||
|
return `url(${page.eyeCatchingImage.url})`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'linear-gradient(to bottom right, #31748f, #9ccfd8)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function share() {
|
function share() {
|
||||||
navigator.share({
|
navigator.share({
|
||||||
title: page.title ?? page.name,
|
title: page.title ?? page.name,
|
||||||
|
@ -118,7 +137,7 @@ function shareWithNote() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function like() {
|
function like() {
|
||||||
os.apiWithDialog('pages/like', {
|
os.api('pages/like', {
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
page.isLiked = true;
|
page.isLiked = true;
|
||||||
|
@ -180,35 +199,65 @@ definePageMetadata(computed(() => page ? {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
margin: 0rem !important;
|
||||||
|
|
||||||
|
> .banner-image {
|
||||||
|
// TODO: 良い感じのアスペクト比で表示
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-image: v-bind('bgImg');
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
> h1 {
|
> h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 8px #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .banner {
|
> .menu-actions {
|
||||||
margin: 0rem !important;
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 24px;
|
||||||
|
width: fit-content;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
left: 1rem;
|
||||||
|
|
||||||
> img {
|
> .menu {
|
||||||
// TODO: 良い感じのアスペクト比で表示
|
vertical-align: bottom;
|
||||||
display: block;
|
height: 31px;
|
||||||
width: 100%;
|
width: 31px;
|
||||||
height: 150px;
|
color: #fff;
|
||||||
object-fit: cover;
|
text-shadow: 0 0 8px #000;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .koudoku {
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
padding: 16px 0 0 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .actions {
|
> .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px 0 0 0;
|
padding: 16px 0;
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
|
||||||
> .like {
|
> .like {
|
||||||
|
@ -226,10 +275,8 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .other {
|
> .other {
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
padding: 8px;
|
padding: 2px;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -237,18 +284,15 @@ 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;
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
width: 52px;
|
width: 40px;
|
||||||
height: 52px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .name {
|
> .name {
|
||||||
|
@ -258,6 +302,8 @@ definePageMetadata(computed(() => page ? {
|
||||||
|
|
||||||
> .koudoku {
|
> .koudoku {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,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-if="$i == null" :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"/>
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import * as os from '@/os';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
import { noteActions } from '@/store';
|
import { noteActions } from '@/store';
|
||||||
|
import { shareAvailable } from '@/scripts/share-available';
|
||||||
|
|
||||||
export function getNoteMenu(props: {
|
export function getNoteMenu(props: {
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -220,23 +221,23 @@ export function getNoteMenu(props: {
|
||||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||||
},
|
},
|
||||||
} : undefined,
|
} : undefined,
|
||||||
{
|
shareAvailable() ? {
|
||||||
icon: 'ph-share-network-bold ph-lg',
|
icon: 'ph-share-network-bold ph-lg',
|
||||||
text: i18n.ts.share,
|
text: i18n.ts.share,
|
||||||
action: share,
|
action: share,
|
||||||
},
|
} : undefined,
|
||||||
instance.translatorAvailable ? {
|
instance.translatorAvailable ? {
|
||||||
icon: 'ph-translate-bold ph-lg',
|
icon: 'ph-translate-bold ph-lg',
|
||||||
text: i18n.ts.translate,
|
text: i18n.ts.translate,
|
||||||
action: translate,
|
action: translate,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
null,
|
null,
|
||||||
statePromise.then(state => state.isFavorited ? {
|
statePromise.then(state => state?.isFavorited ? {
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||||
text: i18n.ts.unfavorite,
|
text: i18n.ts.unfavorite,
|
||||||
action: () => toggleFavorite(false),
|
action: () => toggleFavorite(false),
|
||||||
} : {
|
} : {
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||||
text: i18n.ts.favorite,
|
text: i18n.ts.favorite,
|
||||||
action: () => toggleFavorite(true),
|
action: () => toggleFavorite(true),
|
||||||
}),
|
}),
|
||||||
|
|
3
packages/client/src/scripts/reduced-motion.ts
Normal file
3
packages/client/src/scripts/reduced-motion.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function reducedMotion(): boolean {
|
||||||
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
}
|
6
packages/client/src/scripts/share-available.ts
Normal file
6
packages/client/src/scripts/share-available.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export function shareAvailable(): boolean {
|
||||||
|
if (navigator.share) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -98,9 +98,9 @@ a {
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
// i {
|
||||||
transform: translateY(0.1em);
|
// transform: translateY(0.1em);
|
||||||
}
|
// }
|
||||||
|
|
||||||
textarea, input {
|
textarea, input {
|
||||||
tap-highlight-color: transparent;
|
tap-highlight-color: transparent;
|
||||||
|
@ -568,6 +568,22 @@ hr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media(prefers-reduced-motion) {
|
||||||
|
@keyframes tada {
|
||||||
|
from {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale3d(1.1, 1.1, 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
._anime_bounce {
|
._anime_bounce {
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
animation: bounce ease 0.7s;
|
animation: bounce ease 0.7s;
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
<XStreamIndicator/>
|
<XStreamIndicator/>
|
||||||
|
|
||||||
<div v-if="pendingApiRequestsCount > 0" id="wait"></div>
|
<!-- <div v-if="pendingApiRequestsCount > 0" id="wait"></div> -->
|
||||||
|
|
||||||
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -99,8 +99,8 @@ if ($i) {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
color: #ff0;
|
color: #f6c177;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: #6e6a86;
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -133,25 +133,25 @@ function getMenu() {
|
||||||
text: i18n.ts.move + '...',
|
text: i18n.ts.move + '...',
|
||||||
icon: 'ph-arrows-out-cardinal-bold ph-lg',
|
icon: 'ph-arrows-out-cardinal-bold ph-lg',
|
||||||
children: [{
|
children: [{
|
||||||
icon: 'ph--left-bold ph-lg',
|
icon: 'ph-caret-left-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapLeft,
|
text: i18n.ts._deck.swapLeft,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapLeftColumn(props.column.id);
|
swapLeftColumn(props.column.id);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
icon: 'ph--right-bold ph-lg',
|
icon: 'ph-caret-right-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapRight,
|
text: i18n.ts._deck.swapRight,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapRightColumn(props.column.id);
|
swapRightColumn(props.column.id);
|
||||||
},
|
},
|
||||||
}, props.isStacked ? {
|
}, props.isStacked ? {
|
||||||
icon: 'ph--up-bold ph-lg',
|
icon: 'ph-caret-up-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapUp,
|
text: i18n.ts._deck.swapUp,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapUpColumn(props.column.id);
|
swapUpColumn(props.column.id);
|
||||||
},
|
},
|
||||||
} : undefined, props.isStacked ? {
|
} : undefined, props.isStacked ? {
|
||||||
icon: 'ph--down-bold ph-lg',
|
icon: 'ph-caret-down-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapDown,
|
text: i18n.ts._deck.swapDown,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapDownColumn(props.column.id);
|
swapDownColumn(props.column.id);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -377,6 +377,10 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||||
|
|
||||||
> .button-wrapper {
|
> .button-wrapper {
|
||||||
|
|
||||||
|
> i {
|
||||||
|
transform: translateY(0.05em);
|
||||||
|
}
|
||||||
|
|
||||||
&.on {
|
&.on {
|
||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
patrons.json
Normal file
11
patrons.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"patrons": [
|
||||||
|
"@atomicpoet@vancity.social",
|
||||||
|
"@shoq@newsroom.social",
|
||||||
|
"@pikadude@erisly.social",
|
||||||
|
"@sage@stop.voring.me",
|
||||||
|
"@sky@therian.club",
|
||||||
|
"@panos@electricrequiem.com",
|
||||||
|
"@redhunt07@www.foxyhole.io"
|
||||||
|
]
|
||||||
|
}
|
10
push-docker.sh
Executable file
10
push-docker.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
sudo systemctl start docker.service
|
||||||
|
sudo docker rmi $(docker images -q)
|
||||||
|
sudo docker compose build
|
||||||
|
sudo docker tag thatonecalculator/calckey:latest thatonecalculator/calckey:$(git describe --tags --exact-match)
|
||||||
|
sudo docker images
|
||||||
|
echo "\nPress any key to continue\n"
|
||||||
|
read
|
||||||
|
sudo docker push thatonecalculator/calckey:$(git describe --tags --exact-match)
|
||||||
|
sudo docker push thatonecalculator/calckey:latest
|
||||||
|
sudo systemctl stop docker.service
|
Loading…
Reference in a new issue