Compare commits
140 Commits
Author | SHA1 | Date | |
---|---|---|---|
0790dd7a2c | |||
408118a1e8 | |||
6a45bb21c3 | |||
5d4e9aa949 | |||
c87b98c2af | |||
5a13c38a6d | |||
67f60ab307 | |||
08c278578d | |||
0e01fb5fc3 | |||
d44dc7e00d | |||
82ee3a538b | |||
380cf0de69 | |||
11f25ea2e7 | |||
ef62497777 | |||
2824d8a5b6 | |||
1c84c0828e | |||
39e4494836 | |||
e7180d529a | |||
b8cd872738 | |||
efaaa76185 | |||
19e1f996a6 | |||
40a69bf200 | |||
9e3abb9989 | |||
5ba48e06f7 | |||
8b3a0a524b | |||
d9fe9cc5df | |||
b202c7906a | |||
b9c868cac6 | |||
33adf3c88d | |||
8b84f40975 | |||
fa131d2023 | |||
a83b38b50a | |||
dcd7b286ef | |||
b85bf769cd | |||
630a20d61e | |||
88c3794cf1 | |||
42eb457ad0 | |||
d153a8de20 | |||
7f7551f44c | |||
23082b55a4 | |||
dac7387a7f | |||
8c6856d894 | |||
2c0e514fb2 | |||
1917b0339e | |||
c05419f223 | |||
e0deaec695 | |||
d70e2a788e | |||
7343e6e2e8 | |||
106d4cc0d6 | |||
c98879cb7a | |||
9ca755c313 | |||
25e0b98840 | |||
5599c43c71 | |||
eb7597d7e4 | |||
d5767e92c4 | |||
ba3749d373 | |||
d8088acdf2 | |||
ad93e0aa3d | |||
691e58f03d | |||
95d5bccfca | |||
2aa8e0a4bf | |||
6e96d6677d | |||
8816c20f51 | |||
e955fe1ffd | |||
5cbcac713a | |||
2b50364ab4 | |||
fa04ac789e | |||
95ce8dce3d | |||
0b5eec4ca8 | |||
6d9716f90e | |||
aa31061d90 | |||
acc7797dff | |||
7959196dc7 | |||
c6ff6939a5 | |||
769960f29e | |||
d92e9759f3 | |||
bf7e19b288 | |||
98954cd6d4 | |||
538bb978ed | |||
10232c5866 | |||
5cd6a0db16 | |||
ff0a05a2d6 | |||
e34b264af2 | |||
00d79487cd | |||
3cace734c7 | |||
f428372869 | |||
5dd2feba9b | |||
a1b026239e | |||
40735ce76b | |||
4a00c13b33 | |||
8e359d54bd | |||
fb76dff836 | |||
2448bf4e4e | |||
91e0fc8c62 | |||
b4f86feddb | |||
7167c8c593 | |||
51b79d4250 | |||
925fcc1c64 | |||
fcdc14862c | |||
dac2844cae | |||
706b0cea16 | |||
842e7844c7 | |||
1dceda50d8 | |||
af8b9abba4 | |||
07b07685fa | |||
cde43fe3c8 | |||
2fe84aa75b | |||
7d79a4840d | |||
3ee9479572 | |||
ccf8e44acc | |||
451acb77df | |||
e2c6227f47 | |||
ebd1c877ad | |||
498094b3c7 | |||
1cc183ecdb | |||
e8948452fd | |||
ade7e62836 | |||
395cfa6108 | |||
b5ff2abdb9 | |||
229e85b2c5 | |||
37058e3480 | |||
a1b82e9723 | |||
db943df0c8 | |||
ff8d300ea8 | |||
8b490b9b94 | |||
f83f8631ac | |||
16da91d8d1 | |||
8ffd62b462 | |||
935367e167 | |||
00618260f2 | |||
77d66fac7b | |||
17d7f59b06 | |||
2561547db1 | |||
7738438616 | |||
3d8fc4a794 | |||
87b4e7905e | |||
13c5d4985a | |||
f0df4096fd | |||
5044424549 | |||
8ce67cdcd6 |
BIN
assets/about/drive.png
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
assets/about/post.png
Normal file
After Width: | Height: | Size: 344 KiB |
BIN
assets/about/reaction.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
assets/about/ui.png
Normal file
After Width: | Height: | Size: 125 KiB |
BIN
assets/ai-orig.png
Normal file
After Width: | Height: | Size: 256 KiB |
BIN
assets/ai.png
Normal file
After Width: | Height: | Size: 243 KiB |
@ -109,6 +109,7 @@ Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
|
||||
|
||||
3. `systemctl daemon-reload ; systemctl enable misskey` systemdを再読み込みしmisskeyサービスを有効化
|
||||
4. `systemctl start misskey` misskeyサービスの起動
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "Nacht Modus"
|
||||
circle-icons: "Kreisförmige Icons"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "Übergang in Fensterköpfen"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "Remove background"
|
||||
dark-mode: "Dark Mode"
|
||||
circle-icons: "Use circle icons"
|
||||
contrasted-acct: "Add contrast to username"
|
||||
gradient-window-header: "Use gradients on window headers"
|
||||
post-form-on-timeline: "Display post form at the top of the timeline"
|
||||
suggest-recent-hashtags: "Show recent popular hashtags on the post form"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "Logging in..."
|
||||
signup-button: "Sign up"
|
||||
timeline: "Timeline"
|
||||
announcements: "Announcements"
|
||||
photos: "Recent uploaded"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "Information"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey storage"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "Dark Mode"
|
||||
i-am-under-limited-internet: "I'm in limited bandwidth"
|
||||
circle-icons: "Use circle icons"
|
||||
contrasted-acct: "Add contrast to username"
|
||||
timeline: "Timeline"
|
||||
show-reply-target: "Show reply target"
|
||||
show-my-renotes: "Show my reposts"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "Post design"
|
||||
post-style-standard: "Standard"
|
||||
post-style-smart: "Smart"
|
||||
notification-position: "Notification style"
|
||||
notification-position-bottom: "Bottom"
|
||||
notification-position-top: "Top"
|
||||
behavior: "Behavior"
|
||||
fetch-on-scroll: "Endless loading on scroll"
|
||||
disable-via-mobile: "Don't mark the post as 'from mobile'"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "Settings"
|
||||
signout: "Sign out"
|
||||
sound: "Sounds"
|
||||
enableSounds: "Enable sounds"
|
||||
enable-sounds: "Enable sounds"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "Follows you"
|
||||
following: "Following"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "Suprimir fondo"
|
||||
dark-mode: "Modo Nocturno"
|
||||
circle-icons: "Usar iconos circulares"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "Usar degradados en las cabeceras de las páginas"
|
||||
post-form-on-timeline: "Mostrar el formulario de las entradas encima de la línea de tiempo"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "Supprimer le fond d'écran"
|
||||
dark-mode: "Mode nuit"
|
||||
circle-icons: "Utiliser des icônes circulaires"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "Utiliser les dégradés sur la barre de titre de la fenêtre"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "Afficher les hashtags populaires dans le champs de saisie"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "Se connecter"
|
||||
signup-button: "S'inscrire"
|
||||
timeline: "Fil d'actualité"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Propulsé par <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Lecteur de Misskey"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "Mode nuit"
|
||||
i-am-under-limited-internet: "J'ai un accès Internet limité"
|
||||
circle-icons: "Utiliser des icônes circulaires"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "Fil d'actualité"
|
||||
show-reply-target: "Afficher les réponses"
|
||||
show-my-renotes: "Afficher mes republications"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "Style de la publication"
|
||||
post-style-standard: "Standard"
|
||||
post-style-smart: "Intelligent"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "Comportement"
|
||||
fetch-on-scroll: "Chargement lors du défilement"
|
||||
disable-via-mobile: "Ne pas mentionner que ma publication provient d'un 'périphérique mobile'"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "Réglages"
|
||||
signout: "Déconnexion"
|
||||
sound: "Sons"
|
||||
enableSounds: "Activer le son"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "vous suit"
|
||||
following: "Abonnements"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -6,6 +6,19 @@ common:
|
||||
misskey: "A ⭐ of fediverse"
|
||||
about-title: "A ⭐ of fediverse."
|
||||
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
|
||||
intro:
|
||||
title: "Misskeyって?"
|
||||
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
|
||||
features: "特徴"
|
||||
rich-contents: "投稿"
|
||||
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
|
||||
reaction: "リアクション"
|
||||
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||
ui: "インターフェース"
|
||||
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
|
||||
drive: "ドライブ"
|
||||
drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんか?もしくは、アップロードしたファイルをフォルダ分けして整理したくなったことはありませんか?Misskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。"
|
||||
outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!"
|
||||
adblock:
|
||||
detected: "広告ブロッカーを無効にしてください"
|
||||
warning: "<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。"
|
||||
@ -73,6 +86,16 @@ common:
|
||||
rip: "RIP"
|
||||
pudding: "Pudding"
|
||||
|
||||
note-visibility:
|
||||
public: "公開"
|
||||
home: "ホーム"
|
||||
home-desc: "ホームタイムラインにのみ公開"
|
||||
followers: "フォロワー"
|
||||
followers-desc: "自分のフォロワーにのみ公開"
|
||||
specified: "ダイレクト"
|
||||
specified-desc: "指定したユーザーにのみ公開"
|
||||
private: "非公開"
|
||||
|
||||
note-placeholders:
|
||||
a: "今どうしてる?"
|
||||
b: "何かありましたか?"
|
||||
@ -724,6 +747,9 @@ desktop/views/components/settings.vue:
|
||||
behaviour: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
|
||||
note-visibility: "投稿の公開範囲"
|
||||
default-note-visibility: "デフォルトの公開範囲"
|
||||
remember-note-visibility: "投稿の公開範囲を記憶する"
|
||||
auto-popout: "ウィンドウの自動ポップアウト"
|
||||
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
||||
advanced: "詳細設定"
|
||||
@ -736,6 +762,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -990,7 +1017,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
@ -1347,6 +1377,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1360,6 +1391,9 @@ mobile/views/pages/settings.vue:
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
note-visibility: "投稿の公開範囲"
|
||||
default-note-visibility: "デフォルトの公開範囲"
|
||||
remember-note-visibility: "投稿の公開範囲を記憶する"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
load-raw-images: "添付された画像を高画質で表示する"
|
||||
load-remote-media: "リモートサーバーのメディアを表示する"
|
||||
@ -1379,7 +1413,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "サインイン中…"
|
||||
signup-button: "サインアップ"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "ドライブ"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "べっぴんさん"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "Donkere modus"
|
||||
circle-icons: "Ronde pictogrammen gebruiken"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "Kleurverloop gebruiken op vensterkoppen"
|
||||
post-form-on-timeline: "Berichtformulier boven de tijdlijn tonen"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "Inloggen"
|
||||
signup-button: "Registreren"
|
||||
timeline: "Tijdlijn"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "Donkere modus"
|
||||
i-am-under-limited-internet: "Ik heb beperkt internet"
|
||||
circle-icons: "Ronde pictogrammen gebruiken"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "Tijdlijn"
|
||||
show-reply-target: "Antwoordknop tonen"
|
||||
show-my-renotes: "Mijn renotes tonen"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "Berichtontwerp"
|
||||
post-style-standard: "Standaard"
|
||||
post-style-smart: "Slim"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "Gedrag"
|
||||
fetch-on-scroll: "Ophalen bij scrollen"
|
||||
disable-via-mobile: "Zonder 'mobiele berichten'"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "Instellingen"
|
||||
signout: "Uitloggen"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "Volgt jou"
|
||||
following: "Volgend"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "Usuń tło"
|
||||
dark-mode: "Tryb ciemny"
|
||||
circle-icons: "Używaj okrągłych ikon"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "Używaj gradientów na pasku tytułu okna"
|
||||
post-form-on-timeline: "Wyświetlaj formularz tworzenia wpisu w górnej części osi czasu"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "Zaloguj się"
|
||||
signup-button: "Zarejestruj się"
|
||||
timeline: "Oś czasu"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Oparto o <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Dysk Misskey"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "Tryb ciemny"
|
||||
i-am-under-limited-internet: "Ograniczaj zużycie transferu"
|
||||
circle-icons: "Używaj okrągłych ikon"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "Oś czasu"
|
||||
show-reply-target: "Pokazuj cel odpowiedzi"
|
||||
show-my-renotes: "Pokazuj moje udostępnienia"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "Styl wpisów"
|
||||
post-style-standard: "Standardowy"
|
||||
post-style-smart: "Inteligentny"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "Zachowanie"
|
||||
fetch-on-scroll: "Automatycznie ładuj po przeciągnięciu w dół"
|
||||
disable-via-mobile: "Nie oznaczaj wpisów jako „wysłane z telefonu”"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "Ustawienia"
|
||||
signout: "Wyloguj"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "Śledzi Cię"
|
||||
following: "Śledzeni"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "Timeline"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Desenvolvido por <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Drive Misskey"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
@ -651,6 +651,7 @@ desktop/views/components/settings.vue:
|
||||
delete-wallpaper: "壁紙を削除"
|
||||
dark-mode: "ダークモード"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||
@ -865,7 +866,10 @@ desktop/views/pages/welcome.vue:
|
||||
signin-button: "やってる"
|
||||
signup-button: "やる"
|
||||
timeline: "タイムライン"
|
||||
announcements: "お知らせ"
|
||||
photos: "最近の画像"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
info: "情報"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Drive"
|
||||
desktop/views/pages/favorites.vue:
|
||||
@ -1153,6 +1157,7 @@ mobile/views/pages/settings.vue:
|
||||
dark-mode: "ダークモード"
|
||||
i-am-under-limited-internet: "私は通信を制限されている"
|
||||
circle-icons: "円形のアイコンを使用"
|
||||
contrasted-acct: "ユーザー名にコントラストを付ける"
|
||||
timeline: "タイムライン"
|
||||
show-reply-target: "リプライ先を表示する"
|
||||
show-my-renotes: "自分の行ったRenoteを表示する"
|
||||
@ -1161,6 +1166,9 @@ mobile/views/pages/settings.vue:
|
||||
post-style: "投稿の表示スタイル"
|
||||
post-style-standard: "標準"
|
||||
post-style-smart: "スマート"
|
||||
notification-position: "通知の表示"
|
||||
notification-position-bottom: "下"
|
||||
notification-position-top: "上"
|
||||
behavior: "動作"
|
||||
fetch-on-scroll: "スクロールで自動読み込み"
|
||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||
@ -1182,7 +1190,7 @@ mobile/views/pages/settings.vue:
|
||||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
sound: "サウンド"
|
||||
enableSounds: "サウンドを有効にする"
|
||||
enable-sounds: "サウンドを有効にする"
|
||||
mobile/views/pages/user.vue:
|
||||
follows-you: "フォローされています"
|
||||
following: "フォロー"
|
||||
|
15
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "8.26.0",
|
||||
"clientVersion": "1.0.9358",
|
||||
"version": "8.33.1",
|
||||
"clientVersion": "1.0.9497",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -55,7 +55,7 @@
|
||||
"@types/koa-send": "4.1.1",
|
||||
"@types/koa-views": "2.0.3",
|
||||
"@types/koa__cors": "2.2.3",
|
||||
"@types/minio": "6.0.2",
|
||||
"@types/minio": "7.0.0",
|
||||
"@types/mkdirp": "0.5.2",
|
||||
"@types/mocha": "5.2.3",
|
||||
"@types/mongodb": "3.1.4",
|
||||
@ -80,7 +80,7 @@
|
||||
"@types/webpack": "4.4.11",
|
||||
"@types/webpack-stream": "3.2.10",
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.0",
|
||||
"@types/ws": "6.0.1",
|
||||
"animejs": "2.2.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
@ -151,7 +151,7 @@
|
||||
"lodash.assign": "4.2.0",
|
||||
"mecab-async": "0.1.2",
|
||||
"merge-options": "1.0.1",
|
||||
"minio": "7.0.0",
|
||||
"minio": "7.0.1",
|
||||
"mkdirp": "0.5.1",
|
||||
"mocha": "5.2.0",
|
||||
"moji": "0.5.1",
|
||||
@ -161,7 +161,7 @@
|
||||
"nan": "2.11.0",
|
||||
"nested-property": "0.0.7",
|
||||
"node-sass": "4.9.3",
|
||||
"node-sass-json-importer": "4.0.0",
|
||||
"node-sass-json-importer": "4.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"on-build-webpack": "0.1.0",
|
||||
@ -194,7 +194,7 @@
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.2.0",
|
||||
"systeminformation": "3.45.0",
|
||||
"systeminformation": "3.45.1",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"tmp": "0.0.33",
|
||||
@ -217,6 +217,7 @@
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-template-compiler": "2.5.17",
|
||||
"vuedraggable": "2.16.0",
|
||||
"vuewordcloud": "18.7.11",
|
||||
"vuex": "3.0.1",
|
||||
"vuex-persistedstate": "2.5.4",
|
||||
"web-push": "3.3.2",
|
||||
|
@ -140,7 +140,7 @@
|
||||
// Random
|
||||
localStorage.setItem('salt', Math.random().toString());
|
||||
|
||||
// Clear cache (serive worker)
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
|
||||
|
@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
|
||||
localStorage.setItem('should-refresh', 'true');
|
||||
localStorage.setItem('v', newer);
|
||||
|
||||
// Clear cache (serive worker)
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
|
@ -1,2 +0,0 @@
|
||||
const gcd = (a, b) => !b ? a : gcd(b, a % b);
|
||||
export default gcd;
|
@ -1,53 +0,0 @@
|
||||
export default function(qs: string) {
|
||||
const q = {
|
||||
text: ''
|
||||
};
|
||||
|
||||
qs.split(' ').forEach(x => {
|
||||
if (/^([a-z_]+?):(.+?)$/.test(x)) {
|
||||
const [key, value] = x.split(':');
|
||||
switch (key) {
|
||||
case 'user':
|
||||
q['includeUserUsernames'] = value.split(',');
|
||||
break;
|
||||
case 'exclude_user':
|
||||
q['excludeUserUsernames'] = value.split(',');
|
||||
break;
|
||||
case 'follow':
|
||||
q['following'] = value == 'null' ? null : value == 'true';
|
||||
break;
|
||||
case 'reply':
|
||||
q['reply'] = value == 'null' ? null : value == 'true';
|
||||
break;
|
||||
case 'renote':
|
||||
q['renote'] = value == 'null' ? null : value == 'true';
|
||||
break;
|
||||
case 'media':
|
||||
q['media'] = value == 'null' ? null : value == 'true';
|
||||
break;
|
||||
case 'poll':
|
||||
q['poll'] = value == 'null' ? null : value == 'true';
|
||||
break;
|
||||
case 'until':
|
||||
case 'since':
|
||||
// YYYY-MM-DD
|
||||
if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
|
||||
const [yyyy, mm, dd] = value.split('-');
|
||||
q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
q[key] = value;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
q.text += x + ' ';
|
||||
}
|
||||
});
|
||||
|
||||
if (q.text) {
|
||||
q.text = q.text.trim();
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as uuid from 'uuid';
|
||||
import Connection from './stream';
|
||||
import { erase } from '../../../../../prelude/array';
|
||||
|
||||
/**
|
||||
* ストリーム接続を管理するクラス
|
||||
@ -89,7 +90,7 @@ export default abstract class StreamManager<T extends Connection> extends EventE
|
||||
* @param userId use で発行したユーザーID
|
||||
*/
|
||||
public dispose(userId) {
|
||||
this.users = this.users.filter(id => id != userId);
|
||||
this.users = erase(userId, this.users);
|
||||
|
||||
this._connection.user = `Managed (${ this.users.length })`;
|
||||
|
||||
|
@ -1,19 +1,25 @@
|
||||
<template>
|
||||
<span class="mk-acct">
|
||||
<span class="name">@{{ user.username }}</span>
|
||||
<span class="host" v-if="user.host">@{{ user.host }}</span>
|
||||
<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail">@{{ user.host || host }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { host } from '../../../config';
|
||||
export default Vue.extend({
|
||||
props: ['user']
|
||||
props: ['user', 'detail'],
|
||||
data() {
|
||||
return {
|
||||
host
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-acct
|
||||
> .host
|
||||
> .host.fade
|
||||
opacity 0.5
|
||||
</style>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import tagCloud from './tag-cloud.vue';
|
||||
import trends from './trends.vue';
|
||||
import analogClock from './analog-clock.vue';
|
||||
import menu from './menu.vue';
|
||||
@ -41,6 +42,7 @@ import uiSelect from './ui/select.vue';
|
||||
import formButton from './ui/form/button.vue';
|
||||
import formRadio from './ui/form/radio.vue';
|
||||
|
||||
Vue.component('mk-tag-cloud', tagCloud);
|
||||
Vue.component('mk-trends', trends);
|
||||
Vue.component('mk-analog-clock', analogClock);
|
||||
Vue.component('mk-menu', menu);
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { erase } from '../../../../../prelude/array';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
@ -53,7 +54,7 @@ export default Vue.extend({
|
||||
|
||||
get() {
|
||||
return {
|
||||
choices: this.choices.filter(choice => choice != '')
|
||||
choices: erase('', this.choices)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
export default Vue.extend({
|
||||
props: ['note'],
|
||||
data() {
|
||||
@ -33,7 +34,7 @@ export default Vue.extend({
|
||||
return this.note.poll;
|
||||
},
|
||||
total(): number {
|
||||
return this.poll.choices.reduce((a, b) => a + b.votes, 0);
|
||||
return sum(this.poll.choices.map(x => x.votes));
|
||||
},
|
||||
isVoted(): boolean {
|
||||
return this.poll.choices.some(c => c.isVoted);
|
||||
|
@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<span class="mk-reaction-icon">
|
||||
<img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
|
||||
<img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
|
||||
<img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
|
||||
<img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
|
||||
<img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
|
||||
<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
|
||||
<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
|
||||
<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
|
||||
<img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%">
|
||||
<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%">
|
||||
<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%">
|
||||
<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%">
|
||||
<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%">
|
||||
<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%">
|
||||
<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%">
|
||||
<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%">
|
||||
<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%">
|
||||
<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%">
|
||||
<template v-if="reaction == 'pudding'">
|
||||
<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
|
||||
<img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
|
||||
<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%">
|
||||
<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%">
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
90
src/client/app/common/views/components/tag-cloud.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
<p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
|
||||
<div v-else>
|
||||
<vue-word-cloud
|
||||
:words="tags.slice(0, 20).map(x => [x.name, x.count])"
|
||||
:color="color"
|
||||
:spacing="1">
|
||||
<template slot-scope="{word, text, weight}">
|
||||
<div style="cursor: pointer;" :title="weight">
|
||||
{{ text }}
|
||||
</div>
|
||||
</template>
|
||||
</vue-word-cloud>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as VueWordCloud from 'vuewordcloud';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
[VueWordCloud.name]: VueWordCloud
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags: [],
|
||||
fetching: true,
|
||||
clock: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
this.clock = setInterval(this.fetch, 1000 * 60);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
(this as any).api('aggregation/hashtags').then(tags => {
|
||||
this.tags = tags;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
color([, weight]) {
|
||||
const peak = Math.max.apply(null, this.tags.map(x => x.count));
|
||||
const w = weight / peak;
|
||||
|
||||
if (w > 0.9) {
|
||||
return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69';
|
||||
} else if (w > 0.5) {
|
||||
return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7';
|
||||
} else {
|
||||
return this.$store.state.device.darkmode ? '#fff' : '#555';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
root(isDark)
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
> .fetching
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> div
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
.jtivnzhfwquxpsfidertopbmwmchmnmo[data-darkmode]
|
||||
root(true)
|
||||
|
||||
.jtivnzhfwquxpsfidertopbmwmchmnmo:not([data-darkmode])
|
||||
root(false)
|
||||
|
||||
</style>
|
@ -14,7 +14,7 @@
|
||||
<header>
|
||||
<h1>{{ title }}</h1>
|
||||
</header>
|
||||
<p>{{ description }}</p>
|
||||
<p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
|
||||
<footer>
|
||||
<img class="icon" v-if="icon" :src="icon"/>
|
||||
<p>{{ sitename }}</p>
|
||||
|
@ -47,7 +47,7 @@ export default Vue.extend({
|
||||
props: ['source', 'compact'],
|
||||
data() {
|
||||
return {
|
||||
v: this.$store.state.device.visibility || 'public'
|
||||
v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -97,7 +97,9 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
choose(visibility) {
|
||||
this.$store.commit('device/setVisibility', visibility);
|
||||
if (this.$store.state.settings.rememberNoteVisibility) {
|
||||
this.$store.commit('device/setVisibility', visibility);
|
||||
}
|
||||
this.$emit('chosen', visibility);
|
||||
this.$destroy();
|
||||
},
|
||||
|
@ -1,22 +1,24 @@
|
||||
<template>
|
||||
<div class="mk-welcome-timeline">
|
||||
<div v-for="note in notes">
|
||||
<mk-avatar class="avatar" :user="note.user" target="_blank"/>
|
||||
<div class="body">
|
||||
<header>
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
|
||||
<span class="username">@{{ note.user | acct }}</span>
|
||||
<div class="info">
|
||||
<router-link class="created-at" :to="note | notePage">
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</router-link>
|
||||
<transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div">
|
||||
<div v-for="note in notes" :key="note.id">
|
||||
<mk-avatar class="avatar" :user="note.user" target="_blank"/>
|
||||
<div class="body">
|
||||
<header>
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
|
||||
<span class="username">@{{ note.user | acct }}</span>
|
||||
<div class="info">
|
||||
<router-link class="created-at" :to="note | notePage">
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</header>
|
||||
<div class="text">
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text"/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="text">
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -63,7 +65,7 @@ export default Vue.extend({
|
||||
local: true,
|
||||
reply: false,
|
||||
renote: false,
|
||||
media: false,
|
||||
file: false,
|
||||
poll: false
|
||||
}).then(notes => {
|
||||
this.notes = notes;
|
||||
@ -83,64 +85,73 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.ldzpakcixzickvggyixyrhqwjaefknon-enter
|
||||
.ldzpakcixzickvggyixyrhqwjaefknon-leave-to
|
||||
opacity 0
|
||||
transform translateY(-30px)
|
||||
|
||||
root(isDark)
|
||||
background isDark ? #282C37 : #fff
|
||||
|
||||
> div
|
||||
padding 16px
|
||||
overflow-wrap break-word
|
||||
font-size .9em
|
||||
color isDark ? #fff : #4C4C4C
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
> div
|
||||
padding 16px
|
||||
overflow-wrap break-word
|
||||
font-size .9em
|
||||
color isDark ? #fff : #4C4C4C
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
float left
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
top 16px
|
||||
width 42px
|
||||
height 42px
|
||||
border-radius 6px
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .body
|
||||
float right
|
||||
width calc(100% - 42px)
|
||||
padding-left 12px
|
||||
> .avatar
|
||||
display block
|
||||
float left
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
top 16px
|
||||
width 42px
|
||||
height 42px
|
||||
border-radius 6px
|
||||
|
||||
> header
|
||||
display flex
|
||||
align-items center
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
> .body
|
||||
float right
|
||||
width calc(100% - 42px)
|
||||
padding-left 12px
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 0 .5em 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
font-weight bold
|
||||
text-overflow ellipsis
|
||||
color isDark ? #fff : #627079
|
||||
> header
|
||||
display flex
|
||||
align-items center
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
|
||||
> .username
|
||||
margin 0 .5em 0 0
|
||||
color isDark ? #606984 : #ccc
|
||||
> .name
|
||||
display block
|
||||
margin 0 .5em 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
font-weight bold
|
||||
text-overflow ellipsis
|
||||
color isDark ? #fff : #627079
|
||||
|
||||
> .info
|
||||
margin-left auto
|
||||
font-size 0.9em
|
||||
> .username
|
||||
margin 0 .5em 0 0
|
||||
color isDark ? #606984 : #ccc
|
||||
|
||||
> .created-at
|
||||
color isDark ? #606984 : #c0c0c0
|
||||
> .info
|
||||
margin-left auto
|
||||
font-size 0.9em
|
||||
|
||||
> .text
|
||||
text-align left
|
||||
> .created-at
|
||||
color isDark ? #606984 : #c0c0c0
|
||||
|
||||
> .text
|
||||
text-align left
|
||||
|
||||
.mk-welcome-timeline[data-darkmode]
|
||||
root(true)
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="mkw-analog-clock">
|
||||
<mk-widget-container :naked="!(props.design % 2)" :show-header="false">
|
||||
<mk-widget-container :naked="props.style % 2 === 0" :show-header="false">
|
||||
<div class="mkw-analog-clock--body">
|
||||
<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/>
|
||||
<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/>
|
||||
</div>
|
||||
</mk-widget-container>
|
||||
</div>
|
||||
@ -13,13 +13,12 @@ import define from '../../../common/define-widget';
|
||||
export default define({
|
||||
name: 'analog-clock',
|
||||
props: () => ({
|
||||
design: -1
|
||||
style: 0
|
||||
})
|
||||
}).extend({
|
||||
methods: {
|
||||
func() {
|
||||
if (++this.props.design > 2)
|
||||
this.props.design = -1;
|
||||
this.props.style = (this.props.style + 1) % 4;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="anltbovirfeutcigvwgmgxipejaeozxi"
|
||||
:data-found="broadcasts.length != 0"
|
||||
:data-found="announcements && announcements.length != 0"
|
||||
:data-melt="props.design == 1"
|
||||
:data-mobile="platform == 'mobile'"
|
||||
>
|
||||
@ -14,12 +14,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
|
||||
<h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1>
|
||||
<h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1>
|
||||
<p v-if="!fetching">
|
||||
<span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
|
||||
<template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template>
|
||||
<span v-if="announcements.length != 0" v-html="announcements[i].text"></span>
|
||||
<template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template>
|
||||
</p>
|
||||
<a v-if="broadcasts.length > 1" @click="next">%i18n:@next% >></a>
|
||||
<a v-if="announcements.length > 1" @click="next">%i18n:@next% >></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -36,18 +36,18 @@ export default define({
|
||||
return {
|
||||
i: 0,
|
||||
fetching: true,
|
||||
broadcasts: []
|
||||
announcements: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.broadcasts = meta.broadcasts;
|
||||
this.announcements = meta.broadcasts;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
if (this.i == this.broadcasts.length - 1) {
|
||||
if (this.i == this.announcements.length - 1) {
|
||||
this.i = 0;
|
||||
} else {
|
||||
this.i++;
|
||||
@ -126,7 +126,7 @@ root(isDark)
|
||||
margin 0
|
||||
font-size 0.95em
|
||||
font-weight normal
|
||||
color #4078c0
|
||||
color isDark ? #539eff : #4078c0
|
||||
|
||||
> p
|
||||
display block
|
||||
|
@ -42,8 +42,8 @@
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media" :raw="true"/>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files" :raw="true"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
||||
@ -86,6 +86,7 @@ import MkRenoteFormWindow from './renote-form-window.vue';
|
||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
||||
import XSub from './notes.note.sub.vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -114,7 +115,7 @@ export default Vue.extend({
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds.length == 0 &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
p(): any {
|
||||
@ -122,9 +123,7 @@ export default Vue.extend({
|
||||
},
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? Object.keys(this.p.reactionCounts)
|
||||
.map(key => this.p.reactionCounts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
title(): string {
|
||||
|
@ -28,8 +28,8 @@
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.renote">RP:</a>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
||||
@ -78,6 +78,7 @@ import MkRenoteFormWindow from './renote-form-window.vue';
|
||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
||||
import XSub from './notes.note.sub.vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
|
||||
function focus(el, fn) {
|
||||
const target = fn(el);
|
||||
@ -110,7 +111,7 @@ export default Vue.extend({
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds.length == 0 &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
@ -120,9 +121,7 @@ export default Vue.extend({
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? Object.keys(this.p.reactionCounts)
|
||||
.map(key => this.p.reactionCounts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
|
@ -122,7 +122,7 @@ export default Vue.extend({
|
||||
prepend(note, silent = false) {
|
||||
//#region 弾く
|
||||
const isMyNote = note.userId == this.$store.state.i.id;
|
||||
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
|
||||
const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
|
||||
if (this.$store.state.settings.showMyRenotes === false) {
|
||||
if (isMyNote && isPureRenote) {
|
||||
|
@ -4,7 +4,7 @@
|
||||
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
||||
<span v-if="!reply">%i18n:@note%</span>
|
||||
<span v-if="reply">%i18n:@reply%</span>
|
||||
<span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
|
||||
<span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span>
|
||||
<span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
|
||||
</span>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
:reply="reply"
|
||||
@posted="onPosted"
|
||||
@change-uploadings="onChangeUploadings"
|
||||
@change-attached-media="onChangeMedia"
|
||||
@change-attached-files="onChangeFiles"
|
||||
@geo-attached="onGeoAttached"
|
||||
@geo-dettached="onGeoDettached"/>
|
||||
</div>
|
||||
@ -29,7 +29,7 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
uploadings: [],
|
||||
media: [],
|
||||
files: [],
|
||||
geo: null
|
||||
};
|
||||
},
|
||||
@ -42,8 +42,8 @@ export default Vue.extend({
|
||||
onChangeUploadings(files) {
|
||||
this.uploadings = files;
|
||||
},
|
||||
onChangeMedia(media) {
|
||||
this.media = media;
|
||||
onChangeFiles(files) {
|
||||
this.files = files;
|
||||
},
|
||||
onGeoAttached(geo) {
|
||||
this.geo = geo;
|
||||
|
@ -20,7 +20,7 @@
|
||||
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
|
||||
v-autocomplete="'text'"
|
||||
></textarea>
|
||||
<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
|
||||
<div class="files" :class="{ with: poll }" v-show="files.length != 0">
|
||||
<x-draggable :list="files" :options="{ animation: 150 }">
|
||||
<div v-for="file in files" :key="file.id">
|
||||
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
|
||||
@ -62,6 +62,7 @@ import getFace from '../../../common/scripts/get-face';
|
||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
|
||||
import parse from '../../../../../mfm/parse';
|
||||
import { host } from '../../../config';
|
||||
import { erase } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -99,7 +100,7 @@ export default Vue.extend({
|
||||
useCw: false,
|
||||
cw: null,
|
||||
geo: null,
|
||||
visibility: this.$store.state.device.visibility || 'public',
|
||||
visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
|
||||
visibleUsers: [],
|
||||
autocomplete: null,
|
||||
draghover: false,
|
||||
@ -188,7 +189,7 @@ export default Vue.extend({
|
||||
(this.$refs.poll as any).set(draft.data.poll);
|
||||
});
|
||||
}
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,12 +226,12 @@ export default Vue.extend({
|
||||
|
||||
attachMedia(driveFile) {
|
||||
this.files.push(driveFile);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
},
|
||||
|
||||
detachMedia(id) {
|
||||
this.files = this.files.filter(x => x.id != id);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
},
|
||||
|
||||
onChangeFile() {
|
||||
@ -249,7 +250,7 @@ export default Vue.extend({
|
||||
this.text = '';
|
||||
this.files = [];
|
||||
this.poll = false;
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
@ -297,7 +298,7 @@ export default Vue.extend({
|
||||
if (driveFile != null && driveFile != '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
this.files.push(file);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
e.preventDefault();
|
||||
}
|
||||
//#endregion
|
||||
@ -346,7 +347,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
removeVisibleUser(user) {
|
||||
this.visibleUsers = this.visibleUsers.filter(u => u != user);
|
||||
this.visibleUsers = erase(user, this.visibleUsers);
|
||||
},
|
||||
|
||||
post() {
|
||||
@ -354,7 +355,7 @@ export default Vue.extend({
|
||||
|
||||
(this as any).api('notes/create', {
|
||||
text: this.text == '' ? undefined : this.text,
|
||||
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
replyId: this.reply ? this.reply.id : undefined,
|
||||
renoteId: this.renote ? this.renote.id : undefined,
|
||||
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
|
||||
@ -514,7 +515,7 @@ root(isDark)
|
||||
margin-right 8px
|
||||
white-space nowrap
|
||||
|
||||
> .medias
|
||||
> .files
|
||||
margin 0
|
||||
padding 0
|
||||
background isDark ? #181b23 : lighten($theme-color, 98%)
|
||||
|
@ -26,6 +26,22 @@
|
||||
<mk-switch v-model="autoPopout" text="%i18n:@auto-popout%">
|
||||
<span>%i18n:@auto-popout-desc%</span>
|
||||
</mk-switch>
|
||||
|
||||
<section>
|
||||
<header>%i18n:@note-visibility%</header>
|
||||
<mk-switch v-model="$store.state.settings.rememberNoteVisibility" @change="onChangeRememberNoteVisibility" text="%i18n:@remember-note-visibility%"/>
|
||||
<section>
|
||||
<header>%i18n:@default-note-visibility%</header>
|
||||
<ui-select v-model="defaultNoteVisibility">
|
||||
<option value="public">%i18n:common.note-visibility.public%</option>
|
||||
<option value="home">%i18n:common.note-visibility.home%</option>
|
||||
<option value="followers">%i18n:common.note-visibility.followers%</option>
|
||||
<option value="specified">%i18n:common.note-visibility.specified%</option>
|
||||
<option value="private">%i18n:common.note-visibility.private%</option>
|
||||
</ui-select>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<details>
|
||||
<summary>%i18n:@advanced%</summary>
|
||||
<mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%">
|
||||
@ -44,6 +60,7 @@
|
||||
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
|
||||
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
|
||||
<mk-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons" text="%i18n:@circle-icons%"/>
|
||||
<mk-switch v-model="$store.state.settings.contrastedAcct" @change="onChangeContrastedAcct" text="%i18n:@contrasted-acct%"/>
|
||||
<mk-switch v-model="$store.state.settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/>
|
||||
<mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/>
|
||||
</div>
|
||||
@ -238,6 +255,11 @@ export default Vue.extend({
|
||||
set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
|
||||
},
|
||||
|
||||
defaultNoteVisibility: {
|
||||
get() { return this.$store.state.settings.defaultNoteVisibility; },
|
||||
set(value) { this.$store.commit('settings/set', { key: 'defaultNoteVisibility', value }); }
|
||||
},
|
||||
|
||||
autoPopout: {
|
||||
get() { return this.$store.state.device.autoPopout; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); }
|
||||
@ -311,6 +333,12 @@ export default Vue.extend({
|
||||
value: v
|
||||
});
|
||||
},
|
||||
onChangeRememberNoteVisibility(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'rememberNoteVisibility',
|
||||
value: v
|
||||
});
|
||||
},
|
||||
onChangeAutoWatch(v) {
|
||||
(this as any).api('i/update', {
|
||||
autoWatch: v
|
||||
@ -376,6 +404,12 @@ export default Vue.extend({
|
||||
value: v
|
||||
});
|
||||
},
|
||||
onChangeContrastedAcct(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'contrastedAcct',
|
||||
value: v
|
||||
});
|
||||
},
|
||||
onChangeILikeSushi(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'iLikeSushi',
|
||||
|
@ -7,9 +7,9 @@
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
|
||||
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a>
|
||||
</div>
|
||||
<details v-if="note.media.length > 0">
|
||||
<summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
|
||||
<mk-media-list :media-list="note.media"/>
|
||||
<details v-if="note.files.length > 0">
|
||||
<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
|
||||
<mk-media-list :media-list="note.files"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
<summary>%i18n:@poll%</summary>
|
||||
|
41
src/client/app/desktop/views/pages/admin/admin.hashtags.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="jdnqwkzlnxcfftthoybjxrebyolvoucw mk-admin-card">
|
||||
<header>%i18n:@hided-tags%</header>
|
||||
<textarea v-model="hidedTags"></textarea>
|
||||
<button class="ui" @click="save">%i18n:@save%</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
hidedTags: '',
|
||||
};
|
||||
},
|
||||
created() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.hidedTags = meta.hidedTags.join('\n');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
(this as any).api('admin/update-meta', {
|
||||
hidedTags: this.hidedTags.split('\n')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.jdnqwkzlnxcfftthoybjxrebyolvoucw
|
||||
textarea
|
||||
width 100%
|
||||
min-height 300px
|
||||
|
||||
</style>
|
@ -5,6 +5,8 @@
|
||||
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li>
|
||||
<li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
|
||||
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li>
|
||||
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li>
|
||||
|
||||
<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> -->
|
||||
<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
|
||||
</ul>
|
||||
@ -17,6 +19,9 @@
|
||||
<div v-show="page == 'announcements'">
|
||||
<x-announcements/>
|
||||
</div>
|
||||
<div v-show="page == 'hashtags'">
|
||||
<x-hashtags/>
|
||||
</div>
|
||||
<div v-if="page == 'users'">
|
||||
<x-suspend-user/>
|
||||
<x-unsuspend-user/>
|
||||
@ -33,6 +38,7 @@
|
||||
import Vue from "vue";
|
||||
import XDashboard from "./admin.dashboard.vue";
|
||||
import XAnnouncements from "./admin.announcements.vue";
|
||||
import XHashtags from "./admin.hashtags.vue";
|
||||
import XSuspendUser from "./admin.suspend-user.vue";
|
||||
import XUnsuspendUser from "./admin.unsuspend-user.vue";
|
||||
import XVerifyUser from "./admin.verify-user.vue";
|
||||
@ -43,6 +49,7 @@ export default Vue.extend({
|
||||
components: {
|
||||
XDashboard,
|
||||
XAnnouncements,
|
||||
XHashtags,
|
||||
XSuspendUser,
|
||||
XUnsuspendUser,
|
||||
XVerifyUser,
|
||||
|
@ -28,6 +28,7 @@
|
||||
import Vue from 'vue';
|
||||
import Menu from '../../../../common/views/components/menu.vue';
|
||||
import contextmenu from '../../../api/contextmenu';
|
||||
import { countIf } from '../../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
@ -117,7 +118,7 @@ export default Vue.extend({
|
||||
toggleActive() {
|
||||
if (!this.isStacked) return;
|
||||
const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id));
|
||||
if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return;
|
||||
if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return;
|
||||
this.active = !this.active;
|
||||
},
|
||||
|
||||
|
@ -68,7 +68,7 @@ export default Vue.extend({
|
||||
(this as any).api('notes/user-list-timeline', {
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
mediaOnly: this.mediaOnly,
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
@ -90,7 +90,7 @@ export default Vue.extend({
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: (this.$refs.timeline as any).tail().id,
|
||||
mediaOnly: this.mediaOnly,
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
@ -109,7 +109,7 @@ export default Vue.extend({
|
||||
return promise;
|
||||
},
|
||||
onNote(note) {
|
||||
if (this.mediaOnly && note.media.length == 0) return;
|
||||
if (this.mediaOnly && note.files.length == 0) return;
|
||||
|
||||
// Prepend a note
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
|
@ -28,8 +28,8 @@
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
<a class="rp" v-if="p.renote != null">RP:</a>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||
@ -54,11 +54,11 @@
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
|
||||
<div v-if="note.media.length > 0">
|
||||
<mk-media-list :media-list="note.media"/>
|
||||
<div v-if="note.files.length > 0">
|
||||
<mk-media-list :media-list="note.files"/>
|
||||
</div>
|
||||
<div v-if="note.renote && note.renote.media.length > 0">
|
||||
<mk-media-list :media-list="note.renote.media"/>
|
||||
<div v-if="note.renote && note.renote.files.length > 0">
|
||||
<mk-media-list :media-list="note.renote.files"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -100,7 +100,7 @@ export default Vue.extend({
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds.length == 0 &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
@ -371,7 +371,7 @@ root(isDark)
|
||||
.mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .media
|
||||
> .files
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
@ -127,7 +127,7 @@ export default Vue.extend({
|
||||
prepend(note, silent = false) {
|
||||
//#region 弾く
|
||||
const isMyNote = note.userId == this.$store.state.i.id;
|
||||
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
|
||||
const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
|
||||
if (this.$store.state.settings.showMyRenotes === false) {
|
||||
if (isMyNote && isPureRenote) {
|
||||
|
@ -96,7 +96,7 @@ export default Vue.extend({
|
||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||
(this as any).api(this.endpoint, {
|
||||
limit: fetchLimit + 1,
|
||||
mediaOnly: this.mediaOnly,
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
@ -117,7 +117,7 @@ export default Vue.extend({
|
||||
|
||||
const promise = (this as any).api(this.endpoint, {
|
||||
limit: fetchLimit + 1,
|
||||
mediaOnly: this.mediaOnly,
|
||||
withFiles: this.mediaOnly,
|
||||
untilId: (this.$refs.timeline as any).tail().id,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
@ -138,7 +138,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
onNote(note) {
|
||||
if (this.mediaOnly && note.media.length == 0) return;
|
||||
if (this.mediaOnly && note.files.length == 0) return;
|
||||
|
||||
// Prepend a note
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
|
@ -85,6 +85,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.title = (this as any).os.instanceName;
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="title">
|
||||
<p class="name">{{ user | userName }}</p>
|
||||
<div>
|
||||
<span class="username"><mk-acct :user="user"/></span>
|
||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
||||
<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span>
|
||||
<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
|
||||
<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
|
||||
|
@ -24,12 +24,12 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
(this as any).api('users/notes', {
|
||||
userId: this.user.id,
|
||||
withMedia: true,
|
||||
withFiles: true,
|
||||
limit: 9
|
||||
}).then(notes => {
|
||||
notes.forEach(note => {
|
||||
note.media.forEach(media => {
|
||||
if (this.images.length < 9) this.images.push(media);
|
||||
note.files.forEach(file => {
|
||||
if (this.images.length < 9) this.images.push(file);
|
||||
});
|
||||
});
|
||||
this.fetching = false;
|
||||
|
@ -66,7 +66,7 @@ export default Vue.extend({
|
||||
limit: fetchLimit + 1,
|
||||
untilDate: this.date ? this.date.getTime() : undefined,
|
||||
includeReplies: this.mode == 'with-replies',
|
||||
withMedia: this.mode == 'with-media'
|
||||
withFiles: this.mode == 'with-media'
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
@ -86,7 +86,7 @@ export default Vue.extend({
|
||||
userId: this.user.id,
|
||||
limit: fetchLimit + 1,
|
||||
includeReplies: this.mode == 'with-replies',
|
||||
withMedia: this.mode == 'with-media',
|
||||
withFiles: this.mode == 'with-media',
|
||||
untilId: (this.$refs.timeline as any).tail().id
|
||||
});
|
||||
|
||||
|
@ -7,45 +7,130 @@
|
||||
|
||||
<mk-forkit class="forkit"/>
|
||||
|
||||
<div class="body">
|
||||
<div class="main block">
|
||||
<h1 v-if="name != 'Misskey'">{{ name }}</h1>
|
||||
<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1>
|
||||
<main>
|
||||
<div class="body">
|
||||
<div class="main block">
|
||||
<div>
|
||||
<h1 v-if="name != 'Misskey'">{{ name }}</h1>
|
||||
<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1>
|
||||
|
||||
<div class="info">
|
||||
<span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span>
|
||||
<span class="stats" v-if="stats">
|
||||
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
|
||||
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
|
||||
</span>
|
||||
<div class="info">
|
||||
<span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span>
|
||||
<span class="stats" v-if="stats">
|
||||
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
|
||||
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
<span class="desc" v-html="description || '%i18n:common.about%'"></span>
|
||||
<a class="about" @click="about">%i18n:@about%</a>
|
||||
</div>
|
||||
|
||||
<p class="sign">
|
||||
<span class="signup" @click="signup">%i18n:@signup%</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="signin" @click="signin">%i18n:@signin%</span>
|
||||
</p>
|
||||
|
||||
<img src="/assets/ai.png" alt="" title="藍" class="char">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="desc" v-html="description || '%i18n:common.about%'"></p>
|
||||
<div class="announcements block">
|
||||
<header>%fa:broadcast-tower% %i18n:@announcements%</header>
|
||||
<div v-if="announcements && announcements.length > 0">
|
||||
<div v-for="announcement in announcements">
|
||||
<h1 v-html="announcement.title"></h1>
|
||||
<div v-html="announcement.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="sign">
|
||||
<span class="signup" @click="signup">%i18n:@signup%</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="signin" @click="signin">%i18n:@signin%</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="photos block">
|
||||
<header>%fa:images% %i18n:@photos%</header>
|
||||
<div>
|
||||
<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="broadcasts block">
|
||||
<div v-for="broadcast in broadcasts">
|
||||
<h1 v-html="broadcast.title"></h1>
|
||||
<div v-html="broadcast.text"></div>
|
||||
<div class="tag-cloud block">
|
||||
<div>
|
||||
<mk-tag-cloud/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav block">
|
||||
<div>
|
||||
<mk-nav class="nav"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side">
|
||||
<div class="trends block">
|
||||
<div>
|
||||
<mk-trends/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tl block">
|
||||
<header>%fa:comment-alt R% %i18n:@timeline%</header>
|
||||
<div>
|
||||
<mk-welcome-timeline class="tl" :max="20"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info block">
|
||||
<header>%fa:info-circle% %i18n:@info%</header>
|
||||
<div>
|
||||
<div v-if="meta" class="body">
|
||||
<p>Version: <b>{{ meta.version }}</b></p>
|
||||
<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="nav block">
|
||||
<mk-nav class="nav"/>
|
||||
</div>
|
||||
|
||||
<div class="side">
|
||||
<mk-trends class="trends block"/>
|
||||
|
||||
<mk-welcome-timeline class="tl block" :max="20"/>
|
||||
</div>
|
||||
</div>
|
||||
<modal name="about" :class="$store.state.device.darkmode ? ['about', 'modal-dark'] : ['about', 'modal-light']" width="800px" height="auto" scrollable>
|
||||
<article class="fpdezooorhntlzyeszemrsqdlgbysvxq">
|
||||
<h1>%i18n:common.intro.title%</h1>
|
||||
<p v-html="'%i18n:common.intro.about%'"></p>
|
||||
<section>
|
||||
<h2>%i18n:common.intro.features%</h2>
|
||||
<section>
|
||||
<div class="body">
|
||||
<h3>%i18n:common.intro.rich-contents%</h3>
|
||||
<p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
|
||||
</div>
|
||||
<div class="image"><img src="/assets/about/post.png" alt=""></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="body">
|
||||
<h3>%i18n:common.intro.reaction%</h3>
|
||||
<p v-html="'%i18n:common.intro.reaction-desc%'"></p>
|
||||
</div>
|
||||
<div class="image"><img src="/assets/about/reaction.png" alt=""></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="body">
|
||||
<h3>%i18n:common.intro.ui%</h3>
|
||||
<p v-html="'%i18n:common.intro.ui-desc%'"></p>
|
||||
</div>
|
||||
<div class="image"><img src="/assets/about/ui.png" alt=""></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="body">
|
||||
<h3>%i18n:common.intro.drive%</h3>
|
||||
<p v-html="'%i18n:common.intro.drive-desc%'"></p>
|
||||
</div>
|
||||
<div class="image"><img src="/assets/about/drive.png" alt=""></div>
|
||||
</section>
|
||||
</section>
|
||||
<p v-html="'%i18n:common.intro.outro%'"></p>
|
||||
</article>
|
||||
</modal>
|
||||
|
||||
<modal name="signup" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable>
|
||||
<header class="formHeader">%i18n:@signup%</header>
|
||||
@ -62,37 +147,62 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { host, copyright } from '../../../config';
|
||||
import { concat } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
meta: null,
|
||||
stats: null,
|
||||
copyright,
|
||||
host,
|
||||
name: 'Misskey',
|
||||
description: '',
|
||||
broadcasts: []
|
||||
announcements: [],
|
||||
photos: []
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.meta = meta;
|
||||
this.name = meta.name;
|
||||
this.description = meta.description;
|
||||
this.broadcasts = meta.broadcasts;
|
||||
this.announcements = meta.broadcasts;
|
||||
});
|
||||
|
||||
(this as any).api('stats').then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
|
||||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
];
|
||||
|
||||
(this as any).api('notes/local-timeline', {
|
||||
fileType: image,
|
||||
limit: 6
|
||||
}).then((notes: any[]) => {
|
||||
const files = concat(notes.map((n: any): any[] => n.files));
|
||||
this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
about() {
|
||||
this.$modal.show('about');
|
||||
},
|
||||
|
||||
signup() {
|
||||
this.$modal.show('signup');
|
||||
},
|
||||
|
||||
signin() {
|
||||
this.$modal.show('signin');
|
||||
},
|
||||
|
||||
dark() {
|
||||
this.$store.commit('device/set', {
|
||||
key: 'darkmode',
|
||||
@ -137,6 +247,54 @@ export default Vue.extend({
|
||||
margin 0 48px
|
||||
font-size 1.5em
|
||||
|
||||
.v--modal-overlay.about
|
||||
.v--modal-box.v--modal
|
||||
margin 32px 0
|
||||
|
||||
.fpdezooorhntlzyeszemrsqdlgbysvxq
|
||||
padding 64px
|
||||
|
||||
> p:last-child
|
||||
margin-bottom 0
|
||||
|
||||
> h1
|
||||
margin-top 0
|
||||
|
||||
> section
|
||||
> h2
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
|
||||
|
||||
> section
|
||||
display grid
|
||||
grid-template-rows 1fr
|
||||
grid-template-columns 180px 1fr
|
||||
gap 32px
|
||||
margin-bottom 32px
|
||||
padding-bottom 32px
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
|
||||
|
||||
&:nth-child(odd)
|
||||
grid-template-columns 1fr 180px
|
||||
|
||||
> .body
|
||||
grid-column 1
|
||||
|
||||
> .image
|
||||
grid-column 2
|
||||
|
||||
> .body
|
||||
grid-row 1
|
||||
grid-column 2
|
||||
|
||||
> .image
|
||||
grid-row 1
|
||||
grid-column 1
|
||||
|
||||
> img
|
||||
display block
|
||||
width 100%
|
||||
height 100%
|
||||
object-fit cover
|
||||
</style>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@ -164,116 +322,176 @@ root(isDark)
|
||||
font-size 18px
|
||||
color isDark ? #fff : #444
|
||||
|
||||
> .body
|
||||
display grid
|
||||
grid-template-rows 0.5fr 0.5fr 64px
|
||||
grid-template-columns 1fr 350px
|
||||
gap 16px
|
||||
width 100%
|
||||
max-width 1200px
|
||||
height 100vh
|
||||
min-height 800px
|
||||
> main
|
||||
margin 0 auto
|
||||
padding 64px
|
||||
width 100%
|
||||
max-width 1200px
|
||||
|
||||
.block
|
||||
color isDark ? #fff : #444
|
||||
background isDark ? #313543 : #fff
|
||||
background isDark ? #282C37 : #fff
|
||||
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
|
||||
//border-radius 8px
|
||||
overflow auto
|
||||
|
||||
> .main
|
||||
grid-row 1
|
||||
grid-column 1
|
||||
padding 32px
|
||||
border-top solid 5px $theme-color
|
||||
> header
|
||||
z-index 1
|
||||
padding 0 16px
|
||||
line-height 48px
|
||||
background isDark ? #313543 : #fff
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
if !isDark
|
||||
box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> img
|
||||
margin -8px 0 0 -16px
|
||||
max-width 280px
|
||||
|
||||
> .info
|
||||
margin 0 auto 16px auto
|
||||
width $width
|
||||
font-size 14px
|
||||
|
||||
> .stats
|
||||
margin-left 16px
|
||||
padding-left 16px
|
||||
border-left solid 1px isDark ? #fff : #444
|
||||
|
||||
> *
|
||||
margin-right 16px
|
||||
|
||||
> .sign
|
||||
font-size 120%
|
||||
|
||||
> .divider
|
||||
margin 0 16px
|
||||
|
||||
> .signin
|
||||
> .signup
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
> .hashtags
|
||||
margin 16px auto
|
||||
width $width
|
||||
font-size 14px
|
||||
background rgba(#000, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
> *
|
||||
display inline-block
|
||||
margin 14px
|
||||
|
||||
> .broadcasts
|
||||
grid-row 2
|
||||
grid-column 1
|
||||
padding 32px
|
||||
& + div
|
||||
max-height calc(100% - 48px)
|
||||
|
||||
> div
|
||||
padding 0 0 16px 0
|
||||
margin 0 0 16px 0
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
font-size 1.5em
|
||||
|
||||
> .nav
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
grid-row 3
|
||||
grid-column 1
|
||||
font-size 14px
|
||||
|
||||
> .side
|
||||
display grid
|
||||
grid-row 1 / 4
|
||||
grid-column 2
|
||||
grid-template-rows 1fr 350px
|
||||
grid-template-columns 1fr
|
||||
gap 16px
|
||||
|
||||
> .tl
|
||||
grid-row 1
|
||||
grid-column 1
|
||||
text-align left
|
||||
max-height 100%
|
||||
overflow auto
|
||||
|
||||
> .trends
|
||||
> .body
|
||||
display grid
|
||||
grid-template-rows 390px 1fr 256px 64px
|
||||
grid-template-columns 1fr 1fr 350px
|
||||
gap 16px
|
||||
height 1150px
|
||||
|
||||
> .main
|
||||
grid-row 1
|
||||
grid-column 1 / 3
|
||||
border-top solid 5px $theme-color
|
||||
|
||||
> div
|
||||
padding 32px
|
||||
min-height 100%
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
|
||||
> img
|
||||
margin -8px 0 0 -16px
|
||||
max-width 280px
|
||||
|
||||
> .info
|
||||
margin 0 auto 16px auto
|
||||
width $width
|
||||
font-size 14px
|
||||
|
||||
> .stats
|
||||
margin-left 16px
|
||||
padding-left 16px
|
||||
border-left solid 1px isDark ? #fff : #444
|
||||
|
||||
> *
|
||||
margin-right 16px
|
||||
|
||||
> .desc
|
||||
max-width calc(100% - 150px)
|
||||
|
||||
> .sign
|
||||
font-size 120%
|
||||
margin-bottom 0
|
||||
|
||||
> .divider
|
||||
margin 0 16px
|
||||
|
||||
> .signin
|
||||
> .signup
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
> .char
|
||||
display block
|
||||
position absolute
|
||||
right 16px
|
||||
bottom 0
|
||||
height 320px
|
||||
opacity 0.7
|
||||
|
||||
> *:not(.char)
|
||||
z-index 1
|
||||
|
||||
> .announcements
|
||||
grid-row 2
|
||||
grid-column 1
|
||||
padding 8px
|
||||
|
||||
> div
|
||||
padding 32px
|
||||
|
||||
> div
|
||||
padding 0 0 16px 0
|
||||
margin 0 0 16px 0
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
font-size 1.25em
|
||||
|
||||
> .photos
|
||||
grid-row 2
|
||||
grid-column 2
|
||||
|
||||
> div
|
||||
display grid
|
||||
grid-template-rows 1fr 1fr 1fr
|
||||
grid-template-columns 1fr 1fr
|
||||
gap 8px
|
||||
height 100%
|
||||
padding 16px
|
||||
|
||||
> div
|
||||
//border-radius 4px
|
||||
background-position center center
|
||||
background-size cover
|
||||
|
||||
> .tag-cloud
|
||||
grid-row 3
|
||||
grid-column 1 / 3
|
||||
|
||||
> div
|
||||
height 256px
|
||||
padding 32px
|
||||
|
||||
> .nav
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
grid-row 4
|
||||
grid-column 1 / 3
|
||||
font-size 14px
|
||||
|
||||
> .side
|
||||
display grid
|
||||
grid-row 1 / 5
|
||||
grid-column 3
|
||||
grid-template-rows 1fr 350px
|
||||
grid-template-columns 1fr
|
||||
gap 16px
|
||||
|
||||
> .tl
|
||||
grid-row 1
|
||||
grid-column 1
|
||||
overflow auto
|
||||
|
||||
> .trends
|
||||
grid-row 2
|
||||
grid-column 1
|
||||
padding 8px
|
||||
|
||||
> .info
|
||||
grid-row 3
|
||||
grid-column 1
|
||||
|
||||
> div
|
||||
padding 16px
|
||||
|
||||
> .body
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
|
||||
.mk-welcome[data-darkmode]
|
||||
root(true)
|
||||
|
@ -49,7 +49,7 @@ export default define({
|
||||
offset: this.offset,
|
||||
renote: false,
|
||||
reply: false,
|
||||
media: false,
|
||||
file: false,
|
||||
poll: false
|
||||
}).then(notes => {
|
||||
const note = notes ? notes[0] : null;
|
||||
|
@ -17,6 +17,7 @@ import Err from './common/views/components/connect-failed.vue';
|
||||
import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
|
||||
import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline';
|
||||
import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
|
||||
import { erase } from '../../prelude/array';
|
||||
|
||||
//#region api requests
|
||||
let spinner = null;
|
||||
@ -537,7 +538,7 @@ export default class MiOS extends EventEmitter {
|
||||
}
|
||||
|
||||
public unregisterStreamConnection(connection: Connection) {
|
||||
this.connections = this.connections.filter(c => c != connection);
|
||||
this.connections = erase(connection, this.connections);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@
|
||||
import Vue from 'vue';
|
||||
import * as EXIF from 'exif-js';
|
||||
import * as hljs from 'highlight.js';
|
||||
import gcd from '../../../common/scripts/gcd';
|
||||
import { gcd } from '../../../../../prelude/math';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['file'],
|
||||
|
@ -40,8 +40,8 @@
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media" :raw="true"/>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files" :raw="true"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
||||
@ -85,6 +85,7 @@ import parse from '../../../../../mfm/parse';
|
||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
||||
import XSub from './note.sub.vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -113,7 +114,7 @@ export default Vue.extend({
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds.length == 0 &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
@ -123,9 +124,7 @@ export default Vue.extend({
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? Object.keys(this.p.reactionCounts)
|
||||
.map(key => this.p.reactionCounts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
@ -369,7 +368,7 @@ root(isDark)
|
||||
> .mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .media
|
||||
> .files
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
@ -28,8 +28,8 @@
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.renote != null">RP:</a>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
@ -70,6 +70,7 @@ import parse from '../../../../../mfm/parse';
|
||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
||||
import XSub from './note.sub.vue';
|
||||
import { sum } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -90,7 +91,7 @@ export default Vue.extend({
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds.length == 0 &&
|
||||
this.note.fileIds.length == 0 &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
@ -100,9 +101,7 @@ export default Vue.extend({
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? Object.keys(this.p.reactionCounts)
|
||||
.map(key => this.p.reactionCounts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
@ -414,7 +413,7 @@ root(isDark)
|
||||
.mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .media
|
||||
> .files
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
@ -125,7 +125,7 @@ export default Vue.extend({
|
||||
prepend(note, silent = false) {
|
||||
//#region 弾く
|
||||
const isMyNote = note.userId == this.$store.state.i.id;
|
||||
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
|
||||
const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
|
||||
if (this.$store.state.settings.showMyRenotes === false) {
|
||||
if (isMyNote && isPureRenote) {
|
||||
|
@ -59,6 +59,7 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho
|
||||
import getFace from '../../../common/scripts/get-face';
|
||||
import parse from '../../../../../mfm/parse';
|
||||
import { host } from '../../../config';
|
||||
import { erase } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -94,7 +95,7 @@ export default Vue.extend({
|
||||
files: [],
|
||||
poll: false,
|
||||
geo: null,
|
||||
visibility: this.$store.state.device.visibility || 'public',
|
||||
visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
|
||||
visibleUsers: [],
|
||||
useCw: false,
|
||||
cw: null,
|
||||
@ -200,12 +201,12 @@ export default Vue.extend({
|
||||
|
||||
attachMedia(driveFile) {
|
||||
this.files.push(driveFile);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
},
|
||||
|
||||
detachMedia(file) {
|
||||
this.files = this.files.filter(x => x.id != file.id);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
this.$emit('change-attached-files', this.files);
|
||||
},
|
||||
|
||||
onChangeFile() {
|
||||
@ -262,14 +263,14 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
removeVisibleUser(user) {
|
||||
this.visibleUsers = this.visibleUsers.filter(u => u != user);
|
||||
this.visibleUsers = erase(user, this.visibleUsers);
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.text = '';
|
||||
this.files = [];
|
||||
this.poll = false;
|
||||
this.$emit('change-attached-media');
|
||||
this.$emit('change-attached-files');
|
||||
},
|
||||
|
||||
post() {
|
||||
@ -277,7 +278,7 @@ export default Vue.extend({
|
||||
const viaMobile = this.$store.state.settings.disableViaMobile !== true;
|
||||
(this as any).api('notes/create', {
|
||||
text: this.text == '' ? undefined : this.text,
|
||||
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
replyId: this.reply ? this.reply.id : undefined,
|
||||
renoteId: this.renote ? this.renote.id : undefined,
|
||||
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
|
||||
|
@ -7,9 +7,9 @@
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
|
||||
<a class="rp" v-if="note.renoteId">RP: ...</a>
|
||||
</div>
|
||||
<details v-if="note.media.length > 0">
|
||||
<summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
|
||||
<mk-media-list :media-list="note.media"/>
|
||||
<details v-if="note.files.length > 0">
|
||||
<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
|
||||
<mk-media-list :media-list="note.files"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
<summary>%i18n:@poll%</summary>
|
||||
|
@ -34,6 +34,12 @@
|
||||
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="announcements" v-if="announcements && announcements.length > 0">
|
||||
<article v-for="announcement in announcements">
|
||||
<span v-html="announcement.title" class="title"></span>
|
||||
<div v-html="announcement.text"></div>
|
||||
</article>
|
||||
</div>
|
||||
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
|
||||
</div>
|
||||
</transition>
|
||||
@ -46,23 +52,32 @@ import { lang } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['isOpen'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
hasGameInvitation: false,
|
||||
connection: null,
|
||||
connectionId: null,
|
||||
aboutUrl: `/docs/${lang}/about`
|
||||
aboutUrl: `/docs/${lang}/about`,
|
||||
announcements: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasUnreadNotification(): boolean {
|
||||
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
|
||||
},
|
||||
|
||||
hasUnreadMessagingMessage(): boolean {
|
||||
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.announcements = meta.broadcasts;
|
||||
});
|
||||
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection = (this as any).os.stream.getConnection();
|
||||
this.connectionId = (this as any).os.stream.use();
|
||||
@ -71,6 +86,7 @@ export default Vue.extend({
|
||||
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection.off('reversi_invited', this.onReversiInvited);
|
||||
@ -78,18 +94,22 @@ export default Vue.extend({
|
||||
(this as any).os.stream.dispose(this.connectionId);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
search() {
|
||||
const query = window.prompt('%i18n:@search%');
|
||||
if (query == null || query == '') return;
|
||||
this.$router.push(`/search?q=${encodeURIComponent(query)}`);
|
||||
},
|
||||
|
||||
onReversiInvited() {
|
||||
this.hasGameInvitation = true;
|
||||
},
|
||||
|
||||
onReversiNoInvites() {
|
||||
this.hasGameInvitation = false;
|
||||
},
|
||||
|
||||
dark() {
|
||||
this.$store.commit('device/set', {
|
||||
key: 'darkmode',
|
||||
@ -204,6 +224,17 @@ root(isDark)
|
||||
color $color
|
||||
opacity 0.5
|
||||
|
||||
.announcements
|
||||
> article
|
||||
background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
|
||||
color isDark ? #fff : #3f4967
|
||||
padding 16px
|
||||
margin 8px 0
|
||||
font-size 12px
|
||||
|
||||
> .title
|
||||
font-weight bold
|
||||
|
||||
.about
|
||||
margin 0 0 8px 0
|
||||
padding 1em 0
|
||||
|
@ -41,7 +41,7 @@ export default Vue.extend({
|
||||
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
|
||||
(this as any).api('users/notes', {
|
||||
userId: this.user.id,
|
||||
withMedia: this.withMedia,
|
||||
withFiles: this.withMedia,
|
||||
limit: fetchLimit + 1
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
@ -62,7 +62,7 @@ export default Vue.extend({
|
||||
|
||||
const promise = (this as any).api('users/notes', {
|
||||
userId: this.user.id,
|
||||
withMedia: this.withMedia,
|
||||
withFiles: this.withMedia,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: (this.$refs.timeline as any).tail().id
|
||||
});
|
||||
|
@ -13,6 +13,7 @@
|
||||
<section>
|
||||
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
|
||||
<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
|
||||
<ui-switch v-model="$store.state.settings.contrastedAcct" @change="onChangeContrastedAcct">%i18n:@contrasted-acct%</ui-switch>
|
||||
<ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch>
|
||||
<ui-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
|
||||
<ui-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
|
||||
@ -52,6 +53,21 @@
|
||||
<ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
|
||||
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>%i18n:@note-visibility%</header>
|
||||
<ui-switch v-model="$store.state.settings.rememberNoteVisibility" @change="onChangeRememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
|
||||
<section>
|
||||
<header>%i18n:@default-note-visibility%</header>
|
||||
<ui-select v-model="defaultNoteVisibility">
|
||||
<option value="public">%i18n:common.note-visibility.public%</option>
|
||||
<option value="home">%i18n:common.note-visibility.home%</option>
|
||||
<option value="followers">%i18n:common.note-visibility.followers%</option>
|
||||
<option value="specified">%i18n:common.note-visibility.specified%</option>
|
||||
<option value="private">%i18n:common.note-visibility.private%</option>
|
||||
</ui-select>
|
||||
</section>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
@ -160,6 +176,11 @@ export default Vue.extend({
|
||||
set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); }
|
||||
},
|
||||
|
||||
defaultNoteVisibility: {
|
||||
get() { return this.$store.state.settings.defaultNoteVisibility; },
|
||||
set(value) { this.$store.commit('settings/set', { key: 'defaultNoteVisibility', value }); }
|
||||
},
|
||||
|
||||
lightmode: {
|
||||
get() { return this.$store.state.device.lightmode; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
|
||||
@ -197,6 +218,13 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
onChangeRememberNoteVisibility(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'rememberNoteVisibility',
|
||||
value: v
|
||||
});
|
||||
},
|
||||
|
||||
onChangeDisableViaMobile(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'disableViaMobile',
|
||||
@ -218,6 +246,13 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
onChangeContrastedAcct(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'contrastedAcct',
|
||||
value: v
|
||||
});
|
||||
},
|
||||
|
||||
onChangeILikeSushi(v) {
|
||||
this.$store.dispatch('settings/set', {
|
||||
key: 'iLikeSushi',
|
||||
|
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div class="title">
|
||||
<h1>{{ user | userName }}</h1>
|
||||
<span class="username"><mk-acct :user="user"/></span>
|
||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
||||
<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
|
@ -26,7 +26,7 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
(this as any).api('users/notes', {
|
||||
userId: this.user.id,
|
||||
withMedia: true,
|
||||
withFiles: true,
|
||||
limit: 6
|
||||
}).then(notes => {
|
||||
notes.forEach(note => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
|
||||
<div>
|
||||
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name">
|
||||
<p class="host">{{ host }}</p>
|
||||
@ -15,12 +15,53 @@
|
||||
<mk-welcome-timeline/>
|
||||
</div>
|
||||
<div class="hashtags">
|
||||
<router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
|
||||
<mk-tag-cloud/>
|
||||
</div>
|
||||
<div class="photos">
|
||||
<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
|
||||
</div>
|
||||
<div class="stats" v-if="stats">
|
||||
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
|
||||
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
|
||||
</div>
|
||||
<div class="announcements" v-if="announcements && announcements.length > 0">
|
||||
<article v-for="announcement in announcements">
|
||||
<span class="title" v-html="announcement.title"></span>
|
||||
<div v-html="announcement.text"></div>
|
||||
</article>
|
||||
</div>
|
||||
<article class="about-misskey">
|
||||
<h1>%i18n:common.intro.title%</h1>
|
||||
<p v-html="'%i18n:common.intro.about%'"></p>
|
||||
<section>
|
||||
<h2>%i18n:common.intro.features%</h2>
|
||||
<section>
|
||||
<h3>%i18n:common.intro.rich-contents%</h3>
|
||||
<div class="image"><img src="/assets/about/post.png" alt=""></div>
|
||||
<p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>%i18n:common.intro.reaction%</h3>
|
||||
<div class="image"><img src="/assets/about/reaction.png" alt=""></div>
|
||||
<p v-html="'%i18n:common.intro.reaction-desc%'"></p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>%i18n:common.intro.ui%</h3>
|
||||
<div class="image"><img src="/assets/about/ui.png" alt=""></div>
|
||||
<p v-html="'%i18n:common.intro.ui-desc%'"></p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>%i18n:common.intro.drive%</h3>
|
||||
<div class="image"><img src="/assets/about/drive.png" alt=""></div>
|
||||
<p v-html="'%i18n:common.intro.drive-desc%'"></p>
|
||||
</section>
|
||||
</section>
|
||||
<p v-html="'%i18n:common.intro.outro%'"></p>
|
||||
</article>
|
||||
<div class="info" v-if="meta">
|
||||
<p>Version: <b>{{ meta.version }}</b></p>
|
||||
<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
|
||||
</div>
|
||||
<footer>
|
||||
<small>{{ copyright }}</small>
|
||||
</footer>
|
||||
@ -30,39 +71,53 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { apiUrl, copyright, host } from '../../../config';
|
||||
import { copyright, host } from '../../../config';
|
||||
import { concat } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
apiUrl,
|
||||
meta: null,
|
||||
copyright,
|
||||
stats: null,
|
||||
host,
|
||||
name: 'Misskey',
|
||||
description: '',
|
||||
tags: []
|
||||
photos: [],
|
||||
announcements: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.meta = meta;
|
||||
this.name = meta.name;
|
||||
this.description = meta.description;
|
||||
this.announcements = meta.broadcasts;
|
||||
});
|
||||
|
||||
(this as any).api('stats').then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
|
||||
(this as any).api('hashtags/trend').then(stats => {
|
||||
this.tags = stats.map(x => x.tag);
|
||||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
];
|
||||
|
||||
(this as any).api('notes/local-timeline', {
|
||||
fileType: image,
|
||||
limit: 6
|
||||
}).then((notes: any[]) => {
|
||||
const files = concat(notes.map((n: any): any[] => n.files));
|
||||
this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.welcome
|
||||
root(isDark)
|
||||
text-align center
|
||||
//background #fff
|
||||
|
||||
@ -138,12 +193,21 @@ export default Vue.extend({
|
||||
-webkit-overflow-scrolling touch
|
||||
|
||||
> .hashtags
|
||||
padding 16px 0
|
||||
border solid 2px #ddd
|
||||
border-radius 8px
|
||||
padding 0 8px
|
||||
height 200px
|
||||
|
||||
> *
|
||||
margin 0 16px
|
||||
> .photos
|
||||
display grid
|
||||
grid-template-rows 1fr 1fr 1fr
|
||||
grid-template-columns 1fr 1fr
|
||||
gap 8px
|
||||
height 300px
|
||||
margin-top 16px
|
||||
|
||||
> div
|
||||
border-radius 4px
|
||||
background-position center center
|
||||
background-size cover
|
||||
|
||||
> .stats
|
||||
margin 16px 0
|
||||
@ -156,6 +220,68 @@ export default Vue.extend({
|
||||
> *
|
||||
margin 0 8px
|
||||
|
||||
> .announcements
|
||||
margin 16px 0
|
||||
|
||||
> article
|
||||
background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
|
||||
border-radius 6px
|
||||
color isDark ? #fff : #3f4967
|
||||
padding 16px
|
||||
margin 8px 0
|
||||
font-size 12px
|
||||
|
||||
> .title
|
||||
font-weight bold
|
||||
|
||||
> .about-misskey
|
||||
margin 16px 0
|
||||
padding 32px
|
||||
font-size 14px
|
||||
background #fff
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
color #3a3e46
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
|
||||
& + p
|
||||
margin-top 8px
|
||||
|
||||
> p:last-child
|
||||
margin-bottom 0
|
||||
|
||||
> section
|
||||
> h2
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
|
||||
|
||||
> section
|
||||
margin-bottom 16px
|
||||
padding-bottom 16px
|
||||
border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
|
||||
|
||||
> h3
|
||||
margin-bottom 8px
|
||||
|
||||
> p
|
||||
margin-bottom 0
|
||||
|
||||
> .image
|
||||
> img
|
||||
display block
|
||||
width 100%
|
||||
height 120px
|
||||
object-fit cover
|
||||
|
||||
> .info
|
||||
padding 16px 0
|
||||
border solid 2px #ddd
|
||||
border-radius 8px
|
||||
|
||||
> *
|
||||
margin 0 16px
|
||||
|
||||
> footer
|
||||
text-align center
|
||||
color #444
|
||||
@ -165,4 +291,10 @@ export default Vue.extend({
|
||||
margin 16px 0 0 0
|
||||
opacity 0.7
|
||||
|
||||
.wgwfgvvimdjvhjfwxropcwksnzftjqes[data-darkmode]
|
||||
root(true)
|
||||
|
||||
.wgwfgvvimdjvhjfwxropcwksnzftjqes:not([data-darkmode])
|
||||
root(false)
|
||||
|
||||
</style>
|
||||
|
@ -4,6 +4,7 @@ import * as nestedProperty from 'nested-property';
|
||||
|
||||
import MiOS from './mios';
|
||||
import { hostname } from './config';
|
||||
import { erase } from '../../prelude/array';
|
||||
|
||||
const defaultSettings = {
|
||||
home: null,
|
||||
@ -15,6 +16,7 @@ const defaultSettings = {
|
||||
suggestRecentHashtags: true,
|
||||
showClockOnHeader: true,
|
||||
circleIcons: true,
|
||||
contrastedAcct: true,
|
||||
gradientWindowHeader: false,
|
||||
showReplyTarget: true,
|
||||
showMyRenotes: true,
|
||||
@ -24,6 +26,8 @@ const defaultSettings = {
|
||||
disableViaMobile: false,
|
||||
memo: null,
|
||||
iLikeSushi: false,
|
||||
rememberNoteVisibility: false,
|
||||
defaultNoteVisibility: 'public',
|
||||
games: {
|
||||
reversi: {
|
||||
showBoardLabels: false,
|
||||
@ -195,7 +199,7 @@ export default (os: MiOS) => new Vuex.Store({
|
||||
|
||||
removeDeckColumn(state, id) {
|
||||
state.deck.columns = state.deck.columns.filter(c => c.id != id);
|
||||
state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
|
||||
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
||||
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
||||
},
|
||||
|
||||
@ -266,7 +270,7 @@ export default (os: MiOS) => new Vuex.Store({
|
||||
|
||||
stackLeftDeckColumn(state, id) {
|
||||
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
|
||||
state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
|
||||
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
||||
const left = state.deck.layout[i - 1];
|
||||
if (left) state.deck.layout[i - 1].push(id);
|
||||
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
||||
@ -274,7 +278,7 @@ export default (os: MiOS) => new Vuex.Store({
|
||||
|
||||
popRightDeckColumn(state, id) {
|
||||
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
|
||||
state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
|
||||
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
||||
state.deck.layout.splice(i + 1, 0, [id]);
|
||||
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
||||
},
|
||||
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import composeNotification from './common/scripts/compose-notification';
|
||||
import { erase } from '../../prelude/array';
|
||||
|
||||
// キャッシュするリソース
|
||||
const cachee = [
|
||||
@ -24,8 +25,7 @@ self.addEventListener('activate', ev => {
|
||||
// Clean up old caches
|
||||
ev.waitUntil(
|
||||
caches.keys().then(keys => Promise.all(
|
||||
keys
|
||||
.filter(key => key != _VERSION_)
|
||||
erase(_VERSION_, keys)
|
||||
.map(key => caches.delete(key))
|
||||
))
|
||||
);
|
||||
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 274 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 8.4 KiB |
@ -4,6 +4,12 @@ import config from '../config';
|
||||
const index = {
|
||||
settings: {
|
||||
analysis: {
|
||||
normalizer: {
|
||||
lowercase_normalizer: {
|
||||
type: 'custom',
|
||||
filter: ['lowercase']
|
||||
}
|
||||
},
|
||||
analyzer: {
|
||||
bigram: {
|
||||
tokenizer: 'bigram_tokenizer'
|
||||
@ -24,7 +30,8 @@ const index = {
|
||||
text: {
|
||||
type: 'text',
|
||||
index: true,
|
||||
analyzer: 'bigram'
|
||||
analyzer: 'bigram',
|
||||
normalizer: 'lowercase_normalizer'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,19 +33,19 @@ props:
|
||||
ja-JP: "投稿の本文"
|
||||
en-US: "The text of this note"
|
||||
|
||||
mediaIds:
|
||||
fileIds:
|
||||
type: "id(DriveFile)[]"
|
||||
optional: true
|
||||
desc:
|
||||
ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)"
|
||||
en-US: "The IDs of the attached media (empty array for response if no media is attached)"
|
||||
ja-JP: "添付されているファイルのID (なければレスポンスでは空配列)"
|
||||
en-US: "The IDs of the attached files (empty array for response if no files is attached)"
|
||||
|
||||
media:
|
||||
files:
|
||||
type: "entity(DriveFile)[]"
|
||||
optional: true
|
||||
desc:
|
||||
ja-JP: "添付されているメディア"
|
||||
en-US: "The attached media"
|
||||
ja-JP: "添付されているファイル"
|
||||
en-US: "The attached files"
|
||||
|
||||
userId:
|
||||
type: "id(User)"
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { count, concat } from "../../prelude/array";
|
||||
|
||||
// MISSKEY REVERSI ENGINE
|
||||
|
||||
/**
|
||||
@ -88,8 +90,8 @@ export default class Reversi {
|
||||
//#endregion
|
||||
|
||||
// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
|
||||
if (this.canPutSomewhere(BLACK).length == 0) {
|
||||
if (this.canPutSomewhere(WHITE).length == 0) {
|
||||
if (!this.canPutSomewhere(BLACK)) {
|
||||
if (!this.canPutSomewhere(WHITE)) {
|
||||
this.turn = null;
|
||||
} else {
|
||||
this.turn = WHITE;
|
||||
@ -101,14 +103,14 @@ export default class Reversi {
|
||||
* 黒石の数
|
||||
*/
|
||||
public get blackCount() {
|
||||
return this.board.filter(x => x === BLACK).length;
|
||||
return count(BLACK, this.board);
|
||||
}
|
||||
|
||||
/**
|
||||
* 白石の数
|
||||
*/
|
||||
public get whiteCount() {
|
||||
return this.board.filter(x => x === WHITE).length;
|
||||
return count(WHITE, this.board);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,9 +172,9 @@ export default class Reversi {
|
||||
|
||||
private calcTurn() {
|
||||
// ターン計算
|
||||
if (this.canPutSomewhere(!this.prevColor).length > 0) {
|
||||
if (this.canPutSomewhere(!this.prevColor)) {
|
||||
this.turn = !this.prevColor;
|
||||
} else if (this.canPutSomewhere(this.prevColor).length > 0) {
|
||||
} else if (this.canPutSomewhere(this.prevColor)) {
|
||||
this.turn = this.prevColor;
|
||||
} else {
|
||||
this.turn = null;
|
||||
@ -204,10 +206,17 @@ export default class Reversi {
|
||||
/**
|
||||
* 打つことができる場所を取得します
|
||||
*/
|
||||
public canPutSomewhere(color: Color): number[] {
|
||||
public puttablePlaces(color: Color): number[] {
|
||||
return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
|
||||
}
|
||||
|
||||
/**
|
||||
* 打つことができる場所があるかどうかを取得します
|
||||
*/
|
||||
public canPutSomewhere(color: Color): boolean {
|
||||
return this.puttablePlaces(color).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定のマスに石を打つことができるかどうかを取得します
|
||||
* @param color 自分の色
|
||||
@ -229,87 +238,55 @@ export default class Reversi {
|
||||
/**
|
||||
* 指定のマスに石を置いた時の、反転させられる石を取得します
|
||||
* @param color 自分の色
|
||||
* @param pos 位置
|
||||
* @param initPos 位置
|
||||
*/
|
||||
public effects(color: Color, pos: number): number[] {
|
||||
public effects(color: Color, initPos: number): number[] {
|
||||
const enemyColor = !color;
|
||||
|
||||
// ひっくり返せる石(の位置)リスト
|
||||
let stones: number[] = [];
|
||||
const diffVectors: [number, number][] = [
|
||||
[ 0, -1], // 上
|
||||
[ +1, -1], // 右上
|
||||
[ +1, 0], // 右
|
||||
[ +1, +1], // 右下
|
||||
[ 0, +1], // 下
|
||||
[ -1, +1], // 左下
|
||||
[ -1, 0], // 左
|
||||
[ -1, -1] // 左上
|
||||
];
|
||||
|
||||
const initPos = pos;
|
||||
|
||||
// 走査
|
||||
const iterate = (fn: (i: number) => number[]) => {
|
||||
let i = 1;
|
||||
const found = [];
|
||||
const effectsInLine = ([dx, dy]: [number, number]): number[] => {
|
||||
const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
|
||||
|
||||
const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
|
||||
let [x, y] = this.transformPosToXy(initPos);
|
||||
while (true) {
|
||||
let [x, y] = fn(i);
|
||||
[x, y] = nextPos(x, y);
|
||||
|
||||
// 座標が指し示す位置がボード外に出たとき
|
||||
if (this.opts.loopedBoard) {
|
||||
if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth);
|
||||
if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight);
|
||||
if (x >= this.mapWidth ) x = x % this.mapWidth;
|
||||
if (y >= this.mapHeight) y = y % this.mapHeight;
|
||||
x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth;
|
||||
y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight;
|
||||
|
||||
// for debug
|
||||
//if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) {
|
||||
// console.log(x, y);
|
||||
//}
|
||||
|
||||
// 一周して自分に帰ってきたら
|
||||
if (this.transformXyToPos(x, y) == initPos) {
|
||||
// ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、
|
||||
// そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります)
|
||||
// このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます
|
||||
// (あと無効な方がゲームとしておもしろそうだった)
|
||||
stones = stones.concat(found);
|
||||
break;
|
||||
// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
|
||||
return found;
|
||||
}
|
||||
} else {
|
||||
if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break;
|
||||
if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) {
|
||||
return []; // 挟めないことが確定 (盤面外に到達)
|
||||
}
|
||||
}
|
||||
|
||||
const pos = this.transformXyToPos(x, y);
|
||||
|
||||
//#region 「配置不能」マスに当たった場合走査終了
|
||||
const pixel = this.mapDataGet(pos);
|
||||
if (pixel == 'null') break;
|
||||
//#endregion
|
||||
|
||||
// 石取得
|
||||
if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
|
||||
const stone = this.board[pos];
|
||||
|
||||
// 石が置かれていないマスなら走査終了
|
||||
if (stone === null) break;
|
||||
|
||||
// 相手の石なら「ひっくり返せるかもリスト」に入れておく
|
||||
if (stone === enemyColor) found.push(pos);
|
||||
|
||||
// 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了
|
||||
if (stone === color) {
|
||||
stones = stones.concat(found);
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
|
||||
if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
|
||||
if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
|
||||
}
|
||||
};
|
||||
|
||||
const [x, y] = this.transformPosToXy(pos);
|
||||
|
||||
iterate(i => [x , y - i]); // 上
|
||||
iterate(i => [x + i, y - i]); // 右上
|
||||
iterate(i => [x + i, y ]); // 右
|
||||
iterate(i => [x + i, y + i]); // 右下
|
||||
iterate(i => [x , y + i]); // 下
|
||||
iterate(i => [x - i, y + i]); // 左下
|
||||
iterate(i => [x - i, y ]); // 左
|
||||
iterate(i => [x - i, y - i]); // 左上
|
||||
|
||||
return stones;
|
||||
return concat(diffVectors.map(effectsInLine));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,10 +4,7 @@ const { JSDOM } = jsdom;
|
||||
import config from '../config';
|
||||
import { INote } from '../models/note';
|
||||
import { TextElement } from './parse';
|
||||
|
||||
function intersperse<T>(sep: T, xs: T[]): T[] {
|
||||
return [].concat(...xs.map(x => [sep, x])).slice(1);
|
||||
}
|
||||
import { intersperse } from '../prelude/array';
|
||||
|
||||
const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: INote['mentionedRemoteUsers']) => void } = {
|
||||
bold({ document }, { bold }) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { capitalize } from "../../../prelude/string";
|
||||
|
||||
function escape(text: string) {
|
||||
return text
|
||||
.replace(/>/g, '>')
|
||||
@ -89,7 +91,7 @@ const _keywords = [
|
||||
];
|
||||
|
||||
const keywords = _keywords
|
||||
.concat(_keywords.map(k => k[0].toUpperCase() + k.substr(1)))
|
||||
.concat(_keywords.map(capitalize))
|
||||
.concat(_keywords.map(k => k.toUpperCase()))
|
||||
.sort((a, b) => b.length - a.length);
|
||||
|
||||
|
@ -16,9 +16,9 @@ const summarize = (note: any): string => {
|
||||
// 本文
|
||||
summary += note.text ? note.text : '';
|
||||
|
||||
// メディアが添付されているとき
|
||||
if (note.media.length != 0) {
|
||||
summary += ` (${note.media.length}つのメディア)`;
|
||||
// ファイルが添付されているとき
|
||||
if (note.files.length != 0) {
|
||||
summary += ` (${note.files.length}つのファイル)`;
|
||||
}
|
||||
|
||||
// 投票が添付されているとき
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { INote } from '../models/note';
|
||||
|
||||
export default function(note: INote): boolean {
|
||||
return note.renoteId != null && (note.text != null || note.poll != null || (note.mediaIds != null && note.mediaIds.length > 0));
|
||||
return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0));
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriv
|
||||
|
||||
// このDriveFileを添付しているNoteをすべて削除
|
||||
await Promise.all((
|
||||
await Note.find({ mediaIds: d._id })
|
||||
await Note.find({ fileIds: d._id })
|
||||
).map(x => deleteNote(x)));
|
||||
|
||||
// このDriveFileを添付しているMessagingMessageをすべて削除
|
||||
|
@ -4,12 +4,13 @@ const Meta = db.get<IMeta>('meta');
|
||||
export default Meta;
|
||||
|
||||
export type IMeta = {
|
||||
broadcasts: any[];
|
||||
stats: {
|
||||
broadcasts?: any[];
|
||||
stats?: {
|
||||
notesCount: number;
|
||||
originalNotesCount: number;
|
||||
usersCount: number;
|
||||
originalUsersCount: number;
|
||||
};
|
||||
disableRegistration: boolean;
|
||||
disableRegistration?: boolean;
|
||||
hidedTags?: string[];
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { IUser, pack as packUser } from './user';
|
||||
import { pack as packApp } from './app';
|
||||
import PollVote, { deletePollVote } from './poll-vote';
|
||||
import Reaction, { deleteNoteReaction } from './note-reaction';
|
||||
import { pack as packFile } from './drive-file';
|
||||
import { pack as packFile, IDriveFile } from './drive-file';
|
||||
import NoteWatching, { deleteNoteWatching } from './note-watching';
|
||||
import NoteReaction from './note-reaction';
|
||||
import Favorite, { deleteFavorite } from './favorite';
|
||||
@ -17,6 +17,7 @@ const Note = db.get<INote>('notes');
|
||||
Note.createIndex('uri', { sparse: true, unique: true });
|
||||
Note.createIndex('userId');
|
||||
Note.createIndex('tagsLower');
|
||||
Note.createIndex('_files.contentType');
|
||||
Note.createIndex({
|
||||
createdAt: -1
|
||||
});
|
||||
@ -34,7 +35,7 @@ export type INote = {
|
||||
_id: mongo.ObjectID;
|
||||
createdAt: Date;
|
||||
deletedAt: Date;
|
||||
mediaIds: mongo.ObjectID[];
|
||||
fileIds: mongo.ObjectID[];
|
||||
replyId: mongo.ObjectID;
|
||||
renoteId: mongo.ObjectID;
|
||||
poll: {
|
||||
@ -92,6 +93,7 @@ export type INote = {
|
||||
inbox?: string;
|
||||
};
|
||||
_replyIds?: mongo.ObjectID[];
|
||||
_files?: IDriveFile[];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -271,11 +273,15 @@ export const pack = async (
|
||||
_note.app = packApp(_note.appId);
|
||||
}
|
||||
|
||||
// Populate media
|
||||
_note.media = hide ? [] : Promise.all(_note.mediaIds.map((fileId: mongo.ObjectID) =>
|
||||
// Populate files
|
||||
_note.files = hide ? [] : Promise.all(_note.fileIds.map((fileId: mongo.ObjectID) =>
|
||||
packFile(fileId)
|
||||
));
|
||||
|
||||
// 後方互換性のため
|
||||
_note.mediaIds = _note.fileIds;
|
||||
_note.media = _note.files;
|
||||
|
||||
// When requested a detailed note data
|
||||
if (opts.detail) {
|
||||
//#region 重いので廃止
|
||||
@ -344,7 +350,7 @@ export const pack = async (
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
_note.mediaIds = [];
|
||||
_note.fileIds = [];
|
||||
_note.text = null;
|
||||
_note.poll = null;
|
||||
_note.cw = null;
|
||||
|
3
src/prelude/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Prelude
|
||||
このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。
|
||||
Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。
|
27
src/prelude/array.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export function countIf<T>(f: (x: T) => boolean, xs: T[]): number {
|
||||
return xs.filter(f).length;
|
||||
}
|
||||
|
||||
export function count<T>(x: T, xs: T[]): number {
|
||||
return countIf(y => x === y, xs);
|
||||
}
|
||||
|
||||
export function concat<T>(xss: T[][]): T[] {
|
||||
return ([] as T[]).concat(...xss);
|
||||
}
|
||||
|
||||
export function intersperse<T>(sep: T, xs: T[]): T[] {
|
||||
return concat(xs.map(x => [sep, x])).slice(1);
|
||||
}
|
||||
|
||||
export function erase<T>(x: T, xs: T[]): T[] {
|
||||
return xs.filter(y => x !== y);
|
||||
}
|
||||
|
||||
export function unique<T>(xs: T[]): T[] {
|
||||
return [...new Set(xs)];
|
||||
}
|
||||
|
||||
export function sum(xs: number[]): number {
|
||||
return xs.reduce((a, b) => a + b, 0);
|
||||
}
|
3
src/prelude/math.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function gcd(a: number, b: number): number {
|
||||
return b === 0 ? a : gcd(b, a % b);
|
||||
}
|
3
src/prelude/string.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||
}
|