Compare commits
	
		
			160 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3ffd6ff5a2 | ||
|   | b05feb5bf7 | ||
|   | fa171f237d | ||
|   | f2ccb684eb | ||
|   | ffea6522ac | ||
|   | 3d40a7df00 | ||
|   | 638c41476b | ||
|   | c6d3088374 | ||
|   | 0f93be9dd4 | ||
|   | f59982c9c5 | ||
|   | dff67a5e54 | ||
|   | 6adcc3b2ed | ||
|   | 877ed3663c | ||
|   | 6000a82917 | ||
|   | 6805f9b3e0 | ||
|   | 1366c785f9 | ||
|   | 70540b4500 | ||
|   | 0967f23b6e | ||
|   | 1f7d66169c | ||
|   | af501f5eeb | ||
|   | 60be60c923 | ||
|   | 48746101e0 | ||
|   | af9c5c6ab7 | ||
|   | 602284d38c | ||
|   | 26898142c2 | ||
|   | b0a8d7abe9 | ||
|   | dc2b266b75 | ||
|   | 07bbd9506a | ||
|   | 14bb218287 | ||
|   | 29f238c929 | ||
|   | a39a1d4fa5 | ||
|   | 15117c63f5 | ||
|   | 507ffb6fc6 | ||
|   | 6b2e0164cf | ||
|   | 02e06eb1de | ||
|   | 1b50f78733 | ||
|   | ead629407c | ||
|   | 0abbc9e7dd | ||
|   | 37681e859e | ||
|   | caabdc68f3 | ||
|   | 9e97eaf24d | ||
|   | 4cd06a789b | ||
|   | a3ffd968de | ||
|   | 0cf40563aa | ||
|   | 3e7e7f864b | ||
|   | 6ae415e36a | ||
|   | 6cefa3ae26 | ||
|   | 70de3af3ea | ||
|   | 66ed814527 | ||
|   | e12cc3b7a8 | ||
|   | 93ea19d7ad | ||
|   | 79d592b431 | ||
|   | c9c3a0be82 | ||
|   | f04be199dd | ||
|   | f36cb1cc66 | ||
|   | a5597e3df9 | ||
|   | 7f4c28053e | ||
|   | ea24043b22 | ||
|   | 44ef60c8a2 | ||
|   | bd68ff2cf3 | ||
|   | 0e8a592b26 | ||
|   | d3b51bf94a | ||
|   | cc137ee1cc | ||
|   | c088482cef | ||
|   | 70e3febe0a | ||
|   | f500cce293 | ||
|   | c6b836b7be | ||
|   | 15485da1bb | ||
|   | 7195f55a44 | ||
|   | 176f8803eb | ||
|   | 5a3a925a3c | ||
|   | 29bfb9d19b | ||
|   | 86b0dfdd33 | ||
|   | ab04f2fce0 | ||
|   | be9f836b21 | ||
|   | 818bc96aab | ||
|   | 14d12c21f2 | ||
|   | aa5250a37c | ||
|   | 2053a041e5 | ||
|   | 0534a0a41e | ||
|   | d2f9a99beb | ||
|   | 9625047dc3 | ||
|   | d6b18ce536 | ||
|   | df00af1dfa | ||
|   | 3570ec0430 | ||
|   | a111b014f8 | ||
|   | 50eebe834a | ||
|   | f965e9f218 | ||
|   | 0ac5fdab49 | ||
|   | 39099909bf | ||
|   | 999ce8e366 | ||
|   | 8678e30cc8 | ||
|   | 8a59e9d9c8 | ||
|   | dddace9d6a | ||
|   | 388cb7db3a | ||
|   | 46b74b3e1c | ||
|   | d53e80c88a | ||
|   | d8a8f36676 | ||
|   | dafdbbf552 | ||
|   | 52bc52293b | ||
|   | 0733aefb64 | ||
|   | aac6dec5da | ||
|   | d44c59ea3e | ||
|   | 9b3c3881c4 | ||
|   | cdd722dca0 | ||
|   | 9ad7a80496 | ||
|   | b85597b15d | ||
|   | ebb98d975b | ||
|   | c1b320710b | ||
|   | 1201794bef | ||
|   | dc58c9bd2f | ||
|   | 9787da7240 | ||
|   | b0f989dbac | ||
|   | a0ec6b8ea7 | ||
|   | fac6868305 | ||
|   | ed8fa59639 | ||
|   | e8edda01a9 | ||
|   | 380a369eca | ||
|   | 781fffee42 | ||
|   | 69b5de3346 | ||
|   | 0d8c83f27c | ||
|   | 8ca58de2ba | ||
|   | d8cd24fab0 | ||
|   | f918081168 | ||
|   | f88fb9bc1d | ||
|   | 062fbd7d27 | ||
|   | 6b6af008d0 | ||
|   | 4d35def548 | ||
|   | b369d6bd5c | ||
|   | 63dfe2726c | ||
|   | 1002d29cc2 | ||
|   | 868240666a | ||
|   | 02a88fdc9c | ||
|   | bc4adf7107 | ||
|   | bd67785802 | ||
|   | 68c90e8ebe | ||
|   | 64519a9fd4 | ||
|   | d21da0211c | ||
|   | 2e919b788f | ||
|   | 2d2056f2bd | ||
|   | 334dabc1de | ||
|   | dfa2c951d6 | ||
|   | e28d1c7569 | ||
|   | 9ce0f96de3 | ||
|   | a408b19bbe | ||
|   | f9a17b8021 | ||
|   | 5eeb200913 | ||
|   | 643a0e6b13 | ||
|   | e7e5f76e9e | ||
|   | 247acd81a9 | ||
|   | a2457a6ac4 | ||
|   | af7a320493 | ||
|   | 4dd8b7e85d | ||
|   | 3a4392af40 | ||
|   | 44f70f0009 | ||
|   | 238c4cf181 | ||
|   | 9171c49d85 | ||
|   | 81ee670dc2 | ||
|   | faf215685b | ||
|   | 83e9711274 | 
							
								
								
									
										37
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								README.md
									
									
									
									
									
								
							| @@ -20,9 +20,10 @@ ultimately sophisticated new type of mini-blog based SNS. | ||||
