1
0
mirror of https://github.com/MisskeyIO/misskey synced 2024-11-27 06:18:40 +09:00

Merge branch 'develop'

This commit is contained in:
syuilo 2021-02-07 18:23:23 +09:00
commit 49e6c2ed75
131 changed files with 2790 additions and 1662 deletions

View File

@ -1,7 +1,8 @@
<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
[![Misskey](/assets/about/banner.svg)](https://join.misskey.page/)
[![Misskey](/assets/title.png)](https://join.misskey.page/)
================================================================
<h1 align="center">Misskey</h1>
<div align="center">
[![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey)
[![Dependencies](https://img.shields.io/david/syuilo/misskey.svg?style=for-the-badge&logo=npm)](https://david-dm.org/syuilo/misskey)
@ -10,71 +11,52 @@
**A forever evolving, interplanetary microblogging platform.**
<p align="justify">
<a href="https://join.misskey.page/">Misskey</a> is a decentralized microblogging platform born on Earth.
Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms.
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://join.misskey.page/">Find an instance!</a>
</p>
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
</div>
---
![](https://ja.mstdn.wiki/images/e/ed/Deck.jpg)
:sparkles: Features
----------------------------------------------------------------
<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
<img src="/assets/about/post.png" align="left" height="200px"/>
<h3 align="left">Posting</h3>
<p align="justify">
<h3>Posting</h3>
<p>
Post your ideas, discussion topics, fun moments, or anything else you want to share! Misskey supports text, emoji, pictures, videos, and polls!
</p>
---
<img src="/assets/about/reaction.png" align="right" height="200px"/>
<h3 align="right">Reactions</h3>
<p align="justify">
<h3 >Reactions</h3>
<p>
Reactions are the simplest way to respond to others' posts. Simply pick a reaction emote from the list! Reactions on Misskey are much more expressive than other social media services which only allow “liking”.
</p>
---
<img src="/assets/about/ui.png" align="left" height="200px"/>
<h3 align="left">Interface</h3>
<p align="justify">
<h3>Interface</h3>
<p>
Customize the UI to your own tastes! No UI will work for everyone, so Misskey is completely customizable. Make Misskey *yours* by editing the style, adjusting timeline layouts, and placing widgets.
</p>
---
<img src="/assets/about/drive.png" align="right" width="300px"/>
<h3 align="right">Misskey Drive</h3>
<p align="justify">
<h3>Misskey Drive</h3>
<p>
Organize and store your files! Want to post a picture you have already uploaded? Wish you could organize your files into folders? Misskey Drive is a solution!
</p>
---
...and more! Experience Misskey with your own eyes at [misskey.io](https://misskey.io/) or join one of the [other instances](https://joinmisskey.github.io/) that are available.
To recive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse.
Screen shots
----------------------------------------------------------------
### Profile page
<img src="/assets/ss/user.jpg" width="500px"/>
### Explore users
<img src="/assets/ss/explore.jpg" width="500px"/>
:new: What's new
----------------------------------------------------------------
Please see the [Release notes](./CHANGELOG.md).
...and more!
:package: Create your own instance
----------------------------------------------------------------
@ -108,6 +90,10 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
</tr>
</table>
---
To receive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse.
:heart: Backers
----------------------------------------------------------------
<!-- PATREON_START -->

74
assets/about/banner.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
assets/banner.afdesign Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -32,6 +32,9 @@ addUser: "اضافة مستخدم"
favorite: "إضافة إلى المفضلة"
favorites: "المفضلات"
unfavorite: "إزالة من المفضلة"
favorited: "تمت الإضافة إلى المفضلة."
alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة."
cantFavorite: "تعذرت الإضافة إلى المفضلة."
pin: "دبّسها على الصفحة الشخصية"
unpin: "ألغ تثبيتها من ملفك الشخصي"
copyContent: "انسخ المحتوى"
@ -398,7 +401,6 @@ useCw: "إخفاء المحتوى"
themeEditor: "مصمم القوالب"
manage: "إدارة "
plugins: "الإضافات"
pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق منه فقط."
width: "العرض"
height: "الإرتفاع"
large: "كبير"
@ -423,6 +425,9 @@ metrics: "المقاييس"
public: "للعامة"
currentVersion: "الإصدار الحالي"
latestVersion: "آخر نسخة مستقرة"
usageAmount: "الإستخدام"
capacity: "السعة"
inUse: "مستخدم"
_mfm:
mention: "أشر الى"
quote: "اقتبس"

View File

@ -548,7 +548,6 @@ author: "Autor"
leaveConfirm: "Es gibt unspeicherte Änderungen. Möchtest du diese verwerfen?"
manage: "Verwaltung"
plugins: "Plugins"
pluginInstallWarn: "Installiere nur vertrauenswürdige Plugins."
deck: "Deck"
undeck: "Deck verlassen"
useBlurEffectForModal: "Weichzeichnungseffekt für Modals verwenden"
@ -692,6 +691,20 @@ deleteConfirm: "Wirklich löschen?"
invalidValue: "Ungültiger Wert."
registry: "Registry"
closeAccount: "Benutzerkonto schließen"
currentVersion: "Momentane Version"
latestVersion: "Neuste Version"
youAreRunningUpToDateClient: "Du verwendest die neuste Version deines Clients."
newVersionOfClientAvailable: "Eine neuere Version deines Clients ist verfügbar."
usageAmount: "Verwendung"
capacity: "Kapazität"
inUse: "Verwendet"
editCode: "Code bearbeiten"
apply: "Anwenden"
receiveAnnouncementFromInstance: "Benachrichtigungen von der Instanz empfangen"
_plugin:
install: "Plugins installieren"
installWarn: "Installiere bitte nur vertrauenswürdige Plugins."
manage: "Plugins verwalten"
_registry:
scope: "Scope"
key: "Schlüssel"
@ -1032,6 +1045,7 @@ _widgets:
onlineUsers: "Benutzer Online"
jobQueue: "Job-Warteschlange"
serverMetric: "Servermetriken"
aiscript: "AiScript-Konsole"
_cw:
hide: "Ausblenden"
show: "Mehr anzeigen"

View File

@ -548,7 +548,6 @@ author: "Author"
leaveConfirm: "There are unsaved changes. Do you want to discard them?"
manage: "Management"
plugins: "Plugins"
pluginInstallWarn: "Please do not install untrustworthy plugins."
deck: "Deck"
undeck: "Leave Deck"
useBlurEffectForModal: "Use blur effect for modals"
@ -692,6 +691,20 @@ deleteConfirm: "Really delete?"
invalidValue: "Invalid value."
registry: "Registry"
closeAccount: "Close account"
currentVersion: "Current version"
latestVersion: "Newest version"
youAreRunningUpToDateClient: "You are using the newest version of your client."
newVersionOfClientAvailable: "There is a newer version of your client available."
usageAmount: "Usage"
capacity: "Capacity"
inUse: "Used"
editCode: "Edit code"
apply: "Apply"
receiveAnnouncementFromInstance: "Receive notifications from the instance"
_plugin:
install: "Install plugins"
installWarn: "Please do not install untrustworthy plugins."
manage: "Manage plugins"
_registry:
scope: "Scope"
key: "Key"
@ -1032,6 +1045,7 @@ _widgets:
onlineUsers: "Online users"
jobQueue: "Job Queue"
serverMetric: "Server metrics"
aiscript: "AiScript console"
_cw:
hide: "Hide"
show: "Load more"

View File

@ -547,7 +547,6 @@ author: "Autor"
leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
manage: "Administrar"
plugins: "Plugins"
pluginInstallWarn: "Por favor no instale plugins que no son de confianza"
deck: "Deck"
undeck: "Quitar deck"
useBlurEffectForModal: "Usar efecto borroso en modales"

View File

@ -541,7 +541,6 @@ author: "Auteur·rice"
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer ?"
manage: "Gestion"
plugins: "Extensions"
pluginInstallWarn: "Ninstallez que des extensions provenant de sources de confiance."
deck: "Deck"
undeck: "Quitter le deck"
useBlurEffectForModal: "Utiliser un effet de flou pour les modals"
@ -623,13 +622,18 @@ notSet: "Non défini"
emailVerified: "Votre adresse e-mail a été vérifiée."
clips: "Clip"
experimentalFeatures: "Fonctionnalités expérimentales"
developer: "Développeur"
makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"."
makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"."
showGapBetweenNotesInTimeline: "Afficher un écart entre les notes sur la Timeline"
left: "Gauche"
center: "Centrer"
wide: "Large"
narrow: "Condensé"
showTitlebar: "Afficher la barre de titre"
clearCache: "Vider le cache"
onlineUsersCount: "{n} utilisateur(s) en ligne"
nUsers: "{n} utilisateur·rice·s"
nNotes: "{n} Notes"
sendErrorReports: "Envoyer les rapports derreur"
sendErrorReportsDescription: "Lorsqu'il est activé, des informations détaillées sur les erreurs sont partagées avec Misskey lorsqu'un problème survient, ce qui contribue à améliorer la qualité de Misskey."
@ -637,12 +641,18 @@ myTheme: "Mes thèmes"
backgroundColor: "Arrière-plan"
textColor: "Texte"
saveAs: "Enregistrer sous ..."
advanced: "Avancé"
value: "Valeur"
saveConfirm: "Voulez-vous sauvegarder les modifications?"
closeAccount: "Fermer le compte"
usageAmount: "Utilisation"
capacity: "Capacité "
inUse: "utilisé"
_registry:
key: "Clé "
keys: "Clé "
domain: "Domaine"
createKey: "Créer une clé"
_aboutMisskey:
about: "Misskey est un logiciel libre et ouvert, développé par syuilo depuis 2014."
contributors: "Principaux contributeurs"
@ -651,15 +661,22 @@ _aboutMisskey:
translation: "Traduire Misskey"
donate: "Soutenir Misskey"
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰"
patrons: "Contributeurs"
_nsfw:
respect: "Cacher les médias sensibles"
ignore: "Afficher les médias sensibles"
force: "Cacher tous les médias"
_mfm:
mention: "Mentionner"
hashtag: "Hashtags"
link: "Lien"
bold: "Gras"
center: "Centrée"
quote: "Citer"
quoteDescription: "Affiche le contenu sous forme de citation."
emoji: "Émojis personnalisés"
search: "Rechercher"
flip: "Inverser"
font: "Police de caractères"
_reversi:
total: "Total"

View File

@ -43,7 +43,7 @@ const primaries = {
'zh': 'CN',
};
const locales = languages.reduce((a, c) => (a[c] = yaml.safeLoad(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8')) || {}, a), {});
const locales = languages.reduce((a, c) => (a[c] = yaml.load(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8')) || {}, a), {});
module.exports = Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => {

View File

@ -1 +1,67 @@
---
_lang_: "Italiano"
search: "Cerca"
notifications: "Notifiche"
username: "Nome utente"
password: "Password"
ok: "OK"
instance: "Istanza"
settings: "Impostazioni"
basicSettings: "Impostazioni generali"
otherSettings: "Altre impostazioni"
profile: "Profilo"
timeline: "Timeline"
login: "Login"
logout: "Logout"
signup: "Iscriviti"
save: "Salva"
users: "Utente"
delete: "Elimina"
reply: "Rispondi"
mention: "Menzioni"
import: "Importa"
note: "Note"
notes: "Notes"
error: "Errore"
somethingHappened: "Qualcosa è andato storto."
retry: "Riprova"
privacy: "Privacy"
quote: "Cita Note"
mute: "Silenzia"
block: "Blocca"
suspend: "Sospendi"
instances: "Istanza"
blocked: "Bloccati"
remove: "Elimina"
saved: "Salvato"
birthday: "Compleanno"
registeredDate: "Iscrizione a.."
location: "Posizione"
smtpUser: "Nome utente"
smtpPass: "Password"
_mfm:
mention: "Menzioni"
quote: "Cita Note"
search: "Cerca"
_theme:
keys:
mention: "Menzioni"
_sfx:
note: "Notes"
notification: "Notifiche"
_widgets:
notifications: "Notifiche"
timeline: "Timeline"
_profile:
username: "Nome utente"
_exportOrImport:
muteList: "Silenzia"
blockingList: "Blocca"
_notification:
_types:
mention: "Menzioni"
quote: "Cita Note"
_deck:
_columns:
notifications: "Notifiche"
tl: "Timeline"

View File

@ -548,7 +548,6 @@ author: "作者"
leaveConfirm: "未保存の変更があります。破棄しますか?"
manage: "管理"
plugins: "プラグイン"
pluginInstallWarn: "信頼できないプラグインはインストールしないでください。"
deck: "デッキ"
undeck: "デッキ解除"
useBlurEffectForModal: "モーダルにぼかし効果を使用"
@ -699,6 +698,14 @@ newVersionOfClientAvailable: "新しいバージョンのクライアントが
usageAmount: "使用量"
capacity: "容量"
inUse: "使用中"
editCode: "コードを編集"
apply: "適用"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
_plugin:
install: "プラグインのインストール"
installWarn: "信頼できないプラグインはインストールしないでください。"
manage: "プラグインの管理"
_registry:
scope: "スコープ"
@ -1061,6 +1068,7 @@ _widgets:
onlineUsers: "オンラインユーザー"
jobQueue: "ジョブキュー"
serverMetric: "サーバーメトリクス"
aiscript: "AiScriptコンソール"
_cw:
hide: "隠す"

View File

@ -1,15 +1,15 @@
---
_lang_: "日本語 (関西弁)"
introMisskey: "ようこそMisskeyってのは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作成し、いま起こっとることを共有したり、あんたんこととか皆に伝えていこう📡\n「リアクション」機能で、皆のートに素はよ反応を追加することもできるんやで✌\n新しい世界を探検してみらん🚀"
introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀"
monthAndDay: "{month}月 {day}日"
search: "探す"
notifications: "通知"
username: "ユーザー名"
password: "パスワード"
fetchingAsApObject: "今ちと連合に照会しとるで"
ok: "おっけー"
ok: "OKや"
gotIt: "ほい"
cancel: "やめとく"
cancel: "やめとく"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenote"
noNotes: "ノートはあらへん"
@ -18,16 +18,16 @@ instance: "インスタンス"
settings: "設定"
basicSettings: "基本設定"
otherSettings: "その他の設定"
openInWindow: "ウィンドウで開いてや"
openInWindow: "ウィンドウで開くで"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はあらへん"
noAccountDescription: "自己紹介食ってもた"
login: "ログイン"
loggingIn: "ログインしよるで"
logout: "ログアウト"
signup: "新規登録"
uploading: "アップロードしるで"
save: "とっとく"
uploading: "アップロードしるで"
save: "保存"
users: "ユーザー"
addUser: "ユーザーを追加や"
favorite: "お気に入り"
@ -42,14 +42,14 @@ copyContent: "内容をコピー"
copyLink: "リンクをコピー"
delete: "ほかす"
deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このノートをほかしてもっかい直すこのートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん"
deleteAndEditConfirm: "このノートをほかして書き直すんかこのートへのリアクション、Renote、返信も全部消えてまうで。"
addToList: "リストに入れたる"
sendMessage: "メッセージを送る"
copyUsername: "ユーザー名をコピー"
searchUser: "ユーザーを検索"
reply: "返"
loadMore: "もっとあるやろ"
showMore: "もっとあるやろ"
reply: "返"
loadMore: "まだまだあるで"
showMore: "まだまだあるで"
youGotNewFollower: "フォローされたで"
receiveFollowRequest: "フォローリクエストされたで"
followRequestAccepted: "フォローが承認されたで"
@ -80,12 +80,12 @@ retry: "もっぺんやる?"
pageLoadError: "ページの読み込みに失敗してしもうたで…"
pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか?"
enterListName: "リスト名を入れてや"
privacy: "プライバシーってなんぞや?"
makeFollowManuallyApprove: "他人からのフォローは自分が決める"
privacy: "プライバシー"
makeFollowManuallyApprove: "ええって言わなフォローできへんようにする"
defaultNoteVisibility: "もとからの公開範囲"
follow: "フォロー"
followRequest: "フォローを頼む"
followRequests: "フォローを頼む"
followRequests: "フォロー申請"
unfollow: "フォローやめる"
followRequestPending: "フォロー許してくれるん待っとる"
enterEmoji: "絵文字を入れてや"
@ -93,18 +93,18 @@ renote: "Renote"
unrenote: "Renoteやめる"
renoted: "Renoteしたで。"
cantRenote: "この投稿はRenoteできへんらしい。"
cantReRenote: "すまん、今このRenoteにRenoteはできへんのや。"
cantReRenote: "Renote自体はRenoteできへんで。"
quote: "引用"
pinnedNote: "ピン留めされとるノート"
you: "あんた"
clickToShow: "押したら見えるようになるで"
clickToShow: "押したら見えるで"
sensitive: "ちょっとアカンやつやで"
add: "増やす"
reaction: "リアクション"
reactionSettingDescription: "リアクションピッカーに出しとくリアクションを選んでや。"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押すと追加できるで。"
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
rememberNoteVisibility: "公開範囲覚えといて"
attachCancel: "やっぱ添付やめてくれん?"
attachCancel: "のっけるのやめる"
markAsSensitive: "ちょっとこれはアカン"
unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや"
@ -130,12 +130,12 @@ emojiUrl: "絵文字画像URL"
addEmoji: "絵文字を追加"
settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになってしまうんやで? サーバーのストレージは節約できるんやけど、かわりにサムネイルが作られんくなるから通信量が増えるで?"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
flagAsBot: "Botやで"
flagAsBotDescription: "もしこのアカウントがプログラムによって運用されるんやったら、このフラグをオンにしてたのむで。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったもんになるんやで。"
flagAsCat: "Catやで"
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストには勝手に許可しとくで。"
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
addAcount: "アカウント追加"
loginFailed: "ログインに失敗してしもうた…"
showOnRemote: "リモートで見る"
@ -180,7 +180,7 @@ clearQueue: "キューにさいなら"
clearQueueConfirmTitle: "キューをクリアしまっか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。"
clearCachedFiles: "キャッシュにさいなら"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルを全部削除しまっか?"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "インスタンスブロック"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定してな。ブロックされてもうたインスタンスとはもう金輪際やり取りできひんくなるで。"
muteAndBlock: "ミュートとブロック"
@ -213,18 +213,18 @@ retypedNotMatch: "そやないねん。"
currentPassword: "今のパスワード"
newPassword: "今度のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
attachFile: "ファイルくっつけて"
more: "他ないんか"
attachFile: "ファイルのっける"
more: "他のやつ"
featured: "ハイライト"
usernameOrUserId: "ユーザー名かユーザーID"
noSuchUser: "ユーザーが見つからへんで"
lookup: "見てきて"
announcements: "これ知っといてな"
announcements: "お知らせ"
imageUrl: "画像URL"
remove: "ほかす"
removed: "削除したで!"
removeAreYouSure: "「{x}」はなおしてしもてええか?"
deleteAreYouSure: "「{x}」はなおしてしもてええか?"
removeAreYouSure: "「{x}」はほかしてええか?"
deleteAreYouSure: "「{x}」はほかしてええか?"
resetAreYouSure: "リセットしてええん?"
saved: "保存したで!"
messaging: "チャット"
@ -245,7 +245,7 @@ agreeTo: "{0}はええで"
tos: "利用規約"
start: "始める"
home: "ホーム"
remoteUserCaution: "リモートユーザーやから、ちゃんとした情報とちゃうで。"
remoteUserCaution: "リモートユーザーやから、足りひん情報あるかもしれへん。"
activity: "アクティビティ"
images: "画像"
birthday: "生まれた日"
@ -259,7 +259,7 @@ light: "ライト"
dark: "ダーク"
lightThemes: "デイゲーム"
darkThemes: "ナイトゲーム"
syncDeviceDarkMode: "試合開始時間はデバイスのダークモードと一緒や"
syncDeviceDarkMode: "デバイスのダークモードと一緒にする"
drive: "ドライブ"
fileName: "ファイル名"
selectFile: "ファイル選んでや"
@ -283,9 +283,9 @@ copyUrl: "URLをコピー"
rename: "名前を変えるで"
avatar: "アイコン"
banner: "バナー"
nsfw: "ちょっとアカンやつやで"
whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき"
disconnectedFromServer: "サーバーが機嫌悪いねん"
nsfw: "閲覧注意"
whenServerDisconnected: "サーバーとの接続が切れたとき"
disconnectedFromServer: "サーバーとの通信が切れたで"
reload: "リロード"
doNothing: "何もせんとく"
reloadConfirm: "リロードしてええか?"
@ -306,7 +306,7 @@ dayX: "{day}日"
monthX: "{month}月"
yearX: "{year}年"
pages: "ページ"
integration: "つないで"
integration: "連携"
connectSerice: "つなげる"
disconnectSerice: "切ってまう"
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
@ -316,7 +316,7 @@ registration: "登録"
enableRegistration: "一見さんでも誰でもいらっしゃ~い"
invite: "来てや"
proxyRemoteFiles: "リモートのファイルをプロキシする"
proxyRemoteFilesDescription: "この設定を入れると、保存しとらんかったり、お腹いっぱいになってしもたせいで保存できんかったリモートファイルをローカルでプロキシして、サムネイル作ってもらうことができるで。サーバーの腹具合には影響せんけどな。"
proxyRemoteFilesDescription: "この設定を有効にしたら、保存してなかったり容量が足らんくて消されたリモートファイルをローカルでプロキシして、サムネイルを作るようになるで。サーバーの容量には関係ないで。"
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
inMb: "メガバイト単位"
@ -380,11 +380,11 @@ unregister: "登録やめる"
passwordLessLogin: "パスワード無くてもログインできるようにする"
resetPassword: "パスワードをリセット"
newPasswordIs: "今度のパスワードは「{password}」や"
reduceUiAnimation: "UIの動きやアニメーションを減らしてくれや。"
reduceUiAnimation: "UIの動きやアニメーションを減ら"
share: "わけわけ"
notFound: "見つからへんね"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
uploadFolder: "とりあえずここへアップロード"
uploadFolder: "とりあえずアップロードしたやつ置いとく所"
cacheClear: "キャッシュをほかす"
markAsReadAllNotifications: "通知はもう全て読んだわっ"
markAsReadAllUnreadNotes: "投稿は全て読んだわっ"
@ -423,9 +423,9 @@ checking: "確認しとるで"
available: "利用できる\n"
unavailable: "利用できん"
usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで"
tooShort: "短すぎやろ!"
tooShort: "短すぎやろ"
tooLong: "長すぎやろ!"
weakPassword: "いパスワード"
weakPassword: "へぼいパスワード"
normalPassword: "普通のパスワード"
strongPassword: "ええ感じのパスワード"
passwordMatched: "よし!一致や!"
@ -439,6 +439,7 @@ useOsNativeEmojis: "OSネイティブの絵文字を使う"
youHaveNoGroups: "グループがあらへんねぇ。"
noHistory: "履歴はあらへんねぇ。"
signinHistory: "ログイン履歴"
disableAnimatedMfm: "動きがやかましいMFMを止める"
doing: "やっとるがな"
category: "カテゴリ"
tags: "タグ"
@ -452,15 +453,28 @@ openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード"
local: "ローカル"
remote: "リモート"
scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Misskeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
leaveConfirm: "未保存の変更があるで!ほかしてええか?"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
smtpHost: "ホスト"
smtpUser: "ユーザー名"
smtpPass: "パスワード"
clearCache: "キャッシュにさいなら"
emailVerified: "メールアドレスは確認されたで"
pageLikesCount: "Pageにええやんと思った数"
pageLikedCount: "Pageにええやんと思ってくれた数"
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
clearCache: "キャッシュをほかす"
onlineUsersCount: "{n}人が起きとるで"
sendErrorReportsDescription: "オンにしたら、なんか変なことが起きたときにエラーの詳細がMisskeyに共有されて、ソフトウェアの品質向上に役立てられるんや。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれるで。"
youAreRunningUpToDateClient: "今使ってるクライアントが最新やで!"
newVersionOfClientAvailable: "新しいバージョンのクライアントが使えるで。"
_mfm:
mention: "メンション"
quote: "引用"
emoji: "カスタム絵文字"
search: "探す"
_channel:
notesCount: "{n}こ投稿があるで"
_sidebar:
icon: "アイコン"
_theme:
@ -472,7 +486,7 @@ _sfx:
notification: "通知"
chat: "チャット"
_ago:
unknown: ""
unknown: "わからん"
future: "未来"
justNow: "たった今"
secondsAgo: "{n}秒前"
@ -487,8 +501,13 @@ _time:
minute: "分"
hour: "時間"
day: "日"
_tutorial:
step3_1: "プロフィール設定はええ感じにできたか?"
_2fa:
alreadyRegistered: "もう設定終わっとるわ"
alreadyRegistered: "もう設定終わっとるわ。"
_permissions:
"read:page-likes": "ページのええやんを見る"
"write:page-likes": "ページのええやんを操作する"
_auth:
permissionAsk: "このアプリは次の権限を要求しとるで"
_antennaSources:
@ -501,7 +520,7 @@ _widgets:
federation: "連合"
jobQueue: "ジョブキュー"
_cw:
show: "もっとあるやろ"
show: "続き見して"
_poll:
noMore: "これ以上追加でけへん"
deadlineTime: "時間"
@ -521,11 +540,15 @@ _exportOrImport:
_timelines:
home: "ホーム"
_rooms:
leaveConfirm: "未保存の変更があるけど、移動してええか?"
_roomType:
default: "デフォルト"
_furnitures:
monitor: "モニター"
_pages:
like: "ええやん"
unlike: "良くないわ"
liked: "ええと思ったページ"
blocks:
image: "画像"
script:
@ -548,6 +571,8 @@ _pages:
array: "リスト"
_notification:
youWereFollowed: "フォローされたで"
youReceivedFollowRequest: "フォロー許可してほしいみたいやな"
yourFollowRequestAccepted: "フォローさせてもろたで"
youWereInvitedToGroup: "グループに招待されとるで"
_types:
follow: "フォロー"
@ -555,6 +580,8 @@ _notification:
renote: "Renote"
quote: "引用"
reaction: "リアクション"
receiveFollowRequest: "フォロー許可してほしいみたいやで"
followRequestAccepted: "フォローが受理されたで"
_deck:
_columns:
notifications: "通知"

View File

@ -547,7 +547,6 @@ author: "작성자"
leaveConfirm: "저장하지 않은 변경사항이 있습니다. 취소하시겠습니까?"
manage: "관리"
plugins: "플러그인"
pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
deck: "덱"
undeck: "덱 해제"
width: "폭"

View File

@ -522,7 +522,6 @@ author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie"
plugins: "Wtyczki"
pluginInstallWarn: "Nie instaluj niezaufanych wtyczek."
deck: "Tablica"
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"

View File

@ -1,5 +1,6 @@
---
_lang_: "Русский"
headlineMisskey: "Сеть, сплетённая из заметок"
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
monthAndDay: "{day}.{month}"
search: "Поиск"
@ -449,7 +450,7 @@ doing: "В процессе"
category: "Категория"
tags: "Метки"
docSource: "Источник документа"
createAccount: "Новый аккаунт"
createAccount: "Новая учётная запись"
existingAcount: "Уже существующий"
regenerate: "Создать повторно"
fontSize: "Размер шрифта"
@ -547,7 +548,6 @@ author: "Автор"
leaveConfirm: "Вы не сохранили изменения. Хотите выйти и потерять их?"
manage: "Управление"
plugins: "Расширения"
pluginInstallWarn: "Пожалуста, не устанавливайте расширения, которым не доверяете"
deck: "Пульт"
undeck: "Покинуть пульт"
useBlurEffectForModal: "Размывка под формой поверх всего"
@ -562,7 +562,7 @@ permission: "Разрешения"
enableAll: "Включить все"
disableAll: "Выключить всё"
tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Этот плагин сможет пользоваться разрешениями, установленными здесь."
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления"
edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
@ -668,19 +668,49 @@ showGapBetweenNotesInTimeline: "Показывать разделитель ме
duplicate: "Дубликат"
left: "Влево"
center: "По центру"
wide: "Толстый"
narrow: "Тонкий"
reloadToApplySetting: "Это настройка вступает в силу при загрузке страницы. Перезагрузить сейчас?"
showTitlebar: "Показать заголовок"
clearCache: "Очистить кэш"
onlineUsersCount: "Пользователей сейчас в сети: {n}"
nUsers: "Пользователей: {n}"
nNotes: "Заметок: {n}"
sendErrorReports: "Посылать отчёты о сбоях"
sendErrorReportsDescription: "Если включено, когда возникнет какая-нибудь техническая проблема, подробные сведения об этом будут отправлены разработчикам Misskey. Это очень помогает делать программу лучше. В отчёты попадают тип и версия ОС, браузера, журнал действий (что привело к сбою) и тому подобное."
myTheme: "Личная тема"
backgroundColor: "Фон"
accentColor: "Акцент"
textColor: "Текст"
saveAs: "Сохранить под названием…"
advanced: "Для продвинутых"
value: "Значения"
updatedAt: "Обновлено"
saveConfirm: "Сохранить изменения?"
deleteConfirm: "Удалить?"
invalidValue: "Недопустимое значение."
registry: "Реестр"
closeAccount: "Закрыть учётную запись"
currentVersion: "Используемая версия"
latestVersion: "Самая свежая версия"
youAreRunningUpToDateClient: "У вас самая свежая версия клиента."
newVersionOfClientAvailable: "Доступна более свежая версия клиента."
usageAmount: "Использовано"
capacity: "Ёмкость"
inUse: "Занято"
editCode: "Редактировать исходный текст"
apply: "Применить"
receiveAnnouncementFromInstance: "Получать оповещения с инстанса"
_plugin:
install: "Установка расширений"
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями"
_registry:
scope: "Область"
key: "Ключ"
keys: "Ключ"
domain: "Домен"
createKey: "Новый ключ"
_aboutMisskey:
about: "Misskey — программа с открытым исходным кодом, которую разрабатывает syuilo с 2014 года."
contributors: "Основные соавторы"
@ -696,7 +726,7 @@ _nsfw:
force: "Скрывать вообще все файлы"
_mfm:
cheatSheet: "Подсказка по разметке MFM"
intro: "MFM — язык оформления текста, придуманный специально для Misskey, который здесь можно много где использовать. На этой странице собраны и кратко изложены способы его применения."
intro: "MFM — язык оформления текста, который придуман специально для Misskey и готов для применения во многих местах. На этой странице собраны и кратко изложены способы его использовать."
dummy: "Misskey расширяет границы Федиверса."
mention: "Упоминание"
mentionDescription: "При помощи знака «собака» перед именем можно упомянуть какого-нибудь пользователя."
@ -742,7 +772,16 @@ _mfm:
twitchDescription: "Заставляет трястись как одержимого"
spin: "Вращение"
spinDescription: "Так можно крутить содержимое в разных направлениях."
x2: "Крупный шрифт"
x2Description: "Увеличивает содержимое."
x3: "Ещё крупнее"
x3Description: "Сильнее увеличивает содержимое."
x4: "Совсем крупно"
x4Description: "Увеличивает содержимое совсем сильно."
blur: "Размытие"
blurDescription: "Размывает текст до нечитаемости, будто его поместили за матовое стекло. Наведение указателя мыши на размытый текст возвращает чёткость."
font: "Шрифт"
fontDescription: "Так можно писать произвольным шрифтом."
_reversi:
reversi: "Реверси"
gameSettings: "Настройки игры"
@ -1005,6 +1044,8 @@ _widgets:
button: "Кнопка"
onlineUsers: "Пользователи сейчас с сети"
jobQueue: "Очередь заданий"
serverMetric: "Показатели сервера"
aiscript: "Консоль AiScript"
_cw:
hide: "Спрятать"
show: "Показать еще"
@ -1496,6 +1537,7 @@ _deck:
swapDown: "Переставить ниже"
stackLeft: "В столбик влево"
popRight: "Из столбика вправо"
profile: "Профиль"
_columns:
main: "Основная"
widgets: "Виджеты"

View File

@ -545,7 +545,6 @@ author: "Автор"
leaveConfirm: "Зміни не збережені. Ви дійсно хочете скасувати зміни?"
manage: "Управління"
plugins: "Плагіни"
pluginInstallWarn: "Будь ласка не встановлюйте плагінів яким ви не довіряєте."
deck: "Дек"
undeck: "Залишити Дек"
useBlurEffectForModal: "Ефект розмиття під модальними діалогами"

View File

@ -13,7 +13,7 @@ gotIt: "我明白了"
cancel: "取消"
enterUsername: "输入用户名"
renotedBy: "{user} 转发了"
noNotes: "没有帖"
noNotes: "没有帖"
noNotifications: "无通知"
instance: "实例"
settings: "设置"
@ -99,14 +99,14 @@ quote: "引用"
pinnedNote: "已置顶的帖子"
you: "您"
clickToShow: "点击以显示"
sensitive: "阅读注意"
sensitive: "敏感内容"
add: "添加"
reaction: "回应"
reactionSettingDescription: "选择您想要置顶的回应。"
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
rememberNoteVisibility: "记录公开范围"
attachCancel: "删除附件"
markAsSensitive: "阅读注意"
markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "请输入文件名"
mute: "屏蔽"
@ -193,7 +193,7 @@ noteDeleteConfirm: "要删除该帖子吗?"
pinLimitExceeded: "无法置顶更多了"
intro: "Misskey的部署结束啦填写管理员账号吧"
done: "完成"
processing: "处理"
processing: "正在处理"
preview: "预览"
default: "默认"
noCustomEmojis: "没有自定义表情符号"
@ -284,7 +284,7 @@ copyUrl: "复制链接"
rename: "重命名"
avatar: "头像"
banner: "Banner"
nsfw: "阅读注意"
nsfw: "敏感内容"
whenServerDisconnected: "与服务器连接中断时"
disconnectedFromServer: "已从服务器断开连接"
reload: "重新加载"
@ -472,7 +472,7 @@ hideThisNote: "隐藏这条帖子"
showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
objectStorage: "对象存储"
useObjectStorage: "使用对象存储"
objectStorageBaseUrl: "基本网址"
objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "URL前缀用于构造URL到对象媒体的引用如果您使用的是CDN或反向代理请指定其URL否则请根据您使用的服务指定可公开访问的地址。例如“https://<bucket>.s3.amazonaws.com”用于AWS S3“https://storage.googleapis.com/<bucket>”用于GCS"
objectStorageBucket: "存储桶"
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
@ -526,7 +526,7 @@ userSuspended: "该用户已被冻结。"
userSilenced: "该用户已被禁言。"
sidebar: "侧边栏"
divider: "分割线"
addItem: "添加项"
addItem: "添加项"
rooms: "房间"
relays: "中继"
addRelay: "添加中继"
@ -548,7 +548,6 @@ author: "作者"
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
manage: "管理"
plugins: "插件"
pluginInstallWarn: "请不要安装不明来源的插件"
deck: "Deck"
undeck: "取消Deck"
useBlurEffectForModal: "模态框使用模糊效果"
@ -648,7 +647,7 @@ driveUsage: "磁盘空间用量"
noCrawle: "拒绝搜索器的索引"
noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。"
lockedAccountInfo: "即使通过了关注请求,只要您不将帖子可见范围设置成“关注者”,任何人都可以看到您的帖子。"
alwaysMarkSensitive: "浏览默认媒体文件时请谨慎"
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
disableShowingAnimatedImages: "不播放动画"
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
@ -692,6 +691,20 @@ deleteConfirm: "确定删除?"
invalidValue: "无效值。"
registry: "注册表"
closeAccount: "关闭账户"
currentVersion: "当前版本"
latestVersion: "最新版本"
youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。"
newVersionOfClientAvailable: "新版本的客户端可用。"
usageAmount: "使用量"
capacity: "容量"
inUse: "已使用"
editCode: "编辑代码"
apply: "应用"
receiveAnnouncementFromInstance: "从实例接收通知"
_plugin:
install: "安装插件"
installWarn: "请不要安装不可信的插件。"
manage: "管理插件..."
_registry:
scope: "范围"
key: "主要"
@ -708,8 +721,8 @@ _aboutMisskey:
morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰"
patrons: "支持者"
_nsfw:
respect: "隐藏NSFW内容"
ignore: "不隐藏NSFW内容"
respect: "隐藏敏感内容"
ignore: "不隐藏敏感内容"
force: "总是隐藏内容"
_mfm:
cheatSheet: "MFM代码速查表"
@ -893,8 +906,8 @@ _theme:
cwBg: "CW 按钮背景"
cwFg: "CW 按钮文本"
cwHoverBg: "CW 按钮背景(悬停)"
toastBg: "吐司提示背景"
toastFg: "吐司提示文本"
toastBg: "吐司通知背景"
toastFg: "吐司通知文本"
buttonBg: "按钮背景"
buttonHoverBg: "按钮背景(悬停)"
inputBorder: "输入框边框"
@ -959,7 +972,7 @@ _2fa:
alreadyRegistered: "此设备已被注册"
registerDevice: "注册设备"
registerKey: "注册密钥"
step1: "首先,在您的设备上安装二步验证应用程序,例如{a}或{b}。"
step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。"
step2: "然后,扫描屏幕上显示的二维码。"
step3: "输入您的应用提供的动态口令以完成设置。"
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
@ -1032,6 +1045,7 @@ _widgets:
onlineUsers: "在线用户"
jobQueue: "作业队列"
serverMetric: "服务器指标"
aiscript: "AiScript控制台"
_cw:
hide: "隐藏"
show: "查看更多"

View File

@ -1,7 +1,7 @@
---
_lang_: "繁體中文"
headlineMisskey: "文連繫網絡"
introMisskey: "歡迎! Misskey是一個開源且去中心化的社群網絡。\n通過「文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能對大家的文表達情感!👍\n一起來探索這個新的世界吧🚀"
headlineMisskey: "文連繫網絡"
introMisskey: "歡迎! Misskey是一個開源且去中心化的社群網絡。\n通過「文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能對大家的文表達情感!👍\n一起來探索這個新的世界吧🚀"
monthAndDay: "{month}月 {day}日"
search: "搜尋"
notifications: "通知"
@ -13,7 +13,7 @@ gotIt: "知道了"
cancel: "取消"
enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉發了"
noNotes: "文不可用。"
noNotes: "文不可用。"
noNotifications: "沒有通知"
instance: "實例"
settings: "設定"
@ -31,19 +31,19 @@ uploading: "上傳中"
save: "儲存"
users: "使用者"
addUser: "新增使用者"
favorite: "收藏"
favorites: "已收藏"
unfavorite: "取消收藏"
favorited: "已添加至收藏夾"
alreadyFavorited: "已經有添加入收藏夾過了"
cantFavorite: "無法添加至收藏夾"
favorite: "我的最愛"
favorites: "我的最愛"
unfavorite: "從我的最愛中移除"
favorited: "已添加至我的最愛"
alreadyFavorited: "我的最愛中已存在。"
cantFavorite: "無法加入至我的最愛。"
pin: "置頂"
unpin: "取消置頂"
copyContent: "複製內容"
copyLink: "複製連結"
delete: "刪除"
deleteAndEdit: "刪除並編輯"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此文的所有情感、轉發和回覆也將會消失。"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此文的所有情感、轉發和回覆也將會消失。"
addToList: "新增至清單"
sendMessage: "發送訊息"
copyUsername: "複製用戶名"
@ -62,14 +62,14 @@ import: "匯入"
export: "匯出"
files: "檔案"
download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的文也會跟著消失。\n"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的文也會跟著消失。\n"
unfollowConfirm: "確定要取消追隨{name}嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
importRequested: "已請求匯入。這可能會花一點時間"
lists: "清單"
noLists: "你沒有任何清單"
note: "文"
notes: "文"
note: "文"
notes: "文"
following: "追隨中"
followers: "追隨者"
followsYou: "追隨你的人"
@ -90,13 +90,13 @@ followRequests: "追隨請求"
unfollow: "取消追隨"
followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉"
unrenote: "取消轉"
renote: "轉"
unrenote: "取消轉"
renoted: "轉發成功"
cantRenote: "無法轉發此文。"
cantRenote: "無法轉發此文。"
cantReRenote: "無法轉發之前已經轉發過的內容"
quote: "引用"
pinnedNote: "已置頂的文"
pinnedNote: "已置頂的文"
you: "您"
clickToShow: "按一下以顯示"
sensitive: "敏感內容"
@ -104,7 +104,7 @@ add: "新增"
reaction: "情感"
reactionSettingDescription: "置頂「反應」表情符號\n"
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
rememberNoteVisibility: "記住文可見性"
rememberNoteVisibility: "記住文可見性"
attachCancel: "移除附件"
markAsSensitive: "標記為敏感內容"
unmarkAsSensitive: "取消標記為敏感內容"
@ -188,8 +188,8 @@ mutedUsers: "已靜音用戶"
blockedUsers: "已封鎖用戶"
noUsers: "沒有任何使用者"
editProfile: "編輯個人檔案"
noteDeleteConfirm: "確定刪除此文嗎?"
pinLimitExceeded: "不能置頂更多文了"
noteDeleteConfirm: "確定刪除此文嗎?"
pinLimitExceeded: "不能置頂更多文了"
intro: "Misskey 部署完成!請開設管理員帳號!"
done: "完成"
processing: "處理中"
@ -327,7 +327,8 @@ pinnedUsers: "置頂用戶"
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的用戶。"
pinnedPages: "釘選頁面"
pinnedPagesDescription: "輸入要固定至實例首頁的頁面路徑,以換行符分隔。"
pinnedNotes: "已置頂的箋文"
pinnedClipId: "置頂的摘錄ID"
pinnedNotes: "已置頂的貼文"
hcaptcha: "hCaptcha"
enableHcaptcha: "啟用 hCaptcha"
hcaptchaSiteKey: "網站金鑰"
@ -344,15 +345,15 @@ antennaSource: "接收來源"
antennaKeywords: "包含關鍵字"
antennaExcludeKeywords: "排除關鍵字"
antennaKeywordsDescription: "用空格分隔指定AND、用換行符分隔指定OR"
notifyAntenna: "通知有新文"
withFileAntenna: "僅帶有附件的文"
notifyAntenna: "通知有新文"
withFileAntenna: "僅帶有附件的文"
serviceworker: "ServiceWorker"
enableServiceworker: "開啟 ServiceWorker"
antennaUsersDescription: "指定用換行符分隔的用戶名"
caseSensitive: "區分大小寫"
withReplies: "包含回覆"
connectedTo: "您的帳號已連接到以下社交帳號"
notesAndReplies: "文與回覆"
notesAndReplies: "文與回覆"
withFiles: "附件"
silence: "禁言"
silenceConfirm: "確定要禁言此用戶嗎?"
@ -388,7 +389,7 @@ notFoundDescription: "找不到與指定URL回應的頁面"
uploadFolder: "預設上傳資料夾"
cacheClear: "清除快取"
markAsReadAllNotifications: "標記所有通知為已讀"
markAsReadAllUnreadNotes: "標記所有文為已讀"
markAsReadAllUnreadNotes: "標記所有文為已讀"
markAsReadAllTalkMessages: "標記所有訊息為已讀"
help: "幫助"
inputMessageHere: "在此輸入訊息"
@ -409,9 +410,9 @@ text: "文字"
enable: "啟用"
next: "下一步"
retype: "重新輸入"
noteOf: "{user}的文"
noteOf: "{user}的文"
inviteToGroup: "邀請至群組"
maxNoteTextLength: "文的字數限制"
maxNoteTextLength: "文的字數限制"
quoteAttached: "引用"
quoteQuestion: "是否要引用?"
noMessagesYet: "沒有訊息"
@ -466,8 +467,10 @@ accountSettings: "帳號設定"
promotion: "推廣"
promote: "推廣"
numberOfDays: "有效天數"
hideThisNote: "隱藏此文"
hideThisNote: "隱藏此文"
showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
objectStorage: "Object Storage (物件儲存)"
useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "Base URL"
objectStorageBucket: "儲存空間Bucket"
objectStoragePrefix: "前綴"
@ -479,8 +482,8 @@ objectStorageUseProxy: "使用網路代理"
objectStorageSetPublicRead: "上載時設定為\"public-read\""
serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送文」方框"
newNoteRecived: "發現新的文"
showFixedPostForm: "於時間軸頁頂顯示「發送文」方框"
newNoteRecived: "發現新的文"
sounds: "音效"
listen: "聆聽"
none: "無"
@ -522,7 +525,7 @@ addRelay: "添加中繼"
inboxUrl: "私信URL"
addedRelays: "已添加的中繼"
serviceworkerInfo: "您需要啟用推送通知"
deletedNote: "已删除的文"
deletedNote: "已删除的文"
invisibleNote: "隱藏的帖子"
enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "可見性"
@ -537,7 +540,6 @@ author: "作者"
leaveConfirm: "有未保存的更改。要放棄嗎?"
manage: "管理"
plugins: "插件"
pluginInstallWarn: "請不要安裝來源不明的插件。"
deck: "多欄模式"
undeck: "取消多欄模式"
useBlurEffectForModal: "在模態框使用模糊效果"
@ -581,6 +583,7 @@ create: "新增"
notificationSetting: "通知設定"
notificationSettingDesc: "選擇顯示通知的類型"
other: "其他"
regenerateLoginToken: "再生登入權杖"
regenerateLoginTokenDescription: "再生用於登入的內部權杖。一般情況下是不需要這樣做的。一旦再生,所有裝置將會被登出。"
fileIdOrUrl: "文檔ID或者URL"
chatOpenBehavior: "開啟聊天窗口時的行為"
@ -596,20 +599,21 @@ abuseMarkAsResolved: "處理完畢"
openInNewTab: "在新分頁中開啟"
openInSideView: "在側欄中開啟"
editTheseSettingsMayBreakAccount: "修改這些設定可能會毀壞您的帳戶"
instanceTicker: "文的實例來源"
instanceTicker: "文的實例來源"
waitingFor: "等待{x}"
random: "隨機"
system: "系統"
switchUi: "切換界面"
desktop: "桌面"
clip: "片段"
clip: "摘錄"
createNew: "新建"
optional: "可選"
createNewClip: "建立新摘錄"
public: "公開"
i18nInfo: "Misskey已經被志願者們翻譯成各種語言版本如果想要幫忙的話可以進入{link}幫助翻譯。"
manageAccessTokens: "管理存取權杖"
accountInfo: "帳戶資訊"
notesCount: "文數量"
notesCount: "文數量"
repliesCount: "回覆數量\n"
renotesCount: "轉發數量"
repliedCount: "回覆數量"
@ -618,27 +622,29 @@ followingCount: "正在跟隨的用戶數量"
followersCount: "跟隨者數量"
sentReactionsCount: "情感發送次數"
receivedReactionsCount: "情感收到次數"
pollVotesCount: "已統計的投票數"
pollVotedCount: "已投票數"
yes: "確定"
no: "取消"
driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量"
noCrawleDescription: "請求網路搜尋引擎不要索引你的個人資料頁、文及頁面等。"
lockedAccountInfo: "即使你通過了追隨者請求,除非你將箋文的可見性設定為 「追隨者」,否則任何人都能看見你的箋文。"
noCrawleDescription: "請求網路搜尋引擎不要索引你的個人資料頁、文及頁面等。"
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
loadRawImages: "以原始圖像質量顯示附件圖像的縮略圖"
disableShowingAnimatedImages: "不播放動態圖像"
notSet: "未設定"
emailVerified: "已成功驗證您的電郵"
noteFavoritesCount: "收藏箋文的數目"
noteFavoritesCount: "我的最愛貼文的數目"
pageLikesCount: "頁面被喜歡次數"
pageLikedCount: "頁面被喜歡次數"
contact: "聯絡人"
useSystemFont: "使用系統默認的字型"
clips: "標籤"
clips: "摘錄"
experimentalFeatures: "測試中的功能"
developer: "開發者"
makeExplorable: "讓自己的帳戶能夠在“探索”版面顯示"
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"版面中。"
showGapBetweenNotesInTimeline: "分開顯示時間線上的文。"
showGapBetweenNotesInTimeline: "分開顯示時間線上的文。"
duplicate: "複製"
left: "左"
center: "向中央"
@ -649,16 +655,31 @@ showTitlebar: "顯示標題列"
clearCache: "清除快取資料"
onlineUsersCount: "{n}人正在線上"
nUsers: "{n}用戶"
nNotes: "{n}箋文"
nNotes: "{n}貼文"
myTheme: "我的佈景主題"
backgroundColor: "背景"
accentColor: "重點色彩"
textColor: "文本"
advanced: "進階"
value: "數值 "
updatedAt: "最後更新"
saveConfirm: "您要儲存變更嗎?"
deleteConfirm: "你確定要刪除嗎?"
invalidValue: "輸入值無效。"
registry: "登錄表"
closeAccount: "停用帳戶"
currentVersion: "當前版本"
latestVersion: "最新版本"
newVersionOfClientAvailable: "新版本的用戶端可用。"
usageAmount: "使用量"
capacity: "容量"
inUse: "已使用"
_registry:
scope: "範圍"
key: "主要"
keys: "主要"
key: "機碼"
keys: "機碼"
domain: "域"
createKey: "新增機碼"
_aboutMisskey:
about: "Misskey是由syuilo於2014年開發的開源軟件。"
contributors: "主要貢獻者"
@ -682,6 +703,8 @@ _mfm:
bold: "粗體"
small: "縮小"
center: "置中"
inlineCode: "程式碼(内嵌)"
blockCode: "程式碼(區塊)"
inlineMath: "數學公式(內嵌)"
inlineMathDescription: "顯示內嵌的KaTex數學公式。"
blockMath: "數學公式(方塊)"
@ -692,9 +715,11 @@ _mfm:
flipDescription: "將內容上下或左右翻轉。"
jelly: "動畫(果凍)"
jellyDescription: "顯示果凍一樣的動畫效果。"
jump: "動畫(跳動)"
bounce: "動畫(反彈)"
shake: "動畫(搖晃)"
twitch: "動畫(顫抖)"
twitchDescription: "顯示強烈顫抖的動畫效果。"
spin: "動畫(旋轉)"
spinDescription: "顯示旋轉的動畫效果。"
x2: "大"
@ -759,8 +784,8 @@ _sidebar:
hide: "隱藏"
_wordMute:
muteWords: "加入靜音文字"
softDescription: "隱藏時間軸中指定條件的文。"
mutedNotes: "已靜音的文"
softDescription: "隱藏時間軸中指定條件的文。"
mutedNotes: "已靜音的文"
_theme:
explore: "取得佈景主題"
install: "安裝佈景主題"
@ -791,7 +816,7 @@ _theme:
hashtag: "#tag"
mention: "提及"
mentionMe: "提及我"
renote: "轉發文"
renote: "轉發文"
divider: "分割線"
scrollbarHandle: "滾動條"
scrollbarHandleHover: "滾動條 (漂浮)"
@ -808,13 +833,14 @@ _theme:
inputBorder: "輸入框邊框"
listItemHoverBg: "列表物品背景 (漂浮)"
driveFolderBg: "雲端硬碟文件夾背景"
badge: "獎章"
messageBg: "私信背景"
accentDarken: "強調色(偏暗)"
accentLighten: "強調色(明亮)"
fgHighlighted: "高亮顯示文本"
_sfx:
note: "文"
noteMy: "我的文"
note: "文"
noteMy: "我的文"
notification: "通知"
chat: "傳送訊息"
antenna: "天線接收"
@ -838,22 +864,22 @@ _time:
_tutorial:
title: "Misskey使用方法"
step1_1: "歡迎!"
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「文」"
step1_3: "由於你沒有發佈任何文,也沒有追隨任何人,所以你的時間軸目前是空的。"
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「文」"
step1_3: "由於你沒有發佈任何文,也沒有追隨任何人,所以你的時間軸目前是空的。"
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
step3_1: "個人資料都打理好了嗎?"
step3_2: "下一步讓我們來試試看發個文,按一下畫面上的鉛筆圖示來開始"
step3_3: "輸入完內容後,按視窗右上角的按鈕來發文"
step3_4: "不知道該寫什麼內容嗎試試看「開始使用Misskey了」如何。"
step4_1: "文發出去了嗎?"
step4_2: "如果你的文出現在時間軸上,就代表發文成功。"
step4_1: "文發出去了嗎?"
step4_2: "如果你的文出現在時間軸上,就代表發文成功。"
step5_1: "現在試試看追隨其他人來讓你的時間軸變得更生動吧。"
step5_2: "你會在{featured}上看到受歡迎的文,你也可以從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
step5_2: "你會在{featured}上看到受歡迎的文,你也可以從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
step5_3: "想要追隨其他人,只要點擊他們的頭像並按「追隨」即可。"
step5_4: "如果使用者的名字旁有鎖頭的圖示,代表他們需要手動核准你的追隨請求。"
step6_1: "現在你可以在時間軸上看到其他用戶的文。"
step6_2: "你也可以對別人的文作出「情感」,作出簡單的回覆。"
step6_1: "現在你可以在時間軸上看到其他用戶的文。"
step6_2: "你也可以對別人的文作出「情感」,作出簡單的回覆。"
step7_1: "以上為Misskey的基本操作說明教學在此告一段落。辛苦了。"
step7_2: "歡迎到{help}來瞭解更多Misskey相關介紹。"
step7_3: "那麼祝您在Misskey玩的開心~ 🚀"
@ -869,14 +895,14 @@ _permissions:
"write:blocks": "編輯已封鎖用戶名單"
"read:drive": "存取雲端硬碟\n"
"write:drive": "編輯雲端硬碟的檔案"
"read:favorites": "瀏覽已收藏"
"write:favorites": "編輯收藏清單"
"read:favorites": "瀏覽我的最愛"
"write:favorites": "編輯我的最愛列表"
"write:following": "追隨/解除追隨"
"read:messaging": "顯示訊息"
"write:messaging": "撰寫或刪除私人訊息"
"read:mutes": "顯示已靜音列表"
"write:mutes": "編輯已靜音列表"
"write:notes": "撰寫或刪除文"
"write:notes": "撰寫或刪除文"
"read:notifications": "查看通知"
"write:notifications": "編輯通知"
"read:reactions": "查看情感"
@ -898,11 +924,11 @@ _auth:
callback: "回到應用程式"
denied: "拒絕訪問"
_antennaSources:
all: "全部文"
homeTimeline: "來自已追隨使用者的文"
users: "來自特定使用者的文"
userList: "來自特定清單中的文"
userGroup: "來自特定群組的文"
all: "全部文"
homeTimeline: "來自已追隨使用者的文"
users: "來自特定使用者的文"
userList: "來自特定清單中的文"
userGroup: "來自特定群組的文"
_weekday:
sunday: "週日"
monday: "週一"
@ -916,7 +942,7 @@ _widgets:
notifications: "通知"
timeline: "時間軸"
calendar: "行事曆"
trends: "發燒文"
trends: "發燒文"
clock: "時鐘"
rss: "RSS閱讀器"
activity: "動態"
@ -951,6 +977,7 @@ _poll:
closed: "已結束"
remainingDays: "{d}天{h}小時後結束"
remainingHours: "{h}小時{m}分後結束"
remainingSeconds: "{s}秒後截止"
_visibility:
public: "公開"
home: "首頁"
@ -960,8 +987,8 @@ _visibility:
localOnly: "僅限本地"
localOnlyDescription: "對遠端使用者隱藏"
_postForm:
replyPlaceholder: "回覆此文..."
quotePlaceholder: "引用此文..."
replyPlaceholder: "回覆此文..."
quotePlaceholder: "引用此文..."
channelPlaceholder: "發佈到頻道"
_placeholders:
a: "今天過得如何?"
@ -980,7 +1007,7 @@ _profile:
metadataLabel: "標籤"
metadataContent: "内容"
_exportOrImport:
allNotes: "所有文"
allNotes: "所有文"
followingList: "追隨中"
muteList: "靜音"
blockingList: "封鎖"
@ -989,10 +1016,10 @@ _charts:
usersIncDec: "使用者増減"
usersTotal: "使用者合共"
activeUsers: "活躍使用者"
notesIncDec: "文増減"
localNotesIncDec: "本地文増減"
remoteNotesIncDec: "遠端文數目增减"
notesTotal: "文合共"
notesIncDec: "文増減"
localNotesIncDec: "本地文増減"
remoteNotesIncDec: "遠端文數目增减"
notesTotal: "文合共"
filesIncDec: "檔案増減"
filesTotal: "累計檔案"
storageUsageIncDec: "儲存空間的増減"
@ -1001,8 +1028,8 @@ _instanceCharts:
requests: "請求"
users: "使用者増減"
usersTotal: "總計使用者"
notes: "文増減"
notesTotal: "累計文"
notes: "文増減"
notesTotal: "累計文"
ff: "追隨/追隨者的増減"
ffTotal: "追隨/追隨者累計"
cacheSize: "增加或減少快取用量"
@ -1058,6 +1085,7 @@ _rooms:
keyboard: "鍵盤"
carpet-stripe: "條紋地毯"
mat: "地毯"
color-box: "層架"
wall-clock: "壁鐘"
photoframe: "相框"
cube: "立方體"
@ -1114,8 +1142,8 @@ _pages:
inputBlocks: "輸入"
specialBlocks: "特殊"
blocks:
text: "文本"
textarea: "字區域"
text: "字串"
textarea: "區域"
section: "區段"
image: "圖片"
button: "按鈕"
@ -1126,7 +1154,7 @@ _pages:
_post:
text: "内容"
canvasId: "畫布ID"
textInput: "插入字"
textInput: "插入"
_textInput:
name: "變數名稱"
text: "標題"
@ -1146,9 +1174,9 @@ _pages:
id: "畫布ID"
width: "寬度"
height: "高度"
note: "嵌式文"
note: "嵌式文"
_note:
id: "文ID"
id: "文ID"
detailed: "顯示詳細內容"
switch: "開關"
_switch:
@ -1193,19 +1221,23 @@ _pages:
convert: "轉換"
list: "清單"
blocks:
text: "文本"
multiLineText: "文本 (多行)"
textList: "文本列表"
text: "字串"
multiLineText: "字串(多行)"
textList: "字串串列"
strLen: "字串長度"
_strLen:
arg1: "文本"
arg1: "字串"
strPick: "提取字元"
_strPick:
arg1: "文本"
arg1: "字串"
arg2: "字元位置"
strReplace: "替換字串"
_strReplace:
arg1: "文本"
arg1: "字串"
strReverse: "倒轉字串"
_strReverse:
arg1: "本文"
arg1: "字串"
join: "合併字串"
_join:
arg1: "清單"
arg2: "分隔字元"
@ -1301,7 +1333,7 @@ _pages:
arg1: "種子"
arg2: "清單"
_DRPWPM:
arg1: "文本列表"
arg1: "字串串列"
pick: "從清單中選取"
_pick:
arg1: "清單"
@ -1310,12 +1342,15 @@ _pages:
_listLen:
arg1: "清單"
number: "數值"
stringToNumber: "將字串轉換至數値"
_stringToNumber:
arg1: "文字"
arg1: "字串"
numberToString: "將數値轉換至字串"
_numberToString:
arg1: "數值"
splitStrByLine: "於換行時分割字串"
_splitStrByLine:
arg1: "文本"
arg1: "字串"
ref: "變數"
aiScriptVar: "AiScript的變數"
fn: "函数"
@ -1325,10 +1360,11 @@ _pages:
arg1: "重複次數"
arg2: "處理"
types:
string: "字"
string: ""
number: "数值"
boolean: "標記"
array: "清單"
stringArray: "文本列表"
stringArray: "字串列表"
enviromentVariables: "環境變數"
pageVariables: "頁面元素"
_relayStatus:
@ -1339,7 +1375,7 @@ _notification:
youGotMention: "{name}提及到您"
youGotReply: "{name}回覆了您"
youGotQuote: "{name}引用了您"
youRenoted: "{name} 轉發了你的文"
youRenoted: "{name} 轉發了你的文"
youGotPoll: "{name}已投票"
youGotMessagingMessageFromUser: "{name}發送給您的訊息"
youWereFollowed: "您有新的追隨者"
@ -1351,7 +1387,7 @@ _notification:
follow: "追隨中"
mention: "提及"
reply: "回覆"
renote: "轉發文"
renote: "轉發文"
quote: "引用"
reaction: "情感"
pollVote: "統計已投票數"

View File

@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class followersUri1611354329133 implements MigrationInterface {
name = 'followersUri1611354329133'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD "followersUri" varchar(512) DEFAULT NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "followersUri"`);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class objectStorageS3ForcePathStyle1611547387175 implements MigrationInterface {
name = 'objectStorageS3ForcePathStyle1611547387175'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageS3ForcePathStyle" boolean NOT NULL DEFAULT true`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageS3ForcePathStyle"`);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class announcementEmail1612619156584 implements MigrationInterface {
name = 'announcementEmail1612619156584'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "receiveAnnouncementEmail" boolean NOT NULL DEFAULT true`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "receiveAnnouncementEmail"`);
}
}

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.68.0",
"version": "12.69.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -35,12 +35,12 @@
"lodash": "^4.17.20"
},
"dependencies": {
"@babel/plugin-transform-runtime": "7.12.1",
"@babel/plugin-transform-runtime": "7.12.15",
"@elastic/elasticsearch": "7.10.0",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-regular-svg-icons": "5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.1",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.15.2",
"@fortawesome/free-regular-svg-icons": "5.15.2",
"@fortawesome/free-solid-svg-icons": "5.15.2",
"@fortawesome/vue-fontawesome": "3.0.0-3",
"@koa/cors": "3.1.0",
"@koa/multer": "3.0.0",
@ -50,21 +50,21 @@
"@sinonjs/fake-timers": "6.0.1",
"@syuilo/aiscript": "0.11.1",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.14.4",
"@types/bull": "3.15.0",
"@types/cbor": "5.0.1",
"@types/dateformat": "3.0.1",
"@types/double-ended-queue": "2.1.1",
"@types/escape-regexp": "0.0.0",
"@types/glob": "7.1.3",
"@types/gulp": "4.0.6",
"@types/gulp-rename": "0.0.33",
"@types/gulp": "4.0.8",
"@types/gulp-rename": "2.0.0",
"@types/gulp-replace": "0.0.31",
"@types/is-url": "1.2.28",
"@types/js-yaml": "3.12.5",
"@types/jsdom": "16.2.5",
"@types/jsonld": "1.5.1",
"@types/js-yaml": "4.0.0",
"@types/jsdom": "16.2.6",
"@types/jsonld": "1.5.2",
"@types/katex": "0.11.0",
"@types/koa": "2.11.6",
"@types/koa": "2.11.7",
"@types/koa-bodyparser": "4.3.0",
"@types/koa-cors": "0.0.0",
"@types/koa-favicon": "2.0.19",
@ -74,20 +74,20 @@
"@types/koa-views": "2.0.4",
"@types/koa__cors": "3.0.2",
"@types/koa__multer": "2.0.2",
"@types/koa__router": "8.0.2",
"@types/markdown-it": "10.0.3",
"@types/matter-js": "0.14.8",
"@types/mocha": "7.0.2",
"@types/node": "14.14.13",
"@types/node-fetch": "2.5.7",
"@types/koa__router": "8.0.4",
"@types/markdown-it": "12.0.1",
"@types/matter-js": "0.14.10",
"@types/mocha": "8.2.0",
"@types/node": "14.14.25",
"@types/node-fetch": "2.5.8",
"@types/nodemailer": "6.4.0",
"@types/nprogress": "0.2.0",
"@types/oauth": "0.9.1",
"@types/parse5": "5.0.3",
"@types/parse5": "6.0.0",
"@types/parsimmon": "1.10.6",
"@types/portscanner": "2.1.0",
"@types/pug": "2.0.4",
"@types/qrcode": "1.3.5",
"@types/qrcode": "1.4.0",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.1",
"@types/redis": "2.8.28",
@ -95,51 +95,50 @@
"@types/request-stats": "3.0.0",
"@types/rimraf": "3.0.0",
"@types/seedrandom": "2.4.28",
"@types/sharp": "0.26.1",
"@types/sinonjs__fake-timers": "6.0.1",
"@types/sharp": "0.27.1",
"@types/sinonjs__fake-timers": "6.0.2",
"@types/speakeasy": "2.0.5",
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.2",
"@types/tmp": "0.2.0",
"@types/uuid": "8.3.0",
"@types/web-push": "3.3.0",
"@types/webpack": "4.41.25",
"@types/webpack": "4.41.26",
"@types/webpack-stream": "3.2.11",
"@types/websocket": "1.0.1",
"@types/ws": "7.4.0",
"@typescript-eslint/parser": "4.10.0",
"@typescript-eslint/parser": "4.14.2",
"@vue/compiler-sfc": "3.0.5",
"abort-controller": "3.0.0",
"apexcharts": "3.23.1",
"apexcharts": "3.24.0",
"autobind-decorator": "2.4.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"aws-sdk": "2.809.0",
"aws-sdk": "2.839.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.3",
"bull": "3.20.0",
"cafy": "15.2.1",
"cbor": "5.1.0",
"cbor": "6.0.1",
"chalk": "4.1.0",
"chart.js": "2.9.4",
"cli-highlight": "2.1.9",
"cli-highlight": "2.1.10",
"commander": "4.1.1",
"content-disposition": "0.5.3",
"core-js": "3.8.1",
"core-js": "3.8.3",
"crc-32": "1.2.0",
"css-loader": "5.0.1",
"cssnano": "4.1.10",
"dateformat": "4.3.1",
"deep-entries": "3.1.0",
"dateformat": "4.5.1",
"diskusage": "1.1.3",
"double-ended-queue": "2.1.0-0",
"escape-regexp": "0.0.1",
"eslint": "7.17.0",
"eslint-plugin-vue": "7.4.1",
"eslint": "7.19.0",
"eslint-plugin-vue": "7.5.0",
"eventemitter3": "4.0.7",
"feed": "4.2.1",
"feed": "4.2.2",
"fibers": "5.0.0",
"file-type": "16.1.0",
"file-type": "16.2.0",
"fluent-ffmpeg": "2.1.2",
"glob": "7.1.6",
"got": "11.8.1",
@ -147,24 +146,23 @@
"gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0",
"gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.5",
"gulp-terser": "2.0.1",
"gulp-tslint": "8.1.4",
"gulp-typescript": "6.0.0-alpha.1",
"hard-source-webpack-plugin": "0.13.1",
"hcaptcha": "0.0.2",
"html-minifier": "4.0.0",
"http-proxy-agent": "4.0.1",
"http-signature": "1.3.5",
"https-proxy-agent": "5.0.0",
"idb-keyval": "5.0.1",
"insert-text-at-cursor": "0.3.0",
"is-root": "2.1.0",
"is-svg": "4.2.1",
"js-yaml": "3.14.0",
"js-yaml": "4.0.0",
"jsdom": "16.4.0",
"json5": "2.1.3",
"json5": "2.2.0",
"json5-loader": "4.0.1",
"jsonld": "3.2.0",
"jsonld": "3.3.0",
"jsrsasign": "8.0.20",
"katex": "0.12.0",
"koa": "2.13.1",
@ -178,9 +176,9 @@
"koa-views": "6.3.1",
"langmap": "0.0.16",
"lookup-dns-cache": "2.1.0",
"markdown-it": "11.0.1",
"markdown-it-anchor": "6.0.1",
"matter-js": "0.14.2",
"markdown-it": "12.0.4",
"markdown-it-anchor": "7.0.1",
"matter-js": "0.16.1",
"mocha": "8.2.1",
"moji": "0.5.1",
"ms": "2.1.3",
@ -196,7 +194,7 @@
"pg": "8.5.1",
"portscanner": "2.2.0",
"postcss": "8.2.4",
"postcss-loader": "4.1.0",
"postcss-loader": "5.0.0",
"prismjs": "1.23.0",
"probe-image-size": "6.0.0",
"promise-limit": "2.7.0",
@ -208,7 +206,6 @@
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.15.9",
"recaptcha-promise": "1.0.0",
"reconnecting-websocket": "4.4.0",
"redis": "3.0.2",
"redis-lock": "0.1.4",
@ -220,10 +217,10 @@
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.32.4",
"sass-loader": "10.1.1",
"sass": "1.32.6",
"sass-loader": "11.0.0",
"seedrandom": "3.0.5",
"sharp": "0.27.0",
"sharp": "0.27.1",
"speakeasy": "2.0.0",
"stringz": "2.1.0",
"style-loader": "2.0.0",
@ -236,7 +233,7 @@
"throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "8.0.14",
"ts-loader": "8.0.15",
"ts-node": "9.1.1",
"tslint": "6.1.3",
"tslint-sonarts": "1.9.0",
@ -250,22 +247,22 @@
"vue": "3.0.5",
"vue-color": "2.8.1",
"vue-json-pretty": "1.7.1",
"vue-loader": "16.0.0",
"vue-loader": "16.1.2",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.1",
"vue-router": "4.0.3",
"vue-style-loader": "4.1.2",
"vuedraggable": "4.0.1",
"web-push": "3.4.4",
"webpack": "5.13.0",
"webpack-cli": "4.3.1",
"webpack": "5.21.1",
"webpack-cli": "4.5.0",
"websocket": "1.0.33",
"ws": "7.4.2",
"ws": "7.4.3",
"xev": "2.0.1"
},
"devDependencies": {
"@types/chai": "4.2.14",
"@types/fluent-ffmpeg": "2.1.16",
"chai": "4.2.0",
"chai": "4.3.0",
"cross-env": "7.0.3"
}
}

View File

@ -1,16 +0,0 @@
declare module 'recaptcha-promise' {
interface IVerifyOptions {
secret_key?: string;
}
interface IVerify {
(response: string, remoteAddress?: string): Promise<boolean>;
init(options: IVerifyOptions): IVerify;
}
namespace recaptchaPromise {} // Hack
const verify: IVerify;
export = verify;
}

View File

@ -57,7 +57,7 @@ export default defineComponent({
src() {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
grecaptcha: 'https://www.google.com/recaptcha',
grecaptcha: 'https://www.recaptcha.net/recaptcha',
} as Record<PropertyKey, unknown>)[this.provider];
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;

View File

@ -21,13 +21,13 @@
>
<div class="contents" ref="contents">
<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
<XFolder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
<XFolder v-for="(f, i) in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" v-anim="i"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
<MkButton ref="moreFolders" v-if="moreFolders">{{ $ts.loadMore }}</MkButton>
</div>
<div class="files" ref="filesContainer" v-show="files.length > 0">
<XFile v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
<XFile v-for="(file, i) in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" v-anim="i"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $ts.loadMore }}</MkButton>

View File

@ -2,6 +2,7 @@
<component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
<span class="schema">{{ schema }}//</span>

View File

@ -52,7 +52,7 @@
<span class="localOnly" v-if="appearNote.localOnly"><Fa :icon="faBiohazard"/></span>
</div>
<div class="username"><MkAcct :user="appearNote.user"/></div>
<MkInstanceTicker class="ticker" :instance="appearNote.user.instance"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
</div>
</header>
<div class="main">

View File

@ -1,9 +1,9 @@
<template>
<component :is="'x-' + value.type" :value="value" :page="page" :hpml="hpml" :key="value.id" :h="h"/>
<component :is="'x-' + block.type" :block="block" :hpml="hpml" :key="block.id" :h="h"/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
@ -19,22 +19,24 @@ import XCounter from './page.counter.vue';
import XRadioButton from './page.radio-button.vue';
import XCanvas from './page.canvas.vue';
import XNote from './page.note.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { Block } from '@/scripts/hpml/block';
export default defineComponent({
components: {
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote
},
props: {
value: {
block: {
type: Object as PropType<Block>,
required: true
},
hpml: {
required: true
},
page: {
type: Object as PropType<Hpml>,
required: true
},
h: {
type: Number,
required: true
}
},

View File

@ -1,51 +1,55 @@
<template>
<div>
<MkButton class="kudkigyw" @click="click()" :primary="value.primary">{{ hpml.interpolate(value.text) }}</MkButton>
<MkButton class="kudkigyw" @click="click()" :primary="block.primary">{{ hpml.interpolate(block.text) }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType, unref } from 'vue';
import MkButton from '../ui/button.vue';
import * as os from '@/os';
import { ButtonBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
MkButton
},
props: {
value: {
block: {
type: Object as PropType<ButtonBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
methods: {
click() {
if (this.value.action === 'dialog') {
if (this.block.action === 'dialog') {
this.hpml.eval();
os.dialog({
text: this.hpml.interpolate(this.value.content)
text: this.hpml.interpolate(this.block.content)
});
} else if (this.value.action === 'resetRandom') {
} else if (this.block.action === 'resetRandom') {
this.hpml.updateRandomSeed(Math.random());
this.hpml.eval();
} else if (this.value.action === 'pushEvent') {
} else if (this.block.action === 'pushEvent') {
os.api('page-push', {
pageId: this.hpml.page.id,
event: this.value.event,
...(this.value.var ? {
var: this.hpml.vars[this.value.var]
event: this.block.event,
...(this.block.var ? {
var: unref(this.hpml.vars)[this.block.var]
} : {})
});
os.dialog({
type: 'success',
text: this.hpml.interpolate(this.value.message)
text: this.hpml.interpolate(this.block.message)
});
} else if (this.value.action === 'callAiScript') {
this.hpml.callAiScript(this.value.fn);
} else if (this.block.action === 'callAiScript') {
this.hpml.callAiScript(this.block.fn);
}
}
}

View File

@ -1,24 +1,36 @@
<template>
<div class="ysrxegms">
<canvas ref="canvas" :width="value.width" :height="value.height"/>
<canvas ref="canvas" :width="block.width" :height="block.height"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
import * as os from '@/os';
import { CanvasBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
value: {
block: {
type: Object as PropType<CanvasBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
mounted() {
this.hpml.registerCanvas(this.value.name, this.$refs.canvas);
setup(props, ctx) {
const canvas: Ref<any> = ref(null);
onMounted(() => {
props.hpml.registerCanvas(props.block.name, canvas.value);
});
return {
canvas
};
}
});
</script>

View File

@ -1,41 +1,43 @@
<template>
<div>
<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(value.text) }}</MkButton>
<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkButton from '../ui/button.vue';
import * as os from '@/os';
import { CounterVarBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
MkButton
},
props: {
value: {
block: {
type: Object as PropType<CounterVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function click() {
props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
props.hpml.eval();
}
return {
v: 0,
click
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
}
},
methods: {
click() {
this.v = this.v + (this.value.inc || 1);
}
}
});
</script>

View File

@ -1,27 +1,29 @@
<template>
<div v-show="hpml.vars[value.var]">
<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h"/>
<div v-show="hpml.vars.value[block.var]">
<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h"/>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { IfBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, defineAsyncComponent, PropType } from 'vue';
export default defineComponent({
components: {
XBlock: defineAsyncComponent(() => import('./page.block.vue'))
},
props: {
value: {
block: {
type: Object as PropType<IfBlock>,
required: true
},
hpml: {
required: true
},
page: {
type: Object as PropType<Hpml>,
required: true
},
h: {
type: Number,
required: true
}
},

View File

@ -5,25 +5,28 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import * as os from '@/os';
import { ImageBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
value: {
block: {
type: Object as PropType<ImageBlock>,
required: true
},
page: {
hpml: {
type: Object as PropType<Hpml>,
required: true
},
}
},
data() {
setup(props, ctx) {
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
return {
image: null,
image
};
},
created() {
this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId);
}
});
</script>

View File

@ -1,15 +1,16 @@
<template>
<div class="voxdxuby">
<XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'"/>
<XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'"/>
<XNote v-if="note && !block.detailed" v-model:note="note" :key="note.id + ':normal'"/>
<XNoteDetailed v-if="note && block.detailed" v-model:note="note" :key="note.id + ':detail'"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
import XNote from '@/components/note.vue';
import XNoteDetailed from '@/components/note-detailed.vue';
import * as os from '@/os';
import { NoteBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
@ -17,20 +18,24 @@ export default defineComponent({
XNoteDetailed,
},
props: {
value: {
required: true
},
hpml: {
block: {
type: Object as PropType<NoteBlock>,
required: true
}
},
data() {
setup(props, ctx) {
const note: Ref<Record<string, any> | null> = ref(null);
onMounted(() => {
os.api('notes/show', { noteId: props.block.note })
.then(result => {
note.value = result;
});
});
return {
note: null,
note
};
},
async mounted() {
this.note = await os.api('notes/show', { noteId: this.value.note });
}
});
</script>

View File

@ -1,36 +1,44 @@
<template>
<div>
<MkInput class="kudkigyw" v-model:value="v" type="number">{{ hpml.interpolate(value.text) }}</MkInput>
<MkInput class="kudkigyw" :value="value" @update:value="updateValue($event)" type="number">{{ hpml.interpolate(block.text) }}</MkInput>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../ui/input.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { NumberInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkInput
},
props: {
value: {
block: {
type: Object as PropType<NumberInputVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@ -6,12 +6,14 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import { faCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons';
import MkTextarea from '../ui/textarea.vue';
import MkButton from '../ui/button.vue';
import { apiUrl } from '@/config';
import * as os from '@/os';
import { PostBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
@ -19,16 +21,18 @@ export default defineComponent({
MkButton,
},
props: {
value: {
block: {
type: Object as PropType<PostBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
text: this.hpml.interpolate(this.value.text),
text: this.hpml.interpolate(this.block.text),
posted: false,
posting: false,
faCheck, faPaperPlane
@ -37,7 +41,7 @@ export default defineComponent({
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.value.text);
this.text = this.hpml.interpolate(this.block.text);
},
deep: true
}
@ -45,7 +49,7 @@ export default defineComponent({
methods: {
upload() {
const promise = new Promise((ok) => {
const canvas = this.hpml.canvases[this.value.canvasId];
const canvas = this.hpml.canvases[this.block.canvasId];
canvas.toBlob(blob => {
const data = new FormData();
data.append('file', blob);
@ -69,7 +73,7 @@ export default defineComponent({
},
async post() {
this.posting = true;
const file = this.value.attachCanvasImage ? await this.upload() : null;
const file = this.block.attachCanvasImage ? await this.upload() : null;
os.apiWithDialog('notes/create', {
text: this.text === '' ? null : this.text,
fileIds: file ? [file.id] : undefined,

View File

@ -1,37 +1,45 @@
<template>
<div>
<div>{{ hpml.interpolate(value.title) }}</div>
<MkRadio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</MkRadio>
<div>{{ hpml.interpolate(block.title) }}</div>
<MkRadio v-for="item in block.values" :modelValue="value" @update:modelValue="updateValue($event)" :value="item" :key="item">{{ item }}</MkRadio>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkRadio from '../ui/radio.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { RadioButtonVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkRadio
},
props: {
value: {
block: {
type: Object as PropType<RadioButtonVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue: string) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@ -1,29 +1,30 @@
<template>
<section class="sdgxphyu">
<component :is="'h' + h">{{ value.title }}</component>
<component :is="'h' + h">{{ block.title }}</component>
<div class="children">
<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h + 1"/>
<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h + 1"/>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { defineComponent, defineAsyncComponent, PropType } from 'vue';
import * as os from '@/os';
import { SectionBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
XBlock: defineAsyncComponent(() => import('./page.block.vue'))
},
props: {
value: {
block: {
type: Object as PropType<SectionBlock>,
required: true
},
hpml: {
required: true
},
page: {
type: Object as PropType<Hpml>,
required: true
},
h: {

View File

@ -1,36 +1,44 @@
<template>
<div class="hkcxmtwj">
<MkSwitch v-model:value="v">{{ hpml.interpolate(value.text) }}</MkSwitch>
<MkSwitch :value="value" @update:value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkSwitch from '../ui/switch.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { SwitchVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkSwitch
},
props: {
value: {
block: {
type: Object as PropType<SwitchVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue: boolean) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@ -1,36 +1,44 @@
<template>
<div>
<MkInput class="kudkigyw" v-model:value="v" type="text">{{ hpml.interpolate(value.text) }}</MkInput>
<MkInput class="kudkigyw" :value="value" @update:value="updateValue($event)" type="text">{{ hpml.interpolate(block.text) }}</MkInput>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../ui/input.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkInput
},
props: {
value: {
block: {
type: Object as PropType<TextInputVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@ -6,7 +6,9 @@
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineAsyncComponent, defineComponent, PropType } from 'vue';
import { parse } from '../../../mfm/parse';
import { unique } from '../../../prelude/array';
@ -15,16 +17,18 @@ export default defineComponent({
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
},
props: {
value: {
block: {
type: Object as PropType<TextBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
text: this.hpml.interpolate(this.value.text),
text: this.hpml.interpolate(this.block.text),
};
},
computed: {
@ -43,7 +47,7 @@ export default defineComponent({
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.value.text);
this.text = this.hpml.interpolate(this.block.text);
},
deep: true
}

View File

@ -1,36 +1,45 @@
<template>
<div>
<MkTextarea v-model:value="v">{{ hpml.interpolate(value.text) }}</MkTextarea>
<MkTextarea :value="value" @update:value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkTextarea>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { HpmlTextInput } from '@/scripts/hpml';
import { TextInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkTextarea
},
props: {
value: {
block: {
type: Object as PropType<TextInputVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@ -3,7 +3,9 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue';
export default defineComponent({
@ -11,22 +13,24 @@ export default defineComponent({
MkTextarea
},
props: {
value: {
block: {
type: Object as PropType<TextBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
text: this.hpml.interpolate(this.value.text),
text: this.hpml.interpolate(this.block.text),
};
},
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.value.text);
this.text = this.hpml.interpolate(this.block.text);
},
deep: true
}

View File

@ -1,77 +1,72 @@
<template>
<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml">
<XBlock v-for="child in page.content" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="2"/>
<XBlock v-for="child in page.content" :block="child" :hpml="hpml" :key="child.id" :h="2"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, nextTick, onUnmounted, PropType } from 'vue';
import { parse } from '@syuilo/aiscript';
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
import { faHeart } from '@fortawesome/free-regular-svg-icons';
import XBlock from './page.block.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { url } from '@/config';
import { $i } from '@/account';
import { defaultStore } from '@/store';
export default defineComponent({
components: {
XBlock
},
props: {
page: {
type: Object,
type: Object as PropType<Record<string, any>>,
required: true
},
},
setup(props, ctx) {
data() {
return {
hpml: null,
faHeartS, faHeart
};
},
created() {
this.hpml = new Hpml(this.page, {
const hpml = new Hpml(props.page, {
randomSeed: Math.random(),
visitor: this.$i,
visitor: $i,
url: url,
enableAiScript: !this.$store.state.disablePagesScript
enableAiScript: !defaultStore.state.disablePagesScript
});
},
mounted() {
this.$nextTick(() => {
if (this.page.script && this.hpml.aiscript) {
let ast;
try {
ast = parse(this.page.script);
} catch (e) {
console.error(e);
/*os.dialog({
type: 'error',
text: 'Syntax error :('
});*/
return;
onMounted(() => {
nextTick(() => {
if (props.page.script && hpml.aiscript) {
let ast;
try {
ast = parse(props.page.script);
} catch (e) {
console.error(e);
/*os.dialog({
type: 'error',
text: 'Syntax error :('
});*/
return;
}
hpml.aiscript.exec(ast).then(() => {
hpml.eval();
}).catch(e => {
console.error(e);
/*os.dialog({
type: 'error',
text: e
});*/
});
} else {
hpml.eval();
}
this.hpml.aiscript.exec(ast).then(() => {
this.hpml.eval();
}).catch(e => {
console.error(e);
/*os.dialog({
type: 'error',
text: e
});*/
});
} else {
this.hpml.eval();
}
});
onUnmounted(() => {
if (hpml.aiscript) hpml.aiscript.abort();
});
});
},
beforeUnmount() {
if (this.hpml.aiscript) this.hpml.aiscript.abort();
return {
hpml,
};
},
});
</script>

View File

@ -0,0 +1,18 @@
import { Directive } from 'vue';
export default {
beforeMount(src, binding, vn) {
src.style.opacity = '0';
src.style.transform = 'scale(0.9)';
// ページネーションと相性が悪いので
//if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`;
src.classList.add('_zoom');
},
mounted(src, binding, vn) {
setTimeout(() => {
src.style.opacity = '1';
src.style.transform = 'none';
}, 1);
},
} as Directive;

View File

@ -6,6 +6,7 @@ import particle from './particle';
import tooltip from './tooltip';
import hotkey from './hotkey';
import appear from './appear';
import anim from './anim';
export default function(app: App) {
app.directive('userPreview', userPreview);
@ -15,4 +16,5 @@ export default function(app: App) {
app.directive('tooltip', tooltip);
app.directive('hotkey', hotkey);
app.directive('appear', appear);
app.directive('anim', anim);
}

View File

@ -1,49 +1,6 @@
import { markRaw } from 'vue';
import { locale } from '@/config';
export class I18n<T extends Record<string, any>> {
public locale: T;
constructor(locale: T) {
this.locale = locale;
if (_DEV_) {
console.log('i18n', this.locale);
}
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
public t(key: string, args?: Record<string, any>): string {
try {
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
if (_DEV_) {
if (!str.includes('{')) {
console.warn(`i18n: '${key}' has no any arg. so ref prop directly instead of call this method.`);
}
}
if (args) {
for (const [k, v] of Object.entries(args)) {
str = str.replace(`{${k}}`, v);
}
}
return str;
} catch (e) {
if (_DEV_) {
console.warn(`missing localization '${key}'`);
return `⚠'${key}'⚠`;
}
return key;
}
}
}
import { I18n } from '@/scripts/i18n';
export const i18n = markRaw(new I18n(locale));

View File

@ -57,6 +57,7 @@ import { fetchInstance, instance } from '@/instance';
import { makeHotkey } from './scripts/hotkey';
import { search } from './scripts/search';
import { getThemes } from './theme-store';
import { initializeSw } from './scripts/initialize-sw';
console.info(`Misskey v${version}`);
@ -171,7 +172,7 @@ fetchInstance().then(() => {
localStorage.setItem('v', instance.version);
// Init service worker
//if (this.store.state.instance.meta.swPublickey) this.registerSw(this.store.state.instance.meta.swPublickey);
initializeSw();
});
stream.init($i);

View File

@ -175,6 +175,7 @@
<MkSwitch v-model:value="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $ts.objectStorageUseSSL }}<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template></MkSwitch>
<MkSwitch v-model:value="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $ts.objectStorageUseProxy }}<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template></MkSwitch>
<MkSwitch v-model:value="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $ts.objectStorageSetPublicRead }}</MkSwitch>
<MkSwitch v-model:value="objectStorageS3ForcePathStyle" :disabled="!useObjectStorage">s3ForcePathStyle</MkSwitch>
</template>
</div>
<div class="_footer">
@ -325,6 +326,7 @@ export default defineComponent({
objectStorageUseSSL: false,
objectStorageUseProxy: false,
objectStorageSetPublicRead: false,
objectStorageS3ForcePathStyle: true,
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
@ -393,6 +395,7 @@ export default defineComponent({
this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
this.objectStorageUseProxy = this.meta.objectStorageUseProxy;
this.objectStorageSetPublicRead = this.meta.objectStorageSetPublicRead;
this.objectStorageS3ForcePathStyle = this.meta.objectStorageS3ForcePathStyle;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
@ -547,6 +550,7 @@ export default defineComponent({
objectStorageUseSSL: this.objectStorageUseSSL,
objectStorageUseProxy: this.objectStorageUseProxy,
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,

View File

@ -10,6 +10,7 @@
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-index="i"
:key="message.id"
v-anim="i"
>
<div>
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>

View File

@ -1,6 +1,6 @@
<template>
<div class="fcuexfpr">
<div v-if="note" class="note">
<div v-if="note" class="note" v-anim>
<div class="_section" v-if="showNext">
<XNotes class="_content _noGap_" :pagination="next"/>
</div>

View File

@ -61,8 +61,10 @@ import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
import { v4 as uuid } from 'uuid';
import XContainer from './page-editor.container.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import { isLiteralBlock, funcDefs, blockDefs } from '@/scripts/hpml/index';
import { blockDefs } from '@/scripts/hpml/index';
import * as os from '@/os';
import { isLiteralValue } from '@/scripts/hpml/expr';
import { funcDefs } from '@/scripts/hpml/lib';
export default defineComponent({
components: {
@ -166,7 +168,7 @@ export default defineComponent({
return;
}
if (isLiteralBlock(this.value)) return;
if (isLiteralValue(this.value)) return;
const empties = [];
for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {

View File

@ -20,7 +20,7 @@
</div>
<div class="_section links">
<div class="_content">
<MkA :to="`./${page.name}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
<template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>

View File

@ -0,0 +1,32 @@
<template>
<div class="graojtoi">
<MkSample/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faEye } from '@fortawesome/free-solid-svg-icons';
import MkSample from '@/components/sample.vue';
export default defineComponent({
components: {
MkSample,
},
data() {
return {
INFO: {
title: this.$ts.preview,
icon: faEye,
},
}
},
});
</script>
<style lang="scss" scoped>
.graojtoi {
padding: var(--margin);
}
</style>

View File

@ -8,6 +8,10 @@
{{ $i.email || $ts.notSet }}
</FormLink>
</FormGroup>
<FormSwitch :value="$i.receiveAnnouncementEmail" @update:value="onChangeReceiveAnnouncementEmail">
{{ $ts.receiveAnnouncementFromInstance }}
</FormSwitch>
</FormBase>
</template>
@ -19,6 +23,7 @@ import FormButton from '@/components/form/button.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
export default defineComponent({
@ -26,6 +31,7 @@ export default defineComponent({
FormBase,
FormLink,
FormButton,
FormSwitch,
FormGroup,
},
@ -46,7 +52,11 @@ export default defineComponent({
},
methods: {
onChangeReceiveAnnouncementEmail(v) {
os.api('i/update', {
receiveAnnouncementEmail: v
});
},
}
});
</script>

View File

@ -18,7 +18,7 @@
<FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $ts.theme }}</FormLink>
<FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $ts.sidebar }}</FormLink>
<FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $ts.sounds }}</FormLink>
<FormLink :active="page === 'plugins'" replace to="/settings/plugins"><template #icon><Fa :icon="faPlug"/></template>{{ $ts.plugins }}</FormLink>
<FormLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><Fa :icon="faPlug"/></template>{{ $ts.plugins }}</FormLink>
</FormGroup>
<FormGroup>
<template #label>{{ $ts.otherSettings }}</template>
@ -105,7 +105,9 @@ export default defineComponent({
case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
case 'update': return defineAsyncComponent(() => import('./update.vue'));

View File

@ -1,26 +1,32 @@
<template>
<section class="_section">
<div class="_content" v-if="enableTwitterIntegration">
<header><Fa :icon="faTwitter"/> Twitter</header>
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter">{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectTwitter">{{ $ts.connectSerice }}</MkButton>
<FormBase>
<div class="_formItem" v-if="enableTwitterIntegration">
<div class="_formLabel"><Fa :icon="faTwitter"/> Twitter</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectSerice }}</MkButton>
</div>
</div>
<div class="_content" v-if="enableDiscordIntegration">
<header><Fa :icon="faDiscord"/> Discord</header>
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discordapp.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord">{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectDiscord">{{ $ts.connectSerice }}</MkButton>
<div class="_formItem" v-if="enableDiscordIntegration">
<div class="_formLabel"><Fa :icon="faDiscord"/> Discord</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectSerice }}</MkButton>
</div>
</div>
<div class="_content" v-if="enableGithubIntegration">
<header><Fa :icon="faGithub"/> GitHub</header>
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub">{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectGithub">{{ $ts.connectSerice }}</MkButton>
<div class="_formItem" v-if="enableGithubIntegration">
<div class="_formLabel"><Fa :icon="faGithub"/> GitHub</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectSerice }}</MkButton>
</div>
</div>
</section>
</FormBase>
</template>
<script lang="ts">
@ -28,11 +34,13 @@ import { defineComponent } from 'vue';
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import { apiUrl } from '@/config';
import FormBase from '@/components/form/base.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
MkButton
},

View File

@ -0,0 +1,146 @@
<template>
<FormBase>
<MkInfo warn>{{ $ts.pluginInstallWarn }}</MkInfo>
<FormGroup>
<FormTextarea v-model:value="code" tall>
<span>{{ $ts.code }}</span>
</FormTextarea>
</FormGroup>
<FormButton @click="install" :disabled="code == null" primary inline><Fa :icon="faCheck"/> {{ $ts.install }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import MkInfo from '@/components/ui/info.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormLink,
FormButton,
MkInfo,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$ts._plugin.install,
icon: faDownload
},
code: null,
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
}));
},
async install() {
let ast;
try {
ast = parse(this.code);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const data = meta.get(null);
if (data == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
os.dialog({
type: 'error',
text: 'Required property not found :('
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(import('@/components/token-generate-window.vue'), {
title: this.$ts.tokenRequested,
information: this.$ts.pluginTokenRequestedDescription,
initialName: name,
initialPermissions: permissions
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
}
}, 'closed');
});
this.installPlugin({
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
os.success();
this.$nextTick(() => {
location.reload();
});
},
}
});
</script>

View File

@ -0,0 +1,117 @@
<template>
<FormBase>
<FormGroup v-for="plugin in plugins" :key="plugin.id">
<template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template>
<FormSwitch :value="plugin.active" @update:value="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
<div class="_formItem">
<div class="_formPanel" style="padding: 16px;">
<div class="_keyValue">
<div>{{ $ts.author }}:</div>
<div>{{ plugin.author }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.description }}:</div>
<div>{{ plugin.description }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.permission }}:</div>
<div>{{ plugin.permissions }}</div>
</div>
</div>
</div>
<div class="_formItem">
<div class="_formPanel" style="padding: 16px;">
<MkButton @click="config(plugin)" inline v-if="plugin.config"><Fa :icon="faCog"/> {{ $ts.settings }}</MkButton>
<MkButton @click="uninstall(plugin)" inline danger><Fa :icon="faTrashAlt"/> {{ $ts.uninstall }}</MkButton>
</div>
</div>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInfo from '@/components/ui/info.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkSelect,
MkInfo,
FormSwitch,
FormBase,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$ts._plugin.manage,
icon: faPlug
},
plugins: ColdDeviceStorage.get('plugins'),
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
uninstall(plugin) {
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
os.success();
this.$nextTick(() => {
location.reload();
});
},
// TODO: storeactionAiScriptAPI
async config(plugin) {
const config = plugin.config;
for (const key in plugin.configData) {
config[key].default = plugin.configData[key];
}
const { canceled, result } = await os.form(plugin.name, config);
if (canceled) return;
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).configData = result;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).active = active;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
}
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,44 @@
<template>
<FormBase>
<FormLink to="/settings/plugin/install"><template #icon><Fa :icon="faDownload"/></template>{{ $ts._plugin.install }}</FormLink>
<FormLink to="/settings/plugin/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
FormBase,
FormLink,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$ts.plugins,
icon: faPlug
},
plugins: ColdDeviceStorage.get('plugins').length,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
mounted() {
this.$emit('info', this.INFO);
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,211 +0,0 @@
<template>
<section class="_section">
<div class="_title"><Fa :icon="faPlug"/> {{ $ts.plugins }}</div>
<div class="_content">
<details>
<summary><Fa :icon="faDownload"/> {{ $ts.install }}</summary>
<MkInfo warn>{{ $ts.pluginInstallWarn }}</MkInfo>
<MkTextarea v-model:value="script" tall>
<span>{{ $ts.script }}</span>
</MkTextarea>
<MkButton @click="install()" primary><Fa :icon="faSave"/> {{ $ts.install }}</MkButton>
</details>
</div>
<div class="_content">
<details>
<summary><Fa :icon="faFolderOpen"/> {{ $ts.manage }}</summary>
<MkSelect v-model:value="selectedPluginId">
<option v-for="x in plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
</MkSelect>
<template v-if="selectedPlugin">
<div style="margin: -8px 0 8px 0;">
<MkSwitch :value="selectedPlugin.active" @update:value="changeActive(selectedPlugin, $event)">{{ $ts.makeActive }}</MkSwitch>
</div>
<div class="_keyValue">
<div>{{ $ts.version }}:</div>
<div>{{ selectedPlugin.version }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.author }}:</div>
<div>{{ selectedPlugin.author }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.description }}:</div>
<div>{{ selectedPlugin.description }}</div>
</div>
<div style="margin-top: 8px;">
<MkButton @click="config()" inline v-if="selectedPlugin.config"><Fa :icon="faCog"/> {{ $ts.settings }}</MkButton>
<MkButton @click="uninstall()" inline><Fa :icon="faTrashAlt"/> {{ $ts.uninstall }}</MkButton>
</div>
</template>
</details>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInfo from '@/components/ui/info.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkSelect,
MkInfo,
MkSwitch,
},
data() {
return {
script: '',
plugins: ColdDeviceStorage.get('plugins'),
selectedPluginId: null,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
computed: {
selectedPlugin() {
if (this.selectedPluginId == null) return null;
return this.plugins.find(x => x.id === this.selectedPluginId);
},
},
methods: {
installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', this.plugins.concat({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
}));
},
async install() {
let ast;
try {
ast = parse(this.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const data = meta.get(null);
if (data == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
os.dialog({
type: 'error',
text: 'Required property not found :('
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(import('@/components/token-generate-window.vue'), {
title: this.$ts.tokenRequested,
information: this.$ts.pluginTokenRequestedDescription,
initialName: name,
initialPermissions: permissions
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
}
}, 'closed');
});
this.installPlugin({
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
os.success();
this.$nextTick(() => {
location.reload();
});
},
uninstall() {
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== this.selectedPluginId));
os.success();
this.$nextTick(() => {
location.reload();
});
},
// TODO: storeactionAiScriptAPI
async config() {
const config = this.selectedPlugin.config;
for (const key in this.selectedPlugin.configData) {
config[key].default = this.selectedPlugin.configData[key];
}
const { canceled, result } = await os.form(this.selectedPlugin.name, config);
if (canceled) return;
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === this.selectedPluginId).configData = result;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).active = active;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
}
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -58,7 +58,7 @@
<FormLink to="/advanced-theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>
</FormGroup>
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}</FormLink>
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
</FormBase>
</template>
@ -106,6 +106,7 @@ export default defineComponent({
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(localStorage.getItem('wallpaper'));
const themesCount = installedThemes.value.length;
watch(darkTheme, () => {
if (defaultStore.state.darkMode) {
@ -150,6 +151,7 @@ export default defineComponent({
lightTheme,
darkMode,
syncDeviceDarkMode,
themesCount,
wallpaper,
setWallpaper(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {

View File

@ -4,12 +4,12 @@
<div class="_formLabel">{{ $ts.backgroundColor }}</div>
<div class="_formPanel colors">
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor?.color === color.color }">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
<div class="preview" :style="{ background: color.forPreview }"></div>
</button>
</div>
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor?.color === color.color }">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
<div class="preview" :style="{ background: color.forPreview }"></div>
</button>
</div>
@ -19,7 +19,7 @@
<div class="_formLabel">{{ $ts.accentColor }}</div>
<div class="_formPanel colors">
<div class="row">
<button v-for="color in accentColors" :key="color" @click="accentColor = color" class="color rounded _button" :class="{ active: accentColor === color }">
<button v-for="color in accentColors" :key="color" @click="setAccentColor(color)" class="color rounded _button" :class="{ active: theme.props.accent === color }">
<div class="preview" :style="{ background: color }"></div>
</button>
</div>
@ -29,34 +29,40 @@
<div class="_formLabel">{{ $ts.textColor }}</div>
<div class="_formPanel colors">
<div class="row">
<button v-for="color in fgColors" :key="color" @click="fgColor = color" class="color char _button" :class="{ active: fgColor === color }">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : bgColor?.kind === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
<button v-for="color in fgColors" :key="color" @click="setFgColor(color)" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
</button>
</div>
</div>
</div>
<div class="_formItem preview">
<div class="_formLabel">{{ $ts.preview }}</div>
<div class="_formPanel preview">
<MkSample class="preview"/>
</div>
</div>
<FormButton @click="saveAs" primary>{{ $ts.saveAs }}</FormButton>
<FormGroup v-if="codeEnabled">
<FormTextarea v-model:value="themeCode" tall>
<span>{{ $ts._theme.code }}</span>
</FormTextarea>
<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton>
</FormGroup>
<FormButton v-else @click="codeEnabled = true"><Fa :icon="faCode"/> {{ $ts.editCode }}</FormButton>
<FormGroup>
<FormButton @click="showPreview"><Fa :icon="faEye"/> {{ $ts.preview }}</FormButton>
<FormButton @click="saveAs" primary><Fa :icon="faSave"/> {{ $ts.saveAs }}</FormButton>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
import { faPalette, faSave, faEye, faCode } from '@fortawesome/free-solid-svg-icons';
import { toUnicode } from 'punycode';
import * as tinycolor from 'tinycolor2';
import { v4 as uuid} from 'uuid';
import * as JSON5 from 'json5';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import MkSample from '@/components/sample.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormGroup from '@/components/form/group.vue';
import { Theme, applyTheme, validateTheme } from '@/scripts/theme';
import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@/scripts/theme';
import { host } from '@/config';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
@ -66,7 +72,8 @@ export default defineComponent({
components: {
FormBase,
FormButton,
MkSample,
FormTextarea,
FormGroup,
},
data() {
@ -75,6 +82,12 @@ export default defineComponent({
title: this.$ts.themeEditor,
icon: faPalette,
},
theme: {
base: 'light',
props: lightTheme.props
} as Theme,
codeEnabled: false,
themeCode: null,
bgColors: [
{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
@ -93,9 +106,7 @@ export default defineComponent({
{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
{ color: '#191919', kind: 'dark', forPreview: '#272727' },
],
bgColor: null,
accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
accentColor: null,
fgColors: [
{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
@ -105,27 +116,13 @@ export default defineComponent({
{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
],
fgColor: null,
changed: false,
faPalette,
faPalette, faSave, faEye, faCode,
}
},
created() {
const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg');
const matchedBgColor = this.bgColors.find(x => tinycolor(x.color).toRgbString() === tinycolor(currentBgColor).toRgbString());
if (matchedBgColor) this.bgColor = matchedBgColor;
const currentAccentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent');
const matchedAccentColor = this.accentColors.find(x => tinycolor(x).toRgbString() === tinycolor(currentAccentColor).toRgbString());
if (matchedAccentColor) this.accentColor = matchedAccentColor;
const currentFgColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(currentFgColor).toRgbString()));
if (matchedFgColor) this.fgColor = matchedFgColor;
this.$watch('bgColor', this.apply);
this.$watch('accentColor', this.apply);
this.$watch('fgColor', this.apply);
this.$watch('theme', this.apply, { deep: true });
window.addEventListener('beforeunload', this.beforeunload);
},
@ -156,28 +153,58 @@ export default defineComponent({
return !canceled;
},
convert(): Theme {
return {
name: this.$ts.myTheme,
base: this.bgColor.kind,
props: {
bg: this.bgColor.color,
fg: this.bgColor.kind === 'light' ? this.fgColor.forLight : this.fgColor.forDark,
accent: this.accentColor,
showPreview() {
os.pageWindow('preview');
},
setBgColor(color) {
if (this.theme.base != color.kind) {
const base = color.kind === 'dark' ? darkTheme : lightTheme;
for (const prop of Object.keys(base.props)) {
if (prop === 'accent') continue;
if (prop === 'fg') continue;
this.theme.props[prop] = base.props[prop];
}
};
}
this.theme.base = color.kind;
this.theme.props.bg = color.color;
if (this.theme.props.fg) {
const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString()));
if (matchedFgColor) this.setFgColor(matchedFgColor);
}
},
setAccentColor(color) {
this.theme.props.accent = color;
},
setFgColor(color) {
this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark;
},
apply() {
if (this.bgColor == null) this.bgColor = this.bgColors[0];
if (this.accentColor == null) this.accentColor = this.accentColors[0];
if (this.fgColor == null) this.fgColor = this.fgColors[0];
const theme = this.convert();
applyTheme(theme, false);
this.themeCode = JSON5.stringify(this.theme, null, '\t');
applyTheme(this.theme, false);
this.changed = true;
},
applyThemeCode() {
let parsed;
try {
parsed = JSON5.parse(this.themeCode);
} catch (e) {
os.dialog({
type: 'error',
text: this.$ts._theme.invalid
});
return;
}
this.theme = parsed;
},
async saveAs() {
const { canceled, result: name } = await os.dialog({
title: this.$ts.name,
@ -187,21 +214,20 @@ export default defineComponent({
});
if (canceled) return;
const theme = this.convert();
theme.id = uuid();
theme.name = name;
theme.author = `@${this.$i.username}@${toUnicode(host)}`;
addTheme(theme);
applyTheme(theme);
this.theme.id = uuid();
this.theme.name = name;
this.theme.author = `@${this.$i.username}@${toUnicode(host)}`;
addTheme(this.theme);
applyTheme(this.theme);
if (this.$store.state.darkMode) {
ColdDeviceStorage.set('darkTheme', theme.id);
ColdDeviceStorage.set('darkTheme', this.theme.id);
} else {
ColdDeviceStorage.set('lightTheme', theme.id);
ColdDeviceStorage.set('lightTheme', this.theme.id);
}
this.changed = false;
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
text: this.$t('_theme.installed', { name: this.theme.name })
});
}
}
@ -265,10 +291,5 @@ export default defineComponent({
}
}
}
> .preview > .preview > .preview {
box-shadow: none;
background: transparent;
}
}
</style>

View File

@ -39,7 +39,7 @@ export function install(plugin) {
function createPluginEnv(opts) {
const config = new Map();
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default));
}
return {

View File

@ -76,6 +76,7 @@ export const router = createRouter({
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
{ path: '/api-console', component: page('api-console') },
{ path: '/preview', component: page('preview') },
{ path: '/test', component: page('test') },
{ path: '/auth/:token', component: page('auth') },
{ path: '/miauth/:session', component: page('miauth') },

View File

@ -0,0 +1,109 @@
// blocks
export type BlockBase = {
id: string;
type: string;
};
export type TextBlock = BlockBase & {
type: 'text';
text: string;
};
export type SectionBlock = BlockBase & {
type: 'section';
title: string;
children: (Block | VarBlock)[];
};
export type ImageBlock = BlockBase & {
type: 'image';
fileId: string | null;
};
export type ButtonBlock = BlockBase & {
type: 'button';
text: any;
primary: boolean;
action: string;
content: string;
event: string;
message: string;
var: string;
fn: string;
};
export type IfBlock = BlockBase & {
type: 'if';
var: string;
children: Block[];
};
export type TextareaBlock = BlockBase & {
type: 'textarea';
text: string;
};
export type PostBlock = BlockBase & {
type: 'post';
text: string;
attachCanvasImage: boolean;
canvasId: string;
};
export type CanvasBlock = BlockBase & {
type: 'canvas';
name: string; // canvas id
width: number;
height: number;
};
export type NoteBlock = BlockBase & {
type: 'note';
detailed: boolean;
note: string | null;
};
export type Block =
TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock;
// variable blocks
export type VarBlockBase = BlockBase & {
name: string;
};
export type NumberInputVarBlock = VarBlockBase & {
type: 'numberInput';
text: string;
};
export type TextInputVarBlock = VarBlockBase & {
type: 'textInput';
text: string;
};
export type SwitchVarBlock = VarBlockBase & {
type: 'switch';
text: string;
};
export type RadioButtonVarBlock = VarBlockBase & {
type: 'radioButton';
title: string;
values: string[];
};
export type CounterVarBlock = VarBlockBase & {
type: 'counter';
text: string;
inc: number;
};
export type VarBlock =
NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock;
const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter'];
export function isVarBlock(block: Block): block is VarBlock {
return varBlock.includes(block.type);
}

View File

@ -1,12 +1,13 @@
import autobind from 'autobind-decorator';
import { Variable, PageVar, envVarsDef, Block, isFnBlock, Fn, HpmlScope, HpmlError } from '.';
import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.';
import { version } from '@/config';
import { AiScript, utils, values } from '@syuilo/aiscript';
import { createAiScriptEnv } from '../aiscript/api';
import { collectPageVars } from '../collect-page-vars';
import { initHpmlLib, initAiLib } from './lib';
import * as os from '@/os';
import { markRaw, ref, Ref } from 'vue';
import { markRaw, ref, Ref, unref } from 'vue';
import { Expr, isLiteralValue, Variable } from './expr';
/**
* Hpml evaluator
@ -94,7 +95,7 @@ export class Hpml {
public interpolate(str: string) {
if (str == null) return null;
return str.replace(/{(.+?)}/g, match => {
const v = this.vars[match.slice(1, -1).trim()];
const v = unref(this.vars)[match.slice(1, -1).trim()];
return v == null ? 'NULL' : v.toString();
});
}
@ -158,72 +159,76 @@ export class Hpml {
}
@autobind
private evaluate(block: Block, scope: HpmlScope): any {
if (block.type === null) {
return null;
}
private evaluate(expr: Expr, scope: HpmlScope): any {
if (block.type === 'number') {
return parseInt(block.value, 10);
}
if (block.type === 'text' || block.type === 'multiLineText') {
return this._interpolateScope(block.value || '', scope);
}
if (block.type === 'textList') {
return this._interpolateScope(block.value || '', scope).trim().split('\n');
}
if (block.type === 'ref') {
return scope.getState(block.value);
}
if (block.type === 'aiScriptVar') {
if (this.aiscript) {
try {
return utils.valToJs(this.aiscript.scope.get(block.value));
} catch (e) {
return null;
}
} else {
if (isLiteralValue(expr)) {
if (expr.type === null) {
return null;
}
}
// Define user function
if (isFnBlock(block)) {
return {
slots: block.value.slots.map(x => x.name),
exec: (slotArg: Record<string, any>) => {
return this.evaluate(block.value.expression, scope.createChildScope(slotArg, block.id));
if (expr.type === 'number') {
return parseInt((expr.value as any), 10);
}
if (expr.type === 'text' || expr.type === 'multiLineText') {
return this._interpolateScope(expr.value || '', scope);
}
if (expr.type === 'textList') {
return this._interpolateScope(expr.value || '', scope).trim().split('\n');
}
if (expr.type === 'ref') {
return scope.getState(expr.value);
}
if (expr.type === 'aiScriptVar') {
if (this.aiscript) {
try {
return utils.valToJs(this.aiscript.scope.get(expr.value));
} catch (e) {
return null;
}
} else {
return null;
}
} as Fn;
}
// Define user function
if (expr.type == 'fn') {
return {
slots: expr.value.slots.map(x => x.name),
exec: (slotArg: Record<string, any>) => {
return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id));
}
} as Fn;
}
return;
}
// Call user function
if (block.type.startsWith('fn:')) {
const fnName = block.type.split(':')[1];
if (expr.type.startsWith('fn:')) {
const fnName = expr.type.split(':')[1];
const fn = scope.getState(fnName);
const args = {} as Record<string, any>;
for (let i = 0; i < fn.slots.length; i++) {
const name = fn.slots[i];
args[name] = this.evaluate(block.args[i], scope);
args[name] = this.evaluate(expr.args[i], scope);
}
return fn.exec(args);
}
if (block.args === undefined) return null;
if (expr.args === undefined) return null;
const funcs = initHpmlLib(block, scope, this.opts.randomSeed, this.opts.visitor);
const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor);
// Call function
const fnName = block.type;
const fnName = expr.type;
const fn = (funcs as any)[fnName];
if (fn == null) {
throw new HpmlError(`No such function '${fnName}'`);
} else {
return fn(...block.args.map(x => this.evaluate(x, scope)));
return fn(...expr.args.map(x => this.evaluate(x, scope)));
}
}
}

View File

@ -0,0 +1,79 @@
import { literalDefs, Type } from '.';
export type ExprBase = {
id: string;
};
// value
export type EmptyValue = ExprBase & {
type: null;
value: null;
};
export type TextValue = ExprBase & {
type: 'text';
value: string;
};
export type MultiLineTextValue = ExprBase & {
type: 'multiLineText';
value: string;
};
export type TextListValue = ExprBase & {
type: 'textList';
value: string;
};
export type NumberValue = ExprBase & {
type: 'number';
value: number;
};
export type RefValue = ExprBase & {
type: 'ref';
value: string; // value is variable name
};
export type AiScriptRefValue = ExprBase & {
type: 'aiScriptVar';
value: string; // value is variable name
};
export type UserFnValue = ExprBase & {
type: 'fn';
value: UserFnInnerValue;
};
type UserFnInnerValue = {
slots: {
name: string;
type: Type;
}[];
expression: Expr;
};
export type Value =
EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue;
export function isLiteralValue(expr: Expr): expr is Value {
if (expr.type == null) return true;
if (literalDefs[expr.type]) return true;
return false;
}
// call function
export type CallFn = ExprBase & { // "fn:hoge" or string
type: string;
args: Expr[];
value: null;
};
// variable
export type Variable = (Value | CallFn) & {
name: string;
};
// expression
export type Expr = Variable | Value | CallFn;

View File

@ -3,52 +3,16 @@
*/
import autobind from 'autobind-decorator';
import {
faMagic,
faSquareRootAlt,
faAlignLeft,
faShareAlt,
faPlus,
faMinus,
faTimes,
faDivide,
faList,
faQuoteRight,
faEquals,
faGreaterThan,
faLessThan,
faGreaterThanEqual,
faLessThanEqual,
faNotEqual,
faDice,
faSortNumericUp,
faExchangeAlt,
faRecycle,
faIndent,
faCalculator,
} from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
import { Hpml } from './evaluator';
export type Block<V = any> = {
id: string;
type: string;
args: Block[];
value: V;
};
export type FnBlock = Block<{
slots: {
name: string;
type: Type;
}[];
expression: Block;
}>;
export type Variable = Block & {
name: string;
};
import { funcDefs } from './lib';
export type Fn = {
slots: string[];
@ -57,46 +21,6 @@ export type Fn = {
export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: faShareAlt, },
for: { in: ['number', 'function'], out: null, category: 'flow', icon: faRecycle, },
not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faPlus, },
subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faMinus, },
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, },
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
round: { in: ['number'], out: 'number', category: 'operation', icon: faCalculator, },
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, },
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, },
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, },
lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThan, },
gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThanEqual, },
ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThanEqual, },
strLen: { in: ['string'], out: 'number', category: 'text', icon: faQuoteRight, },
strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: faQuoteRight, },
strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: faQuoteRight, },
strReverse: { in: ['string'], out: 'string', category: 'text', icon: faQuoteRight, },
join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: faQuoteRight, },
stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: faExchangeAlt, },
numberToString: { in: ['number'], out: 'string', category: 'convert', icon: faExchangeAlt, },
splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: faExchangeAlt, },
pick: { in: [null, 'number'], out: null, category: 'list', icon: faIndent, },
listLen: { in: [null], out: 'number', category: 'list', icon: faIndent, },
rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: faDice, },
random: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: faDice, },
randomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
dailyRandomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: faDice, },
DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: faDice, }, // dailyRandomPickWithProbabilityMapping
};
export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
text: { out: 'string', category: 'value', icon: faQuoteRight, },
multiLineText: { out: 'string', category: 'value', icon: faAlignLeft, },
@ -116,10 +40,6 @@ export const blockDefs = [
}))
];
export function isFnBlock(block: Block): block is FnBlock {
return block.type === 'fn';
}
export type PageVar = { name: string; value: any; type: Type; };
export const envVarsDef: Record<string, Type> = {
@ -140,12 +60,6 @@ export const envVarsDef: Record<string, Type> = {
NULL: null,
};
export function isLiteralBlock(v: Block) {
if (v.type === null) return true;
if (literalDefs[v.type]) return true;
return false;
}
export class HpmlScope {
private layerdStates: Record<string, any>[];
public name: string;

View File

@ -2,9 +2,31 @@ import * as tinycolor from 'tinycolor2';
import Chart from 'chart.js';
import { Hpml } from './evaluator';
import { values, utils } from '@syuilo/aiscript';
import { Block, Fn, HpmlScope } from '.';
import { Fn, HpmlScope } from '.';
import { Expr } from './expr';
import * as seedrandom from 'seedrandom';
import {
faShareAlt,
faPlus,
faMinus,
faTimes,
faDivide,
faQuoteRight,
faEquals,
faGreaterThan,
faLessThan,
faGreaterThanEqual,
faLessThanEqual,
faNotEqual,
faDice,
faExchangeAlt,
faRecycle,
faIndent,
faCalculator,
} from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
Chart.pluginService.register({
beforeDraw: (chart, easing) => {
@ -125,7 +147,47 @@ export function initAiLib(hpml: Hpml) {
};
}
export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string, visitor?: any) {
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: faShareAlt, },
for: { in: ['number', 'function'], out: null, category: 'flow', icon: faRecycle, },
not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faPlus, },
subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faMinus, },
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, },
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
round: { in: ['number'], out: 'number', category: 'operation', icon: faCalculator, },
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, },
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, },
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, },
lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThan, },
gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThanEqual, },
ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThanEqual, },
strLen: { in: ['string'], out: 'number', category: 'text', icon: faQuoteRight, },
strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: faQuoteRight, },
strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: faQuoteRight, },
strReverse: { in: ['string'], out: 'string', category: 'text', icon: faQuoteRight, },
join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: faQuoteRight, },
stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: faExchangeAlt, },
numberToString: { in: ['number'], out: 'string', category: 'convert', icon: faExchangeAlt, },
splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: faExchangeAlt, },
pick: { in: [null, 'number'], out: null, category: 'list', icon: faIndent, },
listLen: { in: [null], out: 'number', category: 'list', icon: faIndent, },
rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: faDice, },
random: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: faDice, },
randomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
dailyRandomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: faDice, },
DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: faDice, }, // dailyRandomPickWithProbabilityMapping
};
export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
const date = new Date();
const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
@ -166,12 +228,12 @@ export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string,
splitStrByLine: (a: string) => a.split('\n'),
pick: (list: any[], i: number) => list[i - 1],
listLen: (list: any[]) => list.length,
random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * 100) < probability,
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * (max - min + 1)),
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * list.length)],
dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability,
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)),
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)],
dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability,
dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)),
dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)],
seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
@ -185,7 +247,7 @@ export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string,
totalFactor += factor;
xs.push({ factor, text });
}
const r = seedrandom(`${day}:${block.id}`)() * totalFactor;
const r = seedrandom(`${day}:${expr.id}`)() * totalFactor;
let stackedFactor = 0;
for (const x of xs) {
if (r >= stackedFactor && r <= stackedFactor + x.factor) {

View File

@ -1,5 +1,7 @@
import autobind from 'autobind-decorator';
import { Type, Block, funcDefs, envVarsDef, Variable, PageVar, isLiteralBlock } from '.';
import { Type, envVarsDef, PageVar } from '.';
import { Expr, isLiteralValue, Variable } from './expr';
import { funcDefs } from './lib';
type TypeError = {
arg: number;
@ -20,10 +22,10 @@ export class HpmlTypeChecker {
}
@autobind
public typeCheck(v: Block): TypeError | null {
if (isLiteralBlock(v)) return null;
public typeCheck(v: Expr): TypeError | null {
if (isLiteralValue(v)) return null;
const def = funcDefs[v.type];
const def = funcDefs[v.type || ''];
if (def == null) {
throw new Error('Unknown type: ' + v.type);
}
@ -58,8 +60,8 @@ export class HpmlTypeChecker {
}
@autobind
public getExpectedType(v: Block, slot: number): Type {
const def = funcDefs[v.type];
public getExpectedType(v: Expr, slot: number): Type {
const def = funcDefs[v.type || ''];
if (def == null) {
throw new Error('Unknown type: ' + v.type);
}
@ -86,7 +88,7 @@ export class HpmlTypeChecker {
}
@autobind
public infer(v: Block): Type {
public infer(v: Expr): Type {
if (v.type === null) return null;
if (v.type === 'text') return 'string';
if (v.type === 'multiLineText') return 'string';
@ -103,7 +105,7 @@ export class HpmlTypeChecker {
return pageVar.type;
}
const envVar = envVarsDef[v.value];
const envVar = envVarsDef[v.value || ''];
if (envVar !== undefined) {
return envVar;
}

View File

@ -0,0 +1,44 @@
// Notice: Service Workerでも使用します
export class I18n<T extends Record<string, any>> {
public locale: T;
constructor(locale: T) {
this.locale = locale;
if (_DEV_) {
console.log('i18n', this.locale);
}
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
public t(key: string, args?: Record<string, any>): string {
try {
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
if (_DEV_) {
if (!str.includes('{')) {
console.warn(`i18n: '${key}' has no any arg. so ref prop directly instead of call this method.`);
}
}
if (args) {
for (const [k, v] of Object.entries(args)) {
str = str.replace(`{${k}}`, v);
}
}
return str;
} catch (e) {
if (_DEV_) {
console.warn(`missing localization '${key}'`);
return `⚠'${key}'⚠`;
}
return key;
}
}
}

View File

@ -0,0 +1,68 @@
import { instance } from '@/instance';
import { $i } from '@/account';
import { api } from '@/os';
import { lang } from '@/config';
export async function initializeSw() {
if (instance.swPublickey &&
('serviceWorker' in navigator) &&
('PushManager' in window) &&
$i && $i.token) {
navigator.serviceWorker.register(`/sw.js`);
navigator.serviceWorker.ready.then(registration => {
registration.active?.postMessage({
msg: 'initialize',
lang,
});
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
}).then(subscription => {
function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
// Register
api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
});
})
// When subscribe failed
.catch(async (err: Error) => {
// 通知が許可されていなかったとき
if (err.name === 'NotAllowedError') {
return;
}
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
// 既に存在していることが原因でエラーになった可能性があるので、
// そのサブスクリプションを解除しておく
const subscription = await registration.pushManager.getSubscription();
if (subscription) subscription.unsubscribe();
});
});
}
}
/**
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

View File

@ -481,6 +481,12 @@ hr {
outline: none;
}
._zoom {
transition-duration: 0.5s, 0.5s;
transition-property: opacity, transform;
transition-timing-function: cubic-bezier(0,.5,.5,1);
}
.zoom-enter-active, .zoom-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}

View File

@ -1,8 +1,17 @@
/**
* Notification composer of Service Worker
*/
declare var self: ServiceWorkerGlobalScope;
import { getNoteSummary } from '../../misc/get-note-summary';
import getUserName from '../../misc/get-user-name';
import { i18n } from '@/sw/i18n';
export default async function(type, data): Promise<[string, NotificationOptions]> {
export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> {
if (!i18n) {
console.log('no i18n');
return;
}
switch (type) {
case 'driveFileCreated': // TODO (Server Side)
return [i18n.t('_notification.fileUploaded'), {

View File

@ -1,5 +0,0 @@
import { I18n } from '@/i18n';
export const i18n = new I18n({
// TODO
});

View File

@ -3,17 +3,30 @@
*/
declare var self: ServiceWorkerGlobalScope;
import { get, set } from 'idb-keyval';
import composeNotification from '@/sw/compose-notification';
import { I18n } from '@/scripts/i18n';
//#region Variables
const version = _VERSION_;
const cacheName = `mk-cache-${version}`;
const apiUrl = `${location.origin}/api/`;
// インストールされたとき
self.addEventListener('install', ev => {
console.info('installed');
let lang: string;
let i18n: I18n<any>;
let pushesPool: any[] = [];
//#endregion
//#region Startup
get('lang').then(async prelang => {
if (!prelang) return;
lang = prelang;
return fetchLocale();
});
//#endregion
//#region Lifecycle: Install
self.addEventListener('install', ev => {
ev.waitUntil(
caches.open(cacheName)
.then(cache => {
@ -24,7 +37,9 @@ self.addEventListener('install', ev => {
.then(() => self.skipWaiting())
);
});
//#endregion
//#region Lifecycle: Activate
self.addEventListener('activate', ev => {
ev.waitUntil(
caches.keys()
@ -36,7 +51,10 @@ self.addEventListener('activate', ev => {
.then(() => self.clients.claim())
);
});
//#endregion
// TODO: 消せるかも ref. https://github.com/syuilo/misskey/pull/7108#issuecomment-774573666
//#region When: Fetching
self.addEventListener('fetch', ev => {
if (ev.request.method !== 'GET' || ev.request.url.startsWith(apiUrl)) return;
ev.respondWith(
@ -49,8 +67,9 @@ self.addEventListener('fetch', ev => {
})
);
});
//#endregion
// プッシュ通知を受け取ったとき
//#region When: Caught Notification
self.addEventListener('push', ev => {
// クライアント取得
ev.waitUntil(self.clients.matchAll({
@ -59,8 +78,65 @@ self.addEventListener('push', ev => {
// クライアントがあったらストリームに接続しているということなので通知しない
if (clients.length != 0) return;
const { type, body } = ev.data.json();
const { type, body } = ev.data?.json();
return self.registration.showNotification(...(await composeNotification(type, body)));
// localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく
if (!i18n) return pushesPool.push({ type, body });
const n = await composeNotification(type, body, i18n);
if (n) return self.registration.showNotification(...n);
}));
});
//#endregion
//#region When: Caught a message from the client
self.addEventListener('message', ev => {
switch(ev.data) {
case 'clear':
return; // TODO
default:
break;
}
if (typeof ev.data === 'object') {
// E.g. '[object Array]' → 'array'
const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
if (otype === 'object') {
if (ev.data.msg === 'initialize') {
lang = ev.data.lang;
set('lang', lang);
fetchLocale();
}
}
}
});
//#endregion
//#region Function: (Re)Load i18n instance
async function fetchLocale() {
//#region localeファイルの読み込み
// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
const localeUrl = `/assets/locales/${lang}.${version}.json`;
let localeRes = await caches.match(localeUrl);
if (!localeRes) {
localeRes = await fetch(localeUrl);
const clone = localeRes?.clone();
if (!clone?.clone().ok) return;
caches.open(cacheName).then(cache => cache.put(localeUrl, clone));
}
i18n = new I18n(await localeRes.json());
//#endregion
//#region i18nをきちんと読み込んだ後にやりたい処理
for (const { type, body } of pushesPool) {
const n = await composeNotification(type, body, i18n);
if (n) self.registration.showNotification(...n);
}
pushesPool = [];
//#endregion
}
//#endregion

View File

@ -57,6 +57,13 @@ export default defineComponent({
},
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;

View File

@ -187,6 +187,13 @@ export default defineComponent({
},
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;

View File

@ -0,0 +1,164 @@
<template>
<MkContainer :show-header="props.showHeader">
<template #header><Fa :icon="faTerminal"/>{{ $ts._widgets.aiscript }}</template>
<div class="uylguesu _monospace">
<textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
<button @click="run" class="_buttonPrimary">RUN</button>
<div class="logs">
<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
</div>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faTerminal } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import * as os from '@/os';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
const widget = define({
name: 'aiscript',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
script: {
type: 'string',
multiline: true,
default: '(1 + 1)',
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer
},
data() {
return {
logs: [],
faTerminal
};
},
methods: {
async run() {
this.logs = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget'
}), {
in: (q) => {
return new Promise(ok => {
os.dialog({
title: q,
input: {}
}).then(({ canceled, result: a }) => {
ok(a);
});
});
},
out: (value) => {
this.logs.push({
id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value),
print: true
});
},
log: (type, params) => {
switch (type) {
case 'end': this.logs.push({
id: Math.random(),
text: utils.valToString(params.val, true),
print: false
}); break;
default: break;
}
}
});
let ast;
try {
ast = parse(this.props.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.dialog({
type: 'error',
text: e
});
}
},
}
});
</script>
<style lang="scss" scoped>
.uylguesu {
text-align: right;
> textarea {
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--fg);
background: transparent;
border: none;
border-bottom: solid 1px var(--divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
&:focus {
outline: none;
}
}
> button {
display: inline-block;
margin: 8px;
padding: 0 10px;
height: 28px;
outline: none;
border-radius: 4px;
&:disabled {
opacity: 0.7;
cursor: default;
}
}
> .logs {
border-top: solid 1px var(--divider);
text-align: left;
padding: 16px;
&:empty {
display: none;
}
> .log {
&:not(.print) {
opacity: 0.7;
}
}
}
}
</style>

View File

@ -18,6 +18,7 @@ export default function(app: App) {
app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
}
export const widgets = [
@ -38,4 +39,5 @@ export const widgets = [
'onlineUsers',
'jobQueue',
'button',
'aiscript',
];

View File

@ -74,12 +74,18 @@ export default defineComponent({
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--inputText);
background: var(--face);
color: var(--fg);
background: transparent;
border: none;
border-bottom: solid var(--lineWidth) var(--faceDivider);
border-bottom: solid 1px var(--divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
font-size: 0.9em;
&:focus {
outline: none;
}
}
> button {

View File

@ -20,7 +20,7 @@ const path = process.env.NODE_ENV === 'test'
: `${dir}/default.yml`;
export default function load() {
const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const mixin = {} as Mixin;

View File

@ -33,7 +33,7 @@ The shortcuts listed here can be used basically everywhere.
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>Delete post</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>Open post context menu</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Toggle show or hide of content marked with CW</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>Unfocus</td><td>-</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>Deselect Note</td><td>-</td></tr>
</tbody>
</table>

View File

@ -10,7 +10,7 @@
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>検索</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Cerca</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
</table>

View File

@ -1,4 +1,4 @@
# ミュート
# Silenzia
ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:

View File

@ -322,33 +322,33 @@ Misskey提供一种被称为“帖子抓取”的机制。该功能以流的形
当您被某人关注时会触发该事件。
## `homeTimeline`
ホームタイムラインの投稿情報が流れてきます。该频道没有参数。
首页的时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
タイムラインに新しい投稿が流れてきたときに発生するイベントです
当时间线有新帖子时触发此事件
## `localTimeline`
ローカルタイムラインの投稿情報が流れてきます。该频道没有参数。
本地的时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです
当本地的时间线有新帖子时触发此事件
## `hybridTimeline`
ソーシャルタイムラインの投稿情報が流れてきます。该频道没有参数。
社交时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
ソーシャルタイムラインに新しい投稿が流れてきたときに発生するイベントです
当社交时间线有新帖子时触发此事件
## `globalTimeline`
グローバルタイムラインの投稿情報が流れてきます。该频道没有参数。
全局时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
グローバルタイムラインに新しい投稿が流れてきたときに発生するイベントです
全局时间线有新帖子时触发此事件

View File

@ -31,8 +31,6 @@
},
}
```
* `id` ... 该主题的唯一 ID推荐采用 UUID。

View File

@ -1,2 +1,2 @@
# 自訂表情符號
カスタム絵文字は、インスタンスで用意された画像を絵文字のように使える機能です。 ノート、リアクション、チャット、自己紹介、名前などの場所で使うことができます。 カスタム絵文字をそれらの場所で使うには、絵文字ピッカーボタン(ある場合)を押すか、`:`を入力して絵文字サジェストを表示します。 テキスト内に`:foo:`のような形式の文字列が見つかると、`foo`の部分がカスタム絵文字名と解釈され、表示時には対応したカスタム絵文字に置き換わります
表情符號功能可以讓您在各個地方使用預置的圖像表情。 它可以用於發帖、回應、聊天、自我介紹和姓名等地方。 要在這些位置使用自定義表情符號,請按表情符號選擇按鈕(如果有)或鍵入`:`以顯示表情符號建議。 如果在文本中找到格式為`:foo:`的字符串,則將`foo`部分解釋為自定義表情符號名稱,並在顯示時替換為相應的自定義表情符號

View File

@ -1,2 +1,2 @@
# 追隨/解除追隨
當你追隨其他使用者時,你的時間軸上將出現他們的文。但是,他們對其他用戶的回覆不會被顯示。 若要追隨一個使用者,請點選其使用者頁面上的「追隨」按鈕。若要解除追隨,請再次點選「追隨」按鈕。
當你追隨其他使用者時,你的時間軸上將出現他們的文。但是,他們對其他用戶的回覆不會被顯示。 若要追隨一個使用者,請點選其使用者頁面上的「追隨」按鈕。若要解除追隨,請再次點選「追隨」按鈕。

View File

@ -1,14 +1,14 @@
# キーボードショートカット
# 鍵盤快速鍵
## 公開
これらのショートカットは基本的にどこでも使えます
這些快捷方式基本上可以在任何地方使用
<table>
<thead>
<tr><th>快速鍵</th><th>功能</th><th>由來</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>發佈</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>發佈</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>轉跳至時間軸最新發佈的內容</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>顯示/隱藏通知</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>搜尋</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>取得說明</td><td><b>H</b>elp</td></tr>
@ -29,11 +29,11 @@
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>お気に入りに登録</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>刪除</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>加入至我的最愛</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>刪除</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>顯示貼文選單</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>隱藏或顯示敏感媒體</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>取消選取</td><td>-</td></tr>
</tbody>
</table>
@ -44,7 +44,7 @@
<tr><th>快速鍵</th><th>功能</th><th>由來</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">Enter</kbd></td><td>轉發</td><td>-</td></tr>
<tr><td><kbd class="key">Enter</kbd></td><td>轉發</td><td>-</td></tr>
<tr><td><kbd class="key">Q</kbd></td><td>展開選單</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>關閉選單</td><td>-</td></tr>
</tbody>

View File

@ -1,13 +1,13 @@
# 靜音
ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
當你靜音某個帳戶時Misskey將停止推播該帳戶的以下內容
* タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote)
* そのユーザーからの通知
* メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
* 該用戶在時間軸及搜尋結果中的貼文(以及對這些貼文的回覆和轉發)。
* 該使用者的通知
* 使用者在訊息歷史記錄列表中的訊息歷史記錄
ユーザーをミュートするには、対象のユーザーのユーザーページに表示されている「ミュート」ボタンを押します
要靜音某個帳戶,可於其用戶檔案頁上點選**靜音**按鈕
ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません
被您靜音的用戶不會被通知已被靜音,也不會知道您已靜音他們
設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます
您可在**設定**>**靜音**中瀏覽已被您靜音的用戶

View File

@ -1,15 +1,15 @@
# 不同時間軸之間的分別
# 不同時間軸的差異
https://docs.google.com/spreadsheets/d/1lxQ2ugKrhz58Bg96HTDK_2F98BUritkMyIiBkOByjHA/edit?usp=sharing
## 首頁
自分のフォローしているユーザーの投稿
顯示已追隨使用者的貼文
## 本地
全てのローカルユーザーの「ホーム」指定されていない投稿
顯示所有的本地用戶的首頁貼文
## 社群
自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿
顯示已追隨使用者的貼文及所有的本地用戶的貼文
## 公開
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿
顯示已追隨使用者、所有的本地用戶及遠端使用者所發佈的的貼文

View File

@ -1,8 +1,10 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { urlRegexFull } from './prelude';
import * as parse5 from 'parse5';
import treeAdapter = require('parse5/lib/tree-adapters/default');
import { URL } from 'url';
import { urlRegex, urlRegexFull } from './prelude';
export function fromHtml(html: string, hashtagNames?: string[]): string {
const dom = parseFragment(html) as DefaultTreeDocumentFragment;
const dom = parse5.parseFragment(html);
let text = '';
@ -12,30 +14,35 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
return text.trim();
function getText(node: any): string {
if (node.nodeName === '#text') return node.value;
function getText(node: parse5.Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.childNodes) {
return node.childNodes.map((n: any) => getText(n)).join('');
return node.childNodes.map(n => getText(n)).join('');
}
return '';
}
function analyze(node: any) {
switch (node.nodeName) {
case '#text':
text += node.value;
break;
function analyze(node: parse5.Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
return;
}
// Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) {
case 'br':
text += '\n';
break;
case 'a':
const txt = getText(node);
const rel = node.attrs.find((x: any) => x.name === 'rel');
const href = node.attrs.find((x: any) => x.name === 'href');
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
@ -44,7 +51,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');
if (part.length === 2) {
if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`;
text += acct;
@ -54,11 +61,28 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
}
// その他
} else {
text += !href ? txt
: txt === href.value
? txt.match(urlRegexFull) ? txt
: `<${txt}>`
: `[${txt}](${href.value})`;
const generateLink = () => {
if (!href && !txt) {
return '';
}
if (!href) {
return txt;
}
if (!txt || txt === href.value) { // #6383: Missing text node
if (href.value.match(urlRegexFull)) {
return href.value;
} else {
return `<${href.value}>`;
}
}
if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
return `[${txt}](<${href.value}>)`; // #6846
} else {
return `[${txt}](${href.value})`;
}
};
text += generateLink();
}
break;

56
src/misc/captcha.ts Normal file
View File

@ -0,0 +1,56 @@
import fetch from 'node-fetch';
import { URLSearchParams } from 'url';
import { getAgentByUrl } from './fetch';
import config from '../config';
export async function verifyRecaptcha(secret: string, response: string) {
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
throw `recaptcha-request-failed: ${e}`;
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
throw `recaptcha-failed: ${errorCodes}`;
}
}
export async function verifyHcaptcha(secret: string, response: string) {
const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
throw `hcaptcha-request-failed: ${e}`;
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
throw `hcaptcha-failed: ${errorCodes}`;
}
}
type CaptchaResponse = {
success: boolean;
'error-codes'?: string[];
};
async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> {
const params = new URLSearchParams({
secret,
response
});
const res = await fetch(url, {
method: 'POST',
body: params,
headers: {
'User-Agent': config.userAgent
},
timeout: 10 * 1000,
agent: getAgentByUrl
}).catch(e => {
throw `${e.message || e}`;
});
if (!res.ok) {
throw `${res.status}`;
}
return await res.json() as CaptchaResponse;
}

View File

@ -1,29 +0,0 @@
import getUserName from './get-user-name';
import { getNoteSummary } from './get-note-summary';
import getReactionEmoji from './get-reaction-emoji';
import locales = require('../../locales');
/**
*
* @param notification
*/
export default function(notification: any): string {
switch (notification.type) {
case 'follow':
return `${getUserName(notification.user)}にフォローされました`;
case 'mention':
return `言及されました:\n${getUserName(notification.user)}${getNoteSummary(notification.note, locales['ja-JP'])}`;
case 'reply':
return `返信されました:\n${getUserName(notification.user)}${getNoteSummary(notification.note, locales['ja-JP'])}`;
case 'renote':
return `Renoteされました:\n${getUserName(notification.user)}${getNoteSummary(notification.note, locales['ja-JP'])}`;
case 'quote':
return `引用されました:\n${getUserName(notification.user)}${getNoteSummary(notification.note, locales['ja-JP'])}`;
case 'reaction':
return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note, locales['ja-JP'])}`;
case 'pollVote':
return `投票されました:\n${getUserName(notification.user)}${getNoteSummary(notification.note, locales['ja-JP'])}`;
default:
return `<不明な通知タイプ: ${notification.type}>`;
}
}

View File

@ -1,13 +1,13 @@
export function isMutedUserRelated(note: any, mutedUserIds: string[]): boolean {
if (mutedUserIds.includes(note.userId)) {
export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
if (mutedUserIds.has(note.userId)) {
return true;
}
if (note.reply != null && mutedUserIds.includes(note.reply.userId)) {
if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && mutedUserIds.includes(note.renote.userId)) {
if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
return true;
}

Some files were not shown because too many files have changed in this diff Show More