| ---------------------------------------------------------------- | ||||
| * Reactions | ||||
| * User lists | ||||
| * Customizable column view (known as MisskeyDeck) | ||||
| * Private messages | ||||
| * Mute | ||||
| * Real time contents | ||||
| * Streaming | ||||
| * ActivityPub compatible | ||||
|  | ||||
| and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz). | ||||
| @@ -45,18 +46,9 @@ If you want to... | ||||
| [![Backers][backers-image]][support-url] | ||||
| [![Sponsors][sponsors-image]][support-url] | ||||
|  | ||||
| :mortar_board: Notable contributors | ||||
| ---------------------------------------------------------------- | ||||
| | ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![tamaina][tamaina-icon] | ![rinsuki][rinsuki-icon] | | ||||
| |:-:|:-:|:-:|:-:|:-:|:-:| | ||||
| | [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [tamaina][tamaina-link] | [rinsuki][rinsuki-link] | | ||||
|  | ||||
| [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors) | ||||
|  | ||||
| ### :earth_americas: Translators | ||||
| | ![][mirro-san-icon] | ![][Conan-kun-icon] | ![][m4sk1n-icon] | | ||||
| |:-:|:-:|:-:| | ||||
| | [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French | [Marcin Mikołajczak][m4sk1n-link]<br>Polish | | ||||
| | ![][ooo-icon] | | ||||
| |:-:| | ||||
| | [ooo][ooo-link] | | ||||
|  | ||||
| :four_leaf_clover: Copyright | ||||
| ---------------------------------------------------------------- | ||||
| @@ -84,23 +76,8 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). | ||||
| [sponsors-image]: https://opencollective.com/misskey/sponsors.svg | ||||
| [support-url]: https://opencollective.com/misskey#support | ||||
|  | ||||
| <!-- Contributors Info --> | ||||
| [syuilo-link]:      https://syuilo.com | ||||
| [syuilo-icon]:      https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 | ||||
| [ayamorisawa-link]: https://github.com/ayamorisawa | ||||
| [ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70 | ||||
| [otofune-link]:     https://github.com/otofune | ||||
| [otofune-icon]:     https://avatars0.githubusercontent.com/u/15062473?v=3&s=70 | ||||
| [akihikodaki-link]: https://github.com/akihikodaki | ||||
| [akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4 | ||||
| [rinsuki-link]:     https://github.com/rinsuki | ||||
| [rinsuki-icon]:     https://avatars0.githubusercontent.com/u/6533808?s=70&v=4 | ||||
| [tamaina-link]:     https://github.com/tamaina | ||||
| [tamaina-icon]:     https://avatars1.githubusercontent.com/u/7973572?s=70&v=4 | ||||
|  | ||||
| [mirro-san-link]:   https://github.com/mirro-san | ||||
| [mirro-san-icon]:   https://avatars1.githubusercontent.com/u/17948612?s=70&v=4 | ||||
| [Conan-kun-link]:   https://github.com/Conan-kun | ||||
| [Conan-kun-icon]:   https://avatars3.githubusercontent.com/u/30003708?s=70&v=4 | ||||
| [m4sk1n-link]:      https://github.com/m4sk1n | ||||
| [m4sk1n-icon]:      https://avatars3.githubusercontent.com/u/21127288?s=70&v=4 | ||||
| [ooo-link]:   https://www.patreon.com/user/creators?u=11601413 | ||||
| [ooo-icon]:   https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D | ||||
|   | ||||
| @@ -3,16 +3,21 @@ const User = require('../built/models/user').default; | ||||
|  | ||||
| const args = process.argv.slice(2); | ||||
|  | ||||
| const userId = new mongo.ObjectID(args[0]); | ||||
| const user = args[0]; | ||||
|  | ||||
| console.log(`Suspending ${userId}...`); | ||||
| const q = user.startsWith('@') ? { | ||||
| 	username: user.split('@')[1], | ||||
| 	host: user.split('@')[2] || null | ||||
| } : { _id: new mongo.ObjectID(user) }; | ||||
|  | ||||
| User.update({ _id: userId }, { | ||||
| console.log(`Suspending ${user}...`); | ||||
|  | ||||
| User.update(q, { | ||||
| 	$set: { | ||||
| 		isSuspended: true | ||||
| 	} | ||||
| }).then(() => { | ||||
| 	console.log(`Suspended ${userId}`); | ||||
| 	console.log(`Suspended ${user}`); | ||||
| }, e => { | ||||
| 	console.error(e); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										101
									
								
								locales/de.yml
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								locales/de.yml
									
									
									
									
									
								
							| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "Spenden" | ||||
|     nav: "Navigation" | ||||
|     tips: "Tipps" | ||||
|   deck: | ||||
|     widgets: "Widget hinzufügen:" | ||||
|     home: "Startseite" | ||||
|     local: "Lokal" | ||||
|     global: "Global" | ||||
|     notifications: "Mitteilungen" | ||||
|     list: "Listen" | ||||
|     swap-left: "Nach links" | ||||
|     swap-right: "Nach rechts" | ||||
|     swap-up: "Nach oben" | ||||
|     swap-down: "Nach unten" | ||||
|     remove: "Spalte löschen" | ||||
|     add-column: "Eine Spalte hinzufügen" | ||||
|     rename: "Umbenennen" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "Verbindung zum Server ist fehlgeschlagen" | ||||
|   description: "Es gibt entweder ein Problem mit deiner Internetverbindung, der Server ist nicht erreichbar oder wird gerade gewartet. Bitte versuche es später noch einmal." | ||||
| @@ -187,7 +203,7 @@ common/views/components/uploader.vue: | ||||
| common/views/components/visibility-chooser.vue: | ||||
|   public: "Öffentlich" | ||||
|   home: "Home" | ||||
|   home-desc: "ホームタイムラインにのみ公開" | ||||
|   home-desc: "Nur auf die Startseite posten" | ||||
|   followers: "Folgende" | ||||
|   followers-desc: "Nur für diejenigen sichtbar, die dir folgen" | ||||
|   specified: "Direkt" | ||||
| @@ -248,7 +264,7 @@ desktop/views/components/drive.file.vue: | ||||
|     rename: "Umbenennen" | ||||
|     copy-url: "URL kopieren" | ||||
|     download: "Download" | ||||
|     else-files: "その他..." | ||||
|     else-files: "Anderes…" | ||||
|     set-as-avatar: "Als Avatar festlegen" | ||||
|     set-as-banner: "Setze als Banner" | ||||
|     open-in-app: "In der App öffnen" | ||||
| @@ -290,10 +306,10 @@ desktop/views/components/drive.vue: | ||||
|     upload: "Eine Datei hochladen" | ||||
|     url-upload: "Von einer URL hochladen" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   following: "Folge ich" | ||||
|   follow: "Folgen" | ||||
|   request-pending: "Ausstehend" | ||||
|   follow-request: "Follower-Anfragen" | ||||
| desktop/views/components/followers-window.vue: | ||||
|   followers: "{} のフォロワー" | ||||
| desktop/views/components/followers.vue: | ||||
| @@ -301,18 +317,18 @@ desktop/views/components/followers.vue: | ||||
| desktop/views/components/following-window.vue: | ||||
|   following: "{} のフォロー" | ||||
| desktop/views/components/following.vue: | ||||
|   empty: "フォロー中のユーザーはいないようです。" | ||||
|   empty: "Du folgst niemanden" | ||||
| desktop/views/components/friends-maker.vue: | ||||
|   title: "気になるユーザーをフォロー:" | ||||
|   empty: "おすすめのユーザーは見つかりませんでした。" | ||||
|   fetching: "読み込んでいます" | ||||
|   refresh: "もっと見る" | ||||
|   close: "閉じる" | ||||
|   title: "Wem folgen?" | ||||
|   empty: "Der ausgewählte Benutzer konnte nicht gefunden werden." | ||||
|   fetching: "Lade…" | ||||
|   refresh: "Mehr" | ||||
|   close: "Schließen" | ||||
| desktop/views/components/game-window.vue: | ||||
|   game: "オセロ" | ||||
| desktop/views/components/home.vue: | ||||
|   done: "完了" | ||||
|   add-widget: "ウィジェットを追加:" | ||||
|   done: "Verbunden" | ||||
|   add-widget: "Widget hinzufügen:" | ||||
|   add: "Hinzufügen" | ||||
| desktop/views/input-dialog.vue: | ||||
|   cancel: "Abbrechen" | ||||
| @@ -323,9 +339,9 @@ desktop/views/components/messaging-window.vue: | ||||
|   title: "Nachrichten" | ||||
| desktop/views/components/note-detail.vue: | ||||
|   more: "Lade weitere Konversationen" | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   reposted-by: "{}がRenote" | ||||
|   private: "Dieser Post ist privat" | ||||
|   deleted: "Dieser Beitrag wurde entfernt" | ||||
|   reposted-by: "Repostet von {}" | ||||
|   location: "Ort" | ||||
|   renote: "Anmerkung" | ||||
|   add-reaction: "Reaktion hinzufügen" | ||||
| @@ -335,8 +351,8 @@ desktop/views/components/notes.note.vue: | ||||
|   renote: "Anmerken" | ||||
|   add-reaction: "Eine Reaktion hinzufügen" | ||||
|   detail: "Zeige Details" | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   private: "Dieser Beitrag ist eine privat" | ||||
|   deleted: "Dieser Beitrag wurde entfernt" | ||||
| desktop/views/components/notes.vue: | ||||
|   error: "Laden fehlgeschlagen." | ||||
|   retry: "Erneut versuchen" | ||||
| @@ -377,34 +393,34 @@ desktop/views/components/renote-form.vue: | ||||
|   success: "Weitergesagt!" | ||||
|   failure: "Weitersagen fehlgeschlagen" | ||||
| desktop/views/components/renote-form-window.vue: | ||||
|   title: "この投稿をRenoteしますか?" | ||||
|   title: "Bist du dir sicher, dass du das reposten willst?" | ||||
| desktop/views/components/settings-window.vue: | ||||
|   settings: "設定" | ||||
|   settings: "Experimentelles" | ||||
| desktop/views/components/settings.vue: | ||||
|   profile: "プロフィール" | ||||
|   notification: "通知" | ||||
|   apps: "アプリ" | ||||
|   mute: "ミュート" | ||||
|   drive: "ドライブ" | ||||
|   notification: "Mitteilungen" | ||||
|   apps: "In App öffnen" | ||||
|   mute: "Stummschalten" | ||||
|   drive: "Dateien vom Drive anfügen" | ||||
|   security: "セキュリティ" | ||||
|   signin: "サインイン履歴" | ||||
|   password: "パスワード" | ||||
|   2fa: "二段階認証" | ||||
|   other: "その他" | ||||
|   license: "ライセンス" | ||||
|   behaviour: "動作" | ||||
|   behaviour: "Verhalten" | ||||
|   fetch-on-scroll: "スクロールで自動読み込み" | ||||
|   fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。" | ||||
|   auto-popout: "ウィンドウの自動ポップアウト" | ||||
|   auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" | ||||
|   advanced: "詳細設定" | ||||
|   api-via-stream: "ストリームを経由したAPIリクエスト" | ||||
|   advanced: "Erweiterte Einstellungen" | ||||
|   api-via-stream: "API-Anfrage via stream" | ||||
|   api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。" | ||||
|   display: "デザインと表示" | ||||
|   customize: "ホームをカスタマイズ" | ||||
|   dark-mode: "ダークモード" | ||||
|   circle-icons: "円形のアイコンを使用" | ||||
|   gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" | ||||
|   display: "Erscheinungsbild und Anzeige" | ||||
|   customize: "Startseite anpassen" | ||||
|   dark-mode: "Nacht Modus" | ||||
|   circle-icons: "Kreisförmige Icons" | ||||
|   gradient-window-header: "Übergang in Fensterköpfen" | ||||
|   post-form-on-timeline: "タイムライン上部に投稿フォームを表示する" | ||||
|   show-reply-target: "リプライ先を表示する" | ||||
|   show-my-renotes: "自分の行ったRenoteをタイムラインに表示する" | ||||
| @@ -416,12 +432,12 @@ desktop/views/components/settings.vue: | ||||
|   enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。" | ||||
|   volume: "ボリューム" | ||||
|   test: "テスト" | ||||
|   mobile: "モバイル" | ||||
|   disable-via-mobile: "「モバイルからの投稿」フラグを付けない" | ||||
|   language: "言語" | ||||
|   pick-language: "言語を選択" | ||||
|   recommended: "推奨" | ||||
|   auto: "自動" | ||||
|   mobile: "Mobil" | ||||
|   disable-via-mobile: "Diesen Beitrag nicht mit 'vom Handy' absenden" | ||||
|   language: "Sprache" | ||||
|   pick-language: "Sprache auswählen" | ||||
|   recommended: "Empfohlen" | ||||
|   auto: "Automatisch" | ||||
|   specify-language: "言語を指定" | ||||
|   language-desc: "変更はページの再度読み込み後に反映されます。" | ||||
|   cache: "キャッシュ" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "Verdunkeln" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "Home" | ||||
|   deck: "デッキ" | ||||
|   messaging: "Nachrichten" | ||||
|   game: "Spielen" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -546,7 +563,7 @@ desktop/views/components/users-list.vue: | ||||
|   all: "すべて" | ||||
|   iknow: "知り合い" | ||||
|   load-more: "もっと" | ||||
|   fetching: "読み込んでいます" | ||||
|   fetching: "Lade…" | ||||
| desktop/views/components/users-list-item.vue: | ||||
|   followed: "フォローされています" | ||||
| desktop/views/components/window.vue: | ||||
| @@ -661,14 +678,14 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| mobile/views/components/friends-maker.vue: | ||||
|   title: "気になるユーザーをフォロー" | ||||
|   empty: "おすすめのユーザーは見つかりませんでした。" | ||||
|   fetching: "読み込んでいます" | ||||
|   fetching: "Lade…" | ||||
|   refresh: "もっと見る" | ||||
|   close: "閉じる" | ||||
| mobile/views/components/note.vue: | ||||
|   | ||||
| @@ -69,9 +69,25 @@ common: | ||||
|     donation: "Donation" | ||||
|     nav: "Navigation" | ||||
|     tips: "Tips" | ||||
|   deck: | ||||
|     widgets: "Widgets" | ||||
|     home: "Home" | ||||
|     local: "Local" | ||||
|     global: "Global" | ||||
|     notifications: "Notifications" | ||||
|     list: "List" | ||||
|     swap-left: "Move left" | ||||
|     swap-right: "Move right" | ||||
|     swap-up: "Move upward" | ||||
|     swap-down: "Move downward" | ||||
|     remove: "Remove" | ||||
|     add-column: "Add a column" | ||||
|     rename: "Rename" | ||||
|     stack-left: "Stack to left" | ||||
|     pop-right: "Pop to right" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "Unable to connect to the server" | ||||
|   description: "There is a problem either with your internet connection, or the server may be down or under maintenance. Please {try again} later." | ||||
|   description: "There is a problem either with your Internet connection, or the server may be down or under maintenance. Please {try again} later." | ||||
|   thanks: "Thank you for using Misskey." | ||||
|   troubleshoot: "Troubleshoot" | ||||
| common/views/components/connect-failed.troubleshooter.vue: | ||||
| @@ -79,7 +95,7 @@ common/views/components/connect-failed.troubleshooter.vue: | ||||
|   network: "Network connection" | ||||
|   checking-network: "Checking network connection" | ||||
|   internet: "Internet connection" | ||||
|   checking-internet: "Checking internet connection" | ||||
|   checking-internet: "Checking Internet connection" | ||||
|   server: "Server connection" | ||||
|   checking-server: "Checking server connection" | ||||
|   finding: "Searching for issues" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "Upload a file" | ||||
|     url-upload: "Upload from a URL" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "Following" | ||||
|   following: "Following" | ||||
|   follow: "Follow" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "Follow request" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "Fall in dark" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "Home" | ||||
|   deck: "Deck" | ||||
|   messaging: "Messages" | ||||
|   game: "Play Othello" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "Hash (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "Following" | ||||
|   following: "Following" | ||||
|   follow: "Follow" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "Follow request" | ||||
|   | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "闇に飲まれる" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   | ||||
							
								
								
									
										561
									
								
								locales/fr.yml
									
									
									
									
									
								
							
							
						
						
									
										561
									
								
								locales/fr.yml
									
									
									
									
									
								
							| @@ -3,8 +3,8 @@ meta: | ||||
|   lang: "Français" | ||||
|   divider: "" | ||||
| common: | ||||
|   misskey: "A planet of fediverse" | ||||
|   about-title: "A ⭐ of fediverse." | ||||
|   misskey: "Une planète du fédiverse" | ||||
|   about-title: "Une ⭐ du fédiverse." | ||||
|   about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。" | ||||
|   time: | ||||
|     unknown: "inconnu" | ||||
| @@ -36,39 +36,55 @@ common: | ||||
|     confused: "Confus" | ||||
|     pudding: "Pudding" | ||||
|   note-placeholders: | ||||
|     a: "今どうしてる?" | ||||
|     b: "何かありましたか?" | ||||
|     c: "何をお考えですか?" | ||||
|     d: "言いたいことは?" | ||||
|     e: "ここに書いてください" | ||||
|     f: "あなたが書くのを待っています..." | ||||
|     a: "Que faîtes vous à cet instant ?" | ||||
|     b: "Quoi de neuf ?" | ||||
|     c: "Qu'avez-vous en tête ?" | ||||
|     d: "Voulez-vous exprimer quelque chose ?" | ||||
|     e: "Écrivez ici" | ||||
|     f: "En attente de vos écrits" | ||||
|   delete: "Supprimer" | ||||
|   loading: "Chargement" | ||||
|   ok: "OK" | ||||
|   update-available: "Une nouvelle version de Misskey est disponible({newer}, version actuelle: {current}). Recharger la page pour appliquer la mise à jour." | ||||
|   my-token-regenerated: "Votre token vient d'être généré, vous allez maintenant être déconnecté." | ||||
|   widgets: | ||||
|     analog-clock: "アナログ時計" | ||||
|     profile: "プロフィール" | ||||
|     calendar: "カレンダー" | ||||
|     analog-clock: "Horloge analogique" | ||||
|     profile: "Profil" | ||||
|     calendar: "Calendrier" | ||||
|     timemachine: "カレンダー(タイムマシン)" | ||||
|     activity: "アクティビティ" | ||||
|     rss: "RSSリーダー" | ||||
|     memo: "メモ" | ||||
|     trends: "トレンド" | ||||
|     photo-stream: "フォトストリーム" | ||||
|     slideshow: "スライドショー" | ||||
|     version: "バージョン" | ||||
|     broadcast: "ブロードキャスト" | ||||
|     notifications: "通知" | ||||
|     users: "おすすめユーザー" | ||||
|     polls: "投票" | ||||
|     activity: "Activité" | ||||
|     rss: "Lecteur de flux RSS" | ||||
|     memo: "Note" | ||||
|     trends: "Tendances" | ||||
|     photo-stream: "Flux de photos" | ||||
|     slideshow: "Diaporama" | ||||
|     version: "Version" | ||||
|     broadcast: "Diffusion" | ||||
|     notifications: "Notifications" | ||||
|     users: "Utilisateurs" | ||||
|     polls: "Sondages" | ||||
|     post-form: "投稿フォーム" | ||||
|     messaging: "メッセージ" | ||||
|     server: "サーバー情報" | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|     messaging: "Messagerie" | ||||
|     server: "Info sur le serveur" | ||||
|     donation: "Dons" | ||||
|     nav: "Navigation" | ||||
|     tips: "Conseils" | ||||
|   deck: | ||||
|     widgets: "Widgets" | ||||
|     home: "Accueil" | ||||
|     local: "Local" | ||||
|     global: "Global" | ||||
|     notifications: "Notifications" | ||||
|     list: "Liste" | ||||
|     swap-left: "Déplacer à gauche" | ||||
|     swap-right: "Déplacer à droite" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "Supprimer" | ||||
|     add-column: "Ajouter une colonne" | ||||
|     rename: "Renommer" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "Impossible de se connecter au server." | ||||
|   description: "Il y a soit un problème avec votre connexion internet, soit le serveur est hors-ligne ou en maintenance. Veuillez {ressayer} plus tard." | ||||
| @@ -91,8 +107,8 @@ common/views/components/connect-failed.troubleshooter.vue: | ||||
|   no-server-desc: "Votre connexion est OK, mais il a été impossible de vous connecter au serveur de Misskey. Il y a des chances que le serveur soit hors-ligne ou en maintenance, veuillez ressayer plus tard." | ||||
|   success: "Connexion au serveur de Misskey reussie!" | ||||
|   success-desc: "La connexion au serveur a été reussie. Veuillez recharger la page." | ||||
|   flush: "キャッシュの削除" | ||||
|   set-version: "バージョン指定" | ||||
|   flush: "Vider le cache" | ||||
|   set-version: "Choisissez une version" | ||||
| common/views/components/messaging.vue: | ||||
|   search-user: "Trouver un utilisateur" | ||||
|   you: "Vous" | ||||
| @@ -119,12 +135,12 @@ common/views/components/nav.vue: | ||||
|   donors: "Donateurs" | ||||
|   repository: "Repo" | ||||
|   develop: "Développeurs" | ||||
|   feedback: "フィードバック" | ||||
|   feedback: "Remarques" | ||||
| common/views/components/note-menu.vue: | ||||
|   favorite: "Favorite this note" | ||||
|   pin: "Épingler sur votre profile" | ||||
|   delete: "削除" | ||||
|   delete-confirm: "この投稿を削除しますか?" | ||||
|   delete: "Supprimer" | ||||
|   delete-confirm: "Supprimer cette publication ?" | ||||
|   remote: "投稿元で見る" | ||||
| common/views/components/poll.vue: | ||||
|   vote-to: "Voter pour '{}'" | ||||
| @@ -185,14 +201,14 @@ common/views/components/twitter-setting.vue: | ||||
| common/views/components/uploader.vue: | ||||
|   waiting: "En attente" | ||||
| common/views/components/visibility-chooser.vue: | ||||
|   public: "公開" | ||||
|   home: "ホーム" | ||||
|   home-desc: "ホームタイムラインにのみ公開" | ||||
|   followers: "フォロワー" | ||||
|   followers-desc: "自分のフォロワーにのみ公開" | ||||
|   specified: "ダイレクト" | ||||
|   specified-desc: "指定したユーザーにのみ公開" | ||||
|   private: "非公開" | ||||
|   public: "Public" | ||||
|   home: "Accueil" | ||||
|   home-desc: "Publier sur le fil d'Accueil uniquement" | ||||
|   followers: "Abonnés" | ||||
|   followers-desc: "Publier à vos abonnés uniquement" | ||||
|   specified: "Direct" | ||||
|   specified-desc: "Publier aux utilisateurs mentionnés" | ||||
|   private: "Privé" | ||||
| common/views/widgets/broadcast.vue: | ||||
|   fetching: "Récuperation" | ||||
|   no-broadcasts: "No broadcasts" | ||||
| @@ -208,14 +224,14 @@ common/views/widgets/server.vue: | ||||
|   title: "Info sur le serveur" | ||||
|   toggle: "Afficher les vues" | ||||
| common/views/widgets/memo.vue: | ||||
|   title: "メモ" | ||||
|   memo: "ここに書いて!" | ||||
|   save: "保存" | ||||
|   title: "Note" | ||||
|   memo: "Écrivez ici !" | ||||
|   save: "Enregistrer" | ||||
| desktop/views/components/activity.chart.vue: | ||||
|   total: "Black ... Total" | ||||
|   notes: "Blue ... Notes" | ||||
|   replies: "Red ... Replies" | ||||
|   renotes: "Green ... Renotes" | ||||
|   notes: "Bleu ... Notes" | ||||
|   replies: "Rouge ... Réponses" | ||||
|   renotes: "Vert ... Partages" | ||||
| desktop/views/components/activity.vue: | ||||
|   title: "Activitié" | ||||
|   toggle: "Afficher les vues" | ||||
| @@ -225,19 +241,19 @@ desktop/views/components/calendar.vue: | ||||
|   next: "Mois prochain" | ||||
|   go: "Cliquer pour naviguer" | ||||
| desktop/views/components/choose-file-from-drive-window.vue: | ||||
|   choose-file: "ファイル選択中" | ||||
|   upload: "PCからドライブにファイルをアップロード" | ||||
|   cancel: "キャンセル" | ||||
|   ok: "決定" | ||||
|   choose-prompt: "ファイルを選択" | ||||
|   choose-file: "Sélection de fichiers" | ||||
|   upload: "Téléverser des fichiers à partir de votre PC" | ||||
|   cancel: "Annuler" | ||||
|   ok: "OK" | ||||
|   choose-prompt: "Choisir un fichier" | ||||
| desktop/views/components/choose-folder-from-drive-window.vue: | ||||
|   cancel: "キャンセル" | ||||
|   ok: "決定" | ||||
|   choose-prompt: "フォルダを選択" | ||||
|   cancel: "Annuler" | ||||
|   ok: "OK" | ||||
|   choose-prompt: "Choisir un dossier" | ||||
| desktop/views/components/crop-window.vue: | ||||
|   skip: "クロップをスキップ" | ||||
|   cancel: "キャンセル" | ||||
|   ok: "決定" | ||||
|   skip: "Ignorer la découpe" | ||||
|   cancel: "Annuler" | ||||
|   ok: "OK" | ||||
| desktop/views/components/drive-window.vue: | ||||
|   used: "utilisé" | ||||
|   drive: "Drive" | ||||
| @@ -290,65 +306,65 @@ desktop/views/components/drive.vue: | ||||
|     upload: "Uploader un fichier" | ||||
|     url-upload: "Uploader d'un URL" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   following: "Abonnements" | ||||
|   follow: "Suivre" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   follow-request: "Demande d'abonnement" | ||||
| desktop/views/components/followers-window.vue: | ||||
|   followers: "{} のフォロワー" | ||||
|   followers: "{} abonnés" | ||||
| desktop/views/components/followers.vue: | ||||
|   empty: "フォロワーはいないようです。" | ||||
|   empty: "Il semble que vous n'avez pas encore d'abonnés." | ||||
| desktop/views/components/following-window.vue: | ||||
|   following: "{} のフォロー" | ||||
|   following: "Suit {}" | ||||
| desktop/views/components/following.vue: | ||||
|   empty: "フォロー中のユーザーはいないようです。" | ||||
|   empty: "Vous ne suivez aucun compte." | ||||
| desktop/views/components/friends-maker.vue: | ||||
|   title: "気になるユーザーをフォロー:" | ||||
|   title: "Utilisateurs recommandés :" | ||||
|   empty: "おすすめのユーザーは見つかりませんでした。" | ||||
|   fetching: "読み込んでいます" | ||||
|   refresh: "もっと見る" | ||||
|   close: "閉じる" | ||||
|   fetching: "Chargement" | ||||
|   refresh: "Plus" | ||||
|   close: "Fermer" | ||||
| desktop/views/components/game-window.vue: | ||||
|   game: "オセロ" | ||||
|   game: "Othello" | ||||
| desktop/views/components/home.vue: | ||||
|   done: "完了" | ||||
|   add-widget: "ウィジェットを追加:" | ||||
|   add: "追加" | ||||
|   done: "Envoyer" | ||||
|   add-widget: "Ajouter un widget" | ||||
|   add: "Ajouter" | ||||
| desktop/views/input-dialog.vue: | ||||
|   cancel: "キャンセル" | ||||
|   ok: "決定" | ||||
|   cancel: "Annuler" | ||||
|   ok: "OK" | ||||
| desktop/views/components/messaging-room-window.vue: | ||||
|   title: "メッセージ:" | ||||
|   title: "Messages :" | ||||
| desktop/views/components/messaging-window.vue: | ||||
|   title: "Messagerie" | ||||
| desktop/views/components/note-detail.vue: | ||||
|   more: "会話をもっと読み込む" | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   reposted-by: "{}がRenote" | ||||
|   location: "位置情報" | ||||
|   renote: "Renote" | ||||
|   add-reaction: "リアクション" | ||||
|   more: "Charger davantage de conversations" | ||||
|   private: "cette publication est privée" | ||||
|   deleted: "cette publication a été supprimée" | ||||
|   reposted-by: "Republié par {}" | ||||
|   location: "Géolocalisation" | ||||
|   renote: "Republier" | ||||
|   add-reaction: "Ajouter votre reaction" | ||||
| desktop/views/components/notes.note.vue: | ||||
|   reposted-by: "Reposté par {}" | ||||
|   reply: "Répondre" | ||||
|   renote: "Renote" | ||||
|   renote: "Republier" | ||||
|   add-reaction: "Ajouter votre reaction" | ||||
|   detail: "Afficher les détails" | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   private: "cette publication est privée" | ||||
|   deleted: "cette publication a été supprimée" | ||||
| desktop/views/components/notes.vue: | ||||
|   error: "読み込みに失敗しました。" | ||||
|   retry: "リトライ" | ||||
|   error: "Échec du chargement." | ||||
|   retry: "Réessayer" | ||||
| desktop/views/components/notifications.vue: | ||||
|   more: "Plus" | ||||
|   empty: "Pas de notifications" | ||||
| desktop/views/components/post-form.vue: | ||||
|   reply-placeholder: "Répondre à cette note" | ||||
|   quote-placeholder: "Citer cette note" | ||||
|   submit: "投稿" | ||||
|   submit: "Poster" | ||||
|   reply: "Répondre" | ||||
|   renote: "Renote" | ||||
|   renote: "Republier" | ||||
|   posted: "Posté!" | ||||
|   replied: "Répondu!" | ||||
|   reposted: "Reposté!" | ||||
| @@ -368,18 +384,18 @@ desktop/views/components/post-form-window.vue: | ||||
|   attaches: "{} media joint(s)" | ||||
|   uploading-media: "Upload du media {}" | ||||
| desktop/views/components/progress-dialog.vue: | ||||
|   waiting: "待機中" | ||||
|   waiting: "En attente" | ||||
| desktop/views/components/renote-form.vue: | ||||
|   quote: "Citer..." | ||||
|   cancel: "Annuler" | ||||
|   renote: "Renote" | ||||
|   renote: "Republier" | ||||
|   reposting: "Repost en cours..." | ||||
|   success: "Reposté!" | ||||
|   failure: "La renote a échoué" | ||||
| desktop/views/components/renote-form-window.vue: | ||||
|   title: "Êtes vous sûr de vouloir renote cette note?" | ||||
| desktop/views/components/settings-window.vue: | ||||
|   settings: "設定" | ||||
|   settings: "Paramètres" | ||||
| desktop/views/components/settings.vue: | ||||
|   profile: "Profil" | ||||
|   notification: "Notification" | ||||
| @@ -392,67 +408,67 @@ desktop/views/components/settings.vue: | ||||
|   2fa: "Vérification en deux étapes" | ||||
|   other: "Autres" | ||||
|   license: "License" | ||||
|   behaviour: "動作" | ||||
|   fetch-on-scroll: "スクロールで自動読み込み" | ||||
|   behaviour: "Comportement" | ||||
|   fetch-on-scroll: "Chargement lors du défilement" | ||||
|   fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。" | ||||
|   auto-popout: "ウィンドウの自動ポップアウト" | ||||
|   auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" | ||||
|   advanced: "詳細設定" | ||||
|   api-via-stream: "ストリームを経由したAPIリクエスト" | ||||
|   advanced: "Paramètres avancés" | ||||
|   api-via-stream: "Requête API via le flux" | ||||
|   api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。" | ||||
|   display: "デザインと表示" | ||||
|   customize: "ホームをカスタマイズ" | ||||
|   dark-mode: "ダークモード" | ||||
|   circle-icons: "円形のアイコンを使用" | ||||
|   display: "Affichage et design" | ||||
|   customize: "Personnaliser l'Accueil" | ||||
|   dark-mode: "Mode nuit" | ||||
|   circle-icons: "Utiliser des icônes circulaires" | ||||
|   gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" | ||||
|   post-form-on-timeline: "タイムライン上部に投稿フォームを表示する" | ||||
|   show-reply-target: "リプライ先を表示する" | ||||
|   show-my-renotes: "自分の行ったRenoteをタイムラインに表示する" | ||||
|   show-my-renotes: "Afficher mes republications dans le fil" | ||||
|   show-renoted-my-notes: "Renoteされた自分の投稿をタイムラインに表示する" | ||||
|   show-maps: "マップの自動展開" | ||||
|   show-maps: "Afficher la carte" | ||||
|   show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。" | ||||
|   sound: "サウンド" | ||||
|   enable-sounds: "サウンドを有効にする" | ||||
|   sound: "Son" | ||||
|   enable-sounds: "Activer le son" | ||||
|   enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。" | ||||
|   volume: "ボリューム" | ||||
|   test: "テスト" | ||||
|   mobile: "モバイル" | ||||
|   volume: "Volume" | ||||
|   test: "Test" | ||||
|   mobile: "Mobile" | ||||
|   disable-via-mobile: "「モバイルからの投稿」フラグを付けない" | ||||
|   language: "言語" | ||||
|   pick-language: "言語を選択" | ||||
|   recommended: "推奨" | ||||
|   auto: "自動" | ||||
|   specify-language: "言語を指定" | ||||
|   language: "Langue" | ||||
|   pick-language: "Sélectionner une langue" | ||||
|   recommended: "Recommandé" | ||||
|   auto: "Automatique" | ||||
|   specify-language: "Spécifier la langue" | ||||
|   language-desc: "変更はページの再度読み込み後に反映されます。" | ||||
|   cache: "キャッシュ" | ||||
|   clean-cache: "クリーンアップ" | ||||
|   cache: "Cache" | ||||
|   clean-cache: "Nettoyage" | ||||
|   cache-warn: "クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。" | ||||
|   cache-cleared: "キャッシュを削除しました" | ||||
|   cache-cleared-desc: "ページを再度読み込みしてください。" | ||||
|   cache-cleared: "Cache nettoyé" | ||||
|   cache-cleared-desc: "Veuillez recharger la page." | ||||
|   auto-watch: "投稿の自動ウォッチ" | ||||
|   auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。" | ||||
|   about: "Misskeyについて" | ||||
|   operator: "このサーバーの運営者" | ||||
|   update: "Misskey Update" | ||||
|   version: "バージョン:" | ||||
|   latest-version: "最新のバージョン:" | ||||
|   update-checking: "アップデートを確認中" | ||||
|   do-update: "アップデートを確認" | ||||
|   update-settings: "詳細設定" | ||||
|   about: "À propose de Misskey" | ||||
|   operator: "L'admin de cette instance" | ||||
|   update: "Mise à jour de Misskey" | ||||
|   version: "Version :" | ||||
|   latest-version: "Dernière version :" | ||||
|   update-checking: "Recherche de mises à jour" | ||||
|   do-update: "Rechercher des mises à jour" | ||||
|   update-settings: "Paramètres avancés" | ||||
|   prevent-update: "アップデートを延期する(非推奨)" | ||||
|   prevent-update-desc: "この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。" | ||||
|   no-updates: "利用可能な更新はありません" | ||||
|   no-updates-desc: "お使いのMisskeyは最新です。" | ||||
|   update-available: "新しいバージョンが利用可能です" | ||||
|   no-updates: "Aucune mise à jour disponible" | ||||
|   no-updates-desc: "Votre Misskey est à jour." | ||||
|   update-available: "Nouvelle version disponible !" | ||||
|   update-available-desc: "ページを再度読み込みすると更新が適用されます。" | ||||
|   advanced-settings: "高度な設定" | ||||
|   debug-mode: "デバッグモードを有効にする" | ||||
|   debug-mode-desc: "この設定はブラウザに記憶されます。" | ||||
|   experimental: "実験的機能を有効にする" | ||||
|   advanced-settings: "Réglages avancés" | ||||
|   debug-mode: "Activer le mode debug" | ||||
|   debug-mode-desc: "Ce paramètre est stocké dans le navigateur." | ||||
|   experimental: "Activer les fonctionnalités expérimentales" | ||||
|   experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。" | ||||
|   tools: "ツール" | ||||
|   task-manager: "タスクマネージャ" | ||||
|   third-parties: "サードパーティ" | ||||
|   tools: "Outils" | ||||
|   task-manager: "Gestionnaire de tâches" | ||||
|   third-parties: "Services tiers" | ||||
| desktop/views/components/settings.2fa.vue: | ||||
|   intro: "Si vous configurez la vérication en deux étapes vous aurez non seulement besoin de votre mot de passe mais aussi un appareil déjà pré-enregistré(tel que votre smartphone) ce qui ameliora grandement la sécurité de votre compte." | ||||
|   detail: "Voir les détails..." | ||||
| @@ -476,10 +492,10 @@ desktop/views/components/settings.api.vue: | ||||
|   caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" | ||||
|   regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。" | ||||
|   regenerate-token: "Regenerer le token" | ||||
|   token: "Token:" | ||||
|   token: "Jeton :" | ||||
|   enter-password: "Veuillez entrer le mot de passe" | ||||
| desktop/views/components/settings.apps.vue: | ||||
|   no-apps: "連携しているアプリケーションはありません" | ||||
|   no-apps: "Aucune application autorisée" | ||||
| desktop/views/components/settings.mute.vue: | ||||
|   no-users: "Aucun utilisateurs mis en sourdine" | ||||
| desktop/views/components/settings.password.vue: | ||||
| @@ -497,32 +513,33 @@ desktop/views/components/settings.profile.vue: | ||||
|   description: "Description" | ||||
|   birthday: "Date de naissance" | ||||
|   save: "Mettre à jour le profil" | ||||
|   is-bot: "このアカウントはBotです" | ||||
|   is-cat: "このアカウントはCatです" | ||||
|   is-bot: "Ce compte est un Bot" | ||||
|   is-cat: "Ce compte est un Chat" | ||||
| desktop/views/components/sub-note-content.vue: | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   media-count: "{}つのメディア" | ||||
|   poll: "投票" | ||||
|   private: "cette publication est privée" | ||||
|   deleted: "cette publication a été supprimée" | ||||
|   media-count: "{} médias attachés" | ||||
|   poll: "Sondages" | ||||
| desktop/views/components/taskmanager.vue: | ||||
|   title: "タスクマネージャ" | ||||
|   title: "Gestionnaire de tâches" | ||||
| desktop/views/components/timeline.vue: | ||||
|   home: "ホーム" | ||||
|   local: "ローカル" | ||||
|   global: "グローバル" | ||||
|   list: "リスト" | ||||
|   home: "Accueil" | ||||
|   local: "Local" | ||||
|   global: "Global" | ||||
|   list: "Listes" | ||||
| desktop/views/components/ui.header.account.vue: | ||||
|   profile: "Votre profil" | ||||
|   drive: "Drive" | ||||
|   favorites: "Favorites" | ||||
|   lists: "リスト" | ||||
|   follow-requests: "フォロー申請" | ||||
|   lists: "Listes" | ||||
|   follow-requests: "Demandes de suivi" | ||||
|   customize: "Modifications" | ||||
|   settings: "Réglages" | ||||
|   signout: "Déconnexion" | ||||
|   dark: "Fall in dark" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "Accueil" | ||||
|   deck: "デッキ" | ||||
|   messaging: "Messages" | ||||
|   game: "Jeux" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -532,40 +549,40 @@ desktop/views/components/ui.header.post.vue: | ||||
| desktop/views/components/ui.header.search.vue: | ||||
|   placeholder: "Chercher" | ||||
| desktop/views/components/received-follow-requests-window.vue: | ||||
|   title: "フォロー申請" | ||||
|   accept: "承認" | ||||
|   reject: "拒否" | ||||
|   title: "Demandes de suivi" | ||||
|   accept: "Approuver" | ||||
|   reject: "Refuser" | ||||
| desktop/views/components/user-lists-window.vue: | ||||
|   title: "リスト" | ||||
|   create-list: "リストを作成" | ||||
|   title: "Listes de l'utilisateur" | ||||
|   create-list: "Créer une liste" | ||||
| desktop/views/components/user-preview.vue: | ||||
|   notes: "投稿" | ||||
|   following: "フォロー" | ||||
|   followers: "フォロワー" | ||||
|   notes: "Publications" | ||||
|   following: "Abonné à" | ||||
|   followers: "Abonnés" | ||||
| desktop/views/components/users-list.vue: | ||||
|   all: "すべて" | ||||
|   iknow: "知り合い" | ||||
|   load-more: "もっと" | ||||
|   fetching: "読み込んでいます" | ||||
|   all: "Tout" | ||||
|   iknow: "Vous connaissez" | ||||
|   load-more: "Afficher plus" | ||||
|   fetching: "Chargement ..." | ||||
| desktop/views/components/users-list-item.vue: | ||||
|   followed: "フォローされています" | ||||
|   followed: "vous suit" | ||||
| desktop/views/components/window.vue: | ||||
|   popout: "ポップアウト" | ||||
|   close: "閉じる" | ||||
|   close: "Fermer" | ||||
| desktop/views/pages/welcome.vue: | ||||
|   about: "詳しく..." | ||||
|   gotit: "わかった" | ||||
|   signin: "ログイン" | ||||
|   signup: "新規登録" | ||||
|   signin-button: "やってる" | ||||
|   signup-button: "やる" | ||||
|   timeline: "タイムライン" | ||||
|   about: "à propos" | ||||
|   gotit: "J'ai compris !" | ||||
|   signin: "Connexion" | ||||
|   signup: "S'enregistrer" | ||||
|   signin-button: "Se connecter" | ||||
|   signup-button: "S'inscrire" | ||||
|   timeline: "Fil d'actualité" | ||||
| desktop/views/pages/drive.vue: | ||||
|   title: "Misskey Drive" | ||||
|   title: "Lecteur de Misskey" | ||||
| desktop/views/pages/favorites.vue: | ||||
|   more: "さらに読み込む" | ||||
|   more: "Plus de résultats" | ||||
| desktop/views/pages/home-customize.vue: | ||||
|   title: "ホームのカスタマイズ" | ||||
|   title: "Personnaliser l'Accueil" | ||||
| desktop/views/pages/note.vue: | ||||
|   prev: "Note précédente" | ||||
|   next: "Note suivante" | ||||
| @@ -575,9 +592,9 @@ desktop/views/pages/selectdrive.vue: | ||||
|   cancel: "Annuler" | ||||
|   upload: "Uploader un ou plusieurs fichier(s) depuis votre PC" | ||||
| desktop/views/pages/user-list.users.vue: | ||||
|   users: "ユーザー" | ||||
|   add-user: "ユーザーを追加" | ||||
|   username: "ユーザー名" | ||||
|   users: "Utilisateurs" | ||||
|   add-user: "Ajouter un utilisateur" | ||||
|   username: "Nom d'utilisateur" | ||||
| desktop/views/pages/user/user.followers-you-know.vue: | ||||
|   title: "Abonnés que vous connaissez" | ||||
|   loading: "Chargement en cours" | ||||
| @@ -605,10 +622,10 @@ desktop/views/pages/user/user.profile.vue: | ||||
|   muted: "Muting" | ||||
|   unmute: "Enlever la sourdine" | ||||
| desktop/views/pages/user/user.timeline.vue: | ||||
|   default: "投稿" | ||||
|   with-replies: "投稿と返信" | ||||
|   with-media: "メディア" | ||||
|   empty: "このユーザーはまだ何も投稿していないようです。" | ||||
|   default: "Publications" | ||||
|   with-replies: "Publications et réponses" | ||||
|   with-media: "Média" | ||||
|   empty: "Cet utilisateur n'a rien posté encore." | ||||
| desktop/views/widgets/messaging.vue: | ||||
|   title: "Messagerie" | ||||
| desktop/views/widgets/notifications.vue: | ||||
| @@ -622,8 +639,8 @@ desktop/views/widgets/post-form.vue: | ||||
|   title: "Post" | ||||
|   note: "Post" | ||||
| desktop/views/widgets/profile.vue: | ||||
|   update-banner: "クリックでバナー編集" | ||||
|   update-avatar: "クリックでアバター編集" | ||||
|   update-banner: "Cliquer pour éditer votre bannière" | ||||
|   update-avatar: "Cliquer pour éditer votre avatar" | ||||
| desktop/views/widgets/trends.vue: | ||||
|   title: "Tendances" | ||||
|   refresh: "Afficher d'autres" | ||||
| @@ -643,13 +660,13 @@ mobile/views/components/drive.vue: | ||||
|   folder-is-empty: "Ce dossier est vide" | ||||
|   prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>" | ||||
|   deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。" | ||||
|   folder-name: "フォルダー名" | ||||
|   folder-name: "Nom du dossier" | ||||
|   root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。" | ||||
|   root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。" | ||||
|   url-prompt: "アップロードしたいファイルのURL" | ||||
|   uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。" | ||||
| mobile/views/components/drive-file-detail.vue: | ||||
|   rename: "名前を変更" | ||||
|   rename: "Renommer" | ||||
| mobile/views/components/drive-file-chooser.vue: | ||||
|   select-file: "Choisissez un fichier" | ||||
| mobile/views/components/drive-folder-chooser.vue: | ||||
| @@ -661,75 +678,75 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "Hash (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "Suivre" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   follow-request: "Demande d'abonnement" | ||||
| mobile/views/components/friends-maker.vue: | ||||
|   title: "気になるユーザーをフォロー" | ||||
|   title: "Abonnez-vous aux utilisateurs" | ||||
|   empty: "おすすめのユーザーは見つかりませんでした。" | ||||
|   fetching: "読み込んでいます" | ||||
|   refresh: "もっと見る" | ||||
|   close: "閉じる" | ||||
|   fetching: "Chargement" | ||||
|   refresh: "Voir plus" | ||||
|   close: "Fermer" | ||||
| mobile/views/components/note.vue: | ||||
|   reposted-by: "Renoté par {}" | ||||
|   more: "もっと見る" | ||||
|   less: "隠す" | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   location: "位置情報" | ||||
|   more: "Voir plus" | ||||
|   less: "Masquer" | ||||
|   private: "cette publication est privée" | ||||
|   deleted: "cette publication a été supprimée" | ||||
|   location: "Géolocalisation" | ||||
| mobile/views/components/note-detail.vue: | ||||
|   reply: "Répondre" | ||||
|   reaction: "Réaction" | ||||
|   reposted-by: "{}がRenote" | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   location: "位置情報" | ||||
|   reposted-by: "Republié par {}" | ||||
|   private: "cette publication est privée" | ||||
|   deleted: "cette publication a été supprimée" | ||||
|   location: "Lieu" | ||||
| mobile/views/components/note-preview.vue: | ||||
|   admin: "admin" | ||||
|   bot: "bot" | ||||
|   cat: "cat" | ||||
|   cat: "chat" | ||||
| mobile/views/components/note-sub.vue: | ||||
|   admin: "admin" | ||||
|   bot: "bot" | ||||
|   cat: "cat" | ||||
|   cat: "chat" | ||||
| mobile/views/components/notes.vue: | ||||
|   failed: "読み込みに失敗しました。" | ||||
|   retry: "リトライ" | ||||
|   failed: "Échec du chargement." | ||||
|   retry: "Réessayer" | ||||
| mobile/views/components/notifications.vue: | ||||
|   more: "Plus" | ||||
|   empty: "Pas de notifications" | ||||
| mobile/views/components/post-form.vue: | ||||
|   add-visible-user: "ユーザーを追加" | ||||
|   add-visible-user: "Ajouter un utilisateur" | ||||
|   submit: "Poster" | ||||
|   reply: "返信" | ||||
|   renote: "Renote" | ||||
|   quote-placeholder: "この投稿を引用... (オプション)" | ||||
|   reply: "Répondre" | ||||
|   renote: "Republier" | ||||
|   quote-placeholder: "Citer ce billet ... (Facultatif)" | ||||
|   reply-placeholder: "Répondre à cette note" | ||||
|   cw-placeholder: "内容への注釈 (オプション)" | ||||
|   location-alert: "お使いの端末は位置情報に対応していません" | ||||
|   error: "エラー" | ||||
|   username-prompt: "ユーザー名を入力してください" | ||||
|   location-alert: "Votre appareil ne prend pas en charge les services de localisation" | ||||
|   error: "Erreur" | ||||
|   username-prompt: "Saisir un nom d'utilisateur" | ||||
| mobile/views/components/sub-note-content.vue: | ||||
|   private: "この投稿は非公開です" | ||||
|   deleted: "この投稿は削除されました" | ||||
|   media-count: "{}つのメディア" | ||||
|   private: "cette publication est privée" | ||||
|   deleted: "cette publication a été supprimée" | ||||
|   media-count: "{} médias attachés" | ||||
|   poll: "Sondage" | ||||
| mobile/views/components/timeline.vue: | ||||
|   empty: "Pas de notes" | ||||
|   load-more: "Afficher plus" | ||||
| mobile/views/components/ui.nav.vue: | ||||
|   timeline: "タイムライン" | ||||
|   timeline: "Fil d'actualité" | ||||
|   notifications: "Notifications" | ||||
|   messaging: "Messages" | ||||
|   follow-requests: "フォロー申請" | ||||
|   follow-requests: "Demandes d'abonnement" | ||||
|   search: "Rechercher" | ||||
|   drive: "Drive" | ||||
|   favorites: "お気に入り" | ||||
|   user-lists: "リスト" | ||||
|   widgets: "ウィジェット" | ||||
|   game: "ゲーム" | ||||
|   darkmode: "ダークモード" | ||||
|   favorites: "Favoris" | ||||
|   user-lists: "Listes" | ||||
|   widgets: "Modules" | ||||
|   game: "Jeux" | ||||
|   darkmode: "Mode nuit" | ||||
|   settings: "Réglages" | ||||
|   about: "À propose de Misskey" | ||||
| mobile/views/components/user-timeline.vue: | ||||
| @@ -741,29 +758,29 @@ mobile/views/components/users-list.vue: | ||||
|   known: "Vous connaissez" | ||||
|   load-more: "Afficher plus" | ||||
| mobile/views/pages/favorites.vue: | ||||
|   title: "お気に入り" | ||||
|   title: "Favoris" | ||||
| mobile/views/pages/user-lists.vue: | ||||
|   title: "リスト" | ||||
|   enter-list-name: "リスト名を入力してください" | ||||
|   title: "Listes" | ||||
|   enter-list-name: "Nom de la liste" | ||||
| mobile/views/pages/drive.vue: | ||||
|   drive: "Drive" | ||||
|   more: "もっと見る" | ||||
|   more: "Afficher plus ..." | ||||
| mobile/views/pages/followers.vue: | ||||
|   followers-of: "Abonnés de {}" | ||||
| mobile/views/pages/following.vue: | ||||
|   following-of: "Abonnements de {}" | ||||
| mobile/views/pages/home.vue: | ||||
|   home: "ホーム" | ||||
|   local: "ローカル" | ||||
|   global: "グローバル" | ||||
|   home: "Accueil" | ||||
|   local: "Local" | ||||
|   global: "Global" | ||||
| mobile/views/pages/messaging.vue: | ||||
|   messaging: "Messagerie" | ||||
| mobile/views/pages/messaging-room.vue: | ||||
|   messaging: "Messagerie" | ||||
| mobile/views/pages/received-follow-requests.vue: | ||||
|   title: "フォロー申請" | ||||
|   accept: "承認" | ||||
|   reject: "拒否" | ||||
|   title: "Demandes d'abonnement" | ||||
|   accept: "Approuver" | ||||
|   reject: "Refuser" | ||||
| mobile/views/pages/note.vue: | ||||
|   title: "Post" | ||||
|   prev: "Note précedante" | ||||
| @@ -772,19 +789,19 @@ mobile/views/pages/notifications.vue: | ||||
|   notifications: "Notifications" | ||||
|   read-all: "Êtes vous sûr de vouloir marqués toutes les notifications non-lus en tant que lus?" | ||||
| mobile/views/pages/settings/settings.profile.vue: | ||||
|   title: "プロフィール" | ||||
|   name: "名前" | ||||
|   account: "アカウント" | ||||
|   location: "場所" | ||||
|   description: "自己紹介" | ||||
|   birthday: "誕生日" | ||||
|   avatar: "アイコン" | ||||
|   banner: "バナー" | ||||
|   is-cat: "このアカウントはCatです" | ||||
|   save: "保存" | ||||
|   saved: "プロフィールを保存しました" | ||||
|   uploading: "アップロード中" | ||||
|   upload-failed: "アップロードに失敗しました" | ||||
|   title: "Profil" | ||||
|   name: "Nom" | ||||
|   account: "Compte" | ||||
|   location: "Lieu" | ||||
|   description: "Description" | ||||
|   birthday: "Date de naissance" | ||||
|   avatar: "Avatar" | ||||
|   banner: "Bannière" | ||||
|   is-cat: "Ce compte est un Bot" | ||||
|   save: "Mettre à jour le profil" | ||||
|   saved: "Profil mis à jour avec succès" | ||||
|   uploading: "En cours d'envoi" | ||||
|   upload-failed: "Échec de l'envoi" | ||||
| mobile/views/pages/search.vue: | ||||
|   search: "Chercher" | ||||
|   empty: "Aucun message trouvé pour '{}' " | ||||
| @@ -792,39 +809,39 @@ mobile/views/pages/selectdrive.vue: | ||||
|   select-file: "Choisissez un fichier" | ||||
| mobile/views/pages/settings.vue: | ||||
|   signed-in-as: "Connecté en tant que {}" | ||||
|   lang: "言語" | ||||
|   lang: "Langue" | ||||
|   lang-tip: "変更はページの再読み込み後に反映されます。" | ||||
|   recommended: "推奨" | ||||
|   auto: "自動" | ||||
|   specify-language: "言語を指定" | ||||
|   design: "デザインと表示" | ||||
|   dark-mode: "ダークモード" | ||||
|   i-am-under-limited-internet: "私は通信を制限されている" | ||||
|   circle-icons: "円形のアイコンを使用" | ||||
|   timeline: "タイムライン" | ||||
|   recommended: "Recommandé" | ||||
|   auto: "Automatique" | ||||
|   specify-language: "Spécifier la langue" | ||||
|   design: "Affichage et design" | ||||
|   dark-mode: "Mode nuit" | ||||
|   i-am-under-limited-internet: "J'ai un accès Internet limité" | ||||
|   circle-icons: "Utiliser des icônes circulaires" | ||||
|   timeline: "Fil d'actualité" | ||||
|   show-reply-target: "リプライ先を表示する" | ||||
|   show-my-renotes: "自分の行ったRenoteを表示する" | ||||
|   show-my-renotes: "Afficher mes republications" | ||||
|   show-renoted-my-notes: "Renoteされた自分の投稿を表示する" | ||||
|   post-style: "投稿の表示スタイル" | ||||
|   post-style-standard: "標準" | ||||
|   post-style-smart: "スマート" | ||||
|   behavior: "動作" | ||||
|   fetch-on-scroll: "スクロールで自動読み込み" | ||||
|   post-style: "Style de la publication" | ||||
|   post-style-standard: "Standard" | ||||
|   post-style-smart: "Intelligent" | ||||
|   behavior: "Comportement" | ||||
|   fetch-on-scroll: "Chargement lors du défilement" | ||||
|   disable-via-mobile: "「モバイルからの投稿」フラグを付けない" | ||||
|   load-raw-images: "添付された画像を高画質で表示する" | ||||
|   load-remote-media: "リモートサーバーのメディアを表示する" | ||||
|   twitter: "Twitter連携" | ||||
|   twitter-connect: "Twitterアカウントに接続する" | ||||
|   twitter-reconnect: "再接続する" | ||||
|   twitter-disconnect: "切断する" | ||||
|   update: "Misskey Update" | ||||
|   version: "バージョン:" | ||||
|   latest-version: "最新のバージョン:" | ||||
|   update-checking: "アップデートを確認中" | ||||
|   check-for-updates: "アップデートを確認" | ||||
|   no-updates: "利用可能な更新はありません" | ||||
|   no-updates-desc: "お使いのMisskeyは最新です。" | ||||
|   update-available: "新しいバージョンが利用可能です" | ||||
|   load-raw-images: "Afficher les photos jointes en haute qualité" | ||||
|   load-remote-media: "Afficher les médias sur le serveur distant" | ||||
|   twitter: "Intégration à Twitter" | ||||
|   twitter-connect: "Se connecter à votre compte Twitter" | ||||
|   twitter-reconnect: "Reconnecter" | ||||
|   twitter-disconnect: "Déconnexion" | ||||
|   update: "Mise à jour de Misskey" | ||||
|   version: "Version :" | ||||
|   latest-version: "Dernière version :" | ||||
|   update-checking: "Recherche de mises à jour" | ||||
|   check-for-updates: "Fréquence de vérification" | ||||
|   no-updates: "Aucune mise à jour disponible" | ||||
|   no-updates-desc: "Votre Misskey est à jour." | ||||
|   update-available: "Nouvelle version disponible !" | ||||
|   update-available-desc: "ページを再度読み込みすると更新が適用されます。" | ||||
|   settings: "Réglages" | ||||
|   signout: "Déconnexion" | ||||
|   | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "闇に飲まれる" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   | ||||
| @@ -76,6 +76,23 @@ common: | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|  | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
|  | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -606,6 +623,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|  | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
|  | ||||
|   | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "闇に飲まれる" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   | ||||
| @@ -5,7 +5,7 @@ meta: | ||||
| common: | ||||
|   misskey: "Planeta Fediwersum" | ||||
|   about-title: "⭐ Fediwersum" | ||||
|   about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。" | ||||
|   about: "Dziękujemy za znalezienie Misskey. Misskey jest <b>zdecentralizowaną platformą mikroblogową</b> powstałą na Ziemi. Ponieważ działa ona w Fediwersum (uniwersum, w którego skład wchodzi wiele sieci społecznościowych), jest ona połączona z innymi platformami społecznościowymi. Spróbujesz odpocząć od zatłoczoneo miasta i zanurzyć się w nowym Internecie?" | ||||
|   time: | ||||
|     unknown: "nieznany" | ||||
|     future: "w przyszłości" | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "Dotacje" | ||||
|     nav: "Nawigacja" | ||||
|     tips: "Wskazówki" | ||||
|   deck: | ||||
|     widgets: "Widżety" | ||||
|     home: "Strona główna" | ||||
|     local: "Lokalne" | ||||
|     global: "Globalne" | ||||
|     notifications: "Powiadomienia" | ||||
|     list: "Listy" | ||||
|     swap-left: "Przesuń w lewo" | ||||
|     swap-right: "Przesuń w prawo" | ||||
|     swap-up: "Przenieś w górę" | ||||
|     swap-down: "Przenieś w dół" | ||||
|     remove: "Usuń" | ||||
|     add-column: "Dodaj kolumnę" | ||||
|     rename: "Zmień nazwę" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "Nie udało się połączyć z serwerem" | ||||
|   description: "Wystąpił problem z Twoim połączeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce." | ||||
| @@ -290,10 +306,10 @@ desktop/views/components/drive.vue: | ||||
|     upload: "Wyślij plik" | ||||
|     url-upload: "Wyślij z adresu URL" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   following: "Śledzisz" | ||||
|   follow: "Śledź" | ||||
|   request-pending: "Oczekiwanie na pozwolenie" | ||||
|   follow-request: "Poproś o śledzenie" | ||||
| desktop/views/components/followers-window.vue: | ||||
|   followers: "Śledzący" | ||||
| desktop/views/components/followers.vue: | ||||
| @@ -516,13 +532,14 @@ desktop/views/components/ui.header.account.vue: | ||||
|   drive: "Dysk" | ||||
|   favorites: "Ulubione" | ||||
|   lists: "Listy" | ||||
|   follow-requests: "フォロー申請" | ||||
|   follow-requests: "Prośby o śledzenie" | ||||
|   customize: "Dostosuj" | ||||
|   settings: "Ustawienia" | ||||
|   signout: "Wyloguj się" | ||||
|   dark: "Sprowadź ciemność" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "Strona główna" | ||||
|   deck: "Talia" | ||||
|   messaging: "Wiadomości" | ||||
|   game: "Gra" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -532,11 +549,11 @@ desktop/views/components/ui.header.post.vue: | ||||
| desktop/views/components/ui.header.search.vue: | ||||
|   placeholder: "Szukaj" | ||||
| desktop/views/components/received-follow-requests-window.vue: | ||||
|   title: "フォロー申請" | ||||
|   accept: "承認" | ||||
|   reject: "拒否" | ||||
|   title: "Poproś o śledzenie" | ||||
|   accept: "Zatwierdź" | ||||
|   reject: "Odmów" | ||||
| desktop/views/components/user-lists-window.vue: | ||||
|   title: "リスト" | ||||
|   title: "Listy" | ||||
|   create-list: "Utwórz listę" | ||||
| desktop/views/components/user-preview.vue: | ||||
|   notes: "Wpisy" | ||||
| @@ -661,10 +678,10 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "Hash (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "Śledzisz" | ||||
|   follow: "Śledź" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   request-pending: "Oczekiwanie na pozwolenie" | ||||
|   follow-request: "Poproś o śledzenie" | ||||
| mobile/views/components/friends-maker.vue: | ||||
|   title: "Zacznij śledzić ludzi takich jak Ty" | ||||
|   empty: "Nie znaleziono podobnych użytkowników." | ||||
| @@ -722,7 +739,7 @@ mobile/views/components/ui.nav.vue: | ||||
|   timeline: "Oś czasu" | ||||
|   notifications: "Powiadomienia" | ||||
|   messaging: "Wiadomości" | ||||
|   follow-requests: "フォロー申請" | ||||
|   follow-requests: "Prośby o śledzenie" | ||||
|   search: "Szukaj" | ||||
|   drive: "Dysk" | ||||
|   favorites: "Ulubione" | ||||
| @@ -761,9 +778,9 @@ mobile/views/pages/messaging.vue: | ||||
| mobile/views/pages/messaging-room.vue: | ||||
|   messaging: "Wiadomości" | ||||
| mobile/views/pages/received-follow-requests.vue: | ||||
|   title: "フォロー申請" | ||||
|   accept: "承認" | ||||
|   reject: "拒否" | ||||
|   title: "Prośby o śledzenie" | ||||
|   accept: "Zatwierdź" | ||||
|   reject: "Odmów" | ||||
| mobile/views/pages/note.vue: | ||||
|   title: "Wpis" | ||||
|   prev: "Poprzedni wpis" | ||||
|   | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "闇に飲まれる" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "闇に飲まれる" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   | ||||
| @@ -69,6 +69,22 @@ common: | ||||
|     donation: "寄付のお願い" | ||||
|     nav: "ナビゲーション" | ||||
|     tips: "ヒント" | ||||
|   deck: | ||||
|     widgets: "ウィジェット" | ||||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     global: "グローバル" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|     swap-right: "右に移動" | ||||
|     swap-up: "上に移動" | ||||
|     swap-down: "下に移動" | ||||
|     remove: "カラムを削除" | ||||
|     add-column: "カラムを追加" | ||||
|     rename: "名前を変更" | ||||
|     stack-left: "左に重ねる" | ||||
|     pop-right: "右に出す" | ||||
| common/views/components/connect-failed.vue: | ||||
|   title: "サーバーに接続できません" | ||||
|   description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" | ||||
| @@ -290,7 +306,7 @@ desktop/views/components/drive.vue: | ||||
|     upload: "ファイルをアップロード" | ||||
|     url-upload: "URLからアップロード" | ||||
| desktop/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
| @@ -523,6 +539,7 @@ desktop/views/components/ui.header.account.vue: | ||||
|   dark: "闇に飲まれる" | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   messaging: "メッセージ" | ||||
|   game: "ゲーム" | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -661,7 +678,7 @@ mobile/views/components/drive.file-detail.vue: | ||||
|   hash: "ハッシュ (md5)" | ||||
|   exif: "EXIF" | ||||
| mobile/views/components/follow-button.vue: | ||||
|   unfollow: "フォロー中" | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-request: "フォロー申請" | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "2.25.2", | ||||
| 	"clientVersion": "1.0.6116", | ||||
| 	"version": "2.31.0", | ||||
| 	"clientVersion": "1.0.6276", | ||||
| 	"codename": "nighthike", | ||||
| 	"main": "./built/index.js", | ||||
| 	"private": true, | ||||
|   | ||||
| @@ -9,9 +9,9 @@ export default function<T extends object>(data: { | ||||
| 			widget: { | ||||
| 				type: Object | ||||
| 			}, | ||||
| 			isMobile: { | ||||
| 				type: Boolean, | ||||
| 				default: false | ||||
| 			platform: { | ||||
| 				type: String, | ||||
| 				required: true | ||||
| 			}, | ||||
| 			isCustomizeMode: { | ||||
| 				type: Boolean, | ||||
| @@ -66,17 +66,10 @@ export default function<T extends object>(data: { | ||||
|  | ||||
| 				this.bakeProps(); | ||||
|  | ||||
| 				if (this.isMobile) { | ||||
| 					(this as any).api('i/update_mobile_home', { | ||||
| 						id: this.id, | ||||
| 						data: this.props | ||||
| 					}); | ||||
| 				} else { | ||||
| 					(this as any).api('i/update_home', { | ||||
| 						id: this.id, | ||||
| 						data: this.props | ||||
| 					}); | ||||
| 				} | ||||
| 				(this as any).api('i/update_widget', { | ||||
| 					id: this.id, | ||||
| 					data: this.props | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| import * as merge from 'object-assign-deep'; | ||||
|  | ||||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| @@ -60,25 +58,18 @@ export class HomeStream extends Stream { | ||||
| 		}); | ||||
|  | ||||
| 		this.on('home_updated', x => { | ||||
| 			if (x.home) { | ||||
| 				os.store.commit('settings/setHome', x.home); | ||||
| 			} else { | ||||
| 				os.store.commit('settings/setHomeWidget', { | ||||
| 					id: x.id, | ||||
| 					data: x.data | ||||
| 				}); | ||||
| 			} | ||||
| 			os.store.commit('settings/setHome', x); | ||||
| 		}); | ||||
|  | ||||
| 		this.on('mobile_home_updated', x => { | ||||
| 			if (x.home) { | ||||
| 				os.store.commit('settings/setMobileHome', x.home); | ||||
| 			} else { | ||||
| 				os.store.commit('settings/setMobileHomeWidget', { | ||||
| 					id: x.id, | ||||
| 					data: x.data | ||||
| 				}); | ||||
| 			} | ||||
| 			os.store.commit('settings/setMobileHome', x); | ||||
| 		}); | ||||
|  | ||||
| 		this.on('widgetUpdated', x => { | ||||
| 			os.store.commit('settings/setWidget', { | ||||
| 				id: x.id, | ||||
| 				data: x.data | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		// トークンが再生成されたとき | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import analogClock from './analog-clock.vue'; | ||||
| import menu from './menu.vue'; | ||||
| import signin from './signin.vue'; | ||||
| import signup from './signup.vue'; | ||||
| import forkit from './forkit.vue'; | ||||
| @@ -29,6 +30,7 @@ import Othello from './othello.vue'; | ||||
| import welcomeTimeline from './welcome-timeline.vue'; | ||||
|  | ||||
| Vue.component('mk-analog-clock', analogClock); | ||||
| Vue.component('mk-menu', menu); | ||||
| Vue.component('mk-signin', signin); | ||||
| Vue.component('mk-signup', signup); | ||||
| Vue.component('mk-forkit', forkit); | ||||
|   | ||||
							
								
								
									
										182
									
								
								src/client/app/common/views/components/menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/client/app/common/views/components/menu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| <template> | ||||
| <div class="mk-menu"> | ||||
| 	<div class="backdrop" ref="backdrop" @click="close"></div> | ||||
| 	<div class="popover" :class="{ hukidasi }" ref="popover"> | ||||
| 		<template v-for="item in items"> | ||||
| 			<div v-if="item === null"></div> | ||||
| 			<button v-if="item" @click="clicked(item.onClick)" v-html="item.content"></button> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as anime from 'animejs'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['source', 'compact', 'items'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hukidasi: !this.compact | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			const popover = this.$refs.popover as any; | ||||
|  | ||||
| 			const rect = this.source.getBoundingClientRect(); | ||||
| 			const width = popover.offsetWidth; | ||||
| 			const height = popover.offsetHeight; | ||||
|  | ||||
| 			let left; | ||||
| 			let top; | ||||
|  | ||||
| 			if (this.compact) { | ||||
| 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||
| 				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); | ||||
| 				left = (x - (width / 2)); | ||||
| 				top = (y - (height / 2)); | ||||
| 			} else { | ||||
| 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||
| 				const y = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||
| 				left = (x - (width / 2)); | ||||
| 				top = y; | ||||
| 			} | ||||
|  | ||||
| 			if (left + width > window.innerWidth) { | ||||
| 				left = window.innerWidth - width; | ||||
| 				this.hukidasi = false; | ||||
| 			} | ||||
|  | ||||
| 			if (top + height > window.innerHeight) { | ||||
| 				top = window.innerHeight - height; | ||||
| 				this.hukidasi = false; | ||||
| 			} | ||||
|  | ||||
| 			popover.style.left = left + 'px'; | ||||
| 			popover.style.top = top + 'px'; | ||||
|  | ||||
| 			anime({ | ||||
| 				targets: this.$refs.backdrop, | ||||
| 				opacity: 1, | ||||
| 				duration: 100, | ||||
| 				easing: 'linear' | ||||
| 			}); | ||||
|  | ||||
| 			anime({ | ||||
| 				targets: this.$refs.popover, | ||||
| 				opacity: 1, | ||||
| 				scale: [0.5, 1], | ||||
| 				duration: 500 | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		clicked(fn) { | ||||
| 			fn(); | ||||
| 			this.close(); | ||||
| 		}, | ||||
| 		close() { | ||||
| 			(this.$refs.backdrop as any).style.pointerEvents = 'none'; | ||||
| 			anime({ | ||||
| 				targets: this.$refs.backdrop, | ||||
| 				opacity: 0, | ||||
| 				duration: 200, | ||||
| 				easing: 'linear' | ||||
| 			}); | ||||
|  | ||||
| 			(this.$refs.popover as any).style.pointerEvents = 'none'; | ||||
| 			anime({ | ||||
| 				targets: this.$refs.popover, | ||||
| 				opacity: 0, | ||||
| 				scale: 0.5, | ||||
| 				duration: 200, | ||||
| 				easing: 'easeInBack', | ||||
| 				complete: () => { | ||||
| 					this.$emit('closed'); | ||||
| 					this.$destroy(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| $border-color = rgba(27, 31, 35, 0.15) | ||||
|  | ||||
| .mk-menu | ||||
| 	position initial | ||||
|  | ||||
| 	> .backdrop | ||||
| 		position fixed | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		z-index 10000 | ||||
| 		width 100% | ||||
| 		height 100% | ||||
| 		background rgba(#000, 0.1) | ||||
| 		opacity 0 | ||||
|  | ||||
| 	> .popover | ||||
| 		position absolute | ||||
| 		z-index 10001 | ||||
| 		padding 8px 0 | ||||
| 		background #fff | ||||
| 		border 1px solid $border-color | ||||
| 		border-radius 4px | ||||
| 		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) | ||||
| 		transform scale(0.5) | ||||
| 		opacity 0 | ||||
|  | ||||
| 		$balloon-size = 16px | ||||
|  | ||||
| 		&.hukidasi | ||||
| 			margin-top $balloon-size | ||||
| 			transform-origin center -($balloon-size) | ||||
|  | ||||
| 			&:before | ||||
| 				content "" | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top -($balloon-size * 2) | ||||
| 				left s('calc(50% - %s)', $balloon-size) | ||||
| 				border-top solid $balloon-size transparent | ||||
| 				border-left solid $balloon-size transparent | ||||
| 				border-right solid $balloon-size transparent | ||||
| 				border-bottom solid $balloon-size $border-color | ||||
|  | ||||
| 			&:after | ||||
| 				content "" | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top -($balloon-size * 2) + 1.5px | ||||
| 				left s('calc(50% - %s)', $balloon-size) | ||||
| 				border-top solid $balloon-size transparent | ||||
| 				border-left solid $balloon-size transparent | ||||
| 				border-right solid $balloon-size transparent | ||||
| 				border-bottom solid $balloon-size #fff | ||||
|  | ||||
| 		> button | ||||
| 			display block | ||||
| 			padding 8px 16px | ||||
| 			width 100% | ||||
|  | ||||
| 			&:hover | ||||
| 				color $theme-color-foreground | ||||
| 				background $theme-color | ||||
| 				text-decoration none | ||||
|  | ||||
| 			&:active | ||||
| 				color $theme-color-foreground | ||||
| 				background darken($theme-color, 10%) | ||||
|  | ||||
| 		> div | ||||
| 			margin 8px 0 | ||||
| 			height 1px | ||||
| 			background #eee | ||||
|  | ||||
| </style> | ||||
| @@ -1,55 +1,41 @@ | ||||
| <template> | ||||
| <div class="mk-note-menu"> | ||||
| 	<div class="backdrop" ref="backdrop" @click="close"></div> | ||||
| 	<div class="popover" :class="{ compact }" ref="popover"> | ||||
| 		<button @click="favorite">%i18n:@favorite%</button> | ||||
| 		<button v-if="note.userId == $store.state.i.id" @click="pin">%i18n:@pin%</button> | ||||
| 		<button v-if="note.userId == $store.state.i.id" @click="del">%i18n:@delete%</button> | ||||
| 		<a v-if="note.uri" :href="note.uri" target="_blank">%i18n:@remote%</a> | ||||
| 	</div> | ||||
| <div class="mk-note-menu" style="position:initial"> | ||||
| 	<mk-menu ref="menu" :source="source" :compact="compact" :items="items" @closed="$destroy"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as anime from 'animejs'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['note', 'source', 'compact'], | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			const popover = this.$refs.popover as any; | ||||
|  | ||||
| 			const rect = this.source.getBoundingClientRect(); | ||||
| 			const width = popover.offsetWidth; | ||||
| 			const height = popover.offsetHeight; | ||||
|  | ||||
| 			if (this.compact) { | ||||
| 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||
| 				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); | ||||
| 				popover.style.left = (x - (width / 2)) + 'px'; | ||||
| 				popover.style.top = (y - (height / 2)) + 'px'; | ||||
| 			} else { | ||||
| 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||
| 				const y = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||
| 				popover.style.left = (x - (width / 2)) + 'px'; | ||||
| 				popover.style.top = y + 'px'; | ||||
| 	computed: { | ||||
| 		items() { | ||||
| 			const items = []; | ||||
| 			items.push({ | ||||
| 				content: '%i18n:@favorite%', | ||||
| 				onClick: this.favorite | ||||
| 			}); | ||||
| 			if (this.note.userId == this.$store.state.i.id) { | ||||
| 				items.push({ | ||||
| 					content: '%i18n:@pin%', | ||||
| 					onClick: this.pin | ||||
| 				}); | ||||
| 				items.push({ | ||||
| 					content: '%i18n:@delete%', | ||||
| 					onClick: this.del | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			anime({ | ||||
| 				targets: this.$refs.backdrop, | ||||
| 				opacity: 1, | ||||
| 				duration: 100, | ||||
| 				easing: 'linear' | ||||
| 			}); | ||||
|  | ||||
| 			anime({ | ||||
| 				targets: this.$refs.popover, | ||||
| 				opacity: 1, | ||||
| 				scale: [0.5, 1], | ||||
| 				duration: 500 | ||||
| 			}); | ||||
| 		}); | ||||
| 			if (this.note.uri) { | ||||
| 				items.push({ | ||||
| 					content: '%i18n:@remote%', | ||||
| 					onClick: () => { | ||||
| 						window.open(this.note.uri, '_blank'); | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 			return items; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		pin() { | ||||
| @@ -78,98 +64,8 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		close() { | ||||
| 			(this.$refs.backdrop as any).style.pointerEvents = 'none'; | ||||
| 			anime({ | ||||
| 				targets: this.$refs.backdrop, | ||||
| 				opacity: 0, | ||||
| 				duration: 200, | ||||
| 				easing: 'linear' | ||||
| 			}); | ||||
|  | ||||
| 			(this.$refs.popover as any).style.pointerEvents = 'none'; | ||||
| 			anime({ | ||||
| 				targets: this.$refs.popover, | ||||
| 				opacity: 0, | ||||
| 				scale: 0.5, | ||||
| 				duration: 200, | ||||
| 				easing: 'easeInBack', | ||||
| 				complete: () => this.$destroy() | ||||
| 			}); | ||||
| 			this.$refs.menu.close(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| $border-color = rgba(27, 31, 35, 0.15) | ||||
|  | ||||
| .mk-note-menu | ||||
| 	position initial | ||||
|  | ||||
| 	> .backdrop | ||||
| 		position fixed | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		z-index 10000 | ||||
| 		width 100% | ||||
| 		height 100% | ||||
| 		background rgba(#000, 0.1) | ||||
| 		opacity 0 | ||||
|  | ||||
| 	> .popover | ||||
| 		position absolute | ||||
| 		z-index 10001 | ||||
| 		padding 8px 0 | ||||
| 		background #fff | ||||
| 		border 1px solid $border-color | ||||
| 		border-radius 4px | ||||
| 		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) | ||||
| 		transform scale(0.5) | ||||
| 		opacity 0 | ||||
|  | ||||
| 		$balloon-size = 16px | ||||
|  | ||||
| 		&:not(.compact) | ||||
| 			margin-top $balloon-size | ||||
| 			transform-origin center -($balloon-size) | ||||
|  | ||||
| 			&:before | ||||
| 				content "" | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top -($balloon-size * 2) | ||||
| 				left s('calc(50% - %s)', $balloon-size) | ||||
| 				border-top solid $balloon-size transparent | ||||
| 				border-left solid $balloon-size transparent | ||||
| 				border-right solid $balloon-size transparent | ||||
| 				border-bottom solid $balloon-size $border-color | ||||
|  | ||||
| 			&:after | ||||
| 				content "" | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top -($balloon-size * 2) + 1.5px | ||||
| 				left s('calc(50% - %s)', $balloon-size) | ||||
| 				border-top solid $balloon-size transparent | ||||
| 				border-left solid $balloon-size transparent | ||||
| 				border-right solid $balloon-size transparent | ||||
| 				border-bottom solid $balloon-size #fff | ||||
|  | ||||
| 		> button | ||||
| 		> a | ||||
| 			display block | ||||
| 			padding 8px 16px | ||||
| 			width 100% | ||||
|  | ||||
| 			&:hover | ||||
| 				color $theme-color-foreground | ||||
| 				background $theme-color | ||||
| 				text-decoration none | ||||
|  | ||||
| 			&:active | ||||
| 				color $theme-color-foreground | ||||
| 				background darken($theme-color, 10%) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -59,7 +59,7 @@ export default Vue.extend({ | ||||
| 	created() { | ||||
| 		if (this.mode == 'relative' || this.mode == 'detail') { | ||||
| 			this.tick(); | ||||
| 			this.tickId = setInterval(this.tick, 1000); | ||||
| 			this.tickId = setInterval(this.tick, 5000); | ||||
| 		} | ||||
| 	}, | ||||
| 	destroyed() { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <div class="mkw-broadcast" | ||||
| 	:data-found="broadcasts.length != 0" | ||||
| 	:data-melt="props.design == 1" | ||||
| 	:data-mobile="isMobile" | ||||
| 	:data-mobile="platform == 'mobile'" | ||||
| > | ||||
| 	<div class="icon"> | ||||
| 		<svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="mkw-calendar" :data-special="special" :data-mobile="isMobile"> | ||||
| <div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'"> | ||||
| 	<mk-widget-container :naked="props.design == 1" :show-header="false"> | ||||
| 		<div class="mkw-calendar--body"> | ||||
| 			<div class="calendar" :data-is-holiday="isHoliday"> | ||||
| @@ -67,7 +67,7 @@ export default define({ | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (this.isMobile) return; | ||||
| 			if (this.platform == 'mobile') return; | ||||
| 			if (this.props.design == 2) { | ||||
| 				this.props.design = 0; | ||||
| 			} else { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="mkw-donation" :data-mobile="isMobile"> | ||||
| <div class="mkw-donation" :data-mobile="platform == 'mobile'"> | ||||
| 	<article> | ||||
| 		<h1>%fa:heart%%i18n:@title%</h1> | ||||
| 		<p> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<template slot="header">%fa:rss-square%RSS</template> | ||||
| 		<button slot="func" title="設定" @click="setting">%fa:cog%</button> | ||||
|  | ||||
| 		<div class="mkw-rss--body" :data-mobile="isMobile"> | ||||
| 		<div class="mkw-rss--body" :data-mobile="platform == 'mobile'"> | ||||
| 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> | ||||
| 			<div class="feed" v-else> | ||||
| 				<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="mkw-slideshow" :data-mobile="isMobile"> | ||||
| <div class="mkw-slideshow" :data-mobile="platform == 'mobile'"> | ||||
| 	<div @click="choose"> | ||||
| 		<p v-if="props.folder === undefined"> | ||||
| 			<template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ export default (os: OS) => opts => { | ||||
| 	const o = opts || {}; | ||||
| 	if (o.renote) { | ||||
| 		const vm = os.new(RenoteFormWindow, { | ||||
| 			renote: o.renote | ||||
| 			note: o.renote | ||||
| 		}); | ||||
| 		document.body.appendChild(vm.$el); | ||||
| 	} else { | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import updateAvatar from './api/update-avatar'; | ||||
| import updateBanner from './api/update-banner'; | ||||
|  | ||||
| import MkIndex from './views/pages/index.vue'; | ||||
| import MkDeck from './views/pages/deck/deck.vue'; | ||||
| import MkUser from './views/pages/user/user.vue'; | ||||
| import MkFavorites from './views/pages/favorites.vue'; | ||||
| import MkSelectDrive from './views/pages/selectdrive.vue'; | ||||
| @@ -50,6 +51,7 @@ init(async (launch) => { | ||||
| 		mode: 'history', | ||||
| 		routes: [ | ||||
| 			{ path: '/', name: 'index', component: MkIndex }, | ||||
| 			{ path: '/deck', name: 'deck', component: MkDeck }, | ||||
| 			{ path: '/i/customize-home', component: MkHomeCustomize }, | ||||
| 			{ path: '/i/favorites', component: MkFavorites }, | ||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||
|   | ||||
| @@ -42,8 +42,3 @@ html | ||||
|  | ||||
| 				&:active | ||||
| 					background-color $theme-color | ||||
|  | ||||
| body | ||||
| 	display flex | ||||
| 	flex-direction column | ||||
| 	min-height 100% | ||||
|   | ||||
| @@ -47,7 +47,7 @@ | ||||
| 				:key="place" | ||||
| 			> | ||||
| 				<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> | ||||
| 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/> | ||||
| 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 			<div class="main"> | ||||
| @@ -60,7 +60,7 @@ | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<div v-for="place in ['left', 'right']" :class="place"> | ||||
| 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/> | ||||
| 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/> | ||||
| 			</div> | ||||
| 			<div class="main"> | ||||
| 				<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| @@ -76,6 +76,50 @@ import Vue from 'vue'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import * as uuid from 'uuid'; | ||||
|  | ||||
| const defaultDesktopHomeWidgets = { | ||||
| 	left: [ | ||||
| 		'profile', | ||||
| 		'calendar', | ||||
| 		'activity', | ||||
| 		'rss', | ||||
| 		'trends', | ||||
| 		'photo-stream', | ||||
| 		'version' | ||||
| 	], | ||||
| 	right: [ | ||||
| 		'broadcast', | ||||
| 		'notifications', | ||||
| 		'users', | ||||
| 		'polls', | ||||
| 		'server', | ||||
| 		'donation', | ||||
| 		'nav', | ||||
| 		'tips' | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| //#region Construct home data | ||||
| const _defaultDesktopHomeWidgets = []; | ||||
|  | ||||
| defaultDesktopHomeWidgets.left.forEach(widget => { | ||||
| 	_defaultDesktopHomeWidgets.push({ | ||||
| 		name: widget, | ||||
| 		id: uuid(), | ||||
| 		place: 'left', | ||||
| 		data: {} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| defaultDesktopHomeWidgets.right.forEach(widget => { | ||||
| 	_defaultDesktopHomeWidgets.push({ | ||||
| 		name: widget, | ||||
| 		id: uuid(), | ||||
| 		place: 'right', | ||||
| 		data: {} | ||||
| 	}); | ||||
| }); | ||||
| //#endregion | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XDraggable | ||||
| @@ -103,7 +147,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	computed: { | ||||
| 		home(): any[] { | ||||
| 			return this.$store.state.settings.home; | ||||
| 			return this.$store.state.settings.home || []; | ||||
| 		}, | ||||
| 		left(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'left'); | ||||
| @@ -119,6 +163,16 @@ export default Vue.extend({ | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		if (this.$store.state.settings.home == null) { | ||||
| 			this.api('i/update_home', { | ||||
| 				home: _defaultDesktopHomeWidgets | ||||
| 			}).then(() => { | ||||
| 				this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
|   | ||||
| @@ -74,7 +74,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	mounted() { | ||||
| 		document.addEventListener('visibilitychange', this.onVisibilitychange, false); | ||||
| 		window.addEventListener('scroll', this.onScroll); | ||||
| 		window.addEventListener('scroll', this.onScroll, { passive: true }); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
|   | ||||
| @@ -206,7 +206,7 @@ root(isDark) | ||||
| 				margin 0 | ||||
| 				padding 16px | ||||
| 				overflow-wrap break-word | ||||
| 				font-size 0.9em | ||||
| 				font-size 12px | ||||
| 				border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) | ||||
|  | ||||
| 				&:last-child | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> | ||||
| 	<span slot="header">%fa:envelope R% %i18n:@title%</span> | ||||
|  | ||||
| 	<div data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4" :data-darkmode="$store.state.device.darkmode"> | ||||
| 	<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode"> | ||||
| 		<div v-for="req in requests"> | ||||
| 			<router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link> | ||||
| 			<span> | ||||
| @@ -63,10 +63,10 @@ root(isDark) | ||||
| 		> span | ||||
| 			margin 0 0 0 auto | ||||
|  | ||||
| [data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4"][data-darkmode] | ||||
| .slpqaxdoxhvglersgjukmvizkqbmbokc[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| [data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4"]:not([data-darkmode]) | ||||
| .slpqaxdoxhvglersgjukmvizkqbmbokc:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -40,6 +40,8 @@ | ||||
| 				<button class="ui button" @click="customizeHome" style="margin-bottom: 16px">%i18n:@customize%</button> | ||||
| 			</div> | ||||
| 			<div class="div"> | ||||
| 				<button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button> | ||||
| 				<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.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/> | ||||
| @@ -293,6 +295,20 @@ export default Vue.extend({ | ||||
| 			this.$router.push('/i/customize-home'); | ||||
| 			this.$emit('done'); | ||||
| 		}, | ||||
| 		updateWallpaper() { | ||||
| 			(this as any).apis.chooseDriveFile({ | ||||
| 				multiple: false | ||||
| 			}).then(file => { | ||||
| 				(this as any).api('i/update', { | ||||
| 					wallpaperId: file.id | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 		deleteWallpaper() { | ||||
| 			(this as any).api('i/update', { | ||||
| 				wallpaperId: null | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeFetchOnScroll(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'fetchOnScroll', | ||||
|   | ||||
| @@ -8,6 +8,12 @@ | ||||
| 					<p>%i18n:@home%</p> | ||||
| 				</router-link> | ||||
| 			</li> | ||||
| 			<li class="deck" :class="{ active: $route.name == 'deck' }"> | ||||
| 				<router-link to="/deck"> | ||||
| 					%fa:columns% | ||||
| 					<p>%i18n:@deck% <small>(beta)</small></p> | ||||
| 				</router-link> | ||||
| 			</li> | ||||
| 			<li class="messaging"> | ||||
| 				<a @click="messaging"> | ||||
| 					%fa:comments% | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="mk-ui"> | ||||
| <div class="mk-ui" :style="style"> | ||||
| 	<x-header class="header"/> | ||||
| 	<div class="content"> | ||||
| 		<slot></slot> | ||||
| @@ -16,6 +16,15 @@ export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XHeader | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		style(): any { | ||||
| 			if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {}; | ||||
| 			return { | ||||
| 				backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null, | ||||
| 				backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })` | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		document.addEventListener('keydown', this.onKeydown); | ||||
| 	}, | ||||
| @@ -37,7 +46,20 @@ export default Vue.extend({ | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-ui | ||||
| 	display flex | ||||
| 	flex-direction column | ||||
| 	flex 1 | ||||
| 	background-size cover | ||||
| 	background-position center | ||||
| 	background-attachment fixed | ||||
|  | ||||
| 	> .header | ||||
| 		@media (max-width 1000px) | ||||
| 			display none | ||||
|  | ||||
| 	> .content | ||||
| 		display flex | ||||
| 		flex-direction column | ||||
| 		flex 1 | ||||
| 		overflow hidden | ||||
| </style> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> | ||||
| 	<span slot="header">%fa:list% %i18n:@title%</span> | ||||
|  | ||||
| 	<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode"> | ||||
| 	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode"> | ||||
| 		<button class="ui" @click="add">%i18n:@create-list%</button> | ||||
| 		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a> | ||||
| 	</div> | ||||
| @@ -60,10 +60,10 @@ root(isDark) | ||||
| 		border solid 1px isDark ? #1c2023 : #eee | ||||
| 		border-radius 4px | ||||
|  | ||||
| [data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"][data-darkmode] | ||||
| .xkxvokkjlptzyewouewmceqcxhpgzprp[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| [data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"]:not([data-darkmode]) | ||||
| .xkxvokkjlptzyewouewmceqcxhpgzprp:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export default Vue.extend({ | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	background isDark ? #282C37 : #fff | ||||
| 	border solid 1px rgba(#000, 0.075) | ||||
| 	border solid 1px rgba(#000, isDark ? 0.2 : 0.075) | ||||
| 	border-radius 6px | ||||
| 	overflow hidden | ||||
|  | ||||
|   | ||||
							
								
								
									
										35
									
								
								src/client/app/desktop/views/pages/deck/deck.column-core.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/client/app/desktop/views/pages/deck/deck.column-core.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| <template> | ||||
| <x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XTlColumn from './deck.tl-column.vue'; | ||||
| import XNotificationsColumn from './deck.notifications-column.vue'; | ||||
| import XWidgetsColumn from './deck.widgets-column.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XTlColumn, | ||||
| 		XNotificationsColumn, | ||||
| 		XWidgetsColumn | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										314
									
								
								src/client/app/desktop/views/pages/deck/deck.column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								src/client/app/desktop/views/pages/deck/deck.column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,314 @@ | ||||
| <template> | ||||
| <div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging }"> | ||||
| 	<header :class="{ indicate: count > 0 }" | ||||
| 			draggable="true" | ||||
| 			@click="toggleActive" | ||||
| 			@dragstart="onDragstart" | ||||
| 			@dragend="onDragend" | ||||
| 			@dragover.prevent.stop="onDragover" | ||||
| 			@dragenter.prevent="onDragenter" | ||||
| 			@dragleave="onDragleave" | ||||
| 			@drop.prevent.stop="onDrop" | ||||
| 		> | ||||
| 		<slot name="header"></slot> | ||||
| 		<span class="count" v-if="count > 0">({{ count }})</span> | ||||
| 		<button ref="menu" @click.stop="showMenu">%fa:caret-down%</button> | ||||
| 	</header> | ||||
| 	<div ref="body" v-show="active"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import Menu from '../../../../common/views/components/menu.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		name: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		menu: { | ||||
| 			type: Array, | ||||
| 			required: false, | ||||
| 			default: null | ||||
| 		}, | ||||
| 		naked: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		narrow: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	inject: { | ||||
| 		getColumnVm: { from: 'getColumnVm' } | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			count: 0, | ||||
| 			active: true, | ||||
| 			dragging: false, | ||||
| 			draghover: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		active(v) { | ||||
| 			if (v && this.isScrollTop()) { | ||||
| 				this.$emit('top'); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			column: this, | ||||
| 			isScrollTop: this.isScrollTop, | ||||
| 			count: v => this.count = v | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true }); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.$refs.body.removeEventListener('scroll', this.onScroll); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		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; | ||||
| 			this.active = !this.active; | ||||
| 		}, | ||||
|  | ||||
| 		isScrollTop() { | ||||
| 			return this.active && this.$refs.body.scrollTop == 0; | ||||
| 		}, | ||||
|  | ||||
| 		onScroll() { | ||||
| 			if (this.isScrollTop()) { | ||||
| 				this.$emit('top'); | ||||
| 			} | ||||
|  | ||||
| 			if (this.$store.state.settings.fetchOnScroll !== false) { | ||||
| 				const current = this.$refs.body.scrollTop + this.$refs.body.clientHeight; | ||||
| 				if (current > this.$refs.body.scrollHeight - 1) this.$emit('bottom'); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		showMenu() { | ||||
| 			const items = [{ | ||||
| 				content: '%fa:pencil-alt% %i18n:common.deck.rename%', | ||||
| 				onClick: () => { | ||||
| 					(this as any).apis.input({ | ||||
| 						title: '%i18n:common.deck.rename%', | ||||
| 						default: this.name, | ||||
| 						allowEmpty: false | ||||
| 					}).then(name => { | ||||
| 						this.$store.dispatch('settings/renameDeckColumn', { id: this.column.id, name }); | ||||
| 					}); | ||||
| 				} | ||||
| 			}, null, { | ||||
| 				content: '%fa:arrow-left% %i18n:common.deck.swap-left%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/swapLeftDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}, { | ||||
| 				content: '%fa:arrow-right% %i18n:common.deck.swap-right%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/swapRightDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}, this.isStacked ? { | ||||
| 				content: '%fa:arrow-up% %i18n:common.deck.swap-up%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/swapUpDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			} : undefined, this.isStacked ? { | ||||
| 				content: '%fa:arrow-down% %i18n:common.deck.swap-down%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/swapDownDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			} : undefined, null, { | ||||
| 				content: '%fa:window-restore R% %i18n:common.deck.stack-left%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/stackLeftDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}, this.isStacked ? { | ||||
| 				content: '%fa:window-maximize R% %i18n:common.deck.pop-right%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/popRightDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			} : undefined, null, { | ||||
| 				content: '%fa:trash-alt R% %i18n:common.deck.remove%', | ||||
| 				onClick: () => { | ||||
| 					this.$store.dispatch('settings/removeDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (this.menu) { | ||||
| 				items.unshift(null); | ||||
| 				this.menu.reverse().forEach(i => items.unshift(i)); | ||||
| 			} | ||||
|  | ||||
| 			this.os.new(Menu, { | ||||
| 				source: this.$refs.menu, | ||||
| 				compact: false, | ||||
| 				items | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onDragstart(e) { | ||||
| 			e.dataTransfer.effectAllowed = 'move'; | ||||
| 			e.dataTransfer.setData('mk-deck-column', this.column.id); | ||||
| 			this.dragging = true; | ||||
| 		}, | ||||
|  | ||||
| 		onDragend(e) { | ||||
| 			this.dragging = false; | ||||
| 		}, | ||||
|  | ||||
| 		onDragover(e) { | ||||
| 			// 自分自身がドラッグされている場合 | ||||
| 			if (this.dragging) { | ||||
| 				// 自分自身にはドロップさせない | ||||
| 				e.dataTransfer.dropEffect = 'none'; | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; | ||||
|  | ||||
| 			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; | ||||
| 		}, | ||||
|  | ||||
| 		onDragenter() { | ||||
| 			if (!this.dragging) this.draghover = true; | ||||
| 		}, | ||||
|  | ||||
| 		onDragleave() { | ||||
| 			this.draghover = false; | ||||
| 		}, | ||||
|  | ||||
| 		onDrop(e) { | ||||
| 			this.draghover = false; | ||||
|  | ||||
| 			const id = e.dataTransfer.getData('mk-deck-column'); | ||||
| 			if (id != null && id != '') { | ||||
| 				this.$store.dispatch('settings/swapDeckColumn', { | ||||
| 					a: this.column.id, | ||||
| 					b: id | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	$header-height = 42px | ||||
|  | ||||
| 	width 330px | ||||
| 	min-width 330px | ||||
| 	height 100% | ||||
| 	background isDark ? #282C37 : #fff | ||||
| 	border-radius 6px | ||||
| 	box-shadow 0 2px 16px rgba(#000, 0.1) | ||||
| 	overflow hidden | ||||
|  | ||||
| 	&.draghover | ||||
| 	&.dragging | ||||
| 		box-shadow 0 0 0 2px rgba($theme-color, 0.7) | ||||
|  | ||||
| 	&:not(.active) | ||||
| 		flex-basis $header-height | ||||
| 		min-height $header-height | ||||
|  | ||||
| 	&:not(.isStacked).narrow | ||||
| 		width 285px | ||||
| 		min-width 285px | ||||
|  | ||||
| 	&.naked | ||||
| 		background rgba(#000, isDark ? 0.25 : 0.1) | ||||
|  | ||||
| 		> header | ||||
| 			background transparent | ||||
| 			box-shadow none | ||||
|  | ||||
| 			if !isDark | ||||
| 				> button | ||||
| 					color #bbb | ||||
|  | ||||
| 	> header | ||||
| 		z-index 1 | ||||
| 		line-height $header-height | ||||
| 		padding 0 16px | ||||
| 		font-size 14px | ||||
| 		color isDark ? #e3e5e8 : #888 | ||||
| 		background isDark ? #313543 : #fff | ||||
| 		box-shadow 0 1px rgba(#000, 0.15) | ||||
| 		cursor pointer | ||||
|  | ||||
| 		&, * | ||||
| 			user-select none | ||||
|  | ||||
| 		*:not(button) | ||||
| 			pointer-events none | ||||
|  | ||||
| 		&.indicate | ||||
| 			box-shadow 0 3px 0 0 $theme-color | ||||
|  | ||||
| 		> span | ||||
| 			[data-fa] | ||||
| 				margin-right 8px | ||||
|  | ||||
| 		> .count | ||||
| 			margin-left 4px | ||||
| 			opacity 0.5 | ||||
|  | ||||
| 		> button | ||||
| 			position absolute | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			width $header-height | ||||
| 			line-height $header-height | ||||
| 			font-size 16px | ||||
| 			color isDark ? #9baec8 : #ccc | ||||
|  | ||||
| 			&:hover | ||||
| 				color isDark ? #b2c1d5 : #aaa | ||||
|  | ||||
| 			&:active | ||||
| 				color isDark ? #b2c1d5 : #999 | ||||
|  | ||||
| 	> div | ||||
| 		height "calc(100% - %s)" % $header-height | ||||
| 		overflow auto | ||||
| 		overflow-x hidden | ||||
|  | ||||
| .dnpfarvgbnfmyzbdquhhzyxcmstpdqzs[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .dnpfarvgbnfmyzbdquhhzyxcmstpdqzs:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										118
									
								
								src/client/app/desktop/views/pages/deck/deck.list-tl.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/client/app/desktop/views/pages/deck/deck.list-tl.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| <template> | ||||
| 	<x-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import { UserListStream } from '../../../../common/scripts/streaming/user-list'; | ||||
|  | ||||
| const fetchLimit = 10; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNotes | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		list: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		mediaOnly: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		mediaOnly() { | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		if (this.connection) this.connection.close(); | ||||
| 		this.connection = new UserListStream((this as any).os, this.$store.state.i, this.list.id); | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 		this.connection.on('userAdded', this.onUserAdded); | ||||
| 		this.connection.on('userRemoved', this.onUserRemoved); | ||||
|  | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.close(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api('notes/user-list-timeline', { | ||||
| 					listId: this.list.id, | ||||
| 					limit: fetchLimit + 1, | ||||
| 					mediaOnly: this.mediaOnly, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 					this.$emit('loaded'); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
| 		more() { | ||||
| 			this.moreFetching = true; | ||||
|  | ||||
| 			const promise = (this as any).api('notes/user-list-timeline', { | ||||
| 				listId: this.list.id, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				mediaOnly: this.mediaOnly, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		}, | ||||
| 		onNote(note) { | ||||
| 			if (this.mediaOnly && note.media.length == 0) return; | ||||
|  | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}, | ||||
| 		onUserAdded() { | ||||
| 			this.fetch(); | ||||
| 		}, | ||||
| 		onUserRemoved() { | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										153
									
								
								src/client/app/desktop/views/pages/deck/deck.note.sub.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/client/app/desktop/views/pages/deck/deck.note.sub.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| <template> | ||||
| <div class="fnlfosztlhtptnongximhlbykxblytcq"> | ||||
| 	<mk-avatar class="avatar" :user="note.user"/> | ||||
| 	<div class="main"> | ||||
| 		<header> | ||||
| 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> | ||||
| 			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span> | ||||
| 			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span> | ||||
| 			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span> | ||||
| 			<span class="username"><mk-acct :user="note.user"/></span> | ||||
| 			<div class="info"> | ||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> | ||||
| 				<router-link class="created-at" :to="note | notePage"> | ||||
| 					<mk-time :time="note.createdAt"/> | ||||
| 				</router-link> | ||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> | ||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> | ||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> | ||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> | ||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> | ||||
| 				</span> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<div class="body"> | ||||
| 			<mk-sub-note-content class="text" :note="note"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		// TODO | ||||
| 		truncate: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	display flex | ||||
| 	padding 16px | ||||
| 	font-size 10px | ||||
| 	background isDark ? #21242d : #fcfcfc | ||||
|  | ||||
| 	&.smart | ||||
| 		> .main | ||||
| 			width 100% | ||||
|  | ||||
| 			> header | ||||
| 				align-items center | ||||
|  | ||||
| 	> .avatar | ||||
| 		flex-shrink 0 | ||||
| 		display block | ||||
| 		margin 0 8px 0 0 | ||||
| 		width 38px | ||||
| 		height 38px | ||||
| 		border-radius 8px | ||||
|  | ||||
| 	> .main | ||||
| 		flex 1 | ||||
| 		min-width 0 | ||||
|  | ||||
| 		> header | ||||
| 			display flex | ||||
| 			align-items baseline | ||||
| 			margin-bottom 2px | ||||
| 			white-space nowrap | ||||
|  | ||||
| 			> .avatar | ||||
| 				flex-shrink 0 | ||||
| 				margin-right 8px | ||||
| 				width 18px | ||||
| 				height 18px | ||||
| 				border-radius 100% | ||||
|  | ||||
| 			> .name | ||||
| 				display block | ||||
| 				margin 0 0.5em 0 0 | ||||
| 				padding 0 | ||||
| 				overflow hidden | ||||
| 				color isDark ? #fff : #607073 | ||||
| 				font-size 1em | ||||
| 				font-weight 700 | ||||
| 				text-align left | ||||
| 				text-decoration none | ||||
| 				text-overflow ellipsis | ||||
|  | ||||
| 				&:hover | ||||
| 					text-decoration underline | ||||
|  | ||||
| 			> .is-admin | ||||
| 			> .is-bot | ||||
| 			> .is-cat | ||||
| 				align-self center | ||||
| 				margin 0 0.5em 0 0 | ||||
| 				padding 1px 5px | ||||
| 				font-size 0.8em | ||||
| 				color isDark ? #758188 : #aaa | ||||
| 				border solid 1px isDark ? #57616f : #ddd | ||||
| 				border-radius 3px | ||||
|  | ||||
| 				&.is-admin | ||||
| 					border-color isDark ? #d42c41 : #f56a7b | ||||
| 					color isDark ? #d42c41 : #f56a7b | ||||
|  | ||||
| 			> .username | ||||
| 				text-align left | ||||
| 				margin 0 | ||||
| 				color isDark ? #606984 : #d1d8da | ||||
|  | ||||
| 			> .info | ||||
| 				margin-left auto | ||||
| 				font-size 0.9em | ||||
|  | ||||
| 				> * | ||||
| 					color isDark ? #606984 : #b2b8bb | ||||
|  | ||||
| 				> .mobile | ||||
| 					margin-right 6px | ||||
|  | ||||
| 				> .visibility | ||||
| 					margin-left 6px | ||||
|  | ||||
| 		> .body | ||||
|  | ||||
| 			> .text | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color isDark ? #959ba7 : #717171 | ||||
|  | ||||
| 				pre | ||||
| 					max-height 120px | ||||
| 					font-size 80% | ||||
|  | ||||
| .fnlfosztlhtptnongximhlbykxblytcq[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .fnlfosztlhtptnongximhlbykxblytcq:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										513
									
								
								src/client/app/desktop/views/pages/deck/deck.note.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										513
									
								
								src/client/app/desktop/views/pages/deck/deck.note.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,513 @@ | ||||
| <template> | ||||
| <div class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }"> | ||||
| 	<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> | ||||
| 		<x-sub :note="p.reply"/> | ||||
| 	</div> | ||||
| 	<div class="renote" v-if="isRenote"> | ||||
| 		<mk-avatar class="avatar" :user="note.user"/> | ||||
| 		%fa:retweet% | ||||
| 		<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> | ||||
| 		<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> | ||||
| 		<span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> | ||||
| 		<mk-time :time="note.createdAt"/> | ||||
| 	</div> | ||||
| 	<article> | ||||
| 		<mk-avatar class="avatar" :user="p.user"/> | ||||
| 		<div class="main"> | ||||
| 			<header> | ||||
| 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> | ||||
| 				<span class="is-admin" v-if="p.user.isAdmin">admin</span> | ||||
| 				<span class="is-bot" v-if="p.user.isBot">bot</span> | ||||
| 				<span class="is-cat" v-if="p.user.isCat">cat</span> | ||||
| 				<span class="username"><mk-acct :user="p.user"/></span> | ||||
| 				<div class="info"> | ||||
| 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> | ||||
| 					<router-link class="created-at" :to="p | notePage"> | ||||
| 						<mk-time :time="p.createdAt"/> | ||||
| 					</router-link> | ||||
| 					<span class="visibility" v-if="p.visibility != 'public'"> | ||||
| 						<template v-if="p.visibility == 'home'">%fa:home%</template> | ||||
| 						<template v-if="p.visibility == 'followers'">%fa:unlock%</template> | ||||
| 						<template v-if="p.visibility == 'specified'">%fa:envelope%</template> | ||||
| 						<template v-if="p.visibility == 'private'">%fa:lock%</template> | ||||
| 					</span> | ||||
| 				</div> | ||||
| 			</header> | ||||
| 			<div class="body"> | ||||
| 				<p v-if="p.cw != null" class="cw"> | ||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 					<span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span> | ||||
| 				</p> | ||||
| 				<div class="content" v-show="p.cw == null || showContent"> | ||||
| 					<div class="text"> | ||||
| 						<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> | ||||
| 						<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> | ||||
| 						<a class="reply" v-if="p.reply">%fa:reply%</a> | ||||
| 						<mk-note-html v-if="p.text && !canHideText(p)" :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> | ||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||
| 					<div class="tags" v-if="p.tags && p.tags.length > 0"> | ||||
| 						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> | ||||
| 					</div> | ||||
| 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 					<div class="renote" v-if="p.renote"> | ||||
| 						<mk-note-preview :note="p.renote"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> | ||||
| 			</div> | ||||
| 			<footer> | ||||
| 				<mk-reactions-viewer :note="p" ref="reactionsViewer"/> | ||||
| 				<button @click="reply"> | ||||
| 					<template v-if="p.reply">%fa:reply-all%</template> | ||||
| 					<template v-else>%fa:reply%</template> | ||||
| 				</button> | ||||
| 				<button @click="renote" title="Renote">%fa:retweet%</button> | ||||
| 				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">%fa:plus%</button> | ||||
| 				<button class="menu" @click="menu" ref="menuButton">%fa:ellipsis-h%</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 	</article> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parse from '../../../../../../text/parse'; | ||||
| import canHideText from '../../../../common/scripts/can-hide-text'; | ||||
|  | ||||
| import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './deck.note.sub.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSub | ||||
| 	}, | ||||
|  | ||||
| 	props: ['note'], | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
|  | ||||
| 		p(): any { | ||||
| 			return this.isRenote ? this.note.renote : this.note; | ||||
| 		}, | ||||
|  | ||||
| 		urls(): string[] { | ||||
| 			if (this.p.text) { | ||||
| 				const ast = parse(this.p.text); | ||||
| 				return ast | ||||
| 					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) | ||||
| 					.map(t => t.url); | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.capture(true); | ||||
|  | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.on('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.decapture(true); | ||||
|  | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('_connected_', this.onStreamConnected); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		canHideText, | ||||
|  | ||||
| 		capture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'capture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		decapture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'decapture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		onStreamConnected() { | ||||
| 			this.capture(); | ||||
| 		}, | ||||
|  | ||||
| 		onStreamNoteUpdated(data) { | ||||
| 			const note = data.note; | ||||
| 			if (note.id == this.note.id) { | ||||
| 				this.$emit('update:note', note); | ||||
| 			} else if (note.id == this.note.renoteId) { | ||||
| 				this.note.renote = note; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		reply() { | ||||
| 			(this as any).apis.post({ | ||||
| 				reply: this.p | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		renote() { | ||||
| 			(this as any).apis.post({ | ||||
| 				renote: this.p | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		react() { | ||||
| 			(this as any).os.new(MkReactionPicker, { | ||||
| 				source: this.$refs.reactButton, | ||||
| 				note: this.p, | ||||
| 				compact: true | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		menu() { | ||||
| 			(this as any).os.new(MkNoteMenu, { | ||||
| 				source: this.$refs.menuButton, | ||||
| 				note: this.p, | ||||
| 				compact: true | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	font-size 12px | ||||
| 	border-bottom solid 1px isDark ? #1c2023 : #eaeaea | ||||
|  | ||||
| 	&:last-of-type | ||||
| 		border-bottom none | ||||
|  | ||||
| 	&.smart | ||||
| 		> article | ||||
| 			> .main | ||||
| 				> header | ||||
| 					align-items center | ||||
| 					margin-bottom 4px | ||||
|  | ||||
| 	> .renote | ||||
| 		display flex | ||||
| 		align-items center | ||||
| 		padding 8px 16px | ||||
| 		line-height 28px | ||||
| 		white-space pre | ||||
| 		color #9dbb00 | ||||
| 		background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%) | ||||
|  | ||||
| 		.avatar | ||||
| 			flex-shrink 0 | ||||
| 			display inline-block | ||||
| 			width 20px | ||||
| 			height 20px | ||||
| 			margin 0 8px 0 0 | ||||
| 			border-radius 6px | ||||
|  | ||||
| 		[data-fa] | ||||
| 			margin-right 4px | ||||
|  | ||||
| 		> span | ||||
| 			flex-shrink 0 | ||||
|  | ||||
| 			&:last-of-type | ||||
| 				margin-right 8px | ||||
|  | ||||
| 		.name | ||||
| 			overflow hidden | ||||
| 			flex-shrink 1 | ||||
| 			text-overflow ellipsis | ||||
| 			white-space nowrap | ||||
| 			font-weight bold | ||||
|  | ||||
| 		> .mk-time | ||||
| 			display block | ||||
| 			margin-left auto | ||||
| 			flex-shrink 0 | ||||
| 			font-size 0.9em | ||||
|  | ||||
| 		& + article | ||||
| 			padding-top 8px | ||||
|  | ||||
| 	> article | ||||
| 		display flex | ||||
| 		padding 16px 16px 9px | ||||
|  | ||||
| 		> .avatar | ||||
| 			flex-shrink 0 | ||||
| 			display block | ||||
| 			margin 0 10px 8px 0 | ||||
| 			width 42px | ||||
| 			height 42px | ||||
| 			border-radius 6px | ||||
| 			//position -webkit-sticky | ||||
| 			//position sticky | ||||
| 			//top 62px | ||||
|  | ||||
| 		> .main | ||||
| 			flex 1 | ||||
| 			min-width 0 | ||||
|  | ||||
| 			> header | ||||
| 				display flex | ||||
| 				align-items baseline | ||||
| 				white-space nowrap | ||||
|  | ||||
| 				> .avatar | ||||
| 					flex-shrink 0 | ||||
| 					margin-right 8px | ||||
| 					width 20px | ||||
| 					height 20px | ||||
| 					border-radius 100% | ||||
|  | ||||
| 				> .name | ||||
| 					display block | ||||
| 					margin 0 0.5em 0 0 | ||||
| 					padding 0 | ||||
| 					overflow hidden | ||||
| 					color isDark ? #fff : #627079 | ||||
| 					font-weight bold | ||||
| 					text-decoration none | ||||
| 					text-overflow ellipsis | ||||
|  | ||||
| 				> .is-admin | ||||
| 				> .is-bot | ||||
| 				> .is-cat | ||||
| 					align-self center | ||||
| 					margin 0 0.5em 0 0 | ||||
| 					padding 1px 6px | ||||
| 					font-size 0.8em | ||||
| 					color isDark ? #758188 : #aaa | ||||
| 					border solid 1px isDark ? #57616f : #ddd | ||||
| 					border-radius 3px | ||||
|  | ||||
| 					&.is-admin | ||||
| 						border-color isDark ? #d42c41 : #f56a7b | ||||
| 						color isDark ? #d42c41 : #f56a7b | ||||
|  | ||||
| 				> .username | ||||
| 					margin 0 0.5em 0 0 | ||||
| 					overflow hidden | ||||
| 					text-overflow ellipsis | ||||
| 					color isDark ? #606984 : #ccc | ||||
|  | ||||
| 				> .info | ||||
| 					margin-left auto | ||||
| 					font-size 0.9em | ||||
|  | ||||
| 					> * | ||||
| 						color isDark ? #606984 : #c0c0c0 | ||||
|  | ||||
| 					> .mobile | ||||
| 						margin-right 6px | ||||
|  | ||||
| 					> .visibility | ||||
| 						margin-left 6px | ||||
|  | ||||
| 			> .body | ||||
|  | ||||
| 				> .cw | ||||
| 					cursor default | ||||
| 					display block | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					overflow-wrap break-word | ||||
| 					color isDark ? #fff : #717171 | ||||
|  | ||||
| 					> .text | ||||
| 						margin-right 8px | ||||
|  | ||||
| 					> .toggle | ||||
| 						display inline-block | ||||
| 						padding 4px 8px | ||||
| 						font-size 0.7em | ||||
| 						color isDark ? #393f4f : #fff | ||||
| 						background isDark ? #687390 : #b1b9c1 | ||||
| 						border-radius 2px | ||||
| 						cursor pointer | ||||
| 						user-select none | ||||
|  | ||||
| 						&:hover | ||||
| 							background isDark ? #707b97 : #bbc4ce | ||||
|  | ||||
| 				> .content | ||||
|  | ||||
| 					> .text | ||||
| 						display block | ||||
| 						margin 0 | ||||
| 						padding 0 | ||||
| 						overflow-wrap break-word | ||||
| 						color isDark ? #fff : #717171 | ||||
|  | ||||
| 						>>> .title | ||||
| 							display block | ||||
| 							margin-bottom 4px | ||||
| 							padding 4px | ||||
| 							font-size 90% | ||||
| 							text-align center | ||||
| 							background isDark ? #2f3944 : #eef1f3 | ||||
| 							border-radius 4px | ||||
|  | ||||
| 						>>> .code | ||||
| 							margin 8px 0 | ||||
|  | ||||
| 						>>> .quote | ||||
| 							margin 8px | ||||
| 							padding 6px 12px | ||||
| 							color isDark ? #6f808e : #aaa | ||||
| 							border-left solid 3px isDark ? #637182 : #eee | ||||
|  | ||||
| 						> .reply | ||||
| 							margin-right 8px | ||||
| 							color isDark ? #99abbf : #717171 | ||||
|  | ||||
| 						> .rp | ||||
| 							margin-left 4px | ||||
| 							font-style oblique | ||||
| 							color #a0bf46 | ||||
|  | ||||
| 						[data-is-me]:after | ||||
| 							content "you" | ||||
| 							padding 0 4px | ||||
| 							margin-left 4px | ||||
| 							font-size 80% | ||||
| 							color $theme-color-foreground | ||||
| 							background $theme-color | ||||
| 							border-radius 4px | ||||
|  | ||||
| 					.mk-url-preview | ||||
| 						margin-top 8px | ||||
|  | ||||
| 					> .tags | ||||
| 						margin 4px 0 0 0 | ||||
|  | ||||
| 						> * | ||||
| 							display inline-block | ||||
| 							margin 0 8px 0 0 | ||||
| 							padding 2px 8px 2px 16px | ||||
| 							font-size 90% | ||||
| 							color #8d969e | ||||
| 							background isDark ? #313543 : #edf0f3 | ||||
| 							border-radius 4px | ||||
|  | ||||
| 							&:before | ||||
| 								content "" | ||||
| 								display block | ||||
| 								position absolute | ||||
| 								top 0 | ||||
| 								bottom 0 | ||||
| 								left 4px | ||||
| 								width 8px | ||||
| 								height 8px | ||||
| 								margin auto 0 | ||||
| 								background isDark ? #282c37 : #fff | ||||
| 								border-radius 100% | ||||
|  | ||||
| 					> .media | ||||
| 						> img | ||||
| 							display block | ||||
| 							max-width 100% | ||||
|  | ||||
| 					> .location | ||||
| 						margin 4px 0 | ||||
| 						font-size 12px | ||||
| 						color #ccc | ||||
|  | ||||
| 					> .map | ||||
| 						width 100% | ||||
| 						height 200px | ||||
|  | ||||
| 						&:empty | ||||
| 							display none | ||||
|  | ||||
| 					> .mk-poll | ||||
| 						font-size 80% | ||||
|  | ||||
| 					> .renote | ||||
| 						margin 8px 0 | ||||
|  | ||||
| 						> .mk-note-preview | ||||
| 							padding 16px | ||||
| 							border dashed 1px isDark ? #4e945e : #c0dac6 | ||||
| 							border-radius 8px | ||||
|  | ||||
| 				> .app | ||||
| 					font-size 12px | ||||
| 					color #ccc | ||||
|  | ||||
| 			> footer | ||||
| 				> button | ||||
| 					margin 0 | ||||
| 					padding 8px | ||||
| 					background transparent | ||||
| 					border none | ||||
| 					box-shadow none | ||||
| 					font-size 1em | ||||
| 					color isDark ? #606984 : #ddd | ||||
| 					cursor pointer | ||||
|  | ||||
| 					&:not(:last-child) | ||||
| 						margin-right 28px | ||||
|  | ||||
| 					&:hover | ||||
| 						color isDark ? #9198af : #666 | ||||
|  | ||||
| 					> .count | ||||
| 						display inline | ||||
| 						margin 0 0 0 8px | ||||
| 						color #999 | ||||
|  | ||||
| 					&.reacted | ||||
| 						color $theme-color | ||||
|  | ||||
| .zyjjkidcqjnlegkqebitfviomuqmseqk[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .zyjjkidcqjnlegkqebitfviomuqmseqk:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										237
									
								
								src/client/app/desktop/views/pages/deck/deck.notes.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								src/client/app/desktop/views/pages/deck/deck.notes.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | ||||
| <template> | ||||
| <div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> | ||||
| 	<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> | ||||
|  | ||||
| 	<div v-if="!fetching && requestInitPromise != null"> | ||||
| 		<p>%i18n:@error%</p> | ||||
| 		<button @click="resolveInitPromise">%i18n:@retry%</button> | ||||
| 	</div> | ||||
|  | ||||
| 	<transition-group name="mk-notes" class="transition"> | ||||
| 		<template v-for="(note, i) in _notes"> | ||||
| 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | ||||
| 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | ||||
| 				<span>%fa:angle-up%{{ note._datetext }}</span> | ||||
| 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
| 		</template> | ||||
| 	</transition-group> | ||||
|  | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">%i18n:@load-more%</template> | ||||
| 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template> | ||||
| 		</button> | ||||
| 	</footer> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import XNote from './deck.note.vue'; | ||||
|  | ||||
| const displayLimit = 30; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNote | ||||
| 	}, | ||||
|  | ||||
| 	inject: ['column', 'isScrollTop', 'count'], | ||||
|  | ||||
| 	props: { | ||||
| 		more: { | ||||
| 			type: Function, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			rootEl: null, | ||||
| 			requestInitPromise: null as () => Promise<any[]>, | ||||
| 			notes: [], | ||||
| 			queue: [], | ||||
| 			fetching: true, | ||||
| 			moreFetching: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		_notes(): any[] { | ||||
| 			return (this.notes as any).map(note => { | ||||
| 				const date = new Date(note.createdAt).getDate(); | ||||
| 				const month = new Date(note.createdAt).getMonth() + 1; | ||||
| 				note._date = date; | ||||
| 				note._datetext = `${month}月 ${date}日`; | ||||
| 				return note; | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		queue(q) { | ||||
| 			this.count(q.length); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.column.$on('top', this.onTop); | ||||
| 		this.column.$on('bottom', this.onBottom); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.column.$off('top', this.onTop); | ||||
| 		this.column.$off('bottom', this.onBottom); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			(this.$el as any).children[0].focus(); | ||||
| 		}, | ||||
|  | ||||
| 		onNoteUpdated(i, note) { | ||||
| 			Vue.set((this as any).notes, i, note); | ||||
| 		}, | ||||
|  | ||||
| 		init(promiseGenerator: () => Promise<any[]>) { | ||||
| 			this.requestInitPromise = promiseGenerator; | ||||
| 			this.resolveInitPromise(); | ||||
| 		}, | ||||
|  | ||||
| 		resolveInitPromise() { | ||||
| 			this.queue = []; | ||||
| 			this.notes = []; | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			const promise = this.requestInitPromise(); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				this.notes = notes; | ||||
| 				this.requestInitPromise = null; | ||||
| 				this.fetching = false; | ||||
| 			}, e => { | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		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; | ||||
|  | ||||
| 			if (this.$store.state.settings.showMyRenotes === false) { | ||||
| 				if (isMyNote && isPureRenote) { | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (this.$store.state.settings.showRenotedMyNotes === false) { | ||||
| 				if (isPureRenote && (note.renote.userId == this.$store.state.i.id)) { | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
| 			//#endregion | ||||
|  | ||||
| 			if (this.isScrollTop()) { | ||||
| 				// Prepend the note | ||||
| 				this.notes.unshift(note); | ||||
|  | ||||
| 				// オーバーフローしたら古い投稿は捨てる | ||||
| 				if (this.notes.length >= displayLimit) { | ||||
| 					this.notes = this.notes.slice(0, displayLimit); | ||||
| 				} | ||||
| 			} else { | ||||
| 				this.queue.push(note); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		append(note) { | ||||
| 			this.notes.push(note); | ||||
| 		}, | ||||
|  | ||||
| 		tail() { | ||||
| 			return this.notes[this.notes.length - 1]; | ||||
| 		}, | ||||
|  | ||||
| 		releaseQueue() { | ||||
| 			this.queue.forEach(n => this.prepend(n, true)); | ||||
| 			this.queue = []; | ||||
| 		}, | ||||
|  | ||||
| 		async loadMore() { | ||||
| 			if (this.more == null) return; | ||||
| 			if (this.moreFetching) return; | ||||
|  | ||||
| 			this.moreFetching = true; | ||||
| 			await this.more(); | ||||
| 			this.moreFetching = false; | ||||
| 		}, | ||||
|  | ||||
| 		onTop() { | ||||
| 			this.releaseQueue(); | ||||
| 		}, | ||||
|  | ||||
| 		onBottom() { | ||||
| 			this.loadMore(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	.transition | ||||
| 		.mk-notes-enter | ||||
| 		.mk-notes-leave-to | ||||
| 			opacity 0 | ||||
| 			transform translateY(-30px) | ||||
|  | ||||
| 		> * | ||||
| 			transition transform .3s ease, opacity .3s ease | ||||
|  | ||||
| 		> .date | ||||
| 			display block | ||||
| 			margin 0 | ||||
| 			line-height 32px | ||||
| 			font-size 14px | ||||
| 			text-align center | ||||
| 			color isDark ? #666b79 : #aaa | ||||
| 			background isDark ? #242731 : #fdfdfd | ||||
| 			border-bottom solid 1px isDark ? #1c2023 : #eaeaea | ||||
|  | ||||
| 			span | ||||
| 				margin 0 16px | ||||
|  | ||||
| 			[data-fa] | ||||
| 				margin-right 8px | ||||
|  | ||||
| 	> footer | ||||
| 		> button | ||||
| 			display block | ||||
| 			margin 0 | ||||
| 			padding 16px | ||||
| 			width 100% | ||||
| 			text-align center | ||||
| 			color #ccc | ||||
| 			background isDark ? #282C37 : #fff | ||||
| 			border-top solid 1px isDark ? #1c2023 : #eaeaea | ||||
| 			border-bottom-left-radius 6px | ||||
| 			border-bottom-right-radius 6px | ||||
|  | ||||
| 			&:hover | ||||
| 				background isDark ? #2e3440 : #f5f5f5 | ||||
|  | ||||
| 			&:active | ||||
| 				background isDark ? #21242b : #eee | ||||
|  | ||||
| .eamppglmnmimdhrlzhplwpvyeaqmmhxu[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .eamppglmnmimdhrlzhplwpvyeaqmmhxu:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										179
									
								
								src/client/app/desktop/views/pages/deck/deck.notification.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/client/app/desktop/views/pages/deck/deck.notification.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| <template> | ||||
| <div class="dsfykdcjpuwfvpefwufddclpjhzktmpw"> | ||||
| 	<div class="notification reaction" v-if="notification.type == 'reaction'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
| 				<mk-reaction-icon :reaction="notification.reaction"/> | ||||
| 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> | ||||
| 				<mk-time :time="notification.createdAt"/> | ||||
| 			</header> | ||||
| 			<router-link class="note-ref" :to="notification.note | notePage"> | ||||
| 				%fa:quote-left%{{ getNoteSummary(notification.note) }} | ||||
| 				%fa:quote-right% | ||||
| 			</router-link> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="notification renote" v-if="notification.type == 'renote'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
| 				%fa:retweet% | ||||
| 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> | ||||
| 				<mk-time :time="notification.createdAt"/> | ||||
| 			</header> | ||||
| 			<router-link class="note-ref" :to="notification.note | notePage"> | ||||
| 				%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% | ||||
| 			</router-link> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="notification follow" v-if="notification.type == 'follow'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
| 				%fa:user-plus% | ||||
| 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> | ||||
| 				<mk-time :time="notification.createdAt"/> | ||||
| 			</header> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
| 				%fa:user-clock% | ||||
| 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> | ||||
| 				<mk-time :time="notification.createdAt"/> | ||||
| 			</header> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
| 				%fa:chart-pie% | ||||
| 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> | ||||
| 				<mk-time :time="notification.createdAt"/> | ||||
| 			</header> | ||||
| 			<router-link class="note-ref" :to="notification.note | notePage"> | ||||
| 				%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% | ||||
| 			</router-link> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<template v-if="notification.type == 'quote'"> | ||||
| 		<x-note :note="notification.note" @update:note="onNoteUpdated"/> | ||||
| 	</template> | ||||
|  | ||||
| 	<template v-if="notification.type == 'reply'"> | ||||
| 		<x-note :note="notification.note" @update:note="onNoteUpdated"/> | ||||
| 	</template> | ||||
|  | ||||
| 	<template v-if="notification.type == 'mention'"> | ||||
| 		<x-note :note="notification.note" @update:note="onNoteUpdated"/> | ||||
| 	</template> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import getNoteSummary from '../../../../../../renderers/get-note-summary'; | ||||
| import XNote from './deck.note.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNote | ||||
| 	}, | ||||
| 	props: ['notification'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			getNoteSummary | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onNoteUpdated(note) { | ||||
| 			switch (this.notification.type) { | ||||
| 				case 'quote': | ||||
| 				case 'reply': | ||||
| 				case 'mention': | ||||
| 					Vue.set(this.notification, 'note', note); | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	> .notification | ||||
| 		padding 16px | ||||
| 		font-size 12px | ||||
| 		overflow-wrap break-word | ||||
|  | ||||
| 		&:after | ||||
| 			content "" | ||||
| 			display block | ||||
| 			clear both | ||||
|  | ||||
| 		> .avatar | ||||
| 			display block | ||||
| 			float left | ||||
| 			width 36px | ||||
| 			height 36px | ||||
| 			border-radius 6px | ||||
|  | ||||
| 		> div | ||||
| 			float right | ||||
| 			width calc(100% - 36px) | ||||
| 			padding-left 8px | ||||
|  | ||||
| 			> header | ||||
| 				display flex | ||||
| 				align-items baseline | ||||
| 				white-space nowrap | ||||
|  | ||||
| 				i, .mk-reaction-icon | ||||
| 					margin-right 4px | ||||
|  | ||||
| 				> .mk-time | ||||
| 					margin-left auto | ||||
| 					color isDark ? #606984 : #c0c0c0 | ||||
| 					font-size 0.9em | ||||
|  | ||||
| 			> .note-preview | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| 			> .note-ref | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| 				[data-fa] | ||||
| 					font-size 1em | ||||
| 					font-weight normal | ||||
| 					font-style normal | ||||
| 					display inline-block | ||||
| 					margin-right 3px | ||||
|  | ||||
| 		&.renote | ||||
| 			> div > header i | ||||
| 				color #77B255 | ||||
|  | ||||
| 		&.follow | ||||
| 			> div > header i | ||||
| 				color #53c7ce | ||||
|  | ||||
| 		&.receiveFollowRequest | ||||
| 			> div > header i | ||||
| 				color #888 | ||||
|  | ||||
| .dsfykdcjpuwfvpefwufddclpjhzktmpw[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .dsfykdcjpuwfvpefwufddclpjhzktmpw:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
| @@ -0,0 +1,38 @@ | ||||
| <template> | ||||
| <x-column :name="name" :column="column" :is-stacked="isStacked"> | ||||
| 	<span slot="header">%fa:bell R%{{ name }}</span> | ||||
|  | ||||
| 	<x-notifications/> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotifications from './deck.notifications.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XNotifications | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		name(): string { | ||||
| 			if (this.column.name) return this.column.name; | ||||
| 			return '%i18n:common.deck.notifications%'; | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										179
									
								
								src/client/app/desktop/views/pages/deck/deck.notifications.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/client/app/desktop/views/pages/deck/deck.notifications.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| <template> | ||||
| <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> | ||||
| 	<transition-group name="mk-notifications" class="transition notifications"> | ||||
| 		<template v-for="(notification, i) in _notifications"> | ||||
| 			<x-notification class="notification" :notification="notification" :key="notification.id"/> | ||||
| 			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> | ||||
| 				<span>%fa:angle-up%{{ notification._datetext }}</span> | ||||
| 				<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
| 		</template> | ||||
| 	</transition-group> | ||||
| 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> | ||||
| 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} | ||||
| 	</button> | ||||
| 	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> | ||||
| 	<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotification from './deck.notification.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNotification | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			fetchingMoreNotifications: false, | ||||
| 			notifications: [], | ||||
| 			moreNotifications: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		_notifications(): any[] { | ||||
| 			return (this.notifications as any).map(notification => { | ||||
| 				const date = new Date(notification.createdAt).getDate(); | ||||
| 				const month = new Date(notification.createdAt).getMonth() + 1; | ||||
| 				notification._date = date; | ||||
| 				notification._datetext = `${month}月 ${date}日`; | ||||
| 				return notification; | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
|  | ||||
| 		this.connection.on('notification', this.onNotification); | ||||
|  | ||||
| 		const max = 10; | ||||
|  | ||||
| 		(this as any).api('i/notifications', { | ||||
| 			limit: max + 1 | ||||
| 		}).then(notifications => { | ||||
| 			if (notifications.length == max + 1) { | ||||
| 				this.moreNotifications = true; | ||||
| 				notifications.pop(); | ||||
| 			} | ||||
|  | ||||
| 			this.notifications = notifications; | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('notification', this.onNotification); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetchMoreNotifications() { | ||||
| 			this.fetchingMoreNotifications = true; | ||||
|  | ||||
| 			const max = 30; | ||||
|  | ||||
| 			(this as any).api('i/notifications', { | ||||
| 				limit: max + 1, | ||||
| 				untilId: this.notifications[this.notifications.length - 1].id | ||||
| 			}).then(notifications => { | ||||
| 				if (notifications.length == max + 1) { | ||||
| 					this.moreNotifications = true; | ||||
| 					notifications.pop(); | ||||
| 				} else { | ||||
| 					this.moreNotifications = false; | ||||
| 				} | ||||
| 				this.notifications = this.notifications.concat(notifications); | ||||
| 				this.fetchingMoreNotifications = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 		onNotification(notification) { | ||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||
| 			this.connection.send({ | ||||
| 				type: 'read_notification', | ||||
| 				id: notification.id | ||||
| 			}); | ||||
|  | ||||
| 			this.notifications.unshift(notification); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
|  | ||||
| 	.transition | ||||
| 		.mk-notifications-enter | ||||
| 		.mk-notifications-leave-to | ||||
| 			opacity 0 | ||||
| 			transform translateY(-30px) | ||||
|  | ||||
| 		> * | ||||
| 			transition transform .3s ease, opacity .3s ease | ||||
|  | ||||
| 	> .notifications | ||||
|  | ||||
| 		> .notification:not(:last-child) | ||||
| 			border-bottom solid 1px isDark ? #1c2023 : #eaeaea | ||||
|  | ||||
| 		> .date | ||||
| 			display block | ||||
| 			margin 0 | ||||
| 			line-height 32px | ||||
| 			text-align center | ||||
| 			font-size 0.8em | ||||
| 			color isDark ? #666b79 : #aaa | ||||
| 			background isDark ? #242731 : #fdfdfd | ||||
| 			border-bottom solid 1px isDark ? #1c2023 : #eaeaea | ||||
|  | ||||
| 			span | ||||
| 				margin 0 16px | ||||
|  | ||||
| 			i | ||||
| 				margin-right 8px | ||||
|  | ||||
| 	> .more | ||||
| 		display block | ||||
| 		width 100% | ||||
| 		padding 16px | ||||
| 		color #555 | ||||
| 		border-top solid 1px rgba(#000, 0.05) | ||||
|  | ||||
| 		&:hover | ||||
| 			background rgba(#000, 0.025) | ||||
|  | ||||
| 		&:active | ||||
| 			background rgba(#000, 0.05) | ||||
|  | ||||
| 		&.fetching | ||||
| 			cursor wait | ||||
|  | ||||
| 		> [data-fa] | ||||
| 			margin-right 4px | ||||
|  | ||||
| 	> .empty | ||||
| 		margin 0 | ||||
| 		padding 16px | ||||
| 		text-align center | ||||
| 		color #aaa | ||||
|  | ||||
| 	> .loading | ||||
| 		margin 0 | ||||
| 		padding 16px | ||||
| 		text-align center | ||||
| 		color #aaa | ||||
|  | ||||
| 		> [data-fa] | ||||
| 			margin-right 4px | ||||
|  | ||||
| .oxynyeqmfvracxnglgulyqfgqxnxmehl[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .oxynyeqmfvracxnglgulyqfgqxnxmehl:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										75
									
								
								src/client/app/desktop/views/pages/deck/deck.tl-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/client/app/desktop/views/pages/deck/deck.tl-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <template> | ||||
| <x-column :menu="menu" :name="name" :column="column" :is-stacked="isStacked"> | ||||
| 	<span slot="header"> | ||||
| 		<template v-if="column.type == 'home'">%fa:home%</template> | ||||
| 		<template v-if="column.type == 'local'">%fa:R comments%</template> | ||||
| 		<template v-if="column.type == 'global'">%fa:globe%</template> | ||||
| 		<template v-if="column.type == 'list'">%fa:list%</template> | ||||
| 		<span>{{ name }}</span> | ||||
| 	</span> | ||||
|  | ||||
| 	<div class="editor" v-if="edit"> | ||||
| 		<mk-switch v-model="column.isMediaOnly" @change="onChangeSettings" text="%i18n:@is-media-only%"/> | ||||
| 		<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/> | ||||
| 	</div> | ||||
| 	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly"/> | ||||
| 	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly"/> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XTl from './deck.tl.vue'; | ||||
| import XListTl from './deck.list-tl.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XTl, | ||||
| 		XListTl | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			edit: false, | ||||
| 			menu: [{ | ||||
| 				content: '%fa:cog% %i18n:@edit%', | ||||
| 				onClick: () => { | ||||
| 					this.edit = !this.edit; | ||||
| 				} | ||||
| 			}] | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		name(): string { | ||||
| 			if (this.column.name) return this.column.name; | ||||
|  | ||||
| 			switch (this.column.type) { | ||||
| 				case 'home': return '%i18n:common.deck.home%'; | ||||
| 				case 'local': return '%i18n:common.deck.local%'; | ||||
| 				case 'global': return '%i18n:common.deck.global%'; | ||||
| 				case 'list': return this.column.list.title; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onChangeSettings(v) { | ||||
| 			this.$store.dispatch('settings/saveDeck'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										147
									
								
								src/client/app/desktop/views/pages/deck/deck.tl.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/client/app/desktop/views/pages/deck/deck.tl.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| <template> | ||||
| 	<x-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
|  | ||||
| const fetchLimit = 10; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNotes | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 			default: 'home' | ||||
| 		}, | ||||
| 		mediaOnly: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		mediaOnly() { | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		stream(): any { | ||||
| 			return this.src == 'home' | ||||
| 				? (this as any).os.stream | ||||
| 				: this.src == 'local' | ||||
| 					? (this as any).os.streams.localTimelineStream | ||||
| 					: (this as any).os.streams.globalTimelineStream; | ||||
| 		}, | ||||
|  | ||||
| 		endpoint(): string { | ||||
| 			return this.src == 'home' | ||||
| 				? 'notes/timeline' | ||||
| 				: this.src == 'local' | ||||
| 					? 'notes/local-timeline' | ||||
| 					: 'notes/global-timeline'; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.stream.getConnection(); | ||||
| 		this.connectionId = this.stream.use(); | ||||
|  | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 			this.connection.on('follow', this.onChangeFollowing); | ||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | ||||
| 		} | ||||
|  | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 			this.connection.off('follow', this.onChangeFollowing); | ||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | ||||
| 		} | ||||
| 		this.stream.dispose(this.connectionId); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api(this.endpoint, { | ||||
| 					limit: fetchLimit + 1, | ||||
| 					mediaOnly: this.mediaOnly, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 					this.$emit('loaded'); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
|  | ||||
| 		more() { | ||||
| 			this.moreFetching = true; | ||||
|  | ||||
| 			const promise = (this as any).api(this.endpoint, { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				mediaOnly: this.mediaOnly, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		}, | ||||
|  | ||||
| 		onNote(note) { | ||||
| 			if (this.mediaOnly && note.media.length == 0) return; | ||||
|  | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeFollowing() { | ||||
| 			this.fetch(); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										215
									
								
								src/client/app/desktop/views/pages/deck/deck.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/client/app/desktop/views/pages/deck/deck.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| <template> | ||||
| <mk-ui :class="$style.root"> | ||||
| 	<div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode"> | ||||
| 		<template v-for="ids in layout"> | ||||
| 			<div v-if="ids.length > 1" class="folder"> | ||||
| 				<template v-for="id, i in ids"> | ||||
| 					<x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true"/> | ||||
| 				</template> | ||||
| 			</div> | ||||
| 			<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])"/> | ||||
| 		</template> | ||||
| 		<button ref="add" @click="add" title="%i18n:common.deck.add-column%">%fa:plus%</button> | ||||
| 	</div> | ||||
| </mk-ui> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumnCore from './deck.column-core.vue'; | ||||
| import Menu from '../../../../common/views/components/menu.vue'; | ||||
| import MkUserListsWindow from '../../components/user-lists-window.vue'; | ||||
| import * as uuid from 'uuid'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumnCore | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		columns(): any[] { | ||||
| 			if (this.$store.state.settings.deck == null) return []; | ||||
| 			return this.$store.state.settings.deck.columns; | ||||
| 		}, | ||||
| 		layout(): any[] { | ||||
| 			if (this.$store.state.settings.deck == null) return []; | ||||
| 			if (this.$store.state.settings.deck.layout == null) return this.$store.state.settings.deck.columns.map(c => [c.id]); | ||||
| 			return this.$store.state.settings.deck.layout; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			getColumnVm: this.getColumnVm | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		if (this.$store.state.settings.deck == null) { | ||||
| 			const deck = { | ||||
| 				columns: [/*{ | ||||
| 					type: 'widgets', | ||||
| 					widgets: [] | ||||
| 				}, */{ | ||||
| 					id: uuid(), | ||||
| 					type: 'home' | ||||
| 				}, { | ||||
| 					id: uuid(), | ||||
| 					type: 'notifications' | ||||
| 				}, { | ||||
| 					id: uuid(), | ||||
| 					type: 'local' | ||||
| 				}, { | ||||
| 					id: uuid(), | ||||
| 					type: 'global' | ||||
| 				}] | ||||
| 			}; | ||||
|  | ||||
| 			deck.layout = deck.columns.map(c => [c.id]); | ||||
|  | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'deck', | ||||
| 				value: deck | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		// 互換性のため | ||||
| 		if (this.$store.state.settings.deck != null && this.$store.state.settings.deck.layout == null) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'deck', | ||||
| 				value: Object.assign({}, this.$store.state.settings.deck, { | ||||
| 					layout: this.$store.state.settings.deck.columns.map(c => [c.id]) | ||||
| 				}) | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		document.documentElement.style.overflow = 'hidden'; | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		document.documentElement.style.overflow = 'auto'; | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		getColumnVm(id) { | ||||
| 			return this.$refs[id][0]; | ||||
| 		}, | ||||
|  | ||||
| 		add() { | ||||
| 			this.os.new(Menu, { | ||||
| 				source: this.$refs.add, | ||||
| 				compact: true, | ||||
| 				items: [{ | ||||
| 					content: '%i18n:common.deck.home%', | ||||
| 					onClick: () => { | ||||
| 						this.$store.dispatch('settings/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'home' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					content: '%i18n:common.deck.local%', | ||||
| 					onClick: () => { | ||||
| 						this.$store.dispatch('settings/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'local' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					content: '%i18n:common.deck.global%', | ||||
| 					onClick: () => { | ||||
| 						this.$store.dispatch('settings/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'global' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					content: '%i18n:common.deck.list%', | ||||
| 					onClick: () => { | ||||
| 						const w = (this as any).os.new(MkUserListsWindow); | ||||
| 						w.$once('choosen', list => { | ||||
| 							this.$store.dispatch('settings/addDeckColumn', { | ||||
| 								id: uuid(), | ||||
| 								type: 'list', | ||||
| 								list: list | ||||
| 							}); | ||||
| 							w.close(); | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					content: '%i18n:common.deck.notifications%', | ||||
| 					onClick: () => { | ||||
| 						this.$store.dispatch('settings/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'notifications' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					content: '%i18n:common.deck.widgets%', | ||||
| 					onClick: () => { | ||||
| 						this.$store.dispatch('settings/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'widgets', | ||||
| 							widgets: [] | ||||
| 						}); | ||||
| 					} | ||||
| 				}] | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" module> | ||||
| .root | ||||
| 	height 100vh | ||||
| </style> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	display flex | ||||
| 	flex 1 | ||||
| 	padding 16px 0 16px 16px | ||||
| 	overflow auto | ||||
|  | ||||
| 	> div | ||||
| 		margin-right 8px | ||||
|  | ||||
| 		&:last-of-type | ||||
| 			margin-right 0 | ||||
|  | ||||
| 		&.folder | ||||
| 			display flex | ||||
| 			flex-direction column | ||||
|  | ||||
| 			> *:not(:last-child) | ||||
| 				margin-bottom 8px | ||||
|  | ||||
| 	> * | ||||
| 		&:first-child | ||||
| 			margin-left auto | ||||
|  | ||||
| 		&:last-child | ||||
| 			margin-right auto | ||||
|  | ||||
| 	> button | ||||
| 		padding 0 16px | ||||
| 		color isDark ? #93a0a5 : #888 | ||||
|  | ||||
| 		&:hover | ||||
| 			color isDark ? #b8c5ca : #777 | ||||
|  | ||||
| 		&:active | ||||
| 			color isDark ? #fff : #555 | ||||
|  | ||||
| .qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .qlvquzbjribqcaozciifydkngcwtyzje:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										161
									
								
								src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| <template> | ||||
| <x-column :menu="menu" :naked="true" :narrow="true" :name="name" :column="column" :is-stacked="isStacked" class="wtdtxvecapixsepjtcupubtsmometobz"> | ||||
| 	<span slot="header">%fa:calculator%{{ name }}</span> | ||||
|  | ||||
| 	<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq"> | ||||
| 		<template v-if="edit"> | ||||
| 			<header> | ||||
| 				<select v-model="widgetAdderSelected"> | ||||
| 					<option value="profile">%i18n:common.widgets.profile%</option> | ||||
| 					<option value="analog-clock">%i18n:common.widgets.analog-clock%</option> | ||||
| 					<option value="calendar">%i18n:common.widgets.calendar%</option> | ||||
| 					<option value="timemachine">%i18n:common.widgets.timemachine%</option> | ||||
| 					<option value="activity">%i18n:common.widgets.activity%</option> | ||||
| 					<option value="rss">%i18n:common.widgets.rss%</option> | ||||
| 					<option value="trends">%i18n:common.widgets.trends%</option> | ||||
| 					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option> | ||||
| 					<option value="slideshow">%i18n:common.widgets.slideshow%</option> | ||||
| 					<option value="version">%i18n:common.widgets.version%</option> | ||||
| 					<option value="broadcast">%i18n:common.widgets.broadcast%</option> | ||||
| 					<option value="notifications">%i18n:common.widgets.notifications%</option> | ||||
| 					<option value="users">%i18n:common.widgets.users%</option> | ||||
| 					<option value="polls">%i18n:common.widgets.polls%</option> | ||||
| 					<option value="post-form">%i18n:common.widgets.post-form%</option> | ||||
| 					<option value="messaging">%i18n:common.widgets.messaging%</option> | ||||
| 					<option value="memo">%i18n:common.widgets.memo%</option> | ||||
| 					<option value="server">%i18n:common.widgets.server%</option> | ||||
| 					<option value="donation">%i18n:common.widgets.donation%</option> | ||||
| 					<option value="nav">%i18n:common.widgets.nav%</option> | ||||
| 					<option value="tips">%i18n:common.widgets.tips%</option> | ||||
| 				</select> | ||||
| 				<button @click="addWidget">%i18n:@add%</button> | ||||
| 			</header> | ||||
| 			<x-draggable | ||||
| 				:list="column.widgets" | ||||
| 				:options="{ handle: '.handle', animation: 150 }" | ||||
| 				@sort="onWidgetSort" | ||||
| 			> | ||||
| 				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id"> | ||||
| 					<header> | ||||
| 						<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> | ||||
| 					</header> | ||||
| 					<div @click="widgetFunc(widget.id)"> | ||||
| 						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck"/> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import * as uuid from 'uuid'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XDraggable | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			edit: false, | ||||
| 			menu: null, | ||||
| 			widgetAdderSelected: null | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		name(): string { | ||||
| 			if (this.column.name) return this.column.name; | ||||
| 			return '%i18n:common.deck.widgets%'; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.menu = [{ | ||||
| 			content: '%fa:cog% %i18n:@edit%', | ||||
| 			onClick: () => { | ||||
| 				this.edit = !this.edit; | ||||
| 			} | ||||
| 		}]; | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		widgetFunc(id) { | ||||
| 			const w = this.$refs[id][0]; | ||||
| 			if (w.func) w.func(); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetSort() { | ||||
| 			this.saveWidgets(); | ||||
| 		}, | ||||
|  | ||||
| 		addWidget() { | ||||
| 			this.$store.dispatch('settings/addDeckWidget', { | ||||
| 				id: this.column.id, | ||||
| 				widget: { | ||||
| 					name: this.widgetAdderSelected, | ||||
| 					id: uuid(), | ||||
| 					data: {} | ||||
| 				} | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		removeWidget(widget) { | ||||
| 			this.$store.dispatch('settings/removeDeckWidget', { | ||||
| 				id: this.column.id, | ||||
| 				widget | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		saveWidgets() { | ||||
| 			this.$store.dispatch('settings/saveDeck'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	.gqpwvtwtprsbmnssnbicggtwqhmylhnq | ||||
| 		.widget, .customize-container | ||||
| 			margin 8px | ||||
|  | ||||
| 			&:first-of-type | ||||
| 				margin-top 0 | ||||
|  | ||||
| 		.customize-container | ||||
| 			background #fff | ||||
|  | ||||
| 		> header | ||||
| 			color isDark ? #fff : #000 | ||||
|  | ||||
| .wtdtxvecapixsepjtcupubtsmometobz[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .wtdtxvecapixsepjtcupubtsmometobz:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|  | ||||
| @@ -46,7 +46,7 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		document.addEventListener('keydown', this.onDocumentKeydown); | ||||
| 		window.addEventListener('scroll', this.onScroll); | ||||
| 		window.addEventListener('scroll', this.onScroll, { passive: true }); | ||||
|  | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|   | ||||
| @@ -37,7 +37,7 @@ export default Vue.extend({ | ||||
| 	mounted() { | ||||
| 		if (this.user.bannerUrl) { | ||||
| 			window.addEventListener('load', this.onScroll); | ||||
| 			window.addEventListener('scroll', this.onScroll); | ||||
| 			window.addEventListener('scroll', this.onScroll, { passive: true }); | ||||
| 			window.addEventListener('resize', this.onScroll); | ||||
| 		} | ||||
| 	}, | ||||
|   | ||||
| @@ -83,9 +83,7 @@ export default Vue.extend({ | ||||
| @import url(https://fonts.googleapis.com/earlyaccess/notosansjp.css); | ||||
|  | ||||
| root(isDark) | ||||
| 	display flex | ||||
| 	flex-direction column | ||||
| 	flex 1 | ||||
| 	min-height 100vh | ||||
| 	background-image isDark ? url('/assets/welcome-bg.dark.svg') : url('/assets/welcome-bg.light.svg') | ||||
| 	background-size cover | ||||
| 	background-position center | ||||
|   | ||||
| @@ -73,12 +73,12 @@ export default class MiOS extends EventEmitter { | ||||
| 	public app: Vue; | ||||
|  | ||||
| 	public new(vm, props) { | ||||
| 		const w = new vm({ | ||||
| 		const x = new vm({ | ||||
| 			parent: this.app, | ||||
| 			propsData: props | ||||
| 		}).$mount(); | ||||
| 		document.body.appendChild(w.$el); | ||||
| 		return w; | ||||
| 		document.body.appendChild(x.$el); | ||||
| 		return x; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
|   | ||||
| @@ -81,7 +81,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	mounted() { | ||||
| 		document.addEventListener('visibilitychange', this.onVisibilitychange, false); | ||||
| 		window.addEventListener('scroll', this.onScroll); | ||||
| 		window.addEventListener('scroll', this.onScroll, { passive: true }); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 		<div> | ||||
| 			<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span> | ||||
| 			<span class="geo" v-if="geo">%fa:map-marker-alt%</span> | ||||
| 			<button class="submit" :disabled="posting" @click="post">{{ submitText }}</button> | ||||
| 			<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> | ||||
| 		</div> | ||||
| 	</header> | ||||
| 	<div class="form"> | ||||
|   | ||||
| @@ -35,13 +35,13 @@ | ||||
| 						<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> | ||||
| 					</header> | ||||
| 					<div @click="widgetFunc(widget.id)"> | ||||
| 						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> | ||||
| 						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true"/> | ||||
| 			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="mobile"/> | ||||
| 		</template> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
|   | ||||
| @@ -5,8 +5,9 @@ import MiOS from './mios'; | ||||
| import { hostname } from './config'; | ||||
|  | ||||
| const defaultSettings = { | ||||
| 	home: [], | ||||
| 	home: null, | ||||
| 	mobileHome: [], | ||||
| 	deck: null, | ||||
| 	fetchOnScroll: true, | ||||
| 	showMaps: true, | ||||
| 	showPostFormOnTopOfTl: false, | ||||
| @@ -123,13 +124,6 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
| 					state.home = data; | ||||
| 				}, | ||||
|  | ||||
| 				setHomeWidget(state, x) { | ||||
| 					const w = state.home.find(w => w.id == x.id); | ||||
| 					if (w) { | ||||
| 						w.data = x.data; | ||||
| 					} | ||||
| 				}, | ||||
|  | ||||
| 				addHomeWidget(state, widget) { | ||||
| 					state.home.unshift(widget); | ||||
| 				}, | ||||
| @@ -138,11 +132,36 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
| 					state.mobileHome = data; | ||||
| 				}, | ||||
|  | ||||
| 				setMobileHomeWidget(state, x) { | ||||
| 					const w = state.mobileHome.find(w => w.id == x.id); | ||||
| 					if (w) { | ||||
| 						w.data = x.data; | ||||
| 				setWidget(state, x) { | ||||
| 					let w; | ||||
|  | ||||
| 					//#region Decktop home | ||||
| 					if (state.home) { | ||||
| 						w = state.home.find(w => w.id == x.id); | ||||
| 						if (w) { | ||||
| 							w.data = x.data; | ||||
| 						} | ||||
| 					} | ||||
| 					//#endregion | ||||
|  | ||||
| 					//#region Mobile home | ||||
| 					if (state.mobileHome) { | ||||
| 						w = state.mobileHome.find(w => w.id == x.id); | ||||
| 						if (w) { | ||||
| 							w.data = x.data; | ||||
| 						} | ||||
| 					} | ||||
| 					//#endregion | ||||
|  | ||||
| 					//#region Deck | ||||
| 					if (state.deck && state.deck.columns) { | ||||
| 						state.deck.columns.filter(c => c.type == 'widgets').forEach(c => { | ||||
| 							c.widgets.forEach(w => { | ||||
| 								if (w.id == x.id) w.data = x.data; | ||||
| 							}); | ||||
| 						}); | ||||
| 					} | ||||
| 					//#endregion | ||||
| 				}, | ||||
|  | ||||
| 				addMobileHomeWidget(state, widget) { | ||||
| @@ -151,11 +170,120 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
|  | ||||
| 				removeMobileHomeWidget(state, widget) { | ||||
| 					state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); | ||||
| 				}, | ||||
|  | ||||
| 				addDeckColumn(state, column) { | ||||
| 					state.deck.columns.push(column); | ||||
| 					state.deck.layout.push([column.id]); | ||||
| 				}, | ||||
|  | ||||
| 				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)); | ||||
| 				}, | ||||
|  | ||||
| 				swapDeckColumn(state, x) { | ||||
| 					const a = x.a; | ||||
| 					const b = x.b; | ||||
| 					const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1); | ||||
| 					const aY = state.deck.layout[aX].findIndex(id => id == a); | ||||
| 					const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1); | ||||
| 					const bY = state.deck.layout[bX].findIndex(id => id == b); | ||||
| 					state.deck.layout[aX][aY] = b; | ||||
| 					state.deck.layout[bX][bY] = a; | ||||
| 				}, | ||||
|  | ||||
| 				swapLeftDeckColumn(state, id) { | ||||
| 					state.deck.layout.some((ids, i) => { | ||||
| 						if (ids.indexOf(id) != -1) { | ||||
| 							const left = state.deck.layout[i - 1]; | ||||
| 							if (left) { | ||||
| 								state.deck.layout[i - 1] = state.deck.layout[i]; | ||||
| 								state.deck.layout[i] = left; | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
|  | ||||
| 				swapRightDeckColumn(state, id) { | ||||
| 					state.deck.layout.some((ids, i) => { | ||||
| 						if (ids.indexOf(id) != -1) { | ||||
| 							const right = state.deck.layout[i + 1]; | ||||
| 							if (right) { | ||||
| 								state.deck.layout[i + 1] = state.deck.layout[i]; | ||||
| 								state.deck.layout[i] = right; | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
|  | ||||
| 				swapUpDeckColumn(state, id) { | ||||
| 					const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1); | ||||
| 					ids.some((x, i) => { | ||||
| 						if (x == id) { | ||||
| 							const up = ids[i - 1]; | ||||
| 							if (up) { | ||||
| 								ids[i - 1] = id; | ||||
| 								ids[i] = up; | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
|  | ||||
| 				swapDownDeckColumn(state, id) { | ||||
| 					const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1); | ||||
| 					ids.some((x, i) => { | ||||
| 						if (x == id) { | ||||
| 							const down = ids[i + 1]; | ||||
| 							if (down) { | ||||
| 								ids[i + 1] = id; | ||||
| 								ids[i] = down; | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
|  | ||||
| 				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)); | ||||
| 					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); | ||||
| 				}, | ||||
|  | ||||
| 				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.splice(i + 1, 0, [id]); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| 				}, | ||||
|  | ||||
| 				addDeckWidget(state, x) { | ||||
| 					const column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					column.widgets.unshift(x.widget); | ||||
| 				}, | ||||
|  | ||||
| 				removeDeckWidget(state, x) { | ||||
| 					const column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					column.widgets = column.widgets.filter(w => w.id != x.widget.id); | ||||
| 				}, | ||||
|  | ||||
| 				renameDeckColumn(state, x) { | ||||
| 					const column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					column.name = x.name; | ||||
| 				} | ||||
| 			}, | ||||
|  | ||||
| 			actions: { | ||||
| 				merge(ctx, settings) { | ||||
| 					if (settings == null) return; | ||||
| 					Object.entries(settings).forEach(([key, value]) => { | ||||
| 						ctx.commit('set', { key, value }); | ||||
| 					}); | ||||
| @@ -172,6 +300,73 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
| 					} | ||||
| 				}, | ||||
|  | ||||
| 				saveDeck(ctx) { | ||||
| 					os.api('i/update_client_setting', { | ||||
| 						name: 'deck', | ||||
| 						value: ctx.state.deck | ||||
| 					}); | ||||
| 				}, | ||||
|  | ||||
| 				addDeckColumn(ctx, column) { | ||||
| 					ctx.commit('addDeckColumn', column); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				removeDeckColumn(ctx, id) { | ||||
| 					ctx.commit('removeDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				swapDeckColumn(ctx, id) { | ||||
| 					ctx.commit('swapDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				swapLeftDeckColumn(ctx, id) { | ||||
| 					ctx.commit('swapLeftDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				swapRightDeckColumn(ctx, id) { | ||||
| 					ctx.commit('swapRightDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				swapUpDeckColumn(ctx, id) { | ||||
| 					ctx.commit('swapUpDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				swapDownDeckColumn(ctx, id) { | ||||
| 					ctx.commit('swapDownDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				stackLeftDeckColumn(ctx, id) { | ||||
| 					ctx.commit('stackLeftDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				popRightDeckColumn(ctx, id) { | ||||
| 					ctx.commit('popRightDeckColumn', id); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				addDeckWidget(ctx, x) { | ||||
| 					ctx.commit('addDeckWidget', x); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				removeDeckWidget(ctx, x) { | ||||
| 					ctx.commit('removeDeckWidget', x); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				renameDeckColumn(ctx, x) { | ||||
| 					ctx.commit('renameDeckColumn', x); | ||||
| 					ctx.dispatch('saveDeck'); | ||||
| 				}, | ||||
|  | ||||
| 				addHomeWidget(ctx, widget) { | ||||
| 					ctx.commit('addHomeWidget', widget); | ||||
|  | ||||
|   | ||||
| @@ -48,6 +48,7 @@ type IUserBase = { | ||||
| 	usernameLower: string; | ||||
| 	avatarId: mongo.ObjectID; | ||||
| 	bannerId: mongo.ObjectID; | ||||
| 	wallpaperId: mongo.ObjectID; | ||||
| 	data: any; | ||||
| 	description: string; | ||||
| 	pinnedNoteId: mongo.ObjectID; | ||||
| @@ -412,6 +413,10 @@ export const pack = ( | ||||
| 		? `${config.drive_url}/${_user.bannerId}` | ||||
| 		: null; | ||||
|  | ||||
| 	_user.wallpaperUrl = _user.wallpaperId != null | ||||
| 		? `${config.drive_url}/${_user.wallpaperId}` | ||||
| 		: null; | ||||
|  | ||||
| 	if (!meId || !meId.equals(_user.id) || !opts.detail) { | ||||
| 		delete _user.avatarId; | ||||
| 		delete _user.bannerId; | ||||
|   | ||||
| @@ -189,6 +189,11 @@ const endpoints: Endpoint[] = [ | ||||
| 		withCredential: true, | ||||
| 		secure: true | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'i/update_widget', | ||||
| 		withCredential: true, | ||||
| 		secure: true | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'i/change_password', | ||||
| 		withCredential: true, | ||||
|   | ||||
| @@ -45,6 +45,11 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => { | ||||
| 	if (bannerIdErr) return rej('invalid bannerId param'); | ||||
| 	if (bannerId !== undefined) updates.bannerId = bannerId; | ||||
|  | ||||
| 	// Get 'wallpaperId' parameter | ||||
| 	const [wallpaperId, wallpaperIdErr] = $.type(ID).optional().nullable().get(params.wallpaperId); | ||||
| 	if (wallpaperIdErr) return rej('invalid wallpaperId param'); | ||||
| 	if (wallpaperId !== undefined) updates.wallpaperId = wallpaperId; | ||||
|  | ||||
| 	// Get 'isLocked' parameter | ||||
| 	const [isLocked, isLockedErr] = $.bool.optional().get(params.isLocked); | ||||
| 	if (isLockedErr) return rej('invalid isLocked param'); | ||||
| @@ -85,6 +90,16 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (wallpaperId) { | ||||
| 		const wallpaper = await DriveFile.findOne({ | ||||
| 			_id: wallpaperId | ||||
| 		}); | ||||
|  | ||||
| 		if (wallpaper != null && wallpaper.metadata.properties.avgColor) { | ||||
| 			updates.wallpaperColor = wallpaper.metadata.properties.avgColor; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	await User.update(user._id, { | ||||
| 		$set: updates | ||||
| 	}); | ||||
|   | ||||
| @@ -1,6 +1,3 @@ | ||||
| /** | ||||
|  * Module dependencies | ||||
|  */ | ||||
| import $ from 'cafy'; | ||||
| import User from '../../../../models/user'; | ||||
| import event from '../../../../publishers/stream'; | ||||
| @@ -13,50 +10,16 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { | ||||
| 			.have('id', $.str) | ||||
| 			.have('place', $.str) | ||||
| 			.have('data', $.obj)) | ||||
| 		.optional() | ||||
| 		.get(params.home); | ||||
| 	if (homeErr) return rej('invalid home param'); | ||||
|  | ||||
| 	// Get 'id' parameter | ||||
| 	const [id, idErr] = $.str.optional().get(params.id); | ||||
| 	if (idErr) return rej('invalid id param'); | ||||
| 	await User.update(user._id, { | ||||
| 		$set: { | ||||
| 			'clientSettings.home': home | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	// Get 'data' parameter | ||||
| 	const [data, dataErr] = $.obj.optional().get(params.data); | ||||
| 	if (dataErr) return rej('invalid data param'); | ||||
| 	res(); | ||||
|  | ||||
| 	if (home) { | ||||
| 		await User.update(user._id, { | ||||
| 			$set: { | ||||
| 				'clientSettings.home': home | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		res(); | ||||
|  | ||||
| 		event(user._id, 'home_updated', { | ||||
| 			home | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if (id == null && data == null) return rej('you need to set id and data params if home param unset'); | ||||
|  | ||||
| 		const _home = user.clientSettings.home; | ||||
| 		const widget = _home.find(w => w.id == id); | ||||
|  | ||||
| 		if (widget == null) return rej('widget not found'); | ||||
|  | ||||
| 		widget.data = data; | ||||
|  | ||||
| 		await User.update(user._id, { | ||||
| 			$set: { | ||||
| 				'clientSettings.home': _home | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		res(); | ||||
|  | ||||
| 		event(user._id, 'home_updated', { | ||||
| 			id, data | ||||
| 		}); | ||||
| 	} | ||||
| 	event(user._id, 'home_updated', home); | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,3 @@ | ||||
| /** | ||||
|  * Module dependencies | ||||
|  */ | ||||
| import $ from 'cafy'; | ||||
| import User from '../../../../models/user'; | ||||
| import event from '../../../../publishers/stream'; | ||||
| @@ -12,49 +9,16 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { | ||||
| 			.have('name', $.str) | ||||
| 			.have('id', $.str) | ||||
| 			.have('data', $.obj)) | ||||
| 		.optional().get(params.home); | ||||
| 		.get(params.home); | ||||
| 	if (homeErr) return rej('invalid home param'); | ||||
|  | ||||
| 	// Get 'id' parameter | ||||
| 	const [id, idErr] = $.str.optional().get(params.id); | ||||
| 	if (idErr) return rej('invalid id param'); | ||||
| 	await User.update(user._id, { | ||||
| 		$set: { | ||||
| 			'clientSettings.mobileHome': home | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	// Get 'data' parameter | ||||
| 	const [data, dataErr] = $.obj.optional().get(params.data); | ||||
| 	if (dataErr) return rej('invalid data param'); | ||||
| 	res(); | ||||
|  | ||||
| 	if (home) { | ||||
| 		await User.update(user._id, { | ||||
| 			$set: { | ||||
| 				'clientSettings.mobileHome': home | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		res(); | ||||
|  | ||||
| 		event(user._id, 'mobile_home_updated', { | ||||
| 			home | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if (id == null && data == null) return rej('you need to set id and data params if home param unset'); | ||||
|  | ||||
| 		const _home = user.clientSettings.mobileHome || []; | ||||
| 		const widget = _home.find(w => w.id == id); | ||||
|  | ||||
| 		if (widget == null) return rej('widget not found'); | ||||
|  | ||||
| 		widget.data = data; | ||||
|  | ||||
| 		await User.update(user._id, { | ||||
| 			$set: { | ||||
| 				'clientSettings.mobileHome': _home | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		res(); | ||||
|  | ||||
| 		event(user._id, 'mobile_home_updated', { | ||||
| 			id, data | ||||
| 		}); | ||||
| 	} | ||||
| 	event(user._id, 'mobile_home_updated', home); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										79
									
								
								src/server/api/endpoints/i/update_widget.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/server/api/endpoints/i/update_widget.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import $ from 'cafy'; | ||||
| import User from '../../../../models/user'; | ||||
| import event from '../../../../publishers/stream'; | ||||
|  | ||||
| module.exports = async (params, user) => new Promise(async (res, rej) => { | ||||
| 	// Get 'id' parameter | ||||
| 	const [id, idErr] = $.str.get(params.id); | ||||
| 	if (idErr) return rej('invalid id param'); | ||||
|  | ||||
| 	// Get 'data' parameter | ||||
| 	const [data, dataErr] = $.obj.get(params.data); | ||||
| 	if (dataErr) return rej('invalid data param'); | ||||
|  | ||||
| 	if (id == null && data == null) return rej('you need to set id and data params if home param unset'); | ||||
|  | ||||
| 	let widget; | ||||
|  | ||||
| 	//#region Desktop home | ||||
| 	if (widget == null && user.clientSettings.home) { | ||||
| 		const desktopHome = user.clientSettings.home; | ||||
| 		widget = desktopHome.find(w => w.id == id); | ||||
| 		if (widget) { | ||||
| 				widget.data = data; | ||||
|  | ||||
| 			await User.update(user._id, { | ||||
| 				$set: { | ||||
| 					'clientSettings.home': desktopHome | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endregion | ||||
|  | ||||
| 	//#region Mobile home | ||||
| 	if (widget == null && user.clientSettings.mobileHome) { | ||||
| 		const mobileHome = user.clientSettings.mobileHome; | ||||
| 		widget = mobileHome.find(w => w.id == id); | ||||
| 		if (widget) { | ||||
| 				widget.data = data; | ||||
|  | ||||
| 			await User.update(user._id, { | ||||
| 				$set: { | ||||
| 					'clientSettings.mobileHome': mobileHome | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endregion | ||||
|  | ||||
| 	//#region Deck | ||||
| 	if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) { | ||||
| 		const deck = user.clientSettings.deck; | ||||
| 		deck.columns.filter(c => c.type == 'widgets').forEach(c => { | ||||
| 			c.widgets.forEach(w => { | ||||
| 				if (w.id == id) widget = w; | ||||
| 			}); | ||||
| 		}); | ||||
| 		if (widget) { | ||||
| 				widget.data = data; | ||||
|  | ||||
| 			await User.update(user._id, { | ||||
| 				$set: { | ||||
| 					'clientSettings.deck': deck | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endregion | ||||
|  | ||||
| 	if (widget) { | ||||
| 		event(user._id, 'widgetUpdated', { | ||||
| 			id, data | ||||
| 		}); | ||||
|  | ||||
| 		res(); | ||||
| 	} else { | ||||
| 		rej('widget not found'); | ||||
| 	} | ||||
| }); | ||||
| @@ -35,6 +35,10 @@ module.exports = async (params, user) => { | ||||
| 		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; | ||||
| 	} | ||||
|  | ||||
| 	// Get 'mediaOnly' parameter | ||||
| 	const [mediaOnly, mediaOnlyErr] = $.bool.optional().get(params.mediaOnly); | ||||
| 	if (mediaOnlyErr) throw 'invalid mediaOnly param'; | ||||
|  | ||||
| 	// ミュートしているユーザーを取得 | ||||
| 	const mutedUserIds = user ? (await Mute.find({ | ||||
| 		muterId: user._id | ||||
| @@ -64,6 +68,10 @@ module.exports = async (params, user) => { | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	if (mediaOnly) { | ||||
| 		query.mediaIds = { $exists: true, $ne: [] }; | ||||
| 	} | ||||
|  | ||||
| 	if (sinceId) { | ||||
| 		sort._id = 1; | ||||
| 		query._id = { | ||||
|   | ||||
| @@ -35,6 +35,10 @@ module.exports = async (params, user) => { | ||||
| 		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; | ||||
| 	} | ||||
|  | ||||
| 	// Get 'mediaOnly' parameter | ||||
| 	const [mediaOnly, mediaOnlyErr] = $.bool.optional().get(params.mediaOnly); | ||||
| 	if (mediaOnlyErr) throw 'invalid mediaOnly param'; | ||||
|  | ||||
| 	// ミュートしているユーザーを取得 | ||||
| 	const mutedUserIds = user ? (await Mute.find({ | ||||
| 		muterId: user._id | ||||
| @@ -67,6 +71,10 @@ module.exports = async (params, user) => { | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	if (mediaOnly) { | ||||
| 		query.mediaIds = { $exists: true, $ne: [] }; | ||||
| 	} | ||||
|  | ||||
| 	if (sinceId) { | ||||
| 		sort._id = 1; | ||||
| 		query._id = { | ||||
|   | ||||
| @@ -44,6 +44,10 @@ module.exports = async (params, user, app) => { | ||||
| 	const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $.bool.optional().get(params.includeRenotedMyNotes); | ||||
| 	if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param'; | ||||
|  | ||||
| 	// Get 'mediaOnly' parameter | ||||
| 	const [mediaOnly, mediaOnlyErr] = $.bool.optional().get(params.mediaOnly); | ||||
| 	if (mediaOnlyErr) throw 'invalid mediaOnly param'; | ||||
|  | ||||
| 	const [followings, mutedUserIds] = await Promise.all([ | ||||
| 		// フォローを取得 | ||||
| 		// Fetch following | ||||
| @@ -137,6 +141,12 @@ module.exports = async (params, user, app) => { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	if (mediaOnly) { | ||||
| 		query.$and.push({ | ||||
| 			mediaIds: { $exists: true, $ne: [] } | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	if (sinceId) { | ||||
| 		sort._id = 1; | ||||
| 		query._id = { | ||||
|   | ||||
| @@ -44,6 +44,10 @@ module.exports = async (params, user, app) => { | ||||
| 	const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $.bool.optional().get(params.includeRenotedMyNotes); | ||||
| 	if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param'; | ||||
|  | ||||
| 	// Get 'mediaOnly' parameter | ||||
| 	const [mediaOnly, mediaOnlyErr] = $.bool.optional().get(params.mediaOnly); | ||||
| 	if (mediaOnlyErr) throw 'invalid mediaOnly param'; | ||||
|  | ||||
| 	// Get 'listId' parameter | ||||
| 	const [listId, listIdErr] = $.type(ID).get(params.listId); | ||||
| 	if (listIdErr) throw 'invalid listId param'; | ||||
| @@ -146,6 +150,12 @@ module.exports = async (params, user, app) => { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	if (mediaOnly) { | ||||
| 		query.$and.push({ | ||||
| 			mediaIds: { $exists: true, $ne: [] } | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	if (sinceId) { | ||||
| 		sort._id = 1; | ||||
| 		query._id = { | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import * as uuid from 'uuid'; | ||||
| import * as Koa from 'koa'; | ||||
| import * as bcrypt from 'bcryptjs'; | ||||
| import { generate as generateKeypair } from '../../../crypto_key'; | ||||
| @@ -11,28 +10,6 @@ recaptcha.init({ | ||||
| 	secret_key: config.recaptcha.secret_key | ||||
| }); | ||||
|  | ||||
| const home = { | ||||
| 	left: [ | ||||
| 		'profile', | ||||
| 		'calendar', | ||||
| 		'activity', | ||||
| 		'rss', | ||||
| 		'trends', | ||||
| 		'photo-stream', | ||||
| 		'version' | ||||
| 	], | ||||
| 	right: [ | ||||
| 		'broadcast', | ||||
| 		'notifications', | ||||
| 		'users', | ||||
| 		'polls', | ||||
| 		'server', | ||||
| 		'donation', | ||||
| 		'nav', | ||||
| 		'tips' | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| export default async (ctx: Koa.Context) => { | ||||
| 	// Verify recaptcha | ||||
| 	// ただしテスト時はこの機構は障害となるため無効にする | ||||
| @@ -82,28 +59,6 @@ export default async (ctx: Koa.Context) => { | ||||
| 	// Generate secret | ||||
| 	const secret = generateUserToken(); | ||||
|  | ||||
| 	//#region Construct home data | ||||
| 	const homeData = []; | ||||
|  | ||||
| 	home.left.forEach(widget => { | ||||
| 		homeData.push({ | ||||
| 			name: widget, | ||||
| 			id: uuid(), | ||||
| 			place: 'left', | ||||
| 			data: {} | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	home.right.forEach(widget => { | ||||
| 		homeData.push({ | ||||
| 			name: widget, | ||||
| 			id: uuid(), | ||||
| 			place: 'right', | ||||
| 			data: {} | ||||
| 		}); | ||||
| 	}); | ||||
| 	//#endregion | ||||
|  | ||||
| 	// Create account | ||||
| 	const account: IUser = await User.insert({ | ||||
| 		avatarId: null, | ||||
| @@ -135,9 +90,6 @@ export default async (ctx: Koa.Context) => { | ||||
| 		}, | ||||
| 		settings: { | ||||
| 			autoWatch: true | ||||
| 		}, | ||||
| 		clientSettings: { | ||||
| 			home: homeData | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
|   | ||||
| @@ -221,7 +221,9 @@ export default async (user: IUser, data: { | ||||
| 		} | ||||
|  | ||||
| 		// Publish note to global timeline stream | ||||
| 		publishGlobalTimelineStream(noteObj); | ||||
| 		if (note.visibility == 'public' && note.replyId == null) { | ||||
| 			publishGlobalTimelineStream(noteObj); | ||||
| 		} | ||||
|  | ||||
| 		if (note.visibility == 'specified') { | ||||
| 			data.visibleUsers.forEach(async u => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user