diff --git a/.config/example.yml b/.config/example.yml index 90bda4617b..ff83777ca3 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -252,7 +252,9 @@ id: 'aidx' #outgoingAddressFamily: ipv4 # Proxy for HTTP/HTTPS -#proxy: http://127.0.0.1:3128 +# + + proxyBypassHosts: - api.deepl.com diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index db899ba386..28049cd1a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - REGISTRY_IMAGE: misskey/misskey + REGISTRY_IMAGE: mattyacocacora/prsmsk-msk TAGS: | type=edge type=ref,event=pr diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a28c9ef64..07b3a9e6fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,19 +16,7 @@ ### Server - チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) -- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) -- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに -- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに -- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに -- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに -- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに -- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに -- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) -- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 -- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 -- Fix: 空文字列のリアクションはフォールバックされるように -- Fix: リノートにリアクションできないように ## 2024.5.0 diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/README.md b/README.md index a02e9895c1..000a4ae67c 100644 --- a/README.md +++ b/README.md @@ -15,35 +15,22 @@ <a href="https://type4ny-hub.net/docs/for-admin/install/guides/"> <img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a> -<a href="./CONTRIBUTING.md"> - <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-contributor-A371F7?logoColor=A371F7&style=for-the-badge&logo=git-merge&labelColor=363B40" alt="become a contributor"/></a> - -<a href="https://discord.gg/Wp8gVStHW3"> - <img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/></a> - -<a href="https://www.patreon.com/syuilo"> - <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a> - +--- </div> -## Thanks +# 当フォークについて -<a href="https://sentry.io/"><img src="https://github.com/misskey-dev/misskey/assets/4439005/98576556-222f-467a-94be-e98dbda1d852" height="30" alt="Sentry" /></a> +当フォークは PrisMisskey.space で使用しているフォークになります。 +このコードを一部でも使用する場合はAbout内にクレジット表示をお願いします。 -Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors. +# Special Thanks -<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a> +[mkkey source](https://github.com/emtkmkk/mkkey) -Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions. +[mkkey Server](https://mkkey.net) -<a href="https://about.codecov.io/for/open-source/"><img src="https://about.codecov.io/wp-content/themes/codecov/assets/brand/sentry-cobranding/logos/codecov-by-sentry-logo.svg" height="30" alt="Codecov" /></a> +[MisskeyIO source](https://github.com/MisskeyIO/misskey) -Thanks to [Codecov](https://about.codecov.io/for/open-source/) for providing the code coverage platform that helps us improve our test coverage. +[MisskeyIO Server](https://Misskey.io) -<a href="https://crowdin.com/"><img src="https://user-images.githubusercontent.com/20679825/230709597-1299a011-171a-4294-a91e-355a9b37c672.svg" height="30" alt="Crowdin" /></a> - -Thanks to [Crowdin](https://crowdin.com/) for providing the localization platform that helps us translate Misskey into many languages. - -<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" height="30" alt="Docker" /></a> - -Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production. +[CherryPick source](https://github.com/kokonect-link/cherrypick) diff --git a/locales/en-US.yml b/locales/en-US.yml index 189e371e08..d04279c3a6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -9,6 +9,8 @@ notifications: "Notifications" username: "Username" password: "Password" forgotPassword: "Forgot password" +setDefaultProfileConfirm: "Do you want to make this profile the default?" +emojiPickerProfile: "Emoji picker profile" fetchingAsApObject: "Fetching from the Fediverse..." ok: "OK" gotIt: "Got it!" @@ -175,6 +177,8 @@ flagAsCat: "Mark this account as a cat" flagAsCatDescription: "Enable this option to mark this account as a cat." flagShowTimelineReplies: "Show replies in timeline" flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." +showMediaTimeline: "Show Media timeline" +showMediaTimelineInfo: "When on, the media timeline is displayed on the top bar. When turned off, it will not be displayed." autoAcceptFollowed: "Automatically approve follow requests from users you're following" addAccount: "Add account" reloadAccountsList: "Reload account list" @@ -305,6 +309,8 @@ location: "Location" theme: "Themes" themeForLightMode: "Theme to use in Light Mode" themeForDarkMode: "Theme to use in Dark Mode" +gamingMode: "Gaming Mode" +gamingModeInfo: "It makes a nice gradation of buttons and other decorations. There is no intense blinking, etc." light: "Light" dark: "Dark" lightThemes: "Light themes" @@ -1072,6 +1078,9 @@ videos: "Videos" audio: "Audio" audioFiles: "Audio" dataSaver: "Data Saver" +cellularWithDataSaver: "Turn on Data Saver in Mobile Data Communications" +UltimatedataSaver: "Ultimate Data Saver" +cellularWithUltimateDataSaver: "Turn on Ultimate Data Saver in Mobile Data Communications" accountMigration: "Account Migration" accountMoved: "This user has moved to a new account:" accountMovedShort: "This account has been migrated." @@ -1811,6 +1820,7 @@ _aboutType4ny: contributors: "Main contributors" allContributors: "All contributors" source: "Source code" + forksource: "Source code for this fork" original: "Original" thisIsModifiedVersion: "{name} uses a modified version of the original Misskey." translation: "Translate Misskey" @@ -1852,6 +1862,7 @@ _wordMute: muteWords: "Muted words" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Surround keywords with slashes to use regular expressions." + hideMutedNotes: "Hide notes containing muted words" _instanceMute: instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription2: "Separate with newlines" @@ -1966,6 +1977,17 @@ _time: minute: "Minute(s)" hour: "Hour(s)" day: "Day(s)" +_timelineTutorial: + title: "How to use Misskey" + step1_1: "This is the \"timeline\". All \"notes\" submitted on {name} will be chronologically displayed here." + step1_2: "There are a few different timelines. For example, the \"Home timeline\" will contain notes of users you follow, and the \"Local timeline\" will contain notes from all users of {name}." + step1_3: 'Besides these two, "Social Timeline" is like Home TL + Local TL, and "Media Timeline" is a stream of notes posted with some file at {name}.' + step2_1: "Let's try posting a note next. You can do so by pressing the button with a pencil icon." + step2_2: "How about writing a self-introduction, or just \"Hello {name}!\" if you don't feel like it?" + step3_1: "Finished posting your first note?" + step3_2: "Your first note should now be displayed on your timeline." + step4_1: "You can also attach \"Reactions\" to notes." + step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with." _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." registerTOTP: "Register authenticator app" @@ -2238,6 +2260,7 @@ _instanceCharts: _timelines: home: "Home" local: "Local" + media: "Media" social: "Social" global: "Global" _play: diff --git a/locales/index.d.ts b/locales/index.d.ts index b87b28ba02..7439e0844b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -60,6 +60,46 @@ export interface Locale extends ILocale { * OK */ "ok": string; + /** + * ノートの投稿フォームを開き直した際に、下書きを復元しないようにします。 + */ + "disableNoteDraftingDescription": string; + /** + * このプロファイルをデフォルトにしますか? + */ + "setDefaultProfileConfirm": string; + /** + * 絵文字ピッカーのプロファイル + */ + "emojiPickerProfile": string; + /** + * 通知のインジケーターの数字を表示する + */ + "notificationIndicator": string; + /** + * アイコンとバナーを反転させる + */ + "hanntenn": string; + /** + * ダークだったらライトのアイコンに、ライトだったらダークのアイコンに。 + */ + "hanntennInfo": string; + /** + * ルビ + */ + "ruby": string; + /** + * ノートの下書きの復元を無効化 + */ + "disableNoteDrafting": string; + /** + * 隠れ家 + */ + "kakuregaFeature": string; + /** + * ピン留めされたチャンネル + */ + "pinnedChannel": string; /** * わかった */ @@ -68,6 +108,10 @@ export interface Locale extends ILocale { * キャンセル */ "cancel": string; + /** + * 自分の作成したリスト + */ + "myLists": string; /** * やめておく */ @@ -76,6 +120,30 @@ export interface Locale extends ILocale { * ユーザー名を入力 */ "enterUsername": string; + /** + * グローバルタイムラインを表示する + */ + "showGlobalTimeline": string; + /** + * ホームタイムラインを表示する + */ + "showHomeTimeline": string; + /** + * ローカルタイムラインを表示する + */ + "showLocalTimeline": string; + /** + * トップバーのカスタムをする + */ + "topbarCustom": string; + /** + * ソーシャルタイムラインを表示する + */ + "showSocialTimeline": string; + /** + * 上のバーにTLの名前を省略して表示する + */ + "topBarNameShown": string; /** * {user}がリノート */ @@ -100,6 +168,14 @@ export interface Locale extends ILocale { * 通知の設定 */ "notificationSettings": string; + /** + * このサーバーの公開のリスト + */ + "localListList": string; + /** + * お気に入りのリスト + */ + "favoriteLists": string; /** * 基本設定 */ @@ -320,6 +396,22 @@ export interface Locale extends ILocale { * ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。 */ "driveFileDeleteConfirm": ParameterizedString<"name">; + /** + * {name}つのファイルを削除しますか?このファイルを使用した一部のコンテンツも削除されます。 + */ + "driveFilesDeleteConfirm": ParameterizedString<"name">; + /** + * {name}つのファイルをセンシティブにしますか? + */ + "driveFilesSensitiveonConfirm": ParameterizedString<"name">; + /** + * {name}つのファイルのセンシティブを解除しますか? + */ + "driveFilesSensitiveoffConfirm": ParameterizedString<"name">; + /** + * フォルダ「{name}」を削除しますか?このフォルダの中に存在するファイルを使用した一部のコンテンツも削除されます。 + */ + "driveFolderDeleteConfirm": ParameterizedString<"name">; /** * {name}のフォローを解除しますか? */ @@ -356,6 +448,10 @@ export interface Locale extends ILocale { * フォロワー */ "followers": string; + /** + * プリズム + */ + "points": string; /** * フォローされています */ @@ -704,14 +800,30 @@ export interface Locale extends ILocale { * にゃああああああああああああああ!!!!!!!!!!!! */ "flagAsCat": string; + /** + * ウホウホウホホウホウホウホウホホホ!!!!!!!!!!! + */ + "flagAsGorilla": string; /** * にゃにゃにゃ?? */ "flagAsCatDescription": string; + /** + * ウホウホウホ?? + */ + "flagAsGorillaDescription": string; /** * タイムラインにノートへの返信を表示する */ "flagShowTimelineReplies": string; + /** + * メディアタイムラインを表示する + */ + "showMediaTimeline": string; + /** + * オンにするとメディアタイムラインを上のバーに表示します。 オフにすると表示しなくなります。 + */ + "showMediaTimelineInfo": string; /** * オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。 */ @@ -1084,6 +1196,10 @@ export interface Locale extends ILocale { * 削除しました */ "removed": string; + /** + * 「{x}」のリクエストを承認しますか? + */ + "requestApprovalAreYouSure": ParameterizedString<"x">; /** * 「{x}」を削除しますか? */ @@ -1092,6 +1208,10 @@ export interface Locale extends ILocale { * 「{x}」を削除しますか? */ "deleteAreYouSure": ParameterizedString<"x">; + /** + * 「{x}」をドラフト解除しますか? + */ + "undraftAreYouSure": ParameterizedString<"x">; /** * リセットしますか? */ @@ -1116,6 +1236,10 @@ export interface Locale extends ILocale { * オリジナル画像を保持 */ "keepOriginalUploading": string; + /** + * ホーム投稿で通知する + */ + "isNotifyIsHome": string; /** * 画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。 */ @@ -1236,6 +1360,14 @@ export interface Locale extends ILocale { * ダークモードで使うテーマ */ "themeForDarkMode": string; + /** + * ゲーミングモード + */ + "gamingMode": string; + /** + * ボタンなどの装飾をいい感じのグラデーションにします。 激しい点滅などはございません。 + */ + "gamingModeInfo": string; /** * ライト */ @@ -1248,6 +1380,10 @@ export interface Locale extends ILocale { * 明るいテーマ */ "lightThemes": string; + /** + * アイコンなどが正常に表示されない場合にここをクリックしてください。 + */ + "remoteUserInfoUpdate": string; /** * 暗いテーマ */ @@ -1304,6 +1440,10 @@ export interface Locale extends ILocale { * フォルダーを削除 */ "deleteFolder": string; + /** + * フォルダー + */ + "Folder": string; /** * フォルダー */ @@ -2120,6 +2260,10 @@ export interface Locale extends ILocale { * アカウント設定 */ "accountSettings": string; + /** + * タイムラインのヘッダー + */ + "timelineHeader": string; /** * プロモーション */ @@ -2228,6 +2372,14 @@ export interface Locale extends ILocale { * タイムライン上部に投稿フォームを表示する(チャンネル) */ "showFixedPostFormInChannel": string; + /** + * ユーザーのページで最新のノートを表示する + */ + "FeaturedOrNote": string; + /** + * ユーザーのページに行ったときにハイライトか最新のノートを表示するかを選択することができます。 オフでハイライト オンで最新のノート です + */ + "FeaturedOrNoteInfo": string; /** * フォローする際、デフォルトで返信をTLに含むようにする */ @@ -2476,6 +2628,10 @@ export interface Locale extends ILocale { * アンケート */ "poll": string; + /** + * 予約投稿 + */ + "schedulePost": string; /** * 内容を隠す */ @@ -2568,6 +2724,10 @@ export interface Locale extends ILocale { * アクセストークンの発行 */ "generateAccessToken": string; + /** + * アクセストークン + */ + "accessToken": string; /** * 権限 */ @@ -2704,6 +2864,10 @@ export interface Locale extends ILocale { * ログ */ "logs": string; + /** + * mfm 装飾 + */ + "mfm": string; /** * 遅延 */ @@ -2756,6 +2920,14 @@ export interface Locale extends ILocale { * スペースで区切って複数設定できます。 */ "setMultipleBySeparatingWithSpace": string; + /** + * 名前には英数字と_が利用できます。 + */ + "emojiNameValidation": string; + /** + * センシティブ + */ + "isSensitive": string; /** * ファイルIDまたはURL */ @@ -2816,6 +2988,14 @@ export interface Locale extends ILocale { * 送信 */ "send": string; + /** + * ファイル付きのみ + */ + "fileAttachedOnly": string; + /** + * 通報されたノート + */ + "reportedNote": string; /** * 対応済みにする */ @@ -2944,6 +3124,14 @@ export interface Locale extends ILocale { * アンケートに投票した数 */ "pollVotesCount": string; + /** + * タイムラインの絞り込みを保存する + */ + "onlyAndWithSave": string; + /** + * ファイルのみ や リプライのみ などが保存されるようになります + */ + "onlyAndWithSaveInfo": string; /** * アンケートに投票された数 */ @@ -3432,6 +3620,18 @@ export interface Locale extends ILocale { * 低 */ "low": string; + /** + * 一覧 + */ + "list": string; + /** + * ゲーミングの光るスピードの調整 + */ + "GamingSpeedChange": string; + /** + * 左にすれば早くなる、右にすれば遅くなる。それだけ。 + */ + "GamingSpeedChangeInfo": string; /** * メールアドレスの設定がされていません。 */ @@ -3440,6 +3640,18 @@ export interface Locale extends ILocale { * 比率 */ "ratio": string; + /** + * ノートの公開範囲を色付けする + */ + "showVisibilityColor": string; + /** + * 新しい絵文字 + */ + "newEmojis": string; + /** + * 申請されている絵文字 + */ + "draftEmojis": string; /** * 本文をプレビュー */ @@ -3568,6 +3780,10 @@ export interface Locale extends ILocale { * アカウント登録にメールアドレスを必須にする */ "emailRequiredForSignup": string; + /** + * GDPRモードを有効にする + */ + "enableGDPRMode": string; /** * 未読 */ @@ -4052,6 +4268,10 @@ export interface Locale extends ILocale { * カスタム絵文字の管理 */ "manageCustomEmojis": string; + /** + * カスタム絵文字のリクエスト + */ + "requestCustomEmojis": string; /** * アバターデコレーションの管理 */ @@ -4248,6 +4468,26 @@ export interface Locale extends ILocale { * ライセンス */ "license": string; + /** + * 申請中 + */ + "requestPending": string; + /** + * 承認 + */ + "approval": string; + /** + * リクエストされている絵文字 + */ + "requestingEmojis": string; + /** + * ドラフト + */ + "draft": string; + /** + * ドラフト解除 + */ + "undrafted": string; /** * お気に入り解除しますか? */ @@ -4316,6 +4556,18 @@ export interface Locale extends ILocale { * データセーバー */ "dataSaver": string; + /** + * モバイルデータ通信でデータセーバーをオンにする + */ + "cellularWithDataSaver": string; + /** + * 究極のデータセーバー + */ + "UltimateDataSaver": string; + /** + * モバイルデータ通信で究極のデータセーバーをオンにする + */ + "cellularWithUltimateDataSaver": string; /** * アカウントの移行 */ @@ -4680,6 +4932,10 @@ export interface Locale extends ILocale { * リノートを表示 */ "showRenotes": string; + /** + * CWを非表示 + */ + "showCw": string; /** * 編集済み */ @@ -4696,10 +4952,6 @@ export interface Locale extends ILocale { * フォロー中またはフォロワー */ "followingOrFollower": string; - /** - * ファイル付きのみ - */ - "fileAttachedOnly": string; /** * TLに他の人への返信を含める */ @@ -4984,6 +5236,74 @@ export interface Locale extends ILocale { * お問い合わせ */ "inquiry": string; + /** + * ノートの自己消滅 + */ + "scheduledNoteDelete": string; + /** + * このノートは{time}に消滅します + */ + "noteDeletationAt": ParameterizedString<"time">; + /** + * 1年以上先の日時を指定することはできません + */ + "cannotScheduleLaterThanOneYear": string; + /** + * アクティビティを非公開にする + */ + "hideActivity": string; + /** + * 自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。 + */ + "hideActivityDescription": string; + /** + * このお知らせはチャンネルのタイムライン上部に表示されます。最初の1行がタイトルとして表示され、2行目以降はお知らせをタップすることで表示されるようになります。 + */ + "channelAnnouncementDescription": string; + /** + * 投稿フォーム + */ + "postForm": string; + /** + * 投稿フォームの下部に表示される項目の並び替えが出来ます。項目をクリックすると削除できます。 + */ + "postFormBottomSettingsDescription": string; + /** + * 投稿フォームをリセット + */ + "clearPost": string; + /** + * 絵文字ピッカーに追加 + */ + "addToEmojiPicker": string; + /** + * リアクション数の非表示 + */ + "hideReactionCount": string; + /** + * 誰がリアクションをしたのかを非表示にする + */ + "hideReactionUsers": string; + /** + * リアクションをホバーした際のユーザー一覧と、ノート詳細ページのリアクションタブにあるリアクションをしたユーザー一覧を非表示にします + */ + "hideReactionUsersDescription": string; + /** + * 下書き + */ + "drafts": string; + /** + * 下書きの保存に関する動作 + */ + "draftSavingBehavior": string; + /** + * 下書きとして保存 + */ + "saveAsDraft": string; + /** + * 下書きを適用すると現在入力されている内容はリセットされます。よろしいですか? + */ + "draftOverwriteConfirm": string; "_delivery": { /** * 配信状態 @@ -6550,6 +6870,10 @@ export interface Locale extends ILocale { * グローバルタイムラインの閲覧 */ "gtlAvailable": string; + /** + * 絵文字ピッカーのプロファイルの上限数(最大5) + */ + "emojiPickerProfileLimit": string; /** * ローカルタイムラインの閲覧 */ @@ -6558,6 +6882,14 @@ export interface Locale extends ILocale { * パブリック投稿の許可 */ "canPublicNote": string; + /** + * ノートの編集 + */ + "canEditNote": string; + /** + * 予約投稿の許可 + */ + "canScheduleNote": string; /** * ノート内の最大メンション数 */ @@ -6582,6 +6914,10 @@ export interface Locale extends ILocale { * カスタム絵文字の管理 */ "canManageCustomEmojis": string; + /** + * カスタム絵文字のリクエスト + */ + "canRequestCustomEmojis": string; /** * アバターデコレーションの管理 */ @@ -6650,6 +6986,14 @@ export interface Locale extends ILocale { * アイコンデコレーションの最大取付個数 */ "avatarDecorationLimit": string; + /** + * ピン留めリストの最大数 + */ + "listPinnedLimit": string; + /** + * 他鯖のローカルTL除けるやつ(最大値5) + */ + "localTimelineAnyLimit": string; }; "_condition": { /** @@ -7203,6 +7547,10 @@ export interface Locale extends ILocale { * キーワードをスラッシュで囲むと正規表現になります。 */ "muteWordsDescription2": string; + /** + * ミュートされた単語を含むノートを非表示にする + */ + "hideMutedNotes": string; }; "_instanceMute": { /** @@ -7644,6 +7992,48 @@ export interface Locale extends ILocale { */ "day": string; }; + "_timelineTutorial": { + /** + * Misskeyの使い方 + */ + "title": string; + /** + * この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。 + */ + "step1_1": ParameterizedString<"name">; + /** + * タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。 + */ + "step1_2": ParameterizedString<"name">; + /** + * この2つ以外にも、「ソーシャルタイムライン」は ホームTL + ローカルTL のようなもので、 「メディアタイムライン」 には{name}で何かしらのファイル付きで投稿されたノートが流れます。 + */ + "step1_3": ParameterizedString<"name">; + /** + * 試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。 + */ + "step2_1": string; + /** + * 初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。 + */ + "step2_2": ParameterizedString<"name">; + /** + * 投稿できましたか? + */ + "step3_1": string; + /** + * あなたのノートがタイムラインに表示されていれば成功です。 + */ + "step3_2": string; + /** + * ノートには、「リアクション」を付けることができます。 + */ + "step4_1": string; + /** + * リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。 + */ + "step4_2": string; + }; "_2fa": { /** * 既に設定は完了しています。 @@ -8203,6 +8593,14 @@ export interface Locale extends ILocale { * 通知 */ "notifications": string; + /** + * ゲーミングモード + */ + "gamingMode": string; + /** + * 反転モード + */ + "gyakubariMode": string; /** * タイムライン */ @@ -8697,6 +9095,10 @@ export interface Locale extends ILocale { * ローカル */ "local": string; + /** + * メディア + */ + "media": string; /** * ソーシャル */ @@ -9037,6 +9439,10 @@ export interface Locale extends ILocale { * 実績を獲得 */ "achievementEarned": string; + /** + * ログインボーナス + */ + "loginbonus": string; /** * 通知テスト */ @@ -9126,6 +9532,10 @@ export interface Locale extends ILocale { * 実績の獲得 */ "achievementEarned": string; + /** + * ログインボーナス + */ + "loginbonus": string; /** * 連携アプリからの通知 */ @@ -9758,6 +10168,40 @@ export interface Locale extends ILocale { }; }; }; + "_schedulePost": { + /** + * 予約投稿一覧 + */ + "list": string; + /** + * 日付 + */ + "postDate": string; + /** + * 時刻 + */ + "postTime": string; + /** + * 端末に設定されているタイムゾーンの時刻で投稿されます。 + */ + "localTime": string; + /** + * 予約設定 + */ + "addSchedule": string; + /** + * {date}に投稿予約しました。 + */ + "willBePostedAtX": ParameterizedString<"date">; + /** + * 予約投稿を削除しますか? + */ + "deleteAreYouSure": string; + /** + * 予約投稿を削除して編集しますか? + */ + "deleteAndEditConfirm": string; + }; "_dataSaver": { "_media": { /** @@ -10070,6 +10514,16 @@ export interface Locale extends ILocale { * その他の貢献者 */ "etcContributor": string; + "_draftSavingBehavior": { + /** + * 自動的に保存する + */ + "auto": string; + /** + * 都度確認する + */ + "manual": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 149c0b128f..f684e55ad5 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -11,16 +11,36 @@ password: "パスワード" forgotPassword: "パスワードを忘れた" fetchingAsApObject: "連合に照会中" ok: "OK" + +disableNoteDraftingDescription: "ノートの投稿フォームを開き直した際に、下書きを復元しないようにします。" +setDefaultProfileConfirm: "このプロファイルをデフォルトにしますか?" +emojiPickerProfile: "絵文字ピッカーのプロファイル" +notificationIndicator: "通知のインジケーターの数字を表示する" +hanntenn: "アイコンとバナーを反転させる" +hanntennInfo: "ダークだったらライトのアイコンに、ライトだったらダークのアイコンに。" +ruby: "ルビ" +disableNoteDrafting: "ノートの下書きの復元を無効化" +kakuregaFeature: "隠れ家" +pinnedChannel: "ピン留めされたチャンネル" gotIt: "わかった" cancel: "キャンセル" +myLists: "自分の作成したリスト" noThankYou: "やめておく" enterUsername: "ユーザー名を入力" +showGlobalTimeline: "グローバルタイムラインを表示する" +showHomeTimeline: "ホームタイムラインを表示する" +showLocalTimeline: "ローカルタイムラインを表示する" +topbarCustom: "トップバーのカスタムをする" +showSocialTimeline: "ソーシャルタイムラインを表示する" +topBarNameShown: "上のバーにTLの名前を省略して表示する" renotedBy: "{user}がリノート" noNotes: "ノートはありません" noNotifications: "通知はありません" instance: "サーバー" settings: "設定" notificationSettings: "通知の設定" +localListList: "このサーバーの公開のリスト" +favoriteLists: "お気に入りのリスト" basicSettings: "基本設定" otherSettings: "その他の設定" openInWindow: "ウィンドウで開く" @@ -76,6 +96,10 @@ export: "エクスポート" files: "ファイル" download: "ダウンロード" driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。" +driveFilesDeleteConfirm: "{name}つのファイルを削除しますか?このファイルを使用した一部のコンテンツも削除されます。" +driveFilesSensitiveonConfirm: "{name}つのファイルをセンシティブにしますか?" +driveFilesSensitiveoffConfirm: "{name}つのファイルのセンシティブを解除しますか?" +driveFolderDeleteConfirm: "フォルダ「{name}」を削除しますか?このフォルダの中に存在するファイルを使用した一部のコンテンツも削除されます。" unfollowConfirm: "{name}のフォローを解除しますか?" exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。" importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。" @@ -85,6 +109,7 @@ note: "ノート" notes: "ノート" following: "フォロー" followers: "フォロワー" +points: "プリズム" followsYou: "フォローされています" createList: "リスト作成" manageLists: "リストの管理" @@ -172,8 +197,12 @@ cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リ flagAsBot: "Botとして設定" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Type4nyのシステム上での扱いがBotに合ったものになります。" flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!" +flagAsGorilla: "ウホウホウホホウホウホウホウホホホ!!!!!!!!!!!" flagAsCatDescription: "にゃにゃにゃ??" +flagAsGorillaDescription: "ウホウホウホ??" flagShowTimelineReplies: "タイムラインにノートへの返信を表示する" +showMediaTimeline: "メディアタイムラインを表示する" +showMediaTimelineInfo: "オンにするとメディアタイムラインを上のバーに表示します。 オフにすると表示しなくなります。" flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。" autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認" addAccount: "アカウントを追加" @@ -267,14 +296,17 @@ announcements: "お知らせ" imageUrl: "画像URL" remove: "削除" removed: "削除しました" +requestApprovalAreYouSure: "「{x}」のリクエストを承認しますか?" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" +undraftAreYouSure: "「{x}」をドラフト解除しますか?" resetAreYouSure: "リセットしますか?" areYouSure: "よろしいですか?" saved: "保存しました" messaging: "チャット" upload: "アップロード" keepOriginalUploading: "オリジナル画像を保持" +isNotifyIsHome: "ホーム投稿で通知する" keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" fromDrive: "ドライブから" fromUrl: "URLから" @@ -305,9 +337,12 @@ location: "場所" theme: "テーマ" themeForLightMode: "ライトモードで使うテーマ" themeForDarkMode: "ダークモードで使うテーマ" +gamingMode: 'ゲーミングモード' +gamingModeInfo: "ボタンなどの装飾をいい感じのグラデーションにします。 激しい点滅などはございません。" light: "ライト" dark: "ダーク" lightThemes: "明るいテーマ" +remoteUserInfoUpdate: "アイコンなどが正常に表示されない場合にここをクリックしてください。" darkThemes: "暗いテーマ" syncDeviceDarkMode: "デバイスのダークモードと同期する" drive: "ドライブ" @@ -322,6 +357,7 @@ folderName: "フォルダー名" createFolder: "フォルダーを作成" renameFolder: "フォルダー名を変更" deleteFolder: "フォルダーを削除" +Folder: "フォルダー" folder: "フォルダー" addFile: "ファイルを追加" emptyDrive: "ドライブは空です" @@ -526,6 +562,7 @@ dayOverDayChanges: "前日比" appearance: "アピアランス" clientSettings: "クライアント設定" accountSettings: "アカウント設定" +timelineHeader: "タイムラインのヘッダー" promotion: "プロモーション" promote: "プロモート" numberOfDays: "日数" @@ -553,6 +590,8 @@ serverLogs: "サーバーログ" deleteAll: "全て削除" showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)" +FeaturedOrNote: "ユーザーのページで最新のノートを表示する" +FeaturedOrNoteInfo: "ユーザーのページに行ったときにハイライトか最新のノートを表示するかを選択することができます。 オフでハイライト オンで最新のノート です" withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする" newNoteRecived: "新しいノートがあります" sounds: "サウンド" @@ -615,6 +654,7 @@ invisibleNote: "非公開の投稿" enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" +schedulePost: "予約投稿" useCw: "内容を隠す" enablePlayer: "プレイヤーを開く" disablePlayer: "プレイヤーを閉じる" @@ -638,6 +678,7 @@ large: "大" medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" +accessToken: "アクセストークン" permission: "権限" adminPermission: "管理者権限" enableAll: "全て有効にする" @@ -672,6 +713,7 @@ copy: "コピー" metrics: "メトリクス" overview: "概要" logs: "ログ" +mfm: 'mfm 装飾' delayed: "遅延" database: "データベース" channel: "チャンネル" @@ -685,6 +727,8 @@ regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" +emojiNameValidation: "名前には英数字と_が利用できます。" +isSensitive: "センシティブ" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" sample: "サンプル" @@ -700,6 +744,8 @@ reporterOrigin: "通報元" forwardReport: "リモートサーバーに通報を転送する" forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" send: "送信" +fileAttachedOnly: "ファイル付きのみ" +reportedNote: "通報されたノート" abuseMarkAsResolved: "対応済みにする" openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" @@ -732,6 +778,8 @@ followersCount: "フォロワー数" sentReactionsCount: "リアクションした数" receivedReactionsCount: "リアクションされた数" pollVotesCount: "アンケートに投票した数" +onlyAndWithSave: "タイムラインの絞り込みを保存する" +onlyAndWithSaveInfo: "ファイルのみ や リプライのみ などが保存されるようになります" pollVotedCount: "アンケートに投票された数" yes: "はい" no: "いいえ" @@ -854,8 +902,14 @@ priority: "優先度" high: "高" middle: "中" low: "低" +list: "一覧" +GamingSpeedChange: "ゲーミングの光るスピードの調整" +GamingSpeedChangeInfo: "左にすれば早くなる、右にすれば遅くなる。それだけ。" emailNotConfiguredWarning: "メールアドレスの設定がされていません。" ratio: "比率" +showVisibilityColor: "ノートの公開範囲を色付けする" +newEmojis: "新しい絵文字" +draftEmojis: "申請されている絵文字" previewNoteText: "本文をプレビュー" customCss: "カスタムCSS" customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。" @@ -888,6 +942,7 @@ itsOff: "オフになっています" on: "オン" off: "オフ" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" +enableGDPRMode: "GDPRモードを有効にする" unread: "未読" filter: "フィルタ" controlPanel: "コントロールパネル" @@ -1009,6 +1064,7 @@ assign: "アサイン" unassign: "アサインを解除" color: "色" manageCustomEmojis: "カスタム絵文字の管理" +requestCustomEmojis: "カスタム絵文字のリクエスト" manageAvatarDecorations: "アバターデコレーションの管理" youCannotCreateAnymore: "これ以上作成することはできません。" cannotPerformTemporary: "一時的に利用できません" @@ -1058,6 +1114,11 @@ hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" +requestPending: "申請中" +approval: "承認" +requestingEmojis: "リクエストされている絵文字" +draft: "ドラフト" +undrafted: "ドラフト解除" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" drivecleaner: "ドライブクリーナー" @@ -1075,6 +1136,9 @@ videos: "動画" audio: "音声" audioFiles: "音声" dataSaver: "データセーバー" +cellularWithDataSaver: "モバイルデータ通信でデータセーバーをオンにする" +UltimateDataSaver: "究極のデータセーバー" +cellularWithUltimateDataSaver: "モバイルデータ通信で究極のデータセーバーをオンにする" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" accountMovedShort: "このアカウントは移行されています" @@ -1166,11 +1230,11 @@ authentication: "認証" authenticationRequiredToContinue: "続けるには認証を行ってください" dateAndTime: "日時" showRenotes: "リノートを表示" +showCw: "CWを非表示" edited: "編集済み" notificationRecieveConfig: "通知の受信設定" mutualFollow: "相互フォロー" followingOrFollower: "フォロー中またはフォロワー" -fileAttachedOnly: "ファイル付きのみ" showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする" @@ -1242,6 +1306,23 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ noDescription: "説明文はありません" alwaysConfirmFollow: "フォローの際常に確認する" inquiry: "お問い合わせ" +scheduledNoteDelete: "ノートの自己消滅" +noteDeletationAt: "このノートは{time}に消滅します" +cannotScheduleLaterThanOneYear: "1年以上先の日時を指定することはできません" +hideActivity: "アクティビティを非公開にする" +hideActivityDescription: "自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。" +channelAnnouncementDescription: "このお知らせはチャンネルのタイムライン上部に表示されます。最初の1行がタイトルとして表示され、2行目以降はお知らせをタップすることで表示されるようになります。" +postForm: "投稿フォーム" +postFormBottomSettingsDescription: "投稿フォームの下部に表示される項目の並び替えが出来ます。項目をクリックすると削除できます。" +clearPost: "投稿フォームをリセット" +addToEmojiPicker: "絵文字ピッカーに追加" +hideReactionCount: "リアクション数の非表示" +hideReactionUsers: "誰がリアクションをしたのかを非表示にする" +hideReactionUsersDescription: "リアクションをホバーした際のユーザー一覧と、ノート詳細ページのリアクションタブにあるリアクションをしたユーザー一覧を非表示にします" +drafts: "下書き" +draftSavingBehavior: "下書きの保存に関する動作" +saveAsDraft: "下書きとして保存" +draftOverwriteConfirm: "下書きを適用すると現在入力されている内容はリセットされます。よろしいですか?" _delivery: status: "配信状態" @@ -1694,14 +1775,18 @@ _role: high: "高" _options: gtlAvailable: "グローバルタイムラインの閲覧" + emojiPickerProfileLimit: "絵文字ピッカーのプロファイルの上限数(最大5)" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" + canEditNote: "ノートの編集" + canScheduleNote: "予約投稿の許可" mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" inviteLimitCycle: "招待コードの発行間隔" inviteExpirationTime: "招待コードの有効期限" canManageCustomEmojis: "カスタム絵文字の管理" + canRequestCustomEmojis: "カスタム絵文字のリクエスト" canManageAvatarDecorations: "アバターデコレーションの管理" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "ファイルにNSFWを常に付与" @@ -1719,6 +1804,8 @@ _role: canSearchNotes: "ノート検索の利用" canUseTranslator: "翻訳機能の利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" + listPinnedLimit: "ピン留めリストの最大数" + localTimelineAnyLimit: "他鯖のローカルTL除けるやつ(最大値5)" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" @@ -1842,6 +1929,7 @@ _aboutType4ny: source: "ソースコード" original: "オリジナル" thisIsModifiedVersion: "{name}はオリジナルのType4nyを改変したバージョンを使用しています。" + forksource: "当フォークのソースコード" translation: "Type4nyを翻訳" donate: "Type4nyに寄付" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" @@ -1887,6 +1975,7 @@ _wordMute: muteWords: "ミュートするワード" muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" + hideMutedNotes: "ミュートされた単語を含むノートを非表示にする" _instanceMute: instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。" @@ -2010,6 +2099,18 @@ _time: hour: "時間" day: "日" +_timelineTutorial: + title: "Misskeyの使い方" + step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。" + step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。" + step1_3: "この2つ以外にも、「ソーシャルタイムライン」は ホームTL + ローカルTL のようなもので、 「メディアタイムライン」 には{name}で何かしらのファイル付きで投稿されたノートが流れます。" + step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。" + step2_2: "初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。" + step3_1: "投稿できましたか?" + step3_2: "あなたのノートがタイムラインに表示されていれば成功です。" + step4_1: "ノートには、「リアクション」を付けることができます。" + step4_2: "リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。" + _2fa: alreadyRegistered: "既に設定は完了しています。" registerTOTP: "認証アプリの設定を開始" @@ -2158,6 +2259,8 @@ _widgets: instanceInfo: "サーバー情報" memo: "付箋" notifications: "通知" + gamingMode: "ゲーミングモード" + gyakubariMode: "反転モード" timeline: "タイムライン" calendar: "カレンダー" trends: "トレンド" @@ -2296,6 +2399,7 @@ _instanceCharts: _timelines: home: "ホーム" local: "ローカル" + media: "メディア" social: "ソーシャル" global: "グローバル" @@ -2389,6 +2493,7 @@ _notification: roleAssigned: "ロールが付与されました" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" + loginbonus: "ログインボーナス" testNotification: "通知テスト" checkNotificationBehavior: "通知の表示を確かめる" sendTestNotification: "テスト通知を送信する" @@ -2413,6 +2518,7 @@ _notification: followRequestAccepted: "フォローが受理された" roleAssigned: "ロールが付与された" achievementEarned: "実績の獲得" + loginbonus: "ログインボーナス" app: "連携アプリからの通知" _actions: @@ -2597,6 +2703,16 @@ _externalResourceInstaller: title: "テーマのインストールに失敗しました" description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。" +_schedulePost: + list: "予約投稿一覧" + postDate: "日付" + postTime: "時刻" + localTime: "端末に設定されているタイムゾーンの時刻で投稿されます。" + addSchedule: "予約設定" + willBePostedAtX: "{date}に投稿予約しました。" + deleteAreYouSure: "予約投稿を削除しますか?" + deleteAndEditConfirm: "予約投稿を削除して編集しますか?" + _dataSaver: _media: title: "メディアの読み込みを無効化" @@ -2686,3 +2802,7 @@ _mediaControls: loop: "ループ再生" etcContributor: "その他の貢献者" + +_draftSavingBehavior: + auto: "自動的に保存する" + manual: "都度確認する" diff --git a/misskey-assets b/misskey-assets index 0179793ec8..cf3ce27b2e 160000 --- a/misskey-assets +++ b/misskey-assets @@ -1 +1 @@ -Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5 +Subproject commit cf3ce27b2eb8417233072e3d6d2fb7c5356c2364 diff --git a/package.json b/package.json index 10d04d9631..ca78eca1bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "type4ny", - "version": "2024.5.0", + "version": "2024.5.0-mattyatea4", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1684236161625-addEmojiDraftFlag.js b/packages/backend/migration/1684236161625-addEmojiDraftFlag.js new file mode 100644 index 0000000000..b0a13ea498 --- /dev/null +++ b/packages/backend/migration/1684236161625-addEmojiDraftFlag.js @@ -0,0 +1,11 @@ +export class AddEmojiDraftFlag1684236161625 { + name = 'AddEmojiDraftFlag1684236161625' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "draft" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "draft"`); + } +} diff --git a/packages/backend/migration/1696044626209-noteEditHistory.js b/packages/backend/migration/1696044626209-noteEditHistory.js new file mode 100644 index 0000000000..37a542aa1e --- /dev/null +++ b/packages/backend/migration/1696044626209-noteEditHistory.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NoteEditHistory1696044626209 { + name = 'NoteEditHistory1696044626209' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "noteEditHistory" varchar(3000) array DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP "noteEditHistory"`); + } +} diff --git a/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js b/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js new file mode 100644 index 0000000000..70220f3058 --- /dev/null +++ b/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NoteUpdateAtHistory1696318192428 { + name = 'NoteUpdateAtHistory1696318192428' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "updatedAtHistory" TIMESTAMP WITH TIME ZONE array`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP "updatedAtHistory"`); + } + +} diff --git a/packages/backend/migration/1696604429200-revert-revert-note-editting.js b/packages/backend/migration/1696604429200-revert-revert-note-editting.js new file mode 100644 index 0000000000..f723e68b44 --- /dev/null +++ b/packages/backend/migration/1696604429200-revert-revert-note-editting.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class revertrevertnoteeditting1696604429200 { + name = 'revertrevertnoteeditting1696604429200' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); + } +} diff --git a/packages/backend/migration/1696604572677-poll_vote_poll.js b/packages/backend/migration/1696604572677-poll_vote_poll.js new file mode 100644 index 0000000000..da52904565 --- /dev/null +++ b/packages/backend/migration/1696604572677-poll_vote_poll.js @@ -0,0 +1,12 @@ +export class PollVotePoll1696604572677 { + name = 'PollVotePoll1696604572677'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_poll_vote_poll" FOREIGN KEY ("noteId") REFERENCES "poll"("noteId") ON DELETE CASCADE`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_poll_vote_poll"`); + } + +} diff --git a/packages/backend/migration/1697641012204-DiscordWebhookUrl.js b/packages/backend/migration/1697641012204-DiscordWebhookUrl.js new file mode 100644 index 0000000000..bde713177d --- /dev/null +++ b/packages/backend/migration/1697641012204-DiscordWebhookUrl.js @@ -0,0 +1,11 @@ +export class DiscordWebhookUrl1697641012204 { + name = 'DiscordWebhookUrl1697641012204' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "DiscordWebhookUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "DiscordWebhookUrl"`); + } +} diff --git a/packages/backend/migration/1697642704514-EmojiBotToken.js b/packages/backend/migration/1697642704514-EmojiBotToken.js new file mode 100644 index 0000000000..8e284ede41 --- /dev/null +++ b/packages/backend/migration/1697642704514-EmojiBotToken.js @@ -0,0 +1,11 @@ +export class EmojiBotToken1697642704514 { + name = 'EmojiBotToken1697642704514' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "EmojiBotToken" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "EmojiBotToken"`); + } +} diff --git a/packages/backend/migration/1697645425687-BaseUrl.js b/packages/backend/migration/1697645425687-BaseUrl.js new file mode 100644 index 0000000000..1d1020b1cf --- /dev/null +++ b/packages/backend/migration/1697645425687-BaseUrl.js @@ -0,0 +1,15 @@ +export class BaseUrl1697645425687 { + name = 'BaseUrl1697645425687' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "ApiBase" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "visibility" SET NOT NULL`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "visibility" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ApiBase"`); + } +} diff --git a/packages/backend/migration/1698131657286-EmojiRequest.js b/packages/backend/migration/1698131657286-EmojiRequest.js new file mode 100644 index 0000000000..7db57c9cd0 --- /dev/null +++ b/packages/backend/migration/1698131657286-EmojiRequest.js @@ -0,0 +1,13 @@ +export class EmojiRequest1698131657286 { + name = 'EmojiRequest1698131657286' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "emoji_request" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "name" character varying(128) NOT NULL, "category" character varying(128), "originalUrl" character varying(512) NOT NULL, "publicUrl" character varying(512) NOT NULL DEFAULT '', "type" character varying(64), "aliases" character varying(128) array NOT NULL DEFAULT '{}', "license" character varying(1024), "fileId" character varying(1024) NOT NULL, "localOnly" boolean NOT NULL DEFAULT false, "isSensitive" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c74521e048dc744f0c7eb65f4a" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ea1d771e867e9843300f09d02c" ON "emoji_request" ("name") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_ea1d771e867e9843300f09d02c"`); + await queryRunner.query(`DROP TABLE "emoji_request"`); + } +} diff --git a/packages/backend/migration/1698907074200-gorillamode.js b/packages/backend/migration/1698907074200-gorillamode.js new file mode 100644 index 0000000000..6d7d747407 --- /dev/null +++ b/packages/backend/migration/1698907074200-gorillamode.js @@ -0,0 +1,13 @@ +export class Gorillamode1698907074200 { + name = 'Gorillamode1698907074200' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "isGorilla" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`COMMENT ON COLUMN "user"."isGorilla" IS 'Whether the User is a gorilla.'`); + } + + async down(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "user"."isGorilla" IS 'Whether the User is a gorilla.'`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isGorilla"`); + } +} diff --git a/packages/backend/migration/1699437894737-schedulenote.js b/packages/backend/migration/1699437894737-schedulenote.js new file mode 100644 index 0000000000..d0188a45ee --- /dev/null +++ b/packages/backend/migration/1699437894737-schedulenote.js @@ -0,0 +1,12 @@ +export class Schedulenote1699437894737 { + name = 'Schedulenote1699437894737' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_e798958c40009bf0cdef4f28b5" ON "note_schedule" ("userId") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP TABLE "note_schedule"`); + } +} diff --git a/packages/backend/migration/1699949373507-schedulenote2.js b/packages/backend/migration/1699949373507-schedulenote2.js new file mode 100644 index 0000000000..b8d8a6394b --- /dev/null +++ b/packages/backend/migration/1699949373507-schedulenote2.js @@ -0,0 +1,11 @@ +export class Schedulenote21699949373507 { + name = 'Schedulenote21699949373507' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_schedule" RENAME COLUMN "expiresAt" TO "scheduledAt"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_schedule" RENAME COLUMN "scheduledAt" TO "expiresAt"`); + } +} diff --git a/packages/backend/migration/1702149469508-abusenoteselect.js b/packages/backend/migration/1702149469508-abusenoteselect.js new file mode 100644 index 0000000000..eb9d5b34d8 --- /dev/null +++ b/packages/backend/migration/1702149469508-abusenoteselect.js @@ -0,0 +1,11 @@ +export class Abusenoteselect1702149469508 { + name = 'Abusenoteselect1702149469508' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "notes" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "notes"`); + } +} diff --git a/packages/backend/migration/1703294653915-requestemoji.js b/packages/backend/migration/1703294653915-requestemoji.js new file mode 100644 index 0000000000..358e77bf07 --- /dev/null +++ b/packages/backend/migration/1703294653915-requestemoji.js @@ -0,0 +1,11 @@ +export class Requestemoji1703294653915 { + name = 'Requestemoji1703294653915' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "requestEmojiAllOk" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "requestEmojiAllOk"`); + } +} diff --git a/packages/backend/migration/1703704097603-GDPRMode.js b/packages/backend/migration/1703704097603-GDPRMode.js new file mode 100644 index 0000000000..86a7e5d85f --- /dev/null +++ b/packages/backend/migration/1703704097603-GDPRMode.js @@ -0,0 +1,11 @@ +export class GDPRMode1703704097603 { + name = 'GDPRMode1703704097603' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableGDPRMode" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) {; + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGDPRMode"`); + } +} diff --git a/packages/backend/migration/1704005554275-abusenoteIds.js b/packages/backend/migration/1704005554275-abusenoteIds.js new file mode 100644 index 0000000000..e52f8850b7 --- /dev/null +++ b/packages/backend/migration/1704005554275-abusenoteIds.js @@ -0,0 +1,11 @@ +export class AbusenoteId1704005554275 { + name = 'AbusenoteId1704005554275' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "noteIds" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "noteIds"`); + } +} diff --git a/packages/backend/migration/1704206095136-avatardecoration.js b/packages/backend/migration/1704206095136-avatardecoration.js new file mode 100644 index 0000000000..797d91a74e --- /dev/null +++ b/packages/backend/migration/1704206095136-avatardecoration.js @@ -0,0 +1,11 @@ +export class Avatardecoration1704206095136 { + name = 'Avatardecoration1704206095136' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "category" character varying(256) NOT NULL DEFAULT ''`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "category"`); + } +} diff --git a/packages/backend/migration/1704343998612-avatardecoration_fed.js b/packages/backend/migration/1704343998612-avatardecoration_fed.js new file mode 100644 index 0000000000..8e182f92f4 --- /dev/null +++ b/packages/backend/migration/1704343998612-avatardecoration_fed.js @@ -0,0 +1,13 @@ +export class AvatardecorationFed1704343998612 { + name = 'AvatardecorationFed1704343998612' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "host" character varying(256)`); + await queryRunner.query(`ALTER TABLE "avatar_decoration" ALTER COLUMN "category" SET DEFAULT ''`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" ALTER COLUMN "category" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "host"`); + } +} diff --git a/packages/backend/migration/1707888646527-proxycheckio.js b/packages/backend/migration/1707888646527-proxycheckio.js new file mode 100644 index 0000000000..62ecc0ddc7 --- /dev/null +++ b/packages/backend/migration/1707888646527-proxycheckio.js @@ -0,0 +1,13 @@ +export class Proxycheckio1707888646527 { + name = 'Proxycheckio1707888646527' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableProxyCheckio" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "proxyCheckioApiKey" character varying(32)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyCheckioApiKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableProxyCheckio"`); + } +} diff --git a/packages/backend/migration/1708081353629-discordwebohookwordbrock.js b/packages/backend/migration/1708081353629-discordwebohookwordbrock.js new file mode 100644 index 0000000000..aee2bfacc4 --- /dev/null +++ b/packages/backend/migration/1708081353629-discordwebohookwordbrock.js @@ -0,0 +1,11 @@ +export class Discordwebohookwordbrock1708081353629 { + name = 'Discordwebohookwordbrock1708081353629' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "DiscordWebhookUrlWordBlock" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "DiscordWebhookUrlWordBlock"`); +} +} diff --git a/packages/backend/migration/1714831133155-outsideprismisskey.js b/packages/backend/migration/1714831133155-outsideprismisskey.js new file mode 100644 index 0000000000..0da10f603d --- /dev/null +++ b/packages/backend/migration/1714831133155-outsideprismisskey.js @@ -0,0 +1,17 @@ +export class Outsideprismisskey1714831133155 { + name = 'Outsideprismisskey1714831133155' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "bannerDark" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "bannerLight" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "iconDark" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "iconLight" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "iconLight"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "iconDark"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bannerLight"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bannerDark"`); + } +} diff --git a/packages/backend/migration/1714831133156-avatardecoration_fed.js b/packages/backend/migration/1714831133156-avatardecoration_fed.js new file mode 100644 index 0000000000..94093092bd --- /dev/null +++ b/packages/backend/migration/1714831133156-avatardecoration_fed.js @@ -0,0 +1,11 @@ +export class AvatardecorationFed1714831133156 { + name = 'AvatardecorationFed1714831133156' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "host"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "host" character varying(256)`); + } +} diff --git a/packages/backend/migration/1715791271605-loginbonus1.js b/packages/backend/migration/1715791271605-loginbonus1.js new file mode 100644 index 0000000000..6d9407a5b1 --- /dev/null +++ b/packages/backend/migration/1715791271605-loginbonus1.js @@ -0,0 +1,11 @@ +export class Loginbonus11715791271605 { + name = 'Loginbonus11715791271605' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "getPoints" integer NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "getPoints" integer NOT NULL DEFAULT '0'`); + } +} diff --git a/packages/backend/migration/1716911535226-gapikey.js b/packages/backend/migration/1716911535226-gapikey.js new file mode 100644 index 0000000000..5ec4594aeb --- /dev/null +++ b/packages/backend/migration/1716911535226-gapikey.js @@ -0,0 +1,11 @@ +export class Gapikey1716911535226 { + name = 'Gapikey1716911535226' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsId" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsId"`); + } +} diff --git a/packages/backend/migration/1718857094676-emojimore.js b/packages/backend/migration/1718857094676-emojimore.js new file mode 100644 index 0000000000..596b3242b9 --- /dev/null +++ b/packages/backend/migration/1718857094676-emojimore.js @@ -0,0 +1,16 @@ +export class Emojimore1718857094676 { + name = 'Emojimore1718857094676' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "IDX_ad0c221b25672daf2df320a817"`); + await queryRunner.query(`CREATE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a7751b74317122d11575bff31c" ON "note_reaction" ("userId", "noteId", "reaction") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_a7751b74317122d11575bff31c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") `); + + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 0467ab0bee..38666e707e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -154,6 +154,7 @@ "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", + "proxycheck-ts": "^0.0.9", "pug": "3.0.2", "punycode": "2.3.1", "qrcode": "1.5.3", @@ -180,6 +181,7 @@ "typescript": "5.5.2", "ulid": "2.3.0", "vary": "1.1.2", + "w3c-xmlserializer": "^5.0.0", "web-push": "3.6.7", "ws": "8.17.0", "xev": "3.0.2" @@ -225,6 +227,7 @@ "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", "@types/vary": "1.1.3", + "@types/w3c-xmlserializer": "^2.0.4", "@types/web-push": "3.6.3", "@types/ws": "8.5.10", "@typescript-eslint/eslint-plugin": "7.7.1", diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index c719d08725..dbcd91c76d 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -130,7 +130,7 @@ export class CacheService implements OnApplicationShutdown { case 'userChangeSuspendedState': case 'userChangeDeletedState': case 'remoteUserUpdated': - case 'localUserUpdated': { + { const user = await this.usersRepository.findOneBy({ id: body.id }); if (user == null) { this.userByIdCache.delete(body.id); diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 18f7177dbc..3c64fb09b6 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -40,6 +40,7 @@ import { MetaService } from './MetaService.js'; import { MfmService } from './MfmService.js'; import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; +import { NoteUpdateService } from './NoteUpdateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; import { NoteReadService } from './NoteReadService.js'; @@ -101,6 +102,7 @@ import { ClipEntityService } from './entities/ClipEntityService.js'; import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; import { EmojiEntityService } from './entities/EmojiEntityService.js'; +import { EmojiRequestsEntityService } from './entities/EmojiRequestsEntityService.js'; import { FollowingEntityService } from './entities/FollowingEntityService.js'; import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js'; import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js'; @@ -181,6 +183,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService }; const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; +const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; @@ -245,6 +248,7 @@ const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService }; const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useExisting: EmojiEntityService }; +const $EmojiRequestsEntityService: Provider = { provide: 'EmojiRequestsEntityService', useExisting: EmojiRequestsEntityService }; const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useExisting: FollowingEntityService }; const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useExisting: FollowRequestEntityService }; const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useExisting: GalleryLikeEntityService }; @@ -327,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting MfmService, ModerationLogService, NoteCreateService, + NoteUpdateService, NoteDeleteService, NotePiningService, NoteReadService, @@ -391,6 +396,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting DriveFileEntityService, DriveFolderEntityService, EmojiEntityService, + EmojiRequestsEntityService, FollowingEntityService, FollowRequestEntityService, GalleryLikeEntityService, @@ -469,6 +475,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $MfmService, $ModerationLogService, $NoteCreateService, + $NoteUpdateService, $NoteDeleteService, $NotePiningService, $NoteReadService, @@ -533,6 +540,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $DriveFileEntityService, $DriveFolderEntityService, $EmojiEntityService, + $EmojiRequestsEntityService, $FollowingEntityService, $FollowRequestEntityService, $GalleryLikeEntityService, @@ -612,6 +620,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting MfmService, ModerationLogService, NoteCreateService, + NoteUpdateService, NoteDeleteService, NotePiningService, NoteReadService, @@ -675,6 +684,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting DriveFileEntityService, DriveFolderEntityService, EmojiEntityService, + EmojiRequestsEntityService, FollowingEntityService, FollowRequestEntityService, GalleryLikeEntityService, @@ -753,6 +763,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $MfmService, $ModerationLogService, $NoteCreateService, + $NoteUpdateService, $NoteDeleteService, $NotePiningService, $NoteReadService, @@ -816,6 +827,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $DriveFileEntityService, $DriveFolderEntityService, $EmojiEntityService, + $EmojiRequestsEntityService, $FollowingEntityService, $FollowRequestEntityService, $GalleryLikeEntityService, diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index eddd801192..7e7bab4c54 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -12,13 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js'; +import type { EmojisRepository, EmojiRequestsRepository, MiRole, MiUser } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; @@ -34,6 +34,9 @@ export class CustomEmojiService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + private utilityService: UtilityService, private idService: IdService, private emojiEntityService: EmojiEntityService, @@ -56,6 +59,41 @@ export class CustomEmojiService implements OnApplicationShutdown { }); } + @bindThis + public async request(data: { + driveFile: MiDriveFile; + name: string; + category: string | null; + aliases: string[]; + license: string | null; + isSensitive: boolean; + localOnly: boolean; + }, me?: MiUser): Promise<MiEmojiRequest> { + const emoji = await this.emojiRequestsRepository.insert({ + id: this.idService.gen(), + updatedAt: new Date(), + name: data.name, + category: data.category, + aliases: data.aliases, + originalUrl: data.driveFile.url, + publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, + type: data.driveFile.webpublicType ?? data.driveFile.type, + license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + fileId: data.driveFile.id, + }).then(x => this.emojiRequestsRepository.findOneByOrFail(x.identifiers[0])); + + if (me) { + this.moderationLogService.log(me, 'addCustomEmoji', { + emojiId: emoji.id, + emoji: emoji, + }); + } + + return emoji; + } + @bindThis public async add(data: { driveFile: MiDriveFile; @@ -159,6 +197,36 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + @bindThis + public async updateRequest(id: MiEmojiRequest['id'], data: { + driveFile?: MiDriveFile; + name?: string; + category?: string | null; + aliases?: string[]; + license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + }, moderator?: MiUser): Promise<void> { + const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id }); + const sameNameEmoji = await this.emojiRequestsRepository.findOneBy({ name: data.name }); + if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); + + await this.emojiRequestsRepository.update(emoji.id, { + updatedAt: new Date(), + name: data.name, + category: data.category, + aliases: data.aliases, + license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + originalUrl: data.driveFile != null ? data.driveFile.url : undefined, + publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, + type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, + }); + + this.localEmojisCache.refresh(); + } + @bindThis public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ @@ -267,6 +335,13 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + @bindThis + public async deleteRequest(id: MiEmojiRequest['id']) { + const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id }); + + await this.emojiRequestsRepository.delete(emoji.id); + } + @bindThis public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) { const emojis = await this.emojisRepository.findBy({ @@ -389,11 +464,21 @@ export class CustomEmojiService implements OnApplicationShutdown { return this.emojisRepository.exists({ where: { name, host: IsNull() } }); } + @bindThis + public checkRequestDuplicate(name: string): Promise<boolean> { + return this.emojiRequestsRepository.exist({ where: { name } }); + } + @bindThis public getEmojiById(id: string): Promise<MiEmoji | null> { return this.emojisRepository.findOneBy({ id }); } + @bindThis + public getEmojiRequestById(id: string): Promise<MiEmojiRequest | null> { + return this.emojiRequestsRepository.findOneBy({ id }); + } + @bindThis public getEmojiByName(name: string): Promise<MiEmoji | null> { return this.emojisRepository.findOneBy({ name, host: IsNull() }); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index f7ba94f431..1ec94e92e2 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -213,6 +213,17 @@ export class EmailService { reason: validated.reason ? formatReason[validated.reason] ?? null : null, }; } + if (meta.enableActiveEmailValidation) { + const dispose = await this.httpRequestService.send('https://raw.githubusercontent.com/mattyatea/disposable-email-domains/master/disposable_email_blocklist.conf', { + method: 'GET', + }); + const disposableEmailDomains = (await dispose.text()).split('\n'); + const domain = emailAddress.split('@')[1]; + console.log(domain) + if (disposableEmailDomains.includes(domain)) { + validated = { valid: false, reason: 'disposable' }; + } + } const emailDomain: string = emailAddress.split('@')[1]; const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 60b59c52d6..198b15fbc6 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -96,6 +96,7 @@ export class FanoutTimelineEndpointService { if (ps.me) { const me = ps.me; + const [ userIdsWhoMeMuting, userIdsWhoMeMutingRenotes, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 7e8ecd8942..99d29fc97a 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -119,7 +119,7 @@ export interface NoteEventTypes { }; updated: { cw: string | null; - text: string; + text: string | null; }; reacted: { reaction: string; @@ -160,6 +160,8 @@ export interface AdminEventTypes { targetUserId: MiUser['id'], reporterId: MiUser['id'], comment: string; + notes: any[]; + noteIds: string[]; }; } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 2ba17bb314..2b380a6fbf 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -6,7 +6,8 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { Window, XMLSerializer } from 'happy-dom'; +import { JSDOM } from 'jsdom'; +import serialize from 'w3c-xmlserializer'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; @@ -243,7 +244,7 @@ export class MfmService { return null; } - const { window } = new Window(); + const { window } = new JSDOM() as unknown as { window: Window }; const doc = window.document; @@ -461,6 +462,6 @@ export class MfmService { appendChildren(nodes, body); - return new XMLSerializer().serializeToString(body); + return serialize(body); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 577f733541..a666d27bf6 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -15,19 +15,16 @@ import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; import { IdService } from '@/core/IdService.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import type { IPoll } from '@/models/Poll.js'; import { MiPoll } from '@/models/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; -import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { MemorySingleCache } from '@/misc/cache.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; +import type { MiNoteCreateOption as Option, MiMinimumUser as MinimumUser } from '@/types.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -128,14 +125,17 @@ type MinimumUser = { type Option = { createdAt?: Date | null; + updatedAt?: Date | null; name?: string | null; text?: string | null; reply?: MiNote | null; renote?: MiNote | null; files?: MiDriveFile[] | null; poll?: IPoll | null; + event?: IEvent | null; localOnly?: boolean | null; reactionAcceptance?: MiNote['reactionAcceptance']; + disableRightClick?: boolean | null; cw?: string | null; visibility?: string; visibleUsers?: MinimumUser[] | null; @@ -227,6 +227,7 @@ export class NoteCreateService implements OnApplicationShutdown { host: MiUser['host']; isBot: MiUser['isBot']; isCat: MiUser['isCat']; + isGorilla: MiUser['isGorilla']; }, data: Option, silent = false): Promise<MiNote> { // チャンネル外にリプライしたら対象のスコープに合わせる // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) @@ -262,13 +263,50 @@ export class NoteCreateService implements OnApplicationShutdown { } } - const hasProhibitedWords = await this.checkProhibitedWordsContain({ - cw: data.cw, - text: data.text, - pollChoices: data.poll?.choices, - }, meta.prohibitedWords); + if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) { + const { DiscordWebhookUrlWordBlock } = (await this.metaService.fetch()); + const regexpregexp = /^\/(.+)\/(.*)$/; + let matchedString = ''; + for (const filter of meta.prohibitedWords) { + // represents RegExp + const regexp = filter.match(regexpregexp); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + const foundWord = words.find(keyword => (data.cw ?? data.text ?? '').includes(keyword)); + if (foundWord) { + matchedString = foundWord; + break; + } + } else { + const match = new RE2(regexp[1], regexp[2]).exec(data.cw ?? data.text ?? ''); + if (match) { + matchedString = match[0]; + break; + } + } + } - if (hasProhibitedWords) { + if (DiscordWebhookUrlWordBlock) { + const data_disc = { 'username': 'ノートブロックお知らせ', + 'content': + 'ユーザー名 :' + user.username + '\n' + + 'url : ' + user.host + '\n' + + 'contents : ' + data.text + '\n' + + '引っかかったワード :' + matchedString, + 'allowed_mentions': { + 'parse': [], + }, + }; + + await fetch(DiscordWebhookUrlWordBlock, { + 'method': 'post', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data_disc), + }); + } throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } @@ -364,6 +402,15 @@ export class NoteCreateService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } + const willCauseNotification = mentionedUsers.filter(u => u.host === null).length > 0 || data.reply?.userHost === null || data.renote?.userHost === null; + + if (user.host !== null && willCauseNotification) { + const userEntity = await this.usersRepository.findOneBy({ id: user.id }); + if ((userEntity?.followersCount ?? 0) === 0) { + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + } + } + tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { @@ -962,6 +1009,9 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } + if (note.visibility === 'public' && note.userHost !== null) { + this.fanoutTimelineService.push(`remoteLocalTimeline:${note.userHost}`, note.id, 1000, r); + } } if (Math.random() < 0.1) { diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts new file mode 100644 index 0000000000..db80036526 --- /dev/null +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -0,0 +1,297 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setImmediate } from 'node:timers/promises'; +import util from 'util'; +import { In, DataSource } from 'typeorm'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import * as mfm from 'mfm-js'; +import type { IMentionedRemoteUsers } from '@/models/Note.js'; +import { MiNote } from '@/models/Note.js'; +import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { RelayService } from '@/core/RelayService.js'; +import { DI } from '@/di-symbols.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { bindThis } from '@/decorators.js'; +import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { SearchService } from '@/core/SearchService.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { MiDriveFile } from '@/models/_.js'; +import { MiPoll, IPoll } from '@/models/Poll.js'; +import { concat } from '@/misc/prelude/array.js'; +import { extractHashtags } from '@/misc/extract-hashtags.js'; +import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; + +type MinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + +type Option = { + updatedAt?: Date | null; + files?: MiDriveFile[] | null; + name?: string | null; + text?: string | null; + cw?: string | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + poll?: IPoll | null; +}; + +@Injectable() +export class NoteUpdateService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private relayService: RelayService, + private apDeliverManagerService: ApDeliverManagerService, + private apRendererService: ApRendererService, + private searchService: SearchService, + private activeUsersChart: ActiveUsersChart, + ) { } + + @bindThis + public async update(user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + isBot: MiUser['isBot']; + }, data: Option, note: MiNote, silent = false): Promise<MiNote | null> { + if (data.updatedAt == null) data.updatedAt = new Date(); + + if (data.text) { + if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { + data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + } + data.text = data.text.trim(); + } else { + data.text = null; + } + + let tags = data.apHashtags; + let emojis = data.apEmojis; + + // Parse MFM if needed + if (!tags || !emojis) { + const tokens = data.text ? mfm.parse(data.text)! : []; + const cwTokens = data.cw ? mfm.parse(data.cw)! : []; + const choiceTokens = data.poll && data.poll.choices + ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) + : []; + + const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); + + tags = data.apHashtags ?? extractHashtags(combinedTokens); + + emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens); + } + + tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); + + const updatedNote = await this.updateNote(user, note, data, tags, emojis); + + if (updatedNote) { + setImmediate('post updated', { signal: this.#shutdownController.signal }).then( + () => this.postNoteUpdated(updatedNote, user, silent), + () => { /* aborted, ignore this */ }, + ); + } + + return updatedNote; + } + + @bindThis + private async updateNote(user: { + id: MiUser['id']; host: MiUser['host']; + }, note: MiNote, data: Option, tags: string[], emojis: string[]): Promise<MiNote | null> { + const updatedAtHistory = note.updatedAtHistory ? note.updatedAtHistory : []; + + const values = new MiNote({ + updatedAt: data.updatedAt!, + fileIds: data.files ? data.files.map(file => file.id) : [], + text: data.text, + hasPoll: data.poll != null, + cw: data.cw ?? null, + tags: tags.map(tag => normalizeForSearch(tag)), + emojis, + attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + updatedAtHistory: [...updatedAtHistory, new Date()], + noteEditHistory: [...note.noteEditHistory, (note.cw ? note.cw + '\n' : '') + note.text!], + }); + + // 投稿を更新 + try { + if (note.hasPoll && values.hasPoll) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.update(MiNote, { id: note.id }, values); + + if (values.hasPoll) { + const old_poll = await transactionalEntityManager.findOneBy(MiPoll, { noteId: note.id }); + if (old_poll!.choices.toString() !== data.poll!.choices.toString() || old_poll!.multiple !== data.poll!.multiple) { + await transactionalEntityManager.delete(MiPoll, { noteId: note.id }); + const poll = new MiPoll({ + noteId: note.id, + choices: data.poll!.choices, + expiresAt: data.poll!.expiresAt, + multiple: data.poll!.multiple, + votes: new Array(data.poll!.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host, + }); + await transactionalEntityManager.insert(MiPoll, poll); + } + } + }); + } else if (!note.hasPoll && values.hasPoll) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.update(MiNote, { id: note.id }, values); + + if (values.hasPoll) { + const poll = new MiPoll({ + noteId: note.id, + choices: data.poll!.choices, + expiresAt: data.poll!.expiresAt, + multiple: data.poll!.multiple, + votes: new Array(data.poll!.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host, + }); + + await transactionalEntityManager.insert(MiPoll, poll); + } + }); + } else if (note.hasPoll && !values.hasPoll) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.update(MiNote, { id: note.id }, values); + + if (!values.hasPoll) { + await transactionalEntityManager.delete(MiPoll, { noteId: note.id }); + } + }); + } else { + await this.notesRepository.update({ id: note.id }, values); + } + + return await this.notesRepository.findOneBy({ id: note.id }); + } catch (e) { + console.error(e); + + throw e; + } + } + + @bindThis + private async postNoteUpdated(note: MiNote, user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + isBot: MiUser['isBot']; + }, silent: boolean) { + if (!silent) { + if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); + + this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, text: note.text }); + + //#region AP deliver + if (this.userEntityService.isLocalUser(user)) { + await (async () => { + // @ts-ignore + const noteActivity = await this.renderNoteActivity(note, user); + + await this.deliverToConcerned(user, note, noteActivity); + })(); + } + //#endregion + } + + // Register to search database + this.reIndex(note); + } + + @bindThis + private async renderNoteActivity(note: MiNote, user: MiUser) { + const content = this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user); + + return this.apRendererService.addContext(content); + } + + @bindThis + private async getMentionedRemoteUsers(note: MiNote) { + const where = [] as any[]; + + // mention / reply / dm + const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); + if (uris.length > 0) { + where.push( + { uri: In(uris) }, + ); + } + + // renote / quote + if (note.renoteUserId) { + where.push({ + id: note.renoteUserId, + }); + } + + if (where.length === 0) return []; + + return await this.usersRepository.find({ + where, + }) as MiRemoteUser[]; + } + + @bindThis + private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { + console.log('deliverToConcerned', util.inspect(content, { depth: null })); + await this.apDeliverManagerService.deliverToFollowers(user, content); + await this.relayService.deliverToRelays(user, content); + const remoteUsers = await this.getMentionedRemoteUsers(note); + for (const remoteUser of remoteUsers) { + await this.apDeliverManagerService.deliverToUser(user, content, remoteUser); + } + } + + @bindThis + private reIndex(note: MiNote) { + if (note.text == null && note.cw == null) return; + + this.searchService.unindexNote(note); + this.searchService.indexNote(note); + } + + @bindThis + public dispose(): void { + this.#shutdownController.abort(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 579335c322..54895c1170 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -21,6 +21,7 @@ import type { Provider } from '@nestjs/common'; export type SystemQueue = Bull.Queue<Record<string, unknown>>; export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>; +export type ScheduleNotePostQueue = Bull.Queue<ScheduleNotePostJobData>; export type DeliverQueue = Bull.Queue<DeliverJobData>; export type InboxQueue = Bull.Queue<InboxJobData>; export type DbQueue = Bull.Queue; @@ -41,6 +42,12 @@ const $endedPollNotification: Provider = { inject: [DI.config], }; +const $scheduleNotePost: Provider = { + provide: 'queue:scheduleNotePost', + useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST)), + inject: [DI.config], +}; + const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), @@ -89,6 +96,7 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, + $scheduleNotePost, $deliver, $inbox, $db, @@ -100,6 +108,7 @@ const $systemWebhookDeliver: Provider = { exports: [ $system, $endedPollNotification, + $scheduleNotePost, $deliver, $inbox, $db, @@ -113,6 +122,7 @@ export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -129,6 +139,7 @@ export class QueueModule implements OnApplicationShutdown { await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), + this.scheduleNotePostQueue.close(), this.deliverQueue.close(), this.inboxQueue.close(), this.dbQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index d90382d367..c52dc6565c 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; + import type { DbJobData, DeliverJobData, @@ -32,6 +33,7 @@ import type { SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, + ScheduleNotePostQueue } from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -44,6 +46,7 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 39e0dab78f..34ee0f285d 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -166,29 +166,56 @@ export class ReactionService { userId: user.id, reaction, }; + if (user.host == null) { + const exists = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + reaction: record.reaction, + }); - // Create reaction - try { - await this.noteReactionsRepository.insert(record); - } catch (e) { - if (isDuplicateKeyValueError(e)) { - const exists = await this.noteReactionsRepository.findOneByOrFail({ - noteId: note.id, - userId: user.id, - }); + const count = await this.noteReactionsRepository.countBy({ + noteId: note.id, + userId: user.id, + }); - if (exists.reaction !== reaction) { - // 別のリアクションがすでにされていたら置き換える - await this.delete(user, note); + if (count > 3) { + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); + } + + if (exists == null) { + if (user.host == null) { await this.noteReactionsRepository.insert(record); } else { - // 同じリアクションがすでにされていたらエラー throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); } } else { - throw e; + // 同じリアクションがすでにされていたらエラー + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); + } + } else { + try { + await this.noteReactionsRepository.insert(record); + } catch (e) { + if (isDuplicateKeyValueError(e)) { + const exists = await this.noteReactionsRepository.findOneByOrFail({ + noteId: note.id, + userId: user.id, + }); + + if (exists.reaction !== reaction) { + // 別のリアクションがすでにされていたら置き換える + await this.delete(user, note); + await this.noteReactionsRepository.insert(record); + } else { + // 同じリアクションがすでにされていたらエラー + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); + } + } else { + throw e; + } } } + // Create reaction // Increment reactions count const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; @@ -281,17 +308,24 @@ export class ReactionService { } @bindThis - public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) { + public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, reaction?: string) { // if already unreacted - const exist = await this.noteReactionsRepository.findOneBy({ - noteId: note.id, - userId: user.id, - }); - + let exist; + if (reaction == null) { + exist = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + }); + } else { + exist = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + reaction: reaction.replace(/@./, ''), + }); + } if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - // Delete reaction const result = await this.noteReactionsRepository.delete(exist.id); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index eda7c0e287..6a222cab6a 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -35,12 +35,15 @@ export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; + canEditNote: boolean; + canScheduleNote: boolean; mentionLimit: number; canInvite: boolean; inviteLimit: number; inviteLimitCycle: number; inviteExpirationTime: number; canManageCustomEmojis: boolean; + canRequestCustomEmojis: boolean; canManageAvatarDecorations: boolean; canSearchNotes: boolean; canUseTranslator: boolean; @@ -57,6 +60,9 @@ export type RolePolicies = { userEachUserListsLimit: number; rateLimitFactor: number; avatarDecorationLimit: number; + emojiPickerProfileLimit: number; + listPinnedLimit: number; + localTimelineAnyLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -64,11 +70,14 @@ export const DEFAULT_POLICIES: RolePolicies = { ltlAvailable: true, canPublicNote: true, mentionLimit: 20, + canEditNote: true, + canScheduleNote: true, canInvite: false, inviteLimit: 0, inviteLimitCycle: 60 * 24 * 7, inviteExpirationTime: 0, canManageCustomEmojis: false, + canRequestCustomEmojis: false, canManageAvatarDecorations: false, canSearchNotes: false, canUseTranslator: true, @@ -85,6 +94,9 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, avatarDecorationLimit: 1, + emojiPickerProfileLimit: 2, + listPinnedLimit: 2, + localTimelineAnyLimit: 3, }; @Injectable() @@ -364,13 +376,17 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), + canScheduleNote: calc('canScheduleNote', vs => vs.some(v => v === true)), + canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), mentionLimit: calc('mentionLimit', vs => Math.max(...vs)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), + canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)), canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)), + canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), @@ -386,6 +402,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), + emojiPickerProfileLimit: calc('emojiPickerProfileLimit', vs => Math.max(...vs)), + listPinnedLimit: calc('listPinnedLimit', vs => Math.max(...vs)), + localTimelineAnyLimit: calc('localTimelineAnyLimit', vs => Math.max(...vs)), }; } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index db13111457..e447bccad4 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; @@ -73,6 +74,7 @@ export class ApInboxService { private notePiningService: NotePiningService, private userBlockingService: UserBlockingService, private noteCreateService: NoteCreateService, + private noteUpdateService: NoteUpdateService, private noteDeleteService: NoteDeleteService, private appLockService: AppLockService, private apResolverService: ApResolverService, @@ -751,11 +753,13 @@ export class ApInboxService { @bindThis private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> { + const uri = getApId(activity); + if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } - this.logger.debug('Update'); + this.logger.debug(`Update: ${uri}`); const resolver = this.apResolverService.createResolver(); @@ -767,14 +771,51 @@ export class ApInboxService { if (isActor(object)) { await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; - } else if (getApType(object) === 'Question') { + } /*else if (getApType(object) === 'Question') { await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); return 'ok: Question updated'; + }*/ else if (getApType(object) === 'Note' || getApType(object) === 'Question') { + await this.updateNote(resolver, actor, object, false, activity); + return 'ok: Note updated'; } else { return `skip: Unknown type: ${getApType(object)}`; } } + @bindThis + private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise<string> { + const uri = getApId(note); + + if (typeof note === 'object') { + if (actor.uri !== note.attributedTo) { + return 'skip: actor.uri !== note.attributedTo'; + } + + if (typeof note.id === 'string') { + if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) { + return 'skip: host in actor.uri !== note.id'; + } + } + } + + const unlock = await this.appLockService.getApLock(uri); + + try { + const target = await this.notesRepository.findOneBy({uri: uri}); + if (!target) return `skip: target note not located: ${uri}`; + await this.apNoteService.updateNote(note, target, resolver, silent); + return 'ok'; + } catch (err) { + if (err instanceof StatusError && err.isClientError) { + return `skip ${err.statusCode}`; + } else { + throw err; + } + } finally { + unlock(); + } + } + @bindThis private async move(actor: MiRemoteUser, activity: IMove): Promise<string> { // fetch the new and old accounts diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 03aec93ee4..7f025a7e4d 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -108,6 +108,7 @@ export class ApRendererService { actor: this.userEntityService.genLocalUserUri(note.userId), type: 'Announce', published: this.idService.parse(note.id).date.toISOString(), + updated: note.updatedAt?.toISOString() ?? undefined, to, cc, object, @@ -438,6 +439,7 @@ export class ApRendererService { _misskey_quote: quote, quoteUrl: quote, published: this.idService.parse(note.id).date.toISOString(), + updated: note.updatedAt?.toISOString() ?? undefined, to, cc, inReplyTo, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index d04a97d2c0..31f7cebed3 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -1,12 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project + * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -24,7 +25,8 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; +import { getApId, getApType, getOneApHrefNullable, getOneApId, isEmoji, validPost } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; @@ -52,6 +54,9 @@ export class ApNoteService { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private idService: IdService, private apMfmService: ApMfmService, private apResolverService: ApResolverService, @@ -69,6 +74,7 @@ export class ApNoteService { private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, + private noteUpdateService: NoteUpdateService, private apDbResolverService: ApDbResolverService, private apLoggerService: ApLoggerService, ) { @@ -295,6 +301,7 @@ export class ApNoteService { try { return await this.noteCreateService.create(actor, { createdAt: note.published ? new Date(note.published) : null, + updatedAt: note.updated ? new Date(note.updated) : null, files, reply, renote: quote, @@ -324,6 +331,85 @@ export class ApNoteService { } } + @bindThis + public async updateNote(value: string | IObject, target: MiNote, resolver?: Resolver, silent = false): Promise<MiNote | null> { + if (resolver == null) resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(value); + const entryUri = getApId(value); + + const err = this.validateNote(object, entryUri); + if (err) { + this.logger.error(err.message, { + resolver: { history: resolver.getHistory() }, + value, + object, + }); + throw new Error('invalid note'); + } + + const note = object as IPost; + + // 投稿者をフェッチ + if (note.attributedTo == null) { + throw new Error('invalid note.attributedTo: ' + note.attributedTo); + } + + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; + + // 投稿者が凍結されていたらスキップ + if (actor.isSuspended) { + throw new Error('actor has been suspended'); + } + + const limit = promiseLimit<MiDriveFile>(2); + const files = (await Promise.all(toArray(note.attachment).map(attach => ( + limit(() => this.apImageService.resolveImage(actor, { + ...attach, + sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + })) + )))); + + const cw = note.summary === '' ? null : note.summary; + + // テキストのパース + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = this.apMfmService.htmlToMfm(note.content, note.tag); + } + + const apHashtags = extractApHashtags(note.tag); + + const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { + this.logger.info(`extractEmojis: ${e}`); + return []; + }); + + const apEmojis = emojis.map(emoji => emoji.name); + + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); + + try { + return await this.noteUpdateService.update(actor, { + updatedAt: note.updated ? new Date(note.updated) : null, + files, + name: note.name, + cw, + text, + apHashtags, + apEmojis, + poll, + }, target, silent); + } catch (err: any) { + this.logger.warn(`note update failed: ${err}`); + return err; + } + } + /** * Noteを解決します。 * diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 97361697ad..0d40829570 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -14,6 +14,7 @@ export interface IObject { summary?: string; _misskey_summary?: string; published?: string; + updated?: string; cc?: ApObject; to?: ApObject; attributedTo?: ApObject; diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index 35ab381c0b..bc0fae1259 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -65,21 +65,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -88,7 +88,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -96,7 +96,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 9658fa6334..115dad6f3e 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -4,12 +4,15 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { AbuseUserReportsRepository, NotesRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import type { Packed } from '@/misc/json-schema.js'; import { UserEntityService } from './UserEntityService.js'; @@ -19,7 +22,11 @@ export class AbuseUserReportEntityService { @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, private idService: IdService, ) { } @@ -34,11 +41,27 @@ export class AbuseUserReportEntityService { }, ) { const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); + const notes = []; + if (report.noteIds && report.noteIds.length > 0) { + for (const x of report.noteIds) { + const exists = await this.notesRepository.countBy({ id: x }); + if (exists === 0) { + notes.push('deleted'); + continue; + } + notes.push(await this.noteEntityService.pack(x)); + } + } else if (report.notes.length > 0) { + notes.push(...(report.notes)); + } + + console.log(report.notes.length, null, notes); return await awaitAll({ id: report.id, createdAt: this.idService.parse(report.id).date.toISOString(), comment: report.comment, + notes, resolved: report.resolved, reporterId: report.reporterId, targetUserId: report.targetUserId, diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index e729263172..9f4903cc25 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -132,7 +132,10 @@ export class DriveFileEntityService { } return url; } - + @bindThis + public async getFromUrl(url: string): Promise<MiDriveFile | null> { + return this.driveFilesRepository.findOneBy({ url: url }); + } @bindThis public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise<number> { const id = typeof user === 'object' ? user.id : user; diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 3187f5c82a..c9c8de9b79 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -34,6 +34,7 @@ export class EmojiEntityService { localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, + draft: emoji.draft, }; } @@ -62,6 +63,7 @@ export class EmojiEntityService { isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + draft: emoji.draft, }; } diff --git a/packages/backend/src/core/entities/EmojiRequestsEntityService.ts b/packages/backend/src/core/entities/EmojiRequestsEntityService.ts new file mode 100644 index 0000000000..46308e654c --- /dev/null +++ b/packages/backend/src/core/entities/EmojiRequestsEntityService.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { EmojiRequestsRepository } from '@/models/_.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { bindThis } from '@/decorators.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; + +@Injectable() +export class EmojiRequestsEntityService { + constructor( + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + ) { + } + + @bindThis + public async packSimple( + src: MiEmojiRequest['id'] | MiEmojiRequest, + ): Promise<Packed<'EmojiRequestSimple'>> { + const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src }); + + return { + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url: emoji.publicUrl, + isSensitive: emoji.isSensitive ? true : undefined, + }; + } + + @bindThis + public packSimpleMany( + emojis: any[], + ) { + return Promise.all(emojis.map(x => this.packSimple(x))); + } + + @bindThis + public async packDetailed( + src: MiEmojiRequest['id'] | MiEmojiRequest, + ): Promise<Packed<'EmojiRequestDetailed'>> { + const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src }); + + return { + id: emoji.id, + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + url: emoji.publicUrl, + license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + fileId: emoji.fileId, + }; + } + + @bindThis + public packDetailedMany( + emojis: any[], + ) { + return Promise.all(emojis.map(x => this.packDetailed(x))); + } +} + diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 2a5f304096..9148d2d5c9 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -70,6 +70,11 @@ export class MetaEntityService { inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + bannerDark: instance.bannerDark, + bannerLight: instance.bannerLight, + iconDark: instance.iconDark, + iconLight: instance.iconLight, + googleAnalyticsId: instance.googleAnalyticsId, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, @@ -85,6 +90,7 @@ export class MetaEntityService { bannerUrl: instance.bannerUrl, infoImageUrl: instance.infoImageUrl, serverErrorImageUrl: instance.serverErrorImageUrl, + googleAnalyticsId: instance.googleAnalyticsId, notFoundImageUrl: instance.notFoundImageUrl, iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 5f4bad70f9..85091e76f0 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -208,6 +208,30 @@ export class NoteEntityService implements OnModuleInit { return undefined; } + @bindThis + public async populateMyReactions(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: { + myReactions: Map<MiNote['id'], string | null>; + }) { + const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); + + if (reactionsCount === 0) return undefined; + + // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない + if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) { + return undefined; + } + + const reactions = await this.noteReactionsRepository.findBy({ + userId: meId, + noteId: note.id, + }); + + if (reactions.length > 0) { + return reactions.map(reaction => this.reactionService.convertLegacyReaction(reaction.reaction)); + } + + return undefined; + } @bindThis public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> { @@ -324,6 +348,9 @@ export class NoteEntityService implements OnModuleInit { const packed: Packed<'Note'> = await awaitAll({ id: note.id, createdAt: this.idService.parse(note.id).date.toISOString(), + updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, + updatedAtHistory: note.updatedAtHistory ? note.updatedAtHistory.map(x => x.toISOString()) : undefined, + noteEditHistory: note.noteEditHistory.length ? note.noteEditHistory : undefined, userId: note.userId, user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me), text: text, @@ -378,6 +405,7 @@ export class NoteEntityService implements OnModuleInit { ...(meId && Object.keys(note.reactions).length > 0 ? { myReaction: this.populateMyReaction(note, meId, options?._hint_), + myReactions: this.populateMyReactions(note, meId, options?._hint_), } : {}), } : {}), }); diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index a6b6dd3336..ef0d273619 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -162,6 +162,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), + ...(notification.type === 'loginbonus' ? { + loginbonus: notification.loginbonus, + } : {}), ...(notification.type === 'app' ? { body: notification.customBody, header: notification.customHeader, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index aec79c940e..3c5d71a7db 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -470,9 +470,8 @@ export class UserEntityService implements OnModuleInit { createdAt: this.idService.parse(announcement.id).date.toISOString(), ...announcement, })) : null; - + console.log(user.getPoints); const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null; - const packed = { id: user.id, name: user.name, @@ -506,7 +505,7 @@ export class UserEntityService implements OnModuleInit { iconUrl: r.iconUrl, displayOrder: r.displayOrder, }))) : undefined, - + ...(user.host == null ? { getPoints: user.getPoints } : {}), ...(isDetailed ? { url: profile!.url, uri: user.uri, diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 35df7e1ee8..22221575b2 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -66,7 +66,7 @@ export class ServerStatsService implements OnApplicationShutdown { if (log.length > 200) log.pop(); }; - tick(); + await tick(); this.intervalId = setInterval(tick, interval); } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 3535f4b8b2..671b140043 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -15,6 +15,7 @@ export const DI = { //#region Repositories usersRepository: Symbol('usersRepository'), notesRepository: Symbol('notesRepository'), + scheduledNotesRepository: Symbol('scheduledNotesRepository'), announcementsRepository: Symbol('announcementsRepository'), announcementReadsRepository: Symbol('announcementReadsRepository'), appsRepository: Symbol('appsRepository'), @@ -40,6 +41,7 @@ export const DI = { followRequestsRepository: Symbol('followRequestsRepository'), instancesRepository: Symbol('instancesRepository'), emojisRepository: Symbol('emojisRepository'), + emojiRequestsRepository: Symbol('emojiRequestsRepository'), driveFilesRepository: Symbol('driveFilesRepository'), driveFoldersRepository: Symbol('driveFoldersRepository'), metasRepository: Symbol('metasRepository'), diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index bb6a9da739..a90ba4c431 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -33,7 +33,7 @@ import { packedClipSchema } from '@/models/json-schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; -import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; +import { packedEmojiDetailedSchema, packedEmojiRequestSimpleSchema, packedEmojiSimpleSchema, packedEmojiRequestDetailedSchema } from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; @@ -94,7 +94,9 @@ export const refs = { FederationInstance: packedFederationInstanceSchema, GalleryPost: packedGalleryPostSchema, EmojiSimple: packedEmojiSimpleSchema, + EmojiRequestSimple: packedEmojiRequestSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, + EmojiRequestDetailed: packedEmojiRequestDetailedSchema, Flash: packedFlashSchema, Signin: packedSigninSchema, RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts index fe56c1464b..92cb7bb7a2 100644 --- a/packages/backend/src/models/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -60,6 +60,16 @@ export class MiAbuseUserReport { }) public comment: string; + @Column('jsonb', { + default: [], + }) + public notes: any[]; + + @Column('jsonb', { + default: [], + }) + public noteIds: string[] | null; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index 21bf7c69b2..d129986c3b 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -31,6 +31,12 @@ export class MiAvatarDecoration { }) public description: string; + @Column('varchar', { + length: 256, + default: '', + }) + public category: string; + // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする @Column('varchar', { array: true, length: 128, default: '{}', diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index 5fc93d33f3..85e9a962a6 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -81,4 +81,10 @@ export class MiEmoji { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + + @Column('boolean', { + default: false, + nullable: false, + }) + public draft: boolean; } diff --git a/packages/backend/src/models/EmojiRequest.ts b/packages/backend/src/models/EmojiRequest.ts new file mode 100644 index 0000000000..53a5563f7e --- /dev/null +++ b/packages/backend/src/models/EmojiRequest.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('emoji_request') +@Index(['name'], { unique: true }) +export class MiEmojiRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true, + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 128, + }) + public name: string; + + @Column('varchar', { + length: 128, nullable: true, + }) + public category: string | null; + + @Column('varchar', { + length: 512, + }) + public originalUrl: string; + + @Column('varchar', { + length: 512, + default: '', + }) + public publicUrl: string; + + // publicUrlの方のtypeが入る + @Column('varchar', { + length: 64, nullable: true, + }) + public type: string | null; + + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public aliases: string[]; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public license: string | null; + + @Column('varchar', { + length: 1024, nullable: false, + }) + public fileId: string; + + @Column('boolean', { + default: false, + }) + public localOnly: boolean; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 1166f1ea2d..10fabf9a46 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -139,6 +139,11 @@ export class MiMeta { nullable: true, }) public serverErrorImageUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public googleAnalyticsId: string | null; @Column('varchar', { length: 1024, @@ -224,11 +229,33 @@ export class MiMeta { }) public enableRecaptcha: boolean; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public DiscordWebhookUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public DiscordWebhookUrlWordBlock: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public EmojiBotToken: string | null; @Column('varchar', { length: 1024, nullable: true, }) public recaptchaSiteKey: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public ApiBase: string | null; @Column('varchar', { length: 1024, @@ -417,6 +444,30 @@ export class MiMeta { }) public objectStorageBaseUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public bannerDark: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public bannerLight: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public iconDark: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public iconLight: string | null; + @Column('varchar', { length: 1024, nullable: true, @@ -589,6 +640,27 @@ export class MiMeta { }) public notesPerOneAd: number; + @Column('boolean', { + default: false, + }) + public requestEmojiAllOk: boolean; + + @Column('boolean', { + default: false, + }) + public enableGDPRMode: boolean; + + @Column('boolean', { + default: false, + }) + public enableProxyCheckio: boolean; + + @Column('varchar', { + length: 32, + nullable: true, + }) + public proxyCheckioApiKey: string; + @Column('boolean', { default: true, }) diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 80425aa24e..35d4d3af26 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -14,6 +14,23 @@ import type { MiDriveFile } from './DriveFile.js'; export class MiNote { @PrimaryColumn(id()) public id: string; + @Column('timestamp with time zone', { + default: null, + }) + public updatedAt: Date | null; + + @Column('timestamp with time zone', { + array: true, + default: null, + }) + public updatedAtHistory: Date[] | null; + + @Column('varchar', { + length: 3000, + array: true, + default: '{}', + }) + public noteEditHistory: string[]; @Index() @Column({ diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts index 11839a0737..edfb81c142 100644 --- a/packages/backend/src/models/NoteReaction.ts +++ b/packages/backend/src/models/NoteReaction.ts @@ -9,7 +9,8 @@ import { MiUser } from './User.js'; import { MiNote } from './Note.js'; @Entity('note_reaction') -@Index(['userId', 'noteId'], { unique: true }) +@Index(['userId', 'noteId', 'reaction'], { unique: true }) +@Index(['userId', 'noteId']) export class MiNoteReaction { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index eb12b29eb3..1626259fad 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Provider } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { MiAbuseReportNotificationRecipient, + MiAbuseUserReport, MiAccessToken, MiAd, @@ -18,7 +18,6 @@ import { MiAuthSession, MiAvatarDecoration, MiBlocking, - MiBubbleGameRecord, MiChannel, MiChannelFavorite, MiChannelFollowing, @@ -28,10 +27,11 @@ import { MiDriveFile, MiDriveFolder, MiEmoji, + MiEmojiRequest, MiFlash, MiFlashLike, - MiFollowing, MiFollowRequest, + MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, @@ -51,19 +51,19 @@ import { MiPollVote, MiPromoNote, MiPromoRead, - MiRegistrationTicket, - MiRegistryItem, - MiRelay, - MiRenoteMuting, MiRepository, miRepository, - MiRetentionAggregation, + MiRegistrationTicket, + MiRegistryItem, MiReversiGame, + MiSystemWebhook, + MiRelay, + MiRenoteMuting, + MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, - MiSystemWebhook, MiUsedUsername, MiUser, MiUserIp, @@ -77,8 +77,10 @@ import { MiUserProfile, MiUserPublickey, MiUserSecurityKey, - MiWebhook -} from './_.js'; + MiWebhook, + MiScheduledNote, + MiBubbleGameRecord } from './_.js'; +import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; const $usersRepository: Provider = { @@ -93,6 +95,12 @@ const $notesRepository: Provider = { inject: [DI.db], }; +const $scheduledNotesRepository: Provider = { + provide: DI.scheduledNotesRepository, + useFactory: (db: DataSource) => db.getRepository(MiScheduledNote), + inject: [DI.db], +}; + const $announcementsRepository: Provider = { provide: DI.announcementsRepository, useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository<MiAnnouncement>), @@ -243,6 +251,12 @@ const $emojisRepository: Provider = { inject: [DI.db], }; +const $emojiRequestsRepository: Provider = { + provide: DI.emojiRequestsRepository, + useFactory: (db: DataSource) => db.getRepository(MiEmojiRequest), + inject: [DI.db], +}; + const $driveFilesRepository: Provider = { provide: DI.driveFilesRepository, useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository<MiDriveFile>), @@ -500,6 +514,7 @@ const $reversiGamesRepository: Provider = { providers: [ $usersRepository, $notesRepository, + $scheduledNotesRepository, $announcementsRepository, $announcementReadsRepository, $appsRepository, @@ -525,6 +540,7 @@ const $reversiGamesRepository: Provider = { $followRequestsRepository, $instancesRepository, $emojisRepository, + $emojiRequestsRepository, $driveFilesRepository, $driveFoldersRepository, $metasRepository, @@ -571,6 +587,7 @@ const $reversiGamesRepository: Provider = { exports: [ $usersRepository, $notesRepository, + $scheduledNotesRepository, $announcementsRepository, $announcementReadsRepository, $appsRepository, @@ -596,6 +613,7 @@ const $reversiGamesRepository: Provider = { $followRequestsRepository, $instancesRepository, $emojisRepository, + $emojiRequestsRepository, $driveFilesRepository, $driveFoldersRepository, $metasRepository, diff --git a/packages/backend/src/models/ScheduledNote.ts b/packages/backend/src/models/ScheduledNote.ts new file mode 100644 index 0000000000..513ad2f845 --- /dev/null +++ b/packages/backend/src/models/ScheduledNote.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import type { MiNoteCreateOption } from '@/types.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('note_schedule') +export class MiScheduledNote { + @PrimaryColumn(id()) + public id: string; + + @Column('jsonb') + public note: MiNoteCreateOption; + + @Index() + @Column('varchar', { + length: 260, + }) + public userId: MiUser['id']; + + @Column('timestamp with time zone') + public scheduledAt: Date; +} diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index c29ea15c85..5436a7d546 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -25,6 +25,11 @@ export class MiUser { }) public lastFetchedAt: Date | null; + @Column('integer', { + default: '0', + }) + public getPoints: number; + @Index() @Column('timestamp with time zone', { nullable: true, @@ -179,6 +184,12 @@ export class MiUser { }) public isCat: boolean; + @Column('boolean', { + default: false, + comment: 'Whether the User is a gorilla.', + }) + public isGorilla: boolean; + @Column('boolean', { default: false, comment: 'Whether the User is the root.', diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 3378ed245c..cba3a2fab3 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -29,6 +29,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { MiDriveFolder } from '@/models/DriveFolder.js'; import { MiEmoji } from '@/models/Emoji.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; import { MiFollowing } from '@/models/Following.js'; import { MiFollowRequest } from '@/models/FollowRequest.js'; import { MiGalleryLike } from '@/models/GalleryLike.js'; @@ -80,6 +81,7 @@ import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; +import { MiScheduledNote } from './ScheduledNote.js'; export interface MiRepository<T extends ObjectLiteral> { createTableColumnNames(this: Repository<T> & MiRepository<T>): string[]; @@ -144,6 +146,7 @@ export { MiDriveFile, MiDriveFolder, MiEmoji, + MiEmojiRequest, MiFollowing, MiFollowRequest, MiGalleryLike, @@ -159,6 +162,7 @@ export { MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, + MiScheduledNote, MiPage, MiPageLike, MiPasswordResetRequest, @@ -215,6 +219,7 @@ export type ClipFavoritesRepository = Repository<MiClipFavorite> & MiRepository< export type DriveFilesRepository = Repository<MiDriveFile> & MiRepository<MiDriveFile>; export type DriveFoldersRepository = Repository<MiDriveFolder> & MiRepository<MiDriveFolder>; export type EmojisRepository = Repository<MiEmoji> & MiRepository<MiEmoji>; +export type EmojiRequestsRepository = Repository<MiEmojiRequest>; export type FollowingsRepository = Repository<MiFollowing> & MiRepository<MiFollowing>; export type FollowRequestsRepository = Repository<MiFollowRequest> & MiRepository<MiFollowRequest>; export type GalleryLikesRepository = Repository<MiGalleryLike> & MiRepository<MiGalleryLike>; @@ -230,6 +235,7 @@ export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository< export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>; export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>; export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>; +export type ScheduledNotesRepository = Repository<MiScheduledNote>; export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>; export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>; export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>; diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index b1ed6c2ed6..1612aeb83c 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -44,6 +44,40 @@ export const packedEmojiSimpleSchema = { format: 'id', }, }, + draft: { + type: 'boolean', + optional: false, nullable: true, + }, + }, +} as const; +export const packedEmojiRequestSimpleSchema = { + type: 'object', + properties: { + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + isSensitive: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; @@ -85,6 +119,10 @@ export const packedEmojiDetailedSchema = { type: 'string', optional: false, nullable: true, }, + draft: { + type: 'boolean', + optional: false, nullable: true, + }, isSensitive: { type: 'boolean', optional: false, nullable: false, @@ -104,3 +142,51 @@ export const packedEmojiDetailedSchema = { }, }, } as const; + +export const packedEmojiRequestDetailedSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + license: { + type: 'string', + optional: false, nullable: true, + }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + localOnly: { + type: 'boolean', + optional: false, nullable: false, + }, + fileId: { + type: 'string', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index c920497edc..84102a4536 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -17,6 +17,24 @@ export const packedNoteSchema = { optional: false, nullable: false, format: 'date-time', }, + updatedAt: { + type: 'string', + optional: true, nullable: true, + format: 'date-time', + }, + updatedAtHistory: { + type: 'array', + optional: true, nullable: true, + items: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + }, + noteEditHistory: { + type: 'array', + optional: true, nullable: false, + }, deletedAt: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 52341d5b71..5c49040d89 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -155,6 +155,14 @@ export const packedUserLiteSchema = { onlineStatus: { type: 'string', nullable: false, optional: false, + isGorilla: { + type: 'boolean', + nullable: false, optional: true, + }, + onlineStatus: { + type: 'string', + format: 'url', + nullable: true, optional: false, enum: ['unknown', 'online', 'active', 'offline'], }, badgeRoles: { @@ -180,7 +188,8 @@ export const packedUserLiteSchema = { }, }, }, -} as const; +} +} as const export const packedUserDetailedNotMeOnlySchema = { type: 'object', diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index ef12dcacdc..a74c3c6963 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -28,6 +28,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { MiDriveFolder } from '@/models/DriveFolder.js'; import { MiEmoji } from '@/models/Emoji.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; import { MiFollowing } from '@/models/Following.js'; import { MiFollowRequest } from '@/models/FollowRequest.js'; import { MiGalleryLike } from '@/models/GalleryLike.js'; @@ -76,6 +77,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; +import { MiScheduledNote } from '@/models/ScheduledNote.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; @@ -99,7 +101,7 @@ class MyCustomLogger implements Logger { @bindThis public logQuery(query: string, parameters?: any[]) { - sqlLogger.info(this.highlight(query).substring(0, 100)); + sqlLogger.info(this.highlight(query)); } @bindThis @@ -153,6 +155,7 @@ export const entities = [ MiRenoteMuting, MiBlocking, MiNote, + MiScheduledNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, @@ -166,6 +169,7 @@ export const entities = [ MiPoll, MiPollVote, MiEmoji, + MiEmojiRequest, MiHashtag, MiSwSubscription, MiAbuseUserReport, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 7f00865845..b7c1e6fd70 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; @@ -75,6 +76,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor UserWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, + ScheduleNotePostProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 5d78150103..5af91d2955 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -82,6 +83,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; + private schedulerNotePostQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -91,6 +93,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private scheduleNotePostProcessorService: ScheduleNotePostProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, @@ -487,6 +490,13 @@ export class QueueProcessorService implements OnApplicationShutdown { } //#endregion + //#region schedule note post + this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.SCHEDULE_NOTE_POST), + autorun: false, + }); + //#endregion + //#region ended poll notification { this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { @@ -515,6 +525,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.schedulerNotePostQueueWorker.run(), ]); } @@ -530,6 +541,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), + this.schedulerNotePostQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 15f01303bb..6466a03ee4 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -11,6 +11,7 @@ export const QUEUE = { INBOX: 'inbox', SYSTEM: 'system', ENDED_POLL_NOTIFICATION: 'endedPollNotification', + SCHEDULE_NOTE_POST: 'scheduleNotePost', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index e25d3913a7..17fae0fe97 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -103,6 +103,7 @@ export class ImportCustomEmojisProcessorService { isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: [], + draft: false, }); } diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts new file mode 100644 index 0000000000..40fc3f3e54 --- /dev/null +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import type { ScheduledNotesRepository, UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { ScheduleNotePostJobData } from '../types.js'; + +@Injectable() +export class ScheduleNotePostProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.scheduledNotesRepository) + private scheduledNotesRepository: ScheduledNotesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private noteCreateService: NoteCreateService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post'); + } + + @bindThis + public async process(job: Bull.Job<ScheduleNotePostJobData>): Promise<void> { + this.scheduledNotesRepository.findOneBy({ id: job.data.scheduledNoteId }).then(async (data) => { + if (!data) { + this.logger.warn(`Schedule note ${job.data.scheduledNoteId} not found`); + } else { + data.note.createdAt = new Date(); + const me = await this.usersRepository.findOneByOrFail({ id: data.userId }); + await this.noteCreateService.create(me, data.note); + await this.scheduledNotesRepository.remove(data); + } + }); + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 1ca829a87a..d953c59b28 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -6,9 +6,13 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; -import type { MiUser } from '@/models/User.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; +import { IPoll } from '@/models/Poll.js'; +import { MiScheduledNote } from '@/models/ScheduledNote.js'; +import { MiChannel } from '@/models/Channel.js'; +import { MiApp } from '@/models/App.js'; import type httpSignature from '@peertube/http-signature'; export type DeliverJobData = { @@ -106,6 +110,17 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; +export type ScheduleNotePostJobData = { + scheduledNoteId: MiNote['id']; +} + +type MinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + export type SystemWebhookDeliverJobData = { type: string; content: unknown; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 77eb2c5a02..677997127c 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -342,7 +342,8 @@ export class FileServerService { 'avatar' in request.query || 'static' in request.query || 'preview' in request.query || - 'badge' in request.query + 'badge' in request.query || + 'datasaver' in request.query ) { if (!isConvertibleImage) { // 画像でないなら404でお茶を濁す @@ -361,7 +362,7 @@ export class FileServerService { } else { const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) .resize({ - height: 'emoji' in request.query ? 128 : 320, + height: 'emoji' in request.query ? 64 : 128, withoutEnlargement: true, }) .webp(webpDefault); @@ -407,7 +408,28 @@ export class FileServerService { ext: 'png', type: 'image/png', }; - } else if (file.mime === 'image/svg+xml') { + } else if ('datasaver' in request.query){ + if (!isAnimationConvertibleImage && !('static' in request.query)) { + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; + } else { + const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) + .resize({ + height: 32, + withoutEnlargement: true, + }) + .webp(webpDefault); + + image = { + data, + ext: 'webp', + type: 'image/webp', + }; + } + }else if (file.mime === 'image/svg+xml') { image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048); } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index a20a1f24eb..3ccf9d6ffc 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -6,6 +6,7 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; +import * as ep___users_lists_list_favorite from '@/server/api/endpoints/users/lists/list-favorite.js'; import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js'; import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js'; @@ -36,18 +37,23 @@ import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; +import * as ep___admin_emoji_setlocalOnlyBulk from './endpoints/admin/emoji/set-localonly-bulk.js'; +import * as ep___admin_emoji_setisSensitiveBulk from './endpoints/admin/emoji/set-issensitive-bulk.js'; import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_addRequest from './endpoints/admin/emoji/add-request.js'; import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_listRequest from './endpoints/admin/emoji/list-request.js'; import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; +import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js'; import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; @@ -264,6 +270,7 @@ import * as ep___invite_list from './endpoints/invite/list.js'; import * as ep___invite_limit from './endpoints/invite/limit.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emojiRequests from './endpoints/emoji-requests.js'; import * as ep___emoji from './endpoints/emoji.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; @@ -278,13 +285,17 @@ import * as ep___notes_children from './endpoints/notes/children.js'; import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; +import * as ep___notes_anyLocalTimeline from './endpoints/notes/any-local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; @@ -349,6 +360,7 @@ import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; +import * as ep___users_user_stats from './endpoints/users/stats.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -387,8 +399,9 @@ import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_verify from './endpoints/reversi/verify.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; +import * as ep___admin_accounts_present_points from './endpoints/admin/accounts/present-points.js'; import type { Provider } from '@nestjs/common'; - +import * as ep___emoji_speedtest from './endpoints/admin/emoji/speedtest.js'; const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default }; const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default }; const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default }; @@ -399,6 +412,7 @@ const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default }; const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default }; +const $admin_accounts_present_points: Provider = { provide: 'ep:admin/accounts/present-points', useClass: ep___admin_accounts_present_points.default }; const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default }; const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default }; const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default }; @@ -414,23 +428,29 @@ const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-de const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; +const $emoji_speedtest: Provider = { provide: 'ep:emoji/speedtest', useClass: ep___emoji_speedtest.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default }; const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default }; +const $admin_emoji_setlocalOnlyBulk: Provider = { provide: 'ep:admin/emoji/set-localonly-bulk', useClass: ep___admin_emoji_setlocalOnlyBulk.default }; +const $admin_emoji_setisSensitiveBulk: Provider = { provide: 'ep:admin/emoji/set-issensitive-bulk', useClass: ep___admin_emoji_setisSensitiveBulk.default }; const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default }; +const $admin_emoji_addRequest: Provider = { provide: 'ep:admin/emoji/add-request', useClass: ep___admin_emoji_addRequest.default }; const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default }; const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default }; const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default }; const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default }; const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default }; const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default }; +const $admin_emoji_listRequest: Provider = { provide: 'ep:admin/emoji/list-request', useClass: ep___admin_emoji_listRequest.default }; const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default }; const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default }; const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default }; const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default }; const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default }; +const $admin_emoji_updateRequest: Provider = { provide: 'ep:admin/emoji/update-request', useClass: ep___admin_emoji_updateRequest.default }; const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default }; const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default }; const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default }; @@ -615,6 +635,7 @@ const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep_ const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; +const $i_userstats: Provider = { provide: 'ep:i/stats', useClass: ep___users_user_stats.default }; const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default }; const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; @@ -647,6 +668,7 @@ const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invit const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; +const $emoji_requests: Provider = { provide: 'ep:emoji-requests', useClass: ep___emojiRequests.default }; const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; @@ -661,13 +683,17 @@ const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep__ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default }; const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; +const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default }; +const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; +const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default }; +const $notes_anyLocalTimeline: Provider = { provide: 'ep:notes/any-local-timeline', useClass: ep___notes_anyLocalTimeline.default }; const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default }; @@ -735,6 +761,7 @@ const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', use const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; +const $users_lists_list_favorite: Provider = { provide: 'ep:users/lists/list-favorite', useClass: ep___users_lists_list_favorite.default }; const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: ep___users_lists_pull.default }; const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; @@ -786,6 +813,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_accounts_create, $admin_accounts_delete, $admin_accounts_findByEmail, + $admin_accounts_present_points, $admin_ad_create, $admin_ad_delete, $admin_ad_list, @@ -800,24 +828,30 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, + $emoji_speedtest, $admin_unsetUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, $admin_drive_showFile, $admin_emoji_addAliasesBulk, + $admin_emoji_setlocalOnlyBulk, + $admin_emoji_setisSensitiveBulk, $admin_emoji_add, + $admin_emoji_addRequest, $admin_emoji_copy, $admin_emoji_deleteBulk, $admin_emoji_delete, $admin_emoji_importZip, $admin_emoji_listRemote, $admin_emoji_list, + $admin_emoji_listRequest, $admin_emoji_removeAliasesBulk, $admin_emoji_setAliasesBulk, $admin_emoji_setCategoryBulk, $admin_emoji_setLicenseBulk, $admin_emoji_update, + $admin_emoji_updateRequest, $admin_federation_deleteAllFiles, $admin_federation_refreshRemoteInstanceMetadata, $admin_federation_removeAllFollowing, @@ -890,6 +924,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $channels_timeline, $channels_unfollow, $channels_update, + $i_userstats, $channels_favorite, $channels_unfavorite, $channels_myFavorites, @@ -1034,6 +1069,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $invite_limit, $meta, $emojis, + $emoji_requests, $emoji, $miauth_genToken, $mute_create, @@ -1048,13 +1084,17 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_clips, $notes_conversation, $notes_create, + $notes_schedule_delete, + $notes_schedule_list, $notes_delete, + $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, $notes_globalTimeline, $notes_hybridTimeline, $notes_localTimeline, + $notes_anyLocalTimeline, $notes_mentions, $notes_polls_recommendation, $notes_polls_vote, @@ -1122,6 +1162,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $users_lists_create, $users_lists_delete, $users_lists_list, + $users_lists_list_favorite, $users_lists_pull, $users_lists_push, $users_lists_show, @@ -1167,6 +1208,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_accounts_create, $admin_accounts_delete, $admin_accounts_findByEmail, + $admin_accounts_present_points, $admin_ad_create, $admin_ad_delete, $admin_ad_list, @@ -1182,24 +1224,31 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, + $emoji_speedtest, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, $admin_drive_showFile, $admin_emoji_addAliasesBulk, $admin_emoji_add, + $admin_emoji_addRequest, $admin_emoji_copy, $admin_emoji_deleteBulk, $admin_emoji_delete, $admin_emoji_importZip, $admin_emoji_listRemote, $admin_emoji_list, + $admin_emoji_listRequest, $admin_emoji_removeAliasesBulk, $admin_emoji_setAliasesBulk, $admin_emoji_setCategoryBulk, $admin_emoji_setLicenseBulk, + $admin_emoji_setlocalOnlyBulk, + $admin_emoji_setisSensitiveBulk, $admin_emoji_update, + $admin_emoji_updateRequest, $admin_federation_deleteAllFiles, + $i_userstats, $admin_federation_refreshRemoteInstanceMetadata, $admin_federation_removeAllFollowing, $admin_federation_updateInstance, @@ -1415,6 +1464,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $invite_limit, $meta, $emojis, + $emoji_requests, $emoji, $miauth_genToken, $mute_create, @@ -1429,13 +1479,17 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_clips, $notes_conversation, $notes_create, + $notes_schedule_delete, + $notes_schedule_list, $notes_delete, + $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, $notes_globalTimeline, $notes_hybridTimeline, $notes_localTimeline, + $notes_anyLocalTimeline, $notes_mentions, $notes_polls_recommendation, $notes_polls_vote, @@ -1501,6 +1555,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $users_lists_create, $users_lists_delete, $users_lists_list, + $users_lists_list_favorite, $users_lists_pull, $users_lists_push, $users_lists_show, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index c74e7f93f6..d27aad0b0c 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; +import ProxyCheck from 'proxycheck-ts'; import { DI } from '@/di-symbols.js'; import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -108,6 +109,24 @@ export class SignupApiService { const invitationCode = body['invitationCode']; const emailAddress = body['emailAddress']; + const { DiscordWebhookUrl } = (await this.metaService.fetch()); + if (DiscordWebhookUrl) { + const data_disc = { 'username': 'ユーザー登録お知らせ', + 'content': + 'ユーザー名 :' + username + '\n' + + 'メールアドレス : ' + emailAddress + '\n' + + 'IPアドレス : ' + request.headers['x-real-ip'] ?? request.ip, + }; + + await fetch(DiscordWebhookUrl, { + 'method': 'post', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data_disc), + }); + } + if (instance.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7604162d79..3bf1242ce8 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -5,7 +5,9 @@ import { permissions } from 'misskey-js'; import type { KeyOf, Schema } from '@/misc/json-schema.js'; - +import { RolePolicies } from '@/core/RoleService.js'; +import * as ep___admin_emoji_setlocalOnlyBulk from './endpoints/admin/emoji/set-localonly-bulk.js'; +import * as ep___admin_emoji_setisSensitiveBulk from './endpoints/admin/emoji/set-issensitive-bulk.js'; import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; import * as ep___admin_abuseReport_notificationRecipient_show @@ -21,6 +23,8 @@ import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js'; +import * as ep___admin_accounts_present_points from './endpoints/admin/accounts/present-points.js'; + import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; @@ -42,17 +46,21 @@ import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_addRequest from './endpoints/admin/emoji/add-request.js'; import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_listRequest from './endpoints/admin/emoji/list-request.js'; import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; +import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js'; +import * as ep___emoji_speedtest from './endpoints/admin/emoji/speedtest.js'; import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; @@ -264,12 +272,14 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___i_user_stats from './endpoints/users/stats.js'; import * as ep___invite_create from './endpoints/invite/create.js'; import * as ep___invite_delete from './endpoints/invite/delete.js'; import * as ep___invite_list from './endpoints/invite/list.js'; import * as ep___invite_limit from './endpoints/invite/limit.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emojiRequests from './endpoints/emoji-requests.js'; import * as ep___emoji from './endpoints/emoji.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; @@ -284,13 +294,17 @@ import * as ep___notes_children from './endpoints/notes/children.js'; import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; +import * as ep___notes_anyLocalTimeline from './endpoints/notes/any-local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; @@ -358,6 +372,7 @@ import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; +import * as ep___users_lists_list_favorite from './endpoints/users/lists/list-favorite.js'; import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; @@ -391,7 +406,6 @@ import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_verify from './endpoints/reversi/verify.js'; - const eps = [ ['admin/meta', ep___admin_meta], ['admin/abuse-user-reports', ep___admin_abuseUserReports], @@ -403,6 +417,7 @@ const eps = [ ['admin/accounts/create', ep___admin_accounts_create], ['admin/accounts/delete', ep___admin_accounts_delete], ['admin/accounts/find-by-email', ep___admin_accounts_findByEmail], + ['admin/accounts/present-points', ep___admin_accounts_present_points], ['admin/ad/create', ep___admin_ad_create], ['admin/ad/delete', ep___admin_ad_delete], ['admin/ad/list', ep___admin_ad_list], @@ -424,17 +439,23 @@ const eps = [ ['admin/drive/show-file', ep___admin_drive_showFile], ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk], ['admin/emoji/add', ep___admin_emoji_add], + ['admin/emoji/add-request', ep___admin_emoji_addRequest], ['admin/emoji/copy', ep___admin_emoji_copy], ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk], ['admin/emoji/delete', ep___admin_emoji_delete], ['admin/emoji/import-zip', ep___admin_emoji_importZip], ['admin/emoji/list-remote', ep___admin_emoji_listRemote], ['admin/emoji/list', ep___admin_emoji_list], + ['admin/emoji/list-request', ep___admin_emoji_listRequest], ['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk], ['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk], ['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk], + ['admin/emoji/set-localonly-bulk', ep___admin_emoji_setlocalOnlyBulk], + ['admin/emoji/set-issensitive-bulk', ep___admin_emoji_setisSensitiveBulk], ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk], ['admin/emoji/update', ep___admin_emoji_update], + ['admin/emoji/update-request', ep___admin_emoji_updateRequest], + ['emoji/speedtest', ep___emoji_speedtest], ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles], ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata], ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing], @@ -645,12 +666,14 @@ const eps = [ ['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/update', ep___i_webhooks_update], ['i/webhooks/delete', ep___i_webhooks_delete], + ['i/stats', ep___i_user_stats], ['invite/create', ep___invite_create], ['invite/delete', ep___invite_delete], ['invite/list', ep___invite_list], ['invite/limit', ep___invite_limit], ['meta', ep___meta], ['emojis', ep___emojis], + ['emoji-requests', ep___emojiRequests], ['emoji', ep___emoji], ['miauth/gen-token', ep___miauth_genToken], ['mute/create', ep___mute_create], @@ -665,13 +688,17 @@ const eps = [ ['notes/clips', ep___notes_clips], ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], + ['notes/schedule/delete', ep___notes_schedule_delete], + ['notes/schedule/list', ep___notes_schedule_list], ['notes/delete', ep___notes_delete], + ['notes/update', ep___notes_update], ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], ['notes/local-timeline', ep___notes_localTimeline], + ['notes/any-local-timeline', ep___notes_anyLocalTimeline], ['notes/mentions', ep___notes_mentions], ['notes/polls/recommendation', ep___notes_polls_recommendation], ['notes/polls/vote', ep___notes_polls_vote], @@ -739,6 +766,7 @@ const eps = [ ['users/lists/create', ep___users_lists_create], ['users/lists/delete', ep___users_lists_delete], ['users/lists/list', ep___users_lists_list], + ['users/lists/list-favorite', ep___users_lists_list_favorite], ['users/lists/pull', ep___users_lists_pull], ['users/lists/push', ep___users_lists_push], ['users/lists/show', ep___users_lists_show], diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/present-points.ts b/packages/backend/src/server/api/endpoints/admin/accounts/present-points.ts new file mode 100644 index 0000000000..d69c6b83c7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/accounts/present-points.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { NotificationService } from '@/core/NotificationService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireAdmin: true, + kind: 'write:admin:account', +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + points: { type: 'number' }, + }, + required: ['userId', 'points'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private notificationService: NotificationService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + this.usersRepository.update( user.id, { + getPoints: user.getPoints + ps.points, + }); + this.notificationService.createNotification(user.id, 'loginbonus', { + loginbonus: ps.points, + }); + + return {}; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index 3adb6c38fc..931474eab5 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -24,6 +24,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['name', 'description', 'url'], } as const; @@ -39,6 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category ?? '', }, me); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index d78c92de38..d0831c7ec0 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -95,6 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- name: avatarDecoration.name, description: avatarDecoration.description, url: avatarDecoration.url, + category: avatarDecoration.category, roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration, })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index d33efa5faa..8b68049c3b 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -30,6 +30,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['id'], } as const; @@ -45,6 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category ?? '', }, me); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-request.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-request.ts new file mode 100644 index 0000000000..147073c2b4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-request.ts @@ -0,0 +1,154 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { ApiError } from '../../../error.js'; +import {MetaService} from "@/core/MetaService.js"; +import {DriveService} from "@/core/DriveService.js"; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canRequestCustomEmojis', + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', + }, + duplicateName: { + message: 'Duplicate name.', + code: 'DUPLICATE_NAME', + id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean', nullable: true }, + localOnly: { type: 'boolean', nullable: true }, + fileId: { type: 'string', format: 'misskey:id' }, + isNotifyIsHome: { type: 'boolean', nullable: true }, + }, + required: ['name', 'fileId'], +} as const; + +// TODO: ロジックをサービスに切り出す + +@Injectable() +// eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private metaService: MetaService, + private customEmojiService: CustomEmojiService, + private driveService: DriveService, + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + const isRequestDuplicate = await this.customEmojiService.checkRequestDuplicate(ps.name); + + if (isDuplicate || isRequestDuplicate) throw new ApiError(meta.errors.duplicateName); + let driveFile; + let tmp = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (tmp == null) throw new ApiError(meta.errors.noSuchFile); + + try { + driveFile = await this.driveService.uploadFromUrl({ url: tmp.url , user: null, force: true }); + } catch (e) { + throw new ApiError(); + } + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + const {ApiBase,EmojiBotToken,DiscordWebhookUrl,requestEmojiAllOk} = (await this.metaService.fetch()) + let emoji; + if (requestEmojiAllOk){ + emoji = await this.customEmojiService.add({ + driveFile, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + host: null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + }); + }else{ + emoji = await this.customEmojiService.request({ + driveFile, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + }); + } + + + await this.moderationLogService.log(me, 'addCustomEmoji', { + emojiId: emoji.id, + emoji: emoji, + }); + + if (EmojiBotToken){ + const data_Miss = { + 'i': EmojiBotToken, + 'visibility': ps.isNotifyIsHome ? 'home' : 'public', + 'text': + '絵文字名 : :' + ps.name + ':\n' + + 'カテゴリ : ' + ps.category + '\n' + + 'ライセンス : ' + ps.license + '\n' + + 'タグ : ' + ps.aliases + '\n' + + '追加したユーザー : ' + '@' + me.username + '\n' + }; + await fetch(ApiBase+'/notes/create', { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body:JSON.stringify( data_Miss) + }) + } + + if (DiscordWebhookUrl){ + const data_disc = {"username": "絵文字追加通知ちゃん", + 'content': + '絵文字名 : :'+ ps.name +':\n' + + 'カテゴリ : ' + ps.category + '\n'+ + 'ライセンス : '+ ps.license + '\n'+ + 'タグ : '+ps.aliases+ '\n'+ + '追加したユーザー : ' + '@'+me.username + '\n' + } + await fetch(DiscordWebhookUrl, { + 'method':'post', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data_disc), + }) + } + return { + id: emoji.id, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 628b4efedc..a5533b6ec2 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -47,15 +47,19 @@ export const paramDef = { nullable: true, description: 'Use `null` to reset the category.', }, - aliases: { type: 'array', items: { - type: 'string', - } }, + aliases: { + type: 'array', items: { + type: 'string', + }, + }, license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { - type: 'string', - } }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', items: { + type: 'string', + }, + }, }, required: ['name', 'fileId'], } as const; @@ -67,13 +71,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private customEmojiService: CustomEmojiService, - private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); if (isDuplicate) throw new ApiError(meta.errors.duplicateName); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index a4de16437b..3d3be444ba 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -37,7 +37,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.delete(ps.id, me); + const emoji = await this.customEmojiService.getEmojiById(ps.id); + const RequestEmoji = await this.customEmojiService.getEmojiRequestById(ps.id); + if (emoji != null) { + await this.customEmojiService.delete(ps.id, me); + } + if (RequestEmoji != null) { + await this.customEmojiService.deleteRequest(ps.id); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-request.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-request.ts new file mode 100644 index 0000000000..f5faca972f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-request.ts @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { EmojiRequestsRepository } from '@/models/_.js'; +import type { MiEmojiRequest } from '@/models/EmojiRequest.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { EmojiRequestsEntityService } from '@/core/entities/EmojiRequestsEntityService.js'; +//import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + query: { type: 'string', nullable: true, default: null }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + + private emojiRequestsEntityService: EmojiRequestsEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const q = this.queryService.makePaginationQuery(this.emojiRequestsRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); + + let emojis: MiEmojiRequest[]; + + if (ps.query) { + //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); + //const emojis = await q.limit(ps.limit).getMany(); + + emojis = await q.getMany(); + const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); + + if (queryarry) { + emojis = emojis.filter(emoji => + queryarry.includes(`:${emoji.name}:`), + ); + } else { + emojis = emojis.filter(emoji => + emoji.name.includes(ps.query!) || + emoji.aliases.some(a => a.includes(ps.query!)) || + emoji.category?.includes(ps.query!)); + } + emojis.splice(ps.limit + 1); + } else { + emojis = await q.limit(ps.limit).getMany(); + } + + return this.emojiRequestsEntityService.packDetailedMany(emojis); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 8ea3bb6546..3ce9881524 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -65,6 +65,7 @@ export const paramDef = { type: 'object', properties: { query: { type: 'string', nullable: true, default: null }, + draft: { type: 'boolean', nullable: true, default: null }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -87,6 +88,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- let emojis: MiEmoji[]; + if (ps.draft !== null) { + if (ps.draft) { + q.andWhere('emoji.draft = TRUE'); + } else { + q.andWhere('emoji.draft = FALSE'); + } + } + if (ps.query) { //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-issensitive-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-issensitive-bulk.ts new file mode 100644 index 0000000000..9206e00e21 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-issensitive-bulk.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', +} as const; + +export const paramDef = { + type: 'object', + properties: { + ids: { type: 'array', items: { + type: 'string', format: 'misskey:id', + } }, + isSensitive: { + type: 'boolean', + nullable: false, + description: 'Use `null` to reset the licence.', + }, + }, + required: ['ids'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private customEmojiService: CustomEmojiService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.customEmojiService.setisSensitiveBulk(ps.ids, ps.isSensitive ?? false); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-localonly-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-localonly-bulk.ts new file mode 100644 index 0000000000..662bf24534 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-localonly-bulk.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', +} as const; + +export const paramDef = { + type: 'object', + properties: { + ids: { type: 'array', items: { + type: 'string', format: 'misskey:id', + } }, + localOnly: { + type: 'boolean', + nullable: false, + description: 'Use `null` to reset the licence.', + }, + }, + required: ['ids'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private customEmojiService: CustomEmojiService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.customEmojiService.setLocalOnlyBulk(ps.ids, ps.localOnly ?? false); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/speedtest.ts b/packages/backend/src/server/api/endpoints/admin/emoji/speedtest.ts new file mode 100644 index 0000000000..ca2a4599db --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/speedtest.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import sharp from 'sharp'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; + +export const meta = { + tags: ['admin'], + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', +} as const; + +export const paramDef = { + type: 'object', + properties: { + url: { + type: 'string', + }, + }, + required: ['url'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private customEmojiService: CustomEmojiService, + ) { + super(meta, paramDef, async (ps, me) => { + const response = await fetch(ps.url, { + 'headers': { + 'accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + }, + 'method': 'GET', + }); + + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); + } + const buffer = await response.arrayBuffer(); + const metadata = await sharp(buffer).metadata(); + + if (!metadata.pages) { + throw new Error('Invalid image format or no animation frames found.'); + } + + const frameRate = metadata.delay && metadata.delay.length > 0 + ? 1000 / metadata.delay[0] + : 30; // Fallback to 30 FPS if no delay information is present + + const colorsPerFrame: number[] = []; + for (let i = 0; i < metadata.pages; i++) { + const { data, info } = await sharp(buffer, { page: i }).raw().toBuffer({ resolveWithObject: true }); + const uniqueColors = new Set<string>(); + for (let y = 0; y < info.height; y++) { + for (let x = 0; x < info.width; x++) { + const offset = (y * info.width + x) * info.channels; + const color = `${data[offset]}-${data[offset + 1]}-${data[offset + 2]}`; + uniqueColors.add(color); + } + } + colorsPerFrame.push(uniqueColors.size); + } + + const colorChanges = colorsPerFrame.map((colorCount, index, arr) => { + if (index === 0) return 0; + return Math.abs(colorCount - arr[index - 1]); + }); + + const averageColorChangePerSecond = colorChanges.reduce((sum, change) => sum + change, 0) / colorsPerFrame.length; + console.log('Average color change per second:', 10 < averageColorChangePerSecond); + return Boolean(10 < averageColorChangePerSecond); + // You can store or use this information as needed + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update-request.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update-request.ts new file mode 100644 index 0000000000..4be16bb780 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update-request.ts @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + + errors: { + noSuchEmoji: { + message: 'No such emoji.', + code: 'NO_SUCH_EMOJI', + id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', + }, + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d', + }, + sameNameEmojiExists: { + message: 'Emoji that have same name already exists.', + code: 'SAME_NAME_EMOJI_EXISTS', + id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + fileId: { type: 'string', format: 'misskey:id' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, + Request: { type: 'boolean' }, + }, + required: ['id', 'name', 'aliases'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private customEmojiService: CustomEmojiService, + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + let driveFile; + const isRequest = !!ps.Request; + if (ps.fileId) { + driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + } + + const emoji = await this.customEmojiService.getEmojiRequestById(ps.id); + if (emoji != null) { + if (ps.name !== emoji.name) { + const isDuplicate = await this.customEmojiService.checkRequestDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); + } + } else { + throw new ApiError(meta.errors.noSuchEmoji); + } + if (!isRequest) { + const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl); + if (file === null) throw new ApiError(meta.errors.noSuchFile); + await this.customEmojiService.add({ + driveFile: file, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + host: null, + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + }, me); + await this.customEmojiService.deleteRequest(ps.id); + } else { + await this.customEmojiService.updateRequest(ps.id, { + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + }, me); + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 90d258cad2..07b293654a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -5,8 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository , EmojisRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -33,6 +34,11 @@ export const meta = { code: 'SAME_NAME_EMOJI_EXISTS', id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', }, + duplicationEmojiAdd: { + message: 'This emoji is already added.', + code: 'DUPLICATION_EMOJI_ADD', + id: 'mattyaski_emoji_duplication_error', + } }, } as const; @@ -56,6 +62,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + Request: { type: 'boolean' }, }, anyOf: [ { required: ['id'] }, @@ -68,11 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private customEmojiService: CustomEmojiService, + private driveFileEntityService: DriveFileEntityService, ) { super(meta, paramDef, async (ps, me) => { let driveFile; + const isRequest = !!ps.Request; if (ps.fileId) { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); @@ -94,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- emojiId = emoji.id; } - await this.customEmojiService.update(emojiId, { + if (!isRequest) {await this.customEmojiService.update(emojiId, { driveFile, name: ps.name, category: ps.category, @@ -103,7 +111,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, - }, me); + draft: false, + }, me);} else { + const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl); + if (file === null) throw new ApiError(meta.errors.noSuchFile); + await this.customEmojiService.request({ + driveFile: file, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + }, me); + await this.customEmojiService.delete(ps.id); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 81c1e38604..66ee9572f7 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -457,6 +457,25 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + enableGDPRMode: { + type: 'boolean', + optional: false, nullable: false, + }, + DiscordWebhookUrl: { + type: 'string', + optional: false, nullable: true, + }, DiscordWebhookUrlWordBlock: { + type: 'string', + optional: false, nullable: true, + }, + enableProxyCheckio: { + type: 'boolean', + optional: false, nullable: false, + }, + proxyCheckioApiKey: { + type: 'string', + optional: false, nullable: true, + }, urlPreviewEnabled: { type: 'boolean', optional: false, nullable: false, @@ -481,6 +500,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + iconLight: { type: 'string', nullable: true }, + iconDark: { type: 'string', nullable: true }, + bannerLight: { type: 'string', nullable: true }, + bannerDark: { type: 'string', nullable: true }, }, }, } as const; @@ -531,6 +554,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- turnstileSiteKey: instance.turnstileSiteKey, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, + requestEmojiAllOk: instance.requestEmojiAllOk, mascotImageUrl: instance.mascotImageUrl, bannerUrl: instance.bannerUrl, serverErrorImageUrl: instance.serverErrorImageUrl, @@ -607,6 +631,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, notesPerOneAd: instance.notesPerOneAd, + DiscordWebhookUrl: instance.DiscordWebhookUrl, + DiscordWebhookUrlWordBlock: instance.DiscordWebhookUrlWordBlock, + EmojiBotToken: instance.EmojiBotToken, + ApiBase: instance.ApiBase, + enableGDPRMode: instance.enableGDPRMode, + enableProxyCheckio: instance.enableProxyCheckio, + proxyCheckioApiKey: instance.proxyCheckioApiKey, summalyProxy: instance.urlPreviewSummaryProxyUrl, urlPreviewEnabled: instance.urlPreviewEnabled, urlPreviewTimeout: instance.urlPreviewTimeout, @@ -614,6 +645,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, urlPreviewUserAgent: instance.urlPreviewUserAgent, urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, + iconLight: instance.iconLight, + iconDark: instance.iconDark, + bannerLight: instance.bannerLight, + bannerDark: instance.bannerDark, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index e2036f8c2d..bb66ae3583 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue, UserWebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 7a9662677e..d28ec641e8 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -50,6 +50,7 @@ export const paramDef = { mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, serverErrorImageUrl: { type: 'string', nullable: true }, + googleAnalyticsId: { type: 'string', nullable: true }, infoImageUrl: { type: 'string', nullable: true }, notFoundImageUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true }, @@ -90,6 +91,9 @@ export const paramDef = { type: 'string', }, }, + summalyProxy: { type: 'string', nullable: true }, + DiscordWebhookUrl: { type: 'string', nullable: true }, + DiscordWebhookUrlWordBlock: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, enableEmail: { type: 'boolean' }, @@ -110,6 +114,7 @@ export const paramDef = { inquiryUrl: { type: 'string', nullable: true }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, + requestEmojiAllOk: { type: 'boolean', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, objectStoragePrefix: { type: 'string', nullable: true }, objectStorageEndpoint: { type: 'string', nullable: true }, @@ -160,6 +165,19 @@ export const paramDef = { urlPreviewRequireContentLength: { type: 'boolean' }, urlPreviewUserAgent: { type: 'string', nullable: true }, urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, + EmojiBotToken: { type: 'string', nullable: true }, + ApiBase: { type: 'string', nullable: true }, + enableGDPRMode: { type: 'boolean' }, + enableProxyCheckio: { + type: 'boolean', nullable: true, + }, + proxyCheckioApiKey: { + type: 'string', nullable: true, + }, + iconLight: { type: 'string', nullable: true }, + iconDark: { type: 'string', nullable: true }, + bannerLight: { type: 'string', nullable: true }, + bannerDark: { type: 'string', nullable: true }, }, required: [], } as const; @@ -206,7 +224,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } - + if (ps.DiscordWebhookUrl !== undefined) { + set.DiscordWebhookUrl = ps.DiscordWebhookUrl; + } + if (ps.DiscordWebhookUrlWordBlock !== undefined) { + set.DiscordWebhookUrlWordBlock = ps.DiscordWebhookUrlWordBlock; + } + if (ps.EmojiBotToken !== undefined) { + set.EmojiBotToken = ps.EmojiBotToken; + } + if (ps.ApiBase !== undefined) { + set.ApiBase = ps.ApiBase; + } if (ps.mascotImageUrl !== undefined) { set.mascotImageUrl = ps.mascotImageUrl; } @@ -230,11 +259,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.serverErrorImageUrl !== undefined) { set.serverErrorImageUrl = ps.serverErrorImageUrl; } + if (ps.googleAnalyticsId !== undefined) { + set.googleAnalyticsId = ps.googleAnalyticsId; + } + if (ps.enableProxyCheckio !== undefined) { + set.enableProxyCheckio = ps.enableProxyCheckio; + } + + if (ps.proxyCheckioApiKey !== undefined) { + set.proxyCheckioApiKey = ps.proxyCheckioApiKey; + } if (ps.infoImageUrl !== undefined) { set.infoImageUrl = ps.infoImageUrl; } + if (ps.enableGDPRMode !== undefined) { + set.enableGDPRMode = ps.enableGDPRMode; + } + + if (ps.requestEmojiAllOk !== undefined) { + set.requestEmojiAllOk = ps.requestEmojiAllOk; + } if (ps.notFoundImageUrl !== undefined) { set.notFoundImageUrl = ps.notFoundImageUrl; } @@ -616,7 +662,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim(); set.urlPreviewSummaryProxyUrl = value === '' ? null : value; } - + if (ps.bannerDark !== undefined) { + set.bannerDark = ps.bannerDark; + } + if (ps.bannerLight !== undefined) { + set.bannerLight = ps.bannerLight; + } + if (ps.iconDark !== undefined) { + set.iconDark = ps.iconDark; + } + if (ps.iconLight !== undefined) { + set.iconLight = ps.iconLight; + } const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 8f65edd951..a05a055412 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -75,7 +75,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); query.andWhere(':file <@ note.fileIds', { file: [file.id] }); - const notes = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me, { diff --git a/packages/backend/src/server/api/endpoints/emoji-requests.ts b/packages/backend/src/server/api/endpoints/emoji-requests.ts new file mode 100644 index 0000000000..63ca88e93e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/emoji-requests.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { EmojiRequestsRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojiRequestsEntityService } from '@/core/entities/EmojiRequestsEntityService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + emojis: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'EmojiRequestSimple', + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + + private emojiRequestsEntityService: EmojiRequestsEntityService, + ) { + super(meta, paramDef, async () => { + const emojis = await this.emojiRequestsRepository.find({ + order: { + category: 'ASC', + name: 'ASC', + }, + }); + + return { + emojis: await this.emojiRequestsEntityService.packSimpleMany(emojis), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 9c560aa2a2..085fcf1891 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -75,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- name: decoration.name, description: decoration.description, url: decoration.url, + category: decoration.category, roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)), })); }); diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 25d9a859cb..88d666d96a 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -4,17 +4,18 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { ApiError } from '../error.js'; export const meta = { tags: ['account'], requireCredential: true, - kind: "read:account", + kind: 'read:account', res: { type: 'object', @@ -43,7 +44,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private notificationService: NotificationService, private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, user, token) => { @@ -52,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const now = new Date(); const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; + let todayGetPoints = 0; // 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得 const userProfile = await this.userProfilesRepository.findOne({ where: { @@ -64,10 +69,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.userIsDeleted); } + function generateSecureRandomNumber(min, max) { + const range = max - min + 1; + const randomBuffer = new Uint32Array(1); + crypto.getRandomValues(randomBuffer); + const randomNumber = randomBuffer[0] / (0xFFFFFFFF + 1); // 0から1未満の浮動小数点数 + return Math.floor(randomNumber * range) + min; + } + if (!userProfile.loggedInDates.includes(today)) { + todayGetPoints = generateSecureRandomNumber(1, 5); this.userProfilesRepository.update({ userId: user.id }, { loggedInDates: [...userProfile.loggedInDates, today], }); + const user_ = await this.usersRepository.findOne({ + where: { + id: user.id, + }, + }); + if (user_ == null) { + throw new ApiError(meta.errors.userIsDeleted); + } + this.usersRepository.update( user.id, { + getPoints: user_.getPoints + todayGetPoints, + }); + this.notificationService.createNotification(user.id, 'loginbonus', { + loginbonus: todayGetPoints, + }); userProfile.loggedInDates = [...userProfile.loggedInDates, today]; } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 74d0fb9952..6f17343d3b 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -173,6 +173,7 @@ export const paramDef = { preventAiLearning: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, + isGorilla: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, @@ -311,7 +312,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; - if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; + if (typeof ps.isCat === 'boolean' && !ps.isGorilla) { + updates.isCat = ps.isCat; + } + if (typeof ps.isGorilla === 'boolean' && !ps.isCat) { + updates.isGorilla = ps.isGorilla; + } if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { @@ -356,11 +362,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations - .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) + .filter(d => (d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))) .map(d => d.id); if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); - updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, angle: d.angle ?? 0, @@ -427,7 +432,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const newName = updates.name === undefined ? user.name : updates.name; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; - const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; + const newFields = profileUpdates.fields ?? profile.fields; if (newName != null) { const tokens = mfm.parseSimple(newName); @@ -455,10 +460,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // ハッシュタグ更新 this.hashtagService.updateUsertags(user, tags); //#endregion - if (Object.keys(updates).length > 0) { await this.usersRepository.update(user.id, updates); - this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); + //this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); } await this.userProfilesRepository.update(user.id, { @@ -473,24 +477,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const updatedProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - this.cacheService.userProfileCache.set(user.id, updatedProfile); + await this.cacheService.userProfileCache.set(user.id, updatedProfile); // Publish meUpdated event this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj); // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 if (user.isLocked && ps.isLocked === false) { - this.userFollowingService.acceptAllFollowRequests(user); + await this.userFollowingService.acceptAllFollowRequests(user); } // フォロワーにUpdateを配信 - this.accountUpdateService.publishToFollowers(user.id); + await this.accountUpdateService.publishToFollowers(user.id); const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://')); for (const url of urls) { - this.verifyLink(url.value, user); + await this.verifyLink(url.value, user); } - return iObj; }); } diff --git a/packages/backend/src/server/api/endpoints/notes/any-local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/any-local-timeline.ts new file mode 100644 index 0000000000..077252da43 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/any-local-timeline.ts @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets, In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; +import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; +import { getApId, isActor, isPost } from '@/core/activitypub/type.js'; +import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import type { NotesRepository } from '@/models/_.js'; +import { MiNote, MiUser } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { MiLocalUser } from '@/models/User.js'; +import { SchemaType } from '@/misc/json-schema.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, + + errors: { + hostIsNull: { + message: 'Host is null', + code: 'HOST_NULL', + id: 'PRSMSK-ANY-LTL-0001', + }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', + }, + remoteTokenIsNull: { + message: 'remoteToken is null', + code: 'REMOTE_TOKEN_NULL', + id: 'PRSMSK-ANY-LTL-0002', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + withFiles: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + host: { type: 'string' }, + remoteToken: { type: 'string' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private idService: IdService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private queryService: QueryService, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: true, + redisTimelines: [`remoteLocalTimeline:${ps.host}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + host: ps.host, + }, me), + }); + + return timeline; + }, + + ); + } + private async getFromDb(ps: { + sinceId: string | null, + untilId: string | null, + limit: number, + withFiles: boolean, + withReplies: boolean, + host: string, + }, me: MiLocalUser | null) { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId) + .andWhere(`(note.visibility = \'public\') AND (note.userHost = \'${ps.host}\') AND (note.channelId IS NULL)`) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (!ps.withReplies) { + query.andWhere(new Brackets(qb => { + qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere(new Brackets(qb => { + qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } + + return await query.limit(ps.limit).getMany(); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index e3e0685eed..a48ff87cf9 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -7,7 +7,8 @@ import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { MiUser } from '@/models/User.js'; -import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, ScheduledNotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; +import type { MiNoteCreateOption } from '@/types.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; @@ -15,6 +16,9 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { IdService } from '@/core/IdService.js'; +import { RoleService } from '@/core/RoleService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { MetaService } from '@/core/MetaService.js'; @@ -42,9 +46,17 @@ export const meta = { properties: { createdNote: { type: 'object', - optional: false, nullable: false, + optional: false, nullable: true, ref: 'Note', }, + scheduledNoteId: { + type: 'string', + optional: true, nullable: true, + }, + scheduledNote: { + type: 'object', + optional: true, nullable: true, + }, }, }, @@ -121,6 +133,27 @@ export const meta = { id: '33510210-8452-094c-6227-4a6c05d99f00', }, + cannotCreateAlreadyExpiredSchedule: { + message: 'Schedule is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_SCHEDULE', + id: '8a9bfb90-fc7e-4878-a3e8-d97faaf5fb07', + }, + specifyScheduleDate: { + message: 'Please specify schedule date.', + code: 'PLEASE_SPECIFY_SCHEDULE_DATE', + id: 'c93a6ad6-f7e2-4156-a0c2-3d03529e5e0f', + }, + noSuchSchedule: { + message: 'No such schedule.', + code: 'NO_SUCH_SCHEDULE', + id: '44dee229-8da1-4a61-856d-e3a4bbc12032', + }, + rolePermissionDenied: { + message: 'You are not assigned to a required role.', + code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', + id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a', + }, containsProhibitedWords: { message: 'Cannot post because it contains prohibited words.', code: 'CONTAINS_PROHIBITED_WORDS', @@ -191,6 +224,13 @@ export const paramDef = { }, required: ['choices'], }, + schedule: { + type: 'object', + nullable: true, + properties: { + scheduledAt: { type: 'string', nullable: false }, + }, + }, }, // (re)note with text, files and poll are optional if: { @@ -207,6 +247,9 @@ export const paramDef = { poll: { type: 'null', }, + schedule:{ + type: 'null' + } }, }, then: { @@ -231,6 +274,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.scheduledNotesRepository) + private scheduledNotesRepository: ScheduledNotesRepository, + @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -242,6 +288,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private noteCreateService: NoteCreateService, + + private roleService: RoleService, + private queueService: QueueService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { let visibleUsers: MiUser[] = []; @@ -313,7 +363,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } } - + let visibility = ps.visibility; let reply: MiNote | null = null; if (ps.replyId != null) { // Fetch reply @@ -364,7 +414,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // 投稿を作成 try { - const note = await this.noteCreateService.create(me, { + const note : MiNoteCreateOption = { createdAt: new Date(), files: files, poll: ps.poll ? { @@ -384,11 +434,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, - }); - - return { - createdNote: await this.noteEntityService.pack(note, me), }; + + if (ps.schedule) { + // 予約投稿 + const canCreateScheduledNote = (await this.roleService.getUserPolicies(me.id)).canScheduleNote; + if (!canCreateScheduledNote) { + throw new ApiError(meta.errors.rolePermissionDenied); + } + + if (!ps.schedule.scheduledAt) { + throw new ApiError(meta.errors.specifyScheduleDate); + } + + me.token = null; + const scheduledNoteId = this.idService.gen(new Date().getTime()); + await this.scheduledNotesRepository.insert({ + id: scheduledNoteId, + note: note, + userId: me.id, + scheduledAt: new Date(ps.schedule.scheduledAt), + }); + + const delay = new Date(ps.schedule.scheduledAt).getTime() - Date.now(); + await this.queueService.ScheduleNotePostQueue.add(delay.toString(), { + scheduledNoteId, + }, { + jobId: scheduledNoteId, + delay, + removeOnComplete: true, + }); + + return { + scheduledNoteId, + scheduledNote: note, + + // ↓互換性のため(微妙) + createdNote: null, + }; + } else { + // 投稿を作成 + const createdNoteRaw = await this.noteCreateService.create(me, note); + return { + createdNote: await this.noteEntityService.pack(createdNoteRaw, me), + }; + } } catch (e) { // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい if (e instanceof IdentifiableError) { diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 1f6a9c73dc..fab2d6db7a 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -102,6 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }); notes.sort((a, b) => a.id > b.id ? -1 : 1); + // TODO: ミュート等考慮 + return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index 9d1815de5a..1e14e811f4 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -42,6 +42,7 @@ export const paramDef = { type: 'object', properties: { noteId: { type: 'string', format: 'misskey:id' }, + reaction: { type: 'string' }, }, required: ['noteId'], } as const; @@ -57,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - await this.reactionService.delete(me, note).catch(err => { + await this.reactionService.delete(me, note, ps.reaction ?? undefined).catch(err => { if (err.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts new file mode 100644 index 0000000000..e108016e80 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import type { ScheduledNotesRepository } from '@/models/_.js'; +import { QueueService } from '@/core/QueueService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: '490be23f-8c1f-4796-819f-94cb4f9d1630', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + scheduledNoteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['scheduledNoteId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.scheduledNotesRepository) + private scheduledNotesRepository: ScheduledNotesRepository, + + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.scheduledNotesRepository.delete({ id: ps.scheduledNoteId }); + if (ps.scheduledNoteId) { + await this.queueService.ScheduleNotePostQueue.remove(ps.scheduledNoteId); + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts new file mode 100644 index 0000000000..9f89cf2a7f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import type { ScheduledNotesRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { IdService } from '@/core/IdService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + requireRolePolicy: 'canScheduleNote', + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.scheduledNotesRepository) + private scheduledNotesRepository: ScheduledNotesRepository, + + private idService: IdService, + private userEntityService: UserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.scheduledNotesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: me.id }); + + const scheduleNotes = await query.limit(ps.limit).getMany(); + const user = await this.userEntityService.pack(me, me); + const scheduleNotesPack = scheduleNotes.map((item) => { + return { + ...item, + scheduledAt: new Date(item.scheduledAt).toISOString(), + note: { + ...item.note, + user: user, + createdAt: new Date(item.scheduledAt).toISOString(), + isSchedule: true, + id: null, + scheduledNoteId: item.id, + }, + }; + }); + + return scheduleNotesPack; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 7ec7411db9..0ba7478c9f 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -48,7 +48,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - return await this.noteEntityService.pack(note, me, { detail: true, }); diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts new file mode 100644 index 0000000000..e05a70c0e3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -0,0 +1,177 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, NotesRepository, DriveFilesRepository, MiDriveFile } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + requireRolePolicy: 'canEditNote', + + kind: 'write:notes', + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', + }, + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, + cannotCreateAlreadyExpiredPoll: { + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibleUserIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + cw: { type: 'string', nullable: true, maxLength: 100 }, + localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + noExtractMentions: { type: 'boolean', default: false }, + noExtractHashtags: { type: 'boolean', default: false }, + noExtractEmojis: { type: 'boolean', default: false }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + channelId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false, + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, + disableRightClick: { type: 'boolean', default: false }, + }, + required: ['noteId', 'text', 'cw'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private getterService: GetterService, + private noteEntityService: NoteEntityService, + private noteUpdateService: NoteUpdateService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + if (note.userId !== me.id) { + throw new ApiError(meta.errors.noSuchNote); + } + + let files: MiDriveFile[] = []; + const fileIds = ps.fileIds ?? ps.mediaIds ?? null; + if (fileIds != null) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + + if (files.length !== fileIds.length) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + if (ps.poll) { + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; + } + } + + const data = { + text: ps.text, + files: files, + cw: ps.cw, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + }; + + const updatedNote = await this.noteUpdateService.update(me, data, note, false); + + return { + updatedNote: await this.noteEntityService.pack(updatedNote!, me), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index ad748d8071..a8be7d474e 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -91,6 +91,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const list = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }) ?? await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, }); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index e6a52b5e2f..ff74e9d526 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -13,6 +13,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { ApiError } from '../../error.js'; +import {RoleService} from "@/core/RoleService.js"; export const meta = { tags: ['role', 'notes'], @@ -63,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - + private roleService: RoleService, private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, @@ -72,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - + const isModerator = await this.roleService.isModerator(me); const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, @@ -91,15 +92,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (noteIds.length === 0) { return []; } - - const query = this.notesRepository.createQueryBuilder('note') + const query = isModerator ? this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .andWhere('(note.visibility = \'public\')') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + .leftJoinAndSelect('renote.user', 'renoteUser') : + this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .andWhere('(note.visibility = \'public\')' ) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); @@ -107,7 +114,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const notes = await query.getMany(); notes.sort((a, b) => a.id > b.id ? -1 : 1); - return await this.noteEntityService.packMany(notes, me); }); } diff --git a/packages/backend/src/server/api/endpoints/users/lists/list-favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/list-favorite.ts new file mode 100644 index 0000000000..a7d01afc86 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/list-favorite.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; +import type { UserListsRepository, UserListFavoritesRepository } from '@/models/_.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { QueryService } from '@/core/QueryService.js'; +export const meta = { + tags: ['lists', 'account'], + + requireCredential: false, + + kind: 'read:account', + + description: 'Show all lists that the authenticated user has created.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'UserList', + }, + }, + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e', + }, + remoteUser: { + message: 'Not allowed to load the remote user\'s list', + code: 'REMOTE_USER_NOT_ALLOWED', + id: '53858f1b-3315-4a01-81b7-db9b48d4b79a', + }, + invalidParam: { + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: 'ab36de0e-29e9-48cb-9732-d82f1281620d', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + private userListEntityService: UserListEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + if (!me) { + throw new ApiError(meta.errors.noSuchUser); + } + const favorites = await this.userListFavoritesRepository.findBy({ userId: me.id }); + + if (favorites == null) { + return []; + } + const listIds = favorites.map(favorite => favorite.userListId); + const lists = await this.userListsRepository.findBy({ id: In(listIds) }); + return await Promise.all(lists.map(async list => await this.userListEntityService.pack(list))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 36caf98077..e0a8b2d675 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -51,6 +51,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, + publicAll: { type: 'boolean', nullable: false }, }, required: [], } as const; @@ -67,22 +68,29 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - if (typeof ps.userId !== 'undefined') { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - if (user === null) throw new ApiError(meta.errors.noSuchUser); - if (user.host !== null) throw new ApiError(meta.errors.remoteUser); - } else if (me === null) { - throw new ApiError(meta.errors.invalidParam); + if (!ps.publicAll ) { + if (typeof ps.userId !== 'undefined') { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user === null) throw new ApiError(meta.errors.noSuchUser); + if (user.host !== null) throw new ApiError(meta.errors.remoteUser); + } else if (me === null) { + throw new ApiError(meta.errors.invalidParam); + } + + const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? { + userId: me.id, + } : { + userId: ps.userId, + isPublic: true, + }); + + return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); + } else { + const userLists = await this.userListsRepository.findBy({ + isPublic: true, + }); + return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); } - - const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? { - userId: me.id, - } : { - userId: ps.userId, - isPublic: true, - }); - - return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index f6fafd3895..86568f0910 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -132,9 +132,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - return Array.isArray(ps.userId) - ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()]) - : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]); + const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; + + const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); + + return Array.isArray(ps.userId) ? relations : relations[0]; }); } } diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 053e8d7b76..6b267fa85c 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -2,9 +2,18 @@ * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project * SPDX-License-Identifier: AGPL-3.0-only */ - -import { Injectable } from '@nestjs/common'; +import { setImmediate } from 'node:timers/promises'; +import sanitizeHtml from 'sanitize-html'; +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import type { AbuseUserReportsRepository, NotesRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { EmailService } from '@/core/EmailService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; @@ -44,6 +53,7 @@ export const paramDef = { properties: { userId: { type: 'string', format: 'misskey:id' }, comment: { type: 'string', minLength: 1, maxLength: 2048 }, + noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id', maxLength: 16 } }, }, required: ['userId', 'comment'], } as const; @@ -51,8 +61,19 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private idService: IdService, + private metaService: MetaService, + private emailService: EmailService, private getterService: GetterService, private roleService: RoleService, + private noteEntityService: NoteEntityService, + private globalEventService: GlobalEventService, private abuseReportService: AbuseReportService, ) { super(meta, paramDef, async (ps, me) => { @@ -70,13 +91,59 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.cannotReportAdmin); } - await this.abuseReportService.report([{ + const notes = ps.noteIds ? await this.notesRepository.find({ + where: { id: In(ps.noteIds), userId: targetUser.id }, + }) : []; + + const report = await this.abuseUserReportsRepository.insert({ + id: this.idService.gen(), targetUserId: targetUser.id, targetUserHost: targetUser.host, reporterId: me.id, reporterHost: null, comment: ps.comment, - }]); + notes: (ps.noteIds && !((await this.metaService.fetch()).enableGDPRMode)) ? await this.noteEntityService.packMany(notes) : [], + noteIds: (ps.noteIds && (await this.metaService.fetch()).enableGDPRMode) ? ps.noteIds : [], + }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); + + // Publish event to moderators + setImmediate(async () => { + const moderators = await this.roleService.getModerators(); + + for (const moderator of moderators) { + this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', { + id: report.id, + targetUserId: report.targetUserId, + reporterId: report.reporterId, + comment: report.comment, + notes: report.notes, + noteIds: report.noteIds ?? [], + }); + } + const meta = await this.metaService.fetch(); + if (meta.DiscordWebhookUrl) { + const data_disc = { 'username': '絵文字追加通知ちゃん', + 'content': + + '通報' + '\n' + + '通報' + report.comment + '\n' + + '通報したユーザー : ' + '@' + me.username + '\n' + + '通報されたユーザー : ' + report.targetUserId + '\n', + }; + await fetch(meta.DiscordWebhookUrl, { + 'method': 'post', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data_disc), + }); + } + if (meta.email) { + this.emailService.sendEmail(meta.email, 'New abuse report', + sanitizeHtml(ps.comment), + sanitizeHtml(ps.comment)); + } + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts new file mode 100644 index 0000000000..6ee9eba839 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -0,0 +1,228 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/_.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + description: 'Show statistics about a user.', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + notesCount: { + type: 'integer', + optional: false, nullable: false, + }, + repliesCount: { + type: 'integer', + optional: false, nullable: false, + }, + renotesCount: { + type: 'integer', + optional: false, nullable: false, + }, + repliedCount: { + type: 'integer', + optional: false, nullable: false, + }, + renotedCount: { + type: 'integer', + optional: false, nullable: false, + }, + pollVotesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pollVotedCount: { + type: 'integer', + optional: false, nullable: false, + }, + localFollowingCount: { + type: 'integer', + optional: false, nullable: false, + }, + remoteFollowingCount: { + type: 'integer', + optional: false, nullable: false, + }, + localFollowersCount: { + type: 'integer', + optional: false, nullable: false, + }, + remoteFollowersCount: { + type: 'integer', + optional: false, nullable: false, + }, + followingCount: { + type: 'integer', + optional: false, nullable: false, + }, + followersCount: { + type: 'integer', + optional: false, nullable: false, + }, + sentReactionsCount: { + type: 'integer', + optional: false, nullable: false, + }, + receivedReactionsCount: { + type: 'integer', + optional: false, nullable: false, + }, + noteFavoritesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pageLikesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pageLikedCount: { + type: 'integer', + optional: false, nullable: false, + }, + driveFilesCount: { + type: 'integer', + optional: false, nullable: false, + }, + driveUsage: { + type: 'integer', + optional: false, nullable: false, + description: 'Drive usage in bytes', + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, + + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const result = await awaitAll({ + notesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + repliesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.replyId IS NOT NULL') + .getCount(), + renotesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.renoteId IS NOT NULL') + .getCount(), + repliedCount: this.notesRepository.createQueryBuilder('note') + .where('note.replyUserId = :userId', { userId: user.id }) + .getCount(), + renotedCount: this.notesRepository.createQueryBuilder('note') + .where('note.renoteUserId = :userId', { userId: user.id }) + .getCount(), + pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote') + .where('vote.userId = :userId', { userId: user.id }) + .getCount(), + pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote') + .innerJoin('vote.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + localFollowingCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NULL') + .getCount(), + remoteFollowingCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NOT NULL') + .getCount(), + localFollowersCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NULL') + .getCount(), + remoteFollowersCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NOT NULL') + .getCount(), + sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') + .where('reaction.userId = :userId', { userId: user.id }) + .getCount(), + receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') + .innerJoin('reaction.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite') + .where('favorite.userId = :userId', { userId: user.id }) + .getCount(), + pageLikesCount: this.pageLikesRepository.createQueryBuilder('like') + .where('like.userId = :userId', { userId: user.id }) + .getCount(), + pageLikedCount: this.pageLikesRepository.createQueryBuilder('like') + .innerJoin('like.page', 'page') + .where('page.userId = :userId', { userId: user.id }) + .getCount(), + driveFilesCount: this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId', { userId: user.id }) + .getCount(), + driveUsage: this.driveFileEntityService.calcDriveUsageOf(user), + }); + + return { + ...result, + followingCount: result.localFollowingCount + result.remoteFollowingCount, + followersCount: result.localFollowersCount + result.remoteFollowersCount, + }; + }); + } +} diff --git a/packages/backend/src/server/api/stream/channels/hybrid-all-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-all-timeline.ts new file mode 100644 index 0000000000..bca26eb36a --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/hybrid-all-timeline.ts @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { MetaService } from '@/core/MetaService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; +import Channel from '../channel.js'; + +class HybridAllTimelineChannel extends Channel { + public readonly chName = 'hybridAllTimeline'; + public static shouldShare = true; + public static requireCredential = false; + private withReplies: boolean; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + //this.onNote = this.onNote.bind(this); + } + + @bindThis + public async init(params: any) { + const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); + if (!policies.ltlAvailable) return; + + this.withReplies = params.withReplies as boolean; + + // Subscribe events + this.subscriber.on('notesStream', this.onNote); + } + + @bindThis + private async onNote(note: Packed<'Note'>) { + if (note.user.host !== null) return; + if (note.visibility === "public") return; + if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; + + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { + detail: true, + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); + } + + // 関係ない返信は除外 + if (note.reply && this.user && !this.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + + // 流れてきたNoteがミュートすべきNoteだったら無視する + // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) + // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 + // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる + if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + + this.connection.cacheNote(note); + + this.send('note', note); + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} + +@Injectable() +export class HybridAllTimelineChannelService { + public readonly shouldShare = HybridAllTimelineChannel.shouldShare; + public readonly requireCredential = HybridAllTimelineChannel.requireCredential; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): HybridAllTimelineChannel { + return new HybridAllTimelineChannel( + this.metaService, + this.roleService, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 7faf4edbc2..a6dcc2f9b2 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -31,7 +31,9 @@ import type { EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, + ScheduleNotePostQueue, SystemQueue, + WebhookDeliverQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, } from '@/core/QueueModule.js'; @@ -62,7 +64,6 @@ const clientAssets = `${_dirname}/../../../../frontend/assets/`; const assets = `${_dirname}/../../../../../built/_frontend_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; const viteOut = `${_dirname}/../../../../../built/_vite_/`; -const tarball = `${_dirname}/../../../../../built/tarball/`; @Injectable() export class ClientServerService { @@ -116,6 +117,7 @@ export class ClientServerService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -192,6 +194,7 @@ export class ClientServerService { appleTouchIcon: meta.app512IconUrl, themeColor: meta.themeColor, serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', + googleAnalyticsId: meta.googleAnalyticsId ?? null, infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', instanceUrl: this.config.url, @@ -245,6 +248,7 @@ export class ClientServerService { queues: [ this.systemQueue, this.endedPollNotificationQueue, + this.scheduleNotePostQueue, this.deliverQueue, this.inboxQueue, this.dbQueue, @@ -322,18 +326,6 @@ export class ClientServerService { decorateReply: false, }); - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: tarball, - prefix: '/tarball/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); - }); - fastify.get('/favicon.ico', async (request, reply) => { return reply.sendFile('/favicon.ico', staticAssets); }); diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index fd25858425..4e3814bfa3 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -45,7 +45,13 @@ html { width: 28px; height: 28px; transform: translateY(70px); - color: var(--accent); + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + -moz-animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } #splashSpinner > .spinner { position: absolute; @@ -74,3 +80,68 @@ html { transform: rotate(360deg); } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 57eed8d59f..a17ecc2a1d 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -40,6 +40,14 @@ html link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) + if googleAnalyticsId + script(async src='https://www.googletagmanager.com/gtag/js?id='+ googleAnalyticsId) + script. + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '#{googleAnalyticsId}'); + //- https://github.com/misskey-dev/misskey/issues/9842 link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 81407789b4..eee86aa9c5 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -3,6 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { IPoll } from '@/models/Poll.js'; +import type { MiChannel } from '@/models/Channel.js'; +import type { MiApp } from '@/models/App.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiScheduledNote } from '@/models/ScheduledNote.js'; + /** * note - 通知オンにしているユーザーが投稿した * follow - フォローされた @@ -34,6 +42,7 @@ export const notificationTypes = [ 'achievementEarned', 'app', 'test', + 'loginbonus', ] as const; export const groupedNotificationTypes = [ @@ -57,6 +66,7 @@ export const moderationLogTypes = [ 'unsuspend', 'updateUserNote', 'addCustomEmoji', + 'requestCustomEmoji', 'updateCustomEmoji', 'deleteCustomEmoji', 'assignRole', @@ -124,6 +134,10 @@ export type ModerationLogPayloads = { emojiId: string; emoji: any; }; + requestCustomEmoji: { + emojiId: string; + emoji: any; + }; updateCustomEmoji: { emojiId: string; before: any; @@ -316,6 +330,36 @@ export type ModerationLogPayloads = { }; }; +export type MiMinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + +export type MiNoteCreateOption = { + createdAt?: Date | null; + name?: string | null; + text?: string | null; + reply?: MiNote | null; + renote?: MiNote | null; + files?: MiDriveFile[] | null; + poll?: IPoll | null; + schedule?: MiScheduledNote | null; + localOnly?: boolean | null; + reactionAcceptance?: MiNote['reactionAcceptance']; + cw?: string | null; + visibility?: string; + visibleUsers?: MiMinimumUser[] | null; + channel?: MiChannel | null; + apMentions?: MiMinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + uri?: string | null; + url?: string | null; + app?: MiApp | null; +}; + export type Serialized<T> = { [K in keyof T]: T[K] extends Date diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts index 0b44e9bbb0..e69de29bb2 100644 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -1,528 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { CoreModule } from '@/core/CoreModule.js'; -import type { MiUser } from '@/models/User.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { genAidx } from '@/misc/id/aidx.js'; -import { - BlockingsRepository, - FollowingsRepository, FollowRequestsRepository, - MiUserProfile, MutingsRepository, RenoteMutingsRepository, - UserMemoRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { IdService } from '@/core/IdService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; -import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; -import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; -import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; -import { MfmService } from '@/core/MfmService.js'; -import { HashtagService } from '@/core/HashtagService.js'; -import UsersChart from '@/core/chart/charts/users.js'; -import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; -import InstanceChart from '@/core/chart/charts/instance.js'; -import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; -import { AccountMoveService } from '@/core/AccountMoveService.js'; -import { ReactionService } from '@/core/ReactionService.js'; -import { NotificationService } from '@/core/NotificationService.js'; - -process.env.NODE_ENV = 'test'; - -describe('UserEntityService', () => { - describe('pack/packMany', () => { - let app: TestingModule; - let service: UserEntityService; - let usersRepository: UsersRepository; - let userProfileRepository: UserProfilesRepository; - let userMemosRepository: UserMemoRepository; - let followingRepository: FollowingsRepository; - let followingRequestRepository: FollowRequestsRepository; - let blockingRepository: BlockingsRepository; - let mutingRepository: MutingsRepository; - let renoteMutingsRepository: RenoteMutingsRepository; - - async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) { - const un = secureRndstr(16); - const user = await usersRepository - .insert({ - ...userData, - id: genAidx(Date.now()), - username: un, - usernameLower: un, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfileRepository.insert({ - ...profileData, - userId: user.id, - }); - - return user; - } - - async function memo(writer: MiUser, target: MiUser, memo: string) { - await userMemosRepository.insert({ - id: genAidx(Date.now()), - userId: writer.id, - targetUserId: target.id, - memo, - }); - } - - async function follow(follower: MiUser, followee: MiUser) { - await followingRepository.insert({ - id: genAidx(Date.now()), - followerId: follower.id, - followeeId: followee.id, - }); - } - - async function requestFollow(requester: MiUser, requestee: MiUser) { - await followingRequestRepository.insert({ - id: genAidx(Date.now()), - followerId: requester.id, - followeeId: requestee.id, - }); - } - - async function block(blocker: MiUser, blockee: MiUser) { - await blockingRepository.insert({ - id: genAidx(Date.now()), - blockerId: blocker.id, - blockeeId: blockee.id, - }); - } - - async function mute(mutant: MiUser, mutee: MiUser) { - await mutingRepository.insert({ - id: genAidx(Date.now()), - muterId: mutant.id, - muteeId: mutee.id, - }); - } - - async function muteRenote(mutant: MiUser, mutee: MiUser) { - await renoteMutingsRepository.insert({ - id: genAidx(Date.now()), - muterId: mutant.id, - muteeId: mutee.id, - }); - } - - function randomIntRange(weight = 10) { - return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx); - } - - beforeAll(async () => { - const services = [ - UserEntityService, - ApPersonService, - NoteEntityService, - PageEntityService, - CustomEmojiService, - AnnouncementService, - RoleService, - FederatedInstanceService, - IdService, - AvatarDecorationService, - UtilityService, - EmojiEntityService, - ModerationLogService, - GlobalEventService, - DriveFileEntityService, - MetaService, - FetchInstanceMetadataService, - CacheService, - ApResolverService, - ApNoteService, - ApImageService, - ApMfmService, - MfmService, - HashtagService, - UsersChart, - ChartLoggerService, - InstanceChart, - ApLoggerService, - AccountMoveService, - ReactionService, - NotificationService, - ]; - - app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - providers: [ - ...services, - ...services.map(x => ({ provide: x.name, useExisting: x })), - ], - }).compile(); - await app.init(); - app.enableShutdownHooks(); - - service = app.get<UserEntityService>(UserEntityService); - usersRepository = app.get<UsersRepository>(DI.usersRepository); - userProfileRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository); - userMemosRepository = app.get<UserMemoRepository>(DI.userMemosRepository); - followingRepository = app.get<FollowingsRepository>(DI.followingsRepository); - followingRequestRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository); - blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository); - mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository); - renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository); - }); - - afterAll(async () => { - await app.close(); - }); - - test('UserLite', async() => { - const me = await createUser(); - const who = await createUser(); - - await memo(me, who, 'memo'); - - const actual = await service.pack(who, me, { schema: 'UserLite' }) as any; - // no detail - expect(actual.memo).toBeUndefined(); - // no detail and me - expect(actual.birthday).toBeUndefined(); - // no detail and me - expect(actual.achievements).toBeUndefined(); - }); - - test('UserDetailedNotMe', async() => { - const me = await createUser(); - const who = await createUser({}, { birthday: '2000-01-01' }); - - await memo(me, who, 'memo'); - - const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any; - // is detail - expect(actual.memo).toBe('memo'); - // is detail - expect(actual.birthday).toBe('2000-01-01'); - // no detail and me - expect(actual.achievements).toBeUndefined(); - }); - - test('MeDetailed', async() => { - const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }]; - const me = await createUser({}, { - birthday: '2000-01-01', - achievements: achievements, - }); - await memo(me, me, 'memo'); - - const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any; - // is detail - expect(actual.memo).toBe('memo'); - // is detail - expect(actual.birthday).toBe('2000-01-01'); - // is detail and me - expect(actual.achievements).toEqual(achievements); - }); - - describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { - test('no-preload', async() => { - const me = await createUser(); - // meがフォローしてる人たち - const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followeeMe) { - await follow(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(true); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meをフォローしてる人たち - const followerMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followerMe) { - await follow(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(true); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがフォローリクエストを送った人たち - const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsFromYou) { - await requestFollow(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(true); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meにフォローリクエストを送った人たち - const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsToYou) { - await requestFollow(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(true); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがブロックしてる人たち - const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingYou) { - await block(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(true); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meをブロックしてる人たち - const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingMe) { - await block(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(true); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがミュートしてる人たち - const muters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of muters) { - await mute(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(true); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがリノートミュートしてる人たち - const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of renoteMuters) { - await muteRenote(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(true); - } - }); - - test('preload', async() => { - const me = await createUser(); - - { - // meがフォローしてる人たち - const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followeeMe) { - await follow(me, who); - } - const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(true); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meをフォローしてる人たち - const followerMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followerMe) { - await follow(who, me); - } - const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(true); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがフォローリクエストを送った人たち - const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsFromYou) { - await requestFollow(me, who); - } - const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(true); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meにフォローリクエストを送った人たち - const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsToYou) { - await requestFollow(who, me); - } - const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(true); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがブロックしてる人たち - const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingYou) { - await block(me, who); - } - const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(true); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meをブロックしてる人たち - const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingMe) { - await block(who, me); - } - const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(true); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがミュートしてる人たち - const muters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of muters) { - await mute(me, who); - } - const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(true); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがリノートミュートしてる人たち - const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of renoteMuters) { - await muteRenote(me, who); - } - const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(true); - } - } - }); - }); - }); -}); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a63d97658b..a8ab11b045 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -20,6 +20,7 @@ "@discordapp/twemoji": "15.0.3", "@github/webauthn-json": "2.1.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", + "@meersagor/wavesurfer-vue": "^0.1.0", "@misskey-dev/browser-image-resizer": "2024.1.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", @@ -73,7 +74,8 @@ "v-code-diff": "1.11.0", "vite": "5.2.11", "vue": "3.4.26", - "vuedraggable": "next" + "vuedraggable": "next", + "wavesurfer.js": "^7.7.14" }, "devDependencies": { "@misskey-dev/eslint-plugin": "1.0.0", diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 563a42276c..b118a596b6 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -28,6 +28,7 @@ export async function mainBoot() { !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + ui === 'twilike' ? defineAsyncComponent(() => import('@/ui/twilike.vue')) : defineAsyncComponent(() => import('@/ui/universal.vue')), )); diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 96f1b362c0..1e6fb11e15 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -12,3 +12,6 @@ export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/role export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list')); export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list')); export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 })); +export const userFavoriteListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list-favorite')); +export const userChannelsCache = new Cache<Misskey.entities.UserChannel[]>(1000 * 60 * 30, () => misskeyApi('channels/owned')); +export const userChannelFollowingsCache = new Cache<Misskey.entities.UserChannelFollowing[]>(1000 * 60 * 30, () => misskeyApi('channels/followed')); diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index fb72cf2388..8ba812dce6 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="bcekxzvu _margin _panel"> - <div class="target"> - <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> - <MkAvatar class="avatar" :user="report.targetUser" indicator/> - <div class="names"> - <MkUserName class="name" :user="report.targetUser"/> - <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> +<div :class="$style.root"> + <div :class="$style.target"> + <MkA v-user-preview="report.targetUserId" :class="$style.info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> + <MkAvatar :class="$style.avatar" :user="report.targetUser" indicator/> + <div :class="$style.name"> + <MkUserName :class="$style.names" :user="report.targetUser"/> + <MkAcct :class="$style.names" :user="report.targetUser" style="display: block;"/> </div> </MkA> <MkKeyValue> @@ -18,9 +18,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> </MkKeyValue> </div> - <div class="detail"> + <div :class="$style.detail"> <div> <Mfm :text="report.comment" :linkNavigationBehavior="'window'"/> + <MkFolder v-if="report.notes.length !== 0" :class="$style.notes"> + <template #label>{{ i18n.ts.reportedNote }}</template> + <div v-for="note in report.notes" :class="$style.notes"> + <MkNoteSimple v-if="note !== 'deleted'" :note="note"/> + <div v-else> note is deleted </div> + </div> + </MkFolder> </div> <hr/> <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div> @@ -42,15 +49,28 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; - +import MkFolder from '@/components/MkFolder.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; const props = defineProps<{ - report: any; + report: { + id: string; + createdAt:string; + targetUserId:Misskey.entities.User['id']; + targetUser:Misskey.entities.User & { createdAt:string; }; + reporter:Misskey.entities.User; + assignee:Misskey.entities.User['id']; + comment:string; + notes:Misskey.entities.Note['id'][]; + forwarded:boolean; + resolved:boolean; + }; }>(); const emit = defineEmits<{ @@ -69,47 +89,57 @@ function resolve() { } </script> -<style lang="scss" scoped> -.bcekxzvu { +<style lang="scss" module> +.root { display: flex; - - > .target { - width: 35%; - box-sizing: border-box; - text-align: left; - padding: 24px; - border-right: solid 1px var(--divider); - - > .info { - display: flex; - box-sizing: border-box; - align-items: center; - padding: 14px; - border-radius: 8px; - --c: rgb(255 196 0 / 15%); - background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); - background-size: 16px 16px; - - > .avatar { - width: 42px; - height: 42px; - } - - > .names { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - > .name { - font-weight: bold; - } - } - } - } - - > .detail { - flex: 1; - padding: 24px; - } + margin: var(--margin) 0; + background: var(--panel); + border-radius: var(--radius); + overflow: clip; } + +.notes { + margin: var(--margin) 0; + padding: 0; +} + +.target { + width: 35%; + box-sizing: border-box; + text-align: left; + padding: 24px; + border-right: solid 1px var(--divider); +} + +.info { + display: flex; + box-sizing: border-box; + align-items: center; + padding: 14px; + border-radius: 8px; + --c: rgb(255 196 0 / 15%); + background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); + background-size: 16px 16px; +} + +.avatar { + width: 42px; + height: 42px; +} + +.names { + margin-left: 0.3em; + padding: 0 8px; + flex: 1; +} + +.name { + font-weight: bold; +} + +.detail { + flex: 1; + padding: 24px; +} + </style> diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index f1a5f105cd..a68830c504 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')"> +<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" style="overflow-x: clip;" @closed="emit('closed')"> <template #header> <i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i> <I18n :src="i18n.ts.reportAbuseOf" tag="span"> @@ -13,19 +13,45 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> </template> - <MkSpacer :marginMin="20" :marginMax="28"> - <div class="_gaps_m" :class="$style.root"> - <div class=""> - <MkTextarea v-model="comment"> - <template #label>{{ i18n.ts.details }}</template> - <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> - </MkTextarea> - </div> - <div class=""> - <MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton> - </div> - </div> - </MkSpacer> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <template v-if="page === 0"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m" :class="$style.root"> + <MkPagination v-slot="{items}" :key="user.id" :pagination="Pagination" :disableAutoLoad="true"> + <div v-for="item in items" :key="item.id" :class="$style.note"> + <MkSwitch :modelValue="abuseNotesId.includes(item.id)" @update:modelValue="pushAbuseReportNote($event,item.id)"></MkSwitch> + <MkAvatar :user="item.user" preview/> + <MkNoteSimple :note="item"/> + </div> + </MkPagination> + <div class="_buttonsCenter"> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </MkSpacer> + </template> + + <template v-else-if="page === 1"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m" :class="$style.root"> + <MkTextarea v-model="comment"> + <template #label>{{ i18n.ts.details }}</template> + <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> + </MkTextarea> + <div class="_buttonsCenter"> + <MkButton @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton> + </div> + </div> + </MkSpacer> + </template> + </Transition> </MkWindow> </template> @@ -37,23 +63,45 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import MkPagination from '@/components/MkPagination.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; const props = defineProps<{ user: Misskey.entities.UserDetailed; initialComment?: string; + initialNoteId?: Misskey.entities.Note['id']; }>(); +const Pagination = { + endpoint: 'users/notes' as const, + limit: 10, + params: { + userId: props.user.id, + }, +}; const emit = defineEmits<{ (ev: 'closed'): void; }>(); +const abuseNotesId = ref(props.initialNoteId ? [props.initialNoteId] : []); +const page = ref(0); const uiWindow = shallowRef<InstanceType<typeof MkWindow>>(); const comment = ref(props.initialComment ?? ''); +function pushAbuseReportNote(ev, id) { + if (ev) { + abuseNotesId.value.push(id); + } else { + abuseNotesId.value = abuseNotesId.value.filter(noteId => noteId !== id); + } +} + function send() { os.apiWithDialog('users/report-abuse', { userId: props.user.id, comment: comment.value, + noteIds: abuseNotesId.value, }, undefined).then(res => { os.alert({ type: 'success', @@ -69,4 +117,22 @@ function send() { .root { --root-margin: 16px; } +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} +.note{ + display: flex; + margin: var(--margin) 0; + align-items: center; + +} </style> diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 76ebadab79..fc1e290d6f 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -91,6 +91,10 @@ const emojiDb = computed(() => { const customEmojiDB: EmojiDef[] = []; for (const x of customEmojis.value) { + if (x.draft) { + continue; + } + customEmojiDB.push({ name: x.name, emoji: `:${x.name}:`, diff --git a/packages/frontend/src/components/MkAvatarDecoEditDialog.vue b/packages/frontend/src/components/MkAvatarDecoEditDialog.vue new file mode 100644 index 0000000000..a39bbc000b --- /dev/null +++ b/packages/frontend/src/components/MkAvatarDecoEditDialog.vue @@ -0,0 +1,164 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :width="400" + @close="dialog.close()" + @closed="$emit('closed')" +> + <template v-if="avatarDecoration" #header>:{{ avatarDecoration.name }}</template> + <template v-else #header>New create</template> + + <div> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m"> + <div class="_gaps_m"> + <XDecoration + v-if="avatarDecoration" + :key="avatarDecoration.id" + :decoration="avatarDecoration" + /> + <MkInput v-model="name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkTextarea v-model="description"> + <template #label>{{ i18n.ts.description }}</template> + </MkTextarea> + <MkInput v-model="url"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <MkInput v-model="category"> + <template #label>{{ i18n.ts.category }}</template> + </MkInput> + </div> + </div> + </MkSpacer> + <div :class="$style.footer"> + <div :class="$style.footerButtons"> + <MkButton danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton primary rounded style="margin: 0 auto;" @click="save"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton> + </div> + </div> + </div> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { computed, ref, watch } from 'vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkButton from '@/components/MkButton.vue'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import MkInput from '@/components/MkInput.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; +import XDecoration from '@/pages/settings/avatar-decoration.decoration.vue'; +const props = defineProps<{ + avatarDecoration?: { + id: string | null; + name: string; + description: string; + url: string; + category: string; + }; +}>(); +let name = ref(props.avatarDecoration?.name ?? ''); +let category = ref(props.avatarDecoration?.category ?? ''); +let description = ref(props.avatarDecoration?.description ?? ''); +let url = ref(props.avatarDecoration?.url ?? ''); +const emit = defineEmits<{ + (ev: 'del'): void +}>(); + +let dialog = ref<InstanceType<typeof MkModalWindow> | null>(null); + +function del() { + os.confirm({ + type: 'warning', + text: i18n.t('deleteAreYouSure', { x: props.avatarDecoration?.name }), + }).then(({ canceled }) => { + if (canceled) return; + misskeyApi('admin/avatar-decorations/delete', { id: props.avatarDecoration?.id }).then(() => { + + }); + }); + emit('del'); +} + +async function save() { + if (props.avatarDecoration == null) { + await os.apiWithDialog('admin/avatar-decorations/create', { + name: name.value, + description: description.value, + url: url.value, + category: category.value, + }); + } else { + await os.apiWithDialog('admin/avatar-decorations/update', { + id: props.avatarDecoration.id ?? '', + name: name.value, + description: description.value, + url: url.value, + category: category.value, + }); + } + emit('del'); +} + +</script> + +<style lang="scss" module> +.imgs { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} + +.imgContainer { + padding: 8px; + border-radius: 6px; +} + +.img { + display: block; + height: 64px; + width: 64px; + object-fit: contain; +} + +.roleItem { + display: flex; +} + +.role { + flex: 1; +} + +.roleUnassign { + width: 32px; + height: 32px; + margin-left: 8px; + align-self: center; +} + +.footer { + position: sticky; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +.footerButtons { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} +</style> diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index a4bfeebc1f..e254900dd7 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -6,7 +6,23 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <button v-if="!link" ref="el" class="_button" - :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" + :class="[ + $style.root, + { + [$style.inline]: inline, + [$style.primary]: primary, + [$style.gradate]: gradate, + [$style.danger]: danger, + [$style.rounded]: rounded, + [$style.full]: full, + [$style.small]: small, + [$style.large]: large, + [$style.transparent]: transparent, + [$style.asLike]: asLike, + [$style.gamingDark]: gaming === 'dark', + [$style.gamingLight]: gaming === 'light', + } + ]" :type="type" :name="name" :value="value" @@ -21,7 +37,23 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </button> <MkA v-else class="_button" - :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" + :class="[ + $style.root, + { + [$style.inline]: inline, + [$style.primary]: primary, + [$style.gradate]: gradate, + [$style.danger]: danger, + [$style.rounded]: rounded, + [$style.full]: full, + [$style.small]: small, + [$style.large]: large, + [$style.transparent]: transparent, + [$style.asLike]: asLike, + [$style.gamingDark]: gaming === 'dark', + [$style.gamingLight]: gaming === 'light', + } + ]" :to="to ?? '#'" :behavior="linkBehavior" @mousedown="onMousedown" @@ -34,32 +66,66 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { nextTick, onMounted, shallowRef } from 'vue'; +import { nextTick, onMounted, shallowRef, computed, ref, watch } from 'vue'; +import { defaultStore } from '@/store.js'; const props = defineProps<{ - type?: 'button' | 'submit' | 'reset'; - primary?: boolean; - gradate?: boolean; - rounded?: boolean; - inline?: boolean; - link?: boolean; - to?: string; - linkBehavior?: null | 'window' | 'browser'; - autofocus?: boolean; - wait?: boolean; - danger?: boolean; - full?: boolean; - small?: boolean; - large?: boolean; - transparent?: boolean; - asLike?: boolean; - name?: string; - value?: string; + type?: 'button' | 'submit' | 'reset'; + primary?: boolean; + gradate?: boolean; + rounded?: boolean; + inline?: boolean; + link?: boolean; + to?: string; + linkBehavior?: null | 'window' | 'browser';autofocus?: boolean; + wait?: boolean; + danger?: boolean; + full?: boolean; + small?: boolean; + large?: boolean; + transparent?: boolean; + gamingdark?: boolean; + gaminglight?: boolean; + asLike?: boolean; + name?: string; + value?: string; disabled?: boolean; }>(); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +// gamingをrefで初期化する +let gaming = ref(''); // 0-off , 1-dark , 2-light +// gaming.valueに新しい値を代入する +if (darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) { + gaming.value = 'dark'; +} else if (!darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) { + gaming.value = 'light'; +} else { + gaming.value = ''; +} + +watch(darkMode, () => { + if (darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +watch(gamingMode, () => { + if (darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value && props.primary || darkMode.value && gamingMode.value && props.gradate ) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); const emit = defineEmits<{ - (ev: 'click', payload: MouseEvent): void; + (ev: 'click', payload: MouseEvent): void; }>(); const el = shallowRef<HTMLElement | null>(null); @@ -168,7 +234,62 @@ function onMousedown(evt: MouseEvent): void { font-weight: bold; color: var(--fgOnAccent) !important; background: var(--accent); + &.gamingLight { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + color: white !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + &:not(:disabled):hover { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + color: white !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &:not(:disabled):active { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + color: white !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ; + } + &:hover{ + background: var(--accent); + } + } + + &.gamingDark { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + color: black; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + + &:not(:disabled):hover { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% ; + color: black; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ; + } + + &:not(:disabled):active { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + color: black; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + } + } &:not(:disabled):hover { background: var(--X8); } @@ -225,6 +346,59 @@ function onMousedown(evt: MouseEvent): void { &:not(:disabled):active { background: linear-gradient(90deg, var(--X8), var(--X8)); } + &.gamingLight { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + color: white !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + + &:not(:disabled):hover { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + color: white !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &:not(:disabled):active { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + color: white !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite ; + } + } + + &.gamingDark { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + color: black; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + + &:not(:disabled):hover { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% ; + color: black; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ; + } + + &:not(:disabled):active { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + color: black; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite ; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.45, 0.30, 1) infinite; + } + } } &.danger { @@ -292,4 +466,34 @@ function onMousedown(evt: MouseEvent): void { z-index: 1; pointer-events: none; } +@-webkit-keyframes AnimationLight { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@-moz-keyframes AnimationLight { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@keyframes AnimationLight { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@-webkit-keyframes AnimationDark { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@-moz-keyframes AnimationDark { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} +@keyframes AnimationDark { + 0%{background-position:0% 50%} + 50%{background-position:100% 50%} + 100%{background-position:0% 50%} +} </style> diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 63a6e782c9..9c353942c4 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -3,132 +3,293 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<button - class="_button" - :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]" - :disabled="wait" - @click="onClick" -> - <template v-if="!wait"> - <template v-if="isFollowing"> - <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> - </template> - <template v-else> - <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> - </template> - </template> - <template v-else> - <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/> - </template> -</button> + <button + class="_button" + :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full },[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]]" + :disabled="wait" + @click="onClick" + > + <template v-if="!wait"> + <template v-if="isFollowing"> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{ + i18n.ts.unfollow + }}</span><i class="ti ti-minus"></i> + </template> + <template v-else> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{ + i18n.ts.follow + }}</span><i class="ti ti-plus"></i> + </template> + </template> + <template v-else> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{ + i18n.ts.processing + }}</span> + <MkLoading :em="true"/> + </template> + </button> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import {computed, ref, watch} from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { i18n } from '@/i18n.js'; +import {i18n} from '@/i18n.js'; +import {defaultStore} from "@/store.js"; const props = withDefaults(defineProps<{ - channel: Misskey.entities.Channel; - full?: boolean; + channel: Misskey.entities.Channel; + full?: boolean; }>(), { - full: false, + full: false, }); +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); + const isFollowing = ref(props.channel.isFollowing); const wait = ref(false); async function onClick() { - wait.value = true; + wait.value = true; - try { - if (isFollowing.value) { - await misskeyApi('channels/unfollow', { - channelId: props.channel.id, - }); - isFollowing.value = false; - } else { - await misskeyApi('channels/follow', { - channelId: props.channel.id, - }); - isFollowing.value = true; - } - } catch (err) { - console.error(err); - } finally { - wait.value = false; - } + try { + if (isFollowing.value) { + await misskeyApi('channels/unfollow', { + channelId: props.channel.id, + }); + isFollowing.value = false; + } else { + await misskeyApi('channels/follow', { + channelId: props.channel.id, + }); + isFollowing.value = true; + } + } catch (err) { + console.error(err); + } finally { + wait.value = false; + } } </script> <style lang="scss" module> .root { - position: relative; - display: inline-block; - font-weight: bold; - color: var(--accent); - background: transparent; - border: solid 1px var(--accent); - padding: 0; - height: 31px; - font-size: 16px; - border-radius: 32px; - background: #fff; + position: relative; + display: inline-block; + font-weight: bold; + color: var(--accent); + border: solid 1px var(--accent); + padding: 0; + height: 31px; + font-size: 16px; + border-radius: 32px; + background: #fff; - &.full { - padding: 0 8px 0 12px; - font-size: 14px; - } + &.gamingDark { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } - &:not(.full) { - width: 31px; - } + &.gamingLight { + color: #fff; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; - &:focus-visible { - &:after { - content: ""; - pointer-events: none; - position: absolute; - top: -5px; - right: -5px; - bottom: -5px; - left: -5px; - border: 2px solid var(--focus); - border-radius: 32px; - } - } + } - &:hover { - //background: mix($primary, #fff, 20); - } + &.full { + padding: 0 8px 0 12px; + font-size: 14px; + } - &:active { - //background: mix($primary, #fff, 40); - } + &:not(.full) { + width: 31px; + } - &.active { - color: var(--fgOnAccent); - background: var(--accent); + &:focus-visible { + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: -5px; + right: -5px; + bottom: -5px; + left: -5px; + border: 2px solid var(--focus); + border-radius: 32px; + } + } - &:hover { - background: var(--accentLighten); - border-color: var(--accentLighten); - } + &:hover { + //background: mix($primary, #fff, 20); + } - &:active { - background: var(--accentDarken); - border-color: var(--accentDarken); - } - } + &:active { + //background: mix($primary, #fff, 40); + } - &.wait { - cursor: wait !important; - opacity: 0.7; - } + &.active { + color: var(--fgOnAccent); + background: var(--accent); + + &:hover { + background: var(--accentLighten); + border-color: var(--accentLighten); + } + + &:active { + background: var(--accentDarken); + border-color: var(--accentDarken); + } + + &.gamingDark:hover { + + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingDark:active { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border-color: white; + } + + &.gamingLight:hover { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border-color: white; + } + + &.gamingLight:active { + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border-color: white; + } + + &.gamingDark { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingLight { + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + + } + + &.wait { + cursor: wait !important; + opacity: 0.7; + } + } } .text { - margin-right: 6px; + margin-right: 6px; +} + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 611bfd2abd..ea4f340742 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -6,9 +6,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div> <div v-if="game.ready" :class="$style.game"> <div :class="$style.cps" class="">{{ number(cps) }}cps</div> - <div :class="$style.count" class="" data-testid="count"><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> + <div :class="$style.count" class="" data-testid="count"> + <img + :class="[$style.icon,{[$style.dark]:darkMode}]" alt="Cosaque daihuku" + src="https://files.prismisskey.space/misskey/630c737c-e96f-4c10-94a4-73e138278576.webp" + /> + {{ number(cookies) }} + </div> <button v-click-anime class="_button" @click="onClick"> - <img src="/client-assets/cookie.png" :class="$style.img"> + <img + src="https://files.prismisskey.space/misskey/630c737c-e96f-4c10-94a4-73e138278576.webp" + :class="$style.img" + > </button> </div> <div v-else> @@ -25,7 +34,9 @@ import { useInterval } from '@/scripts/use-interval.js'; import * as game from '@/scripts/clicker-game.js'; import number from '@/filters/number.js'; import { claimAchievement } from '@/scripts/achievements.js'; +import { defaultStore } from '@/store.js'; +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); const saveData = game.saveData; const cookies = computed(() => saveData.value?.cookies); const cps = ref(0); @@ -91,4 +102,15 @@ onUnmounted(() => { .img { max-width: 90px; } + +$color-scheme: var(--color-scheme); + +.icon { + width: 1.3em; + vertical-align: -24%; +} + +.dark { + filter: invert(1); +} </style> diff --git a/packages/frontend/src/components/MkCustomEmojiEditLocal.vue b/packages/frontend/src/components/MkCustomEmojiEditLocal.vue new file mode 100644 index 0000000000..9a29622827 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiEditLocal.vue @@ -0,0 +1,262 @@ +<template> +<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off"> + <template #prefix><i class="ti ti-search"></i></template> + <template #label>{{ i18n.ts.search }}</template> +</MkInput> +<MkSwitch v-model="selectMode" style="margin: 8px 0;"> + <template #label>Select mode</template> +</MkSwitch> +<div v-if="selectMode" class="_buttons"> + <MkButton inline @click="selectAll">Select all</MkButton> + <MkButton inline @click="setCategoryBulk">Set category</MkButton> + <MkButton inline @click="setTagBulk">Set tag</MkButton> + <MkButton inline @click="addTagBulk">Add tag</MkButton> + <MkButton inline @click="removeTagBulk">Remove tag</MkButton> + <MkButton inline @click="setLisenceBulk">Set Lisence</MkButton> + <MkButton inline @click="isSensitiveBulk">Set isSensitive</MkButton> + <MkButton inline @click="setlocalOnlyBulk">Set localOnly</MkButton> + <MkButton inline danger @click="delBulk">Delete</MkButton> +</div> +<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="100"> + <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> + <template #default="{items}"> + <div :class="$style.root"> + <div v-for="emoji in items" :key="emoji.id"> + <button v-if="emoji.request" class="_panel _button" :class="[{ [$style.selected]: selectedEmojis.includes(emoji.id) },$style.emoji,$style.emojirequest]" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> + <img :src="emoji.url" class="img" :alt="emoji.name"/> + <div class="body"> + <div class="name _monospace">{{ emoji.name }}</div> + <div class="info">{{ emoji.category }}</div> + </div> + </button> + <button v-else class="_panel _button" :class="[{ [$style.selected]: selectedEmojis.includes(emoji.id) },$style.emoji]" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> + <img :src="emoji.url" :class="$style.img" :alt="emoji.name"/> + <div :class="$style.body"> + <div :class="$style.name" class="_monospace">{{ emoji.name }}</div> + <div :class="$style.info">{{ emoji.category }}</div> + </div> + </button> + </div> + </div> + </template> +</MkPagination> +</template> + +<script lang="ts" setup> +import { computed, defineAsyncComponent, ref, shallowRef } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkPagination from '@/components/MkPagination.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; + +const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); + +const query = ref(null); +const selectMode = ref(false); +const selectedEmojis = ref<string[]>([]); + +const pagination = { + endpoint: 'admin/emoji/list' as const, + limit: 30, + params: computed(() => ({ + query: (query.value && query.value !== '') ? query.value : null, + })), +}; + +const selectAll = () => { + + if (selectedEmojis.value.length > 0) { + selectedEmojis.value = []; + } else { + selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id); + } +}; +const setisSensitiveBulk = async () => { + const { canceled, result } = await os.switch1({ + title: 'isSensitive', + type: "mksw" + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-issensitive-bulk', { + ids: selectedEmojis.value, + isSensitive: result + }); + emojisPaginationComponent.value.reload(); +}; +const setlocalOnlyBulk = async () => { + const { canceled, result } = await os.switch1({ + title: 'localOnly', + type: "mksw" + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-localonly-bulk', { + ids: selectedEmojis.value, + localOnly: result + }); + emojisPaginationComponent.value.reload(); +}; + + +const toggleSelect = (emoji) => { + console.log(selectedEmojis.value) + if (selectedEmojis.value.includes(emoji.id)) { + selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id); + } else { + selectedEmojis.value.push(emoji.id); + } +}; + +const edit = (emoji) => { + os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), { + emoji: emoji, + isRequest: false, + }, { + done: result => { + if (result.updated) { + emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ + ...oldEmoji, + ...result.updated, + })); + emojisPaginationComponent.value.reload(); + } else if (result.deleted) { + emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); + } + }, + }, 'closed'); +}; + +const setCategoryBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Category', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-category-bulk', { + ids: selectedEmojis.value, + category: result, + }); + emojisPaginationComponent.value.reload(); +}; + +const setLisenceBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'License', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-license-bulk', { + ids: selectedEmojis.value, + license: result, + }); + emojisPaginationComponent.value.reload(); +}; + +const isSensitiveBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'License', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-issensitive-bulk', { + ids: selectedEmojis.value, + license: result, + }); + emojisPaginationComponent.value.reload(); +}; + +const addTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/add-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const removeTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/remove-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const setTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const delBulk = async () => { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/delete-bulk', { + ids: selectedEmojis.value, + }); + emojisPaginationComponent.value.reload(); +}; +</script> + +<style lang="scss" module> +.root { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + grid-gap: var(--margin); +} +.emoji { + display: flex; + align-items: center; + padding: 11px; + text-align: left; + border: solid 1px var(--panel); + width: 100%; + + &:hover { + border-color: var(--inputBorderHover); + } + + +} +.selected { + border-color: var(--accent); +} +.img { + width: 42px; + height: 42px; +} +.body { + padding: 0 0 0 8px; + white-space: nowrap; + overflow: hidden; +} +.name { + text-overflow: ellipsis; + overflow: hidden; +} + +.info { + opacity: 0.5; + text-overflow: ellipsis; + overflow: hidden; +} +.emojirequest { + --c: rgb(255 196 0 / 15%);; + background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%); + background-size: 16px 16px; +} +</style> diff --git a/packages/frontend/src/components/MkCustomEmojiEditRemote.vue b/packages/frontend/src/components/MkCustomEmojiEditRemote.vue new file mode 100644 index 0000000000..b5d5b31e40 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiEditRemote.vue @@ -0,0 +1,106 @@ +<template> +<FormSplit> + <MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off"> + <template #prefix><i class="ti ti-search"></i></template> + <template #label>{{ i18n.ts.search }}</template> + </MkInput> + <MkInput v-model="host" :debounce="true"> + <template #label>{{ i18n.ts.host }}</template> + </MkInput> +</FormSplit> +<MkPagination :pagination="remotePagination"> + <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> + <template #default="{items}"> + <div :class="$style.root"> + <div v-for="emoji in items" :key="emoji.id" :class="$style.emoji" class="_panel _button" @click="remoteMenu(emoji, $event)"> + <img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" :class="$style.img" :alt="emoji.name"/> + <div :class="$style.body"> + <div :class="$style.name" class="_monospace">{{ emoji.name }}</div> + <div :class="$style.info">{{ emoji.host }}</div> + </div> + </div> + </div> + </template> +</MkPagination> +</template> + +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import MkInput from '@/components/MkInput.vue'; +import MkPagination from '@/components/MkPagination.vue'; +import FormSplit from '@/components/form/split.vue'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; + +const queryRemote = ref(null); +const host = ref(null); + +const remotePagination = { + endpoint: 'admin/emoji/list-remote' as const, + limit: 30, + params: computed(() => ({ + query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null, + host: (host.value && host.value !== '') ? host.value : null, + })), +}; + +const im = (emoji) => { + os.apiWithDialog('admin/emoji/copy', { + emojiId: emoji.id, + }); +}; + +const remoteMenu = (emoji, ev: MouseEvent) => { + os.popupMenu([{ + type: 'label', + text: ':' + emoji.name + ':', + }, { + text: i18n.ts.import, + icon: 'ti ti-plus', + action: () => { im(emoji); }, + }], ev.currentTarget ?? ev.target); +}; +</script> + +<style lang="scss" module> + +.root { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + grid-gap: 12px; + margin: var(--margin) 0; +} + +.emoji { + display: flex; + align-items: center; + padding: 12px; + text-align: left; + + &:hover { + color: var(--accent); + } +} + +.img { + width: 32px; + height: 32px; +} + +.body { + padding: 0 0 0 8px; + white-space: nowrap; + overflow: hidden; +} +.name { + text-overflow: ellipsis; + overflow: hidden; +} + +.info { + opacity: 0.5; + font-size: 90%; + text-overflow: ellipsis; + overflow: hidden; +} +</style> diff --git a/packages/frontend/src/components/MkCustomEmojiEditRequest.vue b/packages/frontend/src/components/MkCustomEmojiEditRequest.vue new file mode 100644 index 0000000000..cd970ac901 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiEditRequest.vue @@ -0,0 +1,199 @@ +<template> +<MkPagination ref="emojisRequestPaginationComponent" :pagination="paginationRequest"> + <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> + <template #default="{items}"> + <template v-for="emoji in items" :key="emoji.id"> + <div :class="$style.emoji" class="_panel"> + <div :class="$style.img"> + <div :class="$style.imgLight"><img :src="emoji.url" :alt="emoji.name"/></div> + <div :class="$style.imgDark"><img :src="emoji.url" :alt="emoji.name"/></div> + </div> + <div :class="$style.info"> + <div :class="$style.name">{{ i18n.ts.name }}: {{ emoji.name }}</div> + <div :class="$style.category">{{ i18n.ts.category }}:{{ emoji.category }}</div> + <div :class="$style.aliases">{{ i18n.ts.tags }}:{{ emoji.aliases.join(' ') }}</div> + <div :class="$style.license">{{ i18n.ts.license }}:{{ emoji.license }}</div> + </div> + <div :class="$style.editbutton"> + <MkButton primary :class="$style.edit" @click="editRequest(emoji)"> + {{ i18n.ts.edit }} + </MkButton> + <MkButton :class="$style.request" @click="unrequested(emoji)"> + {{ i18n.ts.approval }} + </MkButton> + <MkButton danger :class="$style.delete" @click="deleteRequest(emoji)"> + {{ i18n.ts.delete }} + </MkButton> + </div> + </div> + </template> + </template> +</MkPagination> +</template> + +<script lang="ts" setup> +import { computed, defineAsyncComponent, ref, shallowRef } from 'vue'; +import MkPagination from '@/components/MkPagination.vue'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import MkButton from '@/components/MkButton.vue'; +import {misskeyApi} from "@/scripts/misskey-api.js"; + +const emojisRequestPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); + +const query = ref(null); + +const paginationRequest = { + endpoint: 'admin/emoji/list-request' as const, + limit: 30, + params: computed(() => ({ + query: (query.value && query.value !== '') ? query.value : null, + })), +}; + +function editRequest(emoji) { + os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), { + emoji: emoji, + isRequest: true, + }, { + done: result => { + if (result.updated) { + emojisRequestPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ + ...oldEmoji, + ...result.updated, + })); + emojisRequestPaginationComponent.value.reload(); + } else if (result.deleted) { + emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id); + emojisRequestPaginationComponent.value.reload(); + } + }, + }, 'closed'); +} + +async function unrequested(emoji) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('requestApprovalAreYouSure', { x: emoji.name }), + }); + if (canceled) return; + + await misskeyApi('admin/emoji/update-request', { + id: emoji.id, + fileId: emoji.fileId, + name: emoji.name, + category: emoji.category, + aliases: emoji.aliases, + license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + isRequest: false, + }); + + emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id); + emojisRequestPaginationComponent.value.reload(); +} + +async function deleteRequest(emoji) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: emoji.name }), + }); + if (canceled) return; + + misskeyApi('admin/emoji/delete', { + id: emoji.id, + }).then(() => { + emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id); + emojisRequestPaginationComponent.value.reload(); + }); +} +</script> + +<style lang="scss" module> +.emoji { + align-items: center; + padding: 11px; + text-align: left; + border: solid 1px var(--panel); + margin: 10px; +} +.img { + display: grid; + grid-row: 1; + grid-column: 1/ span 2; + grid-template-columns: 50% 50%; + place-content: center; + place-items: center; +} +.imgLight { + display: grid; + grid-column: 1; + background-color: #fff; + margin-bottom: 12px; + img { + max-height: 64px; + max-width: 100%; + } +} +.imgDark { + display: grid; + grid-column: 2; + background-color: #000; + margin-bottom: 12px; + img { + max-height: 64px; + max-width: 100%; + } +} +.info { + display: grid; + grid-row: 2; + grid-template-rows: 30px 30px 30px; +} +.name { + grid-row: 1; + text-overflow: ellipsis; + overflow: hidden; +} + +.category { + grid-row: 2; + text-overflow: ellipsis; + overflow: hidden; +} + +.aliases { + grid-row: 3; + text-overflow: ellipsis; + overflow: hidden; +} + +.license { + grid-row: 4; + text-overflow: ellipsis; + overflow: hidden; +} +.editbutton { + display: grid; + grid-template-rows: 42px; + margin-top: 6px; +} +.edit { + grid-row: 1; + width: 100%; + margin: 6px 0; +} + +.request { + grid-row: 2; + width: 100%; + margin: 6px 0; +} + +.delete { + grid-row: 3; + width: 100%; + margin: 6px 0; +} +</style> diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 802129cd9d..e13240e428 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -43,65 +43,73 @@ export default defineComponent({ setup(props, { slots, expose }) { const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫 + const dateTextCache = new Map<string, string>(); + function getDateText(time: string) { + if (dateTextCache.has(time)) { + return dateTextCache.get(time)!; + } const date = new Date(time).getDate(); const month = new Date(time).getMonth() + 1; - return i18n.tsx.monthAndDay({ + const text = i18n.tsx.monthAndDay({ month: month.toString(), day: date.toString(), }); + dateTextCache.set(time, text); + return text; } if (props.items.length === 0) return; - const renderChildrenImpl = () => props.items.map((item, i) => { - if (!slots || !slots.default) return; + const renderChildrenImpl = () => { + const slotContent = slots.default ? slots.default : () => []; + return props.items.map((item, i) => { + const el = slotContent({ + item: item, + })[0]; + if (el.key == null && item.id) el.key = item.id; - const el = slots.default({ - item: item, - })[0]; - if (el.key == null && item.id) el.key = item.id; - - if ( - i !== props.items.length - 1 && - new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() - ) { - const separator = h('div', { - class: $style['separator'], - key: item.id + ':separator', - }, h('p', { - class: $style['date'], - }, [ - h('span', { - class: $style['date-1'], + if ( + i !== props.items.length - 1 && + new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() + ) { + const separator = h('div', { + class: $style['separator'], + key: item.id + ':separator', + }, h('p', { + class: $style['date'], }, [ - h('i', { - class: `ti ti-chevron-up ${$style['date-1-icon']}`, - }), - getDateText(item.createdAt), - ]), - h('span', { - class: $style['date-2'], - }, [ - getDateText(props.items[i + 1].createdAt), - h('i', { - class: `ti ti-chevron-down ${$style['date-2-icon']}`, - }), - ]), - ])); + h('span', { + class: $style['date-1'], + }, [ + h('i', { + class: `ti ti-chevron-up ${$style['date-1-icon']}`, + }), + getDateText(item.createdAt), + ]), + h('span', { + class: $style['date-2'], + }, [ + getDateText(props.items[i + 1].createdAt), + h('i', { + class: `ti ti-chevron-down ${$style['date-2-icon']}`, + }), + ]), + ])); - return [el, separator]; - } else { - if (props.ad && item._shouldInsertAd_) { - return [h(MkAd, { - key: item.id + ':ad', - prefer: ['horizontal', 'horizontal-big'], - }), el]; + return [el, separator]; } else { - return el; + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ':ad', + prefer: ['horizontal', 'horizontal-big'], + }), el]; + } else { + return el; + } } - } - }); + }); + }; const renderChildren = () => { const children = renderChildrenImpl(); @@ -120,14 +128,12 @@ export default defineComponent({ function onBeforeLeave(element: Element) { const el = element as HTMLElement; - el.style.top = `${el.offsetTop}px`; - el.style.left = `${el.offsetLeft}px`; + el.classList.add('before-leave'); } function onLeaveCancelled(element: Element) { const el = element as HTMLElement; - el.style.top = ''; - el.style.left = ''; + el.classList.remove('before-leave'); } // eslint-disable-next-line vue/no-setup-props-reactivity-loss @@ -157,21 +163,21 @@ export default defineComponent({ container-type: inline-size; &:global { - > .list-move { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } + > .list-move { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); + } - &.deny-move-transition > .list-move { - transition: none !important; - } + &.deny-move-transition > .list-move { + transition: none !important; + } - > .list-enter-active { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } + > .list-enter-active { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); + } - > *:empty { - display: none; - } + > *:empty { + display: none; + } } &:not(.date-separated-list-nogap) > *:not(:last-child) { @@ -194,20 +200,20 @@ export default defineComponent({ .direction-up { &:global { - > .list-enter-from, - > .list-leave-to { - opacity: 0; - transform: translateY(64px); - } + > .list-enter-from, + > .list-leave-to { + opacity: 0; + transform: translateY(64px); + } } } .direction-down { &:global { - > .list-enter-from, - > .list-leave-to { - opacity: 0; - transform: translateY(-64px); - } + > .list-enter-from, + > .list-leave-to { + opacity: 0; + transform: translateY(-64px); + } } } @@ -246,5 +252,8 @@ export default defineComponent({ .date-2-icon { margin-left: 8px; } -</style> +.before-leave { + position: absolute !important; +} +</style> diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 15677071af..572b0a41c9 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -23,6 +23,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <i v-else-if="type === 'info'" :class="$style.iconInner" class="ti ti-info-circle"></i> <i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i> <MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/> + <div v-if="type === 'mksw'" :class="$style.text"><MkSwitch :helpText="text" v-model="mkresult"/></div> </div> <header v-if="title" :class="$style.title"><Mfm :text="title"/></header> <div v-if="text" :class="$style.text"><Mfm :text="text"/></div> @@ -56,9 +57,10 @@ import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import { i18n } from '@/i18n.js'; +import MkSwitch from "@/components/MkSwitch.vue"; type Input = { - type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local'; + type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local' | 'mksw'; placeholder?: string | null; autocomplete?: string; default: string | number | null; @@ -77,11 +79,12 @@ type Select = { type Result = string | number | true | null; const props = withDefaults(defineProps<{ - type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting'; + type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting' | 'mksw'; title?: string; text?: string; input?: Input; select?: Select; + mksw?: boolean; icon?: string; actions?: { text: string; @@ -110,7 +113,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>(); const inputValue = ref<string | number | null>(props.input?.default ?? null); const selectedValue = ref(props.select?.default ?? null); - +const mkresult= ref(false) const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => { if (props.input) { if (props.input.minLength) { @@ -142,6 +145,7 @@ async function ok() { const result = props.input ? inputValue.value : props.select ? selectedValue.value : + mkresult ? mkresult.value : true; done(false, result); } diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 8400339e6f..3921d6f7a3 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div - :class="[$style.root, { [$style.isSelected]: isSelected }]" + :class="[$style.root, { [$style.isSelected]: isSelected || isSelectedFile }]" draggable="true" :title="title" @click="onClick" @@ -37,14 +37,15 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; +import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js'; +import { isTouchUsing } from '@/scripts/touch.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { useRouter } from '@/router/supplier.js'; @@ -55,6 +56,7 @@ const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder | null; isSelected?: boolean; selectMode?: boolean; + SelectFiles?: string[]; }>(), { isSelected: false, selectMode: false, @@ -67,13 +69,25 @@ const emit = defineEmits<{ }>(); const isDragging = ref(false); - +const isSelectedFile = ref(false); const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`); +watch(props.SelectFiles, () => { + const index = props.SelectFiles.findIndex(item => item.id === props.file.id); + isSelectedFile.value = index !== -1; +}); + function onClick(ev: MouseEvent) { + if (props.selectMode) { emit('chosen', props.file); - } else { + } else if (!ev.shiftKey && !isTouchUsing && !isSelectedFile.value) { + os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + } else if (!ev.shiftKey && isSelectedFile.value && props.SelectFiles.length === 0) { + os.popupMenu(getDriveMultiFileMenu(props.SelectFiles), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + } else if (isTouchUsing && !isSelectedFile.value && props.SelectFiles.length === 0) { + os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + }else { if (deviceKind === 'desktop') { router.push(`/my/drive/file/${props.file.id}`); } else { @@ -83,7 +97,14 @@ function onClick(ev: MouseEvent) { } function onContextmenu(ev: MouseEvent) { - os.contextMenu(getDriveFileMenu(props.file, props.folder), ev); + + if (!isTouchUsing) { + if (!ev.shiftKey && !isSelectedFile.value) { + os.contextMenu(getDriveFileMenu(props.file, props.folder), ev); + } else if (isSelectedFile.value) { + os.contextMenu(getDriveMultiFileMenu(props.SelectFiles), ev); + } + } } function onDragstart(ev: DragEvent) { @@ -92,7 +113,7 @@ function onDragstart(ev: DragEvent) { ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file)); } isDragging.value = true; - + (isDragging.value) emit('dragstart'); } diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index fd3a0cd4bb..a25cc670e0 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -45,6 +45,7 @@ const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; isSelected?: boolean; selectMode?: boolean; + selectedFiles?: string[]; }>(), { isSelected: false, selectMode: false, @@ -144,10 +145,19 @@ function onDrop(ev: DragEvent) { if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder.id, - }); + if (props.selectedFiles.length > 0) { + props.selectedFiles.forEach((e) => { + misskeyApi('drive/files/update', { + fileId: e.id, + folderId: props.folder.id, + }); + }); + } else { + misskeyApi('drive/files/update', { + fileId: file.id, + folderId: props.folder.id, + }); + } } //#endregion @@ -222,26 +232,108 @@ function rename() { } function deleteFolder() { - misskeyApi('drive/folders/delete', { + misskeyApi('drive/folders/show', { folderId: props.folder.id, - }).then(() => { - if (defaultStore.state.uploadFolder === props.folder.id) { - defaultStore.set('uploadFolder', null); + }).then(async (r) => { + if (r.foldersCount > 0) { + await os.alert({ + type: 'error', + title: i18n.ts.unableToDelete, + text: 'フォルダ内にフォルダが存在するため、削除できません。 \n フォルダ内のフォルダを削除してから試してみてください。', + }); } - }).catch(err => { - switch (err.id) { - case 'b0fc8a17-963c-405d-bfbc-859a487295e1': - os.alert({ - type: 'error', - title: i18n.ts.unableToDelete, - text: i18n.ts.hasChildFilesOrFolders, - }); - break; - default: - os.alert({ - type: 'error', - text: i18n.ts.unableToDelete, - }); + + if (r.filesCount > 0) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFolderDeleteConfirm', { name: props.folder.name }), + }); + + if (canceled) return; + + let allResults = []; + let Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31 }); + allResults = allResults.concat(Result); + while (Result.length >= 31) { + const untilId = Result[Result.length - 1].id; + Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31, untilId }); + allResults = allResults.concat(Result); // pushをconcatに変更 + } + allResults.forEach((r, i) => { + misskeyApi('drive/files/delete', { fileId: r.id }); + }); + + misskeyApi('drive/folders/show', { + folderId: props.folder.id, + }).then(async (r) => { + if (r.filesCount > 0) { + let allResults = []; + let Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31 }); + allResults = allResults.concat(Result); + while (Result.length >= 31) { + const untilId = Result[Result.length - 1].id; + Result = await misskeyApi('drive/files', { folderId: props.folder.id, limit: 31, untilId }); + allResults = allResults.concat(Result); + } + allResults.forEach((r, i) => { + misskeyApi('drive/files/delete', { fileId: r.id }); + }); + + misskeyApi('drive/folders/delete', { + folderId: props.folder.id, + }).then(() => { + if (defaultStore.state.uploadFolder === props.folder.id) { + defaultStore.set('uploadFolder', null); + } + }).catch(err => { + switch (err.id) { + case 'b0fc8a17-963c-405d-bfbc-859a487295e1': + os.alert({ + type: 'error', + title: i18n.ts.unableToDelete, + text: i18n.ts.hasChildFilesOrFolders, + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.ts.unableToDelete, + }); + } + }); + + misskeyApi('drive/folders/delete', { + folderId: props.folder.id, + }); + } else { + misskeyApi('drive/folders/delete', { + folderId: props.folder.id, + }); + } + }); + } else { + await misskeyApi('drive/folders/delete', { + folderId: props.folder.id, + }).then(() => { + if (defaultStore.state.uploadFolder === props.folder.id) { + defaultStore.set('uploadFolder', null); + } + }).catch(err => { + switch (err.id) { + case 'b0fc8a17-963c-405d-bfbc-859a487295e1': + os.alert({ + type: 'error', + title: i18n.ts.unableToDelete, + text: i18n.ts.hasChildFilesOrFolders, + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.ts.unableToDelete, + }); + } + }); } }); } diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 1f23e0e711..07d76d4564 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -25,6 +25,7 @@ import { i18n } from '@/i18n.js'; const props = defineProps<{ folder?: Misskey.entities.DriveFolder; parentFolder: Misskey.entities.DriveFolder | null; + selectedFiles: string[]; }>(); const emit = defineEmits<{ @@ -111,10 +112,19 @@ function onDrop(ev: DragEvent) { if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder ? props.folder.id : null, - }); + if (props.selectedFiles.length > 0) { + props.selectedFiles.forEach((e) => { + misskeyApi('drive/files/update', { + fileId: e.id, + folderId: props.folder ? props.folder.id : null, + }); + }); + } else { + misskeyApi('drive/files/update', { + fileId: file.id, + folderId: props.folder ? props.folder.id : null, + }); + } } //#endregion diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 689398af73..bd2babab3c 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -9,6 +9,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <XNavFolder :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" :parentFolder="folder" + :selectedFiles="selectedFiles" @move="move" @upload="upload" @removeFile="removeFile" @@ -20,16 +21,30 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :folder="f" :parentFolder="folder" :class="[$style.navPathItem]" + :selectedFiles="selectedFiles" @move="move" @upload="upload" @removeFile="removeFile" @removeFolder="removeFolder" /> </template> - <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i + class="ti ti-chevron-right" + ></i></span> <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> </div> - <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> + <button v-if="!multiple" class="_button" :class="$style.navMenu" @click="filesSelect">複数選択モード</button> + <span v-if="multiple && selectedFiles.length > 0" style="padding-right: 12px; margin-top: auto; margin-bottom: auto;opacity: 0.5;"> + ({{ number(selectedFiles.length) }}) + </span> + <button v-if="multiple" class="_button" :class="$style.navMenu" @click="filesSelect">複数選択モード解除</button> + <button v-if="multiple && selectedFiles.length === 0" style="padding-right: 12px;" class="_button" @click="filesAllSelect"> + 全選択 + </button> + <button v-if="multiple && selectedFiles.length !== 0" style="padding-right: 12px;" class="_button" @click="filesAllSelect"> + 全選択解除 + </button> + <button class="_button" @click="showMenu"><i class="ti ti-dots"></i></button> </nav> <div ref="main" @@ -50,6 +65,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :folder="f" :selectMode="select === 'folder'" :isSelected="selectedFolders.some(x => x.id === f.id)" + :selectedFiles="selectedFiles" @chosen="chooseFolder" @move="move" @upload="upload" @@ -71,7 +87,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :file="file" :folder="folder" :selectMode="select === 'file'" + :SelectFiles="selectedFiles" :isSelected="selectedFiles.some(x => x.id === file.id)" + @click.shift.left.exact="filesSelect" @chosen="chooseFile" @dragstart="isDragSource = true" @dragend="isDragSource = false" @@ -82,14 +100,21 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </div> <div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty"> <div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div> - <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div> + <div v-if="!draghover && folder == null"> + <strong>{{ + i18n.ts.emptyDrive + }}</strong><br/>{{ i18n.ts['empty-drive-description'] }} + </div> <div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div> </div> </div> <MkLoading v-if="fetching"/> </div> <div v-if="draghover" :class="$style.dropzone"></div> - <input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> + <input + ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" + @change="onChangeFileInput" + /> </div> </template> @@ -108,23 +133,24 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { uploadFile, uploads } from '@/scripts/upload.js'; import { claimAchievement } from '@/scripts/achievements.js'; +import number from '@/filters/number.js'; const props = withDefaults(defineProps<{ - initialFolder?: Misskey.entities.DriveFolder; - type?: string; - multiple?: boolean; - select?: 'file' | 'folder' | null; + initialFolder?: Misskey.entities.DriveFolder; + type?: string; + multiple?: boolean; + select?: 'file' | 'folder' | null; }>(), { multiple: false, select: null, }); const emit = defineEmits<{ - (ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; - (ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; - (ev: 'move-root'): void; - (ev: 'cd', v: Misskey.entities.DriveFolder | null): void; - (ev: 'open-folder', v: Misskey.entities.DriveFolder): void; + (ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; + (ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; + (ev: 'move-root'): void; + (ev: 'cd', v: Misskey.entities.DriveFolder | null): void; + (ev: 'open-folder', v: Misskey.entities.DriveFolder): void; }>(); const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>(); @@ -141,6 +167,8 @@ const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); const uploadings = uploads; const connection = useStream().useChannel('drive'); const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい +const multiple = ref(props.multiple || false); +const select = ref(props.select || null); // ドロップされようとしているか const draghover = ref(false); @@ -391,7 +419,7 @@ function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null function chooseFile(file: Misskey.entities.DriveFile) { const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id); - if (props.multiple) { + if (multiple.value) { if (isAlreadySelected) { selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id); } else { @@ -410,7 +438,7 @@ function chooseFile(file: Misskey.entities.DriveFile) { function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) { const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id); - if (props.multiple) { + if (multiple.value) { if (isAlreadySelected) { selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id); } else { @@ -497,6 +525,7 @@ function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) { function removeFile(file: Misskey.entities.DriveFile | string) { const fileId = typeof file === 'object' ? file.id : file; files.value = files.value.filter(f => f.id !== fileId); + selectedFiles.value = selectedFiles.value.filter(f => f.id !== fileId); } function appendFile(file: Misskey.entities.DriveFile) { @@ -623,33 +652,129 @@ function getMenu() { }, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => { selectLocalFile(); }, + action: () => { + selectLocalFile(); + }, }, { text: i18n.ts.fromUrl, icon: 'ti ti-link', - action: () => { urlUpload(); }, + action: () => { + urlUpload(); + }, }, { type: 'divider' }, { text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label', }, folder.value ? { text: i18n.ts.renameFolder, icon: 'ti ti-forms', - action: () => { if (folder.value) renameFolder(folder.value); }, + action: () => { + if (folder.value)renameFolder(folder.value); + }, } : undefined, folder.value ? { text: i18n.ts.deleteFolder, icon: 'ti ti-trash', - action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, + action: () => { + deleteFolder(folder.value as Misskey.entities.DriveFolder); + }, } : undefined, { text: i18n.ts.createFolder, icon: 'ti ti-folder-plus', - action: () => { createFolder(); }, + action: () => { + createFolder(); + }, }]; return menu; } +async function isSensitive(Files, isSensitive: boolean) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t(isSensitive ? 'driveFilesSensitiveonConfirm' : 'driveFilesSensitiveoffConfirm', { name: Files.length }), + }); + + if (canceled) return; + Files.forEach((file) => { + misskeyApi('drive/files/update', { + fileId: file.id, + isSensitive, + }); + }); +} + +async function fileDelete(Files) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFilesDeleteConfirm', { name: Files.length }), + }); + + if (canceled) return; + Files.forEach((file) => { + misskeyApi('drive/files/delete', { + fileId: file.id, + }); + }); +} + +function getFilesMenu(Files) { + return [{ + text: i18n.ts.createNoteFromTheFile, + icon: 'ti ti-pencil', + action: () => { + if (Files.length >= 16) { + os.confirm({ + type: 'warning', + text: '16ファイル以上添付しようとしています', + }); + return; + } else { + os.post({ + initialFiles: [...Files], + }); + } + }, + }, { + text: i18n.ts.unmarkAsSensitive, + icon: 'ti ti-eye', + action: () => { + isSensitive(Files, false); + }, + }, { + text: i18n.ts.markAsSensitive, + icon: 'ti ti-eye-exclamation', + action: () => { + isSensitive(Files, true); + }, + }, { + text: i18n.ts.delete, + icon: 'ti ti-trash', + danger: true, + action: () => { + fileDelete(Files); + }, + }]; +} + +function filesSelect() { + multiple.value = !multiple.value; + select.value = (select.value === null) ? 'file' : null; + selectedFiles.value = []; +} + +function filesAllSelect() { + if (selectedFiles.value.length === 0) { + selectedFiles.value = files.value; + } else { + selectedFiles.value = []; + } +} + function showMenu(ev: MouseEvent) { - os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + if (selectedFiles.value.length === 0) { + os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + } else { + os.popupMenu(getFilesMenu(selectedFiles.value), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + } } function onContextmenu(ev: MouseEvent) { @@ -693,114 +818,119 @@ onBeforeUnmount(() => { <style lang="scss" module> .root { - display: flex; - flex-direction: column; - height: 100%; + display: flex; + flex-direction: column; + height: 100%; } .nav { - display: flex; - z-index: 2; - width: 100%; - padding: 0 8px; - box-sizing: border-box; - overflow: auto; - font-size: 0.9em; - box-shadow: 0 1px 0 var(--divider); - user-select: none; + display: flex; + top: 0; + position: sticky; + z-index: 1000; + background-color: var(--bg); + width: 100%; + padding: 0 8px; + box-sizing: border-box; + overflow: auto; + font-size: 0.9em; + box-shadow: 0 1px 0 var(--divider); + user-select: none; + height: 55px; } .navPath { - display: inline-block; - vertical-align: bottom; - line-height: 42px; - white-space: nowrap; + display: inline-block; + vertical-align: bottom; + line-height: 42px; + white-space: nowrap; + margin: auto 0; } .navPathItem { - display: inline-block; - margin: 0; - padding: 0 8px; - line-height: 42px; - cursor: pointer; + display: inline-block; + margin: 0; + padding: 0 8px; + line-height: 42px; + cursor: pointer; - &:hover { - text-decoration: underline; - } + &:hover { + text-decoration: underline; + } - &.navCurrent { - font-weight: bold; - cursor: default; + &.navCurrent { + font-weight: bold; + cursor: default; - &:hover { - text-decoration: none; - } - } + &:hover { + text-decoration: none; + } + } - &.navSeparator { - margin: 0; - padding: 0; - opacity: 0.5; - cursor: default; - } + &.navSeparator { + margin: 0; + padding: 0; + opacity: 0.5; + cursor: default; + } } .navMenu { - margin-left: auto; - padding: 0 12px; + margin-left: auto; + padding: 0 12px; } .main { - flex: 1; - overflow: auto; - padding: var(--margin); - user-select: none; + flex: 1; + overflow: auto; + padding: var(--margin); + user-select: none; - &.fetching { - cursor: wait !important; - opacity: 0.5; - pointer-events: none; - } + &.fetching { + cursor: wait !important; + opacity: 0.5; + pointer-events: none; + } - &.uploading { - height: calc(100% - 38px - 100px); - } + &.uploading { + height: calc(100% - 38px - 100px); + } } .folders, .files { - display: flex; - flex-wrap: wrap; + display: flex; + flex-wrap: wrap; } .folder, .file { - flex-grow: 1; - width: 128px; - margin: 4px; - box-sizing: border-box; + flex-grow: 1; + width: 128px; + margin: 4px; + box-sizing: border-box; } .padding { - flex-grow: 1; - pointer-events: none; - width: 128px + 8px; + flex-grow: 1; + pointer-events: none; + width: 128px + 8px; } .empty { - padding: 16px; - text-align: center; - pointer-events: none; - opacity: 0.5; + padding: 16px; + text-align: center; + pointer-events: none; + opacity: 0.5; } .dropzone { - position: absolute; - left: 0; - top: 38px; - width: 100%; - height: calc(100% - 38px); - border: dashed 2px var(--focus); - pointer-events: none; + position: absolute; + left: 0; + top: 38px; + width: 100%; + height: calc(100% - 38px); + border: dashed 2px var(--focus); + pointer-events: none; } </style> diff --git a/packages/frontend/src/components/MkEmojiEditDialog.vue b/packages/frontend/src/components/MkEmojiEditDialog.vue new file mode 100644 index 0000000000..2cd685d01a --- /dev/null +++ b/packages/frontend/src/components/MkEmojiEditDialog.vue @@ -0,0 +1,309 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="windowEl" + :initialWidth="600" + :initialHeight="600" + :canResize="true" + @close="windowEl.close()" + @closed="$emit('closed')" +> + <template v-if="emoji" #header>:{{ emoji.name }}:</template> + <template v-else-if="isRequest && !emoji" #header>{{ i18n.ts.requestCustomEmojis }}</template> + <template v-else #header>New emoji</template> + + <div> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m" style="display: flex; flex-direction: row"> + <div> + <div v-if="imgUrl != null" :class="$style.imgs"> + <div style="background: #000;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + <div style="background: #222;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + <div style="background: #ddd;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + <div style="background: #fff;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + </div> + <MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton> + <MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off"> + <template #label>{{ i18n.ts.name }}</template> + <template #caption>{{ i18n.ts.emojiNameValidation }}</template> + </MkInput> + <MkInput v-model="category" :datalist="customEmojiCategories"> + <template #label>{{ i18n.ts.category }}</template> + </MkInput> + <MkInput v-model="aliases" autocapitalize="off"> + <template #label>{{ i18n.ts.tags }}</template> + <template #caption> + {{ i18n.ts.theKeywordWhenSearchingForCustomEmoji }}<br/> + {{ i18n.ts.setMultipleBySeparatingWithSpace }} + </template> + </MkInput> + <MkInput v-model="license" :mfmAutocomplete="true"> + <template #label>{{ i18n.ts.license }}</template> + </MkInput> + <MkFolder v-if="!isRequest"> + <template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template> + <template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template> + + <div class="_gaps"> + <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + + <div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem"> + <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> + <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button> + <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> + </div> + + <MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo> + <MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo> + </div> + </MkFolder> + <MkSwitch v-model="isSensitive">{{ i18n.ts.isSensitive }}</MkSwitch> + <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> + <MkSwitch v-model="isNotifyIsHome"> + {{ i18n.ts.isNotifyIsHome }} + </MkSwitch> + </div> + <div v-if="imgUrl" style="width: 30%"> + <MkInput v-model="text"> + <template #label>テスト文章</template> + </MkInput><br/> + <MkNoteSimple :emojireq="true" :note="{isHidden:false,replyId:null,renoteId:null,files:[],user: $i,text:text,cw:null, emojis: {[name]: imgUrl}}"/> + <p v-if="speed ">基準より眩しい可能性があります。</p> + <p v-if="!speed">問題は見つかりませんでした。</p> + <p>※上記の物は問題がないことを保証するものではありません。</p> + </div> + </div> + </MkSpacer> + + <div :class="$style.footer"> + <div :class="$style.footerButtons"> + <MkButton v-if="!isRequest" danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-if="validation" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton> + <MkButton v-else rounded style="margin: 0 auto;"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton> + </div> + </div> + </div> +</MkWindow> +</template> + +<script lang="ts" setup> +import { computed, onMounted, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkWindow from '@/components/MkWindow.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { customEmojiCategories } from '@/custom-emojis.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { selectFile, selectFiles } from '@/scripts/select-file.js'; +import MkRolePreview from '@/components/MkRolePreview.vue'; +import { $i } from '@/account.js'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +const props = defineProps<{ + emoji?: any, + isRequest: boolean, +}>(); +const text = ref<string>('テスト文章'); +const speed = ref<boolean>(false); +const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); +const name = ref<string>(props.emoji ? props.emoji.name : ''); +const category = ref<string>(props.emoji ? props.emoji.category : ''); +const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : ''); +const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : ''); +const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false); +const localOnly = ref(props.emoji ? props.emoji.localOnly : false); +const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); +const rolesThatCanBeUsedThisEmojiAsReaction = ref<Misskey.entities.Role[]>([]); +const file = ref<Misskey.entities.DriveFile>(); +let isRequest = ref(props.isRequest ?? false); +watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { + rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); +}, { immediate: true }); +const isNotifyIsHome = ref(props.emoji ? props.emoji.isNotifyIsHome : false); +const imgUrl = computed(() => file.value ? file.value.url : props.emoji && !isRequest.value ? `/emoji/${props.emoji.name}.webp` : props.emoji && props.emoji.url ? props.emoji.url : null); +const validation = computed(() => { + return name.value.match(/^[a-zA-Z0-9_]+$/) && imgUrl.value != null; +}); +const emit = defineEmits<{ + (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, + (ev: 'closed'): void +}>(); + +const colorChanges = ref<number | null>(null); + +watch(colorChanges, (value) => { + console.log(value); +}); + +async function changeImage(ev) { + file.value = await selectFile(ev.currentTarget ?? ev.target, null); + const candidate = file.value.name.replace(/\.(.+)$/, ''); + if (candidate.match(/^[a-z0-9_]+$/)) { + name.value = candidate; + } +} + +async function addRole() { + const roles = await misskeyApi('admin/roles/list'); + const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id); + + const { canceled, result: role } = await os.select({ + items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })), + }); + if (canceled || role == null) return; + + rolesThatCanBeUsedThisEmojiAsReaction.value.push(role); +} + +async function removeRole(role, ev) { + rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id); +} + +async function done() { + const params = { + name: name.value, + category: category.value === '' ? null : category.value, + aliases: aliases.value.replace(' ', ' ').split(' ').filter(x => x !== ''), + license: license.value === '' ? null : license.value, + Request: isRequest.value, + isSensitive: isSensitive.value, + localOnly: localOnly.value, + roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id), + isNotifyIsHome: isNotifyIsHome.value, + }; + + if (file.value) { + params.fileId = file.value.id; + } + if (props.emoji) { + if (isRequest.value) { + await os.apiWithDialog('admin/emoji/update-request', { + id: props.emoji.id, + ...params, + }); + } else { + await os.apiWithDialog('admin/emoji/update', { + id: props.emoji.id, + ...params, + }); + } + + emit('done', { + updated: { + id: props.emoji.id, + ...params, + }, + }); + + windowEl.value.close(); + } else { + const created = isRequest.value + ? await os.apiWithDialog('admin/emoji/add-request', params) + : await os.apiWithDialog('admin/emoji/add', params); + + emit('done', { + created: created, + }); + + windowEl.value.close(); + } +} + +async function del() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.removeAreYouSure({ x: name }), + }); + if (canceled) return; + + misskeyApi('admin/emoji/delete', { + id: props.emoji.id, + }).then(() => { + emit('done', { + deleted: true, + }); + windowEl.value.close(); + }); +} + +watch(imgUrl, async (value) => { + speed.value = await misskeyApi('emoji/speedtest', { + url: value, + }); +}); +</script> + +<style lang="scss" module> +.imgs { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} + +.imgContainer { + padding: 8px; + border-radius: 6px; +} + +.img { + display: block; + height: 64px; + width: 64px; + object-fit: contain; +} +.preview { + display: block; + height: 16px; + + object-fit: contain; +} + +.roleItem { + display: flex; +} + +.role { + flex: 1; +} + +.roleUnassign { + width: 32px; + height: 32px; + margin-left: 8px; + align-self: center; +} + +.footer { + position: sticky; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +.footerButtons { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} +</style> diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 01dfcdc7a3..e0a5e111a1 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -3,60 +3,58 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと --> -<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) --> -<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);"> - <header class="_acrylic" @click="shown = !shown"> - <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }}) - </header> - <div v-if="shown" class="body"> - <button - v-for="emoji in emojis" - :key="emoji" - :data-emoji="emoji" - class="_button item" - :disabled="disabledEmojis?.value.includes(emoji)" - @pointerenter="computeButtonTitle" - @click="emit('chosen', emoji, $event)" - > - <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true" :fallbackToImage="true"/> - <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> - </button> - </div> -</section> -<!-- フォルダの中にはカスタム絵文字やフォルダがある --> -<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);"> - <header class="_acrylic" @click="shown = !shown"> - <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }}) - </header> - <div v-if="shown" style="padding-left: 9px;"> - <MkEmojiPickerSection - v-for="child in customEmojiTree" - :key="`custom:${child.value}`" - :initialShown="initialShown" - :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))" - :hasChildSection="child.children.length !== 0" - :customEmojiTree="child.children" - @chosen="nestedChosen" - > - {{ child.value || i18n.ts.other }} - </MkEmojiPickerSection> - </div> - <div v-if="shown" class="body"> - <button - v-for="emoji in emojis" - :key="emoji" - :data-emoji="emoji" - class="_button item" - :disabled="disabledEmojis?.value.includes(emoji)" - @pointerenter="computeButtonTitle" - @click="emit('chosen', emoji, $event)" - > - <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> - <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> - </button> - </div> -</section> + <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと --> + <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) --> + <section v-if="!hasChildSection" style="border-radius: 6px;"> + <header class="_acrylic" @click="shown = !shown" > + <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> ({{ emojis.length }}) + </header> + <div v-if="shown" class="body"> + <button + v-for="emoji in emojis" + :key="emoji" + :data-emoji="emoji" + class="_button item" + :disabled="disabledEmojis?.value.includes(emoji)"@pointerenter="computeButtonTitle" + @click="emit('chosen', emoji, $event)" + > + <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true":fallbackToImage="true"/> + <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> + </button> + </div> + </section> + <!-- フォルダの中にはカスタム絵文字やフォルダがある --> + <section v-else style="border-radius: 6px;"> + <header class="_acrylic" @click="shown = !shown"> + <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (フォルダー) + </header> + <div v-if="shown" style="padding-left: 9px; "> + <MkEmojiPickerSection + v-for="child in customEmojiTree" + :key="`custom:${child.value}`" + :initialShown="initialShown" + :emojis="computed(() => customEmojis.filter(e => e.category === child.category || e.category === child.category+'/'+child.category).map(e => `:${e.name}:`))" + :hasChildSection="child.children.length !== 0" + :customEmojiTree="child.children" + @chosen="nestedChosen" + > + {{ child.value || i18n.ts.other }} + </MkEmojiPickerSection> + </div> + <div v-if="shown" class="body"> + <button + v-for="emoji in emojis" + :key="emoji" + :data-emoji="emoji" + class="_button item":disabled="disabledEmojis?.value.includes(emoji)" + @pointerenter="computeButtonTitle" + @click="emit('chosen', emoji, $event)" + > + <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> + <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> + </button> + </div> + </section> </template> <script lang="ts" setup> @@ -67,19 +65,18 @@ import { customEmojis } from '@/custom-emojis.js'; import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; const props = defineProps<{ + category?: string[]; emojis: string[] | Ref<string[]>; disabledEmojis?: Ref<string[]>; initialShown?: boolean; hasChildSection?: boolean; customEmojiTree?: CustomEmojiFolderTree[]; }>(); - const emit = defineEmits<{ (ev: 'chosen', v: string, event: MouseEvent): void; }>(); const emojis = computed(() => Array.isArray(props.emojis) ? props.emojis : props.emojis.value); - const shown = ref(!!props.initialShown); /** @see MkEmojiPicker.vue */ @@ -89,7 +86,7 @@ function computeButtonTitle(ev: MouseEvent): void { elm.title = getEmojiName(emoji); } -function nestedChosen(emoji: any, ev: MouseEvent) { +function nestedChosen(emoji: any, ev?: MouseEvent) { emit('chosen', emoji, ev); } </script> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 7ad9c12ceb..26ea25fc94 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -36,7 +36,12 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </section> <div v-if="tab === 'index'" class="group index"> - <section v-if="showPinned && (pinned && pinned.length > 0)"> + <section v-if="showPinned"> + <div style="display: flex; "> + <div v-for="a in profileMax" :key="a" :title="defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`]" class="sllfktkhgl" :class="{ active: activeIndex === a || isDefaultProfile === a }" @click="pinnedProfileSelect(a)"> + {{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }} + </div> + </div> <div class="body"> <button v-for="emoji in pinnedEmojisDef" @@ -121,12 +126,15 @@ import { deviceKind } from '@/scripts/device-kind.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; - +import { deepClone } from '@/scripts/clone.js'; +import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; +import MkEmoji from '@/components/global/MkEmoji.vue'; +const $i = signinRequired(); const props = withDefaults(defineProps<{ showPinned?: boolean; - pinnedEmojis?: string[]; + pinnedEmojis?: string[]; maxHeight?: number; asDrawer?: boolean; asWindow?: boolean; @@ -139,7 +147,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', v: string): void; }>(); - +const profileMax = $i.policies.emojiPickerProfileLimit; const searchEl = shallowRef<HTMLInputElement>(); const emojisEl = shallowRef<HTMLDivElement>(); @@ -154,7 +162,7 @@ const recentlyUsedEmojisDef = computed(() => { return recentlyUsedEmojis.value.map(getDef); }); const pinnedEmojisDef = computed(() => { - return pinned.value?.map(getDef); + return pinnedEmojis.value?.map(getDef); }); const pinned = computed(() => props.pinnedEmojis); @@ -165,25 +173,29 @@ const q = ref<string>(''); const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); - +const pinnedEmojis = ref(pinned.value); const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] }; function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree { - const parts = input.split('/').map(p => p.trim()); - let currentNode: CustomEmojiFolderTree = root; - - for (const part of parts) { - let existingNode = currentNode.children.find((node) => node.value === part); - - if (!existingNode) { - const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; - currentNode.children.push(newNode); - existingNode = newNode; - } - - currentNode = existingNode; + const parts = input.split('/').map(p => p.trim()); // スラッシュで区切って配列にしてる + let currentNode: CustomEmojiFolderTree = root; // currentNode は root + let includesPart = customEmojis.value.some(emoji => emoji.category !== null && emoji.category.includes(parts[0] + '/')); + if (parts.length === 1 && parts[0] !== '' && includesPart) { // parts が 1 つで空じゃなかったら + parts.push(parts[0]); // parts に parts[0] を追加 (test category だったら test/test category になる) } + for (const part of parts) { // parts を順番に見ていく + let existingNode = currentNode.children.find((node) => node.value === part); // currentNode の children から part と同じ value を持つ node を探す + + if (!existingNode) { // なかったら + const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; // 新しい node を作る + + currentNode.children.push(newNode); // currentNode の children に newNode を追加 + existingNode = newNode; // existingNode に newNode を代入 + } + + currentNode = existingNode; // currentNode に existingNode を代入 + } return currentNode; } @@ -245,9 +257,7 @@ watch(q, () => { if (matches.size >= max) break; } } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { + if (matches.size >= max) return matches; for (const emoji of emojis) { if (emoji.name.startsWith(newQ)) { matches.add(emoji); if (matches.size >= max) break; @@ -357,7 +367,7 @@ function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string } function filterCategory(emoji: Misskey.entities.EmojiSimple, category: string): boolean { - return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category; + return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category && !customEmojis.value.some(e => e.category !== null && e.category.includes(emoji.category + '/')) || emoji.category === category + '/' + category && !emoji.category; } function focus() { @@ -435,6 +445,14 @@ function onEnter(ev: KeyboardEvent) { done(); } +const activeIndex = ref(defaultStore.state.pickerProfileDefault); +pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]); + +function pinnedProfileSelect(index:number) { + pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${index > 1 ? index - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${index > 1 ? index - 1 : ''}`]); + activeIndex.value = index; +} + function done(query?: string): boolean | void { if (query == null) query = q.value; if (query == null || typeof query !== 'string') return; @@ -660,8 +678,8 @@ defineExpose({ > header { /*position: sticky; - top: 0; - left: 0;*/ +top: 0; +left: 0;*/ height: 32px; line-height: 32px; z-index: 2; @@ -675,7 +693,8 @@ defineExpose({ position: sticky; top: 0; left: 0; - line-height: 28px; + height: 32px; + line-height: 32px; z-index: 1; padding: 0 8px; font-size: 12px; @@ -745,4 +764,24 @@ defineExpose({ } } } +.sllfktkhgl{ + display: inline-block; + padding: 0 4px; + font-size: 12px; + line-height: 32px; + text-align: center; + color: var(--fg); + cursor: pointer; + width: 100%; + transition: transform 0.3s ease; + box-shadow: 0 1.5px 0 var(--divider); + height: 32px; + overflow: hidden; + &:hover { + transform: translateY(1.5px); + } + &.active { + transform: translateY(5px); + } +} </style> diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index e1b264b280..aa75bdb50b 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -48,7 +48,6 @@ const props = withDefaults(defineProps<{ const rootEl = shallowRef<HTMLDivElement>(); const bg = ref<string>(); const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); - watch(showBody, () => { if (props.persistKey) { miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f'); diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index b459e10570..f43f2834a8 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -1,56 +1,79 @@ <!-- -SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License-Identifier: AGPL-3.0-only +SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project +SPDX-License-Identifier: AGPL-3.0-only --> - <template> -<button - class="_button" - :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]" - :disabled="wait" - @click="onClick" -> - <template v-if="!wait"> - <template v-if="hasPendingFollowRequestFromYou && user.isLocked"> - <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i> - </template> - <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> - <!-- つまりリモートフォローの場合。 --> - <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> - </template> - <template v-else-if="isFollowing"> - <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> - </template> - <template v-else-if="!isFollowing && user.isLocked"> - <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> - </template> - <template v-else-if="!isFollowing && !user.isLocked"> - <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> - </template> - </template> - <template v-else> - <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> - </template> -</button> + <button + class="_button" + :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large },{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' +,}]" + :disabled="wait" + @click="onClick" + > + <template v-if="!wait"> + <template v-if="hasPendingFollowRequestFromYou && user.isLocked"> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light',}]">{{ + i18n.ts.followRequestPending + }}</span><i class="ti ti-hourglass-empty"></i> + </template> + <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> + <!-- つまりリモートフォローの場合。 --> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }] ">{{ + i18n.ts.processing + }}</span> + <MkLoading :em="true" :colored="false"/> + </template> + <template v-else-if="isFollowing"> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }] ">{{ + i18n.ts.unfollow + }}</span><i class="ti ti-minus"></i> + </template> + <template v-else-if="!isFollowing && user.isLocked"> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">{{ + i18n.ts.followRequest + }}</span><i class="ti ti-plus"></i> + </template> + <template v-else-if="!isFollowing && !user.isLocked"> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">{{ + i18n.ts.follow + }}</span><i class="ti ti-plus"></i> + </template> + </template> + <template v-else> + <span v-if="full" + :class="[$style.text,{[$style.gamingDark]: gamingType === 'dark' ,[$style.gamingLight]: gamingType === 'light'} ]">{{ + i18n.ts.processing + }}</span> + <MkLoading :em="true" :colored="false"/> + </template> + </button> </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, ref } from 'vue'; +import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useStream } from '@/stream.js'; -import { i18n } from '@/i18n.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import {useStream} from '@/stream.js'; +import {i18n} from '@/i18n.js'; +import {claimAchievement} from '@/scripts/achievements.js'; +import {$i} from '@/account.js'; +import {defaultStore} from '@/store.js'; + +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = withDefaults(defineProps<{ - user: Misskey.entities.UserDetailed, - full?: boolean, - large?: boolean, + user: Misskey.entities.UserDetailed, + full?: boolean, + large?: boolean, }>(), { - full: false, - large: false, + full: false, + large: false, }); const emit = defineEmits<{ @@ -63,36 +86,36 @@ const wait = ref(false); const connection = useStream().useChannel('main'); if (props.user.isFollowing == null) { - misskeyApi('users/show', { - userId: props.user.id, - }) - .then(onFollowChange); + misskeyApi('users/show', { + userId: props.user.id, + }) + .then(onFollowChange); } function onFollowChange(user: Misskey.entities.UserDetailed) { - if (user.id === props.user.id) { - isFollowing.value = user.isFollowing; - hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou; - } + if (user.id === props.user.id) { + isFollowing.value = user.isFollowing; + hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou; + } } async function onClick() { - wait.value = true; + wait.value = true; - try { - if (isFollowing.value) { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }), - }); + try { + if (isFollowing.value) { + const {canceled} = await os.confirm({ + type: 'warning', + text: i18n.tsx.unfollowConfirm({name: props.user.name || props.user.username}), + }); - if (canceled) return; + if (canceled) return; - await misskeyApi('following/delete', { - userId: props.user.id, - }); - } else { - if (defaultStore.state.alwaysConfirmFollow) { + await misskeyApi('following/delete', { + userId: props.user.id, + }); + } else { + if (defaultStore.state.alwaysConfirmFollow) { const { canceled } = await os.confirm({ type: 'question', text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }), @@ -105,14 +128,14 @@ async function onClick() { } if (hasPendingFollowRequestFromYou.value) { - await misskeyApi('following/requests/cancel', { - userId: props.user.id, - }); - hasPendingFollowRequestFromYou.value = false; - } else { - await misskeyApi('following/create', { - userId: props.user.id, - withReplies: defaultStore.state.defaultWithReplies, + await misskeyApi('following/requests/cancel', { + userId: props.user.id, + }); + hasPendingFollowRequestFromYou.value = false; + } else { + await misskeyApi('following/create', { + userId: props.user.id, + withReplies: defaultStore.state.defaultWithReplies, }); emit('update:user', { ...props.user, @@ -120,113 +143,298 @@ async function onClick() { }); hasPendingFollowRequestFromYou.value = true; - if ($i == null) return; + if ($i == null) return; claimAchievement('following1'); - if ($i.followingCount >= 10) { - claimAchievement('following10'); - } - if ($i.followingCount >= 50) { - claimAchievement('following50'); - } - if ($i.followingCount >= 100) { - claimAchievement('following100'); - } - if ($i.followingCount >= 300) { - claimAchievement('following300'); - } - } - } - } catch (err) { - console.error(err); - } finally { - wait.value = false; - } + if ($i.followingCount >= 10) { + claimAchievement('following10'); + } + if ($i.followingCount >= 50) { + claimAchievement('following50'); + } + if ($i.followingCount >= 100) { + claimAchievement('following100'); + } + if ($i.followingCount >= 300) { + claimAchievement('following300'); + } + } + } + } catch (err) { + console.error(err); + } finally { + wait.value = false; + } } onMounted(() => { - connection.on('follow', onFollowChange); - connection.on('unfollow', onFollowChange); + connection.on('follow', onFollowChange); + connection.on('unfollow', onFollowChange); }); onBeforeUnmount(() => { - connection.dispose(); + connection.dispose(); }); </script> <style lang="scss" module> .root { - position: relative; - display: inline-block; - font-weight: bold; - color: var(--fgOnWhite); - border: solid 1px var(--accent); - padding: 0; - height: 31px; - font-size: 16px; - border-radius: 32px; - background: #fff; + position: relative; + display: inline-block; + font-weight: bold; + color: var(--fgOnWhite); + border: solid 1px var(--accent); + padding: 0; + height: 31px; + font-size: 16px; + border-radius: 32px; + background: #fff; - &.full { - padding: 0 8px 0 12px; - font-size: 14px; - } - &.large { - font-size: 16px; - height: 38px; - padding: 0 12px 0 16px; - } + &.gamingDark { + color: black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border: solid 1px black; + } - &:not(.full) { - width: 31px; - } + &.gamingLight { + color: white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border: solid 1px white; + } - &:focus-visible { - &:after { - content: ""; - pointer-events: none; - position: absolute; - top: -5px; - right: -5px; - bottom: -5px; - left: -5px; - border: 2px solid var(--focus); - border-radius: 32px; - } - } + &.full { + padding: 0 8px 0 12px; + font-size: 14px; + &.gamingDark { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; - &:hover { - //background: mix($primary, #fff, 20); - } + } - &:active { - //background: mix($primary, #fff, 40); - } + &.gamingLight { + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } - &.active { - color: var(--fgOnAccent); - background: var(--accent); + &.large { + font-size: 16px; + height: 38px; + padding: 0 12px 0 16px; + } - &:hover { - background: var(--accentLighten); - border-color: var(--accentLighten); - } + &:not(.full) { + width: 31px; + } - &:active { - background: var(--accentDarken); - border-color: var(--accentDarken); - } - } + &:focus-visible { + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: -5px; + right: -5px; + bottom: -5px; + left: -5px; + border: 2px solid var(--focus); + border-radius: 32px; + } + } - &.wait { - cursor: wait !important; - opacity: 0.7; - } + &:hover { + //background: mix($primary, #fff, 20); + } + + &:active { + //background: mix($primary, #fff, 40); + } + + &.active { + color: var(--fgOnAccent); + background: var(--accent); + + &:hover { + background: var(--accentLighten); + border-color: var(--accentLighten); + } + + &:active { + background: var(--accentDarken); + border-color: var(--accentDarken); + } + + &.gamingDark:hover { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border: solid 1px white; + } + + &.gamingDark:active { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border: solid 1px white; + } + + &.gamingLight:hover { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border: solid 1px white; + } + + &.gamingLight:active { + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border: solid 1px white; + } + + &.gamingDark { + -webkit-text-fill-color: unset !important; + color: black; + border: solid 1px white; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingLight { + -webkit-text-fill-color: unset !important; + color: white; + border: solid 1px white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + + } + + } + + &.wait { + cursor: wait !important; + opacity: 0.7; + } } .text { - margin-right: 6px; + margin-right: 6px; + + &.gamingDark { + color: black; + + } + + &.gamingLight { + color: white; + + } + +} + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 922c8b6da7..9dd492678a 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -5,16 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="bg"> - <img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/> + <img v-if="faviconUrl && !defaultStore.state.enableUltimateDataSaverMode" :class="$style.icon" :src="faviconUrl"/> <div :class="$style.name">{{ instance.name }}</div> </div> </template> <script lang="ts" setup> import { computed } from 'vue'; -import { instanceName } from '@/config.js'; -import { instance as Instance } from '@/instance.js'; -import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; +import { instanceName } from '@/config'; +import { instance as Instance } from '@/instance'; +import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; +import {defaultStore} from "@/store"; const props = defineProps<{ instance?: { diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index dcf26ab7ac..becf035c92 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -7,16 +7,16 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }"> <div class="main"> <template v-for="item in items" :key="item.text"> - <button v-if="item.action" v-click-anime class="_button item" @click="$event => { item.action($event); close(); }"> + <button v-if="item.action" v-click-anime class="_button item" :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }" @click="$event => { item.action($event); close(); }"> <i class="icon" :class="item.icon"></i> <div class="text">{{ item.text }}</div> - <span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> + <span v-if="item.indicate && item.indicateValue && indicatorCounterToggle" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> </button> - <MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()"> + <MkA v-else v-click-anime :to="item.to" class="item" :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }" @click.passive="close()"> <i class="icon" :class="item.icon"></i> <div class="text">{{ item.text }}</div> - <span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> + <span v-if="item.indicate && item.indicateValue && indicatorCounterToggle" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> </MkA> </template> @@ -26,12 +26,16 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> +import { ref , computed , watch} from 'vue'; import { shallowRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import { navbarItemDef } from '@/navbar.js'; import { defaultStore } from '@/store.js'; import { deviceKind } from '@/scripts/device-kind.js'; +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); +const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle')); + const props = withDefaults(defineProps<{ src?: HTMLElement; anchor?: { x: string; y: string; }; @@ -100,6 +104,20 @@ function close() { vertical-align: bottom; height: 100px; border-radius: 10px; + &.gamingDark:hover{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + color: black; + } + &.gamingLight:hover{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + color: white; + } padding: 10px; box-sizing: border-box; @@ -148,4 +166,69 @@ function close() { } } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index f0ead439f2..6c7060e69f 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -4,6 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div + ref="playerEl" v-hotkey="keymap" tabindex="0" @@ -28,9 +29,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License preload="metadata" controls :class="$style.nativeAudio" + :src="audio.url" @keydown.prevent > - <source :src="audio.url"> </audio> </div> @@ -38,8 +39,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <audio ref="audioEl" preload="metadata" + :src="audio.url" > - <source :src="audio.url"> </audio> <div :class="[$style.controlsChild, $style.controlsLeft]"> <button class="_button" :class="$style.controlButton" @click="togglePlayPause"> @@ -63,10 +64,24 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :class="$style.volumeSeekbar" /> </div> + + <WaveSurferPlayer + v-if="!defaultStore.state.dataSaver.media && audioEl" + :class="$style.seekbarRoot" + :options="{ media: audioEl, + height: 32, + waveColor: 'gray', + progressColor: accent, + barGap: 3, + barWidth: 3, + barRadius: 5, + duration: 80, + }" + ></WaveSurferPlayer> <MkMediaRange + v-if="defaultStore.state.dataSaver.media && !hide" v-model="rangePercent" :class="$style.seekbarRoot" - :buffer="bufferedDataRatio" /> </div> </div> @@ -75,6 +90,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <script lang="ts" setup> import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; +import { WaveSurferPlayer } from '@meersagor/wavesurfer-vue'; +import tinycolor from 'tinycolor2'; +import type WaveSurfer from 'wavesurfer.js'; import type { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -83,7 +101,6 @@ import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; import { $i, iAmModerator } from '@/account.js'; - const props = defineProps<{ audio: Misskey.entities.DriveFile; }>(); @@ -130,6 +147,7 @@ const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.data // Menu const menuShowing = ref(false); +const accent = ref(); function showMenu(ev: MouseEvent) { let menu: MenuItem[] = []; @@ -225,10 +243,7 @@ const volume = ref(.25); const speed = ref(1); const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える const bufferedEnd = ref(0); -const bufferedDataRatio = computed(() => { - if (!audioEl.value) return 0; - return bufferedEnd.value / audioEl.value.duration; -}); +let audioContext = new AudioContext(); // MediaControl Events function togglePlayPause() { @@ -259,7 +274,8 @@ let stopAudioElWatch: () => void; function init() { if (onceInit) return; onceInit = true; - + const computedStyle = getComputedStyle(document.documentElement); + accent.value = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); stopAudioElWatch = watch(audioEl, () => { if (audioEl.value) { isReady.value = true; @@ -324,7 +340,7 @@ watch(loop, (to) => { if (audioEl.value) audioEl.value.loop = to; }); -onMounted(() => { +onMounted(async () => { init(); }); diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 7258fcc540..518cb04192 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior"> +<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe && gamingType === '' , [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior"> <img :class="$style.icon" :src="avatarUrl" alt=""> <span> <span>@{{ username }}</span> @@ -14,7 +14,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <script lang="ts" setup> import { toUnicode } from 'punycode'; -import { computed } from 'vue'; +import {computed, ref, watch} from 'vue'; import tinycolor from 'tinycolor2'; import { host as localHost } from '@/config.js'; import { $i } from '@/account.js'; @@ -22,6 +22,8 @@ import { defaultStore } from '@/store.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { MkABehavior } from '@/components/global/MkA.vue'; +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); + const props = defineProps<{ username: string; host: string; @@ -38,12 +40,13 @@ const isMe = $i && ( const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention')); bg.setAlpha(0.1); -const bgCss = bg.toRgbString(); const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) : `/avatar/@${props.username}@${props.host}`, ); +const bgCss = (gamingType.value === '') ? bg.toRgbString() : ""; +//const bgCss = `background:${bg.toRgbString()}; ${result}` ; </script> <style lang="scss" module> @@ -53,8 +56,26 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages border-radius: 999px; color: var(--mention); + &.gamingLight{ + color: white; + opacity: 0.9; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + &.gamingDark{ + opacity: 0.9; + color: white; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + &.isMe { color: var(--mentionMe); + } } @@ -70,4 +91,69 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages .host { opacity: 0.5; } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 7303fa728a..3ed577dc9f 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -7,19 +7,19 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div ref="itemsEl" v-hotkey="keymap" class="_popup _shadow" - :class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]" + :class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer },{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" @contextmenu.self="e => e.preventDefault()" > <template v-for="(item, i) in (items2 ?? [])"> <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div> - <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> + <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"> <span style="opacity: 0.7;">{{ item.text }}</span> </span> - <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]"> + <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"> <span><MkEllipsis/></span> </span> - <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="[$style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> @@ -27,22 +27,22 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </div> </MkA> - <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="[$style.item,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </div> </a> - <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active },{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> <div v-if="item.indicate" :class="$style.item_content"> <span :class="$style.indicator"><i class="_indicatorCircle"></i></span> </div> </button> - <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> - <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> + <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } , { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> + <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" model-value/> <div :class="$style.item_content"> <span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span> <MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> @@ -63,15 +63,15 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <span :class="$style.item_content_text">{{ item.text }}</span> </div> </button> - <button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)"> + <button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item } , { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> - <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> + <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> @@ -92,11 +92,13 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <script lang="ts"> import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { focusPrev, focusNext } from '@/scripts/focus.js'; -import MkSwitchButton from '@/components/MkSwitch.button.vue'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { isTouchUsing } from '@/scripts/touch.js'; +import {defaultStore} from '@/store.js' +import MkSwitchButton from '@/components/MkSwitch.button.vue'; +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); </script> @@ -357,10 +359,32 @@ onBeforeUnmount(() => { &:not(:disabled):hover { color: var(--accent); text-decoration: none; + &:before { + background: var(--accentedBg); + } + &.gamingDark{ + color:black !important; + } + &.gamingLight{ + color:white !important; + } + &.gamingDark:before{ + color:black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + + } + &.gamingLight:before{ + color:white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + + } - &:before { - background: var(--accentedBg); - } } &.danger { @@ -385,12 +409,32 @@ onBeforeUnmount(() => { &:active, &.active { - color: var(--fgOnAccent) !important; + color: var(--fgOnAccent); opacity: 1; - + &.gamingDark{ + color:black !important; + } + &.gamingLight{ + color:white !important; + } &:before { - background: var(--accent) !important; + background: var(--accent); } + &.gamingDark:before{ + color:black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingLight:before{ + color:white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } } &.radioActive { @@ -432,9 +476,32 @@ onBeforeUnmount(() => { color: var(--accent); text-decoration: none; - &:before { - background: var(--accentedBg); - } + &:before { + background: var(--accentedBg); + } + &.gamingDark{ + color:black !important; + } + &.gamingLight{ + color:white !important; + } + &.gamingDark:before{ + color:black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingLight:before{ + color:white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + + } + } } } @@ -502,10 +569,11 @@ onBeforeUnmount(() => { } .indicator { - display: flex; - align-items: center; + position: absolute; + top: 5px; + right: 18px; color: var(--indicator); - font-size: 12px; + font-size: 8px; animation: global-blink 1s infinite; } @@ -541,4 +609,69 @@ onBeforeUnmount(() => { } } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e7bb955697..a995bdf943 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -8,7 +8,14 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" + :class="[$style.root, + { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover } , + {[$style.home] : defaultStore.state.showVisibilityColor && note.visibility === 'home' + ,[$style.followers] : defaultStore.state.showVisibilityColor && note.visibility === 'followers' + ,[$style.specified] : defaultStore.state.showVisibilityColor && note.visibility === 'specified' + },{[$style.localonly] : defaultStore.state.showVisibilityColor && note.localOnly } + ]" + :tabindex="!isDeleted ? '-1' : undefined" > <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> @@ -31,7 +38,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <i class="ti ti-dots" :class="$style.renoteMenu"></i> <MkTime :time="note.createdAt"/> </button> - <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> + <span + v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" + :title="i18n.ts._visibility[note.visibility]" + > <i v-if="note.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> @@ -118,8 +128,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <i class="ti ti-ban"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReactions?.length >= 4 " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-else-if="appearNote.myReactions?.length >= 3 || appearNote.myReaction && appearNote.user.host" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ti ti-plus"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -134,7 +144,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </div> </article> </div> -<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false"> +<div v-else-if="muted && !hideMutedNotes" :class="$style.muted" @click="muted = false"> <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> @@ -199,7 +209,7 @@ import { shouldCollapsed } from '@/scripts/collapsed.js'; import { isEnabledUrlPreview } from '@/instance.js'; const props = withDefaults(defineProps<{ - note: Misskey.entities.Note; + note: Misskey.entities.Note & {myReactions: string[]}; pinned?: boolean; mock?: boolean; withHardMute?: boolean; @@ -217,7 +227,6 @@ const emit = defineEmits<{ const inTimeline = inject<boolean>('inTimeline', false); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); - const note = ref(deepClone(props.note)); // plugin @@ -433,25 +442,9 @@ function react(viaKeyboard = false): void { } } -function undoReact(targetNote: Misskey.entities.Note): void { - const oldReaction = targetNote.myReaction; - if (!oldReaction) return; - - if (props.mock) { - emit('removeReaction', oldReaction); - return; - } - - misskeyApi('notes/reactions/delete', { - noteId: targetNote.id, - }); -} - function toggleReact() { - if (appearNote.value.myReaction == null) { + if (appearNote.value.myReactions?.length < 4 || appearNote.value.myReaction && appearNote.value.user.host || !appearNote.value.myReactions ) { react(); - } else { - undoReact(appearNote.value); } } @@ -477,7 +470,14 @@ function onContextmenu(ev: MouseEvent): void { ev.preventDefault(); react(); } else { - const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ + note: note.value, + translating, + translation, + + isDeleted, + currentClip: currentClip?.value, + }); os.contextMenu(menu, ev).then(focus).finally(cleanup); } } @@ -487,7 +487,14 @@ function showMenu(viaKeyboard = false): void { return; } - const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ + note: note.value, + translating, + translation, + + isDeleted, + currentClip: currentClip?.value, + }); os.popupMenu(menu, menuButton.value, { viaKeyboard, }).then(focus).finally(cleanup); @@ -498,7 +505,11 @@ async function clip() { return; } - os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ + note: note.value, + isDeleted, + currentClip: currentClip?.value, + }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { @@ -557,13 +568,6 @@ function focusAfter() { focusNext(rootEl.value ?? null); } -function readPromo() { - misskeyApi('promo/read', { - noteId: appearNote.value.id, - }); - isDeleted.value = true; -} - function emitUpdReaction(emoji: string, delta: number) { if (delta < 0) { emit('removeReaction', emoji); @@ -581,6 +585,20 @@ function emitUpdReaction(emoji: string, delta: number) { overflow: clip; contain: content; + &.home { + background-color: rgba(var(--homeColor), 0.20) !important; + } + + &.followers { + background-color: rgba(var(--followerColor), 0.20) !important; + } + + &.specified { + background-color: rgba(var(--specifiedColor), 0.20) !important; + } + &.localonly { + background-color: rgba(var(--localOnlyColor), 0.20) !important; + } // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、 @@ -1024,4 +1042,8 @@ function emitUpdReaction(emoji: string, delta: number) { opacity: .8; font-size: 95%; } + +.root:has(.ti-home) { + background-color: rgba(255, 255, 100, 0.10) !important; +} </style> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index a7fb993a93..0263a38761 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -76,7 +76,6 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" - :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" @@ -87,7 +86,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <MkLoading v-if="translating" mini/> <div v-else-if="translation"> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> <div v-if="appearNote.files && appearNote.files.length > 0"> @@ -103,6 +102,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </div> <footer> <div :class="$style.noteFooterInfo"> + <div v-if="appearNote.updatedAt"> + {{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/> + </div> <MkA :to="notePage(appearNote)"> <MkTime :time="appearNote.createdAt" mode="detail" colored/> </MkA> @@ -126,8 +128,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <i class="ti ti-ban"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReactions?.length >= 4 " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-else-if="appearNote.myReactions?.length >= 4 || appearNote.myReaction && appearNote.user.host " class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ti ti-plus"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -141,9 +143,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </footer> </article> <div :class="$style.tabs"> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' },{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' && tab === 'replies'}]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes'},{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' && tab === 'renotes'}]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions'},{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' && tab === 'reactions'}]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'history' },{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'}]" @click="tab = 'history'"><i class="ti ti-pencil"></i> {{ i18n.ts.edited }}</button> </div> <div> <div v-if="tab === 'replies'"> @@ -180,25 +183,46 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> </MkPagination> </div> + <div v-else-if="tab === 'history'" :class="$style.tab_history"> + <div style="display: grid;"> + <div v-for="(text, index) in appearNote.noteEditHistory" :key="text" :class="$style.historyRoot"> + <MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/> + <div :class="$style.historyMain"> + <div :class="$style.historyHeader"> + <MkUserName :user="appearNote.user" :nowrap="true"/> + <MkTime :class="$style.updatedAt" :time="appearNote.updatedAtHistory![index]"/> + </div> + <div> + <div> + <Mfm :text="text.trim()" :author="appearNote.user" :i="$i"/> + </div> + <CodeDiff + :oldString="appearNote.noteEditHistory[index - 1] || ''" + :newString="text" + :trim="true" + :hideHeader="true" + diffStyle="char" + /> + </div> + </div> + </div> + <div v-if="appearNote.noteEditHistory == null" class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> + </div> </div> </div> -<div v-else class="_panel" :class="$style.muted" @click="muted = false"> - <I18n :src="i18n.ts.userSaysSomething" tag="small"> - <template #name> - <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> - <MkUserName :user="appearNote.user"/> - </MkA> - </template> - </I18n> -</div> </template> <script lang="ts" setup> -import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue'; +import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import MkNotePreview from '@/components/MkNotePreview.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue'; import MkMediaList from '@/components/MkMediaList.vue'; @@ -231,7 +255,12 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; -import { isEnabledUrlPreview } from '@/instance.js'; +import { miLocalStorage } from '@/local-storage.js'; +import { infoImageUrl, instance, isEnabledUrlPreview } from '@/instance.js'; +import MkPostForm from '@/components/MkPostFormSimple.vue'; +import { deviceKind } from '@/scripts/device-kind.js'; + +const MOBILE_THRESHOLD = 500; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -244,6 +273,37 @@ const inChannel = inject('inChannel', null); const note = ref(deepClone(props.note)); +let gaming = ref(''); + +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; +} else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; +} else { + gaming.value = ''; +} + +watch(darkMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +watch(gamingMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { @@ -441,10 +501,8 @@ function undoReact(targetNote: Misskey.entities.Note): void { } function toggleReact() { - if (appearNote.value.myReaction == null) { + if (appearNote.value.myReactions?.length < 4 || appearNote.value.myReaction && appearNote.value.user.host || !appearNote.value.myReactions ) { react(); - } else { - undoReact(appearNote.value); } } @@ -748,6 +806,9 @@ function loadConversation() { .tabActive { border-bottom: solid 2px var(--accent); + &.gamingLight{ + border-bottom: solid 2px black; + } } .tab_renotes { @@ -758,6 +819,9 @@ function loadConversation() { padding: 16px; } +.tab_history { + padding: 16px; +} .reactionTabs { display: flex; gap: 8px; @@ -773,6 +837,12 @@ function loadConversation() { .reactionTabActive { border-color: var(--accent); + &.gamingLight{ + border-bottom: solid 2px black; + } + &.gamingDark{ + border-bottom: solid 2px black; + } } @container (max-width: 500px) { @@ -813,12 +883,44 @@ function loadConversation() { width: 50px; height: 50px; } + .noteFooterButton { + &:not(:last-child) { + margin-right: 12px; + } + } +} +.historyRoot { + display: flex; + margin: 0; + padding: 10px; + overflow: clip; + font-size: 0.95em; +} - .noteFooterButton { - &:not(:last-child) { - margin-right: 12px; - } - } +.historyMain { + flex: 1; + min-width: 0; +} + +.historyHeader { + display: flex; + margin-bottom: 2px; + font-weight: bold; + width: 100%; + overflow: clip; + text-overflow: ellipsis; +} + +.historyNote { + padding-top: 10px; + min-height: 75px; + overflow: auto; +} + +.updatedAt { + flex-shrink: 0; + margin-left: auto; + font-size: 0.9em; } .muted { @@ -826,4 +928,37 @@ function loadConversation() { text-align: center; opacity: 0.7; } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 1966b880c6..4252b76bc2 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -4,10 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <header :class="$style.root"> - <div v-if="mock" :class="$style.name"> - <MkUserName :user="note.user"/> - </div> - <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> </MkA> <div v-if="note.user.isBot" :class="$style.isBot">bot</div> @@ -16,12 +13,14 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> </div> <div :class="$style.info"> - <div v-if="mock"> - <MkTime :time="note.createdAt" colored/> - </div> + <span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span> + <div v-if="mock"> + <MkTime :time="note.createdAt" colored/> + </div> + <MkTime v-else-if="note.isSchedule" mode="absolute" :time="note.createdAt" colored/> <MkA v-else :to="notePage(note)"> - <MkTime :time="note.createdAt" colored/> - </MkA> + <MkTime :time="note.createdAt" colored/> + </MkA> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> @@ -34,17 +33,16 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { inject } from 'vue'; +import {inject} from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; - -defineProps<{ - note: Misskey.entities.Note; -}>(); - const mock = inject<boolean>('mock', false); +defineProps<{ + note: Misskey.entities.Note & {isSchedule? : boolean}; + scheduled?: boolean; +}>(); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index f4168d45b5..7c7852a560 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -3,17 +3,21 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<div :class="$style.root"> +<div v-show="!isDeleted" :class="$style.root" :tabindex="!isDeleted ? '-1' : undefined"> <MkAvatar :class="$style.avatar" :user="note.user" link preview/> <div :class="$style.main"> <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> + <Mfm v-if="note.cw != ''" :emojireq="emojireq" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/> </p> <div v-show="note.cw == null || showContent"> - <MkSubNoteContent :class="$style.text" :note="note"/> + <MkSubNoteContent :emojireq="emojireq" :class="$style.text" :note="note"/> + </div> + <div v-if="note.isSchedule" style="margin-top: 10px;"> + <MkButton :class="$style.button" inline @click="editScheduleNote()"><i class="ti ti-pencil"></i> {{ i18n.ts.deleteAndEdit }}</MkButton> + <MkButton :class="$style.button" inline danger @click="deleteScheduleNote()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> </div> @@ -23,14 +27,62 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { i18n } from '../i18n.js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; - +import MkButton from '@/components/MkButton.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +const isDeleted = ref(false); const props = defineProps<{ - note: Misskey.entities.Note; + note: Misskey.entities.Note & { + id: string | null; + isSchedule?: boolean; + scheduledNoteId?: string; + }; + emojireq:boolean; }>(); +const emit = defineEmits<{ + (ev: 'editScheduleNote'): void; +}>(); + +async function deleteScheduleNote() { + if (!props.note.isSchedule || !props.note.scheduledNoteId) return; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts._schedulePost.deleteAreYouSure, + }); + if (canceled) return; + + await os.apiWithDialog('notes/schedule/delete', { scheduledNoteId: props.note.scheduledNoteId }) + .then(() => { + isDeleted.value = true; + }); +} + +async function editScheduleNote() { + if (!props.note.isSchedule || !props.note.scheduledNoteId) return; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts._schedulePost.deleteAndEditConfirm, + }); + + if (canceled) return; + + await misskeyApi('notes/schedule/delete', { scheduledNoteId: props.note.scheduledNoteId }) + .then(() => { + isDeleted.value = true; + }); + + await os.post({ initialNote: props.note, renote: props.note.renote, reply: props.note.reply, channel: props.note.channel }); + + emit('editScheduleNote'); +} + const showContent = ref(false); </script> @@ -40,8 +92,12 @@ const showContent = ref(false); margin: 0; padding: 0; font-size: 0.95em; -} +} +.button{ + margin-right: var(--margin); + margin-bottom: var(--margin); +} .avatar { flex-shrink: 0; display: block; diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index f22e9db331..33eeafb1a2 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -27,15 +27,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA> </div> </div> -<div v-else :class="$style.muted" @click="muted = false"> - <I18n :src="i18n.ts.userSaysSomething" tag="small"> - <template #name> - <MkA v-user-preview="note.userId" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - </template> - </I18n> -</div> + <div v-else /> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 11a2e97689..dcbe36014c 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -23,7 +23,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :ad="true" :class="$style.notes" > - <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> + <MkNote v-if="props.withCw && !note.cw || !props.withCw" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> </MkDateSeparatedList> </div> </template> @@ -42,8 +42,8 @@ const props = defineProps<{ pagination: Paging; noGap?: boolean; disableAutoLoad?: boolean; + withCw?: boolean; }>(); - const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); defineExpose({ diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index ffdf60fbfa..6508ab13ac 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <div :class="$style.head"> <MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> - <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> + <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'loginbonus'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> @@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_quote]: notification.type === 'quote', [$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_achievementEarned]: notification.type === 'achievementEarned', + [$style.t_achievementEarned]: notification.type === 'loginbonus', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, }]" > @@ -37,6 +38,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> + <i v-else-if="notification.type === 'loginbonus'" class="ti ti-medal"></i> + <template v-else-if="notification.type === 'roleAssigned'"> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <i v-else class="ti ti-badges"></i> @@ -56,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> + <span v-else-if="notification.type === 'loginbonus'">{{ i18n.ts._notification.loginbonus }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> @@ -94,10 +98,13 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> {{ notification.role.name }} + </div> <div v-else-if="notification.type === 'loginbonus'" :class="$style.text"> + {{ notification.loginbonus }}プリズム入手しました! </div> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> + <template v-else-if="notification.type === 'follow'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> </template> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 17c03132b5..51bb645e1e 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -94,13 +94,6 @@ onUnmounted(() => { if (connection) connection.dispose(); }); -onDeactivated(() => { - if (connection) connection.dispose(); -}); - -defineExpose({ - reload, -}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkNotifyButton.vue b/packages/frontend/src/components/MkNotifyButton.vue new file mode 100644 index 0000000000..d6d3ad0c83 --- /dev/null +++ b/packages/frontend/src/components/MkNotifyButton.vue @@ -0,0 +1,366 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> +<template> +<button + v-if="isFollowing" + class="_button" :class="[$style.root,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' + ,}]" + @click="onClick" +> + <span v-if="props.user.notify === 'none'" :class="[{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }] "><i class="ti ti-bell"></i></span> + <span v-else-if="props.user.notify === 'normal'" :class="[{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]"><i class="ti ti-bell-off"></i></span> +</button> +</template> + +<script lang="ts" setup> +import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import * as os from '@/os.js'; +import { useStream } from '@/stream.js'; +import { defaultStore } from '@/store.js'; + +let gaming = ref(''); + +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; +} else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; +} else { + gaming.value = ''; +} + +watch(darkMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +watch(gamingMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +const props = withDefaults(defineProps<{ + user: Misskey.entities.UserDetailed, + full?: boolean, + large?: boolean, +}>(), { + full: false, + large: false, +}); + +let isFollowing = ref(props.user.isFollowing); +let notify = ref(props.user.notify); +const connection = useStream().useChannel('main'); + +if (props.user.isFollowing == null) { + misskeyApi('users/show', { + userId: props.user.id, + }).then(onFollowChange); +} + +if (props.user.notify == null) { + misskeyApi('users/show', { + userId: props.user.id, + }).then(onNotifyChange); +} + +function onFollowChange(user: Misskey.entities.UserDetailed) { + if (user.id === props.user.id) { + isFollowing.value = user.isFollowing; + } +} + +function onNotifyChange(user: Misskey.entities.UserDetailed) { + if (user.id === props.user.id) { + notify.value = user.notify; + console.log(props.user.notify); + } +} + +async function onClick() { + try { + await os.apiWithDialog('following/update', { + userId: props.user.id, + notify: props.user.notify === 'normal' ? 'none' : 'normal', + }).then(() => { + props.user.notify = props.user.notify === 'normal' ? 'none' : 'normal'; + }); + } finally { + + } +} + +onMounted(() => { + connection.on('follow', onFollowChange); + connection.on('unfollow', onFollowChange); +}); +onBeforeUnmount(() => { + connection.dispose(); +}); +</script> + +<style lang="scss" module> +.root { + position: relative; + display: inline-block; + font-weight: bold; + color: var(--fgOnWhite); + border: solid 1px var(--accent); + padding: 0; + height: 31px; + font-size: 16px; + border-radius: 32px; + background: #fff; + vertical-align: bottom; + + &.gamingDark { + color: black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border: solid 1px black; + } + + &.gamingLight { + color: white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border: solid 1px white; + } + + &.full { + padding: 0 8px 0 12px; + font-size: 14px; + &.gamingDark { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + + } + + &.gamingLight { + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + &.large { + font-size: 16px; + height: 38px; + padding: 0 12px 0 16px; + } + + &:not(.full) { + width: 31px; + } + + &:focus-visible { + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: -5px; + right: -5px; + bottom: -5px; + left: -5px; + border: 2px solid var(--focus); + border-radius: 32px; + } + } + + &:hover { + //background: mix($primary, #fff, 20); + } + + &:active { + //background: mix($primary, #fff, 40); + } + + &.active { + color: var(--fgOnAccent); + background: var(--accent); + + &:hover { + background: var(--accentLighten); + border-color: var(--accentLighten); + } + + &:active { + background: var(--accentDarken); + border-color: var(--accentDarken); + } + + &.gamingDark:hover { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border: solid 1px white; + } + + &.gamingDark:active { + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + border: solid 1px white; + } + + &.gamingLight:hover { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border: solid 1px white; + } + + &.gamingLight:active { + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + border: solid 1px white; + } + + &.gamingDark { + -webkit-text-fill-color: unset !important; + color: black; + border: solid 1px white; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingLight { + -webkit-text-fill-color: unset !important; + color: white; + border: solid 1px white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + + } + + } + +} + + .gamingDark { + color: black; + + } + + .gamingLight { + color: white; + + } + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +</style> diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 6e94f1bbe6..3d7caf3643 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -212,7 +212,6 @@ async function init(): Promise<void> { const item = res[i]; if (i === 3) item._shouldInsertAd_ = true; } - if (res.length === 0 || props.pagination.noPaging) { concatItems(res); more.value = false; @@ -221,7 +220,6 @@ async function init(): Promise<void> { concatItems(res); more.value = true; } - offset.value = res.length; error.value = false; fetching.value = false; @@ -235,69 +233,43 @@ const reload = (): Promise<void> => { return init(); }; -const fetchMore = async (): Promise<void> => { +async function fetchMore(): Promise<void> { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; + moreFetching.value = true; - const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT, - ...(props.pagination.offsetMode ? { - offset: offset.value, - } : { - untilId: Array.from(items.value.keys()).at(-1), - }), - }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 10) item._shouldInsertAd_ = true; - } + try { + const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + const response = await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { + ...params, + limit: SECOND_FETCH_LIMIT, + ...(props.pagination.offsetMode ? { offset: offset.value } : { untilId: Array.from(items.value.keys()).pop() }), + }); - const reverseConcat = _res => { - const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight(); - const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY; + const isReversed = props.pagination.reversed; + if (isReversed) { + const oldHeight = scrollableElement.value?.scrollHeight || 0; + const oldScroll = scrollableElement.value?.scrollTop || 0; - items.value = concatMapWithArray(items.value, _res); + items.value = concatMapWithArray(items.value, response); - return nextTick(() => { - if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); - } else { - window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); - } - - return nextTick(); - }); - }; - - if (res.length === 0) { - if (props.pagination.reversed) { - reverseConcat(res).then(() => { - more.value = false; - moreFetching.value = false; + await nextTick(); + if (scrollableElement.value) { + scroll(scrollableElement.value, { + top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), + behavior: 'instant', }); - } else { - items.value = concatMapWithArray(items.value, res); - more.value = false; - moreFetching.value = false; } } else { - if (props.pagination.reversed) { - reverseConcat(res).then(() => { - more.value = true; - moreFetching.value = false; - }); - } else { - items.value = concatMapWithArray(items.value, res); - more.value = true; - moreFetching.value = false; - } + items.value = concatMapWithArray(items.value, response); } - offset.value += res.length; - }, err => { + + more.value = response.length > 0; + } catch (error) { + console.error(error); + } finally { moreFetching.value = false; - }); -}; + } +} const fetchMoreAhead = async (): Promise<void> => { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index 3e4946da88..bdc65874dc 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -19,7 +19,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <MkButton v-if="choices.length < 10" class="add" @click="add">{{ i18n.ts.add }}</MkButton> <MkButton v-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton> <MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch> - <section> + <section style="margin-bottom: 8px; border-top: solid 1.5px var(--divider);"> <div> <MkSelect v-model="expiration" small> <template #label>{{ i18n.ts._poll.expiration }}</template> @@ -146,8 +146,10 @@ watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit(' <style lang="scss" scoped> .zmdxowus { - padding: 8px 16px; - + margin: 4px 8px; + padding: 4px 8px; + border-radius: 8px; + border: solid 1.5px var(--divider); > .caution { margin: 0 0 8px 0; font-size: 0.8em; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 06795cf962..45bfafe029 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -31,21 +31,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <span :class="$style.headerRightButtonText">{{ channel.name }}</span> </button> </template> + <button v-click-anime v-tooltip="i18n.ts.drafts" class="_button" :class="$style.headerRightItem" @click="chooseDraft"><i class="ti ti-note"></i></button> <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span> </button> - <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> - <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> - <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> - <span v-else><i class="ti ti-icons"></i></span> - </button> + <button v-tooltip="i18n.ts.otherSettings" class="_button" :class="[$style.headerRightItem]" @click="openOtherSettingsMenu"><i class="ti ti-dots"></i></button> <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> - <div :class="$style.submitInner"> + <div :class="[$style.submitInner ,{ [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]"> <template v-if="posted"></template> <template v-else-if="posting"><MkEllipsis/></template> - <template v-else>{{ submitText }}</template> - <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i> + <template v-else-if="screenWidth >= 355">{{ submitText }}</template> + <i :class="[posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : schedule ? 'ti ti-clock-hour-4' : 'ti ti-send',$style.mgnlft]"></i> </div> </button> </div> @@ -72,7 +69,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> - <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> + <div :class="$style.postOptionsRoot"> + <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> + <MkScheduleEditor v-if="schedule" v-model="schedule" @destroyed="schedule = null"/> + </div> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/> <div v-if="showingOptions" style="padding: 8px 16px;"> </div> @@ -86,6 +86,8 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button> + <button v-tooltip="i18n.ts.ruby" :class="['_button', $style.footerButton]" @click="insertRuby"><i class="ti ti-abc"></i></button> + <button v-tooltip="i18n.ts.saveAsDraft" class="_button" :class="$style.footerButton" @click="saveDraft(false)"><i class="ti ti-device-floppy"></i></button> </div> <div :class="$style.footerRight"> <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> @@ -114,9 +116,19 @@ import { extractMentions } from '@/scripts/extract-mentions.js'; import { formatTimeString } from '@/scripts/format-time-string.js'; import { Autocomplete } from '@/scripts/autocomplete.js'; import * as os from '@/os.js'; +import * as noteDrafts from '@/scripts/note-drafts.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFiles } from '@/scripts/select-file.js'; -import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js'; +import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { + bannerDark, + bannerLight, + defaultStore, + iconDark, + iconLight, + notePostInterruptors, + postFormActions, +} from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; @@ -128,10 +140,13 @@ import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +import MkScheduleEditor from '@/components/MkScheduleEditor.vue'; +import { listSchedulePost } from '@/os.js'; const $i = signinRequired(); const modal = inject('modal'); +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = withDefaults(defineProps<{ reply?: Misskey.entities.Note; @@ -151,6 +166,7 @@ const props = withDefaults(defineProps<{ autofocus?: boolean; freezeAfterPosted?: boolean; mock?: boolean; + updateMode?: boolean; }>(), { initialVisibleUsers: () => [], autofocus: true, @@ -161,9 +177,9 @@ const props = withDefaults(defineProps<{ provide('mock', props.mock); const emit = defineEmits<{ - (ev: 'posted'): void; - (ev: 'cancel'): void; - (ev: 'esc'): void; + (ev: 'posted'): void; + (ev: 'cancel'): void; + (ev: 'esc'): void; // Mock用 (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; @@ -179,13 +195,19 @@ const posted = ref(false); const text = ref(props.initialText ?? ''); const files = ref(props.initialFiles ?? []); const poll = ref<PollEditorModelValue | null>(null); +let schedule = ref<{ + scheduledAt: string | null; +}| null>(null); const useCw = ref<boolean>(!!props.initialCw); +const renote = ref(props.renote); +const reply = ref(props.reply); const showPreview = ref(defaultStore.state.showPreview); watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction); watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value)); const cw = ref<string | null>(props.initialCw ?? null); const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly)); + const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility)); const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]); if (props.initialVisibleUsers) { @@ -201,24 +223,19 @@ const imeText = ref(''); const showingOptions = ref(false); const textAreaReadOnly = ref(false); -const draftKey = computed((): string => { - let key = props.channel ? `channel:${props.channel.id}` : ''; - - if (props.renote) { - key += `renote:${props.renote.id}`; - } else if (props.reply) { - key += `reply:${props.reply.id}`; - } else { - key += `note:${$i.id}`; - } - - return key; +const draftType = computed(() => { + if (props.channel) return 'channel'; + if (renote.value) return 'quote'; + if (reply.value) return 'reply'; + return 'note'; }); +const draftAuxId = computed<string | null>(() => props.channel ? props.channel.id : renote.value ? renote.value.id : reply.value ? reply.value.id : null); + const placeholder = computed((): string => { - if (props.renote) { + if (renote.value) { return i18n.ts._postForm.quotePlaceholder; - } else if (props.reply) { + } else if (reply.value) { return i18n.ts._postForm.replyPlaceholder; } else if (props.channel) { return i18n.ts._postForm.channelPlaceholder; @@ -236,11 +253,13 @@ const placeholder = computed((): string => { }); const submitText = computed((): string => { - return props.renote + return renote.value ? i18n.ts.quote - : props.reply + : reply.value ? i18n.ts.reply - : i18n.ts.note; + : schedule.value + ? i18n.ts._schedulePost.addSchedule + : i18n.ts.note; }); const textLength = computed((): number => { @@ -253,13 +272,7 @@ const maxTextLength = computed((): number => { const canPost = computed((): boolean => { return !props.mock && !posting.value && !posted.value && - ( - 1 <= textLength.value || - 1 <= files.value.length || - poll.value != null || - props.renote != null || - (props.reply != null && quoteId.value != null) - ) && + (1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!renote.value) && (textLength.value <= maxTextLength.value) && (!poll.value || poll.value.choices.length >= 2); }); @@ -267,11 +280,103 @@ const canPost = computed((): boolean => { const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags')); const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags')); +const bottomItemActionDef = ref({ + attachFile: { + action: chooseFileFrom, + }, + poll: { + active: poll, + action: togglePoll, + }, + useCw: { + active: useCw, + action: () => useCw.value = !useCw.value, + }, + mention: { + action: insertMention, + }, + hashtags: { + active: withHashtags, + action: () => withHashtags.value = !withHashtags.value, + }, + plugins: { + hide: postFormActions.length === 0, + action: showActions, + }, + emoji: { + action: insertEmoji, + }, + addMfmFunction: { + hide: computed(() => !showAddMfmFunction.value), + action: insertMfmFunction, + }, + clearPost: { + action: clear, + }, + saveAsDraft: { + action: () => saveDraft(false), + }, +}); + watch(text, () => { checkMissingMention(); }, { immediate: true }); +const bottomItemDef = { + attachFile: { + title: i18n.ts.attachFile, + icon: 'ti-photo-plus', + }, + poll: { + title: i18n.ts.poll, + icon: 'ti-chart-arrows', + }, + useCw: { + title: i18n.ts.useCw, + icon: 'ti-eye-off', + }, + mention: { + title: i18n.ts.mention, + icon: 'ti-at', + }, + hashtags: { + title: i18n.ts.hashtags, + icon: 'ti-hash', + }, + plugins: { + title: i18n.ts.plugins, + icon: 'ti-plug', + }, + emoji: { + title: i18n.ts.emoji, + icon: 'ti-mood-happy', + }, + addMfmFunction: { + title: i18n.ts.addMfmFunction, + icon: 'ti-palette', + }, + clearPost: { + title: i18n.ts.clearPost, + icon: 'ti-trash', + }, + saveAsDraft: { + title: i18n.ts.saveAsDraft, + icon: 'ti-note', + }, +}; watch(visibility, () => { + switch (visibility.value) { + case 'public': + localOnly.value = props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; + break; + case 'home': + localOnly.value = props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultHomeNoteLocalOnly; + break; + case 'followers': + localOnly.value = props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultFollowersNoteLocalOnly; + break; + } + checkMissingMention(); }, { immediate: true }); @@ -281,82 +386,86 @@ watch(visibleUsers, () => { deep: true, }); -if (props.mention) { - text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; - text.value += ' '; -} - -if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { - text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; -} - -if (props.reply && props.reply.text != null) { - const ast = mfm.parse(props.reply.text); - const otherHost = props.reply.user.host; - - for (const x of extractMentions(ast)) { - const mention = x.host ? - `@${x.username}@${toASCII(x.host)}` : - (otherHost == null || otherHost === host) ? - `@${x.username}` : - `@${x.username}@${toASCII(otherHost)}`; - - // 自分は除外 - if ($i.username === x.username && (x.host == null || x.host === host)) continue; - - // 重複は除外 - if (text.value.includes(`${mention} `)) continue; - - text.value += `${mention} `; +function initialize() { + if (props.mention) { + text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; + text.value += ' '; } -} -if ($i.isSilenced && visibility.value === 'public') { - visibility.value = 'home'; -} + if (reply.value && (reply.value.user.username !== $i.username || (reply.value.user.host != null && reply.value.user.host !== host))) { + text.value = `@${reply.value.user.username}${reply.value.user.host != null ? '@' + toASCII(reply.value.user.host) : ''} `; + } -if (props.channel) { - visibility.value = 'public'; - localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す -} + if (reply.value && reply.value.text != null) { + const ast = mfm.parse(reply.value.text); + const otherHost = reply.value.user.host; -// 公開以外へのリプライ時は元の公開範囲を引き継ぐ -if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { - if (props.reply.visibility === 'home' && visibility.value === 'followers') { - visibility.value = 'followers'; - } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') { + for (const x of extractMentions(ast)) { + const mention = x.host ? + `@${x.username}@${toASCII(x.host)}` : + (otherHost == null || otherHost === host) ? + `@${x.username}` : + `@${x.username}@${toASCII(otherHost)}`; + + // 自分は除外 + if ($i.username === x.username && (x.host == null || x.host === host)) continue; + + // 重複は除外 + if (text.value.includes(`${mention} `)) continue; + + text.value += `${mention} `; + } + } + + if ($i.isSilenced && visibility.value === 'public') { + visibility.value = 'home'; + } + + if (props.channel) { + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す + } + + // 公開以外へのリプライ時は元の公開範囲を引き継ぐ + if (reply.value && ['home', 'followers', 'specified'].includes(reply.value.visibility)) { + if (reply.value.visibility === 'home' && visibility.value === 'followers') { + visibility.value = 'followers'; + } else if (['home', 'followers'].includes(reply.value.visibility) && visibility.value === 'specified') { + visibility.value = 'specified'; + } else { + visibility.value = reply.value.visibility; + } + + if (visibility.value === 'specified') { + if (reply.value.visibleUserIds) { + misskeyApi('users/show', { + userIds: reply.value.visibleUserIds.filter(uid => uid !== $i.id && uid !== reply.value?.userId), + }).then(users => { + users.forEach(u => pushVisibleUser(u)); + }); + } + + if (reply.value.userId !== $i.id) { + misskeyApi('users/show', { userId: reply.value.userId }).then(user => { + pushVisibleUser(user); + }); + } + } + } + + if (props.specified) { visibility.value = 'specified'; - } else { - visibility.value = props.reply.visibility; + pushVisibleUser(props.specified); } - if (visibility.value === 'specified') { - if (props.reply.visibleUserIds) { - misskeyApi('users/show', { - userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId), - }).then(users => { - users.forEach(u => pushVisibleUser(u)); - }); - } - - if (props.reply.userId !== $i.id) { - misskeyApi('users/show', { userId: props.reply.userId }).then(user => { - pushVisibleUser(user); - }); - } + // keep cw when reply + if (defaultStore.state.keepCw && reply.value && reply.value.cw) { + useCw.value = true; + cw.value = reply.value.cw; } } -if (props.specified) { - visibility.value = 'specified'; - pushVisibleUser(props.specified); -} - -// keep cw when reply -if (defaultStore.state.keepCw && props.reply && props.reply.cw) { - useCw.value = true; - cw.value = props.reply.cw; -} +initialize(); function watchForDraft() { watch(text, () => saveDraft()); @@ -394,6 +503,10 @@ function addMissingMention() { } } +function insertRuby() { + insertTextAtCursor(textareaEl.value, '$[ruby 本文 上につくやつ]'); +} + function togglePoll() { if (poll.value) { poll.value = null; @@ -407,6 +520,16 @@ function togglePoll() { } } +function toggleSchedule() { + if (schedule.value) { + schedule.value = null; + } else { + schedule.value = { + scheduledAt: null, + }; + } +} + function addTag(tag: string) { insertTextAtCursor(textareaEl.value, ` #${tag} `); } @@ -419,9 +542,7 @@ function focus() { } function chooseFileFrom(ev) { - if (props.mock) return; - - selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { + if (props.mock) return; selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { for (const file of files_) { files.value.push(file); } @@ -448,9 +569,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities } function upload(file: File, name?: string): void { - if (props.mock) return; - - uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { + if (props.mock) return; uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.value.push(res); }); } @@ -464,10 +583,9 @@ function setVisibility() { os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { currentVisibility: visibility.value, - isSilenced: $i.isSilenced, - localOnly: localOnly.value, + isSilenced: $i.isSilenced, localOnly: localOnly.value, src: visibilityButton.value, - ...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}), + ...(reply.value ? { isReplyVisibilitySpecified: reply.value.visibility === 'specified' } : {}), }, { changeVisibility: v => { visibility.value = v; @@ -561,7 +679,7 @@ function removeVisibleUser(user) { function clear() { text.value = ''; - files.value = []; + schedule.value = null; files.value = []; poll.value = null; quoteId.value = null; } @@ -580,13 +698,11 @@ function onCompositionEnd(ev: CompositionEvent) { } async function onPaste(ev: ClipboardEvent) { - if (props.mock) return; - if (!ev.clipboardData) return; + if (props.mock) return; if (!ev.clipboardData) return; for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) { if (item.kind === 'file') { - const file = item.getAsFile(); - if (!file) continue; + const file = item.getAsFile(); if (!file) continue; const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; @@ -596,7 +712,7 @@ async function onPaste(ev: ClipboardEvent) { const paste = ev.clipboardData.getData('text'); - if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) { + if (!renote.value && !quoteId.value && paste.startsWith(url + '/notes/')) { ev.preventDefault(); os.confirm({ @@ -684,34 +800,87 @@ function onDrop(ev: DragEvent): void { //#endregion } -function saveDraft() { +async function saveDraft(auto = true) { if (props.instant || props.mock) return; - const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); + if (auto && defaultStore.state.draftSavingBehavior !== 'auto') return; - draftData[draftKey.value] = { - updatedAt: new Date(), - data: { - text: text.value, - useCw: useCw.value, - cw: cw.value, - visibility: visibility.value, - localOnly: localOnly.value, - files: files.value, - poll: poll.value, - visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined, - }, - }; + if (!auto) { + // 手動での保存の場合は自動保存したものを削除した上で保存 + await noteDrafts.remove(draftType.value, $i.id, 'default', draftAuxId.value as string); + } - miLocalStorage.setItem('drafts', JSON.stringify(draftData)); + await noteDrafts.set(draftType.value, $i.id, auto ? 'default' : Date.now().toString(), { + text: text.value, + useCw: useCw.value, + cw: cw.value, + visibility: visibility.value, + localOnly: localOnly.value, + files: files.value, + poll: poll.value, + visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined, + }, draftAuxId.value as string); + + if (!auto) { + clear(); + } } function deleteDraft() { - const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); + noteDrafts.remove(draftType.value, $i.id, 'default', draftAuxId.value as string); +} - delete draftData[draftKey.value]; +function chooseDraft() { + os.popup(defineAsyncComponent(() => import('@/components/MkPostFormDrafts.vue')), { + channelId: props.channel?.id, + }, { + selected: async (res) => { + const draft = await res as noteDrafts.NoteDraft; - miLocalStorage.setItem('drafts', JSON.stringify(draftData)); + if (text.value !== '' || files.value.length > 0) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.draftOverwriteConfirm, + }); + if (canceled) return; + } + + applyDraft(draft); + }, + }, 'closed'); +} + +async function applyDraft(draft: noteDrafts.NoteDraft, native = false) { + if (!native) { + switch (draft.type) { + case 'quote': { + await os.apiWithDialog('notes/show', { noteId: draft.auxId as string }).then(note => { + renote.value = note; + reply.value = undefined; + }); + break; + } + case 'reply': { + await os.apiWithDialog('notes/show', { noteId: draft.auxId as string }).then(note => { + reply.value = note; + renote.value = undefined; + }); + break; + } + } + + initialize(); + } + + text.value = draft.data.text; + useCw.value = draft.data.useCw; + cw.value = draft.data.cw; + visibility.value = draft.data.visibility; + localOnly.value = draft.data.localOnly; + files.value = (draft.data.files || []).filter(draftFile => draftFile); + if (draft.data.poll) { + poll.value = draft.data.poll; + } } async function post(ev?: MouseEvent) { @@ -721,9 +890,7 @@ async function post(ev?: MouseEvent) { text: i18n.ts.cwNotationRequired, }); return; - } - - if (ev) { + } if (ev) { const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; if (el) { @@ -734,14 +901,12 @@ async function post(ev?: MouseEvent) { } } - if (props.mock) return; - - const annoying = - text.value.includes('$[x2') || - text.value.includes('$[x3') || - text.value.includes('$[x4') || - text.value.includes('$[scale') || - text.value.includes('$[position'); + if (props.mock) return; const annoying = + text.value.includes('$[x2') || + text.value.includes('$[x3') || + text.value.includes('$[x4') || + text.value.includes('$[scale') || + text.value.includes('$[position'); if (annoying && visibility.value === 'public') { const { canceled, result } = await os.actions({ @@ -770,15 +935,18 @@ async function post(ev?: MouseEvent) { let postData = { text: text.value === '' ? null : text.value, fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, - replyId: props.reply ? props.reply.id : undefined, - renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, + replyId: reply.value ? reply.value.id : undefined, + renoteId: renote.value ? renote.value.id : quoteId.value ? quoteId.value : undefined, channelId: props.channel ? props.channel.id : undefined, + schedule: schedule.value, poll: poll.value, cw: useCw.value ? cw.value ?? '' : null, localOnly: localOnly.value, visibility: visibility.value, visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, reactionAcceptance: reactionAcceptance.value, + + noteId: props.updateMode ? props.initialNote?.id : undefined, }; if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') { @@ -815,7 +983,7 @@ async function post(ev?: MouseEvent) { } posting.value = true; - misskeyApi('notes/create', postData, token).then(() => { + misskeyApi(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => { if (props.freezeAfterPosted) { posted.value = true; } else { @@ -837,6 +1005,12 @@ async function post(ev?: MouseEvent) { claimAchievement('notes1'); } + if (postData.schedule?.scheduledAt) { + const d = new Date(postData.schedule.scheduledAt); + const str = dateTimeFormat.format(d); + os.toast(i18n.t('_schedulePost.willBePostedAtX', { date: str })); + } + const text = postData.text ?? ''; const lowerCase = text.toLowerCase(); if ((lowerCase.includes('love') || lowerCase.includes('❤')) && lowerCase.includes('misskey')) { @@ -859,7 +1033,7 @@ async function post(ev?: MouseEvent) { claimAchievement('brainDiver'); } - if (props.renote && (props.renote.userId === $i.id) && text.length > 0) { + if (renote.value && (renote.value.userId === $i.id) && text.length > 0) { claimAchievement('selfQuote'); } @@ -873,6 +1047,7 @@ async function post(ev?: MouseEvent) { if (m === 0 && s === 0) { claimAchievement('postedAt0min0sec'); } + clear(); }); }).catch(err => { posting.value = false; @@ -887,6 +1062,18 @@ function cancel() { emit('cancel'); } +async function closed() { + if (defaultStore.state.draftSavingBehavior === 'manual' && (text.value !== '' || files.value.length > 0)) { + os.confirm({ + type: 'question', + text: i18n.ts.saveConfirm, + }).then(({ canceled }) => { + if (canceled) return; + saveDraft(false); + }); + } +} + function insertMention() { os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => { insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); @@ -937,9 +1124,7 @@ function showActions(ev: MouseEvent) { const postAccount = ref<Misskey.entities.UserDetailed | null>(null); function openAccountMenu(ev: MouseEvent) { - if (props.mock) return; - - openAccountMenu_({ + if (props.mock) return; openAccountMenu_({ withExtraOperation: false, includeCurrentAccount: true, active: postAccount.value != null ? postAccount.value.id : $i.id, @@ -953,7 +1138,51 @@ function openAccountMenu(ev: MouseEvent) { }, ev); } +function openOtherSettingsMenu(ev: MouseEvent) { + let reactionAcceptanceIcon: string; + switch (reactionAcceptance.value) { + case 'likeOnly': + reactionAcceptanceIcon = 'ti ti-heart'; + break; + case 'likeOnlyForRemote': + reactionAcceptanceIcon = 'ti ti-heart-plus'; + break; + default: + reactionAcceptanceIcon = 'ti ti-icons'; + break; + } + + os.popupMenu([{ + type: 'button', + text: i18n.ts.reactionAcceptance, + icon: reactionAcceptanceIcon, + action: toggleReactionAcceptance, + }, ($i.policies.canScheduleNote) ? { + type: 'button', + text: i18n.ts.schedulePost, + icon: 'ti ti-calendar-time', + indicate: (schedule.value != null), + action: toggleSchedule, + } : undefined, ...(($i.policies.canScheduleNote) ? [{ type: 'divider' }, { + type: 'button', + text: i18n.ts._schedulePost.list, + icon: 'ti ti-calendar-event', + action: () => { + // 投稿フォームが二重に出ないようにとじておく + emit('cancel'); + listSchedulePost(); + }, + }] : [])], ev.currentTarget ?? ev.target, { + align: 'right', + }); +} + +const screenWidth = ref(0); onMounted(() => { + screenWidth.value = window.innerWidth; + window.addEventListener('resize', () => { + screenWidth.value = window.innerWidth; + }); if (props.autofocus) { focus(); @@ -967,26 +1196,13 @@ onMounted(() => { if (cwInputEl.value) new Autocomplete(cwInputEl.value, cw); if (hashtagsInputEl.value) new Autocomplete(hashtagsInputEl.value, hashtags); - nextTick(() => { + nextTick(async () => { + await noteDrafts.migrate($i.id); + // 書きかけの投稿を復元 - if (!props.instant && !props.mention && !props.specified && !props.mock) { - const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value]; - if (draft) { - text.value = draft.data.text; - useCw.value = draft.data.useCw; - cw.value = draft.data.cw; - visibility.value = draft.data.visibility; - localOnly.value = draft.data.localOnly; - files.value = (draft.data.files || []).filter(draftFile => draftFile); - if (draft.data.poll) { - poll.value = draft.data.poll; - } - if (draft.data.visibleUserIds) { - misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => { - users.forEach(u => pushVisibleUser(u)); - }); - } - } + if (!props.instant && !props.mention && !props.specified && !props.mock && !defaultStore.state.disableNoteDrafting) { + const draft = await noteDrafts.get(draftType.value, $i.id, 'default', draftAuxId.value as string); + if (draft) applyDraft(draft, true); } // 削除して編集 @@ -996,6 +1212,11 @@ onMounted(() => { files.value = init.files ?? []; cw.value = init.cw ?? null; useCw.value = init.cw != null; + if (init.isSchedule) { + schedule.value = { + scheduledAt: init.createdAt, + }; + } if (init.poll) { poll.value = { choices: init.poll.choices.map(x => x.text), @@ -1015,89 +1236,90 @@ onMounted(() => { defineExpose({ clear, + closed, }); </script> <style lang="scss" module> .root { - position: relative; - container-type: inline-size; + position: relative; + container-type: inline-size; - &.modal { - width: 100%; - max-width: 520px; - } + &.modal { + width: 100%; + max-width: 520px; + } } //#region header .header { - z-index: 1000; - min-height: 50px; - display: flex; - flex-wrap: nowrap; - gap: 4px; + z-index: 1000; + min-height: 50px; + display: flex; + flex-wrap: nowrap; + gap: 4px; } .headerLeft { - display: flex; - flex: 0 1 100px; + display: flex; + flex: 0 1 100px; } .cancel { - padding: 0; - font-size: 1em; - height: 100%; - flex: 0 1 50px; + padding: 0; + font-size: 1em; + height: 100%; + flex: 0 1 50px; } .account { - height: 100%; - display: inline-flex; - vertical-align: bottom; - flex: 0 1 50px; + height: 100%; + display: inline-flex; + vertical-align: bottom; + flex: 0 1 50px; } .avatar { - width: 28px; - height: 28px; - margin: auto; + width: 28px; + height: 28px; + margin: auto; } .headerRight { - display: flex; - min-height: 48px; - font-size: 0.9em; - flex-wrap: nowrap; - align-items: center; - margin-left: auto; - gap: 4px; - overflow: clip; - padding-left: 4px; + display: flex; + min-height: 48px; + font-size: 0.9em; + flex-wrap: nowrap; + align-items: center; + margin-left: auto; + gap: 4px; + overflow: clip; + padding-left: 4px; } .submit { - margin: 12px 12px 12px 6px; - vertical-align: bottom; + margin: 12px 12px 12px 6px; + vertical-align: bottom; - &:disabled { - opacity: 0.7; - } + &:disabled { + opacity: 0.7; + } - &.posting { - cursor: wait; - } + &.posting { + cursor: wait; + } - &:not(:disabled):hover { - > .inner { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } - } + &:not(:disabled):hover { + > .inner { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } + } - &:not(:disabled):active { - > .inner { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } - } + &:not(:disabled):active { + > .inner { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } + } } .colorBar { @@ -1109,265 +1331,367 @@ defineExpose({ border-radius: 999px; pointer-events: none; } - +.submitInner i::after { + content: ""; +} +@media (width < 355px) { + .submitInner { + min-width: 20px !important; + } + .mgnlft{ + margin-left: 0 !important; + } +} +.mgnlft{ + margin-left: 6px; +} .submitInner { - padding: 0 12px; - line-height: 34px; - font-weight: bold; - border-radius: 6px; - min-width: 90px; - box-sizing: border-box; - color: var(--fgOnAccent); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + padding: 0 12px; + line-height: 34px; + font-weight: bold; + border-radius: 6px; + min-width: 90px; + box-sizing: border-box; + color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + } + &.gamingDark{ + color: white; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + } } .headerRightItem { - margin: 0; - padding: 8px; - border-radius: 6px; + margin: 0; + padding: 8px; + border-radius: 6px; - &:hover { - background: var(--X5); - } + &:hover { + background: var(--X5); + } - &:disabled { - background: none; - } + &:disabled { + background: none; + } + + &.headerRightButtonActive { + color: var(--accent); + } &.danger { - color: #ff2a2a; - } + color: #ff2a2a; + } } .headerRightButtonText { - padding-left: 6px; + padding-left: 6px; } .visibility { - overflow: clip; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 210px; + overflow: clip; + text-overflow: ellipsis; + white-space: nowrap;max-width: 210px; - &:enabled { - > .headerRightButtonText { - opacity: 0.8; - } - } + &:enabled { + > .headerRightButtonText { + opacity: 0.8; + } + } } //#endregion .preview { - padding: 16px 20px 0 20px; - min-height: 75px; - max-height: 150px; - overflow: auto; + padding: 16px 20px 0 20px; + min-height: 75px;max-height: 150px; + overflow: auto; } .targetNote { - padding: 0 20px 16px 20px; + padding: 0 20px 16px 20px; } .withQuote { - margin: 0 0 8px 0; - color: var(--accent); + margin: 0 0 8px 0; + color: var(--accent); } .toSpecified { - padding: 6px 24px; - margin-bottom: 8px; - overflow: auto; - white-space: nowrap; + padding: 6px 24px; + margin-bottom: 8px; + overflow: auto; + white-space: nowrap; } .visibleUsers { - display: inline; - top: -1px; - font-size: 14px; + display: inline; + top: -1px; + font-size: 14px; } .visibleUser { - margin-right: 14px; - padding: 8px 0 8px 8px; - border-radius: 8px; - background: var(--X4); + margin-right: 14px; + padding: 8px 0 8px 8px; + border-radius: 8px; + background: var(--X4); } .hasNotSpecifiedMentions { - margin: 0 20px 16px 20px; + margin: 0 20px 16px 20px; } .cw, .hashtags, .text { - display: block; - box-sizing: border-box; - padding: 0 24px; - margin: 0; - width: 100%; - font-size: 16px; - border: none; - border-radius: 0; - background: transparent; - color: var(--fg); - font-family: inherit; + display: block; + box-sizing: border-box; + padding: 0 24px; + margin: 0; + width: 100%; + font-size: 16px; + border: none; + border-radius: 0; + background: transparent; + color: var(--fg); + font-family: inherit; - &:focus { - outline: none; - } + &:focus { + outline: none; + } - &:disabled { - opacity: 0.5; - } + &:disabled { + opacity: 0.5; + } } .cw { - z-index: 1; - padding-bottom: 8px; - border-bottom: solid 0.5px var(--divider); + z-index: 1; + padding-bottom: 8px; + border-bottom: solid 1px var(--divider); +} + +.postOptionsRoot { + >* { + border-bottom: solid 1px var(--divider); + } + } .hashtags { - z-index: 1; - padding-top: 8px; - padding-bottom: 8px; - border-top: solid 0.5px var(--divider); + z-index: 1; + padding-top: 8px; + padding-bottom: 8px; + border-top: solid 1px var(--divider); } .textOuter { - width: 100%; - position: relative; + width: 100%; + position: relative; - &.withCw { - padding-top: 8px; - } + &.withCw { + padding-top: 8px; + } } .text { - max-width: 100%; - min-width: 100%; - width: 100%; - min-height: 90px; - height: 100%; + max-width: 100%; + min-width: 100%; + width: 100%; + min-height: 90px; + height: 100%; } .textCount { - position: absolute; - top: 0; - right: 2px; - padding: 4px 6px; - font-size: .9em; - color: var(--warn); - border-radius: 6px; - min-width: 1.6em; - text-align: center; + position: absolute; + top: 0; + right: 2px; + padding: 4px 6px; + font-size: .9em; + color: var(--warn); + border-radius: 6px; + min-width: 1.6em; + text-align: center; - &.textOver { - color: #ff2a2a; - } + &.textOver { + color: #ff2a2a; + } } .footer { - display: flex; - padding: 0 16px 16px 16px; - font-size: 1em; + display: flex; + padding: 0 16px 16px 16px; + font-size: 1em; } .footerLeft { - flex: 1; - display: grid; - grid-auto-flow: row; - grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); - grid-auto-rows: 40px; + flex: 1; + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); + grid-auto-rows: 40px; } .footerRight { - flex: 0; - margin-left: auto; - display: grid; - grid-auto-flow: row; - grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); - grid-auto-rows: 40px; - direction: rtl; + flex: 0; + margin-left: auto; + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); + grid-auto-rows: 40px; + direction: rtl; } .footerButton { - display: inline-block; - padding: 0; - margin: 0; - font-size: 1em; - width: auto; - height: 100%; - border-radius: 6px; + display: inline-block; + padding: 0; + margin: 0; + font-size: 1em; + width: auto; + height: 100%; + border-radius: 6px; - &:hover { - background: var(--X5); - } + &:hover { + background: var(--X5); + } - &.footerButtonActive { - color: var(--accent); - } + &.footerButtonActive { + color: var(--accent); + } } .previewButtonActive { - color: var(--accent); + color: var(--accent); } @container (max-width: 500px) { - .headerRight { - font-size: .9em; - } + .headerRight { + font-size: .9em; + } - .headerRightButtonText { - display: none; - } + .headerRightButtonText { + display: none; + } - .visibility { - overflow: initial; - } + .visibility { + overflow: initial; + } - .submit { - margin: 8px 8px 8px 4px; - } + .submit { + margin: 8px 8px 8px 4px; + } - .toSpecified { - padding: 6px 16px; - } + .toSpecified { + padding: 6px 16px; + } - .preview { - padding: 16px 14px 0 14px; - } - .cw, - .hashtags, - .text { - padding: 0 16px; - } + .preview { + padding: 16px 14px 0 14px; + } + .cw, + .hashtags, + .text { + padding: 0 16px; + } - .text { - min-height: 80px; - } + .text { + min-height: 80px; + } - .footer { - padding: 0 8px 8px 8px; - } + .footer { + padding: 0 8px 8px 8px; + } } @container (max-width: 350px) { - .footer { - font-size: 0.9em; - } + .footer { + font-size: 0.9em; + } - .footerLeft { - grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); - } + .footerLeft { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + } - .footerRight { - grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); - } - - .headerRight { - gap: 0; - } + .footerRight { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + } + .headerRight { + gap: 0; + } +} +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index dbf5b71098..aa43cc68a1 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -31,6 +31,7 @@ const props = withDefaults(defineProps<{ instant?: boolean; fixed?: boolean; autofocus?: boolean; + updateMode?: boolean; }>(), { initialLocalOnly: undefined, }); @@ -49,6 +50,7 @@ function onPosted() { } function onModalClosed() { + form.value?.closed(); emit('closed'); } </script> diff --git a/packages/frontend/src/components/MkPostFormDrafts.vue b/packages/frontend/src/components/MkPostFormDrafts.vue new file mode 100644 index 0000000000..27bcbe1324 --- /dev/null +++ b/packages/frontend/src/components/MkPostFormDrafts.vue @@ -0,0 +1,155 @@ +<template> +<MkModalWindow + ref="dialog" + :width="500" + :height="600" + @close="dialog?.close()" + @closed="$emit('closed')" +> + <template #header>{{ i18n.ts.drafts }}</template> + + <div :class="$style.container"> + <div v-if="notes === null" :class="$style.center">{{ i18n.ts.loading }}</div> + <div v-else-if="Object.keys(notes).length === 0" :class="$style.center">{{ i18n.ts.nothing }}</div> + <div v-for="(note, key) of notes" v-else :key="key" class="_panel" :class="$style.wrapper" :aria-disabled="!noteFilter(note)"> + <div v-if="note" :class="$style.note" @click="() => select(note)"> + <div v-if="note.type === 'quote'" :class="$style.subtext"><i class="ti ti-quote"></i> {{ i18n.ts.quote }}</div> + <div v-if="note.type === 'reply'" :class="$style.subtext"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.reply }}</div> + <div v-if="note.type === 'channel'" :class="$style.subtext"><i class="ti ti-device-tv"></i> {{ i18n.ts.channel }}</div> + <Mfm v-if="note.data.text" :text="note.data.text" :nyaize="'respect'"/> + <div :class="[$style.subtext, $style.bottom]"> + <MkTime :time="note.updatedAt"/> + <div v-if="note.data.files.length"><i class="ti ti-photo-plus" :class="$style.icon"></i>{{ note.data.files.length }}</div> + </div> + </div> + <div :class="$style.trash" @click="() => remove(note)"><i class="ti ti-trash"></i></div> + </div> + </div> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { shallowRef, ref, onMounted } from 'vue'; +import * as noteDrafts from '@/scripts/note-drafts.js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import { i18n } from '@/i18n.js'; +import { signinRequired } from '@/account.js'; +import * as os from '@/os.js'; + +const $i = signinRequired(); + +const props = defineProps<{ + channelId?: string; +}>(); + +const emit = defineEmits<{ + (ev: 'selected', res: noteDrafts.NoteDraft): void; + (ev: 'closed'): void; +}>(); + +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const notes = ref<Record<string, noteDrafts.NoteDraft | undefined> | null>(null); + +function noteFilter(note: noteDrafts.NoteDraft | undefined) { + if (!note) return false; + + // チャンネルモードの場合はチャンネル内での下書きのみを表示 + if (props.channelId) return note.type === 'channel' && note.auxId === props.channelId; + + // チャンネル外ならチャンネル内の下書きは表示しない + if (note.type === 'channel') return false; + + return true; +} + +function select(note: noteDrafts.NoteDraft) { + if (!noteFilter(note)) return; + emit('selected', note); + dialog.value?.close(); +} + +async function remove(note: noteDrafts.NoteDraft | undefined) { + if (!note) return; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }); + + if (canceled) return; + await noteDrafts.remove(note.type, $i.id, note.uniqueId, note.auxId as string); + notes.value = await noteDrafts.getAll($i.id); +} + +onMounted(async () => { + notes.value = await noteDrafts.getAll($i.id); +}); +</script> + +<style lang="scss" module> +.container { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 16px; + overflow-x: clip; + padding: 16px; +} + +.wrapper { + display: flex; + border-radius: 12px; + background-color: var(--buttonBg); + cursor: pointer; + + &:hover:not([aria-disabled="true"]) { + background-color: var(--buttonHoverBg); + } + + &[aria-disabled="true"] { + opacity: 0.5; + cursor: not-allowed; + } +} + +.note { + display: flex; + flex-direction: column; + justify-content: center; + padding: 10px; + gap: 6px; + flex-grow: 1; +} + +.subtext { + font-size: 0.8em; + opacity: 0.7; + user-select: none; +} + +.bottom { + display: flex; + gap: 12px; +} + +.icon { + margin-right: 4px; +} + +.center { + text-align: center; +} + +.trash { + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + color: var(--error); + + &:hover { + background-color: var(--error); + color: white; + } +} +</style> diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index 13726c1cba..6c0d1d7426 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -5,7 +5,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div v-adaptive-border - :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" + :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked ,[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' } ]" :aria-checked="checked" :aria-disabled="disabled" @click="toggle" @@ -15,7 +15,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :disabled="disabled" :class="$style.input" > - <span :class="$style.button"> + <span :class="[$style.button , {[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]"> <span></span> </span> <span :class="$style.label"><slot></slot></span> @@ -23,7 +23,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { ref,computed,watch } from 'vue'; +import {defaultStore} from "@/store.js"; + +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = defineProps<{ modelValue: any; @@ -73,14 +76,48 @@ function toggle(): void { border-color: var(--accentedBg) !important; color: var(--accent); cursor: default !important; - + &.gamingDark{ + color:black !important; + border-color: black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + } + &.gamingLight{ + color:white; + border-color: white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + } > .button { border-color: var(--accent); - + &.gamingDark{ + border-color:black; + color:black !important; + } + &.gamingLight{ + border-color: white; + color:white; + } + &.gamingDark:after{ + background-color: black; + transform: scale(1); + opacity: 1; + } + &.gamingLight:after{ + background-color:white !important; + transform: scale(1); + opacity: 1; + } &:after { background-color: var(--accent); transform: scale(1); opacity: 1; + } } } @@ -124,4 +161,69 @@ function toggle(): void { line-height: 20px; cursor: pointer; } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 11035fd9d0..ffedbacaaa 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -8,12 +8,12 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div v-adaptive-border class="body"> <div ref="containerEl" class="container"> <div class="track"> - <div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div> + <div :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light'}" class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div> </div> <div v-if="steps && showTicks" class="ticks"> <div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> </div> - <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> + <div ref="thumbEl" v-tooltip="textConverter(finalValue)" :class="{gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light'}" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> </div> </div> <div class="caption"><slot name="caption"></slot></div> @@ -23,6 +23,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <script lang="ts" setup> import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue'; import * as os from '@/os.js'; +import {defaultStore} from "@/store.js"; + + +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = withDefaults(defineProps<{ modelValue: number; @@ -213,6 +217,20 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { height: 100%; background: var(--accent); opacity: 0.5; + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + &.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } } } @@ -245,9 +263,36 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { cursor: grab; background: var(--accent); border-radius: 999px; - + &.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } &:hover { background: var(--accentLighten); + &.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } } } } @@ -269,4 +314,69 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { } } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 76eafd97ba..22da07d416 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -7,17 +7,17 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License ref="buttonEl" v-ripple="canToggle" class="_button" - :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" + :class="[$style.root, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' ,[$style.reacted]: note.myReactions?.includes(reaction) , [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" @contextmenu.prevent.stop="menu" > <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> - <span :class="$style.count">{{ count }}</span> + <span :class="[$style.count,{ [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{ count }}</span> </button> </template> <script lang="ts" setup> -import { computed, inject, onMounted, shallowRef, watch } from 'vue'; +import { computed, inject, onMounted, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue'; @@ -35,11 +35,15 @@ import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.j import { customEmojisMap } from '@/custom-emojis.js'; import { getUnicodeEmoji } from '@/scripts/emojilist.js'; +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); + const props = defineProps<{ reaction: string; count: number; isInitial: boolean; - note: Misskey.entities.Note; + note: Misskey.entities.Note & { + myReactions: string[]; + } }>(); const mock = inject<boolean>('mock', false); @@ -61,14 +65,14 @@ const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction async function toggleReaction() { if (!canToggle.value) return; - const oldReaction = props.note.myReaction; + const oldReaction = props.note.myReactions?.includes(props.reaction) ? props.reaction : null; if (oldReaction) { const confirm = await os.confirm({ type: 'warning', text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm, }); if (confirm.canceled) return; - + props.note.myReactions.splice(props.note.myReactions.indexOf(oldReaction), 1); if (oldReaction !== props.reaction) { sound.playMisskeySfx('reaction'); } @@ -80,8 +84,9 @@ async function toggleReaction() { misskeyApi('notes/reactions/delete', { noteId: props.note.id, + reaction: oldReaction, }).then(() => { - if (oldReaction !== props.reaction) { + if (oldReaction !== props.reaction ) { misskeyApi('notes/reactions/create', { noteId: props.note.id, reaction: props.reaction, @@ -211,8 +216,32 @@ if (!mock) { color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; + &.gamingDark{ + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + box-shadow: 0 0 0px 1px white inset; + } + + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + box-shadow: 0 0 0px 1px white inset; + color: white !important; + } + > .count { color: var(--accent); + &.gamingLight{ + color: white; + } + &.gamingDark{ + color: black; + } } > .icon { @@ -231,4 +260,69 @@ if (!mock) { line-height: 42px; margin: 0 0 0 4px; } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 652bde4ecf..639b804177 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -23,7 +23,9 @@ import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ - note: Misskey.entities.Note; + note: Misskey.entities.Note & { + myReactions: string[]; + } maxNumber?: number; }>(), { maxNumber: Infinity, @@ -56,7 +58,6 @@ function onMockToggleReaction(emoji: string, count: number) { watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; hasMoreReactions.value = Object.keys(newSource).length > maxNumber; - for (let i = 0; i < reactions.value.length; i++) { const reaction = reactions.value[i][0]; if (reaction in newSource && newSource[reaction] !== 0) { @@ -75,7 +76,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe newReactions = newReactions.slice(0, props.maxNumber); - if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) { + if (props.note. myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) { newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); } diff --git a/packages/frontend/src/components/MkRemoteInfoUpdate.vue b/packages/frontend/src/components/MkRemoteInfoUpdate.vue new file mode 100644 index 0000000000..d9fe7b9c30 --- /dev/null +++ b/packages/frontend/src/components/MkRemoteInfoUpdate.vue @@ -0,0 +1,39 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<a :class="$style.root" @click="UserInfoUpdate"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserInfoUpdate }}</a> +</template> + +<script lang="ts" setup> +import { i18n } from '@/i18n.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +const props = withDefaults(defineProps<{ + UserId: string; +}>(), { + UserId: null, +}); + +function UserInfoUpdate() { + misskeyApi('federation/update-remote-user', { userId: props.UserId }); +} + +</script> + +<style lang="scss" module> +.root { + font-size: 0.8em; + padding: 16px; + background: var(--infoWarnBg); + color: var(--infoWarnFg); + border-radius: var(--radius); + overflow: clip; +} + +.link { + margin-left: 4px; + color: var(--accent); +} +</style> diff --git a/packages/frontend/src/components/MkScheduleEditor.vue b/packages/frontend/src/components/MkScheduleEditor.vue new file mode 100644 index 0000000000..2035dcc450 --- /dev/null +++ b/packages/frontend/src/components/MkScheduleEditor.vue @@ -0,0 +1,61 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div style="padding: 8px 16px"> + <section class="_gaps_s"> + <MkInput v-model="atDate" small type="date" class="input"> + <template #label>{{ i18n.ts._schedulePost.postDate }}</template> + </MkInput> + <MkInput v-model="atTime" small type="time" class="input"> + <template #label>{{ i18n.ts._schedulePost.postTime }}</template> + <template #caption>{{ i18n.ts._schedulePost.localTime }}</template> + </MkInput> + </section> +</div> +</template> + +<script lang="ts" setup> +import { ref, watch } from 'vue'; +import MkInput from './MkInput.vue'; +import { formatDateTimeString } from '@/scripts/format-time-string.js'; +import { addTime } from '@/scripts/time.js'; +import { i18n } from '@/i18n.js'; + +const props = defineProps<{ + modelValue: { + scheduledAt: string; + }; +}>(); +const emit = defineEmits<{ + (ev: 'update:modelValue', v: { + scheduledAt: string; + }): void; +}>(); + +const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd')); +const atTime = ref('00:00'); +if ( props.modelValue.scheduledAt) { + const date = new Date(props.modelValue.scheduledAt); + atDate.value = formatDateTimeString(date, 'yyyy-MM-dd'); + atTime.value = formatDateTimeString(date, 'HH:mm'); +} + +function get() { + const calcAt = () => { + return new Date(`${atDate.value}T${atTime.value}`).toISOString(); + }; + + return { + ...( + props.modelValue ? { scheduledAt: calcAt() } : '' + ), + }; +} + +watch([atDate, atTime], () => emit('update:modelValue', get()), { + immediate: true, +}); +</script> diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue new file mode 100644 index 0000000000..23bcb6d25e --- /dev/null +++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue @@ -0,0 +1,64 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :withOkButton="false" + @click="cancel()" + @close="cancel()" +> + <template #header>{{ i18n.ts._schedulePost.list }}</template> + <MkSpacer :marginMin="14" :marginMax="16"> + <MkPagination ref="paginationEl" :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </template> + + <template #default="{ items }"> + <div class="_gaps"> + <MkNoteSimple v-for="item in items" :key="item.id" :scheduled="true" :note="item.note" @editScheduleNote="listUpdate"/> + </div> + </template> + </MkPagination> + </MkSpacer> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import type { Paging } from '@/components/MkPagination.vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkPagination from '@/components/MkPagination.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import { i18n } from '@/i18n.js'; +import { infoImageUrl } from '@/instance.js'; + +const emit = defineEmits<{ + (ev: 'cancel'): void; +}>(); + +const dialogEl = ref(); +const cancel = () => { + emit('cancel'); + dialogEl.value.close(); +}; +const paginationEl = ref(); +const pagination: Paging = { + endpoint: 'notes/schedule/list', + limit: 10, +}; + +function listUpdate() { + paginationEl.value.reload(); +} +</script> + +<style lang="scss" module> +</style> diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index ddcac0e0bd..21d7c14018 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -181,7 +181,7 @@ function show() { <style lang="scss" module> .label { font-size: 0.85em; - padding: 0 0 8px 0; + padding: 8px 0; user-select: none; &:empty { diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 3a1fa65ae8..decbf3947e 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -3,102 +3,141 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<div> - <div :class="$style.banner"> - <i class="ti ti-user-edit"></i> - </div> - <MkSpacer :marginMin="20" :marginMax="32"> - <form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit"> - <MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required> - <template #label>{{ i18n.ts.invitationCode }}</template> - <template #prefix><i class="ti ti-key"></i></template> - </MkInput> - <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername"> - <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - <template #caption> - <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> - <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> - <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> - <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> - <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> - </template> - </MkInput> - <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> - <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> - <template #prefix><i class="ti ti-mail"></i></template> - <template #caption> - <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> - <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> - <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> - <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> - <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> - <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> - <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> - </template> - </MkInput> - <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> - <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ti ti-lock"></i></template> - <template #caption> - <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> - <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> - <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> - </template> - </MkInput> - <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> - <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> - <template #prefix><i class="ti ti-lock"></i></template> - <template #caption> - <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> - <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> - </template> - </MkInput> - <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> - <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> - <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> - <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> - <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> - <template v-if="submitting"> - <MkLoading :em="true" :colored="false"/> - </template> - <template v-else>{{ i18n.ts.start }}</template> - </MkButton> - </form> - </MkSpacer> -</div> + <div> + <div :class="[$style.banner ,{[$style.gamingDark]: gamingType ==='dark' , [$style.gamingLight]: gamingType ==='light'}]"> + <i class="ti ti-user-edit"></i> + </div> + <MkSpacer :marginMin="20" :marginMax="32"> + <form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit"> + <MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required> + <template #label>{{ i18n.ts.invitationCode }}</template> + <template #prefix><i class="ti ti-key"></i></template> + </MkInput> + <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" + autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername"> + <template #label>{{ i18n.ts.username }} + <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div> + </template> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + <template #caption> + <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> + <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ + i18n.ts.checking + }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i + class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> + </template> + </MkInput> + <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" + :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> + <template #label>{{ i18n.ts.emailAddress }} + <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i + class="ti ti-help-circle"></i></div> + </template> + <template #prefix><i class="ti ti-mail"></i></template> + <template #caption> + <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i + class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> + <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> + <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> + <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> + <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> + <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + </template> + </MkInput> + <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password + @update:modelValue="onChangePassword"> + <template #label>{{ i18n.ts.password }}</template> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption> + <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> + <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i + class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> + <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i + class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> + </template> + </MkInput> + <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required + data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> + <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption> + <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ + i18n.ts.passwordMatched + }}</span> + <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i + class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> + </template> + </MkInput> + <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" + provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> + <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" + provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" + provider="turnstile" :sitekey="instance.turnstileSiteKey"/> + <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit + style="margin: 0 auto;"> + <template v-if="submitting"> + <MkLoading :em="true" :colored="false"/> + </template> + <template v-else>{{ i18n.ts.start }}</template> + </MkButton> + </form> + </MkSpacer> + </div> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { computed, ref, watch } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; -import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; +import MkCaptcha, {type Captcha} from '@/components/MkCaptcha.vue'; import * as config from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { login } from '@/account.js'; -import { instance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; +import {login} from '@/account.js'; +import {instance} from '@/instance.js'; +import {i18n} from '@/i18n.js'; +import {defaultStore} from "@/store.js"; + + +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = withDefaults(defineProps<{ - autoSet?: boolean; + autoSet?: boolean; }>(), { - autoSet: false, + autoSet: false, }); const emit = defineEmits<{ - (ev: 'signup', user: Misskey.entities.SigninResponse): void; - (ev: 'signupEmailPending'): void; + (ev: 'signup', user: Misskey.entities.SigninResponse): void; + (ev: 'signupEmailPending'): void; }>(); const host = toUnicode(config.host); @@ -125,182 +164,272 @@ const usernameAbortController = ref<null | AbortController>(null); const emailAbortController = ref<null | AbortController>(null); const shouldDisableSubmitting = computed((): boolean => { - return submitting.value || - instance.enableHcaptcha && !hCaptchaResponse.value || - instance.enableMcaptcha && !mCaptchaResponse.value || - instance.enableRecaptcha && !reCaptchaResponse.value || - instance.enableTurnstile && !turnstileResponse.value || - instance.emailRequiredForSignup && emailState.value !== 'ok' || - usernameState.value !== 'ok' || - passwordRetypeState.value !== 'match'; + return submitting.value || + instance.enableHcaptcha && !hCaptchaResponse.value || + instance.enableMcaptcha && !mCaptchaResponse.value ||instance.enableRecaptcha && !reCaptchaResponse.value || + instance.enableTurnstile && !turnstileResponse.value || + instance.emailRequiredForSignup && emailState.value !== 'ok' || + usernameState.value !== 'ok' || + passwordRetypeState.value !== 'match'; }); function getPasswordStrength(source: string): number { - let strength = 0; - let power = 0.018; + let strength = 0; + let power = 0.018; - // 英数字 - if (/[a-zA-Z]/.test(source) && /[0-9]/.test(source)) { - power += 0.020; - } + // 英数字 + if (/[a-zA-Z]/.test(source) && /[0-9]/.test(source)) { + power += 0.020; + } - // 大文字と小文字が混ざってたら - if (/[a-z]/.test(source) && /[A-Z]/.test(source)) { - power += 0.015; - } + // 大文字と小文字が混ざってたら + if (/[a-z]/.test(source) && /[A-Z]/.test(source)) { + power += 0.015; + } - // 記号が混ざってたら - if (/[!\x22\#$%&@'()*+,-./_]/.test(source)) { - power += 0.02; - } + // 記号が混ざってたら + if (/[!\x22\#$%&@'()*+,-./_]/.test(source)) { + power += 0.02; + } - strength = power * source.length; + strength = power * source.length; - return Math.max(0, Math.min(1, strength)); + return Math.max(0, Math.min(1, strength)); } function onChangeUsername(): void { - if (username.value === '') { - usernameState.value = null; - return; - } + if (username.value === '') { + usernameState.value = null; + return; + } - { - const err = - !username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : - username.value.length < 1 ? 'min-range' : - username.value.length > 20 ? 'max-range' : - null; + { + const err = + !username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : + username.value.length < 1 ? 'min-range' : + username.value.length > 20 ? 'max-range' : + null; - if (err) { - usernameState.value = err; - return; - } - } + if (err) { + usernameState.value = err; + return; + } + } - if (usernameAbortController.value != null) { - usernameAbortController.value.abort(); - } - usernameState.value = 'wait'; - usernameAbortController.value = new AbortController(); + if (usernameAbortController.value != null) { + usernameAbortController.value.abort(); + } + usernameState.value = 'wait'; + usernameAbortController.value = new AbortController(); - misskeyApi('username/available', { - username: username.value, - }, undefined, usernameAbortController.value.signal).then(result => { - usernameState.value = result.available ? 'ok' : 'unavailable'; - }).catch((err) => { - if (err.name !== 'AbortError') { - usernameState.value = 'error'; - } - }); + misskeyApi('username/available', { + username: username.value, + }, undefined, usernameAbortController.value.signal).then(result => { + usernameState.value = result.available ? 'ok' : 'unavailable'; + }).catch((err) => { + if (err.name !== 'AbortError') { + usernameState.value = 'error'; + } + }); } function onChangeEmail(): void { - if (email.value === '') { - emailState.value = null; - return; - } + if (email.value === '') { + emailState.value = null; + return; + } - if (emailAbortController.value != null) { - emailAbortController.value.abort(); - } - emailState.value = 'wait'; - emailAbortController.value = new AbortController(); + if (emailAbortController.value != null) { + emailAbortController.value.abort(); + } + emailState.value = 'wait'; + emailAbortController.value = new AbortController(); - misskeyApi('email-address/available', { - emailAddress: email.value, - }, undefined, emailAbortController.value.signal).then(result => { - emailState.value = result.available ? 'ok' : - result.reason === 'used' ? 'unavailable:used' : - result.reason === 'format' ? 'unavailable:format' : - result.reason === 'disposable' ? 'unavailable:disposable' : - result.reason === 'banned' ? 'unavailable:banned' : + misskeyApi('email-address/available', { + emailAddress: email.value, + }, undefined, emailAbortController.value.signal).then(result => { + emailState.value = result.available ? 'ok' : + result.reason === 'used' ? 'unavailable:used' : + result.reason === 'format' ? 'unavailable:format' : + result.reason === 'disposable' ? 'unavailable:disposable' : + result.reason === 'banned' ? 'unavailable:banned' : result.reason === 'mx' ? 'unavailable:mx' : - result.reason === 'smtp' ? 'unavailable:smtp' : - 'unavailable'; - }).catch((err) => { - if (err.name !== 'AbortError') { - emailState.value = 'error'; - } - }); + result.reason === 'smtp' ? 'unavailable:smtp' : + 'unavailable'; + }).catch((err) => { + if (err.name !== 'AbortError') { + emailState.value = 'error'; + } + }); } function onChangePassword(): void { - if (password.value === '') { - passwordStrength.value = ''; - return; - } + if (password.value === '') { + passwordStrength.value = ''; + return; + } - const strength = getPasswordStrength(password.value); - passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; + const strength = getPasswordStrength(password.value); + passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; } function onChangePasswordRetype(): void { - if (retypedPassword.value === '') { - passwordRetypeState.value = null; - return; - } + if (retypedPassword.value === '') { + passwordRetypeState.value = null; + return; + } - passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match'; + passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match'; } async function onSubmit(): Promise<void> { - if (submitting.value) return; - submitting.value = true; + if (submitting.value) return; + submitting.value = true; - try { - await misskeyApi('signup', { - username: username.value, - password: password.value, - emailAddress: email.value, - invitationCode: invitationCode.value, - 'hcaptcha-response': hCaptchaResponse.value, - 'm-captcha-response': mCaptchaResponse.value, + try { + await misskeyApi('signup', { + username: username.value, + password: password.value, + emailAddress: email.value, + invitationCode: invitationCode.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, - 'turnstile-response': turnstileResponse.value, - }); - if (instance.emailRequiredForSignup) { - os.alert({ - type: 'success', - title: i18n.ts._signup.almostThere, - text: i18n.tsx._signup.emailSent({ email: email.value }), - }); - emit('signupEmailPending'); - } else { - const res = await misskeyApi('signin', { - username: username.value, - password: password.value, - }); - emit('signup', res); + 'turnstile-response': turnstileResponse.value, + }); + if (instance.emailRequiredForSignup) { + os.alert({ + type: 'success', + title: i18n.ts._signup.almostThere, + text: i18n.tsx._signup.emailSent({email: email.value}), + }); + emit('signupEmailPending'); + } else { + const res = await misskeyApi('signin', { + username: username.value, + password: password.value, + }); + emit('signup', res); - if (props.autoSet) { - return login(res.i); - } - } - } catch { - submitting.value = false; - hcaptcha.value?.reset?.(); - recaptcha.value?.reset?.(); - turnstile.value?.reset?.(); + if (props.autoSet) { + return login(res.i); + } + } + } catch { + submitting.value = false; + hcaptcha.value?.reset?.(); + recaptcha.value?.reset?.(); + turnstile.value?.reset?.(); - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); - } + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); + } } </script> <style lang="scss" module> .banner { - padding: 16px; - text-align: center; - font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + padding: 16px; + text-align: center; + font-size: 26px; + background-color: var(--accentedBg); + color: var(--accent); + + &.gamingDark { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + color: var(--navFg); + } + + &.gamingLight { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + color: var(--navFg); + } + } .captcha { - margin: 16px 0; + margin: 16px 0; +} + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 11af246501..f09845a994 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div> - <div :class="$style.banner"> + <div :class="[$style.banner ,{[$style.gamingDark]: gamingType ==='dark' , [$style.gamingLight]: gamingType ==='light'}]"> <i class="ti ti-checklist"></i> </div> <MkSpacer :marginMin="20" :marginMax="28"> @@ -69,6 +69,8 @@ import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; +import {defaultStore} from "@/store.js"; +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const availableServerRules = instance.serverRules.length > 0; const availableTos = instance.tosUrl != null && instance.tosUrl !== ''; @@ -146,11 +148,30 @@ async function updateAgreeNote(v: boolean) { <style lang="scss" module> .banner { - padding: 16px; - text-align: center; - font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + padding: 16px; + text-align: center; + font-size: 26px; + background-color: var(--accentedBg); + color: var(--accent); + + &.gamingDark { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + color: var(--navFg); + } + + &.gamingLight { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + color: var(--navFg); + } + } .rules { @@ -188,4 +209,70 @@ async function updateAgreeNote(v: boolean) { .ruleText { padding-top: 6px; } + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 48430cc585..cc7657382d 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> + + <Mfm v-if="note.text" :emojireq="emojireq" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files && note.files.length > 0"> @@ -39,6 +40,7 @@ import { shouldCollapsed } from '@/scripts/collapsed.js'; const props = defineProps<{ note: Misskey.entities.Note; + emojireq: boolean; }>(); const isLong = shouldCollapsed(props.note, []); diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index f0baff4fa5..a00a263bf0 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -9,15 +9,15 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div class="items"> <template v-for="(item, i) in group.items"> - <a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> + <a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active, gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }"> <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span> <span class="text">{{ item.text }}</span> </a> - <button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)"> + <button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active , gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }" :disabled="item.active" @click="ev => item.action(ev)"> <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span> <span class="text">{{ item.text }}</span> </button> - <MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> + <MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active , gamingDark: gamingType === 'dark',gamingLight: gamingType === 'light' }"> <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span> <span class="text">{{ item.text }}</span> </MkA> @@ -28,7 +28,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { } from 'vue'; +import {ref , computed , watch } from 'vue'; +import {defaultStore} from "@/store.js"; + +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); defineProps<{ def: any[]; @@ -69,6 +72,21 @@ defineProps<{ &.active { color: var(--accent); background: var(--accentedBg); + &.gamingDark{ + color: black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + &.gamingLight{ + color: white; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } } &.danger { @@ -153,4 +171,70 @@ defineProps<{ } } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> + diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue index f77dbf521e..cdc2fb3a5c 100644 --- a/packages/frontend/src/components/MkSwitch.button.vue +++ b/packages/frontend/src/components/MkSwitch.button.vue @@ -4,87 +4,198 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <span - v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" - :class="{ + v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" + :class="{ [$style.button]: true, + [$style.gamingDark]: gamingType === 'dark' && checked, + [$style.gamingLight]: gamingType === 'light' && checked, [$style.buttonChecked]: checked, - [$style.buttonDisabled]: props.disabled + [$style.buttonDisabled]: props.disabled, + }" - data-cy-switch-toggle - @click.prevent.stop="toggle" + data-cy-switch-toggle + @click.prevent.stop="toggle" > - <div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div> + <div + :class="{ [$style.knob]: true, [$style.knobChecked]: checked, [$style.gamingDark]: gamingType === 'dark' && checked,[$style.gamingLight]: gamingType === 'light' && checked}"></div> </span> </template> <script lang="ts" setup> -import { toRefs, Ref } from 'vue'; -import { i18n } from '@/i18n.js'; +import {toRefs, Ref, computed} from 'vue'; +import {i18n} from '@/i18n.js'; +import {defaultStore} from "@/store.js"; + +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = withDefaults(defineProps<{ - checked: boolean | Ref<boolean>; - disabled?: boolean | Ref<boolean>; + checked: boolean | Ref<boolean>; + disabled?: boolean | Ref<boolean>; }>(), { - disabled: false, + disabled: false, }); const emit = defineEmits<{ - (ev: 'toggle'): void; + (ev: 'toggle'): void; }>(); const checked = toRefs(props).checked; const toggle = () => { - emit('toggle'); + emit('toggle'); }; </script> <style lang="scss" module> .button { - --height: 21px; + --height: 21px;position: relative; + display: inline-flex; + flex-shrink: 0; + margin: 0; + box-sizing: border-box; + width: calc(var(--height) * 1.6); + height: calc(var(--height) + 2px); // 枠線 + outline: none; + background: var(--switchOffBg); + background-clip: content-box; + border: solid 1px var(--switchOffBg); + border-radius: 999px; + cursor: pointer; + transition: inherit; + user-select: none; - position: relative; - display: inline-flex; - flex-shrink: 0; - margin: 0; - box-sizing: border-box; - width: calc(var(--height) * 1.6); - height: calc(var(--height) + 2px); // 枠線 - outline: none; - background: var(--switchOffBg); - background-clip: content-box; - border: solid 1px var(--switchOffBg); - border-radius: 999px; - cursor: pointer; - transition: inherit; - user-select: none; + &.gamingLight { + border-image: conic-gradient(#e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd) 1; + border: solid 1px; + } + + &.gamingDark { + border-image: conic-gradient(#c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4) 1; + border: solid 1px; + } } .buttonChecked { - background-color: var(--switchOnBg) !important; - border-color: var(--switchOnBg) !important; + background-color: var(--switchOnBg); + border-color: var(--switchOnBg); +} + +.gamingLight { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; +} + +.gamingDark { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; } .buttonDisabled { - cursor: not-allowed; + cursor: not-allowed; } .knob { - position: absolute; - box-sizing: border-box; - top: 3px; - width: calc(var(--height) - 6px); - height: calc(var(--height) - 6px); - border-radius: 999px; - transition: all 0.2s ease; + position: absolute; + box-sizing: border-box;top: 3px; + width: calc(var(--height) - 6px); + height: calc(var(--height) - 6px); + border-radius: 999px; + transition: all 0.2s ease; - &:not(.knobChecked) { - left: 3px; - background: var(--switchOffFg); - } + &:not(.knobChecked) { + left: 3px; + background: var(--switchOffFg); + + } } .knobChecked { - left: calc(calc(100% - var(--height)) + 3px); - background: var(--switchOnFg); + left: calc(calc(100% - var(--height)) + 3px); + background: var(--switchOnFg); + + &.gamingDark { + background: white !important; + } + + &.gamingLight { + background: white !important; + } +} + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index bc12931e95..5a3a4a697a 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<div :class="[$style.root, { [$style.disabled]: disabled }]"> +<div :class="[$style.root, { [$style.disabled]: disabled }]"> <input ref="input" type="checkbox" @@ -12,7 +12,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License @keydown.enter="toggle" > <XButton :checked="checked" :disabled="disabled" @toggle="toggle"/> - <span v-if="!noBody" :class="$style.body"> + <span v-if="!noBody" :class="$style.body,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}"> <!-- TODO: 無名slotの方は廃止 --> <span :class="$style.label"> <span @click="toggle"> @@ -26,8 +26,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { toRefs, Ref } from 'vue'; +import {toRefs, Ref, ref, computed, watch} from 'vue'; import XButton from '@/components/MkSwitch.button.vue'; +import {defaultStore} from "@/store.js"; +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = defineProps<{ modelValue: boolean | Ref<boolean>; @@ -66,6 +68,29 @@ const toggle = () => { opacity: 0.6; cursor: not-allowed; } + + &.gamingDarkDisabled{ + opacity: 0.6; + cursor: not-allowed; + background: linear-gradient(270deg, #a84f4f, #a88c4f, #9aa24b, #6da85c, #53a8a6, #7597b5, #8679b5, #b579b5, #b56d96); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + + } + &.gamingLightDisabled{ + opacity: 0.6; + cursor: not-allowed; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.25, 0.25, 1) infinite; + + } + //&.checked { + //} } .input { @@ -105,4 +130,6 @@ const toggle = () => { font-size: 85%; vertical-align: top; } + + </style> diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue index b48a9ca2cb..19358753d2 100644 --- a/packages/frontend/src/components/MkTab.vue +++ b/packages/frontend/src/components/MkTab.vue @@ -3,7 +3,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <script lang="ts"> -import { defineComponent, h, resolveDirective, withDirectives } from 'vue'; +import {computed, defineComponent, h, resolveDirective, withDirectives , ref , watch} from 'vue'; +import {defaultStore} from "@/store.js"; + +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); export default defineComponent({ props: { @@ -17,7 +20,7 @@ export default defineComponent({ return () => h('div', { class: 'pxhvhrfw', }, options.map(option => withDirectives(h('button', { - class: ['_button', { active: props.modelValue === option.props?.value }], + class: ['_button', { active: props.modelValue === option.props?.value , gamingDark: gamingType.value == 'dark' && props.modelValue === option.props.value,gamingLight: gamingType.value == 'light' && props.modelValue === option.props.value } ], key: option.key as string, disabled: props.modelValue === option.props?.value, onClick: () => { @@ -48,6 +51,24 @@ export default defineComponent({ &.active { color: var(--accent); background: var(--accentedBg); + + &.gamingDark{ + color: black !important; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + &.gamingLight{ + + color:white !important; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } } &:not(.active):hover { @@ -74,4 +95,69 @@ export default defineComponent({ } } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index ba3b6e070d..9ef20659a9 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -1,5 +1,6 @@ <!-- -SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License-Identifier: AGPL-3.0-only +SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project +SPDX-License-Identifier: AGPL-3.0-only --> <template> @@ -9,6 +10,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License ref="tlComponent" :pagination="paginationQuery" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" + :withCw="props.withCw" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)" /> @@ -16,7 +18,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; +import { computed, watch, onUnmounted, provide, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; @@ -28,7 +30,7 @@ import { defaultStore } from '@/store.js'; import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ - src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; + src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role' | 'media'; list?: string; antenna?: string; channel?: string; @@ -37,10 +39,12 @@ const props = withDefaults(defineProps<{ withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + withCw?: boolean; }>(), { withRenotes: true, withReplies: false, onlyFiles: false, + withCw: false, }); const emit = defineEmits<{ @@ -51,24 +55,13 @@ const emit = defineEmits<{ provide('inTimeline', true); provide('inChannel', computed(() => props.src === 'channel')); -type TimelineQueryType = { - antennaId?: string, - withRenotes?: boolean, - withReplies?: boolean, - withFiles?: boolean, - visibility?: string, - listId?: string, - channelId?: string, - roleId?: string -} - const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>(); const tlComponent = shallowRef<InstanceType<typeof MkNotes>>(); let tlNotesCount = 0; function prepend(note) { - if (tlComponent.value == null) return; + if (!tlComponent.value) return; tlNotesCount++; @@ -92,11 +85,8 @@ let paginationQuery: Paging | null = null; const stream = useStream(); function connectChannel() { - if (props.src === 'antenna') { - if (props.antenna == null) return; - connection = stream.useChannel('antenna', { - antennaId: props.antenna, - }); + if (props.src === 'antenna' && props.antenna) { + connection = stream.useChannel('antenna', { antennaId: props.antenna }); } else if (props.src === 'home') { connection = stream.useChannel('homeTimeline', { withRenotes: props.withRenotes, @@ -109,14 +99,15 @@ function connectChannel() { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }); - } else if (props.src === 'social') { + } else if (props.src === 'media') { connection = stream.useChannel('hybridTimeline', { + withFiles: true, withRenotes: props.withRenotes, withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, }); - } else if (props.src === 'global') { - connection = stream.useChannel('globalTimeline', { + } else if (props.src === 'social' || props.src === 'global') { + const channel = props.src === 'social' ? 'hybridTimeline' : 'globalTimeline'; + connection = stream.useChannel(channel, { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }); @@ -125,31 +116,25 @@ function connectChannel() { connection.on('mention', prepend); } else if (props.src === 'directs') { const onNote = note => { - if (note.visibility === 'specified') { - prepend(note); - } + if (note.visibility === 'specified') prepend(note); }; connection = stream.useChannel('main'); connection.on('mention', onNote); - } else if (props.src === 'list') { - if (props.list == null) return; + } else if (props.src === 'list' && props.list) { connection = stream.useChannel('userList', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }); - } else if (props.src === 'channel') { - if (props.channel == null) return; - connection = stream.useChannel('channel', { - channelId: props.channel, - }); - } else if (props.src === 'role') { - if (props.role == null) return; - connection = stream.useChannel('roleTimeline', { - roleId: props.role, - }); + } else if (props.src === 'channel' && props.channel) { + connection = stream.useChannel('channel', { channelId: props.channel }); + } else if (props.src === 'role' && props.role) { + connection = stream.useChannel('roleTimeline', { roleId: props.role }); + } + + if (props.src !== 'directs' && props.src !== 'mentions') { + connection?.on('note', prepend); } - if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend); } function disconnectChannel() { @@ -158,78 +143,45 @@ function disconnectChannel() { } function updatePaginationQuery() { - let endpoint: keyof Misskey.Endpoints | null; - let query: TimelineQueryType | null; + const endpoints = { + antenna: 'antennas/notes', + home: 'notes/timeline', + local: 'notes/local-timeline', + social: 'notes/hybrid-timeline', + global: 'notes/global-timeline', + media: 'notes/hybrid-timeline', + mentions: 'notes/mentions', + directs: 'notes/mentions', + list: 'notes/user-list-timeline', + channel: 'channels/timeline', + role: 'roles/notes', + }; - if (props.src === 'antenna') { - endpoint = 'antennas/notes'; - query = { - antennaId: props.antenna, - }; - } else if (props.src === 'home') { - endpoint = 'notes/timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'local') { - endpoint = 'notes/local-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'social') { - endpoint = 'notes/hybrid-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'global') { - endpoint = 'notes/global-timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'mentions') { - endpoint = 'notes/mentions'; - query = null; - } else if (props.src === 'directs') { - endpoint = 'notes/mentions'; - query = { - visibility: 'specified', - }; - } else if (props.src === 'list') { - endpoint = 'notes/user-list-timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - listId: props.list, - }; - } else if (props.src === 'channel') { - endpoint = 'channels/timeline'; - query = { - channelId: props.channel, - }; - } else if (props.src === 'role') { - endpoint = 'roles/notes'; - query = { - roleId: props.role, - }; - } else { - endpoint = null; - query = null; - } - - if (endpoint && query) { + const queries = { + antenna: { antennaId: props.antenna }, + home: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined }, + local: { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined }, + social: { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined }, + global: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined }, + media: { withFiles: true, withRenotes: props.withRenotes, withReplies: false }, + mentions: null, + directs: { visibility: 'specified' }, + list: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list }, + channel: { channelId: props.channel }, + role: { roleId: props.role }, + }; + if (props.src.startsWith('remoteLocalTimeline')) { paginationQuery = { - endpoint: endpoint, + endpoint: 'notes/any-local-timeline', limit: 10, - params: query, + params: { + host: props.list, + }, }; } else { - paginationQuery = null; + const endpoint = endpoints[props.src]; + const query = queries[props.src]; + paginationQuery = endpoint && query ? { endpoint, limit: 10, params: query } : null; } } @@ -238,12 +190,9 @@ function refreshEndpointAndChannel() { disconnectChannel(); connectChannel(); } - updatePaginationQuery(); } -// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる -// IDが切り替わったら切り替え先のTLを表示させたい watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); // 初回表示用 @@ -255,13 +204,11 @@ onUnmounted(() => { function reloadTimeline() { return new Promise<void>((res) => { - if (tlComponent.value == null) return; + if (!tlComponent.value) return; tlNotesCount = 0; - tlComponent.value.pagingComponent?.reload().then(() => { - res(); - }); + tlComponent.value.pagingComponent?.reload().then(() => res()); }); } diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 44a9409cb0..9e666dc8cd 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -13,10 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" scrolling="no" - :allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')" + :allow="player.allow == null ? 'encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')" :class="$style.playerIframe" - :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" - :style="{ border: 0 }" + :src="player.url" + :style="{ border: 0, backgroundColor: 'transparent' }" + allowtransparency="true" ></iframe> <span v-else>invalid url</span> </div> @@ -27,14 +28,18 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> <template v-else-if="tweetId && tweetExpanded"> - <div ref="twitter"> + <div ref="twitter" :class="$style.twitter"> <iframe ref="tweet" allow="fullscreen;web-share" sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin" scrolling="no" - :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }" + data-transparent="true" + + :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0,borderRadius: '14px'}" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`" + frameborder="0" + allowtransparency="true" ></iframe> </div> <div :class="$style.action"> @@ -83,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; +import { defineAsyncComponent, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; import type { summaly } from '@misskey-dev/summaly'; import { url as local } from '@/config.js'; import { i18n } from '@/i18n.js'; @@ -135,6 +140,15 @@ onDeactivated(() => { playerEnabled.value = false; }); +onMounted(() => { + if (defaultStore.state.alwaysShowPlayer) { + playerEnabled.value = true; + } + if (defaultStore.state.alwaysExpandTweet) { + tweetExpanded.value = true; + } +}); + const requestUrl = new URL(props.url); if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); @@ -206,7 +220,13 @@ onUnmounted(() => { position: relative; width: 100%; } +.twitter{ + width: 70%; +} +.app{ + background: red; +} .disablePlayer { position: absolute; top: -1.5em; diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index fae2bb2c15..ddfabc74d1 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkModalWindow ref="dialogEl" :withOkButton="true" - :okButtonDisabled="selected == null" + :okButtonDisabled="(!selected && multipleSelected.length < 1)" @click="cancel()" @close="cancel()" @ok="ok()" @@ -31,9 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </FormSplit> </div> + <div v-if="username != '' || host != ''" :class="[$style.result, { [$style.hit]: users.length > 0 }]"> <div v-if="users.length > 0" :class="$style.users"> - <div v-for="user in users" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()"> + <div v-for="user in users" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id || multipleSelected.includes(user)}]" @click="multiple ? (multipleSelected.includes(user) ? multipleSelected.splice(multipleSelected.indexOf(user), 1) : multipleSelected.push(user)) : selected = user" @dblclick="ok()"> <MkAvatar :user="user" :class="$style.avatar" indicator/> <div :class="$style.userBody"> <MkUserName :user="user" :class="$style.userName"/> @@ -47,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="username == '' && host == ''" :class="$style.recent"> <div :class="$style.users"> - <div v-for="user in recentUsers" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()"> + <div v-for="user in recentUsers" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id || multipleSelected.includes(user) }]" @click="multiple ? (multipleSelected.includes(user) ? multipleSelected.splice(multipleSelected.indexOf(user), 1) : multipleSelected.push(user)) : selected = user" @dblclick="ok()"> <MkAvatar :user="user" :class="$style.avatar" indicator/> <div :class="$style.userBody"> <MkUserName :user="user" :class="$style.userName"/> @@ -71,7 +72,6 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { host as currentHost, hostname } from '@/config.js'; - const emit = defineEmits<{ (ev: 'ok', selected: Misskey.entities.UserDetailed): void; (ev: 'cancel'): void; @@ -80,6 +80,7 @@ const emit = defineEmits<{ const props = withDefaults(defineProps<{ includeSelf?: boolean; + multiple?: boolean; localOnly?: boolean; }>(), { includeSelf: false, @@ -91,6 +92,7 @@ const host = ref(''); const users = ref<Misskey.entities.UserLite[]>([]); const recentUsers = ref<Misskey.entities.UserDetailed[]>([]); const selected = ref<Misskey.entities.UserLite | null>(null); +const multipleSelected = ref<Misskey.entities.UserDetailed[]>([]); const dialogEl = ref(); function search() { @@ -114,17 +116,13 @@ function search() { }); } -async function ok() { - if (selected.value == null) return; - - const user = await misskeyApi('users/show', { - userId: selected.value.id, - }); - emit('ok', user); - +function ok() { + if ((!selected.value && multipleSelected.value.length < 1)) return; + emit('ok', selected.value ?? multipleSelected.value); dialogEl.value.close(); // 最近使ったユーザー更新 + if (multipleSelected.value.length < 0) return; let recents = defaultStore.state.recentlyUsedUsers; recents = recents.filter(x => x !== selected.value?.id); recents.unshift(selected.value.id); diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 4aec6b9b4b..1db40925d4 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -20,7 +20,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div style="overflow-x: clip;"> <div :class="$style.progressBar"> - <div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div> + <div :class="[$style.progressBarValue , {[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" :style="{ width: `${(page / 5) * 100}%` }"></div> </div> <Transition mode="out-in" @@ -127,7 +127,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </template> <script lang="ts" setup> -import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue'; +import {computed, ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import XProfile from '@/components/MkUserSetupDialog.Profile.vue'; @@ -140,6 +140,7 @@ import { host } from '@/config.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const emit = defineEmits<{ (ev: 'closed'): void; @@ -222,6 +223,20 @@ async function later(later: boolean) { height: 100%; background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); transition: all 0.5s cubic-bezier(0,.5,.5,1); + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + &.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } } .centerPage { @@ -253,4 +268,69 @@ async function later(later: boolean) { -webkit-backdrop-filter: blur(15px); backdrop-filter: blur(15px); } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index deced63fad..e2c638346c 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> </div> <div class="_gaps_s" :class="$style.mainActions"> - <MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton> + <MkButton :class="[$style.mainAction , $style.gamingDark]" full rounded data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton> <MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton> <MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton> </div> @@ -165,6 +165,22 @@ function exploreOtherServers() { line-height: 28px; } +.gamingDark{ + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; +} +.gamingDark:hover{ + color: black; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd) !important; + background-size: 1800% 1800% !important; + -webkit-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation:AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; +} .stats { display: grid; grid-template-columns: 1fr 1fr; @@ -200,4 +216,69 @@ function exploreOtherServers() { height: 350px; overflow: auto; } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/XNote.vue b/packages/frontend/src/components/XNote.vue new file mode 100644 index 0000000000..16f50870f9 --- /dev/null +++ b/packages/frontend/src/components/XNote.vue @@ -0,0 +1,1075 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + v-if="!hardMuted && muted === false" + v-show="!isDeleted" + ref="rootEl" + v-hotkey="keymap" + :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" + :tabindex="!isDeleted ? '-1' : undefined" +> + <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> + <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> + <div v-if="isRenote" :class="$style.renote"> + <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> + <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> + <I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText"> + <template #user> + <MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + </template> + </I18n> + <div :class="$style.renoteInfo"> + <button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()"> + <i class="ti ti-dots" :class="$style.renoteMenu"></i> + <MkTime :time="note.createdAt"/> + </button> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> + </div> + </div> + <div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget"> + <MkAvatar :class="$style.collapsedRenoteTargetAvatar" :user="appearNote.user" link preview/> + <Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/> + </div> + <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> + <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> + <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> + <div :class="$style.main"> + + <XNoteHeader :note="appearNote" :mini="true"/> + + <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> + <div style="container-type: inline-size;"> + <p v-if="appearNote.cw != null" :class="$style.cw"> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> + <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;"/> + </p> + <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> + <div :class="$style.text"> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> + <MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> + <Mfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'respect'" + :emojiUrls="appearNote.emojis" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> + <div v-if="translating || translation" :class="$style.translation"> + <MkLoading v-if="translating" mini/> + <div v-else-if="translation"> + <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> + </div> + </div> + </div> + <div v-if="appearNote.files && appearNote.files.length > 0"> + <MkMediaList :mediaList="appearNote.files"/> + </div> + <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> + <div v-if="isEnabledUrlPreview"> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/> + </div> + <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> + <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> + <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> + </button> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true"> + <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> + </button> + </div> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> + </div> + <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction"> + <template #more> + <MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA> + </template> + </MkReactionsViewer> + <footer :class="$style.footer"> + <div :class="$style.footerLeft"> + <button :class="$style.footerButton" class="_button" @click="reply()"> + <i class="ti ti-arrow-back-up"></i> + <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p> + </button> + <button + v-if="canRenote" + ref="renoteButton" + :class="$style.footerButton" + class="_button" + @mousedown="renote()" + > + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p> + </button> + <button v-else :class="$style.footerButton" class="_button" disabled> + <i class="ti ti-ban"></i> + </button> + <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()"> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> + </button> + </div> + <div :class="$style.footerRight"> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> + <i class="ti ti-paperclip"></i> + </button> + + <button :class="$style.footerButton" class="_button" @mousedown="toggleFavorite()"> + <i v-if="!isFavorite" class="ti ti-star"></i> + <i v-if="isFavorite" class="ti ti-star-off"></i> + </button> + </div> + </footer> + </div> + </article> +</div> +<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false"> + <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small"> + <template #name> + <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> + <MkUserName :user="appearNote.user"/> + </MkA> + </template> + </I18n> + <I18n v-else :src="i18n.ts.userSaysSomething" tag="small"> + <template #name> + <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> + <MkUserName :user="appearNote.user"/> + </MkA> + </template> + </I18n> +</div> +<div v-else> + <!-- + MkDateSeparatedList uses TransitionGroup which requires single element in the child elements + so MkNote create empty div instead of no elements + --> +</div> +</template> + +<script lang="ts" setup> +import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue'; +import * as mfm from 'mfm-js'; +import * as Misskey from 'misskey-js'; +import MkNoteSub from '@/components/MkNoteSub.vue'; +import XNoteHeader from '@/components/XNoteHeader.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; +import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue'; +import MkMediaList from '@/components/MkMediaList.vue'; +import MkCwButton from '@/components/MkCwButton.vue'; +import MkPoll from '@/components/MkPoll.vue'; +import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; +import MkUrlPreview from '@/components/MkUrlPreview.vue'; +import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; +import { pleaseLogin } from '@/scripts/please-login.js'; +import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { userPage } from '@/filters/user.js'; +import number from '@/filters/number.js'; +import * as os from '@/os.js'; +import * as sound from '@/scripts/sound.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; +import { defaultStore, noteViewInterruptors } from '@/store.js'; +import { reactionPicker } from '@/scripts/reaction-picker.js'; +import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; +import { $i } from '@/account.js'; +import { i18n } from '@/i18n.js'; +import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; +import { useNoteCapture } from '@/scripts/use-note-capture.js'; +import { deepClone } from '@/scripts/clone.js'; +import { useTooltip } from '@/scripts/use-tooltip.js'; +import { claimAchievement } from '@/scripts/achievements.js'; +import { getNoteSummary } from '@/scripts/get-note-summary.js'; +import { MenuItem } from '@/types/menu.js'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import { shouldCollapsed } from '@/scripts/collapsed.js'; +import { isEnabledUrlPreview } from '@/instance.js'; + +const props = withDefaults(defineProps<{ + note: Misskey.entities.Note; + pinned?: boolean; + mock?: boolean; + withHardMute?: boolean; +}>(), { + mock: false, +}); + +provide('mock', props.mock); + +const emit = defineEmits<{ + (ev: 'reaction', emoji: string): void; + (ev: 'removeReaction', emoji: string): void; +}>(); + +const inTimeline = inject<boolean>('inTimeline', false); +const inChannel = inject('inChannel', null); +const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); + +const note = ref(deepClone(props.note)); + +// plugin +if (noteViewInterruptors.length > 0) { + onMounted(async () => { + let result: Misskey.entities.Note | null = deepClone(note.value); + for (const interruptor of noteViewInterruptors) { + try { + result = await interruptor.handler(result!) as Misskey.entities.Note | null; + if (result === null) { + isDeleted.value = true; + return; + } + } catch (err) { + console.error(err); + } + } + note.value = result as Misskey.entities.Note; + }); +} + +const isRenote = ( + note.value.renote != null && + note.value.reply == null && + note.value.text == null && + note.value.cw == null && + note.value.fileIds && note.value.fileIds.length === 0 && + note.value.poll == null +); + +const rootEl = shallowRef<HTMLElement>(); +const renoteButton = shallowRef<HTMLElement>(); +const renoteTime = shallowRef<HTMLElement>(); +const reactButton = shallowRef<HTMLElement>(); +const clipButton = shallowRef<HTMLElement>(); +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const isMyRenote = $i && ($i.id === note.value.userId); +const showContent = ref(false); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); +const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null); +const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); +const collapsed = ref(appearNote.value.cw == null && isLong); +const isDeleted = ref(false); +const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); +const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true)); +const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); +const translating = ref(false); +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); +const isFavorite = ref(false); +let statePromise; +if ($i) { + statePromise = misskeyApi('notes/state', { + noteId: props.note.id, + }); + statePromise.then((state) => { + isFavorite.value = state.isFavorited; + }); + +} +const renoteCollapsed = ref( + defaultStore.state.collapseRenotes && isRenote && ( + ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 + (appearNote.value.myReaction != null) + ), +); + +/* Overload FunctionにLintが対応していないのでコメントアウト +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; +*/ +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { + if (mutedWords == null) return false; + + if (checkWordMute(noteToCheck, $i, mutedWords)) return true; + if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; + if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + + if (checkOnly) return false; + + if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; + return false; +} + +const keymap = { + 'r': () => reply(true), + 'e|a|plus': () => react(true), + 'q': () => renote(true), + 'up|k|shift+tab': focusBefore, + 'down|j|tab': focusAfter, + 'esc': blur, + 's': () => showContent.value !== showContent.value, +}; + +function toggleFavorite(): void { + if ($i == null) { + pleaseLogin(); + return; + } + claimAchievement('noteFavorited1'); + os.apiWithDialog(!isFavorite.value ? 'notes/favorites/create' : 'notes/favorites/delete', { + noteId: props.note.id, + }); + isFavorite.value = !isFavorite.value; +} +provide('react', (reaction: string) => { + misskeyApi('notes/reactions/create', { + noteId: appearNote.value.id, + reaction: reaction, + }); +}); + +if (props.mock) { + watch(() => props.note, (to) => { + note.value = deepClone(to); + }, { deep: true }); +} else { + useNoteCapture({ + rootEl: rootEl, + note: appearNote, + pureNote: note, + isDeletedRef: isDeleted, + }); +} + +if (!props.mock) { + useTooltip(renoteButton, async (showing) => { + const renotes = await misskeyApi('notes/renotes', { + noteId: appearNote.value.id, + limit: 11, + }); + + const users = renotes.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.value.renoteCount, + targetElement: renoteButton.value, + }, {}, 'closed'); + }); + + if (appearNote.value.reactionAcceptance === 'likeOnly') { + useTooltip(reactButton, async (showing) => { + const reactions = await misskeyApiGet('notes/reactions', { + noteId: appearNote.value.id, + limit: 10, + _cacheKey_: appearNote.value.reactionCount, + }); + + const users = reactions.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkReactionsViewerDetails, { + showing, + reaction: '❤️', + users, + count: appearNote.value.reactionCount, + targetElement: reactButton.value!, + }, {}, 'closed'); + }); + } +} + +function renote(viaKeyboard = false) { + pleaseLogin(); + showMovedDialog(); + + const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock }); + os.popupMenu(menu, renoteButton.value, { + viaKeyboard, + }); +} + +function reply(viaKeyboard = false): void { + pleaseLogin(); + if (props.mock) { + return; + } + os.post({ + reply: appearNote.value, + channel: appearNote.value.channel, + animation: !viaKeyboard, + }).then(() => { + focus(); + }); +} + +function react(viaKeyboard = false): void { + pleaseLogin(); + showMovedDialog(); + if (appearNote.value.reactionAcceptance === 'likeOnly') { + sound.playMisskeySfx('reaction'); + + if (props.mock) { + return; + } + + misskeyApi('notes/reactions/create', { + noteId: appearNote.value.id, + reaction: '❤️', + }); + const el = reactButton.value; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + } else { + blur(); + reactionPicker.show(reactButton.value ?? null, note.value, reaction => { + sound.playMisskeySfx('reaction'); + + if (props.mock) { + emit('reaction', reaction); + return; + } + + misskeyApi('notes/reactions/create', { + noteId: appearNote.value.id, + reaction: reaction, + }); + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } + }, () => { + focus(); + }); + } +} + +function undoReact(targetNote: Misskey.entities.Note): void { + const oldReaction = targetNote.myReaction; + if (!oldReaction) return; + + if (props.mock) { + emit('removeReaction', oldReaction); + return; + } + + misskeyApi('notes/reactions/delete', { + noteId: targetNote.id, + }); +} + +function toggleReact() { + if (appearNote.value.myReaction == null) { + react(); + } else { + undoReact(appearNote.value); + } +} + +function onContextmenu(ev: MouseEvent): void { + if (props.mock) { + return; + } + + const isLink = (el: HTMLElement): boolean => { + if (el.tagName === 'A') return true; + // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。 + if (el.tagName === 'AUDIO') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; + }; + + if (ev.target && isLink(ev.target as HTMLElement)) return; + if (window.getSelection()?.toString() !== '') return; + + if (defaultStore.state.useReactionPickerForContextMenu) { + ev.preventDefault(); + react(); + } else { + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); + os.contextMenu(menu, ev).then(focus).finally(cleanup); + } +} + +function showMenu(viaKeyboard = false): void { + if (props.mock) { + return; + } + + const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); + os.popupMenu(menu, menuButton.value, { + viaKeyboard, + }).then(focus).finally(cleanup); +} + +async function clip() { + if (props.mock) { + return; + } + + os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); +} + +function showRenoteMenu(viaKeyboard = false): void { + if (props.mock) { + return; + } + + function getUnrenote(): MenuItem { + return { + text: i18n.ts.unrenote, + icon: 'ti ti-trash', + danger: true, + action: () => { + misskeyApi('notes/delete', { + noteId: note.value.id, + }); + isDeleted.value = true; + }, + }; + } + + if (isMyRenote) { + pleaseLogin(); + os.popupMenu([ + getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + { type: 'divider' }, + getUnrenote(), + ], renoteTime.value, { + viaKeyboard: viaKeyboard, + }); + } else { + os.popupMenu([ + getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + { type: 'divider' }, + getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), + ($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined, + ], renoteTime.value, { + viaKeyboard: viaKeyboard, + }); + } +} + +function focus() { + rootEl.value?.focus(); +} + +function blur() { + rootEl.value?.blur(); +} + +function focusBefore() { + focusPrev(rootEl.value ?? null); +} + +function focusAfter() { + focusNext(rootEl.value ?? null); +} + +function readPromo() { + misskeyApi('promo/read', { + noteId: appearNote.value.id, + }); + isDeleted.value = true; +} + +function emitUpdReaction(emoji: string, delta: number) { + if (delta < 0) { + emit('removeReaction', emoji); + } else if (delta > 0) { + emit('reaction', emoji); + } +} +</script> + +<style lang="scss" module> +.root { + position: relative; + transition: box-shadow 0.1s ease; + font-size: 1.05em; + overflow: clip; + contain: content; + + // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 + // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう + // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、 + // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる + // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?) + //content-visibility: auto; + //contain-intrinsic-size: 0 128px; + + &:focus-visible { + outline: none; + + &:after { + content: ""; + pointer-events: none; + display: block; + position: absolute; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: calc(100% - 8px); + height: calc(100% - 8px); + border: dashed 1px var(--focus); + border-radius: var(--radius); + box-sizing: border-box; + } + } + + .footer { + display: flex; + font-size: 1em; + justify-content: flex-start; + } + + .footer { + display: flex; + font-size: 1em; + justify-content: flex-start; + margin-left: -64px; + } + + .footerLeft { + flex: 1; + display: grid; + grid-auto-flow: column; + grid-template-columns: repeat(auto-fit, minmax(42px, 1fr)); + grid-auto-rows: 32px; + } + + .footerRight { + flex: 0; + display: grid; + grid-auto-flow: column; + grid-template-columns: repeat(auto-fit, minmax(42px, 1fr)); + grid-auto-rows: 32px; + direction: rtl; + } + + &:hover > .article > .main > .footer > .footerButton { + opacity: 1; + } + + &.showActionsOnlyHover { + .footer { + visibility: hidden; + position: absolute; + top: 12px; + right: 12px; + padding: 0 4px; + margin-bottom: 0 !important; + background: var(--popup); + border-radius: 8px; + box-shadow: 0px 4px 32px var(--shadow); + } + + .footerButton { + font-size: 90%; + + &:not(:last-child) { + margin-right: 0; + } + } + } + + &.showActionsOnlyHover:hover { + .footer { + visibility: visible; + } + } +} + +.tip { + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 24px; + font-size: 90%; + white-space: pre; + color: #d28a3f; +} + +.tip + .article { + padding-top: 8px; +} + +.replyTo { + opacity: 0.7; + padding-bottom: 0; +} + +.renote { + position: relative; + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 28px; + white-space: pre; + color: var(--renote); + + & + .article { + padding-top: 8px; + } + + > .colorBar { + height: calc(100% - 6px); + } +} + +.renoteAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; +} + +.renoteText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.renoteUserName { + font-weight: bold; +} + +.renoteInfo { + margin-left: auto; + font-size: 0.9em; +} + +.renoteTime { + flex-shrink: 0; + color: inherit; +} + +.renoteMenu { + margin-right: 4px; +} + +.collapsedRenoteTarget { + display: flex; + align-items: center; + line-height: 28px; + white-space: pre; + padding: 0 32px 18px; +} + +.collapsedRenoteTargetAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; +} + +.collapsedRenoteTargetText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 90%; + opacity: 0.7; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.article { + position: relative; + display: flex; + padding: 16px 24px; +} + +.colorBar { + position: absolute; + top: 8px; + left: 8px; + width: 5px; + height: calc(100% - 16px); + border-radius: 999px; + pointer-events: none; +} + +.avatar { + flex-shrink: 0; + display: block !important; + margin: 0 14px 0 0; + width: 58px; + height: 58px; + position: sticky !important; + top: calc(22px + var(--stickyTop, 0px)); + left: 0; +} + +.main { + flex: 1; + min-width: 0; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.showLess { + width: 100%; + margin-top: 14px; + position: sticky; + bottom: calc(var(--stickyBottom, 0px) + 14px); +} + +.showLessLabel { + display: inline-block; + background: var(--popup); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.contentCollapsed { + position: relative; + max-height: 9em; + overflow: clip; +} + +.collapsed { + display: block; + position: absolute; + bottom: 0; + left: 0; + z-index: 2; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), var(--X15)); + + &:hover > .collapsedLabel { + background: var(--panelHighlight); + } +} + +.collapsedLabel { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.text { + overflow-wrap: break-word; +} + +.replyIcon { + color: var(--accent); + margin-right: 0.5em; +} + +.translation { + border: solid 0.5px var(--divider); + border-radius: var(--radius); + padding: 12px; + margin-top: 8px; +} + +.urlPreview { + margin-top: 8px; +} + +.poll { + font-size: 80%; +} + +.quote { + padding: 8px 0; +} + +.quoteNote { + padding: 16px; + border: dashed 1px var(--renote); + border-radius: 8px; + overflow: clip; +} + +.channel { + opacity: 0.7; + font-size: 80%; +} + +.footer { + margin-bottom: -14px; +} + +.footerButton { + margin: 0; + + opacity: 0.7; + + &:hover { + color: var(--fgHighlighted); + } +} + +.footerButtonCount { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; +} + +@container (max-width: 580px) { + .root { + font-size: 0.95em; + } + + .renote { + padding: 12px 26px 0 26px; + } + + .article { + padding: 24px 26px; + } + + .avatar { + width: 42px; + height: 42px; + } +} + +@container (max-width: 500px) { + .root { + font-size: 0.9em; + } + + .renote { + padding: 10px 22px 0 22px; + } + + .article { + padding: 20px 22px; + } + + .footer { + margin-bottom: -8px; + } +} + +@container (max-width: 480px) { + .renote { + padding: 8px 16px 0 16px; + } + + .tip { + padding: 8px 16px 0 16px; + } + + .collapsedRenoteTarget { + padding: 0 16px 9px; + margin-top: 4px; + } + + .article { + padding: 14px 16px; + } +} + +@container (max-width: 450px) { + .avatar { + margin: 0 10px 0 0; + width: 46px; + height: 46px; + top: calc(14px + var(--stickyTop, 0px)); + } +} + +@container (max-width: 400px) { + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 18px; + } + } + } +} + +@container (max-width: 350px) { + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 12px; + } + } + } + + .colorBar { + top: 6px; + left: 6px; + width: 4px; + height: calc(100% - 12px); + } +} + +@container (max-width: 300px) { + .avatar { + width: 44px; + height: 44px; + } + + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 8px; + } + } + } +} + +@container (max-width: 250px) { + .quoteNote { + padding: 12px; + } +} + +.muted { + padding: 8px; + text-align: center; + opacity: 0.7; +} + +.reactionOmitted { + display: inline-block; + margin-left: 8px; + opacity: .8; + font-size: 95%; +} +</style> diff --git a/packages/frontend/src/components/XNoteHeader.vue b/packages/frontend/src/components/XNoteHeader.vue new file mode 100644 index 0000000000..a38604382b --- /dev/null +++ b/packages/frontend/src/components/XNoteHeader.vue @@ -0,0 +1,137 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<header :class="$style.root"> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + <div v-if="note.user.isBot" :class="$style.isBot">bot</div> + <div :class="$style.username"><MkAcct :user="note.user"/></div> + <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> + <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> + </div> + <div v-if="mock"> + <MkTime :time="note.createdAt" colored/> + </div> + <MkA v-else :class="$style.time" :to="notePage(note)"> + <MkTime :time="note.createdAt" colored/> + </MkA> + <div :class="$style.info"> + <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()"> + <i class="ti ti-dots"></i> + </button> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> + </div> +</header> +</template> + +<script lang="ts" setup> +import { inject, shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; +import { notePage } from '@/filters/note.js'; +import { userPage } from '@/filters/user.js'; +import { getNoteMenu } from '@/scripts/get-note-menu.js'; +import * as os from '@/os.js'; +const menuButton = shallowRef<HTMLElement>(); + +const props = defineProps<{ + note: Misskey.entities.Note; +}>(); + +function showMenu(viaKeyboard = false): void { + if (mock) { + return; + } + + const { menu, cleanup } = getNoteMenu({ note: props.note }); + os.popupMenu(menu, menuButton.value, { + viaKeyboard, + }).then(focus).finally(cleanup); +} + +const mock = inject<boolean>('mock', false); +</script> + +<style lang="scss" module> +.root { + display: flex; + align-items: baseline; + white-space: nowrap; +} +.footerButton { + margin: -12px 0 0; + opacity: 0.7; + + &:hover { + color: var(--fgHighlighted); + } +} +.name { + flex-shrink: 1; + display: block; + margin: 0 .5em 0 0; + padding: 0; + overflow: hidden; + font-size: 1em; + font-weight: bold; + text-decoration: none; + text-overflow: ellipsis; + + &:hover { + text-decoration: underline; + } +} + +.isBot { + flex-shrink: 0; + align-self: center; + margin: 0 .5em 0 0; + padding: 1px 6px; + font-size: 80%; + border: solid 0.5px var(--divider); + border-radius: 3px; +} + +.username { + flex-shrink: 9999999; + margin: 0 .5em 0 0; + overflow: hidden; + text-overflow: ellipsis; + color: var(--fgTransparentWeak); +} + +.info { + flex-shrink: 0; + margin-left: auto; + +} + +.badgeRoles { + margin: 0 .5em 0 0; +} + +.badgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .badgeRole { + margin-left: 0.2em; + } +} +.time{ + color: var(--fgTransparentWeak); +} +</style> diff --git a/packages/frontend/src/components/XPostForm.vue b/packages/frontend/src/components/XPostForm.vue new file mode 100644 index 0000000000..030ccf6318 --- /dev/null +++ b/packages/frontend/src/components/XPostForm.vue @@ -0,0 +1,1349 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + :style="!dialog ? { borderBottom: 'solid 1px var(--divider)' } : {}" + :class="[$style.root, { [$style.modal]: modal, _popup: modal }]" + @dragover.stop="onDragover" + @dragenter="onDragenter" + @dragleave="onDragleave" + @drop.stop="onDrop" +> + <header v-if="!fixed" :class="$style.header"> + <button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button> + </header> + <MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/> + <MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/> + <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> + <div v-if="visibility === 'specified'" :class="$style.toSpecified"> + <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> + <div :class="$style.visibleUsers"> + <span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser"> + <MkAcct :user="u"/> + <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> + </span> + <button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> + </div> + </div> + <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> + <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> + <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> + <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> + + <div style="display: flex;"> + <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu"> + <MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/> + </button> + + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + </div> + <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> + </div> + <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> + <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> + <MkPollEditor v-if="poll" v-model="poll" style="margin: 8px 32px; padding: 8px; border: solid 0.5px var(--divider); border-radius: 16px;" @destroyed="poll = null"/> + <div v-if="showingOptions" style="padding: 8px 16px;"> + </div> + + <footer :style="dialog ? { borderTop: 'solid 1px var(--divider)' , padding: ' 8px'} : {padding: '0 16px 16px 64px'}" :class="$style.footer"> + <div :class="$style.footerLeft"> + <button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> + <button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button> + <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> + <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button> + + <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button> + <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> + <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> + <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button> + </div> + <div :class="$style.footerRight"> + <button v-click-anime class="_button" :class="[$style.footerButton, $style.submit]" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> + <div :class="$style.submitInner"> + <template v-if="posted"></template> + <template v-else-if="posting"><MkEllipsis/></template> + <template v-else>{{ submitText }}</template> + <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i> + </div> + </button> + </div> + </footer> + <datalist id="hashtags"> + <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> + </datalist> +</div> +</template> + +<script lang="ts" setup> +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue'; +import * as mfm from 'mfm-js'; +import * as Misskey from 'misskey-js'; +import insertTextAtCursor from 'insert-text-at-cursor'; +import { toASCII } from 'punycode/'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import MkNotePreview from '@/components/MkNotePreview.vue'; +import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; +import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue'; +import { host, url } from '@/config.js'; +import { erase, unique } from '@/scripts/array.js'; +import { extractMentions } from '@/scripts/extract-mentions.js'; +import { formatTimeString } from '@/scripts/format-time-string.js'; +import { Autocomplete } from '@/scripts/autocomplete.js'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { selectFiles } from '@/scripts/select-file.js'; +import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js'; +import MkInfo from '@/components/MkInfo.vue'; +import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; +import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js'; +import { uploadFile } from '@/scripts/upload.js'; +import { deepClone } from '@/scripts/clone.js'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { miLocalStorage } from '@/local-storage.js'; +import { claimAchievement } from '@/scripts/achievements.js'; +import { emojiPicker } from '@/scripts/emoji-picker.js'; +import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; + +const $i = signinRequired(); + +const modal = inject('modal'); + +const props = withDefaults(defineProps<{ + reply?: Misskey.entities.Note; + renote?: Misskey.entities.Note; + channel?: Misskey.entities.Channel; // TODO + mention?: Misskey.entities.User; + specified?: Misskey.entities.UserDetailed; + initialText?: string; + initialCw?: string; + initialVisibility?: (typeof Misskey.noteVisibilities)[number]; + initialFiles?: Misskey.entities.DriveFile[]; + initialLocalOnly?: boolean; + initialVisibleUsers?: Misskey.entities.UserDetailed[]; + initialNote?: Misskey.entities.Note; + instant?: boolean; + fixed?: boolean; + autofocus?: boolean; + freezeAfterPosted?: boolean; + mock?: boolean; + dialog?: boolean; +}>(), { + initialVisibleUsers: () => [], + autofocus: true, + mock: false, + initialLocalOnly: undefined, +}); + +provide('mock', props.mock); + +const emit = defineEmits<{ + (ev: 'posted'): void; + (ev: 'cancel'): void; + (ev: 'esc'): void; + + // Mock用 + (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; +}>(); + +const textareaEl = shallowRef<HTMLTextAreaElement | null>(null); +const cwInputEl = shallowRef<HTMLInputElement | null>(null); +const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null); +const visibilityButton = shallowRef<HTMLElement>(); + +const posting = ref(false); +const posted = ref(false); +const text = ref(props.initialText ?? ''); +const files = ref(props.initialFiles ?? []); +const poll = ref<PollEditorModelValue | null>(null); +const useCw = ref<boolean>(!!props.initialCw); +const showPreview = ref(defaultStore.state.showPreview); +watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); +const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction); +watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value)); +const cw = ref<string | null>(props.initialCw ?? null); +const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly)); +const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility)); +const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]); +if (props.initialVisibleUsers) { + props.initialVisibleUsers.forEach(u => pushVisibleUser(u)); +} +const reactionAcceptance = ref(defaultStore.state.reactionAcceptance); +const autocomplete = ref(null); +const draghover = ref(false); +const quoteId = ref<string | null>(null); +const hasNotSpecifiedMentions = ref(false); +const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); +const imeText = ref(''); +const showingOptions = ref(false); +const textAreaReadOnly = ref(false); + +const draftKey = computed((): string => { + let key = props.channel ? `channel:${props.channel.id}` : ''; + + if (props.renote) { + key += `renote:${props.renote.id}`; + } else if (props.reply) { + key += `reply:${props.reply.id}`; + } else { + key += `note:${$i.id}`; + } + + return key; +}); + +const placeholder = computed((): string => { + if (props.renote) { + return i18n.ts._postForm.quotePlaceholder; + } else if (props.reply) { + return i18n.ts._postForm.replyPlaceholder; + } else if (props.channel) { + return i18n.ts._postForm.channelPlaceholder; + } else { + const xs = [ + i18n.ts._postForm._placeholders.a, + i18n.ts._postForm._placeholders.b, + i18n.ts._postForm._placeholders.c, + i18n.ts._postForm._placeholders.d, + i18n.ts._postForm._placeholders.e, + i18n.ts._postForm._placeholders.f, + ]; + return xs[Math.floor(Math.random() * xs.length)]; + } +}); + +const submitText = computed((): string => { + return props.renote + ? i18n.ts.quote + : props.reply + ? i18n.ts.reply + : i18n.ts.note; +}); + +const textLength = computed((): number => { + return (text.value + imeText.value).trim().length; +}); + +const maxTextLength = computed((): number => { + return instance ? instance.maxNoteTextLength : 1000; +}); + +const canPost = computed((): boolean => { + return !props.mock && !posting.value && !posted.value && + ( + 1 <= textLength.value || + 1 <= files.value.length || + poll.value != null || + props.renote != null || + (props.reply != null && quoteId.value != null) + ) && + (textLength.value <= maxTextLength.value) && + (!poll.value || poll.value.choices.length >= 2); +}); + +const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags')); +const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags')); + +watch(text, () => { + checkMissingMention(); +}, { immediate: true }); + +watch(visibility, () => { + checkMissingMention(); +}, { immediate: true }); + +watch(visibleUsers, () => { + checkMissingMention(); +}, { + deep: true, +}); + +if (props.mention) { + text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; + text.value += ' '; +} + +if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { + text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; +} + +if (props.reply && props.reply.text != null) { + const ast = mfm.parse(props.reply.text); + const otherHost = props.reply.user.host; + + for (const x of extractMentions(ast)) { + const mention = x.host ? + `@${x.username}@${toASCII(x.host)}` : + (otherHost == null || otherHost === host) ? + `@${x.username}` : + `@${x.username}@${toASCII(otherHost)}`; + + // 自分は除外 + if ($i.username === x.username && (x.host == null || x.host === host)) continue; + + // 重複は除外 + if (text.value.includes(`${mention} `)) continue; + + text.value += `${mention} `; + } +} + +if ($i.isSilenced && visibility.value === 'public') { + visibility.value = 'home'; +} + +if (props.channel) { + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す +} + +// 公開以外へのリプライ時は元の公開範囲を引き継ぐ +if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { + if (props.reply.visibility === 'home' && visibility.value === 'followers') { + visibility.value = 'followers'; + } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') { + visibility.value = 'specified'; + } else { + visibility.value = props.reply.visibility; + } + + if (visibility.value === 'specified') { + if (props.reply.visibleUserIds) { + misskeyApi('users/show', { + userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId), + }).then(users => { + users.forEach(u => pushVisibleUser(u)); + }); + } + + if (props.reply.userId !== $i.id) { + misskeyApi('users/show', { userId: props.reply.userId }).then(user => { + pushVisibleUser(user); + }); + } + } +} + +if (props.specified) { + visibility.value = 'specified'; + pushVisibleUser(props.specified); +} + +// keep cw when reply +if (defaultStore.state.keepCw && props.reply && props.reply.cw) { + useCw.value = true; + cw.value = props.reply.cw; +} + +function watchForDraft() { + watch(text, () => saveDraft()); + watch(useCw, () => saveDraft()); + watch(cw, () => saveDraft()); + watch(poll, () => saveDraft()); + watch(files, () => saveDraft(), { deep: true }); + watch(visibility, () => saveDraft()); + watch(localOnly, () => saveDraft()); +} + +function checkMissingMention() { + if (visibility.value === 'specified') { + const ast = mfm.parse(text.value); + + for (const x of extractMentions(ast)) { + if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) { + hasNotSpecifiedMentions.value = true; + return; + } + } + } + hasNotSpecifiedMentions.value = false; +} + +function addMissingMention() { + const ast = mfm.parse(text.value); + + for (const x of extractMentions(ast)) { + if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) { + misskeyApi('users/show', { username: x.username, host: x.host }).then(user => { + pushVisibleUser(user); + }); + } + } +} + +function togglePoll() { + if (poll.value) { + poll.value = null; + } else { + poll.value = { + choices: ['', ''], + multiple: false, + expiresAt: null, + expiredAfter: null, + }; + } +} + +function addTag(tag: string) { + insertTextAtCursor(textareaEl.value, ` #${tag} `); +} + +function focus() { + if (textareaEl.value) { + textareaEl.value.focus(); + textareaEl.value.setSelectionRange(textareaEl.value.value.length, textareaEl.value.value.length); + } +} + +function chooseFileFrom(ev) { + if (props.mock) return; + + selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { + for (const file of files_) { + files.value.push(file); + } + }); +} + +function detachFile(id) { + files.value = files.value.filter(x => x.id !== id); +} + +function updateFileSensitive(file, sensitive) { + if (props.mock) { + emit('fileChangeSensitive', file.id, sensitive); + } + files.value[files.value.findIndex(x => x.id === file.id)].isSensitive = sensitive; +} + +function updateFileName(file, name) { + files.value[files.value.findIndex(x => x.id === file.id)].name = name; +} + +function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void { + files.value[files.value.findIndex(x => x.id === file.id)] = newFile; +} + +function upload(file: File, name?: string): void { + if (props.mock) return; + + uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { + files.value.push(res); + }); +} + +function setVisibility() { + if (props.channel) { + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す + return; + } + + os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { + currentVisibility: visibility.value, + isSilenced: $i.isSilenced, + localOnly: localOnly.value, + src: visibilityButton.value, + ...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}), + }, { + changeVisibility: v => { + visibility.value = v; + if (defaultStore.state.rememberNoteVisibility) { + defaultStore.set('visibility', visibility.value); + } + }, + }, 'closed'); +} + +async function toggleLocalOnly() { + if (props.channel) { + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す + return; + } + + const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo'); + + if (!localOnly.value && neverShowInfo !== 'true') { + const confirm = await os.actions({ + type: 'question', + title: i18n.ts.disableFederationConfirm, + text: i18n.ts.disableFederationConfirmWarn, + actions: [ + { + value: 'yes' as const, + text: i18n.ts.disableFederationOk, + primary: true, + }, + { + value: 'neverShow' as const, + text: `${i18n.ts.disableFederationOk} (${i18n.ts.neverShow})`, + danger: true, + }, + { + value: 'no' as const, + text: i18n.ts.cancel, + }, + ], + }); + if (confirm.canceled) return; + if (confirm.result === 'no') return; + + if (confirm.result === 'neverShow') { + miLocalStorage.setItem('neverShowLocalOnlyInfo', 'true'); + } + } + + localOnly.value = !localOnly.value; + if (defaultStore.state.rememberNoteVisibility) { + defaultStore.set('localOnly', localOnly.value); + } +} + +async function toggleReactionAcceptance() { + const select = await os.select({ + title: i18n.ts.reactionAcceptance, + items: [ + { value: null, text: i18n.ts.all }, + { value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote }, + { value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly }, + { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }, + { value: 'likeOnly' as const, text: i18n.ts.likeOnly }, + ], + default: reactionAcceptance.value, + }); + if (select.canceled) return; + reactionAcceptance.value = select.result; +} + +function pushVisibleUser(user: Misskey.entities.UserDetailed) { + if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { + visibleUsers.value.push(user); + } +} + +function addVisibleUser() { + os.selectUser().then(user => { + pushVisibleUser(user); + + if (!text.value.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { + text.value = `@${Misskey.acct.toString(user)} ${text.value}`; + } + }); +} + +function removeVisibleUser(user) { + visibleUsers.value = erase(user, visibleUsers.value); +} + +function clear() { + text.value = ''; + files.value = []; + poll.value = null; + quoteId.value = null; +} + +function onKeydown(ev: KeyboardEvent) { + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); + if (ev.key === 'Escape') emit('esc'); +} + +function onCompositionUpdate(ev: CompositionEvent) { + imeText.value = ev.data; +} + +function onCompositionEnd(ev: CompositionEvent) { + imeText.value = ''; +} + +async function onPaste(ev: ClipboardEvent) { + if (props.mock) return; + if (!ev.clipboardData) return; + + for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) { + if (item.kind === 'file') { + const file = item.getAsFile(); + if (!file) continue; + const lio = file.name.lastIndexOf('.'); + const ext = lio >= 0 ? file.name.slice(lio) : ''; + const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; + upload(file, formatted); + } + } + + const paste = ev.clipboardData.getData('text'); + + if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) { + ev.preventDefault(); + + os.confirm({ + type: 'info', + text: i18n.ts.quoteQuestion, + }).then(({ canceled }) => { + if (canceled) { + insertTextAtCursor(textareaEl.value, paste); + return; + } + + quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null; + }); + } + + if (paste.length > 1000) { + ev.preventDefault(); + os.confirm({ + type: 'info', + text: i18n.ts.attachAsFileQuestion, + }).then(({ canceled }) => { + if (canceled) { + insertTextAtCursor(textareaEl.value, paste); + return; + } + + const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0'); + const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); + upload(file, `${fileName}.txt`); + }); + } +} + +function onDragover(ev) { + if (!ev.dataTransfer.items[0]) return; + const isFile = ev.dataTransfer.items[0].kind === 'file'; + const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; + if (isFile || isDriveFile) { + ev.preventDefault(); + draghover.value = true; + switch (ev.dataTransfer.effectAllowed) { + case 'all': + case 'uninitialized': + case 'copy': + case 'copyLink': + case 'copyMove': + ev.dataTransfer.dropEffect = 'copy'; + break; + case 'linkMove': + case 'move': + ev.dataTransfer.dropEffect = 'move'; + break; + default: + ev.dataTransfer.dropEffect = 'none'; + break; + } + } +} + +function onDragenter() { + draghover.value = true; +} + +function onDragleave() { + draghover.value = false; +} + +function onDrop(ev: DragEvent): void { + draghover.value = false; + + // ファイルだったら + if (ev.dataTransfer && ev.dataTransfer.files.length > 0) { + ev.preventDefault(); + for (const x of Array.from(ev.dataTransfer.files)) upload(x); + return; + } + + //#region ドライブのファイル + const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile !== '') { + const file = JSON.parse(driveFile); + files.value.push(file); + ev.preventDefault(); + } + //#endregion +} + +function saveDraft() { + if (props.instant || props.mock) return; + + const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); + + draftData[draftKey.value] = { + updatedAt: new Date(), + data: { + text: text.value, + useCw: useCw.value, + cw: cw.value, + visibility: visibility.value, + localOnly: localOnly.value, + files: files.value, + poll: poll.value, + visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined, + }, + }; + + miLocalStorage.setItem('drafts', JSON.stringify(draftData)); +} + +function deleteDraft() { + const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); + + delete draftData[draftKey.value]; + + miLocalStorage.setItem('drafts', JSON.stringify(draftData)); +} + +async function post(ev?: MouseEvent) { + if (useCw.value && (cw.value == null || cw.value.trim() === '')) { + os.alert({ + type: 'error', + text: i18n.ts.cwNotationRequired, + }); + return; + } + + if (ev) { + const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; + + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + } + + if (props.mock) return; + + const annoying = + text.value.includes('$[x2') || + text.value.includes('$[x3') || + text.value.includes('$[x4') || + text.value.includes('$[scale') || + text.value.includes('$[position'); + + if (annoying && visibility.value === 'public') { + const { canceled, result } = await os.actions({ + type: 'warning', + text: i18n.ts.thisPostMayBeAnnoying, + actions: [{ + value: 'home', + text: i18n.ts.thisPostMayBeAnnoyingHome, + primary: true, + }, { + value: 'cancel', + text: i18n.ts.thisPostMayBeAnnoyingCancel, + }, { + value: 'ignore', + text: i18n.ts.thisPostMayBeAnnoyingIgnore, + }], + }); + + if (canceled) return; + if (result === 'cancel') return; + if (result === 'home') { + visibility.value = 'home'; + } + } + + let postData = { + text: text.value === '' ? null : text.value, + fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, + replyId: props.reply ? props.reply.id : undefined, + renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, + channelId: props.channel ? props.channel.id : undefined, + poll: poll.value, + cw: useCw.value ? cw.value ?? '' : null, + localOnly: localOnly.value, + visibility: visibility.value, + visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, + reactionAcceptance: reactionAcceptance.value, + }; + + if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') { + const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); + if (!postData.text) { + postData.text = hashtags_; + } else { + const postTextLines = postData.text.split('\n'); + if (postTextLines[postTextLines.length - 1].trim() === '') { + postTextLines[postTextLines.length - 1] += hashtags_; + } else { + postTextLines[postTextLines.length - 1] += ' ' + hashtags_; + } + postData.text = postTextLines.join('\n'); + } + } + + // plugin + if (notePostInterruptors.length > 0) { + for (const interruptor of notePostInterruptors) { + try { + postData = await interruptor.handler(deepClone(postData)) as typeof postData; + } catch (err) { + console.error(err); + } + } + } + + let token: string | undefined = undefined; + + if (postAccount.value) { + const storedAccounts = await getAccounts(); + token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token; + } + + posting.value = true; + misskeyApi('notes/create', postData, token).then(() => { + if (props.freezeAfterPosted) { + posted.value = true; + } else { + clear(); + } + nextTick(() => { + deleteDraft(); + emit('posted'); + if (postData.text && postData.text !== '') { + const hashtags_ = mfm.parse(postData.text).map(x => x.type === 'hashtag' && x.props.hashtag).filter(x => x) as string[]; + const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[]; + miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); + } + posting.value = false; + postAccount.value = null; + + incNotesCount(); + if (notesCount === 1) { + claimAchievement('notes1'); + } + + const text = postData.text ?? ''; + const lowerCase = text.toLowerCase(); + if ((lowerCase.includes('love') || lowerCase.includes('❤')) && lowerCase.includes('misskey')) { + claimAchievement('iLoveMisskey'); + } + if ([ + 'https://youtu.be/Efrlqw8ytg4', + 'https://www.youtube.com/watch?v=Efrlqw8ytg4', + 'https://m.youtube.com/watch?v=Efrlqw8ytg4', + + 'https://youtu.be/XVCwzwxdHuA', + 'https://www.youtube.com/watch?v=XVCwzwxdHuA', + 'https://m.youtube.com/watch?v=XVCwzwxdHuA', + + 'https://open.spotify.com/track/3Cuj0mZrlLoXx9nydNi7RB', + 'https://open.spotify.com/track/7anfcaNPQWlWCwyCHmZqNy', + 'https://open.spotify.com/track/5Odr16TvEN4my22K9nbH7l', + 'https://open.spotify.com/album/5bOlxyl4igOrp2DwVQxBco', + ].some(url => text.includes(url))) { + claimAchievement('brainDiver'); + } + + if (props.renote && (props.renote.userId === $i.id) && text.length > 0) { + claimAchievement('selfQuote'); + } + + const date = new Date(); + const h = date.getHours(); + const m = date.getMinutes(); + const s = date.getSeconds(); + if (h >= 0 && h <= 3) { + claimAchievement('postedAtLateNight'); + } + if (m === 0 && s === 0) { + claimAchievement('postedAt0min0sec'); + } + }); + }).catch(err => { + posting.value = false; + os.alert({ + type: 'error', + text: err.message + '\n' + (err as any).id, + }); + }); +} + +function cancel() { + emit('cancel'); +} + +function insertMention() { + os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => { + insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); + }); +} + +async function insertEmoji(ev: MouseEvent) { + textAreaReadOnly.value = true; + const target = ev.currentTarget ?? ev.target; + if (target == null) return; + emojiPicker.show( + target as HTMLElement, + emoji => { + insertTextAtCursor(textareaEl.value, emoji); + }, + () => { + textAreaReadOnly.value = false; + nextTick(() => focus()); + }, + ); +} + +async function insertMfmFunction(ev: MouseEvent) { + if (textareaEl.value == null) return; + mfmFunctionPicker( + ev.currentTarget ?? ev.target, + textareaEl.value, + text, + ); +} + +function showActions(ev: MouseEvent) { + os.popupMenu(postFormActions.map(action => ({ + text: action.title, + action: () => { + action.handler({ + text: text.value, + cw: cw.value, + }, (key, value: any) => { + if (typeof key !== 'string') return; + if (key === 'text') { text.value = value; } + if (key === 'cw') { useCw.value = value !== null; cw.value = value; } + }); + }, + })), ev.currentTarget ?? ev.target); +} + +const postAccount = ref<Misskey.entities.UserDetailed | null>(null); + +function openAccountMenu(ev: MouseEvent) { + if (props.mock) return; + + openAccountMenu_({ + withExtraOperation: false, + includeCurrentAccount: true, + active: postAccount.value != null ? postAccount.value.id : $i.id, + onChoose: (account) => { + if (account.id === $i.id) { + postAccount.value = null; + } else { + postAccount.value = account; + } + }, + }, ev); +} + +onMounted(() => { + if (props.autofocus) { + focus(); + + nextTick(() => { + focus(); + }); + } + + // TODO: detach when unmount + if (textareaEl.value) new Autocomplete(textareaEl.value, text); + if (cwInputEl.value) new Autocomplete(cwInputEl.value, cw); + if (hashtagsInputEl.value) new Autocomplete(hashtagsInputEl.value, hashtags); + + nextTick(() => { + // 書きかけの投稿を復元 + if (!props.instant && !props.mention && !props.specified && !props.mock) { + const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value]; + if (draft) { + text.value = draft.data.text; + useCw.value = draft.data.useCw; + cw.value = draft.data.cw; + visibility.value = draft.data.visibility; + localOnly.value = draft.data.localOnly; + files.value = (draft.data.files || []).filter(draftFile => draftFile); + if (draft.data.poll) { + poll.value = draft.data.poll; + } + if (draft.data.visibleUserIds) { + misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => { + users.forEach(u => pushVisibleUser(u)); + }); + } + } + } + + // 削除して編集 + if (props.initialNote) { + const init = props.initialNote; + text.value = init.text ? init.text : ''; + files.value = init.files ?? []; + cw.value = init.cw ?? null; + useCw.value = init.cw != null; + if (init.poll) { + poll.value = { + choices: init.poll.choices.map(x => x.text), + multiple: init.poll.multiple, + expiresAt: init.poll.expiresAt ? (new Date(init.poll.expiresAt)).getTime() : null, + expiredAfter: null, + }; + } + visibility.value = init.visibility; + localOnly.value = init.localOnly ?? false; + quoteId.value = init.renote ? init.renote.id : null; + } + + nextTick(() => watchForDraft()); + }); +}); + +defineExpose({ + clear, +}); +</script> + +<style lang="scss" module> +.root { + position: relative; + container-type: inline-size; + &.modal { + width: 100%; + max-width: 520px; + } +} + +//#region header +.header { + z-index: 1000; + min-height: 50px; + display: flex; + flex-wrap: wrap; + align-content: center; + gap: 4px; +} + +.headerLeft { + display: flex; + flex: 0 1 100px; +} + +.cancel { + padding: 0; + font-size: 1em; + height: 100%; + flex: 0 1 50px; +} + +.account { + height: 100%; + margin-left: 16px; +} + +.avatar { + width: 36px; + height: 36px; + margin: auto; +} + +.headerRight { + display: flex; + min-height: 24px; + font-size: 0.9em; + flex-wrap: nowrap; + align-items: center; + margin-left: auto; + gap: 4px; + overflow: clip; + padding-left: 4px; +} + +.submit { + margin: 12px 12px 12px 6px; + vertical-align: bottom; + + &:disabled { + opacity: 0.7; + } + + &.posting { + cursor: wait; + } + + &:not(:disabled):hover { + > .inner { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } + } + + &:not(:disabled):active { + > .inner { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } + } +} + +.colorBar { + position: absolute; + top: 0px; + left: 12px; + width: 5px; + height: 100% ; + border-radius: 999px; + pointer-events: none; +} + +.submitInner { + padding: 0 12px; + line-height: 35px; + font-weight: bold; + border-radius: 6px; + min-width: 90px; + color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); +} + +.headerRightItem { + margin: 0; + padding: 8px; + border-radius: 6px; + + &:hover { + background: var(--X5); + } + + &:disabled { + background: none; + } + + &.danger { + color: #ff2a2a; + } +} + +.headerRightButtonText { + padding-left: 6px; +} + +.visibility { + overflow: clip; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 210px; + + &:enabled { + > .headerRightButtonText { + opacity: 0.8; + } + } +} +//#endregion + +.preview { + padding: 16px 20px 0 20px; + min-height: 75px; + max-height: 150px; + overflow: auto; +} + +.targetNote { + padding: 0 20px 16px 20px; +} + +.withQuote { + margin: 0 0 8px 0; + color: var(--accent); +} + +.toSpecified { + padding: 6px 24px; + margin-bottom: 8px; + overflow: auto; + white-space: nowrap; +} + +.visibleUsers { + display: inline; + top: -1px; + font-size: 14px; +} + +.visibleUser { + margin-right: 14px; + padding: 8px 0 8px 8px; + border-radius: 8px; + background: var(--X4); +} + +.hasNotSpecifiedMentions { + margin: 0 20px 16px 20px; +} + +.cw, +.hashtags, +.text { + display: block; + box-sizing: border-box; + padding: 0 24px; + margin: 0; + width: 100%; + font-size: 16px; + border: none; + border-radius: 0; + background: transparent; + color: var(--fg); + font-family: inherit; + + &:focus { + outline: none; + } + + &:disabled { + opacity: 0.5; + } +} + +.cw { + z-index: 1; + padding-bottom: 8px; + border-bottom: solid 0.5px var(--divider); +} + +.hashtags { + z-index: 1; + padding-top: 8px; + padding-bottom: 8px; + border-top: solid 0.5px var(--divider); +} + +.textOuter { + width: 100%; + position: relative; + + &.withCw { + padding-top: 8px; + } +} + +.text { + max-width: 92%; + min-width: 92%; + width: 92%; + min-height: 50px; + margin-top: 8px; + height: 100%; +} + +.textCount { + position: absolute; + top: 0; + right: 2px; + padding: 4px 6px; + font-size: .9em; + color: var(--warn); + border-radius: 6px; + min-width: 1.6em; + text-align: center; + + &.textOver { + color: #ff2a2a; + } +} + +.footer { + display: flex; + font-size: 1em; + justify-content: center; + align-items: center; +} + +.footerLeft { + flex: 1; + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fill, minmax(35px, 1fr)); + grid-auto-rows: 20px; +} + +.footerRight { + flex: 0; + margin-left: auto; + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + grid-auto-rows: 35px; + place-items: center; + direction: rtl; +} + +.footerButton { + display: inline-block; + padding: 0; + margin: 0; + font-size: 1em; + width: auto; + height: 100%; + border-radius: 6px; + + &:hover { + background: var(--X5); + } + + &.footerButtonActive { + color: var(--accent); + } +} + +.previewButtonActive { + color: var(--accent); +} + +@container (max-width: 500px) { + .headerRight { + font-size: .9em; + } + + .headerRightButtonText { + display: none; + } + + .visibility { + overflow: initial; + } + + .submit { + margin: 8px 8px 8px 4px; + } + + .toSpecified { + padding: 6px 16px; + } + + .preview { + padding: 16px 14px 0 14px; + } + .cw, + .hashtags, + .text { + padding: 0 16px; + } + + .text { + min-height: 80px; + } + + .footer { + padding: 0 8px 8px 8px; + } +} + +@container (max-width: 350px) { + .footer { + font-size: 0.9em; + } + + .footerLeft { + grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); + } + + .headerRight { + gap: 0; + } + +} +</style> diff --git a/packages/frontend/src/components/XPostFormDialog.vue b/packages/frontend/src/components/XPostFormDialog.vue new file mode 100644 index 0000000000..a6cfa7449d --- /dev/null +++ b/packages/frontend/src/components/XPostFormDialog.vue @@ -0,0 +1,62 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()"> + <XPostForm ref="form" :class="$style.form" :dialog="true" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/> +</MkModal> +</template> + +<script lang="ts" setup> +import { shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkModal from '@/components/MkModal.vue'; +import MkPostForm from '@/components/MkPostForm.vue'; +import XPostForm from '@/components/XPostForm.vue'; + +const props = withDefaults(defineProps<{ + reply?: Misskey.entities.Note; + renote?: Misskey.entities.Note; + channel?: any; // TODO + mention?: Misskey.entities.User; + specified?: Misskey.entities.UserDetailed; + initialText?: string; + initialCw?: string; + initialVisibility?: (typeof Misskey.noteVisibilities)[number]; + initialFiles?: Misskey.entities.DriveFile[]; + initialLocalOnly?: boolean; + initialVisibleUsers?: Misskey.entities.UserDetailed[]; + initialNote?: Misskey.entities.Note; + instant?: boolean; + fixed?: boolean; + autofocus?: boolean; +}>(), { + initialLocalOnly: undefined, +}); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); + +const modal = shallowRef<InstanceType<typeof MkModal>>(); +const form = shallowRef<InstanceType<typeof MkPostForm>>(); + +function onPosted() { + modal.value?.close({ + useSendAnimation: true, + }); +} + +function onModalClosed() { + emit('closed'); +} +</script> + +<style lang="scss" module> +.form { + max-height: 100%; + margin: 0 auto auto auto; +} +</style> diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 490f24ef36..6a85bc639c 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> - <MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/> + <MkImgWithBlurhash :class="$style.inner" :src="url" :hash="defaultStore.state.enableUltimateDataSaverMode ? null:user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <div v-if="user.isCat" :class="[$style.ears]"> <div :class="$style.earLeft"> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index ab5330e2b6..87aaa787ed 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -9,7 +9,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License src="/client-assets/dummy.png" :title="alt" /> -<span v-else-if="errored">:{{ customEmojiName }}:</span> +<span v-else-if="errored || isDraft">:{{ customEmojiName }}:</span> <img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" @@ -51,6 +51,7 @@ const react = inject<((name: string) => void) | null>('react', null); const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); +const isDraft = computed(() => customEmojisMap.get(customEmojiName.value)?.draft ?? false); const rawUrl = computed(() => { if (props.url) { @@ -64,13 +65,20 @@ const rawUrl = computed(() => { const url = computed(() => { if (rawUrl.value == null) return undefined; - + const useOriginalSize = props.useOriginalSize; + const enableDataSaverMode = defaultStore.state.enableUltimateDataSaverMode; + let datasaver_result ; + if (enableDataSaverMode) { + datasaver_result = useOriginalSize ? undefined : 'datasaver'; + } else { + datasaver_result = useOriginalSize ? undefined : 'emoji'; + } const proxied = (rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value)) ? rawUrl.value : getProxiedImageUrl( rawUrl.value, - props.useOriginalSize ? undefined : 'emoji', + datasaver_result, false, true, ); @@ -123,7 +131,8 @@ function onClick(ev: MouseEvent) { height: 2em; vertical-align: middle; transition: transform 0.2s ease; - + max-width: 100% !important; + object-fit: contain !important; &:hover { transform: scale(1.2); } diff --git a/packages/frontend/src/components/global/MkEmojiKitchen.vue b/packages/frontend/src/components/global/MkEmojiKitchen.vue new file mode 100644 index 0000000000..a507a5a509 --- /dev/null +++ b/packages/frontend/src/components/global/MkEmojiKitchen.vue @@ -0,0 +1,46 @@ +<template> +<span v-if="errored">{{ alt }}</span> +<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/> +</template> + +<script lang="ts" setup> +import {computed, ref} from 'vue'; + +const props = defineProps<{ + name: string; + normal?: boolean; + url: string; +}>(); + +const rawUrl = computed(() => props.url); + +const url = computed(() => rawUrl.value); + +const alt = computed(() => props.name); +let errored = ref(url.value == null); +</script> + +<style lang="scss" module> +.root { + height: 2em; + vertical-align: middle; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.2); + } +} + +.normal { + height: 1.25em; + vertical-align: -0.25em; + + &:hover { + transform: none; + } +} + +.noStyle { + height: auto !important; +} +</style> diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue index 6380103e6f..a5f805c90d 100644 --- a/packages/frontend/src/components/global/MkLoading.vue +++ b/packages/frontend/src/components/global/MkLoading.vue @@ -55,7 +55,7 @@ const props = withDefaults(defineProps<{ --size: 38px; &.colored { - color: var(--accent); + color: #5f5f5f; } &.inline { @@ -83,7 +83,9 @@ const props = withDefaults(defineProps<{ height: var(--size); margin: 0 auto; } - +.text{ + color: var(--fg); +} .spinner { position: absolute; top: 0; diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index d7c89958b8..9adee33198 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -6,20 +6,24 @@ import { VNode, h, SetupContext, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; +import { ID, Instance } from 'misskey-js/built/entities.js'; import MkUrl from '@/components/global/MkUrl.vue'; import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; import MkMention from '@/components/MkMention.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; +import MkEmojiKitchen from '@/components/global/MkEmojiKitchen.vue'; import MkCode from '@/components/MkCode.vue'; import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA, { MkABehavior } from '@/components/global/MkA.vue'; -import { host } from '@/config.js'; -import { defaultStore } from '@/store.js'; +import { host } from '@/config'; +import { defaultStore } from '@/store'; +import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer'; import { nyaize as doNyaize } from '@/scripts/nyaize.js'; +import { uhoize as doUhoize } from '@/scripts/uhoize.js'; import { safeParseFloat } from '@/scripts/safe-parse.js'; const QUOTE_STYLE = ` @@ -35,11 +39,42 @@ type MfmProps = { text: string; plain?: boolean; nowrap?: boolean; - author?: Misskey.entities.UserLite; + emojireq?: boolean; + author?: { + id: ID; + username: string; + host: string | null; + name: string | null; + onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; + avatarUrl: string; + avatarBlurhash: string; + avatarDecorations: { + id: ID; + url: string; + angle?: number; + flipH?: boolean; + }[]; + emojis: { + name: string; + url: string; + }[]; + instance?: { + name: Instance['name']; + softwareName: Instance['softwareName']; + softwareVersion: Instance['softwareVersion']; + iconUrl: Instance['iconUrl']; + faviconUrl: Instance['faviconUrl']; + themeColor: Instance['themeColor']; + }; + isGorilla?: boolean; + isCat?: boolean; + isBot?: boolean;}; + i?: Misskey.entities.UserLite | null; isNote?: boolean; emojiUrls?: Record<string, string>; rootScale?: number; nyaize?: boolean | 'respect'; + uhoize?: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; enableEmojiMenu?: boolean; enableEmojiMenuReaction?: boolean; @@ -56,14 +91,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const isNote = props.isNote ?? true; const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; - + const shouldUhoize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isGorilla : false : false; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.text == null || props.text === '') return; const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text); const validTime = (t: string | boolean | null | undefined) => { - if (t == null) return null; + if (t == null || typeof t === 'boolean') return null; if (typeof t === 'boolean') return null; return t.match(/^[0-9.]+s$/) ? t : null; }; @@ -80,15 +115,18 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven * @param ast MFM AST * @param scale How times large the text is * @param disableNyaize Whether nyaize is disabled or not + * @param disableUhoize */ - const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => { + const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false, disableUhoize = false) => ast.map((token): VNode | string | (VNode | string)[] => { switch (token.type) { case 'text': { let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!disableNyaize && shouldNyaize) { text = doNyaize(text); } - + if (!disableUhoize && shouldUhoize) { + text = doUhoize(text); + } if (!props.plain) { const res: (VNode | string)[] = []; for (const t of text.split('\n')) { @@ -206,19 +244,16 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven break; } case 'blur': { + const radius = parseFloat(token.props.args.rad ?? '6'); return h('span', { class: '_mfm_blur_', + style: `--blur-px: ${radius}px;`, }, genEl(token.children, scale)); } case 'rainbow': { - if (!useAnim) { - return h('span', { - class: '_mfm_rainbow_fallback_', - }, genEl(token.children, scale)); - } const speed = validTime(token.props.args.speed) ?? '1s'; const delay = validTime(token.props.args.delay) ?? '0s'; - style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`; + style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};` : ''; break; } case 'sparkle': { @@ -229,7 +264,23 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'rotate': { const degrees = safeParseFloat(token.props.args.deg) ?? 90; - style = `transform: rotate(${degrees}deg); transform-origin: center center;`; + let rotateText = `rotate(${degrees}deg)`; + if (!token.props.args.deg && (token.props.args.x || token.props.args.y || token.props.args.z)) { + rotateText = ''; + } + if (token.props.args.x) { + const degrees = parseFloat(token.props.args.x ?? '0'); + rotateText += ` rotateX(${degrees}deg)`; + } + if (token.props.args.y) { + const degrees = parseFloat(token.props.args.y ?? '0'); + rotateText += ` rotateY(${degrees}deg)`; + } + if (token.props.args.z) { + const degrees = parseFloat(token.props.args.z ?? '0'); + rotateText += ` rotateZ(${degrees}deg)`; + } + style = `transform: ${rotateText}; transform-origin: center center;`; break; } case 'position': { @@ -293,6 +344,29 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); } } + case 'mix': { + const ch = token.children; + if (ch.length != 2 || ch.some(c => c.type !== 'unicodeEmoji')) { + style = null; + break; + } + + const emoji1 = ch[0].props.emoji; + const emoji2 = ch[1].props.emoji; + + const mixedEmojiUrl = mixEmoji(emoji1, emoji2); + if (!mixedEmojiUrl) { + style = null; + break; + } + + return h(MkEmojiKitchen, { + key: Math.random(), + name: emoji1 + emoji2, + normal: props.plain, + url: mixedEmojiUrl, + }); + } case 'unixtime': { const child = token.children[0]; const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); @@ -401,7 +475,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven case 'emojiCode': { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (props.author?.host == null) { + console.log('emojiCode', props.emojireq); + if (props.author?.host == null && !props.emojireq ) { return [h(MkCustomEmoji, { key: Math.random(), name: token.props.name, @@ -422,7 +497,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven name: token.props.name, url: props.emojiUrls && props.emojiUrls[token.props.name], normal: props.plain, - host: props.author.host, + host: props.author.host ? props.author.host : null, useOriginalSize: scale >= 2.5, })]; } diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 84590fb918..8e44f8d443 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -4,10 +4,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div ref="el" :class="$style.tabs" @wheel="onTabWheel"> - <div :class="$style.tabsInner"> + <div :class="ui !== 'twilike' ? $style.tabsInner : $style.tabsInnerX"> <button v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title" - class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]" + class="_button" :class="[ui !== 'twilike' ? $style.tab : $style.tabX, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]" @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)" > <div :class="$style.tabInner"> @@ -29,7 +29,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License </div> <div ref="tabHighlightEl" - :class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]" + :class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value , [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" ></div> </div> </template> @@ -52,8 +52,11 @@ export type Tab = { </script> <script lang="ts" setup> -import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue'; +import { onMounted, onUnmounted, watch, nextTick, shallowRef, ref, computed } from 'vue'; import { defaultStore } from '@/store.js'; +import { ui } from '@/config.js'; + +const gamingType = computed(defaultStore.makeGetterSetter('gamingType')); const props = withDefaults(defineProps<{ tabs?: Tab[]; @@ -204,6 +207,15 @@ onUnmounted(() => { white-space: nowrap; } +.tabsInnerX { + display: flex; + height: var(--height); + white-space: nowrap; + justify-content: space-around; +} +.tabX{ + flex: 1; +} .tab { display: inline-block; position: relative; @@ -228,10 +240,12 @@ onUnmounted(() => { .tabInner { display: flex; align-items: center; + justify-content: center; } .tabIcon + .tabTitle { padding-left: 4px; + font-weight: 900; } .tabTitle { @@ -250,9 +264,88 @@ onUnmounted(() => { border-radius: 999px; transition: none; pointer-events: none; - + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + &.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } &.animate { transition: width 0.15s ease, left 0.15s ease; } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index dc64a6423c..a757e52be5 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -8,10 +8,10 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu"> <MkAvatar :class="$style.avatar" :user="$i"/> </div> - <div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft"/> + <div v-else-if="!thin_ && narrow && !hideTitle"/> <template v-if="pageMetadata"> - <div v-if="!hideTitle" :class="$style.titleContainer" @click="top"> + <div v-if="!hideTitle && !hide" :class="$style.titleContainer" @click="top"> <div v-if="pageMetadata.avatar" :class="$style.titleAvatarContainer"> <MkAvatar :class="$style.titleAvatar" :user="pageMetadata.avatar" indicator/> </div> @@ -55,6 +55,7 @@ const props = withDefaults(defineProps<{ actions?: PageHeaderItem[] | null; thin?: boolean; displayMyAvatar?: boolean; + hide?:boolean; }>(), { tabs: () => ([] as Tab[]), }); @@ -142,14 +143,14 @@ onUnmounted(() => { } .upper { - --height: 50px; + --height: 55px; display: flex; gap: var(--margin); height: var(--height); .tabs:first-child { - margin-left: auto; padding: 0 12px; + width: 100%; } .tabs { margin-right: auto; @@ -166,14 +167,16 @@ onUnmounted(() => { } &.slim { + width: 100%; text-align: center; gap: 0; - + .buttonsRight { + margin-left: auto; + } .tabs:first-child { margin-left: 0; } > .titleContainer { - margin: 0 auto; max-width: 100%; } } diff --git a/packages/frontend/src/components/global/MkRuby.vue b/packages/frontend/src/components/global/MkRuby.vue new file mode 100644 index 0000000000..5036af70fb --- /dev/null +++ b/packages/frontend/src/components/global/MkRuby.vue @@ -0,0 +1,21 @@ +<script setup lang="ts"> +import { computed } from 'vue'; +import MkEmoji from './MkEmoji.vue'; +import MkCustomEmoji from './MkCustomEmoji.vue'; +const props = defineProps<{ + base: string; + text: string; + basetype: string; +}>(); +</script> + +<template> + <ruby> + <MkEmoji v-if="basetype === 'unicodeEmoji' " class="emoji" :emoji="base" :normal="true" /> + <MkCustomEmoji v-else-if="basetype === 'emojiCode' " :name="base"/> + <span style="white-space: pre-wrap;" v-else >{{base}}</span> + <rt>{{text}}</rt> + </ruby> +</template> +<style> +</style> diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index e2e9e6213c..831d716f1e 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -5,7 +5,7 @@ import { App } from 'vue'; -import Mfm from './global/MkMisskeyFlavoredMarkdown.js'; +import Mfm from './global/MkMisskeyFlavoredMarkdown.ts'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; import MkAvatar from './global/MkAvatar.vue'; @@ -23,7 +23,6 @@ import MkError from './global/MkError.vue'; import MkAd from './global/MkAd.vue'; import MkPageHeader from './global/MkPageHeader.vue'; import MkSpacer from './global/MkSpacer.vue'; -import MkFooterSpacer from './global/MkFooterSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; import MkLazy from './global/MkLazy.vue'; @@ -52,7 +51,6 @@ export const components = { MkAd: MkAd, MkPageHeader: MkPageHeader, MkSpacer: MkSpacer, - MkFooterSpacer: MkFooterSpacer, MkStickyContainer: MkStickyContainer, MkLazy: MkLazy, }; @@ -77,7 +75,6 @@ declare module '@vue/runtime-core' { MkAd: typeof MkAd; MkPageHeader: typeof MkPageHeader; MkSpacer: typeof MkSpacer; - MkFooterSpacer: typeof MkFooterSpacer; MkStickyContainer: typeof MkStickyContainer; MkLazy: typeof MkLazy; } diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 2c2ac8edc6..2a0b7ca0ec 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -75,12 +75,15 @@ export const ROLE_POLICIES = [ 'gtlAvailable', 'ltlAvailable', 'canPublicNote', + 'canEditNote', + 'canScheduleNote', 'mentionLimit', 'canInvite', 'inviteLimit', 'inviteLimitCycle', 'inviteExpirationTime', 'canManageCustomEmojis', + 'canRequestCustomEmojis', 'canManageAvatarDecorations', 'canSearchNotes', 'canUseTranslator', @@ -97,6 +100,9 @@ export const ROLE_POLICIES = [ 'userEachUserListsLimit', 'rateLimitFactor', 'avatarDecorationLimit', + 'emojiPickerProfileLimit', + 'listPinnedLimit', + 'localTimelineAnyLimit', ] as const; // なんか動かない @@ -126,7 +132,7 @@ export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = { position: ['x=', 'y='], fg: ['color='], bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], + border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], blur: [], rainbow: ['speed=', 'delay='], diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index c73be5a5f1..d8fcc5b555 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { shallowRef, computed, markRaw, watch } from 'vue'; +import { shallowRef, computed, markRaw, triggerRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; @@ -11,6 +11,7 @@ import { get, set } from '@/scripts/idb-proxy.js'; const storageCache = await get('emojis'); export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []); +export const customEmojisNameMap = computed(() => new Map(customEmojis.value.map(item => [item.name, item]))); export const customEmojiCategories = computed<[ ...string[], null ]>(() => { const categories = new Set<string>(); for (const emoji of customEmojis.value) { @@ -34,16 +35,19 @@ const stream = useStream(); stream.on('emojiAdded', emojiData => { customEmojis.value = [emojiData.emoji, ...customEmojis.value]; + triggerRef(customEmojis); set('emojis', customEmojis.value); }); stream.on('emojiUpdated', emojiData => { customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item); - set('emojis', customEmojis.value); + triggerRef(customEmojis); + set('emojis', customEmojis.value); }); stream.on('emojiDeleted', emojiData => { customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name)); + triggerRef(customEmojis); set('emojis', customEmojis.value); }); @@ -60,6 +64,7 @@ export async function fetchCustomEmojis(force = false) { } customEmojis.value = res.emojis; + triggerRef(customEmojis); set('emojis', res.emojis); set('lastEmojisFetchedAt', now); } diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index 91fcc00eec..76fef0e389 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -32,6 +32,8 @@ export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {} export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL); +export const googleAnalyticsId = computed(() => instance.googleAnalyticsId ?? null); + export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL); export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 7c7dec4e6c..d335017610 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -147,6 +147,13 @@ export const navbarItemDef = reactive({ miLocalStorage.setItem('ui', 'classic'); unisonReload(); }, + }, { + text: 'twilike', + active: ui === 'twilike', + action: () => { + miLocalStorage.setItem('ui', 'twilike'); + unisonReload(); + }, }], ev.currentTarget ?? ev.target); }, }, @@ -177,6 +184,14 @@ export const navbarItemDef = reactive({ show: computed(() => $i != null), to: `/@${$i?.username}`, }, + scheduledNotes: { + title: i18n.ts._schedulePost.list, + icon: 'ti ti-calendar-event', + show: computed(() => $i && $i.policies?.canScheduleNote), + action: (ev) => { + os.listSchedulePost(); + }, + }, cacheClear: { title: i18n.ts.clearCache, icon: 'ti ti-trash', diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 513594a24c..2a6854555a 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -13,6 +13,7 @@ import type { Form, GetFormResultType } from '@/scripts/form.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; +import XPostFormDialog from '@/components/XPostFormDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; import MkPageWindow from '@/components/MkPageWindow.vue'; import MkToast from '@/components/MkToast.vue'; @@ -24,6 +25,8 @@ import MkContextMenu from '@/components/MkContextMenu.vue'; import { MenuItem } from '@/types/menu.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { ui } from '@/config.js'; export const openingWindowsCount = ref(0); @@ -223,9 +226,8 @@ export function alert(props: { }, 'closed'); }); } - export function confirm(props: { - type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; + type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'|'mksw'; title?: string; text?: string; okText?: string; @@ -242,6 +244,24 @@ export function confirm(props: { }, 'closed'); }); } +export function switch1(props: { + type: 'mksw'; + title?: string | null; + text?: string | null; + okText?: string; + cancelText?: string; +}): Promise<{ canceled: boolean, result: boolean }> { + return new Promise((resolve, reject) => { + popup(MkDialog, { + ...props, + showCancelButton: true, + }, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + }, 'closed'); + }); +} // TODO: const T extends ... にしたい // https://zenn.dev/general_link/articles/813e47b7a0eef7#const-type-parameters @@ -528,11 +548,12 @@ export function form<F extends Form>(title: string, f: F): Promise<{ canceled: t }); } -export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> { +export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; multiple?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> { return new Promise(resolve => { popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), { includeSelf: opts.includeSelf, localOnly: opts.localOnly, + multiple: opts.multiple, }, { ok: user => { resolve(user); @@ -540,7 +561,13 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool }, 'closed'); }); } - +export async function listSchedulePost() { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkSchedulePostListDialog.vue')), { + }, { + }, 'closed'); + }); +} export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> { return new Promise(resolve => { popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { @@ -657,14 +684,25 @@ export function post(props: Record<string, any> = {}): Promise<void> { // 複数のpost formを開いたときに場合によってはエラーになる // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが let dispose; - popup(MkPostFormDialog, props, { - closed: () => { - resolve(); - dispose(); - }, - }).then(res => { - dispose = res.dispose; - }); + if (ui !== 'twilike') { + popup(MkPostFormDialog, props, { + closed: () => { + resolve(); + dispose(); + }, + }).then(res => { + dispose = res.dispose; + }); + } else { + popup(XPostFormDialog, props, { + closed: () => { + resolve(); + dispose(); + }, + }).then(res => { + dispose = res.dispose; + }); + } }); } diff --git a/packages/frontend/src/pages/about-type4ny.vue b/packages/frontend/src/pages/about-type4ny.vue index 0b0118c03b..f3de10849b 100644 --- a/packages/frontend/src/pages/about-type4ny.vue +++ b/packages/frontend/src/pages/about-type4ny.vue @@ -105,6 +105,10 @@ SPDX-License-Identifier: AGPL-3.0-only <img src="https://avatars.githubusercontent.com/u/22656849?v=4" :class="$style.contributorAvatar"> <span :class="$style.contributorUsername">@anatawa12</span> </a> + <a href="https://github.com/mattyatea" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/56515516?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@mattyatea</span> + </a> </div> </MkFoldableSection> <FormSection> diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index 6c6aeda40e..7149d9809a 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -4,52 +4,76 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps"> - <MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton> +<MkStickyContainer> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer v-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> + <MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton> + <MkButton v-if="$i && (!$i.isModerator || $i.policies.canRequestCustomEmojis)" primary @click="edit">{{ i18n.ts.requestCustomEmojis }}</MkButton> - <div class="query"> - <MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off"> - <template #prefix><i class="ti ti-search"></i></template> - </MkInput> + <div class="query" style="margin-top: 10px;"> + <MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off"> + <template #prefix><i class="ti ti-search"></i></template> + </MkInput> <!-- たくさんあると邪魔 <div class="tags"> <span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> </div> --> - </div> - - <MkFoldableSection v-if="searchEmojis"> - <template #header>{{ i18n.ts.searchResult }}</template> - <div :class="$style.emojis"> - <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji"/> </div> - </MkFoldableSection> - <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category"> - <template #header>{{ category || i18n.ts.other }}</template> + <MkFoldableSection v-if="searchEmojis" :expanded="false"> + <template #header>{{ i18n.ts.searchResult }}</template> + <div :class="$style.emojis"> + <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji" :request="emoji.request"/> + </div> + </MkFoldableSection> + + <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category" :expanded="false"> + <template #header>{{ category || i18n.ts.other }}</template> + <div :class="$style.emojis"> + <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/> + </div> + </MkFoldableSection> + </MkSpacer> + <MkSpacer v-if="tab === 'request'" :contentMax="1000" :marginMin="20"> <div :class="$style.emojis"> - <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/> + <XEmoji v-for="emoji in requestEmojis.emojis" :key="emoji.name" :emoji="emoji" :request="true"/> </div> - </MkFoldableSection> -</div> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> -import { watch, ref } from 'vue'; +import { watch, defineAsyncComponent, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import XEmoji from './emojis.emoji.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js'; +import { customEmojis, customEmojiCategories } from '@/custom-emojis.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import * as os from '@/os.js'; +let tab = ref('emojis'); +const headerActions = computed(() => []); -const customEmojiTags = getCustomEmojiTags(); -const q = ref(''); -const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null); -const selectedTags = ref(new Set()); +const headerTabs = computed(() => [{ + key: 'emojis', + title: i18n.ts.list, +}, { + key: 'request', + title: i18n.ts.requestingEmojis, +}]); + +definePageMetadata(ref({})); + +let q = ref(''); +let searchEmojis = ref<Misskey.entities.CustomEmoji[]>(null); +let selectedTags = ref(new Set()); +const requestEmojis = await misskeyApiGet('emoji-requests'); function search() { if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) { @@ -65,28 +89,35 @@ function search() { queryarry.includes(`:${emoji.name}:`), ); } else { - searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value)); + searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q)); } } else { - searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t))); + searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t))); } } -function toggleTag(tag) { - if (selectedTags.value.has(tag)) { - selectedTags.value.delete(tag); - } else { - selectedTags.value.add(tag); - } -} +const edit = () => { + os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), { + isRequest: true, + }, { + done: result => { + window.location.reload(); + }, + }, 'closed'); +}; -watch(q, () => { +watch((q), () => { search(); }); -watch(selectedTags, () => { +watch((selectedTags), () => { search(); }, { deep: true }); + +definePageMetadata({ + title: i18n.ts.customEmojis, + icon: null, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 47f7861d3d..e87c415b95 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -42,7 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> <div :class="$style.items"> - <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`"> + <MkA + v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" + :class="$style.item" :to="`/instance-info/${instance.host}`" + > <MkInstanceCardMini :instance="instance"/> </MkA> </div> diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index ebc657fd84..89946942da 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> <div class="_gaps_m"> - <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> + <div :class="$style.banner" :style="{ backgroundImage: `url(${ bannerUrl })` }"> <div style="overflow: clip;"> - <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/> + <img :src="iconUrl" alt="" :class="$style.bannerIcon"/> <div :class="$style.bannerName"> <b>{{ instance.name ?? host }}</b> </div> @@ -31,10 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> <div v-html="i18n.tsx.poweredByType4nyDescription({ name: instance.name ?? host })"> </div> - <FormLink to="/about-type4ny"> - <template #icon><i class="ti ti-info-circle"></i></template> - {{ i18n.ts.aboutType4ny }} - </FormLink> + <FormLink to="/about-type4ny">{{ i18n.ts.aboutType4ny }}</FormLink> <FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external> <template #icon><i class="ti ti-code"></i></template> {{ i18n.ts.sourceCode }} @@ -42,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-else warn> {{ i18n.ts.sourceCodeIsNotYetProvided }} </MkInfo> + ソースコード含め問い合わせは下記のメールアドレスへよろしくお願いします。 </div> </FormSection> @@ -116,9 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSection> </div> </MkSpacer> - <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> - <XEmojis/> - </MkSpacer> + <XEmojis v-else-if="tab === 'emojis'"/> <MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20"> <XFederation/> </MkSpacer> @@ -130,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XEmojis from './about.emojis.vue'; import XFederation from './about.federation.vue'; @@ -150,6 +146,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { instance } from '@/instance.js'; +import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store.js'; const props = withDefaults(defineProps<{ initialTab?: string; @@ -165,6 +162,27 @@ watch(tab, () => { claimAchievement('viewInstanceChart'); } }); +let bannerUrl = ref(defaultStore.state.bannerUrl); +let iconUrl = ref(defaultStore.state.iconUrl); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); + +if (darkMode.value) { + bannerUrl.value = bannerDark; + iconUrl.value = iconDark; +} else { + bannerUrl.value = bannerLight; + iconUrl.value = iconLight; +} + +watch(darkMode, () => { + if (darkMode.value) { + bannerUrl.value = bannerDark; + iconUrl.value = iconDark; + } else { + bannerUrl.value = bannerLight; + iconUrl.value = iconLight; + } +}); const initStats = () => misskeyApi('stats', { }).then((res) => { diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index a36acf226a..320410bd04 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -98,6 +98,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> </div> + <div> + <MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="presentsPoints">ぷりずむを付与する</MkButton> + </div> <MkFolder> <template #icon><i class="ti ti-license"></i></template> @@ -312,6 +315,15 @@ async function resetPassword() { } } +async function presentsPoints() { + const { canceled, result } = await os.inputText({ + title: 'ポイント', + }); + if (canceled) return; + if (result === null) return; + await misskeyApi('admin/accounts/present-points', { userId: user.value.id, points: parseInt(result) }); +} + async function toggleSuspend(v) { const confirm = await os.confirm({ type: 'warning', diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 79ade595d7..a5f17b7ad8 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -14,6 +14,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.iconUrl }}</template> </MkInput> + <MkInput v-model="iconDark" type="url"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts._serverSettings.iconUrl }} (Dark)</template> + </MkInput> + <MkInput v-model="iconLight" type="url"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts._serverSettings.iconUrl }} (Light)</template> + </MkInput> <MkInput v-model="app192IconUrl" type="url"> <template #prefix><i class="ti ti-link"></i></template> @@ -41,6 +49,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.bannerUrl }}</template> </MkInput> + <MkInput v-model="bannerDark" type="url"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.bannerUrl }} (Dark)</template> + </MkInput> + <MkInput v-model="bannerLight" type="url"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.bannerUrl }} (Light)</template> + </MkInput> <MkInput v-model="backgroundImageUrl" type="url"> <template #prefix><i class="ti ti-link"></i></template> @@ -61,6 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.somethingHappened }}</template> </MkInput> + <MkInput v-model="googleAnalyticsId" type="url"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>googleAnal </template> + </MkInput> <MkColorInput v-model="themeColor"> <template #label>{{ i18n.ts.themeColor }}</template> @@ -128,10 +148,16 @@ const themeColor = ref<string | null>(null); const defaultLightTheme = ref<string | null>(null); const defaultDarkTheme = ref<string | null>(null); const serverErrorImageUrl = ref<string | null>(null); +const googleAnalyticsId = ref<string | null>(null); + const infoImageUrl = ref<string | null>(null); const notFoundImageUrl = ref<string | null>(null); const repositoryUrl = ref<string | null>(null); const feedbackUrl = ref<string | null>(null); +const iconDark = ref<string | null>(null); +const iconLight = ref<string | null>(null); +const bannerDark = ref<string | null>(null); +const bannerLight = ref<string | null>(null); const manifestJsonOverride = ref<string>('{}'); async function init() { @@ -150,6 +176,10 @@ async function init() { repositoryUrl.value = meta.repositoryUrl; feedbackUrl.value = meta.feedbackUrl; manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'); + iconDark.value = meta.iconDark; + iconLight.value = meta.iconLight; + bannerDark.value = meta.bannerDark; + bannerLight.value = meta.bannerLight; } function save() { @@ -165,9 +195,14 @@ function save() { infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value, notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value, serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value, + googleAnalyticsId: googleAnalyticsId.value === '' ? null : googleAnalyticsId.value, repositoryUrl: repositoryUrl.value === '' ? null : repositoryUrl.value, feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value, manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)), + iconDark: iconDark.value === '' ? null : iconDark.value, + iconLight: iconLight.value === '' ? null : iconLight.value, + bannerDark: bannerDark.value === '' ? null : bannerDark.value, + bannerLight: bannerLight.value === '' ? null : bannerLight.value, }).then(() => { fetchInstance(true); }); diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 6dee1ad5d3..d9a9c1ca5c 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700" :marginMin="16"> <div class="lxpfedzu"> <div class="banner"> - <img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> + <img :src="iconUrl" alt="" class="icon"/> </div> <div class="_gaps_s"> @@ -40,8 +40,9 @@ import { lookup } from '@/scripts/lookup.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js'; -import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useRouter } from '@/router/supplier.js'; +import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; +import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store.js'; const isEmpty = (x: string | null) => x == null || x === ''; @@ -67,7 +68,20 @@ let noEmailServer = !instance.enableEmail; let noInquiryUrl = isEmpty(instance.inquiryUrl); const thereIsUnresolvedAbuseReport = ref(false); const currentPage = computed(() => router.currentRef.value.child); - +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +let iconUrl = ref(); +if (darkMode.value) { + iconUrl.value = iconDark; +} else { + iconUrl.value = iconLight; +} +watch(darkMode, () => { + if (darkMode.value) { + iconUrl.value = iconDark; + } else { + iconUrl.value = iconLight; + } +}); misskeyApi('admin/abuse-user-reports', { state: 'unresolved', limit: 1, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index 07a2f02a36..ae7ae2a03d 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> + <template #header> + <MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/> + </template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <MkTextarea v-if="tab === 'block'" v-model="blockedHosts"> @@ -33,6 +35,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkSpacer from '@/components/global/MkSpacer.vue'; +import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; const blockedHosts = ref<string>(''); const silencedHosts = ref<string>(''); @@ -40,8 +44,8 @@ const tab = ref('block'); async function init() { const meta = await misskeyApi('admin/meta'); - blockedHosts.value = meta.blockedHosts.join('\n'); - silencedHosts.value = meta.silencedHosts.join('\n'); + blockedHosts.value = meta.blockedHosts ? meta.blockedHosts.join('\n') : ''; + silencedHosts.value = meta.silencedHosts ? meta.silencedHosts.join('\n') : ''; } function save() { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 461e71bcba..324f8c04dd 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> </MkSwitch> + <MkSwitch v-model="enableGDPRMode"> + <template #label>{{ i18n.ts.enableGDPRMode }}</template> + </MkSwitch> + <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink> <MkInput v-model="tosUrl" type="url"> @@ -93,6 +97,7 @@ const preservedUsernames = ref<string>(''); const tosUrl = ref<string | null>(null); const privacyPolicyUrl = ref<string | null>(null); const inquiryUrl = ref<string | null>(null); +const enableGDPRMode = ref<boolean>(false); async function init() { const meta = await misskeyApi('admin/meta'); @@ -105,6 +110,7 @@ async function init() { tosUrl.value = meta.tosUrl; privacyPolicyUrl.value = meta.privacyPolicyUrl; inquiryUrl.value = meta.inquiryUrl; + enableGDPRMode.value = meta.enableGDPRMode; } function save() { @@ -118,6 +124,7 @@ function save() { prohibitedWords: prohibitedWords.value.split('\n'), hiddenTags: hiddenTags.value.split('\n'), preservedUsernames: preservedUsernames.value.split('\n'), + enableGDPRMode: enableGDPRMode.value, }).then(() => { fetchInstance(true); }); diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index e9f48a1a25..6dac86c03e 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -36,6 +36,25 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> </MkSwitch> </div> + <MkInput v-model="DiscordWebhookUrl" type="password"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>Discord Webhook URL</template> + </MkInput> + <MkInput v-model="DiscordWebhookUrlWordBlock" type="password"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>Discord Webhook Url WordBlock</template> + </MkInput> + <MkInput v-model="EmojiBotToken" type="password"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>EmojiBotToken</template> + </MkInput> + <MkInput v-model="ApiBase"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>ApiBase</template> + </MkInput> + <MkSwitch v-model="requestEmojiAllOk"> + 絵文字の申請全部許可 + </MkSwitch> </div> </FormSuspense> </MkSpacer> @@ -52,11 +71,17 @@ import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkSwitch from '@/components/MkSwitch.vue'; +import MkInput from '@/components/MkInput.vue'; const enableServerMachineStats = ref<boolean>(false); const enableIdenticonGeneration = ref<boolean>(false); const enableChartsForRemoteUser = ref<boolean>(false); const enableChartsForFederatedInstances = ref<boolean>(false); +const requestEmojiAllOk = ref(false); +let DiscordWebhookUrl = ref(null); +let DiscordWebhookUrlWordBlock = ref(null); +let EmojiBotToken = ref(null); +let ApiBase = ref(null); async function init() { const meta = await misskeyApi('admin/meta'); @@ -64,6 +89,11 @@ async function init() { enableIdenticonGeneration.value = meta.enableIdenticonGeneration; enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser; enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances; + requestEmojiAllOk.value = meta.requestEmojiAllOk; + DiscordWebhookUrl.value = meta.DiscordWebhookUrl; + DiscordWebhookUrlWordBlock.value = meta.DiscordWebhookUrlWordBlock; + EmojiBotToken.value = meta.EmojiBotToken; + ApiBase.value = meta.ApiBase; } function save() { @@ -71,7 +101,12 @@ function save() { enableServerMachineStats: enableServerMachineStats.value, enableIdenticonGeneration: enableIdenticonGeneration.value, enableChartsForRemoteUser: enableChartsForRemoteUser.value, + requestEmojiAllOk: requestEmojiAllOk.value, enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, + DiscordWebhookUrl: DiscordWebhookUrl.value, + EmojiBotToken: EmojiBotToken.value, + ApiBase: ApiBase.value, + DiscordWebhookUrlWordBlock: DiscordWebhookUrlWordBlock.value, }).then(() => { fetchInstance(true); }); diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 6e8ff6aedb..1f28849b51 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -99,7 +99,24 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> </div> </MkFolder> - + <MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> + <template #label>{{ i18n.ts._role._options.mentionMax }}</template> + <template #suffix> + <span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.mentionLimit.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly"> + </MkInput> + <MkRange v-model="role.policies.mentionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> <MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])"> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #suffix> @@ -160,25 +177,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> - <template #label>{{ i18n.ts._role._options.mentionMax }}</template> - <template #suffix> - <span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> - <span v-else>{{ role.policies.mentionLimit.value }}</span> - <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span> - </template> - <div class="_gaps"> - <MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly"> - <template #label>{{ i18n.ts._role.useBaseValue }}</template> - </MkSwitch> - <MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly"> - </MkInput> - <MkRange v-model="role.policies.mentionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> - <template #label>{{ i18n.ts._role.priority }}</template> - </MkRange> - </div> - </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> <template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #suffix> @@ -570,6 +568,63 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.emojiPickerProfileLimit, 'emojiPickerProfileLimit'])"> + <template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template> + <template #suffix> + <span v-if="role.policies.emojiPickerProfileLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.emojiPickerProfileLimit.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.emojiPickerProfileLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.emojiPickerProfileLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.emojiPickerProfileLimit.value" type="number" :min="0" :disabled="role.policies.emojiPickerProfileLimit.useDefault" > + <template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template> + </MkInput> + <MkRange v-model="role.policies.emojiPickerProfileLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.listPinnedLimit, 'listPinnedLimit'])"> + <template #label>{{ i18n.ts._role._options.listPinnedLimit }}</template> + <template #suffix> + <span v-if="role.policies.listPinnedLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.listPinnedLimit.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.listPinnedLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.listPinnedLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.listPinnedLimit.value" type="number" :min="0" :disabled="role.policies.listPinnedLimit.useDefault" > + <template #label>{{ i18n.ts._role._options.listPinnedLimit }}</template> + </MkInput> + <MkRange v-model="role.policies.listPinnedLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.localTimelineAnyLimit, 'localTimelineAnyLimit'])"> + <template #label>{{ i18n.ts._role._options.localTimelineAnyLimit }}</template> + <template #suffix> + <span v-if="role.policies.localTimelineAnyLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.localTimelineAnyLimit.value}}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.localTimelineAnyLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.localTimelineAnyLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.localTimelineAnyLimit.value" type="number" :min="0" :disabled="role.policies.localTimelineAnyLimit.useDefault" > + <template #label>{{ i18n.ts._role._options.localTimelineAnyLimit }}</template> + </MkInput> + <MkRange v-model="role.policies.localTimelineAnyLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> </div> </FormSlot> </div> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 0844dec988..c6799f677f 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -23,188 +23,261 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template> </MkRange> </MkFolder> + <MkFoldableSection :expanded="false"> + <template #header>タイムライン系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> + <template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.gtlAvailable"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])"> - <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> - <template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.gtlAvailable"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template> + <template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.ltlAvailable"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>ノート系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canPublicNote }}</template> + <template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canPublicNote"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> + <template #label>{{ i18n.ts._role._options.mentionMax }}</template> + <template #suffix>{{ policies.mentionLimit }}</template> + <MkInput v-model="policies.mentionLimit" type="number"> + </MkInput> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canEditNote }}</template> + <template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canEditNote"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])"> - <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template> - <template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.ltlAvailable"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canScheduleNote, 'canScheduleNote'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canScheduleNote }}</template> + <template #suffix>{{ policies.canScheduleNote ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canScheduleNote"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canSearchNotes }}</template> + <template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canSearchNotes"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])"> - <template #label>{{ i18n.ts._role._options.canPublicNote }}</template> - <template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canPublicNote"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canUseTranslator }}</template> + <template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canUseTranslator"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.pinMax }}</template> + <template #suffix>{{ policies.pinLimit }}</template> + <MkInput v-model="policies.pinLimit" type="number"> + </MkInput> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>招待系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> + <template #label>{{ i18n.ts._role._options.canInvite }}</template> + <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canInvite"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.inviteLimit }}</template> + <template #suffix>{{ policies.inviteLimit }}</template> + <MkInput v-model="policies.inviteLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> - <template #label>{{ i18n.ts._role._options.mentionMax }}</template> - <template #suffix>{{ policies.mentionLimit }}</template> - <MkInput v-model="policies.mentionLimit" type="number"> - </MkInput> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> + <template #label>{{ i18n.ts._role._options.canInvite }}</template> + <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canInvite"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> - <template #label>{{ i18n.ts._role._options.canInvite }}</template> - <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canInvite"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])"> + <template #label>{{ i18n.ts._role._options.inviteLimit }}</template> + <template #suffix>{{ policies.inviteLimit }}</template> + <MkInput v-model="policies.inviteLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])"> - <template #label>{{ i18n.ts._role._options.inviteLimit }}</template> - <template #suffix>{{ policies.inviteLimit }}</template> - <MkInput v-model="policies.inviteLimit" type="number"> - </MkInput> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template> + <template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template> + <MkInput v-model="policies.inviteLimitCycle" type="number"> + <template #suffix>{{ i18n.ts._time.minute }}</template> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])"> - <template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template> - <template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template> - <MkInput v-model="policies.inviteLimitCycle" type="number"> - <template #suffix>{{ i18n.ts._time.minute }}</template> - </MkInput> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template> + <template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template> + <MkInput v-model="policies.inviteExpirationTime" type="number"> + <template #suffix>{{ i18n.ts._time.minute }}</template> + </MkInput> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>PrisMisskey独自機能系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.emojiPickerProfileLimit, 'pickerProfileDefault'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template> + <template #suffix>{{ policies.emojiPickerProfileLimit }}</template> + <MkInput v-model="policies.emojiPickerProfileLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])"> - <template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template> - <template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template> - <MkInput v-model="policies.inviteExpirationTime" type="number"> - <template #suffix>{{ i18n.ts._time.minute }}</template> - </MkInput> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.listPinnedLimit, 'listPinnedLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.listPinnedLimit }}</template> + <template #suffix>{{ policies.listPinnedLimit }}</template> + <MkInput v-model="policies.listPinnedLimit" type="number"> + </MkInput> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.localTimelineAnyLimit, 'localTimelineAnyLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.localTimelineAnyLimit }}</template> + <template #suffix>{{ policies.localTimelineAnyLimit }}</template> + <MkInput v-model="policies.localTimelineAnyLimit" type="number"> + </MkInput> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>カスタム絵文字系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> + <template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canManageCustomEmojis"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])"> - <template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template> - <template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canManageAvatarDecorations"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canRequestCustomEmojis, 'canRequestCustomEmojis'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canRequestCustomEmojis }}</template> + <template #suffix>{{ policies.canRequestCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canRequestCustomEmojis"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>ドライブ、ファイル系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.driveCapacity }}</template> + <template #suffix>{{ policies.driveCapacityMb }}MB</template> + <MkInput v-model="policies.driveCapacityMb" type="number"> + <template #suffix>MB</template> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])"> - <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> - <template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canManageCustomEmojis"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> + <template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.alwaysMarkNsfw"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>アイコンデコレーション系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template> + <template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canManageAvatarDecorations"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> + <template #suffix>{{ policies.avatarDecorationLimit }}</template> + <MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0"> + </MkInput> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>クリップ系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.clipMax }}</template> + <template #suffix>{{ policies.clipLimit }}</template> + <MkInput v-model="policies.clipLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])"> - <template #label>{{ i18n.ts._role._options.canSearchNotes }}</template> - <template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canSearchNotes"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template> + <template #suffix>{{ policies.noteEachClipsLimit }}</template> + <MkInput v-model="policies.noteEachClipsLimit" type="number"> + </MkInput> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>リスト系</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.userListMax }}</template> + <template #suffix>{{ policies.userListLimit }}</template> + <MkInput v-model="policies.userListLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])"> - <template #label>{{ i18n.ts._role._options.canUseTranslator }}</template> - <template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canUseTranslator"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template> + <template #suffix>{{ policies.userEachUserListsLimit }}</template> + <MkInput v-model="policies.userEachUserListsLimit" type="number"> + </MkInput> + </MkFolder> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>その他</template> + <MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.antennaMax }}</template> + <template #suffix>{{ policies.antennaLimit }}</template> + <MkInput v-model="policies.antennaLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])"> - <template #label>{{ i18n.ts._role._options.driveCapacity }}</template> - <template #suffix>{{ policies.driveCapacityMb }}MB</template> - <MkInput v-model="policies.driveCapacityMb" type="number"> - <template #suffix>MB</template> - </MkInput> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.wordMuteMax }}</template> + <template #suffix>{{ policies.wordMuteLimit }}</template> + <MkInput v-model="policies.wordMuteLimit" type="number"> + <template #suffix>chars</template> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])"> - <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> - <template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.alwaysMarkNsfw"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.webhookMax }}</template> + <template #suffix>{{ policies.webhookLimit }}</template> + <MkInput v-model="policies.webhookLimit" type="number"> + </MkInput> + </MkFolder> - <MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])"> - <template #label>{{ i18n.ts._role._options.pinMax }}</template> - <template #suffix>{{ policies.pinLimit }}</template> - <MkInput v-model="policies.pinLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])"> - <template #label>{{ i18n.ts._role._options.antennaMax }}</template> - <template #suffix>{{ policies.antennaLimit }}</template> - <MkInput v-model="policies.antennaLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])"> - <template #label>{{ i18n.ts._role._options.wordMuteMax }}</template> - <template #suffix>{{ policies.wordMuteLimit }}</template> - <MkInput v-model="policies.wordMuteLimit" type="number"> - <template #suffix>chars</template> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])"> - <template #label>{{ i18n.ts._role._options.webhookMax }}</template> - <template #suffix>{{ policies.webhookLimit }}</template> - <MkInput v-model="policies.webhookLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])"> - <template #label>{{ i18n.ts._role._options.clipMax }}</template> - <template #suffix>{{ policies.clipLimit }}</template> - <MkInput v-model="policies.clipLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])"> - <template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template> - <template #suffix>{{ policies.noteEachClipsLimit }}</template> - <MkInput v-model="policies.noteEachClipsLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])"> - <template #label>{{ i18n.ts._role._options.userListMax }}</template> - <template #suffix>{{ policies.userListLimit }}</template> - <MkInput v-model="policies.userListLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])"> - <template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template> - <template #suffix>{{ policies.userEachUserListsLimit }}</template> - <MkInput v-model="policies.userEachUserListsLimit" type="number"> - </MkInput> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])"> - <template #label>{{ i18n.ts._role._options.canHideAds }}</template> - <template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template> - <MkSwitch v-model="policies.canHideAds"> - <template #label>{{ i18n.ts.enable }}</template> - </MkSwitch> - </MkFolder> - - <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])"> - <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> - <template #suffix>{{ policies.avatarDecorationLimit }}</template> - <MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0"> - </MkInput> - </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])" class="_margin"> + <template #label>{{ i18n.ts._role._options.canHideAds }}</template> + <template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canHideAds"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + </MkFoldableSection> <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> </div> diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 5c662ff22d..eecc5a49ed 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -103,6 +103,22 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="bannedEmailDomains"> <template #label>Banned Email Domains List</template> </MkTextarea> + + <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #label> Signup Protection</template> + + <div class="_gaps_m"> + <MkSwitch v-model="enableProxyCheckio"> + <template #label>Use ProxyCheck.io API</template> + </MkSwitch> + <MkInput v-model="proxyCheckioApiKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>ProxyCheck.io API Key</template> + </MkInput> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </MkFolder> @@ -153,11 +169,13 @@ const enableSensitiveMediaDetectionForVideos = ref<boolean>(false); const enableIpLogging = ref<boolean>(false); const enableActiveEmailValidation = ref<boolean>(false); const enableVerifymailApi = ref<boolean>(false); +const enableProxyCheckio = ref<boolean>(false); const verifymailAuthKey = ref<string | null>(null); const enableTruemailApi = ref<boolean>(false); const truemailInstance = ref<string | null>(null); const truemailAuthKey = ref<string | null>(null); const bannedEmailDomains = ref<string>(''); +const proxyCheckioApiKey = ref<string | null>(null); async function init() { const meta = await misskeyApi('admin/meta'); @@ -182,6 +200,8 @@ async function init() { truemailInstance.value = meta.truemailInstance; truemailAuthKey.value = meta.truemailAuthKey; bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; + enableProxyCheckio.value = meta.enableProxyCheckio; + proxyCheckioApiKey.value = meta.proxyCheckioApiKey; } function save() { @@ -204,6 +224,8 @@ function save() { truemailInstance: truemailInstance.value, truemailAuthKey: truemailAuthKey.value, bannedEmailDomains: bannedEmailDomains.value.split('\n'), + enableProxyCheckio: enableProxyCheckio.value, + proxyCheckioApiKey: proxyCheckioApiKey.value, }).then(() => { fetchInstance(true); }); diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index 1f98f96912..146c4c04c0 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -7,45 +7,38 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="900"> + <MkSwitch v-model="select">SelectMode</MkSwitch> + <MkButton @click="setCategoryBulk">Set Category</MkButton> + <MkButton @click="deletes">Delete</MkButton> <div class="_gaps"> - <MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null"> - <template #label>{{ avatarDecoration.name }}</template> - <template #caption>{{ avatarDecoration.description }}</template> - - <div class="_gaps_m"> - <MkInput v-model="avatarDecoration.name"> - <template #label>{{ i18n.ts.name }}</template> - </MkInput> - <MkTextarea v-model="avatarDecoration.description"> - <template #label>{{ i18n.ts.description }}</template> - </MkTextarea> - <MkInput v-model="avatarDecoration.url"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <div class="buttons _buttons"> - <MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> - </div> - </MkFolder> + <div :class="$style.decorations"> + <XDecoration + v-for="avatarDecoration in avatarDecorations" + :key="avatarDecoration.id" + :class=" selectItemsId.includes(avatarDecoration.id) ? $style.selected : '' " + :decoration="avatarDecoration" + @click="select ? selectItems(avatarDecoration.id) : openDecorationEdit(avatarDecoration)" + /> + </div> </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, defineAsyncComponent, watch } from 'vue'; import * as Misskey from 'misskey-js'; -import MkButton from '@/components/MkButton.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import MkFolder from '@/components/MkFolder.vue'; +import XDecoration from '@/pages/settings/avatar-decoration.decoration.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]); +const select = ref(false); +const selectItemsId = ref<string[]>([]); function add() { avatarDecorations.value.unshift({ @@ -54,9 +47,19 @@ function add() { name: '', description: '', url: '', + category: '', }); } +function selectItems(decorationId) { + if (selectItemsId.value.includes(decorationId)) { + const index = selectItemsId.value.indexOf(decorationId); + selectItemsId.value.splice(index, 1); + } else { + selectItemsId.value.push(decorationId); + } +} + function del(avatarDecoration) { os.confirm({ type: 'warning', @@ -73,10 +76,29 @@ async function save(avatarDecoration) { await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration); load(); } else { - os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration); + selectItemsId.value.push(decorationId); } } +function openDecorationEdit(avatarDecoration) { + os.popup(defineAsyncComponent(() => import('@/components/MkAvatarDecoEditDialog.vue')), { + avatarDecoration: avatarDecoration, + }, { + del: () => { + window.location.reload(); + }, + }); +} + +function openDecorationCreate() { + os.popup(defineAsyncComponent(() => import('@/components/MkAvatarDecoEditDialog.vue')), { + }, { + del: result => { + avatarDecorations.value.unshift(result); + }, + }); +} + function load() { misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => { avatarDecorations.value = _avatarDecorations; @@ -84,12 +106,40 @@ function load() { } load(); +watch(select, () => { + selectItemsId.value = []; +}); + +async function setCategoryBulk() { + const { canceled, result } = await os.inputText({ + title: 'Category', + }); + if (canceled) return; + if (selectItemsId.value.length > 1) { + for (let i = 0; i < selectItemsId.value.length; i++) { + let decorationId = selectItemsId.value[i]; + await misskeyApi('admin/avatar-decorations/update', { + id: decorationId, + category: result, + }); + } + } +} + +async function deletes() { + if (selectItemsId.value.length > 0) { + selectItemsId.value.forEach(decorationId => { + console.log(decorationId); + misskeyApi('admin/avatar-decorations/delete', { id: decorationId }); + }); + } +} const headerActions = computed(() => [{ asFullButton: true, icon: 'ti ti-plus', text: i18n.ts.add, - handler: add, + handler: openDecorationCreate, }]); const headerTabs = computed(() => []); @@ -99,3 +149,13 @@ definePageMetadata(() => ({ icon: 'ti ti-sparkles', })); </script> +<style module> +.decorations { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + grid-gap: 12px; +} +.selected{ + border: 0.1px solid var(--accent); +} +</style> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 2b02688eda..f94a6917ad 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -38,7 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる --> <MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> - <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/> </div> <div v-else-if="tab === 'featured'" key="featured"> diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue index c58fdedb82..a35ba11c63 100644 --- a/packages/frontend/src/pages/clicker.vue +++ b/packages/frontend/src/pages/clicker.vue @@ -17,8 +17,8 @@ import MkClickerGame from '@/components/MkClickerGame.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; definePageMetadata(() => ({ - title: '🍪👈', - icon: 'ti ti-cookie', + title: '●👈', + icon: 'ti ti-circle', })); </script> diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 3df47272b9..de7172cd0f 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -10,62 +10,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="900"> <div class="ogwlenmc"> <div v-if="tab === 'local'" class="local"> - <MkInput v-model="query" :debounce="true" type="search" autocapitalize="off"> - <template #prefix><i class="ti ti-search"></i></template> - <template #label>{{ i18n.ts.search }}</template> - </MkInput> - <MkSwitch v-model="selectMode" style="margin: 8px 0;"> - <template #label>Select mode</template> - </MkSwitch> - <div v-if="selectMode" class="_buttons"> - <MkButton inline @click="selectAll">Select all</MkButton> - <MkButton inline @click="setCategoryBulk">Set category</MkButton> - <MkButton inline @click="setTagBulk">Set tag</MkButton> - <MkButton inline @click="addTagBulk">Add tag</MkButton> - <MkButton inline @click="removeTagBulk">Remove tag</MkButton> - <MkButton inline @click="setLicenseBulk">Set License</MkButton> - <MkButton inline danger @click="delBulk">Delete</MkButton> - </div> - <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> - <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> - <template #default="{items}"> - <div class="ldhfsamy"> - <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> - <img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.category }}</div> - </div> - </button> - </div> - </template> - </MkPagination> + <MkCustomEmojiEditLocal/> + </div> + <div v-if="tab === 'request'" class="request"> + <MkCustomEmojiEditRequest/> </div> - <div v-else-if="tab === 'remote'" class="remote"> - <FormSplit> - <MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off"> - <template #prefix><i class="ti ti-search"></i></template> - <template #label>{{ i18n.ts.search }}</template> - </MkInput> - <MkInput v-model="host" :debounce="true"> - <template #label>{{ i18n.ts.host }}</template> - </MkInput> - </FormSplit> - <MkPagination :pagination="remotePagination"> - <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> - <template #default="{items}"> - <div class="ldhfsamy"> - <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> - <img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.host }}</div> - </div> - </div> - </div> - </template> - </MkPagination> + <MkCustomEmojiEditRemote/> </div> </div> </MkSpacer> @@ -74,105 +25,31 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref, shallowRef } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkPagination from '@/components/MkPagination.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; -import FormSplit from '@/components/form/split.vue'; -import { selectFile } from '@/scripts/select-file.js'; -import * as os from '@/os.js'; +import { computed, defineAsyncComponent, ref } from 'vue'; +import MkCustomEmojiEditRequest from '@/components/MkCustomEmojiEditRequest.vue'; +import MkCustomEmojiEditLocal from '@/components/MkCustomEmojiEditLocal.vue'; +import MkCustomEmojiEditRemote from '@/components/MkCustomEmojiEditRemote.vue'; +import { selectFile } from '@/scripts/select-file'; +import * as os from '@/os'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; -const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); - -const tab = ref('local'); -const query = ref<string | null>(null); -const queryRemote = ref<string | null>(null); -const host = ref<string | null>(null); -const selectMode = ref(false); -const selectedEmojis = ref<string[]>([]); - -const pagination = { - endpoint: 'admin/emoji/list' as const, - limit: 30, - params: computed(() => ({ - query: (query.value && query.value !== '') ? query.value : null, - })), -}; - -const remotePagination = { - endpoint: 'admin/emoji/list-remote' as const, - limit: 30, - params: computed(() => ({ - query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null, - host: (host.value && host.value !== '') ? host.value : null, - })), -}; - -const selectAll = () => { - if (selectedEmojis.value.length > 0) { - selectedEmojis.value = []; - } else { - selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id); - } -}; - -const toggleSelect = (emoji) => { - if (selectedEmojis.value.includes(emoji.id)) { - selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id); - } else { - selectedEmojis.value.push(emoji.id); - } -}; +const tab = ref('request'); const add = async (ev: MouseEvent) => { - os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { + os.popup(defineAsyncComponent(() => import('../components/MkEmojiEditDialog.vue')), { }, { done: result => { - if (result.created) { - emojisPaginationComponent.value.prepend(result.created); - } + //TODO: emitにして追加を反映 + // if (result.created) { + // emojisPaginationComponent.value.prepend(result.created); + // emojisPaginationComponent.value.reload(); + // } }, }, 'closed'); }; -const edit = (emoji) => { - os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { - emoji: emoji, - }, { - done: result => { - if (result.updated) { - emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ - ...oldEmoji, - ...result.updated, - })); - } else if (result.deleted) { - emojisPaginationComponent.value.removeItem(emoji.id); - } - }, - }, 'closed'); -}; - -const importEmoji = (emoji) => { - os.apiWithDialog('admin/emoji/copy', { - emojiId: emoji.id, - }); -}; - -const remoteMenu = (emoji, ev: MouseEvent) => { - os.popupMenu([{ - type: 'label', - text: ':' + emoji.name + ':', - }, { - text: i18n.ts.import, - icon: 'ti ti-plus', - action: () => { importEmoji(emoji); }, - }], ev.currentTarget ?? ev.target); -}; - const menu = (ev: MouseEvent) => { os.popupMenu([{ icon: 'ti ti-download', @@ -215,78 +92,6 @@ const menu = (ev: MouseEvent) => { }], ev.currentTarget ?? ev.target); }; -const setCategoryBulk = async () => { - const { canceled, result } = await os.inputText({ - title: 'Category', - }); - if (canceled) return; - await os.apiWithDialog('admin/emoji/set-category-bulk', { - ids: selectedEmojis.value, - category: result, - }); - emojisPaginationComponent.value.reload(); -}; - -const setLicenseBulk = async () => { - const { canceled, result } = await os.inputText({ - title: 'License', - }); - if (canceled) return; - await os.apiWithDialog('admin/emoji/set-license-bulk', { - ids: selectedEmojis.value, - license: result, - }); - emojisPaginationComponent.value.reload(); -}; - -const addTagBulk = async () => { - const { canceled, result } = await os.inputText({ - title: 'Tag', - }); - if (canceled) return; - await os.apiWithDialog('admin/emoji/add-aliases-bulk', { - ids: selectedEmojis.value, - aliases: result.split(' '), - }); - emojisPaginationComponent.value.reload(); -}; - -const removeTagBulk = async () => { - const { canceled, result } = await os.inputText({ - title: 'Tag', - }); - if (canceled) return; - await os.apiWithDialog('admin/emoji/remove-aliases-bulk', { - ids: selectedEmojis.value, - aliases: result.split(' '), - }); - emojisPaginationComponent.value.reload(); -}; - -const setTagBulk = async () => { - const { canceled, result } = await os.inputText({ - title: 'Tag', - }); - if (canceled) return; - await os.apiWithDialog('admin/emoji/set-aliases-bulk', { - ids: selectedEmojis.value, - aliases: result.split(' '), - }); - emojisPaginationComponent.value.reload(); -}; - -const delBulk = async () => { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }); - if (canceled) return; - await os.apiWithDialog('admin/emoji/delete-bulk', { - ids: selectedEmojis.value, - }); - emojisPaginationComponent.value.reload(); -}; - const headerActions = computed(() => [{ asFullButton: true, icon: 'ti ti-plus', @@ -298,6 +103,9 @@ const headerActions = computed(() => [{ }]); const headerTabs = computed(() => [{ + key: 'request', + title: i18n.ts.requestingEmojis, +}, { key: 'local', title: i18n.ts.local, }, { @@ -312,103 +120,4 @@ definePageMetadata(() => ({ </script> <style lang="scss" scoped> -.ogwlenmc { - > .local { - .empty { - margin: var(--margin); - } - - .ldhfsamy { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - margin: var(--margin) 0; - - > .emoji { - display: flex; - align-items: center; - padding: 11px; - text-align: left; - border: solid 1px var(--panel); - - &:hover { - border-color: var(--inputBorderHover); - } - - &.selected { - border-color: var(--accent); - } - - > .img { - width: 42px; - height: 42px; - } - - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; - - > .name { - text-overflow: ellipsis; - overflow: hidden; - } - - > .info { - opacity: 0.5; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - } - } - - > .remote { - .empty { - margin: var(--margin); - } - - .ldhfsamy { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - margin: var(--margin) 0; - - > .emoji { - display: flex; - align-items: center; - padding: 12px; - text-align: left; - - &:hover { - color: var(--accent); - } - - > .img { - width: 32px; - height: 32px; - } - - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; - - > .name { - text-overflow: ellipsis; - overflow: hidden; - } - - > .info { - opacity: 0.5; - font-size: 90%; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - } - } -} </style> diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 4e99ef49ed..a43f7840fb 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -234,7 +234,7 @@ const NORAML_MONOS: FrontendMonoDefinition[] = [{ img: '/client-assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png', imgSizeX: 256, imgSizeY: 256, - spriteScale: 1.12, + spriteScale: 1.0, }, { id: 'beb30459-b064-4888-926b-f572e4e72e0c', sfxPitch: 0.75, @@ -508,9 +508,81 @@ const SWEETS_MONOS: FrontendMonoDefinition[] = [{ imgSizeY: 32, spriteScale: 1, }]; +const PRISMISSKEY_MONOS: FrontendMonoDefinition[] = [{ + id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525', + sfxPitch: 0.25, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fc4c7e430-bd92-415a-a7d3-031ddb7f0641.apng', + imgSizeX: 3400, + imgSizeY: 3400, + spriteScale: 2.3, +}, { + id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1', + sfxPitch: 0.5, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F5ba71ab2-1673-4eb0-bd3b-b44e063365ba.apng', + imgSizeX: 2000, + imgSizeY: 2000, + spriteScale: 1.65, +}, { + id: '41607ef3-b6d6-4829-95b6-3737bf8bb956', + sfxPitch: 0.75, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fd6fe438b-c550-484f-95d1-1739a2b5173d.apng', + + imgSizeX: 2000, + imgSizeY: 2000, + spriteScale: 1.8, +}, { + id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416', + sfxPitch: 1, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F8226211b-58e2-46ba-be20-4ea635d614ab.webp', + imgSizeX: 500, + imgSizeY: 501, + spriteScale: 1.0, +}, { + id: '1092e069-fe1a-450b-be97-b5d477ec398c', + sfxPitch: 1.5, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fde30a66c-5c98-4c2e-a425-4d1f73d96899.png', + imgSizeX: 340, + imgSizeY: 351, + spriteScale: 0.98, +}, { + id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0', + sfxPitch: 2, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fdc8d893a-6d1f-4a86-847e-a30e56270249.png', + imgSizeX: 1023, + imgSizeY: 1000, + spriteScale: 1.0, +}, { + id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a', + sfxPitch: 2.5, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F56a303b9-4385-44bb-a4d4-2453450eef01.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.4, +}, { + id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919', + sfxPitch: 3, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2F96a87c60-543d-4e83-a24d-c2a3247eb2ea.webp', + imgSizeX: 630, + imgSizeY: 620, + spriteScale: 0.6, +}, { + id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d', + sfxPitch: 3.5, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fdff2812b-2c80-4ecf-b5f1-b2874048899e.webp', + imgSizeX: 1500, + imgSizeY: 1500, + spriteScale: 1.15, +}, { + id: '35e476ee-44bd-4711-ad42-87be245d3efd', + sfxPitch: 4, + img: '/proxy/image.webp?url=https%3A%2F%2Ffiles.prismisskey.space%2Fmisskey%2Fc4448bf6-d95f-49e5-844d-b6b9530e82cc.png', + imgSizeX: 200, + imgSizeY: 200, + spriteScale: 1.5, +}]; const props = defineProps<{ - gameMode: 'normal' | 'square' | 'yen' | 'sweets' | 'space'; + gameMode: 'normal' | 'square' | 'yen' | 'sweets' | 'prismisskey' | 'space'; mute: boolean; }>(); @@ -524,6 +596,7 @@ const monoDefinitions = computed(() => { props.gameMode === 'yen' ? YEN_MONOS : props.gameMode === 'sweets' ? SWEETS_MONOS : props.gameMode === 'space' ? NORAML_MONOS : + props.gameMode === 'prismisskey' ? PRISMISSKEY_MONOS : [] as never; }); @@ -532,6 +605,7 @@ function getScoreUnit(gameMode: string) { gameMode === 'square' ? 'pt' : gameMode === 'yen' ? '円' : gameMode === 'sweets' ? 'kcal' : + gameMode === 'prismisskey' ? 'pt' : '' as never; } diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 6002fe3940..46d5ec6de1 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="square">SQUARE</option> <option value="yen">YEN</option> <option value="sweets">SWEETS</option> + <option value="prismisskey">PRISMISSKEY</option> <!--<option value="space">SPACE</option>--> </MkSelect> <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton> @@ -72,8 +73,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s" style="padding: 16px;"> <div><b>Credit</b></div> <div> - <div>Ai-chan illustration: @poteriri@misskey.io</div> - <div>BGM: @ys@misskey.design</div> + <div>Ai-chan illustration: <MkA href="/@poteriri@misskey.io">@poteriri@misskey.io</MkA></div> + <div>BGM: <MkA href="/@ys@misskey.design">@ys@misskey.design</MkA></div> + <div>Emoji Thanks: <MkA to="/@User2_Moo@prismisskey.space">@User2_Moo@prismisskey.space</MkA></div> + <div>Emoji Thanks: <MkA to="/@z_n_jin@prismisskey.space">@z_n_jin@prismisskey.space</MkA></div> + <div>Emoji Thanks: <MkA to="/@nekomimi@prismisskey.space">@nekomimi@prismisskey.space</MkA></div> </div> </div> </div> @@ -95,7 +99,7 @@ import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal'); +const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space' | 'prismisskey'>('normal'); const gameStarted = ref(false); const mute = ref(false); const ranking = ref(null); @@ -110,6 +114,7 @@ function getScoreUnit(gameMode: string) { gameMode === 'yen' ? '円' : gameMode === 'sweets' ? 'kcal' : gameMode === 'space' ? 'pt' : + gameMode === 'prismisskey' ? 'pt' : '' as never; } diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index f884332df0..f787353eda 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -4,11 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<button class="_button" :class="$style.root" @click="menu"> +<button v-if="request" class="_button emoji-request" :class="$style.root" @click="menu"> + <img :src="emoji.url" :class="$style.img" loading="lazy"/> + <div class="body"> + <div class="name _monospace">{{ emoji.name + ' (request)' }}</div> + <div class="info">{{ emoji.aliases.join(' ') }}</div> + </div> +</button> +<button v-else class="_button" :class="$style.root" @click="menu"> <img :src="emoji.url" :class="$style.img" loading="lazy"/> <div :class="$style.body"> - <div :class="$style.name" class="_monospace">{{ emoji.name }}</div> - <div :class="$style.info">{{ emoji.aliases.join(' ') }}</div> + <div class="name _monospace">{{ emoji.name }}</div> + <div class="info">{{ emoji.aliases.join(' ') }}</div> </div> </button> </template> @@ -23,6 +30,7 @@ import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialo const props = defineProps<{ emoji: Misskey.entities.EmojiSimple; + request?: boolean; }>(); function menu(ev) { @@ -54,39 +62,45 @@ function menu(ev) { <style lang="scss" module> .root { - display: flex; - align-items: center; - padding: 12px; - text-align: left; - background: var(--panel); - border-radius: 8px; + display: flex; + align-items: center; + padding: 12px; + text-align: left; + background: var(--panel); + border-radius: 8px; - &:hover { - border-color: var(--accent); - } + &:hover { + border-color: var(--accent); + } } .img { - width: 42px; - height: 42px; - object-fit: contain; + width: 42px; + height: 42px; + object-fit: contain; } .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; + padding: 0 0 0 8px; + white-space: nowrap; + overflow: hidden; } .name { - text-overflow: ellipsis; - overflow: hidden; + text-overflow: ellipsis; + overflow: hidden; } .info { - opacity: 0.5; - font-size: 0.9em; - text-overflow: ellipsis; - overflow: hidden; + opacity: 0.5; + font-size: 0.9em; + text-overflow: ellipsis; + overflow: hidden; +} + +.emoji-request { + --c: rgb(255 196 0 / 15%);; + background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%); + background-size: 16px 16px; } </style> diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index b4bc75b006..da1605df6c 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkSpacer :contentMax="800"> - <MkTab v-model="tab" style="margin-bottom: var(--margin);"> + <MkTab v-model="tab" style="margin-bottom: var(--margin);"> <option value="notes">{{ i18n.ts.notes }}</option> <option value="polls">{{ i18n.ts.poll }}</option> </MkTab> diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index e2437418e9..7c326d13db 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> + + <MkSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> @@ -14,10 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.nothing }} </p> </div> - </MKSpacer> + </MkSpacer> <MkSpacer v-else-if="list" :contentMax="700" :class="$style.main"> - <div v-if="list" class="members _margin"> - <div :class="$style.member_text">{{ i18n.ts.members }}</div> + <MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton> + <MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton> + <MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton> + <MkFolder v-if="list" class="members _margin"> + <template #label>{{ i18n.ts.members }}</template> + <div :class="$style.member_text"></div> <div class="_gaps_s"> <div v-for="user in users" :key="user.id" :class="$style.userItem"> <MkA :class="$style.userItemBody" :to="`${userPage(user)}`"> @@ -25,10 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </div> </div> - </div> - <MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton> - <MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton> - <MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton> + </MkFolder> </MkSpacer> </MkStickyContainer> </template> @@ -44,6 +46,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkButton from '@/components/MkButton.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { serverErrorImageUrl } from '@/instance.js'; +import MkFolder from '@/components/MkFolder.vue'; const props = defineProps<{ listId: string; diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 006c74d75f..536a43011f 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -7,44 +7,87 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700"> - <div class="_gaps"> - <div v-if="items.length === 0" class="empty"> - <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> - <div>{{ i18n.ts.nothing }}</div> + <MkFoldableSection style="margin-bottom: 32px;"> + <template #header>{{ i18n.ts.favoriteLists }}</template> + + <div class="_gaps"> + <div v-if="feautureList.length === 0" class="empty"> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> + + <div v-if="feautureList.length > 0" class="_gaps"> + <MkA v-for="list in feautureList" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`"> + <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}` }) }})</span></div> + <MkAvatars :userIds="list.userIds" :limit="10"/> + </MkA> </div> </div> + </MkFoldableSection> + <MkFoldableSection style="margin-bottom: 32px;"> + <template #header>{{ i18n.ts.localListList }}</template> + <div class="_gaps"> + <div v-if="localList.length === 0" class="empty"> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> - <MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> - - <div v-if="items.length > 0" class="_gaps"> - <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> - <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div> - <MkAvatars :userIds="list.userIds" :limit="10"/> - </MkA> + <div v-if="localList.length > 0" class="_gaps"> + <MkA v-for="list in localList" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`"> + <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}` }) }})</span></div> + <MkAvatars :userIds="list.userIds" :limit="10"/> + </MkA> + </div> </div> - </div> + </MkFoldableSection> + <MkFoldableSection> + <template #header>{{ i18n.ts.myLists }}</template> + <div class="_gaps"> + <div v-if="items.length === 0" class="empty"> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> + + <div v-if="items.length > 0" class="_gaps"> + <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> + <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div> + <MkAvatars :userIds="list.userIds" :limit="10"/> + </MkA> + </div> + </div> + </MkFoldableSection> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> import { onActivated, computed } from 'vue'; -import MkButton from '@/components/MkButton.vue'; import MkAvatars from '@/components/MkAvatars.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { userListsCache } from '@/cache.js'; +import { userFavoriteListsCache, userListsCache } from '@/cache.js'; import { infoImageUrl } from '@/instance.js'; import { signinRequired } from '@/account.js'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const $i = signinRequired(); const items = computed(() => userListsCache.value.value ?? []); +const localList = await misskeyApi('users/lists/list', { publicAll: true }); +const feautureList = computed(() => userFavoriteListsCache.value.value ?? []); function fetch() { userListsCache.fetch(); + userFavoriteListsCache.delete(); + userFavoriteListsCache.fetch(); } fetch(); @@ -67,12 +110,17 @@ const headerActions = computed(() => [{ userListsCache.delete(); fetch(); }, +}, { + asFullButton: true, + icon: 'ti ti-plus', + text: i18n.ts.createList, + handler: create, }]); const headerTabs = computed(() => []); definePageMetadata(() => ({ - title: i18n.ts.manageLists, + title: i18n.ts._exportOrImport.userLists, icon: 'ti ti-list', })); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 5394a1aef4..93b790bdde 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -8,22 +8,20 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :class="$style.main"> <div v-if="list" class="_gaps"> - <MkFolder> - <template #label>{{ i18n.ts.settings }}</template> + <div>{{ i18n.ts.settings }}</div> - <div class="_gaps"> - <MkInput v-model="name"> - <template #label>{{ i18n.ts.name }}</template> - </MkInput> - <MkSwitch v-model="isPublic">{{ i18n.ts.public }}</MkSwitch> - <div class="_buttons"> - <MkButton rounded primary @click="updateSettings">{{ i18n.ts.save }}</MkButton> - <MkButton rounded danger @click="deleteList()">{{ i18n.ts.delete }}</MkButton> - </div> + <div class="_gaps" style="margin: 8px 0; "> + <MkInput v-model="name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkSwitch v-model="isPublic">{{ i18n.ts.public }}</MkSwitch> + <div class="_buttons"> + <MkButton rounded primary @click="updateSettings">{{ i18n.ts.save }}</MkButton> + <MkButton rounded danger @click="deleteList()">{{ i18n.ts.delete }}</MkButton> </div> - </MkFolder> + </div> - <MkFolder defaultOpen> + <MkFolder> <template #label>{{ i18n.ts.members }}</template> <template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template> @@ -104,14 +102,22 @@ function fetchList() { } function addUser() { - os.selectUser().then(user => { + os.selectUser( { multiple: true }).then(user => { if (!list.value) return; - os.apiWithDialog('users/lists/push', { - listId: list.value.id, - userId: user.id, - }).then(() => { - paginationEl.value.reload(); - }); + if (Array.isArray(user)) { + user.forEach(u => { + misskeyApi('users/lists/push', { + listId: list.value.id, + userId: u.id, + }); + }); + } else if (typeof user === 'string') { + os.apiWithDialog('users/lists/push', { + listId: list.value.id, + userId: user.id, + }); + } + paginationEl.value.reload(); }); } diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 0f93a2915c..b3c49d5e02 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs" :hide="true" :actions="headerActions"/></template> <MkSpacer :contentMax="800"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <div v-if="tab === 'all'" key="all"> @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import XNotifications from '@/components/MkNotifications.vue'; import MkNotes from '@/components/MkNotes.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; @@ -31,6 +31,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { notificationTypes } from '@/const.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const tab = ref('all'); const includeTypes = ref<string[] | null>(null); @@ -67,18 +68,12 @@ function setFilter(ev) { os.popupMenu(items, ev.currentTarget ?? ev.target); } -const headerActions = computed(() => [tab.value === 'all' ? { +const headerActions = computed(() => [{ text: i18n.ts.filter, icon: 'ti ti-filter', highlighted: includeTypes.value != null, handler: setFilter, -} : undefined, tab.value === 'all' ? { - text: i18n.ts.markAllAsRead, - icon: 'ti ti-check', - handler: () => { - os.apiWithDialog('notifications/mark-all-as-read'); - }, -} : undefined].filter(x => x !== undefined)); +}].filter(x => x !== undefined)); const headerTabs = computed(() => [{ key: 'all', @@ -98,6 +93,9 @@ definePageMetadata(() => ({ title: i18n.ts.notifications, icon: 'ti ti-bell', })); +onMounted(() => { + misskeyApi('notifications/mark-all-as-read'); +}); </script> <style module lang="scss"> diff --git a/packages/frontend/src/pages/settings/account-stats.vue b/packages/frontend/src/pages/settings/account-stats.vue new file mode 100644 index 0000000000..b01a7dc54b --- /dev/null +++ b/packages/frontend/src/pages/settings/account-stats.vue @@ -0,0 +1,118 @@ +<template> +<div class="_gaps_m"> + <FormSection v-if="stats" first> + <template #label>{{ i18n.ts.statistics }}</template> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.notesCount }}</template> + <template #value>{{ number(stats.notesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.repliesCount }}</template> + <template #value>{{ number(stats.repliesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.renotesCount }}</template> + <template #value>{{ number(stats.renotesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.repliedCount }}</template> + <template #value>{{ number(stats.repliedCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.renotedCount }}</template> + <template #value>{{ number(stats.renotedCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.pollVotesCount }}</template> + <template #value>{{ number(stats.pollVotesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.pollVotedCount }}</template> + <template #value>{{ number(stats.pollVotedCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.sentReactionsCount }}</template> + <template #value>{{ number(stats.sentReactionsCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.receivedReactionsCount }}</template> + <template #value>{{ number(stats.receivedReactionsCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.noteFavoritesCount }}</template> + <template #value>{{ number(stats.noteFavoritesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.followingCount }}</template> + <template #value>{{ number(stats.followingCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template> + <template #value>{{ number(stats.localFollowingCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template> + <template #value>{{ number(stats.remoteFollowingCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.followersCount }}</template> + <template #value>{{ number(stats.followersCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template> + <template #value>{{ number(stats.localFollowersCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template> + <template #value>{{ number(stats.remoteFollowersCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.pageLikesCount }}</template> + <template #value>{{ number(stats.pageLikesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.pageLikedCount }}</template> + <template #value>{{ number(stats.pageLikedCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.driveFilesCount }}</template> + <template #value>{{ number(stats.driveFilesCount) }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ i18n.ts.driveUsage }}</template> + <template #value>{{ bytes(stats.driveUsage) }}</template> + </MkKeyValue> + </FormSection> +</div> +</template> + +<script lang="ts" setup> +import { onMounted, ref, computed } from 'vue'; +import FormSection from '@/components/form/section.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import number from '@/filters/number'; +import bytes from '@/filters/bytes'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import { misskeyApi } from '@/scripts/misskey-api.js'; + +const stats = ref<any>({}); + +onMounted(() => { + misskeyApi('i/stats', { + userId: $i!.id, + }).then(response => { + stats.value = response; + }); +}); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata({ + title: i18n.ts.accountInfo, + icon: 'ti ti-info-circle', +}); +</script> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index a41a8a786e..66b9949857 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -20,6 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="decorationsForPreview" forceShowDecoration/> </div> <div class="_gaps_s"> + {{ i18n.ts.description }} + <p style="white-space: pre-wrap;">{{ decoration.description }}</p> <MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`"> <template #label>{{ i18n.ts.angle }}</template> </MkRange> @@ -61,6 +63,7 @@ const props = defineProps<{ id: string; url: string; name: string; + description: string; }; }>(); diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 7013bb1e2a..6a9fd5152e 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -28,15 +28,30 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton> </div> - - <div :class="$style.decorations"> + <MkInput v-model="q" :placeholder="i18n.ts.search"/> + <div v-if="searchResult.length > 0" :class="$style.decorations"> + <span> {{ i18n.ts.searchResult }}</span><br> <XDecoration - v-for="avatarDecoration in avatarDecorations" - :key="avatarDecoration.id" + v-for="avatarDecoration in searchResult" + :key="avatarDecoration.name" :decoration="avatarDecoration" @click="openDecoration(avatarDecoration)" /> </div> + <div v-for="category in categories"> + <MkFoldableSection :expanded="false"> + <template #header> {{ (category !== '') ? category : i18n.ts.other }}</template> + <div :class="$style.decorations"> + <div v-for="avatarDecoration in avatarDecorations.filter(ad => ad.category === category)"> + <XDecoration + :key="avatarDecoration.id" + :decoration="avatarDecoration" + @click="openDecoration(avatarDecoration)" + /> + </div> + </div> + </MkFoldableSection> + </div> </div> <div v-else> <MkLoading/> @@ -45,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, defineAsyncComponent, computed } from 'vue'; +import { ref, defineAsyncComponent, computed, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XDecoration from './avatar-decoration.decoration.vue'; import MkButton from '@/components/MkButton.vue'; @@ -55,16 +70,72 @@ import { i18n } from '@/i18n.js'; import { signinRequired } from '@/account.js'; import MkInfo from '@/components/MkInfo.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import MkInput from '@/components/MkInput.vue'; const $i = signinRequired(); - +const searchResult = ref([]); const loading = ref(true); -const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]); +const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse & { category:string }>([]); +const q = ref<string>(''); +watch(() => q.value, () => { + const searchCustom = () => { + const max = 100; + const matches = new Set(); + const decos = avatarDecorations.value; + const exactMatch = decos.find(avatarDecoration => avatarDecoration.name === q.value); + if (exactMatch) matches.add(exactMatch); + if (decos.includes(' ')) { // AND検索 + const keywords = q.value.split(' '); + + // 名前にキーワードが含まれている + for (const deco of decos) { + if (keywords.every(keyword => deco.name.includes(keyword))) { + matches.add(deco); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; + + // 名前またはエイリアスにキーワードが含まれている + for (const deco of decos) { + if (keywords.every(keyword => deco.name.includes(keyword))) { + matches.add(deco); + if (matches.size >= max) break; + } + } + } else { + for (const deco of decos) { + if (deco.name.startsWith(q.value)) { + matches.add(deco); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; + + for (const deco of decos) { + if (deco.name.includes(q.value)) { + matches.add(deco); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; + } + + return matches; + }; + searchResult.value = Array.from(searchCustom()); +}); misskeyApi('get-avatar-decorations').then(_avatarDecorations => { avatarDecorations.value = _avatarDecorations; loading.value = false; }); +const categories = computed(() => { + const allCategories = avatarDecorations.value.map(ad => ad.category); + const uniqueCategories = [...new Set(allCategories)]; + return uniqueCategories.sort(); +}); function openDecoration(avatarDecoration, index?: number) { os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), { diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index 3284e30d44..4b67253f6b 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -5,6 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> + <MkSelect v-model="nowProfileId"> + <template #label>{{ i18n.ts.emojiPickerProfile }}</template> + <option v-for="a in profileMax" :key="a" :value="a">{{ a }}. {{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }} {{ nowDefaultProfileId === a ? `(${i18n.ts.default})`: '' }} </option> + </MkSelect> + <MkInput v-model="profileName"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> <MkFolder :defaultOpen="true"> <template #icon><i class="ti ti-pin"></i></template> <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template> @@ -84,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </MkFolder> - + <MkButton inline primary @click="setDefaultProfile"> {{ i18n.ts.default }}</MkButton> <FormSection> <template #label>{{ i18n.ts.emojiPickerDisplay }}</template> @@ -139,6 +146,9 @@ import { emojiPicker } from '@/scripts/emoji-picker.js'; import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; import MkFolder from '@/components/MkFolder.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import { signinRequired } from '@/account.js'; +import MkInput from '@/components/MkInput.vue'; const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions)); const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis)); @@ -155,6 +165,23 @@ const setDefaultReaction = () => setDefault(pinnedEmojisForReaction); const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev); const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev); const setDefaultEmoji = () => setDefault(pinnedEmojis); +const nowProfileId = ref(defaultStore.state.pickerProfileDefault); + +const $i = signinRequired(); +const profileMax = $i.policies.emojiPickerProfileLimit; +const profileName = ref(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); +const nowDefaultProfileId = ref(defaultStore.state['pickerProfileDefault']); +const nowDefaultProfileName = ref(); +nowDefaultProfileName.value = deepClone(defaultStore.state[`pickerProfileName${nowDefaultProfileId.value > 1 ? nowDefaultProfileId.value - 1 : ''}`]); +pinnedEmojisForReaction.value = deepClone(defaultStore.state[`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); +pinnedEmojis.value = deepClone(defaultStore.state[`pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); +profileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); + +watch(nowProfileId, () => { + pinnedEmojisForReaction.value = deepClone(defaultStore.state[`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); + pinnedEmojis.value = deepClone(defaultStore.state[`pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); + profileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); +}); function previewReaction(ev: MouseEvent) { reactionPicker.show(getHTMLElement(ev), null); @@ -226,17 +253,34 @@ function getHTMLElement(ev: MouseEvent): HTMLElement { } watch(pinnedEmojisForReaction, () => { - defaultStore.set('reactions', pinnedEmojisForReaction.value); + defaultStore.set(`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, pinnedEmojisForReaction.value); }, { deep: true, }); watch(pinnedEmojis, () => { - defaultStore.set('pinnedEmojis', pinnedEmojis.value); + defaultStore.set( `pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, pinnedEmojis.value); }, { deep: true, }); +watch(profileName, () => { + defaultStore.set(`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, profileName.value); +}, { + deep: true, +}); + +async function setDefaultProfile() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.setDefaultProfileConfirm, + }); + if (canceled) return; + nowDefaultProfileId.value = nowProfileId.value; + nowDefaultProfileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]); + await defaultStore.set('pickerProfileDefault', nowProfileId.value); +} + definePageMetadata(() => ({ title: i18n.ts.emojiPicker, icon: 'ti ti-mood-happy', diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index f70422b80c..f6d933538d 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -36,17 +36,61 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> + <MkSwitch v-model="alwaysShowPlayer">Youtube.comや、nicovideo.jpのプレイヤーを全て開いた状態にする</MkSwitch> + <MkSwitch v-model="alwaysExpandTweet">Xのポストを常時表示させる</MkSwitch> + <MkSelect v-model="draftSavingBehavior"> + <template #label>{{ i18n.ts.draftSavingBehavior }}<span class="_beta">{{ i18n.ts.kakuregaFeature }}</span></template> + <option value="auto">{{ i18n.ts._draftSavingBehavior.auto }}</option> + <option value="manual">{{ i18n.ts._draftSavingBehavior.manual }}</option> + </MkSelect> + <MkSwitch v-model="disableNoteDrafting"> + <template #caption>{{ i18n.ts.disableNoteDraftingDescription }}</template> + {{ i18n.ts.disableNoteDrafting }} + <span class="_beta">{{ i18n.ts.kakuregaFeature }}</span> + </MkSwitch> <MkFolder> <template #label>{{ i18n.ts.pinnedList }}</template> - <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> - <MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> - <MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> + <div v-for="pinnedLists in defaultStore.reactiveState.pinnedUserLists.value" class="_margin"> + {{ pinnedLists.name }} + <MkButton danger @click="removePinnedList(pinnedLists.id,pinnedLists.name)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> + </div> + <MkButton v-if="pinnedMax > defaultStore.reactiveState.pinnedUserLists.value.length " @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> + <MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length " danger @click="removePinnedList('all')"><i class="ti ti-trash"></i> {{ i18n.ts.all }}{{ i18n.ts.remove }}</MkButton> </MkFolder> + <MkFolder> + <template #label>{{ i18n.ts.pinnedChannel }}</template> + <div v-for="pinnedLists in defaultStore.reactiveState.pinnedChannels.value" class="_margin"> + {{ pinnedLists.name }} + <MkButton danger @click="removePinnedChannel(pinnedLists.id,pinnedLists.name)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> + </div> + <MkButton v-if="pinnedMax > defaultStore.reactiveState.pinnedChannels.value.length " @click="setPinnedChannel()">{{ i18n.ts.add }}</MkButton> + <MkButton v-if="defaultStore.reactiveState.pinnedChannels.value.length " danger @click="removePinnedChannel('all')"><i class="ti ti-trash"></i> {{ i18n.ts.all }}{{ i18n.ts.remove }}</MkButton> + </MkFolder> + <MkFoldableSection :expanded="false" class="item"> + <template #header>{{ i18n.ts.topbarCustom }}</template> + + {{ i18n.ts._timelines.home }} + <MkSwitch v-model="showHomeTimeline">{{ i18n.ts.enable }}</MkSwitch> + <br> + {{ i18n.ts._timelines.local }} + <MkSwitch v-model="showLocalTimeline">{{ i18n.ts.enable }}</MkSwitch> + <br> + {{ i18n.ts._timelines.social }} + <MkSwitch v-model="showSocialTimeline">{{ i18n.ts.enable }}</MkSwitch> + <br> + {{ i18n.ts._timelines.media }} + <MkSwitch v-model="showMediaTimeline">{{ i18n.ts.enable }}</MkSwitch> + <br> + {{ i18n.ts._timelines.global }} + <MkSwitch v-model="showGlobalTimeline">{{ i18n.ts.enable }}</MkSwitch> + <br> + {{ i18n.ts.topBarNameShown }} + <MkSwitch v-model="topBarNameShown">{{ i18n.ts.enable }}</MkSwitch> + </MkFoldableSection> </div> </FormSection> - - <FormSection> - <template #label>{{ i18n.ts.displayOfNote }}</template> + <MkFoldableSection :expanded="false" class="item"> + <template #header>{{ i18n.ts.displayOfNote }}</template> <div class="_gaps_m"> <div class="_gaps_s"> @@ -56,6 +100,20 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch> <MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch> + <MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch> + <MkSwitch v-model="showVisibilityColor">{{ i18n.ts.showVisibilityColor }}</MkSwitch> + <MkColorInput v-if="showVisibilityColor" v-model="homeColor"> + <template #label>{{ i18n.ts._visibility.home }}</template> + </MkColorInput> + <MkColorInput v-if="showVisibilityColor" v-model="followerColor"> + <template #label>{{ i18n.ts._visibility.followers }}</template> + </MkColorInput> + <MkColorInput v-if="showVisibilityColor" v-model="specifiedColor"> + <template #label>{{ i18n.ts._visibility.specified }}</template> + </MkColorInput> + <MkColorInput v-if="showVisibilityColor" v-model="localOnlyColor"> + <template #label>{{ i18n.ts.localOnly }}</template> + </MkColorInput> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch> @@ -93,10 +151,9 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option> </MkRadios> </div> - </FormSection> - - <FormSection> - <template #label>{{ i18n.ts.notificationDisplay }}</template> + </MkFoldableSection> + <MkFoldableSection :expanded="false" class="item"> + <template #header>{{ i18n.ts.notificationDisplay }}</template> <div class="_gaps_m"> <MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch> @@ -117,11 +174,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton> </div> - </FormSection> - - <FormSection> - <template #label>{{ i18n.ts.appearance }}</template> - + </MkFoldableSection> + <MkFoldableSection :expanded="false" class="item"> + <template #header>{{ i18n.ts.appearance }}</template> <div class="_gaps_m"> <div class="_gaps_s"> <MkSwitch v-model="reduceAnimation">{{ i18n.ts.reduceUiAnimation }}</MkSwitch> @@ -134,6 +189,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch> <MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch> <MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch> + <MkSwitch v-model="enableGamingMode">{{ i18n.ts.gamingMode }} <template #caption>{{ i18n.ts.gamingModeInfo }} </template></MkSwitch> + <MkSwitch v-model="enableonlyAndWithSave">{{ i18n.ts.onlyAndWithSave }}<template #caption>{{ i18n.ts.onlyAndWithSaveInfo }} </template></MkSwitch> + <MkSwitch v-model="enablehanntenn">{{ i18n.ts.hanntenn }}<template #caption>{{ i18n.ts.hanntennInfo }} </template></MkSwitch> <MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch> <MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch> </div> @@ -155,10 +213,9 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="3"><span style="font-size: 17px;">Aa</span></option> </MkRadios> </div> - </FormSection> - - <FormSection> - <template #label>{{ i18n.ts.behavior }}</template> + </MkFoldableSection> + <MkFoldableSection :expanded="false" class="item"> + <template #header>{{ i18n.ts.behavior }}</template> <div class="_gaps_m"> <div class="_gaps_s"> @@ -180,7 +237,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.numberOfPageCache }}</template> <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template> </MkRange> - + <MkRange v-model="numberOfGamingSpeed" :min="1" :max="60" :step="1" easing> + <template #label>{{ i18n.ts.GamingSpeedChange }}</template> + <template #caption>{{ i18n.ts.GamingSpeedChangeInfo }}</template> + </MkRange> <MkFolder> <template #label>{{ i18n.ts.dataSaver }}</template> @@ -212,8 +272,94 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> </div> - </FormSection> + </MkFoldableSection> + <MkFoldableSection :expanded="false"> + <template #header>他のサーバーのローカルタイムラインを覗けるようにする</template> + <div class="_gaps_m"> + <div v-if="maxLocalTimeline >= 1"> + <MkInput v-model="remoteLocalTimelineName1" placeholder="prismisskey"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineDomain1" placeholder="prismisskey.space"> + <template #label>サーバーURL</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineToken1" placeholder=""> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.accessToken }}</template> + </MkInput> + <MkSwitch v-model="remoteLocalTimelineEnable1"> + {{ i18n.ts.enable }} + </MkSwitch> + </div> + <div v-if="maxLocalTimeline >= 2"> + <MkInput v-model="remoteLocalTimelineName2" placeholder="prismisskey"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineDomain2" placeholder="prismisskey.space"> + <template #label>サーバーURL</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineToken2" placeholder=""> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.accessToken }}</template> + </MkInput> + <MkSwitch v-model="remoteLocalTimelineEnable2"> + {{ i18n.ts.enable }} + </MkSwitch> + </div> + <div v-if="maxLocalTimeline >= 3"> + <MkInput v-model="remoteLocalTimelineName3" placeholder="prismisskey"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineDomain3" placeholder="prismisskey.space"> + <template #label>サーバーURL</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineToken3" placeholder=""> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.accessToken }}</template> + </MkInput> + <MkSwitch v-model="remoteLocalTimelineEnable3"> + {{ i18n.ts.enable }} + </MkSwitch> + </div> + + <div v-if="maxLocalTimeline >= 4"> + <MkInput v-model="remoteLocalTimelineName4" placeholder="prismisskey"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineDomain4" placeholder="prismisskey.space"> + <template #label>サーバーURL</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineToken4" placeholder=""> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.accessToken }}</template> + </MkInput> + <MkSwitch v-model="remoteLocalTimelineEnable4"> + {{ i18n.ts.enable }} + </MkSwitch> + </div> + + <div v-if="maxLocalTimeline >= 5"> + <MkInput v-model="remoteLocalTimelineName5" placeholder="prismisskey"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineDomain5" placeholder="prismisskey.space"> + <template #label>サーバーURL</template> + </MkInput> + <MkInput v-model="remoteLocalTimelineToken5" placeholder=""> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.accessToken }}</template> + </MkInput> + <MkSwitch v-model="remoteLocalTimelineEnable5"> + {{ i18n.ts.enable }} + </MkSwitch> + </div> + + <MkButton @click="remoteLocaltimelineSave"> + {{ i18n.ts.save }} + </MkButton> + </div> + </MkFoldableSection> <FormSection> <template #label>{{ i18n.ts.other }}</template> @@ -250,13 +396,17 @@ import MkInfo from '@/components/MkInfo.vue'; import { langs } from '@/config.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { signinRequired } from '@/account.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; import { globalEvents } from '@/events.js'; import { claimAchievement } from '@/scripts/achievements.js'; +import MkColorInput from '@/components/MkColorInput.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import MkInput from '@/components/MkInput.vue'; +import { userChannelFollowingsCache, userChannelsCache, userFavoriteListsCache, userListsCache } from '@/cache.js'; const lang = ref(miLocalStorage.getItem('lang')); const fontSize = ref(miLocalStorage.getItem('fontSize')); @@ -299,7 +449,17 @@ const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm')); const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel')); +const alwaysShowPlayer = computed(defaultStore.makeGetterSetter('alwaysShowPlayer')); + +const disableNoteDrafting = computed(defaultStore.makeGetterSetter('disableNoteDrafting')); +const draftSavingBehavior = computed(defaultStore.makeGetterSetter('draftSavingBehavior')); +const alwaysExpandTweet = computed(defaultStore.makeGetterSetter('alwaysExpandTweet')); const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache')); +const numberOfGamingSpeed = computed(defaultStore.makeGetterSetter('numberOfGamingSpeed')); +const homeColor = computed(defaultStore.makeGetterSetter('homeColor')); +const followerColor = computed(defaultStore.makeGetterSetter('followerColor')); +const specifiedColor = computed(defaultStore.makeGetterSetter('specifiedColor')); +const localOnlyColor = computed(defaultStore.makeGetterSetter('localOnlyColor')); const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')); const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu')); @@ -309,19 +469,77 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter(' const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); +const enableGamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const enableonlyAndWithSave = computed(defaultStore.makeGetterSetter('onlyAndWithSave')); +const enablehanntenn = computed(defaultStore.makeGetterSetter('enablehanntenn')); +const showMediaTimeline = computed(defaultStore.makeGetterSetter('showMediaTimeline')); +const showGlobalTimeline = computed(defaultStore.makeGetterSetter('showGlobalTimeline')); +const showLocalTimeline = computed(defaultStore.makeGetterSetter('showLocalTimeline')); +const showHomeTimeline = computed(defaultStore.makeGetterSetter('showHomeTimeline')); +const showSocialTimeline = computed(defaultStore.makeGetterSetter('showSocialTimeline')); +const topBarNameShown = computed(defaultStore.makeGetterSetter('topBarNameShown')); +const showVisibilityColor = computed(defaultStore.makeGetterSetter('showVisibilityColor')); const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect')); const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe')); +const remoteLocalTimelineDomain1 = ref(defaultStore.state['remoteLocalTimelineDomain1']); +const remoteLocalTimelineToken1 = ref(defaultStore.state['remoteLocalTimelineToken1']); +const remoteLocalTimelineDomain2 = ref(defaultStore.state['remoteLocalTimelineDomain2']); +const remoteLocalTimelineToken2 = ref(defaultStore.state['remoteLocalTimelineToken2']); +const remoteLocalTimelineDomain3 = ref(defaultStore.state['remoteLocalTimelineDomain3']); +const remoteLocalTimelineToken3 = ref(defaultStore.state['remoteLocalTimelineToken3']); +const remoteLocalTimelineDomain4 = ref(defaultStore.state['remoteLocalTimelineDomain4']); +const remoteLocalTimelineToken4 = ref(defaultStore.state['remoteLocalTimelineToken4']); +const remoteLocalTimelineDomain5 = ref(defaultStore.state['remoteLocalTimelineDomain5']); +const remoteLocalTimelineToken5 = ref(defaultStore.state['remoteLocalTimelineToken5']); +const remoteLocalTimelineName1 = ref(defaultStore.state['remoteLocalTimelineName1']); +const remoteLocalTimelineName2 = ref(defaultStore.state['remoteLocalTimelineName2']); +const remoteLocalTimelineName3 = ref(defaultStore.state['remoteLocalTimelineName3']); +const remoteLocalTimelineName4 = ref(defaultStore.state['remoteLocalTimelineName4']); +const remoteLocalTimelineName5 = ref(defaultStore.state['remoteLocalTimelineName5']); const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer')); const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow')); +const remoteLocalTimelineEnable1 = computed(defaultStore.makeGetterSetter('remoteLocalTimelineEnable1')); +const remoteLocalTimelineEnable2 = computed(defaultStore.makeGetterSetter('remoteLocalTimelineEnable2')); +const remoteLocalTimelineEnable3 = computed(defaultStore.makeGetterSetter('remoteLocalTimelineEnable3')); +const remoteLocalTimelineEnable4 = computed(defaultStore.makeGetterSetter('remoteLocalTimelineEnable4')); +const remoteLocalTimelineEnable5 = computed(defaultStore.makeGetterSetter('remoteLocalTimelineEnable5')); +const $i = signinRequired(); +const pinnedMax = $i.policies.listPinnedLimit; +const maxLocalTimeline = $i.policies.localTimelineAnyLimit; watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); miLocalStorage.removeItem('locale'); miLocalStorage.removeItem('localeVersion'); }); +document.documentElement.style.setProperty('--gamingspeed', numberOfGamingSpeed.value + 's'); + +function hexToRgb(hex) { + // 16進数のカラーコードから "#" を除去 + hex = hex.replace(/^#/, ''); + + // 16進数をRGBに変換 + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return `${r},${g},${b}`; +} + +document.documentElement.style.setProperty('--homeColor', hexToRgb(homeColor.value)); +document.documentElement.style.setProperty('--followerColor', hexToRgb(followerColor.value)); +document.documentElement.style.setProperty('--specifiedColor', hexToRgb(specifiedColor.value)); +watch([homeColor, specifiedColor, followerColor], () => { + document.documentElement.style.setProperty('--homeColor', hexToRgb(homeColor.value)); + document.documentElement.style.setProperty('--followerColor', hexToRgb(followerColor.value)); + document.documentElement.style.setProperty('--specifiedColor', hexToRgb(specifiedColor.value)); +}); +watch(numberOfGamingSpeed, () => { + document.documentElement.style.setProperty('--gamingspeed', numberOfGamingSpeed.value + 's'); +}); watch(fontSize, () => { if (fontSize.value == null) { miLocalStorage.removeItem('fontSize'); @@ -354,13 +572,46 @@ watch([ limitWidthOfReaction, highlightSensitiveMedia, keepScreenOn, + showMediaTimeline, + showVisibilityColor, + enableonlyAndWithSave, + showGlobalTimeline, + showSocialTimeline, + showLocalTimeline, + showHomeTimeline, + topBarNameShown, disableStreamingTimeline, enableSeasonalScreenEffect, alwaysConfirmFollow, + alwaysShowPlayer, + alwaysExpandTweet, ], async () => { await reloadAsk(); }); +async function remoteLocaltimelineSave() { + os.alert({ + type: 'success', + text: i18n.ts.saved, + }); + defaultStore.set('remoteLocalTimelineDomain1', remoteLocalTimelineDomain1.value); + defaultStore.set('remoteLocalTimelineToken1', remoteLocalTimelineToken1.value); + defaultStore.set('remoteLocalTimelineDomain2', remoteLocalTimelineDomain2.value); + defaultStore.set('remoteLocalTimelineToken2', remoteLocalTimelineToken2.value); + defaultStore.set('remoteLocalTimelineDomain3', remoteLocalTimelineDomain3.value); + defaultStore.set('remoteLocalTimelineToken3', remoteLocalTimelineToken3.value); + defaultStore.set('remoteLocalTimelineDomain4', remoteLocalTimelineDomain4.value); + defaultStore.set('remoteLocalTimelineToken4', remoteLocalTimelineToken4.value); + defaultStore.set('remoteLocalTimelineDomain5', remoteLocalTimelineDomain5.value); + defaultStore.set('remoteLocalTimelineToken5', remoteLocalTimelineToken5.value); + defaultStore.set('remoteLocalTimelineName1', remoteLocalTimelineName1.value); + defaultStore.set('remoteLocalTimelineName2', remoteLocalTimelineName2.value); + defaultStore.set('remoteLocalTimelineName3', remoteLocalTimelineName3.value); + defaultStore.set('remoteLocalTimelineName4', remoteLocalTimelineName4.value); + defaultStore.set('remoteLocalTimelineName5', remoteLocalTimelineName5.value); + await reloadAsk(); +} + const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const; function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) { @@ -406,7 +657,9 @@ function removeEmojiIndex(lang: string) { } async function setPinnedList() { - const lists = await misskeyApi('users/lists/list'); + const myLists = await userListsCache.fetch(); + const favoriteLists = await userFavoriteListsCache.fetch(); + let lists = [...new Set([...myLists, ...favoriteLists])]; const { canceled, result: list } = await os.select({ title: i18n.ts.selectList, items: lists.map(x => ({ @@ -414,12 +667,73 @@ async function setPinnedList() { })), }); if (canceled) return; + let pinnedLists = defaultStore.state.pinnedUserLists; - defaultStore.set('pinnedUserLists', [list]); + // Check if the id is already present in pinnedLists + if (!pinnedLists.some(pinnedList => pinnedList.id === list.id)) { + pinnedLists.push(list); + defaultStore.set('pinnedUserLists', pinnedLists); + } } -function removePinnedList() { - defaultStore.set('pinnedUserLists', []); +async function removePinnedList(id, name?:string) { + if (!id) return; + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.removeAreYouSure({ x: name ?? id }), + }); + if (canceled) return; + + if (id === 'all') { + if (canceled) return; + + defaultStore.set('pinnedUserLists', []); + return; + } + + const pinnedLists = defaultStore.state.pinnedUserLists; + const newPinnedLists = pinnedLists.filter(pinnedList => pinnedList.id !== id); + defaultStore.set('pinnedUserLists', newPinnedLists); +} + +async function setPinnedChannel() { + const myChannels = await userChannelsCache.fetch(); + const favoriteChannels = await userChannelFollowingsCache.fetch(); + let channels = [...new Set([...myChannels, ...favoriteChannels])]; + const { canceled, result: channel } = await os.select({ + title: i18n.ts.selectList, + items: channels.map(x => ({ + value: x, text: x.name, + })), + }); + if (canceled) return; + let pinnedChannels = defaultStore.state.pinnedChannels; + + // Check if the id is already present in pinnedLists + if (!pinnedChannels.some(pinnedChannel => pinnedChannel.id === channel.id)) { + pinnedChannels.push(channel); + defaultStore.set('pinnedChannels', pinnedChannels); + } +} + +async function removePinnedChannel(id, name?:string) { + if (!id) return; + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.removeAreYouSure({ x: name ?? id }), + }); + if (canceled) return; + + if (id === 'all') { + if (canceled) return; + + defaultStore.set('pinnedChannels', []); + return; + } + + const pinnedChannels = defaultStore.state.pinnedChannels; + const newPinnedChannels = pinnedChannels.filter(pinnedchannel => pinnedchannel.id !== id); + defaultStore.set('pinnedChannels', newPinnedChannels); } let smashCount = 0; diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index fa868c1c0f..50e3f4c5d2 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -23,7 +23,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </MkSpacer> - <MkFooterSpacer/> </mkstickycontainer> </template> @@ -115,6 +114,11 @@ const menuDef = computed(() => [{ text: i18n.ts.navbar, to: '/settings/navbar', active: currentPage.value?.route.name === 'navbar', + }, { + icon: 'ti ti-layout-navbar', + text: i18n.ts.timelineHeader, + to: '/settings/timeline-header', + active: currentPage.value?.route.name === 'timelineHeader', }, { icon: 'ti ti-equal-double', text: i18n.ts.statusbar, diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 2653b5e2a7..a786e43732 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -4,125 +4,125 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps_m"> - <MkFolder> - <template #icon><i class="ti ti-message-off"></i></template> - <template #label>{{ i18n.ts.wordMute }}</template> + <div class="_gaps_m"> + <MkFolder> + <template #icon><i class="ti ti-message-off"></i></template> + <template #label>{{ i18n.ts.wordMute }}</template> - <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/> - </MkFolder> + <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/> + </MkFolder> - <MkFolder> - <template #icon><i class="ti ti-message-off"></i></template> - <template #label>{{ i18n.ts.hardWordMute }}</template> + <MkFolder> + <template #icon><i class="ti ti-message-off"></i></template> + <template #label>{{ i18n.ts.hardWordMute }}</template> - <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/> - </MkFolder> + <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/> + </MkFolder> - <MkFolder> - <template #icon><i class="ti ti-planet-off"></i></template> - <template #label>{{ i18n.ts.instanceMute }}</template> + <MkFolder> + <template #icon><i class="ti ti-planet-off"></i></template> + <template #label>{{ i18n.ts.instanceMute }}</template> - <XInstanceMute/> - </MkFolder> + <XInstanceMute/> + </MkFolder> - <MkFolder> - <template #icon><i class="ti ti-repeat-off"></i></template> - <template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template> + <MkFolder> + <template #icon><i class="ti ti-repeat-off"></i></template> + <template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template> - <MkPagination :pagination="renoteMutingPagination"> - <template #empty> - <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> - <div>{{ i18n.ts.noUsers }}</div> - </div> - </template> + <MkPagination :pagination="renoteMutingPagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> - <template #default="{ items }"> - <div class="_gaps_s"> - <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]"> - <div :class="$style.userItemMain"> - <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)"> - <MkUserCardMini :user="item.mutee"/> - </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> - <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button> - </div> - <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub"> - <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> - </div> - </div> - </div> - </template> - </MkPagination> - </MkFolder> + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]"> + <div :class="$style.userItemMain"> + <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)"> + <MkUserCardMini :user="item.mutee"/> + </MkA> + <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button> + </div> + <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub"> + <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> + </div> + </div> + </div> + </template> + </MkPagination> + </MkFolder> - <MkFolder> - <template #icon><i class="ti ti-eye-off"></i></template> - <template #label>{{ i18n.ts.mutedUsers }}</template> + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> + <template #label>{{ i18n.ts.mutedUsers }}</template> - <MkPagination :pagination="mutingPagination"> - <template #empty> - <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> - <div>{{ i18n.ts.noUsers }}</div> - </div> - </template> + <MkPagination :pagination="mutingPagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> - <template #default="{ items }"> - <div class="_gaps_s"> - <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]"> - <div :class="$style.userItemMain"> - <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)"> - <MkUserCardMini :user="item.mutee"/> - </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> - <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button> - </div> - <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> - <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> - <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> - <div v-else>Period: {{ i18n.ts.indefinitely }}</div> - </div> - </div> - </div> - </template> - </MkPagination> - </MkFolder> + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]"> + <div :class="$style.userItemMain"> + <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)"> + <MkUserCardMini :user="item.mutee"/> + </MkA> + <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button> + </div> + <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> + <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> + <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> + <div v-else>Period: {{ i18n.ts.indefinitely }}</div> + </div> + </div> + </div> + </template> + </MkPagination> + </MkFolder> - <MkFolder> - <template #icon><i class="ti ti-ban"></i></template> - <template #label>{{ i18n.ts.blockedUsers }}</template> + <MkFolder> + <template #icon><i class="ti ti-ban"></i></template> + <template #label>{{ i18n.ts.blockedUsers }}</template> - <MkPagination :pagination="blockingPagination"> - <template #empty> - <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> - <div>{{ i18n.ts.noUsers }}</div> - </div> - </template> + <MkPagination :pagination="blockingPagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> - <template #default="{ items }"> - <div class="_gaps_s"> - <div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]"> - <div :class="$style.userItemMain"> - <MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)"> - <MkUserCardMini :user="item.blockee"/> - </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> - <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button> - </div> - <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> - <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> - <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> - <div v-else>Period: {{ i18n.ts.indefinitely }}</div> - </div> - </div> - </div> - </template> - </MkPagination> - </MkFolder> -</div> + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]"> + <div :class="$style.userItemMain"> + <MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)"> + <MkUserCardMini :user="item.blockee"/> + </MkA> + <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button> + </div> + <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> + <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> + <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> + <div v-else>Period: {{ i18n.ts.indefinitely }}</div> + </div> + </div> + </div> + </template> + </MkPagination> + </MkFolder> + </div> </template> <script lang="ts" setup> @@ -143,18 +143,18 @@ import MkFolder from '@/components/MkFolder.vue'; const $i = signinRequired(); const renoteMutingPagination = { - endpoint: 'renote-mute/list' as const, - limit: 10, + endpoint: 'renote-mute/list' as const, + limit: 10, }; const mutingPagination = { - endpoint: 'mute/list' as const, - limit: 10, + endpoint: 'mute/list' as const, + limit: 10, }; const blockingPagination = { - endpoint: 'blocking/list' as const, - limit: 10, + endpoint: 'blocking/list' as const, + limit: 10, }; const expandedRenoteMuteItems = ref([]); @@ -162,68 +162,68 @@ const expandedMuteItems = ref([]); const expandedBlockItems = ref([]); async function unrenoteMute(user, ev) { - os.popupMenu([{ - text: i18n.ts.renoteUnmute, - icon: 'ti ti-x', - action: async () => { - await os.apiWithDialog('renote-mute/delete', { userId: user.id }); - //role.users = role.users.filter(u => u.id !== user.id); - }, - }], ev.currentTarget ?? ev.target); + os.popupMenu([{ + text: i18n.ts.renoteUnmute, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('renote-mute/delete', { userId: user.id }); + //role.users = role.users.filter(u => u.id !== user.id); + }, + }], ev.currentTarget ?? ev.target); } async function unmute(user, ev) { - os.popupMenu([{ - text: i18n.ts.unmute, - icon: 'ti ti-x', - action: async () => { - await os.apiWithDialog('mute/delete', { userId: user.id }); - //role.users = role.users.filter(u => u.id !== user.id); - }, - }], ev.currentTarget ?? ev.target); + os.popupMenu([{ + text: i18n.ts.unmute, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('mute/delete', { userId: user.id }); + //role.users = role.users.filter(u => u.id !== user.id); + }, + }], ev.currentTarget ?? ev.target); } async function unblock(user, ev) { - os.popupMenu([{ - text: i18n.ts.unblock, - icon: 'ti ti-x', - action: async () => { - await os.apiWithDialog('blocking/delete', { userId: user.id }); - //role.users = role.users.filter(u => u.id !== user.id); - }, - }], ev.currentTarget ?? ev.target); + os.popupMenu([{ + text: i18n.ts.unblock, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('blocking/delete', { userId: user.id }); + //role.users = role.users.filter(u => u.id !== user.id); + }, + }], ev.currentTarget ?? ev.target); } async function toggleRenoteMuteItem(item) { - if (expandedRenoteMuteItems.value.includes(item.id)) { - expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id); - } else { - expandedRenoteMuteItems.value.push(item.id); - } + if (expandedRenoteMuteItems.value.includes(item.id)) { + expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id); + } else { + expandedRenoteMuteItems.value.push(item.id); + } } async function toggleMuteItem(item) { - if (expandedMuteItems.value.includes(item.id)) { - expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id); - } else { - expandedMuteItems.value.push(item.id); - } + if (expandedMuteItems.value.includes(item.id)) { + expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id); + } else { + expandedMuteItems.value.push(item.id); + } } async function toggleBlockItem(item) { - if (expandedBlockItems.value.includes(item.id)) { - expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id); - } else { - expandedBlockItems.value.push(item.id); - } + if (expandedBlockItems.value.includes(item.id)) { + expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id); + } else { + expandedBlockItems.value.push(item.id); + } } async function saveMutedWords(mutedWords: (string | string[])[]) { - await misskeyApi('i/update', { mutedWords }); + await misskeyApi('i/update', { mutedWords }); } async function saveHardMutedWords(hardMutedWords: (string | string[])[]) { - await misskeyApi('i/update', { hardMutedWords }); + await misskeyApi('i/update', { hardMutedWords }); } const headerActions = computed(() => []); @@ -231,47 +231,47 @@ const headerActions = computed(() => []); const headerTabs = computed(() => []); definePageMetadata(() => ({ - title: i18n.ts.muteAndBlock, - icon: 'ti ti-ban', + title: i18n.ts.muteAndBlock, + icon: 'ti ti-ban', })); </script> <style lang="scss" module> .userItemMain { - display: flex; + display: flex; } .userItemSub { - padding: 6px 12px; - font-size: 85%; - color: var(--fgTransparentWeak); + padding: 6px 12px; + font-size: 85%; + color: var(--fgTransparentWeak); } .userItemMainBody { - flex: 1; - min-width: 0; - margin-right: 8px; + flex: 1; + min-width: 0; + margin-right: 8px; - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; + } } .userToggle, .remove { - width: 32px; - height: 32px; - align-self: center; + width: 32px; + height: 32px; + align-self: center; } .chevron { - display: block; - transition: transform 0.1s ease-out; + display: block; + transition: transform 0.1s ease-out; } .userItem.userItemOpend { - .chevron { - transform: rotateX(180deg); - } + .chevron { + transform: rotateX(180deg); + } } </style> diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue index 72c148362e..66c63865f5 100644 --- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue @@ -4,15 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps_m"> - <div> - <MkTextarea v-model="mutedWords"> - <span>{{ i18n.ts._wordMute.muteWords }}</span> - <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> - </MkTextarea> - </div> - <MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> -</div> + <div class="_gaps_m"> + <div> + <MkTextarea v-model="mutedWords"> + <span>{{ i18n.ts._wordMute.muteWords }}</span> + <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> + </MkTextarea> + </div> + <MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + </div> </template> <script lang="ts" setup> @@ -23,70 +23,70 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - muted: (string[] | string)[]; + muted: (string[] | string)[]; }>(); const emit = defineEmits<{ - (ev: 'save', value: (string[] | string)[]): void; + (ev: 'save', value: (string[] | string)[]): void; }>(); const render = (mutedWords) => mutedWords.map(x => { - if (Array.isArray(x)) { - return x.join(' '); - } else { - return x; - } + if (Array.isArray(x)) { + return x.join(' '); + } else { + return x; + } }).join('\n'); const mutedWords = ref(render(props.muted)); const changed = ref(false); watch(mutedWords, () => { - changed.value = true; + changed.value = true; }); async function save() { - const parseMutes = (mutes) => { - // split into lines, remove empty lines and unnecessary whitespace - let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== ''); + const parseMutes = (mutes) => { + // split into lines, remove empty lines and unnecessary whitespace + let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== ''); - // check each line if it is a RegExp or not - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const regexp = line.match(/^\/(.+)\/(.*)$/); - if (regexp) { - // check that the RegExp is valid - try { - new RegExp(regexp[1], regexp[2]); - // note that regex lines will not be split by spaces! - } catch (err: any) { - // invalid syntax: do not save, do not reset changed flag - os.alert({ - type: 'error', - title: i18n.ts.regexpError, - text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(), - }); - // re-throw error so these invalid settings are not saved - throw err; - } - } else { - lines[i] = line.split(' '); - } - } + // check each line if it is a RegExp or not + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const regexp = line.match(/^\/(.+)\/(.*)$/); + if (regexp) { + // check that the RegExp is valid + try { + new RegExp(regexp[1], regexp[2]); + // note that regex lines will not be split by spaces! + } catch (err: any) { + // invalid syntax: do not save, do not reset changed flag + os.alert({ + type: 'error', + title: i18n.ts.regexpError, + text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(), + }); + // re-throw error so these invalid settings are not saved + throw err; + } + } else { + lines[i] = line.split(' '); + } + } - return lines; - }; + return lines; + }; - let parsed; - try { - parsed = parseMutes(mutedWords.value); - } catch (err) { - // already displayed error message in parseMutes - return; - } + let parsed; + try { + parsed = parseMutes(mutedWords.value); + } catch (err) { + // already displayed error message in parseMutes + return; + } - emit('save', parsed); + emit('save', parsed); - changed.value = false; + changed.value = false; } </script> diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 8cc07c5c63..0f7531a1a8 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -81,11 +81,11 @@ const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadM const userLists = await misskeyApi('users/lists/list'); async function readAllUnreadNotes() { - await os.apiWithDialog('i/read-all-unread-notes'); + await misskeyApi('i/read-all-unread-notes'); } async function readAllNotifications() { - await os.apiWithDialog('notifications/mark-all-as-read'); + await misskeyApi('notifications/mark-all-as-read'); } async function updateReceiveConfig(type, value) { diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 85bdbe75cc..b8de19fd4e 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #key>{{ i18n.ts.registeredDate }}</template> <template #value><MkTime :time="$i.createdAt" mode="detail"/></template> </MkKeyValue> + <FormLink to="/settings/account-stats"><template #icon><i class="ti ti-info-circle"></i></template>{{ i18n.ts.statistics }}</FormLink> </div> </MkFolder> diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 3db4638a8d..6950edfa0b 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -61,7 +61,9 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="followers">{{ i18n.ts._visibility.followers }}</option> <option value="specified">{{ i18n.ts._visibility.specified }}</option> </MkSelect> - <MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch> + <MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.public }}+{{ i18n.ts._visibility.disableFederation }}</MkSwitch> + <MkSwitch v-model="defaultHomeNoteLocalOnly">{{ i18n.ts._visibility.home }}+{{ i18n.ts._visibility.disableFederation }}</MkSwitch> + <MkSwitch v-model="defaultFollowersNoteLocalOnly">{{ i18n.ts._visibility.followers }}+{{ i18n.ts._visibility.disableFederation }}</MkSwitch> </div> </MkFolder> </div> @@ -97,6 +99,8 @@ const followersVisibility = ref($i.followersVisibility); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); +const defaultHomeNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultHomeNoteLocalOnly')); +const defaultFollowersNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultFollowersNoteLocalOnly')); const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); const keepCw = computed(defaultStore.makeGetterSetter('keepCw')); diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 58b00a37da..6a3b35271f 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -92,7 +92,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.advancedSettings }}</template> <div class="_gaps_m"> - <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> + <MkSwitch :disabled="profile.isGorilla" v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> + <MkSwitch :disabled="profile.isCat" v-model="profile.isGorilla">{{ i18n.ts.flagAsGorilla }}<template #caption>{{ i18n.ts.flagAsGorillaDescription }}</template></MkSwitch> <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> </div> </MkFolder> @@ -143,6 +144,7 @@ const profile = reactive({ lang: $i.lang, isBot: $i.isBot ?? false, isCat: $i.isCat ?? false, + isGorilla: $i.isGorilla, }); watch(() => profile, () => { @@ -192,6 +194,7 @@ function save() { lang: profile.lang || null, isBot: !!profile.isBot, isCat: !!profile.isCat, + isGorilla: !!profile.isGorilla, }); globalEvents.emit('requestClearPageCache'); claimAchievement('profileFilled'); @@ -201,6 +204,9 @@ function save() { if (profile.isCat) { claimAchievement('markedAsCat'); } + if (profile.isGorilla) { + claimAchievement('markedAsGorilla'); + } } function changeAvatar(ev) { diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index 07c0236279..65e15fe68e 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -4,90 +4,107 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps_m rsljpzjq"> - <div v-adaptive-border class="rfqxtzch _panel"> - <div class="toggle"> - <div class="toggleWrapper"> - <input id="dn" v-model="darkMode" type="checkbox" class="dn"/> - <label for="dn" class="toggle"> - <span class="before">{{ i18n.ts.light }}</span> - <span class="after">{{ i18n.ts.dark }}</span> - <span class="toggle__handler"> + <div class="_gaps_m rsljpzjq"> + <div v-adaptive-border class="rfqxtzch _panel"> + <div class="toggle"> + <div class="toggleWrapper"> + <input id="dn" v-model="darkMode" type="checkbox" class="dn"/> + <label for="dn" class="toggle"> + <span class="before">{{ i18n.ts.light }}</span> + <span class="after">{{ i18n.ts.dark }}</span> + <span class="toggle__handler"> <span class="crater crater--1"></span> <span class="crater crater--2"></span> <span class="crater crater--3"></span> </span> - <span class="star star--1"></span> - <span class="star star--2"></span> - <span class="star star--3"></span> - <span class="star star--4"></span> - <span class="star star--5"></span> - <span class="star star--6"></span> - </label> + <span class="star star--1"></span> + <span class="star star--2"></span> + <span class="star star--3"></span> + <span class="star star--4"></span> + <span class="star star--5"></span> + <span class="star star--6"></span> + </label> + </div> + </div> + <div class="sync"> + <MkSwitch v-model="syncDeviceDarkMode">{{ i18n.ts.syncDeviceDarkMode }}</MkSwitch> </div> </div> - <div class="sync"> - <MkSwitch v-model="syncDeviceDarkMode">{{ i18n.ts.syncDeviceDarkMode }}</MkSwitch> + + <div class="selects"> + <MkSelect v-model="lightThemeId" large class="select"> + <template #label>{{ i18n.ts.themeForLightMode }}</template> + <template #prefix><i class="ti ti-sun"></i></template> + <option v-if="instanceLightTheme" :key="'instance:' + instanceLightTheme.id" :value="instanceLightTheme.id"> + {{ instanceLightTheme.name }} + </option> + <optgroup v-if="installedLightThemes.length > 0" :label="i18n.ts._theme.installedThemes"> + <option v-for="x in installedLightThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="i18n.ts._theme.builtinThemes"> + <option v-for="x in builtinLightThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option> + </optgroup> + </MkSelect> + <MkSelect v-model="darkThemeId" large class="select"> + <template #label>{{ i18n.ts.themeForDarkMode }}</template> + <template #prefix><i class="ti ti-moon"></i></template> + <option v-if="instanceDarkTheme" :key="'instance:' + instanceDarkTheme.id" :value="instanceDarkTheme.id"> + {{ instanceDarkTheme.name }} + </option> + <optgroup v-if="installedDarkThemes.length > 0" :label="i18n.ts._theme.installedThemes"> + <option v-for="x in installedDarkThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="i18n.ts._theme.builtinThemes"> + <option v-for="x in builtinDarkThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option> + </optgroup> + </MkSelect> </div> + + <FormSection> + <div class="_formLinksGrid"> + <FormLink to="/settings/theme/manage"> + <template #icon><i class="ti ti-tool"></i></template> + {{ i18n.ts._theme.manage }} + <template #suffix>{{ themesCount }}</template> + </FormLink> + <FormLink to="https://assets.misskey.io/theme/list" external> + <template #icon><i class="ti ti-world"></i></template> + {{ i18n.ts._theme.explore }} + </FormLink> + <FormLink to="/settings/theme/install"> + <template #icon><i class="ti ti-download"></i></template> + {{ i18n.ts._theme.install }} + </FormLink> + <FormLink to="/theme-editor"> + <template #icon><i class="ti ti-paint"></i></template> + {{ i18n.ts._theme.make }} + </FormLink> + </div> + </FormSection> + + <MkButton v-if="wallpaper == null" @click="setWallpaper">{{ i18n.ts.setWallpaper }}</MkButton> + <MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton> </div> - - <div class="selects"> - <MkSelect v-model="lightThemeId" large class="select"> - <template #label>{{ i18n.ts.themeForLightMode }}</template> - <template #prefix><i class="ti ti-sun"></i></template> - <option v-if="instanceLightTheme" :key="'instance:' + instanceLightTheme.id" :value="instanceLightTheme.id">{{ instanceLightTheme.name }}</option> - <optgroup v-if="installedLightThemes.length > 0" :label="i18n.ts._theme.installedThemes"> - <option v-for="x in installedLightThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="i18n.ts._theme.builtinThemes"> - <option v-for="x in builtinLightThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option> - </optgroup> - </MkSelect> - <MkSelect v-model="darkThemeId" large class="select"> - <template #label>{{ i18n.ts.themeForDarkMode }}</template> - <template #prefix><i class="ti ti-moon"></i></template> - <option v-if="instanceDarkTheme" :key="'instance:' + instanceDarkTheme.id" :value="instanceDarkTheme.id">{{ instanceDarkTheme.name }}</option> - <optgroup v-if="installedDarkThemes.length > 0" :label="i18n.ts._theme.installedThemes"> - <option v-for="x in installedDarkThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="i18n.ts._theme.builtinThemes"> - <option v-for="x in builtinDarkThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option> - </optgroup> - </MkSelect> - </div> - - <FormSection> - <div class="_formLinksGrid"> - <FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink> - <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ti ti-world"></i></template>{{ i18n.ts._theme.explore }}</FormLink> - <FormLink to="/settings/theme/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._theme.install }}</FormLink> - <FormLink to="/theme-editor"><template #icon><i class="ti ti-paint"></i></template>{{ i18n.ts._theme.make }}</FormLink> - </div> - </FormSection> - - <MkButton v-if="wallpaper == null" @click="setWallpaper">{{ i18n.ts.setWallpaper }}</MkButton> - <MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton> -</div> </template> <script lang="ts" setup> -import { computed, onActivated, ref, watch } from 'vue'; +import {computed, onActivated, ref, watch} from 'vue'; import JSON5 from 'json5'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import MkButton from '@/components/MkButton.vue'; -import { getBuiltinThemesRef } from '@/scripts/theme.js'; -import { selectFile } from '@/scripts/select-file.js'; -import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; -import { uniqueBy } from '@/scripts/array.js'; -import { fetchThemes, getThemes } from '@/theme-store.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { miLocalStorage } from '@/local-storage.js'; +import {getBuiltinThemesRef} from '@/scripts/theme'; +import {selectFile} from '@/scripts/select-file'; +import {isDeviceDarkmode} from '@/scripts/is-device-darkmode'; +import {ColdDeviceStorage, defaultStore, bannerDark, bannerLight, iconDark, iconLight} from '@/store'; +import {i18n} from '@/i18n'; +import {instance} from '@/instance'; +import {uniqueBy} from '@/scripts/array'; +import {fetchThemes, getThemes} from '@/theme-store.js'; +import {definePageMetadata} from '@/scripts/page-metadata'; +import {miLocalStorage} from '@/local-storage'; import { unisonReload } from '@/scripts/unison-reload.js'; import * as os from '@/os.js'; @@ -141,6 +158,18 @@ const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); const wallpaper = ref(miLocalStorage.getItem('wallpaper')); const themesCount = installedThemes.value.length; +watch(darkMode, () => { + if (darkMode.value) { + defaultStore.set('bannerUrl', bannerDark) + defaultStore.set('iconUrl', iconDark) + }else if(!darkMode.value) { + defaultStore.set('bannerUrl', bannerLight) + defaultStore.set('iconUrl', iconLight) + }else{ + defaultStore.set('bannerUrl', bannerDark) + defaultStore.set('iconUrl', iconDark) + } +}) watch(syncDeviceDarkMode, () => { if (syncDeviceDarkMode.value) { @@ -250,9 +279,9 @@ definePageMetadata(() => ({ height: 50px - 6; background-color: #FFCF96; border-radius: 50px; - box-shadow: 0 2px 6px rgba(0,0,0,.3); + box-shadow: 0 2px 6px rgba(0, 0, 0, .3); transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; - transform: rotate(-45deg); + transform: rotate(-45deg); .crater { position: absolute; @@ -328,7 +357,7 @@ definePageMetadata(() => ({ z-index: 0; width: 2px; height: 2px; - transform: translate3d(3px,0,0); + transform: translate3d(3px, 0, 0); } .star--5 { @@ -337,7 +366,7 @@ definePageMetadata(() => ({ z-index: 0; width: 3px; height: 3px; - transform: translate3d(3px,0,0); + transform: translate3d(3px, 0, 0); } .star--6 { @@ -346,7 +375,7 @@ definePageMetadata(() => ({ z-index: 0; width: 2px; height: 2px; - transform: translate3d(3px,0,0); + transform: translate3d(3px, 0, 0); } input:checked { @@ -365,7 +394,9 @@ definePageMetadata(() => ({ background-color: #FFE5B5; transform: translate3d(40px, 0, 0) rotate(0); - .crater { opacity: 1; } + .crater { + opacity: 1; + } } .star--1 { @@ -389,7 +420,7 @@ definePageMetadata(() => ({ .star--5, .star--6 { opacity: 1; - transform: translate3d(0,0,0); + transform: translate3d(0, 0, 0); } .star--4 { diff --git a/packages/frontend/src/pages/settings/timelineHeader.vue b/packages/frontend/src/pages/settings/timelineHeader.vue new file mode 100644 index 0000000000..dcec07abd7 --- /dev/null +++ b/packages/frontend/src/pages/settings/timelineHeader.vue @@ -0,0 +1,193 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> +<template> +<div class="_gaps_m"> + <FormSlot> + <template #label>{{ i18n.ts.timelineHeader }}</template> + <MkContainer :showHeader="false"> + <div style="overflow-x: auto;"> + <Sortable + v-model="items" + itemKey="id" + :class="$style.container" + :animation="150" + :handle="'.' + $style.itemHandle" + @start="e => e.item.classList.add('active')" + @end="e => e.item.classList.remove('active')" + > + <template #item="{element,index}"> + <div + :class="$style.item" + > + <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button> + <i class="ti-fw" :class="[$style.itemIcon, timelineHeaderItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ timelineHeaderItemDef[element.type]?.title }}</span> + <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button> + </div> + </template> + </Sortable> + </div> + </MkContainer> + </FormSlot> + <MkFoldableSection> + <template #header>リモートのローカルタイムライン</template> + + <div v-if="remoteLocalTimeline.length < 3"> + <MkInput v-model="tmpName" placeholder="remoteLocalTimeline 1"/> + <MkInput v-model="tmpServer" placeholder="https://prismisskey.space"/> + <MkButton @click="addRemote"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> + </div> + + <div v-for="(a,i) in remoteLocalTimeline" :key="i"> + <MkInput v-model="remoteLocalTimeline[i]['name']" :placeholder="a"/> + <MkInput v-model="remoteLocalTimeline[i]['host']" :placeholder="a"/> + <MkButton danger @click="deleteRemote(i)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </MkFoldableSection> + <div class="_buttons"> + <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> + <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, defineAsyncComponent, ref } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSlot from '@/components/form/slot.vue'; +import MkContainer from '@/components/MkContainer.vue'; +import * as os from '@/os.js'; +import { defaultStore } from '@/store.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { timelineHeaderItemDef } from '@/timeline-header.js'; +import MkInput from '@/components/MkInput.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import { $i } from '@/account.js'; + +const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); +const tmpName = ref(); +const tmpServer = ref(); + +const items = ref(defaultStore.state.timelineHeader.map(x => ({ + id: Math.random().toString(), + type: x, +}))); +const remoteLocalTimeline = ref(defaultStore.state.remoteLocalTimeline); +const maxLocalTimeline = $i.policies.localTimelineAnyLimit; + +async function reloadAsk() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; + + unisonReload(); +} + +async function addRemote() { + if (!tmpName.value || !tmpServer.value) return; + if (maxLocalTimeline <= remoteLocalTimeline.value.length) return; + remoteLocalTimeline.value.push({ + id: Math.random().toString(), + name: tmpName.value, + host: tmpServer.value, + }); + tmpName.value = ''; + tmpServer.value = ''; + await defaultStore.set('remoteLocalTimeline', remoteLocalTimeline.value); +} + +const menu = computed(() => { + return Object.keys(timelineHeaderItemDef).filter(k => !items.value.map(item => item.type).includes(k)); +}); + +async function deleteRemote(index: number) { + remoteLocalTimeline.value.splice(index, 1); +} + +async function addItem() { + const { canceled, result: item } = await os.select({ + title: i18n.ts.addItem, + items: [...menu.value.map(k => ({ + value: k, text: timelineHeaderItemDef[k].title, + }))], + }); + if (canceled) return; + items.value = [...items.value, { + id: Math.random().toString(), + type: item, + }]; +} + +function removeItem(index: number) { + items.value.splice(index, 1); +} + +async function save() { + defaultStore.set('timelineHeader', items.value.map(x => x.type)); + await reloadAsk(); +} + +function reset() { + items.value = defaultStore.def.timelineHeader.default.map(x => ({ + id: Math.random().toString(), + type: x, + })); +} + +definePageMetadata(() => ({ + title: i18n.ts.navbar, + icon: 'ti ti-list', +})); +</script> + +<style lang="scss" module> +.item { + position: relative; + display: block; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: var(--navFg); + min-width: 200px; +} + +.itemIcon { + position: relative; + width: 32px; + margin-right: 8px; +} + +.itemText { + position: relative; + font-size: 0.9em; +} + +.itemRemove { + position: absolute; + z-index: 10000; + width: 32px; + height: 32px; + color: #ff2a2a; + right: 8px; + opacity: 0.8; +} + +.itemHandle { + cursor: move; + width: 32px; + height: 32px; + margin: 0 8px; + opacity: 0.5; +} + +.container{ + display: flex; +} +</style> diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 97a660d7b5..cc5d10bd83 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -12,7 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> {{ i18n.ts._timelineDescription[src] }} </MkInfo> - <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> + <MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostForm.value && ui !== 'twilike'" :channel="channelInfo" :autofocus="deviceKind === 'desktop'" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> + <XPostForm v-if="$i && ui === 'twilike' " :channel="channelInfo" :autofocus="deviceKind === 'desktop'" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div :class="$style.tl"> <MkTimeline @@ -20,9 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only :key="src + withRenotes + withReplies + onlyFiles" :src="src.split(':')[0]" :list="src.split(':')[1]" + :channel="src.split(':')[1]" + :antenna="src.split(':')[1]" :withRenotes="withRenotes" :withReplies="withReplies" :onlyFiles="onlyFiles" + :withCw="withCw" :sound="true" @queue="queueUpdated" /> @@ -45,19 +49,20 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; +import { antennasCache, userFavoriteListsCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; import { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; +import { timelineHeaderItemDef } from '@/timeline-header.js'; +import { isLocalTimelineAvailable, isGlobalTimelineAvailable } from '@/scripts/get-timeline-available.js'; +import { ui } from '@/config.js'; +import XPostForm from '@/components/XPostForm.vue'; provide('shouldOmitHeaderTitle', true); -const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); -const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); const keymap = { 't': focus, }; @@ -75,6 +80,10 @@ const withRenotes = computed<boolean>({ get: () => defaultStore.reactiveState.tl.value.filter.withRenotes, set: (x) => saveTlFilter('withRenotes', x), }); +const withCw = computed<boolean>({ + get: () => defaultStore.reactiveState.tl.value.filter.withCw, + set: (x) => saveTlFilter('withCw', x), +}); // computed内での無限ループを防ぐためのフラグ const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies'); @@ -116,8 +125,19 @@ const withSensitive = computed<boolean>({ set: (x) => saveTlFilter('withSensitive', x), }); -watch(src, () => { +const channelInfo = ref(); +if (src.value.split(':')[0] === 'channel') { + const channelId = src.value.split(':')[1]; + channelInfo.value = await misskeyApi('channels/show', { channelId }); +} +watch(src, async () => { queue.value = 0; + if (src.value.split(':')[0] === 'channel') { + const channelId = src.value.split(':')[1]; + channelInfo.value = await misskeyApi('channels/show', { channelId }); + } else { + channelInfo.value = null; + } }); watch(withSensitive, () => { @@ -134,9 +154,11 @@ function top(): void { } async function chooseList(ev: MouseEvent): Promise<void> { - const lists = await userListsCache.fetch(); - const items: MenuItem[] = [ - ...lists.map(list => ({ + const myLists = await userListsCache.fetch(); + const favoriteLists = await userFavoriteListsCache.fetch(); + let lists = [...new Set([...myLists, ...favoriteLists])]; + const items : MenuItem[] = [ + ... lists.map(list => ({ type: 'link' as const, text: list.name, to: `/timeline/list/${list.id}`, @@ -154,13 +176,12 @@ async function chooseList(ev: MouseEvent): Promise<void> { async function chooseAntenna(ev: MouseEvent): Promise<void> { const antennas = await antennasCache.fetch(); - const items: MenuItem[] = [ - ...antennas.map(antenna => ({ + const items : MenuItem[] = [ + ... antennas.map(antenna => ({ type: 'link' as const, text: antenna.name, indicate: antenna.hasUnreadNote, - to: `/timeline/antenna/${antenna.id}`, - })), + to: `/timeline/antenna/${antenna.id}` })), (antennas.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, @@ -179,12 +200,10 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null; const hasUnreadNote = (lastReadedAt && channel.lastNotedAt) ? Date.parse(channel.lastNotedAt) > lastReadedAt : !!(!lastReadedAt && channel.lastNotedAt); - return { - type: 'link' as const, - text: channel.name, - indicate: hasUnreadNote, - to: `/channels/${channel.id}`, - }; + return { type: 'link' as const, + text: channel.name, + indicate: hasUnreadNote, + to: `/channels/${channel.id}` }; }), (channels.length === 0 ? undefined : { type: 'divider' }), { @@ -197,9 +216,8 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { os.popupMenu(items, ev.currentTarget ?? ev.target); } -function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void { +function saveSrc(newSrc: 'home' | 'local' | 'media' | 'social' | 'global' | `list:${string}`): void { const out = deepMerge({ src: newSrc }, defaultStore.state.tl); - if (newSrc.startsWith('userList:')) { const id = newSrc.substring('userList:'.length); out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null; @@ -240,30 +258,33 @@ function closeTutorial(): void { const headerActions = computed(() => { const tmp = [ - { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: (ev) => { - os.popupMenu([{ - type: 'switch', - text: i18n.ts.showRenotes, - ref: withRenotes, - }, src.value === 'local' || src.value === 'social' ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, - } : undefined, { - type: 'switch', - text: i18n.ts.withSensitive, - ref: withSensitive, - }, { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: src.value === 'local' || src.value === 'social' ? withReplies : false, - }], ev.currentTarget ?? ev.target); - }, + { icon: 'ti ti-dots', + text: i18n.ts.options, + handler: (ev) => { + os.popupMenu([{ + type: 'switch', + text: i18n.ts.showRenotes, + ref: withRenotes, + }, { + type: 'switch', + text: i18n.ts.showCw, + ref: withCw, + }, src.value === 'local' || src.value === 'social' ? { + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles } : undefined, { + type: 'switch', + text: i18n.ts.withSensitive, + ref: withSensitive, + }, { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + + ref: onlyFiles, + disabled: src.value === 'local' || src.value === 'social' ? withReplies : false, + }], ev.currentTarget ?? ev.target); + }, }, ]; if (deviceKind === 'desktop') { @@ -277,48 +298,36 @@ const headerActions = computed(() => { } return tmp; }); +const headerTabs = computed(() => defaultStore.reactiveState.timelineHeader.value.map(tab => { + if ((tab === 'local' || tab === 'social') && !isLocalTimelineAvailable) { + return {}; + } else if (tab === 'global' && !isGlobalTimelineAvailable) { + return {}; + } -const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ - key: 'list:' + l.id, - title: l.name, - icon: 'ti ti-star', - iconOnly: true, -}))), { - key: 'home', - title: i18n.ts._timelines.home, - icon: 'ti ti-home', - iconOnly: true, -}, ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ti ti-planet', - iconOnly: true, -}, { - key: 'social', - title: i18n.ts._timelines.social, - icon: 'ti ti-universe', - iconOnly: true, -}] : []), ...(isGlobalTimelineAvailable ? [{ - key: 'global', - title: i18n.ts._timelines.global, - icon: 'ti ti-whirl', - iconOnly: true, -}] : []), { - icon: 'ti ti-list', - title: i18n.ts.lists, - iconOnly: true, - onClick: chooseList, -}, { - icon: 'ti ti-antenna', - title: i18n.ts.antennas, - iconOnly: true, - onClick: chooseAntenna, -}, { - icon: 'ti ti-device-tv', - title: i18n.ts.channel, - iconOnly: true, - onClick: chooseChannel, -}] as Tab[]); + const tabDef = timelineHeaderItemDef[tab]; + if (!tabDef) { + return {}; + } + + return { + ...(!['channels', 'antennas', 'lists'].includes(tab) ? { + key: tab, + } : {}), + title: tabDef.title, + icon: tabDef.icon, + iconOnly: tabDef.iconOnly, + ...(tab === 'lists' ? { + onClick: (ev) => chooseList(ev), + } : {}), + ...(tab === 'antennas' ? { + onClick: (ev) => chooseAntenna(ev), + } : {}), + ...(tab === 'channels' ? { + onClick: (ev) => chooseChannel(ev), + } : {}), + }; +}) as Tab[]); const headerTabsWhenNotLogin = computed(() => [ ...(isLocalTimelineAvailable ? [{ diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index e85292b7d5..5bb098be87 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -47,6 +47,7 @@ const rootEl = shallowRef<HTMLElement>(); watch(() => props.listId, async () => { list.value = await misskeyApi('users/lists/show', { listId: props.listId, + forPublic: true, }); }, { immediate: true }); diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 11e6eb416c..24ce22dfd8 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="profile _gaps"> <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/> <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> - + <MkRemoteInfoUpdate v-if="user.host != null" :UserId="user.id" class="warn"/> <div :key="user.id" class="main _panel"> <div class="banner-container" :style="style"> <div ref="bannerEl" class="banner" :style="style"></div> @@ -23,7 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName class="name" :user="user" :nowrap="true"/> <div class="bottom"> <span class="username"><MkAcct :user="user" :detail="true"/></span> - <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> + <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i + class="ti ti-shield" + ></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> @@ -34,7 +36,11 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> <div v-if="$i" class="actions"> <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button> - <MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> + <MkNotifyButton v-if="$i.id != user.id " :user="user"></MkNotifyButton> + <MkFollowButton + v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" + class="koudoku" + /> </div> </div> <MkAvatar class="avatar" :user="user" indicator/> @@ -42,13 +48,18 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName :user="user" :nowrap="false" class="name"/> <div class="bottom"> <span class="username"><MkAcct :user="user" :detail="true"/></span> - <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> + <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i + class="ti ti-shield" + ></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> </div> </div> <div v-if="user.roles.length > 0" class="roles"> - <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> + <span + v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" + :style="{ '--color': role.color }" + > <MkA v-adaptive-bg :to="`/roles/${role.id}`"> <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> {{ role.name }} @@ -56,7 +67,10 @@ SPDX-License-Identifier: AGPL-3.0-only </span> </div> <div v-if="iAmModerator" class="moderationNote"> - <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave> + <MkTextarea + v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" + v-model="moderationNote" manualSave + > <template #label>{{ i18n.ts.moderationNote }}</template> </MkTextarea> <div v-else> @@ -87,11 +101,19 @@ SPDX-License-Identifier: AGPL-3.0-only </dl> <dl v-if="user.birthday" class="field"> <dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt> - <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd> + <dd class="value"> + {{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ + i18n.tsx.yearsOld({age}) + }}) + </dd> </dl> <dl class="field"> <dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt> - <dd class="value">{{ dateString(user.createdAt) }} (<MkTime :time="user.createdAt"/>)</dd> + <dd class="value"> + {{ dateString(user.createdAt) }} ( + <MkTime :time="user.createdAt"/> + ) + </dd> </dl> </div> <div v-if="user.fields.length > 0" class="fields"> @@ -101,7 +123,10 @@ SPDX-License-Identifier: AGPL-3.0-only </dt> <dd class="value"> <Mfm :text="field.value" :author="user" :colored="false"/> - <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i> + <i + v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" + class="ti ti-circle-check" :class="$style.verifiedLink" + ></i> </dd> </dl> </div> @@ -118,6 +143,10 @@ SPDX-License-Identifier: AGPL-3.0-only <b>{{ number(user.followersCount) }}</b> <span>{{ i18n.ts.followers }}</span> </MkA> + <MkA v-if="!user.host"> + <b> {{ number(user.getPoints) }}</b> + <span>{{ i18n.ts.points }}</span> + </MkA> </div> </div> </div> @@ -135,10 +164,9 @@ SPDX-License-Identifier: AGPL-3.0-only <XActivity :key="user.id" :user="user"/> </MkLazy> </template> - <div v-if="!disableNotes"> - <MkLazy> - <XTimeline :user="user"/> - </MkLazy> + <div> + <div style="margin-bottom: 8px;">{{ i18n.ts._sfx.note }}</div> + <MkNotes :class="$style.tl" :noGap="true" :pagination="Notes"/> </div> </div> </div> @@ -166,13 +194,17 @@ import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; +import { useRouter } from '@/router/supplier.js'; import { i18n } from '@/i18n.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; -import { useRouter } from '@/router/supplier.js'; +import MkNotifyButton from '@/components/MkNotifyButton.vue'; +import MkRemoteInfoUpdate from '@/components/MkRemoteInfoUpdate.vue'; +import MkNotes from '@/components/MkNotes.vue'; +import MkLazy from '@/components/global/MkLazy.vue'; function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -194,9 +226,9 @@ const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const props = withDefaults(defineProps<{ - user: Misskey.entities.UserDetailed; - /** Test only; MkNotes currently causes problems in vitest */ - disableNotes: boolean; + user: Misskey.entities.UserDetailed; + /** Test only; MkNotes currently causes problems in vitest */ + disableNotes: boolean; }>(), { disableNotes: false, }); @@ -218,10 +250,25 @@ watch(moderationNote, async () => { await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); }); +const pagination = { + endpoint: 'users/featured-notes' as const, + limit: 10, + params: computed(() => ({ + userId: props.user.id, + })), +}; +const Notes = { + endpoint: 'users/notes' as const, + limit: 10, + params: computed(() => ({ + userId: props.user.id, + })), +}; + const style = computed(() => { if (props.user.bannerUrl == null) return {}; return { - backgroundImage: `url(${ props.user.bannerUrl })`, + backgroundImage: `url(${props.user.bannerUrl})`, }; }); @@ -307,379 +354,379 @@ onUnmounted(() => { <style lang="scss" scoped> .ftskorzw { - > .main { + > .main { - > .punished { - font-size: 0.8em; - padding: 16px; - } + > .punished { + font-size: 0.8em; + padding: 16px; + } - > .profile { + > .profile { - > .main { - position: relative; - overflow: clip; + > .main { + position: relative; + overflow: clip; - > .banner-container { - position: relative; - height: 250px; - overflow: clip; - background-size: cover; - background-position: center; + > .banner-container { + position: relative; + height: 250px; + overflow: clip; + background-size: cover; + background-position: center; - > .banner { - height: 100%; - background-color: #4c5e6d; - background-size: cover; - background-position: center; - box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; - will-change: background-position; - } + > .banner { + height: 100%; + background-color: #4c5e6d; + background-size: cover; + background-position: center; + box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; + will-change: background-position; + } - > .fade { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 78px; - background: linear-gradient(transparent, rgba(#000, 0.7)); - } + > .fade { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 78px; + background: linear-gradient(transparent, rgba(#000, 0.7)); + } - > .followed { - position: absolute; - top: 12px; - left: 12px; - padding: 4px 8px; - color: #fff; - background: rgba(0, 0, 0, 0.7); - font-size: 0.7em; - border-radius: 6px; - } + > .followed { + position: absolute; + top: 12px; + left: 12px; + padding: 4px 8px; + color: #fff; + background: rgba(0, 0, 0, 0.7); + font-size: 0.7em; + border-radius: 6px; + } - > .actions { - position: absolute; - top: 12px; - right: 12px; - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - background: rgba(0, 0, 0, 0.2); - padding: 8px; - border-radius: 24px; + > .actions { + position: absolute; + top: 12px; + right: 12px; + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + background: rgba(0, 0, 0, 0.2); + padding: 8px; + border-radius: 24px; - > .menu { - vertical-align: bottom; - height: 31px; - width: 31px; - color: #fff; - text-shadow: 0 0 8px #000; - font-size: 16px; - } + > .menu { + vertical-align: bottom; + height: 31px; + width: 31px; + color: #fff; + text-shadow: 0 0 8px #000; + font-size: 16px; + } - > .koudoku { - margin-left: 4px; - vertical-align: bottom; - } - } + > .koudoku { + margin-left: 4px; + vertical-align: bottom; + } + } - > .title { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - padding: 0 0 8px 154px; - box-sizing: border-box; - color: #fff; + > .title { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 0 0 8px 154px; + box-sizing: border-box; + color: #fff; - > .name { - display: block; - margin: -10px; + > .name { + display: block; + margin: -10px; padding: 10px; - line-height: 32px; - font-weight: bold; - font-size: 1.8em; - filter: drop-shadow(0 0 4px #000); - } + line-height: 32px; + font-weight: bold; + font-size: 1.8em; + filter: drop-shadow(0 0 4px #000); + } - > .bottom { - > * { - display: inline-block; - margin-right: 16px; - line-height: 20px; - opacity: 0.8; + > .bottom { + > * { + display: inline-block; + margin-right: 16px; + line-height: 20px; + opacity: 0.8; - &.username { - font-weight: bold; - } - } + &.username { + font-weight: bold; + } + } - > .add-note-button { - background: rgba(0, 0, 0, 0.2); - color: #fff; - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - border-radius: 24px; - padding: 4px 8px; - font-size: 80%; - } - } - } - } + > .add-note-button { + background: rgba(0, 0, 0, 0.2); + color: #fff; + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + border-radius: 24px; + padding: 4px 8px; + font-size: 80%; + } + } + } + } - > .title { - display: none; - text-align: center; - padding: 50px 8px 16px 8px; - font-weight: bold; - border-bottom: solid 0.5px var(--divider); + > .title { + display: none; + text-align: center; + padding: 50px 8px 16px 8px; + font-weight: bold; + border-bottom: solid 0.5px var(--divider); - > .bottom { - > * { - display: inline-block; - margin-right: 8px; - opacity: 0.8; - } - } - } + > .bottom { + > * { + display: inline-block; + margin-right: 8px; + opacity: 0.8; + } + } + } - > .avatar { - display: block; - position: absolute; - top: 170px; - left: 16px; - z-index: 2; - width: 120px; - height: 120px; - box-shadow: 1px 1px 3px rgba(#000, 0.2); - } + > .avatar { + display: block; + position: absolute; + top: 170px; + left: 16px; + z-index: 2; + width: 120px; + height: 120px; + box-shadow: 1px 1px 3px rgba(#000, 0.2); + } - > .roles { - padding: 24px 24px 0 154px; - font-size: 0.95em; - display: flex; - flex-wrap: wrap; - gap: 8px; + > .roles { + padding: 24px 24px 0 154px; + font-size: 0.95em; + display: flex; + flex-wrap: wrap; + gap: 8px; - > .role { - border: solid 1px var(--color, var(--divider)); - border-radius: 999px; - margin-right: 4px; - padding: 3px 8px; - } - } + > .role { + border: solid 1px var(--color, var(--divider)); + border-radius: 999px; + margin-right: 4px; + padding: 3px 8px; + } + } - > .moderationNote { - margin: 12px 24px 0 154px; - } + > .moderationNote { + margin: 12px 24px 0 154px; + } - > .memo { - margin: 12px 24px 0 154px; - background: transparent; - color: var(--fg); - border: 1px solid var(--divider); - border-radius: 8px; - padding: 8px; - line-height: 0; + > .memo { + margin: 12px 24px 0 154px; + background: transparent; + color: var(--fg); + border: 1px solid var(--divider); + border-radius: 8px; + padding: 8px; + line-height: 0; - > .heading { - text-align: left; - color: var(--fgTransparent); - line-height: 1.5; - font-size: 85%; - } + > .heading { + text-align: left; + color: var(--fgTransparent); + line-height: 1.5; + font-size: 85%; + } - textarea { - margin: 0; - padding: 0; - resize: none; - border: none; - outline: none; - width: 100%; - height: auto; - min-height: 0; - line-height: 1.5; - color: var(--fg); - overflow: hidden; - background: transparent; - font-family: inherit; - } - } + textarea { + margin: 0; + padding: 0; + resize: none; + border: none; + outline: none; + width: 100%; + height: auto; + min-height: 0; + line-height: 1.5; + color: var(--fg); + overflow: hidden; + background: transparent; + font-family: inherit; + } + } - > .description { - padding: 24px 24px 24px 154px; - font-size: 0.95em; + > .description { + padding: 24px 24px 24px 154px; + font-size: 0.95em; - > .empty { - margin: 0; - opacity: 0.5; - } - } + > .empty { + margin: 0; + opacity: 0.5; + } + } - > .fields { - padding: 24px; - font-size: 0.9em; - border-top: solid 0.5px var(--divider); + > .fields { + padding: 24px; + font-size: 0.9em; + border-top: solid 0.5px var(--divider); - > .field { - display: flex; - padding: 0; - margin: 0; - align-items: center; + > .field { + display: flex; + padding: 0; + margin: 0; + align-items: center; - &:not(:last-child) { - margin-bottom: 8px; - } + &:not(:last-child) { + margin-bottom: 8px; + } - > .name { - width: 30%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-weight: bold; - text-align: center; - } + > .name { + width: 30%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: bold; + text-align: center; + } - > .value { - width: 70%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0; - } - } + > .value { + width: 70%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0; + } + } - &.system > .field > .name { - } - } + &.system > .field > .name { + } + } - > .status { - display: flex; - padding: 24px; - border-top: solid 0.5px var(--divider); + > .status { + display: flex; + padding: 24px; + border-top: solid 0.5px var(--divider); - > a { - flex: 1; - text-align: center; + > a { + flex: 1; + text-align: center; - &.active { - color: var(--accent); - } + &.active { + color: var(--accent); + } - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; + } - > b { - display: block; - line-height: 16px; - } + > b { + display: block; + line-height: 16px; + } - > span { - font-size: 70%; - } - } - } - } - } + > span { + font-size: 70%; + } + } + } + } + } - > .contents { - > .content { - margin-bottom: var(--margin); - } - } - } + > .contents { + > .content { + margin-bottom: var(--margin); + } + } + } - &.wide { - display: flex; - width: 100%; + &.wide { + display: flex; + width: 100%; - > .main { - width: 100%; - min-width: 0; - } + > .main { + width: 100%; + min-width: 0; + } - > .sub { - max-width: 350px; - min-width: 350px; - margin-left: var(--margin); - } - } + > .sub { + max-width: 350px; + min-width: 350px; + margin-left: var(--margin); + } + } } @container (max-width: 500px) { - .ftskorzw { - > .main { - > .profile > .main { - > .banner-container { - height: 140px; + .ftskorzw { + > .main { + > .profile > .main { + > .banner-container { + height: 140px; - > .fade { - display: none; - } + > .fade { + display: none; + } - > .title { - display: none; - } - } + > .title { + display: none; + } + } - > .title { - display: block; - } + > .title { + display: block; + } - > .avatar { - top: 90px; - left: 0; - right: 0; - width: 92px; - height: 92px; - margin: auto; - } + > .avatar { + top: 90px; + left: 0; + right: 0; + width: 92px; + height: 92px; + margin: auto; + } - > .roles { - padding: 16px 16px 0 16px; - justify-content: center; - } + > .roles { + padding: 16px 16px 0 16px; + justify-content: center; + } - > .moderationNote { - margin: 16px 16px 0 16px; - } + > .moderationNote { + margin: 16px 16px 0 16px; + } - > .memo { - margin: 16px 16px 0 16px; - } + > .memo { + margin: 16px 16px 0 16px; + } - > .description { - padding: 16px; - text-align: center; - } + > .description { + padding: 16px; + text-align: center; + } - > .fields { - padding: 16px; - } + > .fields { + padding: 16px; + } - > .status { - padding: 16px; - } - } + > .status { + padding: 16px; + } + } - > .contents { - > .nav { - font-size: 80%; - } - } - } - } + > .contents { + > .nav { + font-size: 80%; + } + } + } + } } </style> <style lang="scss" module> .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; + background: var(--bg); + border-radius: var(--radius); + overflow: clip; } .verifiedLink { - margin-left: 4px; - color: var(--success); + margin-left: 4px; + color: var(--success); } </style> diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 19b7bb7f04..660e019cb2 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -98,7 +98,10 @@ misskeyApiGet('federation/instances', { left: 0; width: 100vw; height: 100vh; - background: var(--accent); + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.75, 1) infinite; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.75, 1) infinite; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.75, 1) infinite; clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%); } > .shape2 { @@ -107,7 +110,10 @@ misskeyApiGet('federation/instances', { left: 0; width: 100vw; height: 100vh; - background: var(--accent); + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800%; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.75, 1) infinite; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.75, 1) infinite; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.75, 1) infinite; clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%); opacity: 0.5; } @@ -177,6 +183,71 @@ misskeyApiGet('federation/instances', { } } } +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} </style> <style lang="scss" module> diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 9909d9bf9d..ee2f4f9169 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -112,6 +112,10 @@ const routes: RouteDef[] = [{ path: '/navbar', name: 'navbar', component: page(() => import('@/pages/settings/navbar.vue')), + }, { + path: '/timeline-header', + name: 'timelineHeader', + component: page(() => import('@/pages/settings/timelineHeader.vue')), }, { path: '/statusbar', name: 'statusbar', diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index 2e837745c9..c6f4f5ef47 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -268,6 +268,11 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', frame: 'bronze', }, + 'markedAsGorilla': { + img: '/fluent-emoji/1f98D.png', + bg: 'linear-gradient(0deg, rgba(55,0,0,1) 0%, rgba(107,5,5,1) 59%, rgba(158,6,6,1) 100%)', + frame: 'bronze', + }, 'following1': { img: '/fluent-emoji/2618.png', bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', diff --git a/packages/frontend/src/scripts/emojiColorChangeSecond.ts b/packages/frontend/src/scripts/emojiColorChangeSecond.ts new file mode 100644 index 0000000000..433c611ed1 --- /dev/null +++ b/packages/frontend/src/scripts/emojiColorChangeSecond.ts @@ -0,0 +1,135 @@ +const imageUrl = ref<string>('path_to_your_image.gif'); // または '.apng' + +// 1秒あたりの色変化数を保持するリアクティブ変数 +const colorChangesPerSecond = ref<number | null>(null); + +// コンポーネントがマウントされたときに画像を取得して解析する +onMounted(() => { + fetchImage(imageUrl.value); +}); + +// 画像を取得する関数 +async function fetchImage(url: string) { + try { + const response = await fetch(url); + const blob = await response.blob(); + const arrayBuffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + + if (url.endsWith('.gif')) { + analyzeGif(bytes); + } else if (url.endsWith('.apng')) { + analyzeApng(bytes); + } + } catch (error) { + console.error('Error fetching the image:', error); + } +} + +// GIFの解析関数 +function analyzeGif(bytes: Uint8Array) { + const frames = extractGifFrames(bytes); + calculateColorChanges(frames); +} + +// APNGの解析関数 +function analyzeApng(bytes: Uint8Array) { + const frames = extractApngFrames(bytes); + calculateColorChanges(frames); +} + +// GIFフレーム抽出関数 +function extractGifFrames(bytes: Uint8Array) { + const frames = []; + let i = 0; + while (i < bytes.length) { + // GIFのヘッダーとロジカルスクリーンディスクリプタをスキップ + if (i === 0) i += 13; + // グローバルカラーテーブルのスキップ + if (i === 13) i += (bytes[10] & 0x80 ? 3 * (2 ** ((bytes[10] & 0x07) + 1)) : 0); + + // イメージディスクリプタを探す + if (bytes[i] === 0x2C) { + const imageLeft = bytes[i + 1] + (bytes[i + 2] << 8); + const imageTop = bytes[i + 3] + (bytes[i + 4] << 8); + const imageWidth = bytes[i + 5] + (bytes[i + 6] << 8); + const imageHeight = bytes[i + 7] + (bytes[i + 8] << 8); + const localColorTableFlag = bytes[i + 9] & 0x80; + const localColorTableSize = 2 ** ((bytes[i + 9] & 0x07) + 1); + + i += 10; + + if (localColorTableFlag) { + i += 3 * localColorTableSize; + } + + while (bytes[i] !== 0x00) { + const blockSize = bytes[i]; + i += blockSize + 1; + } + + i++; + const frame = extractPixelColor(bytes, imageLeft, imageTop, imageWidth, imageHeight); + frames.push(frame); + } else { + i++; + } + } + + return frames; +} + +// APNGフレーム抽出関数 +function extractApngFrames(bytes: Uint8Array) { + const frames = []; + let i = 8; // PNGシグネチャをスキップ + + while (i < bytes.length) { + const length = (bytes[i] << 24) + (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + const type = String.fromCharCode(bytes[i + 4], bytes[i + 5], bytes[i + 6], bytes[i + 7]); + + if (type === 'IDAT' || type === 'fdAT') { + const imageLeft = 0; + const imageTop = 0; + const imageWidth = 0; + const imageHeight = 0; + + const frame = extractPixelColor(bytes, imageLeft, imageTop, imageWidth, imageHeight); + frames.push(frame); + } + + i += length + 12; + } + + return frames; +} + +// 中央ピクセルの色を抽出する関数 +function extractPixelColor(bytes: Uint8Array, left: number, top: number, width: number, height: number) { + const centerX = left + Math.floor(width / 2); + const centerY = top + Math.floor(height / 2); + const index = (centerY * width + centerX) * 4; + + return { + r: bytes[index], + g: bytes[index + 1], + b: bytes[index + 2], + }; +} + +// 色の変化を計算する関数 +function calculateColorChanges(frames: { r: number; g: number; b: number }[]) { + let colorChangeCount = 0; + for (let i = 1; i < frames.length; i++) { + const prevFrame = frames[i - 1]; + const currFrame = frames[i]; + if (prevFrame.r !== currFrame.r || prevFrame.g !== currFrame.g || prevFrame.b !== currFrame.b) { + colorChangeCount++; + } + } + + // 仮にFPSが30と仮定し、1秒あたりの色変化回数を計算 + const fps = 30; + const durationInSeconds = frames.length / fps; + colorChangesPerSecond.value = colorChangeCount / durationInSeconds; +} diff --git a/packages/frontend/src/scripts/emojiKitchen/emojiData.ts b/packages/frontend/src/scripts/emojiKitchen/emojiData.ts new file mode 100644 index 0000000000..7b218a5478 --- /dev/null +++ b/packages/frontend/src/scripts/emojiKitchen/emojiData.ts @@ -0,0 +1,40276 @@ +// original Source: https://github.com/alcor/emoji-supply/blob/main/docs/kitchen/emojidata.js +// Last commit data: 2a08dea + +export const points = + ['2764-fe0f','1f9e1','1f49b','1f49a','1fa75','1f499','1f49c','1fa77','1f90e','1fa76','1f5a4','1f90d','1fa84','1f600','1f603','1f604','1f601','1f606','1f605','1f923','1f602','1f642','1f643','1fae0','1f609','1f60a','1f607','1f970','1f60d','1f929','1f618','1f617','263a-fe0f','1f61a','1f619','1f972','1f60b','1f61b','1f61c','1f92a','1f61d','1f911','1f917','1f92d','1fae2','1fae3','1f92b','1f914','1fae1','1f910','1f928','1f610','1f611','1f636','1fae5','1f636-200d-1f32b-fe0f','1f60f','1f612','1f644','1f62c','1f62e-200d-1f4a8','1f925','1f60c','1f614','1f62a','1f924','1f634','1f637','1f912','1f915','1f922','1f92e','1f927','1f975','1f976','1f974','1f635','1f92f','1f920','1f973','1f978','1f60e','1f913','1f9d0','1f615','1fae4','1f61f','1f641','2639-fe0f','1f62e','1f62f','1f632','1f633','1f97a','1f979','1f626','1f627','1f628','1f630','1f625','1f622','1f62d','1f631','1f616','1f623','1f61e','1f613','1f629','1f62b','1f971','1f624','1f621','1f620','1f92c','1f608','1f47f','1f480','1f4a9','1f921','1f479','1f47a','1f47b','1f47d','1f916','1f648','1f48c','1f498','1f49d','1f496','1f497','1f493','1f49e','1f495','1f49f','2763-fe0f','1f494','2764-fe0f-200d-1fa79','1f48b','1f4af','1f4a5','1f4ab','1f573-fe0f','1f4ac','1f5ef-fe0f','1f44d','1f9e0','1fac0','1fac1','1f9b7','1f9b4','1f440','1f441-fe0f','1f444','1fae6','1f937','1f5e3-fe0f','1f464','1f465','1fac2','1f463','1f435','1f436','1f429','1f43a','1f98a','1f99d','1f431','1f981','1f42f','1f984','1f98c','1f42e','1f437','1f410','1f999','1f42d','1f430','1f994','1f987','1f43b','1f428','1f43c','1f9a5','1f43e','1f414','1f426','1f427','1f54a-fe0f','1f989','1f9a9','1fabf','1f438','1f422','1f433','1f41f','1f988','1f419','1f41a','1fab8','1f40c','1f98b','1f41d','1fab2','1f41e','1f997','1fab3','1f577-fe0f','1f982','1f9a0','1f490','1f338','1f4ae','1fab7','1f3f5-fe0f','1f339','1f940','1f33a','1f33b','1f33c','1f337','1fabb','1f331','1fab4','1f332','1f333','1f334','1f335','1f33e','1f33f','1f340','1f341','1f342','1f343','1fab9','1f344','1f347','1f348','1f349','1f34a','1f34b','1f34c','1f34d','1f96d','1f34e','1f350','1f352','1f353','1fad0','1f95d','1f345','1fad2','1f965','1f951','1f954','1f955','1f33d','1f336-fe0f','1fad1','1f952','1f96c','1f966','1f9c4','1f9c5','1f95c','1fad8','1f330','1fada','1fadb','1f35e','1f950','1fad3','1f968','1f96f','1f95e','1f9c7','1f9c0','1f356','1f357','1f969','1f953','1f354','1f35f','1f32d','1f96a','1f32e','1f32f','1fad4','1f959','1f9c6','1f373','1f958','1f372','1fad5','1f963','1f957','1f37f','1f9c8','1f9c2','1f96b','1f371','1f358','1f359','1f35a','1f35b','1f35c','1f35d','1f360','1f362','1f363','1f364','1f365','1f361','1f95f','1f960','1f961','1f980','1f99e','1f9aa','1f366','1f367','1f368','1f369','1f36a','1f382','1f370','1f9c1','1f967','1f36b','1f36c','1f36d','1f36e','1f36f','1f37c','1f95b','2615','1fad6','1f375','1f376','1f37e','1f377','1f379','1f942','1fad7','1f964','1f9cb','1f9c3','1f9c9','1f962','1f37d-fe0f','1f3fa','1f30d','1f3d4-fe0f','26f0-fe0f','1f30b','1f3d5-fe0f','1f3d6-fe0f','1f3dd-fe0f','1f3de-fe0f','1f3df-fe0f','1f3db-fe0f','1f9f1','1faa8','1fab5','1f3da-fe0f','1f3e0','1f3f0','1f5fc','1f307','2668-fe0f','1f3a0','1f3a1','1f3a2','1f3aa','1f682','1f687','1f68c','1f695','1f697','1f69a','1f69b','1f69c','1f3ce-fe0f','1f3cd-fe0f','1f6b2','1f6f4','1f6f9','1f6fc','1f6e3-fe0f','26fd','1f6a8','1f6a6','1f6d1','1f6a7','2693','1f6df','1f6f6','2708-fe0f','1f6a0','1f6a1','1f680','1f6f8','1f9f3','231b','23f3','231a','23f0','1f570-fe0f','1f31a','1f31b','1f31c','1f321-fe0f','1f31d','1f31e','1fa90','2b50','1f31f','1f30c','2601-fe0f','26c5','1f327-fe0f','1f329-fe0f','1f32a-fe0f','1f32c-fe0f','1f300','1f308','2602-fe0f','26a1','26c4','2604-fe0f','1f525','1f4a7','1f30a','1f383','1f386','1f388','1f38a','1f380','1f381','1f397-fe0f','1f39f-fe0f','1f396-fe0f','1f3c6','1f3c5','1f947','1f948','1f949','26bd','26be','1f94e','1f3c0','1f3d0','1f3c8','1f3c9','1f3be','1f94f','1f3b3','1f3cf','1f3d1','1f3d2','1f94d','1f3d3','1f3f8','1f94a','1f94b','1f945','26f3','26f8-fe0f','1f3a3','1f93f','1f3bd','1f3bf','1f6f7','1f94c','1f3af','1fa80','1fa81','1f3b1','1f52e','1f3ae','1f3b0','1f3b2','1f9e9','1faa9','2660-fe0f','2665-fe0f','265f-fe0f','1f0cf','1f004','1f3b4','1f3ad','1f3a8','1f9f5','1faa1','1f9f6','1f455','1f9e6','1f457','1faad','1f45c','1f6cd-fe0f','1f45f','1f97f','1f460','1fa70','1faae','1f451','1f452','1f393','1f48d','1f48e','1f508','1f4e3','1f514','1f399-fe0f','1f39a-fe0f','1f39b-fe0f','1f3a4','1f3a7','1f4fb','1f3b7','1fa97','1f3b8','1f3b9','1f3ba','1f3bb','1fa95','1f941','1fa98','1fa87','1fa88','1f4f1','260e-fe0f','1f4df','1f4e0','1f50b','1faab','1f50c','1f4bb','1f5a8-fe0f','1f4be','1f4bf','1f39e-fe0f','1f3ac','1f4fa','1f4f7','1f4fc','1f50e','1f4a1','1fa94','1f4da','1f4f0','1f4b8','1f4e6','1f5f3-fe0f','270f-fe0f','2712-fe0f','1f58a-fe0f','1f58c-fe0f','1f58d-fe0f','1f4c8','1f4c9','1f4ca','1f587-fe0f','1f4cf','2702-fe0f','1f5c3-fe0f','1f5d1-fe0f','1f512','1f5dd-fe0f','26cf-fe0f','1f6e0-fe0f','1fa83','1f3f9','2699-fe0f','2696-fe0f','26d3-fe0f','1f9f2','1f9ea','1f9ec','1f52c','1f52d','1fa78','1fa7a','1f6cb-fe0f','1faa4','1fa92','1f9f9','1f9fa','1f9fc','1fae7','1f6d2','1f9ff','1f5ff','1f6ae','26a0-fe0f','2622-fe0f','2623-fe0f','262f-fe0f','262e-fe0f','2648','2649','264a','264b','264c','264d','264e','264f','2650','2651','2652','2653','26ce','1f4f4','2716-fe0f','2795','2796','2797','267e-fe0f','2049-fe0f','2753','2757','3030-fe0f','267b-fe0f','2705','27b0','27bf','a9-fe0f','ae-fe0f','2122-fe0f','1f522','1f192','1f193','1f195','1f197','1f198','1f199','1f47e'] + +export const revisions = + [20201001,20230418,20230803,20211115,20230301,20220815,20230127,20220203,20221101,20220506,20210831,20220406,20210218,20230126,20230821,20230216,20220110,20221107,20210521,20230818,20230426,20220823,20230421,20230221,20230613] + +export const pairs = ` +e.0.0. +e.0.1. +e.0.2. +e.1.1. +e.0.3. +e.1.2. +e.1.3. +e.2.2. +e.4.0. +e.0.5. +e.2.3. +e.4.1. +e.0.6. +e.1.5. +e.3.3. +e.4.2. +e.1.6. +e.2.5. +e.4.3. +e.7.0. +e.0.8. +e.2.6. +e.3.5. +e.4.4. +e.7.1. +e.1.8. +e.3.6. +e.4.5. +e.7.2. +e.9.0. +e.0.a. +e.2.8. +e.4.6. +e.5.5. +e.7.3. +e.9.1. +e.0.b. +e.1.a. +e.3.8. +e.5.6. +e.9.2. +j.4.7. +e.1.b. +e.2.a. +e.4.8. +e.6.6. +e.7.5. +e.9.3. +i.c.0. +e.2.b. +e.3.a. +e.5.8. +e.7.6. +e.9.4. +i.c.1. +e.3.b. +e.4.a. +e.6.8. +e.7.7. +e.9.5. +i.c.2. +e.4.b. +e.5.a. +e.7.8. +e.9.6. +i.c.3. +e.4.c. +e.5.b. +e.6.a. +e.8.8. +e.9.7. +e.6.b. +e.7.a. +e.9.8. +i.c.5. +e.7.b. +e.8.a. +e.9.9. +i.c.6. +e.7.c. +e.8.b. +e.9.a. +e.9.b. +e.a.a. +i.c.8. +e.9.c. +e.b.a. +e.b.b. +i.c.a. +i.c.b. +i.c.c. +j.n.0. +j.n.1. +i.c.d. +j.n.2. +i.c.e. +j.n.3. +i.c.f. +j.n.4. +i.c.g. +j.n.5. +0.d.d. +0.r.0. +i.c.h. +j.n.6. +0.d.e. +0.r.1. +0.s.0. +i.c.i. +j.n.7. +0.d.f. +0.e.e. +0.r.2. +0.s.1. +0.t.0. +i.c.j. +j.n.8. +0.d.g. +0.e.f. +0.r.3. +0.s.2. +0.t.1. +0.u.0. +i.c.k. +j.n.9. +0.d.h. +0.e.g. +0.f.f. +0.s.3. +0.t.2. +0.u.1. +e.4.r. +i.c.l. +j.n.a. +0.d.i. +0.e.h. +0.f.g. +0.r.5. +0.t.3. +0.u.2. +e.4.s. +i.c.m. +j.n.b. +0.d.j. +0.e.i. +0.f.h. +0.g.g. +0.r.6. +0.s.5. +0.u.3. +3.n.c. +e.4.t. +0.d.k. +0.e.j. +0.f.i. +0.g.h. +0.s.6. +0.t.5. +e.4.u. +e.7.r. +i.c.o. +0.d.l. +0.e.k. +0.f.j. +0.g.i. +0.r.8. +0.t.6. +0.u.5. +4.h.h. +e.7.s. +i.c.p. +0.d.m. +0.e.l. +0.f.k. +0.g.j. +0.s.8. +0.u.6. +4.h.i. +e.7.t. +e.9.r. +i.c.q. +0.f.l. +0.g.k. +0.i.i. +0.r.a. +0.t.8. +3.n.d. +4.e.m. +4.h.j. +e.7.u. +e.9.s. +0.d.o. +0.f.m. +0.g.l. +0.i.j. +0.r.b. +0.s.a. +0.u.8. +3.n.e. +4.h.k. +e.9.t. +0.d.p. +0.e.o. +0.g.m. +0.h.l. +0.i.k. +0.j.j. +0.s.b. +0.t.a. +3.n.f. +e.9.u. +i.c.r. +0.d.q. +0.e.p. +0.f.o. +0.h.m. +0.i.l. +0.k.j. +0.t.b. +0.u.a. +3.n.g. +i.c.s. +0.e.q. +0.f.p. +0.g.o. +0.j.l. +0.k.k. +0.u.b. +3.n.h. +4.i.m. +i.c.t. +0.f.q. +0.g.p. +0.h.o. +0.k.l. +3.n.i. +4.j.m. +i.c.u. +0.d.r. +0.g.q. +0.h.p. +0.i.o. +0.l.l. +4.k.m. +e.n.j. +i.c.v. +0.d.s. +0.e.r. +0.i.p. +0.j.o. +3.n.k. +4.h.q. +4.l.m. +i.c.w. +0.e.s. +0.f.r. +0.i.q. +0.j.p. +0.k.o. +3.n.l. +4.d.t. +4.m.m. +i.c.x. +0.d.u. +0.f.s. +0.g.r. +0.j.q. +0.k.p. +0.l.o. +3.n.m. +4.e.t. +i.c.y. +0.d.v. +0.e.u. +0.g.s. +0.h.r. +0.k.q. +0.m.o. +0.p.l. +3.n.n. +4.f.t. +i.c.z. +0.d.w. +0.e.v. +0.f.u. +0.g.t. +0.h.s. +0.i.r. +0.l.q. +0.p.m. +3.n.o. +0.e.w. +0.f.v. +0.g.u. +0.h.t. +0.i.s. +0.m.q. +3.n.p. +4.d.x. +4.j.r. +4.o.o. +0.d.y. +0.f.w. +0.g.v. +0.h.u. +0.j.s. +0.p.o. +3.n.q. +4.e.x. +4.i.t. +4.k.r. +i.c.A. +0.d.z. +0.e.y. +0.g.w. +0.h.v. +0.i.u. +0.k.s. +0.o.q. +0.p.p. +0.r.l. +4.f.x. +4.j.t. +i.c.B. +0.e.z. +0.f.y. +0.h.w. +0.i.v. +0.j.u. +0.p.q. +0.r.m. +0.s.l. +4.g.x. +4.k.t. +i.c.C. +0.f.z. +0.g.y. +0.h.x. +0.i.w. +0.j.v. +0.k.u. +0.q.q. +0.s.m. +0.t.l. +3.n.r. +i.c.D. +0.d.A. +0.g.z. +0.h.y. +0.k.v. +0.r.o. +0.t.m. +0.u.l. +3.n.s. +4.i.x. +4.j.w. +i.c.E. +0.d.B. +0.e.A. +0.i.y. +0.k.w. +0.p.r. +0.s.o. +0.u.m. +0.v.l. +3.n.t. +4.h.z. +4.j.x. +i.c.F. +0.d.C. +0.e.B. +0.f.A. +0.i.z. +0.j.y. +0.p.s. +0.r.q. +0.t.o. +0.v.m. +0.w.l. +3.n.u. +4.k.x. +0.d.D. +0.e.C. +0.f.B. +0.g.A. +0.j.z. +0.k.y. +0.p.t. +0.s.q. +0.u.o. +0.w.m. +0.x.l. +3.n.v. +0.d.E. +0.e.D. +0.f.C. +0.g.B. +0.h.A. +0.k.z. +0.t.q. +0.u.p. +0.v.o. +0.x.m. +0.y.l. +3.n.w. +i.c.G. +0.e.E. +0.f.D. +0.g.C. +0.h.B. +0.i.A. +0.l.z. +0.r.r. +0.u.q. +0.v.p. +0.w.o. +0.y.m. +3.n.x. +4.c.H. +4.d.F. +0.e.F. +0.f.E. +0.g.D. +0.h.C. +0.i.B. +0.j.A. +0.m.z. +0.r.s. +0.v.q. +0.w.p. +0.x.o. +3.I.c. +3.n.y. +j.S.0. +0.f.F. +0.g.E. +0.h.D. +0.i.C. +0.j.B. +0.k.A. +0.r.t. +0.w.q. +0.x.p. +0.y.o. +3.J.c. +3.n.z. +e.s.s. +j.S.1. +0.d.G. +0.g.F. +0.h.E. +0.i.D. +0.j.C. +0.k.B. +0.l.A. +0.o.z. +0.u.r. +0.x.q. +0.y.p. +4.s.t. +i.c.K. +j.S.2. +0.d.H. +0.e.G. +0.h.F. +0.i.E. +0.j.D. +0.k.C. +0.l.B. +0.m.A. +0.p.z. +0.t.t. +0.u.s. +0.v.r. +0.y.q. +i.c.L. +j.S.3. +0.e.H. +0.f.G. +0.i.F. +0.j.E. +0.k.D. +0.l.C. +0.m.B. +0.q.z. +0.u.t. +0.v.s. +0.w.r. +3.I.d. +3.M.c. +3.n.A. +j.S.4. +0.f.H. +0.g.G. +0.j.F. +0.k.E. +0.l.D. +0.m.C. +0.o.A. +0.u.u. +0.v.t. +0.w.s. +0.x.r. +3.I.e. +3.J.d. +3.n.B. +j.S.5. +0.d.K. +0.g.H. +0.h.G. +0.k.F. +0.l.E. +0.m.D. +0.o.B. +0.p.A. +0.v.u. +0.w.t. +0.x.s. +0.y.r. +3.I.f. +3.J.e. +3.n.C. +j.S.6. +0.A.q. +0.d.L. +0.e.K. +0.h.H. +0.i.G. +0.l.F. +0.m.E. +0.o.C. +0.p.B. +0.r.z. +0.u.w. +0.v.v. +0.x.t. +0.y.s. +3.I.g. +3.J.f. +3.n.D. +i.c.N. +j.S.7. +0.B.q. +0.e.L. +0.f.K. +0.i.H. +0.o.D. +0.p.C. +0.s.z. +0.v.w. +0.x.u. +0.y.t. +3.I.h. +3.J.g. +3.M.d. +3.n.E. +4.j.G. +4.m.F. +i.c.O. +j.S.8. +0.C.q. +0.f.L. +0.g.K. +0.j.H. +0.k.G. +0.o.E. +0.p.D. +0.v.x. +0.y.u. +3.I.i. +3.J.h. +3.M.e. +3.n.F. +4.t.z. +4.w.w. +i.c.P. +j.S.9. +0.D.q. +0.g.L. +0.G.l. +0.h.K. +0.k.H. +0.o.F. +0.p.E. +0.r.A. +0.v.y. +0.x.w. +3.I.j. +3.J.i. +3.M.f. +4.u.z. +i.c.Q. +j.S.a. +0.d.N. +0.E.q. +0.G.m. +0.i.K. +0.l.H. +0.p.F. +0.r.B. +0.s.A. +0.x.x. +0.y.w. +3.I.k. +3.J.j. +3.M.g. +4.h.L. +4.v.z. +i.c.R. +j.S.b. +0.d.O. +0.e.N. +0.i.L. +0.j.K. +0.m.H. +0.r.C. +0.s.B. +0.t.A. +0.w.z. +0.y.x. +3.I.l. +3.J.k. +3.M.h. +3.n.G. +4.q.F. +4.S.c. +0.d.P. +0.e.O. +0.f.N. +0.G.o. +0.j.L. +0.k.K. +0.r.D. +0.s.C. +0.t.B. +0.u.A. +0.y.y. +3.I.m. +3.J.l. +3.M.i. +3.n.H. +4.x.z. +i.c.T. +0.d.Q. +0.e.P. +0.f.O. +0.g.N. +0.k.L. +0.l.K. +0.o.H. +0.p.G. +0.r.E. +0.s.D. +0.t.C. +0.u.B. +0.v.A. +3.I.n. +3.J.m. +3.M.j. +4.y.z. +i.c.U. +0.d.R. +0.e.Q. +0.f.P. +0.g.O. +0.G.q. +0.h.N. +0.l.L. +0.m.K. +0.p.H. +0.r.F. +0.s.E. +0.t.D. +0.u.C. +0.v.B. +0.w.A. +0.z.z. +3.I.o. +3.J.n. +3.M.k. +i.c.V. +0.e.R. +0.f.Q. +0.g.P. +0.H.q. +0.i.N. +0.m.L. +0.s.F. +0.t.E. +0.u.D. +0.v.C. +0.w.B. +0.x.A. +3.I.p. +3.J.o. +3.M.l. +3.n.K. +4.h.O. +4.S.d. +i.c.W. +0.f.R. +0.g.Q. +0.h.P. +0.i.O. +0.j.N. +0.o.K. +0.t.F. +0.u.E. +0.v.D. +0.w.C. +0.x.B. +0.y.A. +3.I.q. +3.J.p. +3.M.m. +3.n.L. +4.S.e. +c.T.d. +i.c.X. +0.A.z. +0.d.U. +0.g.R. +0.h.Q. +0.i.P. +0.j.O. +0.k.N. +0.o.L. +0.p.K. +0.r.G. +0.u.F. +0.v.E. +0.w.D. +0.x.C. +0.y.B. +3.J.q. +3.n.M. +4.S.f. +c.T.e. +i.c.Y. +0.B.z. +0.d.V. +0.e.U. +0.h.R. +0.i.Q. +0.j.P. +0.k.O. +0.K.q. +0.l.N. +0.p.L. +0.r.H. +0.s.G. +0.v.F. +0.w.E. +0.x.D. +0.y.C. +3.M.o. +4.S.g. +c.T.f. +i.c.Z. +0.C.z. +0.d.W. +0.e.V. +0.f.U. +0.i.R. +0.k.P. +0.l.O. +0.L.q. +0.m.N. +0.s.H. +0.t.G. +0.w.F. +0.x.E. +0.y.D. +3.I.r. +3.M.p. +4.j.Q. +4.S.h. +c.T.g. +0.A.A. +0.d.X. +0.D.z. +0.e.W. +0.f.V. +0.g.U. +0.j.R. +0.k.Q. +0.l.P. +0.m.O. +0.t.H. +0.u.G. +0.x.F. +0.y.E. +3.I.s. +3.J.r. +3.M.q. +3.n.N. +4.S.i. +c.T.h. +0.A.B. +0.e.X. +0.E.z. +0.f.W. +0.g.V. +0.k.R. +0.l.Q. +0.m.P. +0.o.N. +0.r.K. +0.u.H. +0.v.G. +0.y.F. +3.I.t. +3.J.s. +3.n.O. +4.h.U. +4.S.j. +c.T.i. +c.Y.d. +0.A.C. +0.B.B. +0.f.X. +0.F.z. +0.g.W. +0.i.U. +0.l.R. +0.m.Q. +0.o.O. +0.p.N. +0.r.L. +0.s.K. +0.v.H. +0.w.G. +3.I.u. +3.J.t. +3.n.P. +4.d.Z. +4.h.V. +4.S.k. +c.T.j. +c.Y.e. +i.c.+. +0.A.D. +0.B.C. +0.g.X. +0.i.V. +0.m.R. +0.N.q. +0.o.P. +0.p.O. +0.s.L. +0.t.K. +0.w.H. +0.x.G. +3.I.v. +3.J.u. +3.M.r. +3.n.Q. +4.e.Z. +4.h.W. +4.j.U. +4.S.l. +c.T.k. +c.Y.f. +i.c./. +0.A.E. +0.B.D. +0.C.C. +0.f.Z. +0.h.X. +0.i.W. +0.j.V. +0.k.U. +0.o.Q. +0.O.q. +0.p.P. +0.t.L. +0.u.K. +0.x.H. +0.y.G. +3.I.w. +3.J.v. +3.M.s. +3.n.R. +4.S.m. +c.T.l. +c.Y.g. +i.c.10. +0.A.F. +0.B.E. +0.C.D. +0.g.Z. +0.i.X. +0.j.W. +0.k.V. +0.l.U. +0.o.R. +0.P.q. +0.p.Q. +0.v.K. +0.y.H. +3.I.x. +3.J.w. +3.M.t. +4.G.z. +4.n.S. +4.u.L. +c.T.m. +c.Y.h. +i.c.11. +0.B.F. +0.d.+. +0.D.D. +0.E.C. +0.h.Z. +0.H.z. +0.j.X. +0.k.W. +0.l.V. +0.m.U. +0.p.R. +0.Q.q. +0.r.N. +0.v.L. +0.w.K. +3.I.y. +3.J.x. +3.M.u. +3.n.T. +4.S.o. +c.Y.i. +i.c.12. +0.C.F. +0.d./. +0.e.+. +0.k.X. +0.l.W. +0.m.V. +0.r.O. +0.R.q. +0.s.N. +0.x.K. +3.I.z. +3.J.y. +3.M.v. +3.n.U. +4.E.D. +4.i.Z. +4.S.p. +4.w.L. +c.T.o. +c.Y.j. +0.d.10. +0.D.F. +0.e./. +0.E.E. +0.f.+. +0.G.A. +0.l.X. +0.m.W. +0.o.U. +0.r.P. +0.s.O. +0.t.N. +0.y.K. +3.J.z. +3.M.w. +3.n.V. +4.j.Z. +4.S.q. +4.x.L. +c.T.p. +c.Y.k. +0.A.H. +0.d.11. +0.e.10. +0.E.F. +0.f./. +0.g.+. +0.G.B. +0.k.Z. +0.K.z. +0.m.X. +0.o.V. +0.p.U. +0.r.Q. +0.s.P. +0.t.O. +0.u.N. +3.M.x. +3.n.W. +4.y.L. +c.T.q. +c.Y.l. +i.c.13. +0.B.H. +0.d.12. +0.e.11. +0.f.10. +0.g./. +0.l.Z. +0.L.z. +0.o.W. +0.p.V. +0.r.R. +0.s.Q. +0.t.P. +0.U.q. +0.v.N. +3.I.A. +3.M.y. +3.n.X. +4.F.F. +4.G.C. +4.h.+. +4.u.O. +c.Y.m. +i.c.14. +0.C.H. +0.e.12. +0.f.11. +0.g.10. +0.G.D. +0.i.+. +0.m.Z. +0.o.X. +0.p.W. +0.s.R. +0.t.Q. +0.u.P. +0.V.q. +0.w.N. +3.I.B. +3.J.A. +3.M.z. +3.n.Y. +4.h./. +4.S.r. +4.v.O. +i.c.15. +j.1e.0. +0.1f.0. +0.A.K. +0.D.H. +0.f.12. +0.g.11. +0.G.E. +0.h.10. +0.i./. +0.j.+. +0.p.X. +0.t.R. +0.u.Q. +0.v.P. +0.W.q. +0.x.N. +3.I.C. +3.J.B. +3.n.Z. +4.S.s. +4.w.O. +c.T.r. +c.Y.o. +i.c.16. +j.1e.1. +0.1f.1. +0.A.L. +0.B.K. +0.d.13. +0.E.H. +0.g.12. +0.h.11. +0.i.10. +0.j./. +0.k.+. +0.o.Z. +0.r.U. +0.u.R. +0.v.Q. +0.w.P. +0.X.q. +0.y.N. +3.I.D. +3.J.C. +4.G.F. +4.S.t. +4.x.O. +c.T.s. +c.Y.p. +i.c.17. +j.1e.2. +0.B.L. +0.C.K. +0.d.14. +0.e.13. +0.h.12. +0.H.F. +0.i.11. +0.j.10. +0.k./. +0.l.+. +0.N.z. +0.p.Z. +0.r.V. +0.s.U. +0.v.R. +0.w.Q. +0.x.P. +3.I.E. +3.J.D. +3.M.A. +4.1f.2. +4.S.u. +4.y.O. +c.T.t. +c.Y.q. +i.c.18. +j.1e.3. +0.1f.3. +0.C.L. +0.d.15. +0.D.K. +0.e.14. +0.f.13. +0.i.12. +0.j.11. +0.k.10. +0.l./. +0.m.+. +0.O.z. +0.q.Z. +0.r.W. +0.s.V. +0.w.R. +0.x.Q. +0.y.P. +3.I.F. +3.J.E. +3.M.B. +4.S.v. +4.t.U. +c.T.u. +e.4.1e. +i.c.19. +0.1h.0. +0.d.16. +0.D.L. +0.e.15. +0.E.K. +0.f.14. +0.g.13. +0.G.G. +0.j.12. +0.k.11. +0.l.10. +0.m./. +0.P.z. +0.r.X. +0.s.W. +0.t.V. +0.x.R. +0.y.Q. +3.J.F. +3.M.C. +3.n.+. +4.S.w. +4.u.U. +c.T.v. +e.4.1f. +i.c.1a. +j.1e.5. +0.1f.5. +0.1h.1. +0.1i.0. +0.A.N. +0.d.17. +0.e.16. +0.E.L. +0.f.15. +0.g.14. +0.G.H. +0.h.13. +0.k.12. +0.K.F. +0.l.11. +0.m.10. +0.o.+. +0.Q.z. +0.s.X. +0.y.R. +3.M.D. +3.n./. +4.S.x. +4.t.W. +4.u.V. +4.v.U. +c.T.w. +c.Y.r. +i.c.1b. +j.1e.6. +0.1f.6. +0.1h.2. +0.1i.1. +0.A.O. +0.B.N. +0.e.17. +0.f.16. +0.g.15. +0.h.14. +0.H.H. +0.i.13. +0.l.12. +0.L.F. +0.m.11. +0.o./. +0.p.+. +0.R.z. +0.r.Z. +0.t.X. +0.u.W. +3.I.G. +3.M.E. +3.n.10. +4.d.18. +4.S.y. +4.v.V. +4.w.U. +c.T.x. +c.Y.s. +e.7.1e. +i.c.1c. +j.1j.0. +0.+.q. +0.1h.3. +0.A.P. +0.B.O. +0.C.N. +0.d.19. +0.f.17. +0.g.16. +0.h.15. +0.i.14. +0.j.13. +0.m.12. +0.o.10. +0.p./. +0.s.Z. +0.u.X. +0.v.W. +3.I.H. +3.J.G. +3.M.F. +3.n.11. +4.1i.2. +4.e.18. +4.S.z. +4.w.V. +4.x.U. +c.T.y. +c.Y.t. +e.7.1f. +j.1e.8. +j.1j.1. +0./.q. +0.1f.8. +0.1i.3. +0.A.Q. +0.B.P. +0.C.O. +0.D.N. +0.e.19. +0.g.17. +0.G.K. +0.h.16. +0.i.15. +0.j.14. +0.k.13. +0.o.11. +0.p.10. +0.t.Z. +0.v.X. +0.w.W. +0.x.V. +0.y.U. +3.I.I. +3.J.H. +3.n.12. +4.d.1a. +4.f.18. +c.T.z. +c.Y.u. +e.4.1h. +e.9.1e. +i.c.1d. +j.1j.2. +0.10.q. +0.1h.5. +0.A.R. +0.B.Q. +0.C.P. +0.d.1b. +0.D.O. +0.e.1a. +0.E.N. +0.f.19. +0.g.18. +0.h.17. +0.i.16. +0.j.15. +0.k.14. +0.K.H. +0.l.13. +0.o.12. +0.p.11. +0.u.Z. +0.U.z. +0.w.X. +0.x.W. +0.y.V. +3.J.I. +4.G.L. +c.Y.v. +e.4.1i. +e.9.1f. +j.1e.a. +j.1j.3. +0.11.q. +0.1f.a. +0.1h.6. +0.1i.5. +0.B.R. +0.C.Q. +0.d.1c. +0.D.P. +0.e.1b. +0.E.O. +0.g.19. +0.h.18. +0.i.17. +0.j.16. +0.k.15. +0.l.14. +0.L.H. +0.m.13. +0.N.F. +0.p.12. +0.r.+. +0.v.Z. +0.V.z. +0.x.X. +0.y.W. +3.I.K. +3.J.J. +3.M.G. +4.f.1a. +4.S.A. +c.Y.w. +j.1e.b. +j.1j.4. +0.1f.b. +0.1i.6. +0.C.R. +0.D.Q. +0.e.1c. +0.E.P. +0.f.1b. +0.g.1a. +0.h.19. +0.j.17. +0.k.16. +0.l.15. +0.m.14. +0.O.F. +0.r./. +0.s.+. +0.W.z. +0.w.Z. +0.y.X. +3.I.L. +3.J.K. +3.M.H. +3.n.13. +4.12.q. +4.i.18. +4.S.B. +c.T.A. +c.Y.x. +e.7.1h. +i.c.1e. +j.1j.5. +0.1h.8. +0.A.U. +0.D.R. +0.E.Q. +0.f.1c. +0.g.1b. +0.h.1a. +0.i.19. +0.k.17. +0.K.K. +0.l.16. +0.m.15. +0.o.13. +0.P.F. +0.r.10. +0.s./. +0.t.+. +0.x.Z. +0.X.z. +3.I.M. +3.J.L. +3.n.14. +4.d.1d. +4.j.18. +4.S.C. +c.T.B. +c.Y.y. +e.7.1i. +i.c.1f. +j.1j.6. +0.1i.8. +0.A.V. +0.B.U. +0.E.R. +0.g.1c. +0.G.N. +0.h.1b. +0.j.19. +0.l.17. +0.L.K. +0.m.16. +0.o.14. +0.p.13. +0.Q.F. +0.r.11. +0.s.10. +0.y.Z. +3.J.M. +3.n.15. +4.e.1d. +4.i.1a. +4.k.18. +4.S.D. +4.t./. +4.u.+. +c.T.C. +c.Y.z. +e.9.1h. +i.c.1g. +j.1j.7. +0.13.q. +0.1h.a. +0.A.W. +0.B.V. +0.C.U. +0.h.1c. +0.i.1b. +0.k.19. +0.l.18. +0.L.L. +0.m.17. +0.N.H. +0.o.15. +0.p.14. +0.r.12. +0.R.F. +0.s.11. +0.t.10. +0.v.+. +3.M.K. +3.n.16. +4.f.1d. +4.G.O. +4.j.1a. +4.S.E. +4.u./. +4.Z.z. +c.T.D. +e.9.1i. +j.1j.8. +0.14.q. +0.1h.b. +0.A.X. +0.B.W. +0.C.V. +0.d.1e. +0.D.U. +0.G.P. +0.i.1c. +0.l.19. +0.o.16. +0.O.H. +0.p.15. +0.s.12. +0.t.11. +0.u.10. +3.I.N. +3.M.L. +3.n.17. +4.1i.a. +4.g.1d. +4.j.1b. +4.k.1a. +4.m.18. +4.S.F. +4.v./. +4.w.+. +c.T.E. +j.1j.9. +0.15.q. +0.1i.b. +0.B.X. +0.C.W. +0.D.V. +0.e.1e. +0.E.U. +0.G.Q. +0.h.1d. +0.H.P. +0.j.1c. +0.k.1b. +0.l.1a. +0.m.19. +0.o.17. +0.p.16. +0.t.12. +0.u.11. +0.v.10. +0.x.+. +3.I.O. +3.J.N. +3.M.M. +3.n.18. +4.d.1f. +4.w./. +c.T.F. +c.Y.A. +i.c.1h. +j.1j.a. +0.16.q. +0.A.Z. +0.C.X. +0.d.1g. +0.D.W. +0.E.V. +0.f.1e. +0.G.R. +0.H.Q. +0.k.1c. +0.l.1b. +0.m.1a. +0.N.K. +0.p.17. +0.r.13. +0.u.12. +0.U.F. +0.v.11. +0.w.10. +0.y.+. +3.I.P. +3.J.O. +3.n.19. +4.e.1f. +4.i.1d. +4.o.18. +4.x./. +c.Y.B. +i.c.1i. +j.1j.b. +0.+.z. +0.17.q. +0.B.Z. +0.D.X. +0.e.1g. +0.E.W. +0.g.1e. +0.H.R. +0.l.1c. +0.L.N. +0.m.1b. +0.o.19. +0.O.K. +0.p.18. +0.r.14. +0.s.13. +0.v.12. +0.V.F. +0.w.11. +0.x.10. +3.I.Q. +3.J.P. +3.n.1a. +4.f.1f. +4.j.1d. +4.S.G. +4.y./. +c.Y.C. +i.c.1j. +0./.z. +0.18.q. +0.C.Z. +0.E.X. +0.f.1g. +0.g.1f. +0.K.P. +0.L.O. +0.m.1c. +0.o.1a. +0.p.19. +0.r.15. +0.s.14. +0.t.13. +0.w.12. +0.W.F. +0.x.11. +0.y.10. +3.I.R. +3.J.Q. +3.M.N. +3.n.1b. +4.h.1e. +4.k.1d. +4.S.H. +c.T.G. +c.Y.D. +0.10.z. +0.19.q. +0.g.1g. +0.h.1f. +0.i.1e. +0.K.Q. +0.L.P. +0.o.1b. +0.p.1a. +0.r.16. +0.s.15. +0.t.14. +0.u.13. +0.x.12. +0.X.F. +0.y.11. +3.J.R. +3.M.O. +3.n.1c. +4.d.1h. +4.D.Z. +4.G.U. +4.I.S. +4.l.1d. +c.T.H. +c.Y.E. +0.11.z. +0.1a.q. +0.A.+. +0.i.1f. +0.j.1e. +0.K.R. +0.L.Q. +0.o.1c. +0.p.1b. +0.s.16. +0.t.15. +0.u.14. +0.U.H. +0.v.13. +0.y.12. +3.I.T. +3.M.P. +4.d.1i. +4.e.1h. +4.E.Z. +4.G.V. +4.h.1g. +4.J.S. +4.m.1d. +c.Y.F. +i.c.1k. +j.17.r. +0.12.z. +0.1b.q. +0.A./. +0.B.+. +0.i.1g. +0.k.1e. +0.L.R. +0.N.N. +0.p.1c. +0.r.18. +0.s.17. +0.t.16. +0.u.15. +0.v.14. +0.V.H. +0.w.13. +3.1l.c. +3.I.U. +3.J.T. +3.M.Q. +3.n.1d. +4.d.1j. +4.e.1i. +4.f.1h. +4.F.Z. +4.G.W. +4.j.1f. +4.S.K. +0.1c.q. +0.A.10. +0.B./. +0.C.+. +0.G.X. +0.j.1g. +0.l.1e. +0.O.N. +0.r.19. +0.t.17. +0.u.16. +0.v.15. +0.w.14. +0.W.H. +0.x.13. +3.I.V. +3.J.U. +3.M.R. +4.e.1j. +4.f.1i. +4.g.1h. +4.k.1f. +4.o.1d. +4.s.18. +4.S.L. +c.T.K. +i.c.1m. +0.A.11. +0.B.10. +0.C./. +0.D.+. +0.h.1h. +0.k.1g. +0.l.1f. +0.m.1e. +0.N.P. +0.r.1a. +0.s.19. +0.t.18. +0.u.17. +0.U.K. +0.v.16. +0.w.15. +0.x.14. +0.X.H. +0.y.13. +3.I.W. +3.J.V. +4.f.1j. +4.g.1i. +4.p.1d. +4.S.M. +c.T.L. +c.Y.G. +e.O.O. +i.c.1n. +0.13.z. +0.1d.q. +0.A.12. +0.B.11. +0.C.10. +0.D./. +0.d.1k. +0.E.+. +0.G.Z. +0.h.1i. +0.l.1g. +0.L.U. +0.m.1f. +0.N.Q. +0.O.P. +0.r.1b. +0.s.1a. +0.t.19. +0.u.18. +0.v.17. +0.V.K. +0.w.16. +0.x.15. +0.y.14. +3.I.X. +3.J.W. +3.M.T. +3.n.1e. +4.g.1j. +4.i.1h. +c.Y.H. +i.c.1o. +0.+.F. +0.14.z. +0.B.12. +0.C.11. +0.D.10. +0.E./. +0.e.1k. +0.H.Z. +0.L.V. +0.m.1g. +0.N.R. +0.o.1e. +0.O.Q. +0.P.P. +0.r.1c. +0.s.1b. +0.t.1a. +0.u.19. +0.v.18. +0.w.17. +0.W.K. +0.x.16. +0.y.15. +3.1l.d. +3.I.Y. +3.J.X. +3.M.U. +3.n.1f. +4.h.1j. +4.i.1i. +4.j.1h. +i.c.1p. +0./.F. +0.15.z. +0.1C.0. +0.C.12. +0.D.11. +0.d.1m. +0.E.10. +0.f.1k. +0.L.W. +0.o.1f. +0.O.R. +0.p.1e. +0.P.Q. +0.s.1c. +0.t.1b. +0.u.1a. +0.v.19. +0.w.18. +0.x.17. +0.X.K. +0.y.16. +3.1l.e. +3.I.Z. +3.J.Y. +3.M.V. +3.n.1g. +4.i.1j. +4.j.1i. +4.k.1h. +4.S.N. +i.c.1q. +0.10.F. +0.16.z. +0.1C.1. +0.A.13. +0.D.12. +0.d.1n. +0.E.11. +0.e.1m. +0.g.1k. +0.L.X. +0.o.1g. +0.p.1f. +0.q.1e. +0.Q.Q. +0.r.1d. +0.R.P. +0.u.1b. +0.v.1a. +0.w.19. +0.x.18. +0.y.17. +3.1l.f. +3.J.Z. +3.M.W. +4.j.1j. +4.k.1i. +4.l.1h. +4.S.O. +4.t.1c. +c.T.N. +c.Y.K. +i.c.1r. +0.11.F. +0.17.z. +0.A.14. +0.B.13. +0.d.1o. +0.E.12. +0.e.1n. +0.f.1m. +0.h.1k. +0.K.Z. +0.p.1g. +0.q.1f. +0.R.Q. +0.s.1d. +0.U.N. +0.v.1b. +0.w.1a. +0.x.19. +0.y.18. +3.1l.g. +3.M.X. +4.1C.2. +4.G.+. +4.k.1j. +4.l.1i. +4.m.1h. +4.S.P. +4.u.1c. +c.T.O. +c.Y.L. +i.c.1s. +0.+.H. +0.12.F. +0.18.z. +0.1C.3. +0.A.15. +0.B.14. +0.C.13. +0.d.1p. +0.e.1o. +0.f.1n. +0.g.1m. +0.i.1k. +0.L.Z. +0.O.U. +0.q.1g. +0.R.R. +0.t.1d. +0.v.1c. +0.V.N. +0.w.1b. +0.x.1a. +0.y.19. +3.1l.h. +3.M.Y. +3.n.1h. +4.G./. +4.l.1j. +4.m.1i. +4.S.Q. +c.T.P. +i.c.1t. +0./.H. +0.19.z. +0.A.16. +0.B.15. +0.C.14. +0.D.13. +0.e.1p. +0.f.1o. +0.G.10. +0.g.1n. +0.h.1m. +0.O.V. +0.r.1e. +0.u.1d. +0.U.P. +0.w.1c. +0.W.N. +0.x.1b. +0.y.1a. +3.1l.i. +3.1u.c. +3.I.+. +3.M.Z. +3.n.1i. +4.d.1q. +4.j.1k. +4.m.1j. +4.o.1h. +4.S.R. +c.T.Q. +0.1a.z. +0.1C.5. +0.A.17. +0.B.16. +0.C.15. +0.D.14. +0.E.13. +0.e.1q. +0.f.1p. +0.G.11. +0.g.1o. +0.H.10. +0.h.1n. +0.i.1m. +0.k.1k. +0.O.W. +0.r.1f. +0.s.1e. +0.U.Q. +0.v.1d. +0.V.P. +0.X.N. +0.y.1b. +3.1l.j. +3.I./. +3.J.+. +3.n.1j. +4.d.1r. +4.o.1i. +4.p.1h. +4.x.1c. +c.T.R. +e.S.S. +i.c.1v. +0.+.K. +0.13.F. +0.1b.z. +0.1C.6. +0.B.17. +0.C.16. +0.D.15. +0.E.14. +0.e.1r. +0.f.1q. +0.G.12. +0.g.1p. +0.H.11. +0.h.1o. +0.i.1n. +0.j.1m. +0.l.1k. +0.O.X. +0.r.1g. +0.s.1f. +0.t.1e. +0.U.R. +0.V.Q. +0.w.1d. +0.W.P. +0.y.1c. +3.1l.k. +3.I.10. +3.J./. +4.A.18. +4.d.1s. +4.o.1j. +4.p.1i. +4.q.1h. +4.S.T. +c.Y.N. +i.c.1w. +0./.K. +0.14.F. +0.1c.z. +0.A.19. +0.B.18. +0.C.17. +0.D.16. +0.d.1t. +0.E.15. +0.e.1s. +0.f.1r. +0.g.1q. +0.H.12. +0.h.1p. +0.i.1o. +0.j.1n. +0.k.1m. +0.L.+. +0.m.1k. +0.N.Z. +0.s.1g. +0.t.1f. +0.V.R. +0.W.Q. +0.x.1d. +0.X.P. +3.1l.l. +3.I.11. +3.J.10. +4.p.1j. +4.q.1i. +4.S.U. +4.u.1e. +c.T.T. +c.Y.O. +i.c.1x. +0.1C.8. +0.1j.q. +0.A.1a. +0.B.19. +0.D.17. +0.E.16. +0.f.1s. +0.g.1r. +0.h.1q. +0.i.1p. +0.j.1o. +0.K.10. +0.k.1n. +0.L./. +0.l.1m. +0.O.Z. +0.u.1f. +0.v.1e. +0.W.R. +0.X.Q. +0.y.1d. +3.1l.m. +3.1u.d. +3.I.12. +3.J.11. +3.M.+. +3.n.1k. +4.15.F. +4.C.18. +4.e.1t. +4.S.V. +4.t.1g. +c.T.U. +c.Y.P. +i.c.1y. +0.16.F. +0.1d.z. +0.A.1b. +0.B.1a. +0.C.19. +0.d.1v. +0.E.17. +0.f.1t. +0.G.13. +0.g.1s. +0.h.1r. +0.i.1q. +0.K.11. +0.k.1o. +0.L.10. +0.l.1n. +0.m.1m. +0.o.1k. +0.P.Z. +0.r.1h. +0.v.1f. +0.w.1e. +0.X.R. +3.1l.n. +3.1u.e. +3.J.12. +3.M./. +4.D.18. +4.j.1p. +4.S.W. +4.u.1g. +4.U.U. +c.T.V. +c.Y.Q. +i.c.1z. +0.17.F. +0.A.1c. +0.B.1b. +0.D.19. +0.e.1v. +0.G.14. +0.g.1t. +0.H.13. +0.h.1s. +0.i.1r. +0.j.1q. +0.K.12. +0.k.1p. +0.L.11. +0.l.1o. +0.m.1n. +0.p.1k. +0.Q.Z. +0.r.1i. +0.U.V. +0.w.1f. +0.x.1e. +3.1l.o. +3.1u.f. +3.M.10. +3.n.1m. +4.C.1a. +4.d.1w. +4.E.18. +4.s.1h. +4.S.X. +4.v.1g. +c.T.W. +c.Y.R. +e.1C.a. +i.c.1A. +0.+.N. +0.18.F. +0.1k.q. +0.B.1c. +0.C.1b. +0.E.19. +0.f.1v. +0.G.15. +0.H.14. +0.h.1t. +0.j.1r. +0.k.1q. +0.L.12. +0.l.1p. +0.m.1o. +0.o.1m. +0.r.1j. +0.R.Z. +0.t.1h. +0.V.V. +0.w.1g. +0.W.U. +0.x.1f. +0.y.1e. +3.1l.p. +3.1u.g. +3.I.13. +3.M.11. +3.n.1n. +4.D.1a. +4.d.1x. +4.e.1w. +4.i.1s. +4.s.1i. +4.S.Y. +c.T.X. +e.1C.b. +i.c.1B. +0./.N. +0.19.F. +0.1e.z. +0.C.1c. +0.D.1b. +0.E.1a. +0.e.1x. +0.G.16. +0.g.1v. +0.H.15. +0.i.1t. +0.k.1r. +0.l.1q. +0.m.1p. +0.O.+. +0.o.1n. +0.p.1m. +0.t.1i. +0.u.1h. +0.U.X. +0.W.V. +0.y.1f. +3.1l.q. +3.1u.h. +3.I.14. +3.J.13. +3.M.12. +3.n.1o. +4.A.1d. +4.d.1y. +4.f.1w. +4.j.1s. +4.s.1j. +4.S.Z. +4.x.1g. +c.T.Y. +i.c.1C. +0.+.P. +0.1a.F. +0.1f.z. +0.1m.q. +0.D.1c. +0.d.1z. +0.E.1b. +0.e.1y. +0.f.1x. +0.G.17. +0.H.16. +0.h.1v. +0.j.1t. +0.K.13. +0.l.1r. +0.m.1q. +0.N.10. +0.O./. +0.o.1o. +0.p.1n. +0.u.1i. +0.v.1h. +0.V.X. +0.W.W. +3.1u.i. +3.I.15. +3.J.14. +3.n.1p. +4.B.1d. +4.g.1w. +4.k.1s. +4.t.1j. +4.y.1g. +c.T.Z. +c.Y.U. +i.c.1D. +0./.P. +0.+.Q. +0.1b.F. +0.1n.q. +0.d.1A. +0.E.1c. +0.e.1z. +0.f.1y. +0.G.18. +0.g.1x. +0.H.17. +0.h.1w. +0.i.1v. +0.K.14. +0.k.1t. +0.L.13. +0.l.1s. +0.m.1r. +0.N.11. +0.O.10. +0.o.1p. +0.p.1o. +0.r.1k. +0.U.Z. +0.v.1i. +0.W.X. +3.1u.j. +3.I.16. +3.J.15. +3.n.1q. +4.C.1d. +4.u.1j. +4.w.1h. +c.Y.V. +h.1g.z. +0./.Q. +0.+.R. +0.1o.q. +0.A.1e. +0.d.1B. +0.e.1A. +0.f.1z. +0.G.19. +0.g.1y. +0.h.1x. +0.j.1v. +0.K.15. +0.L.14. +0.l.1t. +0.m.1s. +0.N.12. +0.O.11. +0.o.1q. +0.P.10. +0.p.1p. +0.s.1k. +0.V.Z. +0.w.1i. +0.x.1h. +0.X.X. +3.1l.r. +3.1u.k. +3.I.17. +3.J.16. +3.M.13. +3.n.1r. +4.1c.F. +4.D.1d. +4.H.18. +4.i.1w. +4.v.1j. +c.Y.W. +i.c.1E. +0./.R. +0.1p.q. +0.A.1f. +0.B.1e. +0.d.1C. +0.e.1B. +0.f.1A. +0.G.1a. +0.g.1z. +0.H.19. +0.h.1y. +0.i.1x. +0.K.16. +0.k.1v. +0.L.15. +0.m.1t. +0.O.12. +0.o.1r. +0.P.11. +0.p.1q. +0.Q.10. +0.r.1m. +0.t.1k. +0.W.Z. +0.x.1i. +0.y.1h. +3.1l.s. +3.1u.l. +3.I.18. +3.J.17. +3.M.14. +3.n.1s. +4.E.1d. +4.j.1w. +4.S.+. +4.w.1j. +c.Y.X. +i.c.1F. +0.1d.F. +0.1h.z. +0.1q.q. +0.A.1g. +0.B.1f. +0.C.1e. +0.d.1D. +0.e.1C. +0.f.1B. +0.g.1A. +0.G.1b. +0.H.1a. +0.h.1z. +0.i.1y. +0.K.17. +0.L.16. +0.l.1v. +0.P.12. +0.p.1r. +0.Q.11. +0.R.10. +0.r.1n. +0.s.1m. +0.u.1k. +0.X.Z. +0.y.1i. +3.1l.t. +3.1u.m. +3.I.19. +3.J.18. +3.M.15. +3.n.1t. +4.j.1x. +4.k.1w. +4.o.1s. +4.S./. +4.x.1j. +c.T.+. +c.Y.Y. +i.c.1G. +0.1i.z. +0.1r.q. +0.B.1g. +0.C.1f. +0.D.1e. +0.e.1D. +0.f.1C. +0.g.1B. +0.h.1A. +0.H.1b. +0.i.1z. +0.j.1y. +0.k.1x. +0.L.17. +0.m.1v. +0.N.13. +0.o.1t. +0.p.1s. +0.Q.12. +0.R.11. +0.r.1o. +0.s.1n. +0.t.1m. +0.U.+. +0.v.1k. +3.1l.u. +3.1u.n. +3.I.1a. +3.J.19. +3.M.16. +4.G.1c. +4.K.18. +4.l.1w. +4.S.10. +4.y.1j. +c.T./. +c.Y.Z. +i.c.1H. +0.1j.z. +0.1s.q. +0.C.1g. +0.d.1E. +0.D.1f. +0.E.1e. +0.f.1D. +0.g.1C. +0.h.1B. +0.H.1c. +0.i.1A. +0.j.1z. +0.K.19. +0.k.1y. +0.L.18. +0.l.1x. +0.N.14. +0.O.13. +0.p.1t. +0.R.12. +0.r.1p. +0.s.1o. +0.t.1n. +0.U./. +0.u.1m. +0.V.+. +0.w.1k. +0.Z.Z. +3.1l.v. +3.1u.o. +3.I.1b. +3.J.1a. +3.M.17. +3.n.1v. +4.m.1w. +4.S.11. +c.T.10. +i.c.1I. +0.13.P. +0.1e.F. +0.1t.q. +0.d.1F. +0.D.1g. +0.e.1E. +0.E.1f. +0.g.1D. +0.G.1d. +0.h.1C. +0.i.1B. +0.j.1A. +0.K.1a. +0.k.1z. +0.L.19. +0.l.1y. +0.m.1x. +0.N.15. +0.O.14. +0.o.1v. +0.r.1q. +0.s.1p. +0.t.1o. +0.U.10. +0.u.1n. +0.V./. +0.v.1m. +0.W.+. +0.x.1k. +3.1l.w. +3.1u.p. +3.I.1c. +3.J.1b. +3.M.18. +3.n.1w. +4.A.1h. +4.S.12. +c.T.11. +i.c.1J. +0.+.X. +0.13.Q. +0.14.P. +0.d.1G. +0.e.1F. +0.E.1g. +0.f.1E. +0.F.1f. +0.h.1D. +0.i.1C. +0.j.1B. +0.k.1A. +0.K.1b. +0.L.1a. +0.l.1z. +0.m.1y. +0.N.16. +0.O.15. +0.p.1v. +0.r.1r. +0.s.1q. +0.t.1p. +0.U.11. +0.u.1o. +0.V.10. +0.v.1n. +0.W./. +0.w.1m. +0.y.1k. +3.1l.x. +3.1u.q. +3.J.1c. +3.M.19. +3.n.1x. +4.A.1i. +4.B.1h. +4.H.1d. +4.o.1w. +c.T.12. +0./.X. +0.13.R. +0.14.Q. +0.15.P. +0.1k.z. +0.1S.0. +0.1v.q. +0.d.1H. +0.e.1G. +0.f.1F. +0.g.1E. +0.i.1D. +0.j.1C. +0.k.1B. +0.K.1c. +0.l.1A. +0.L.1b. +0.m.1z. +0.N.17. +0.O.16. +0.o.1x. +0.p.1w. +0.r.1s. +0.s.1r. +0.t.1q. +0.U.12. +0.u.1p. +0.V.11. +0.v.1o. +0.W.10. +0.w.1n. +0.x.1m. +3.1l.y. +3.I.1d. +3.M.1a. +3.n.1y. +4.A.1j. +4.B.1i. +4.C.1h. +4.F.1g. +c.Y.+. +0.+.Z. +0.14.R. +0.15.Q. +0.16.P. +0.1B.l. +0.1S.1. +0.1w.q. +0.d.1I. +0.e.1H. +0.f.1G. +0.g.1F. +0.j.1D. +0.k.1C. +0.L.1c. +0.m.1A. +0.N.18. +0.O.17. +0.o.1y. +0.p.1x. +0.r.1t. +0.s.1s. +0.t.1r. +0.u.1q. +0.V.12. +0.v.1p. +0.W.11. +0.w.1o. +0.X.10. +0.x.1n. +0.y.1m. +3.1l.z. +3.J.1d. +3.M.1b. +3.n.1z. +4.B.1j. +4.C.1i. +4.D.1h. +4.G.1e. +4.h.1E. +4.S.13. +c.Y./. +i.c.1K. +j.0.1T. +0./.Z. +0.15.R. +0.16.Q. +0.17.P. +0.1B.m. +0.1m.z. +0.1x.q. +0.d.1J. +0.e.1I. +0.f.1H. +0.G.1f. +0.g.1G. +0.H.1e. +0.h.1F. +0.i.1E. +0.k.1D. +0.l.1C. +0.N.19. +0.o.1z. +0.p.1y. +0.s.1t. +0.u.1r. +0.v.1q. +0.W.12. +0.w.1p. +0.X.11. +0.x.1o. +0.y.1n. +2.1.1T. +3.1u.r. +3.M.1c. +3.n.1A. +4.1S.2. +4.C.1j. +4.D.1i. +4.E.1h. +4.K.1d. +4.O.18. +4.S.14. +4.t.1s. +c.T.13. +c.Y.10. +i.c.1L. +j.0.1U. +0.10.Z. +0.16.R. +0.17.Q. +0.18.P. +0.1n.z. +0.1S.3. +0.1V.0. +0.1y.q. +0.A.1k. +0.e.1J. +0.f.1I. +0.g.1H. +0.H.1f. +0.h.1G. +0.i.1F. +0.j.1E. +0.L.1d. +0.l.1D. +0.m.1C. +0.N.1a. +0.O.19. +0.o.1A. +0.p.1z. +0.r.1v. +0.t.1t. +0.U.13. +0.u.1s. +0.v.1r. +0.w.1q. +0.X.12. +0.x.1p. +0.y.1o. +2.1.1U. +2.2.1T. +3.1u.s. +3.I.1e. +4.D.1j. +4.E.1i. +4.F.1h. +4.G.1g. +4.S.15. +c.T.14. +c.Y.11. +e.n.1B. +i.c.1M. +0.11.Z. +0.17.R. +0.18.Q. +0.19.P. +0.1B.o. +0.1o.z. +0.1V.1. +0.1z.q. +0.B.1k. +0.f.1J. +0.g.1I. +0.H.1g. +0.h.1H. +0.i.1G. +0.j.1F. +0.k.1E. +0.m.1D. +0.N.1b. +0.O.1a. +0.p.1A. +0.r.1w. +0.s.1v. +0.U.14. +0.u.1t. +0.V.13. +0.v.1s. +0.w.1r. +0.x.1q. +0.y.1p. +2.2.1U. +2.3.1T. +3.1l.A. +3.1u.t. +3.I.1f. +3.J.1e. +3.M.1d. +3.n.1C. +3.S.16. +4.E.1j. +4.F.1i. +c.T.15. +c.Y.12. +e.4.1S. +i.c.1N. +0.+.+. +0.18.R. +0.19.Q. +0.1a.P. +0.1A.q. +0.1B.p. +0.1j.F. +0.1p.z. +0.1S.5. +0.1V.2. +0.A.1m. +0.C.1k. +0.d.1K. +0.g.1J. +0.h.1I. +0.i.1H. +0.j.1G. +0.K.1e. +0.k.1F. +0.l.1E. +0.N.1c. +0.O.1b. +0.o.1C. +0.r.1x. +0.t.1v. +0.U.15. +0.V.14. +0.v.1t. +0.W.13. +0.w.1s. +0.x.1r. +0.y.1q. +2.3.1U. +2.4.1T. +3.1l.B. +3.1u.u. +3.I.1g. +3.J.1f. +3.n.1D. +4.12.Z. +4.S.17. +4.s.1w. +c.T.16. +i.c.1O. +0./.+. +0.19.R. +0.1a.Q. +0.1b.P. +0.1B.q. +0.1q.z. +0.1S.6. +0.1V.3. +0.A.1n. +0.B.1m. +0.D.1k. +0.d.1L. +0.e.1K. +0.G.1h. +0.h.1J. +0.i.1I. +0.j.1H. +0.K.1f. +0.k.1G. +0.L.1e. +0.l.1F. +0.m.1E. +0.O.1c. +0.o.1D. +0.p.1C. +0.r.1y. +0.s.1x. +0.t.1w. +0.U.16. +0.u.1v. +0.V.15. +0.W.14. +0.w.1t. +0.X.13. +0.x.1s. +0.y.1r. +2.4.1U. +2.5.1T. +3.1l.C. +3.1u.v. +3.J.1g. +4.S.18. +c.T.17. +i.c.1P. +0././. +0.+.10. +0.1a.R. +0.1b.Q. +0.1c.P. +0.1C.q. +0.1r.z. +0.A.1o. +0.B.1n. +0.C.1m. +0.d.1M. +0.E.1k. +0.e.1L. +0.f.1K. +0.G.1i. +0.i.1J. +0.j.1I. +0.K.1g. +0.k.1H. +0.L.1f. +0.l.1G. +0.m.1F. +0.p.1D. +0.r.1z. +0.s.1y. +0.t.1x. +0.U.17. +0.u.1w. +0.V.16. +0.v.1v. +0.W.15. +0.X.14. +0.x.1t. +0.y.1s. +2.5.1U. +2.6.1T. +3.1l.D. +3.1u.w. +3.M.1e. +3.n.1E. +3.S.19. +4.H.1h. +4.N.1d. +c.T.18. +c.Y.13. +e.7.1S. +i.c.1Q. +0./.10. +0.+.11. +0.13.Z. +0.1b.R. +0.1c.Q. +0.1D.q. +0.1k.F. +0.1S.8. +0.1s.z. +0.1V.5. +0.A.1p. +0.B.1o. +0.C.1n. +0.D.1m. +0.d.1N. +0.e.1M. +0.f.1L. +0.g.1K. +0.j.1J. +0.k.1I. +0.L.1g. +0.l.1H. +0.m.1G. +0.o.1E. +0.r.1A. +0.s.1z. +0.t.1y. +0.u.1x. +0.V.17. +0.v.1w. +0.W.16. +0.w.1v. +0.X.15. +0.y.1t. +2.6.1U. +2.7.1T. +3.1l.E. +3.1u.x. +3.I.1h. +3.M.1f. +3.n.1F. +3.S.1a. +4.G.1j. +4.H.1i. +4.O.1d. +4.U.18. +c.T.19. +c.Y.14. +0./.11. +0.+.12. +0.10.10. +0.1B.r. +0.1c.R. +0.1d.P. +0.1j.H. +0.1t.z. +0.1V.6. +0.A.1q. +0.B.1p. +0.C.1o. +0.D.1n. +0.d.1O. +0.E.1m. +0.e.1N. +0.f.1M. +0.g.1L. +0.h.1K. +0.k.1J. +0.l.1I. +0.m.1H. +0.o.1F. +0.p.1E. +0.s.1A. +0.t.1z. +0.U.19. +0.u.1y. +0.V.18. +0.v.1x. +0.W.17. +0.w.1w. +0.X.16. +0.x.1v. +2.7.1U. +3.1l.F. +3.1u.y. +3.I.1i. +3.J.1h. +3.M.1g. +3.n.1G. +4.14.Z. +4.S.1b. +c.T.1a. +c.Y.15. +e.9.1S. +j.8.1T. +0./.12. +0.11.10. +0.1B.s. +0.1d.Q. +0.1E.q. +0.1m.F. +0.A.1r. +0.B.1q. +0.C.1p. +0.D.1o. +0.d.1P. +0.E.1n. +0.e.1O. +0.f.1N. +0.g.1M. +0.h.1L. +0.i.1K. +0.l.1J. +0.m.1I. +0.N.1e. +0.o.1G. +0.p.1F. +0.r.1C. +0.t.1A. +0.U.1a. +0.u.1z. +0.V.19. +0.v.1y. +0.w.1x. +0.X.17. +0.x.1w. +0.y.1v. +2.9.1T. +3.1u.z. +3.I.1j. +3.J.1i. +3.n.1H. +4.15.Z. +4.1S.a. +4.K.1h. +4.S.1c. +4.W.18. +c.T.1b. +c.Y.16. +j.8.1U. +0.11.11. +0.12.10. +0.16.Z. +0.1B.t. +0.1d.R. +0.1F.q. +0.1n.F. +0.1S.b. +0.1V.8. +0.1v.z. +0.A.1s. +0.B.1r. +0.C.1q. +0.D.1p. +0.E.1o. +0.e.1P. +0.f.1O. +0.G.1k. +0.g.1N. +0.h.1M. +0.i.1L. +0.j.1K. +0.L.1h. +0.m.1J. +0.N.1f. +0.O.1e. +0.o.1H. +0.p.1G. +0.r.1D. +0.s.1C. +0.u.1A. +0.U.1b. +0.V.1a. +0.v.1z. +0.W.19. +0.w.1y. +0.X.18. +0.x.1x. +0.y.1w. +2.9.1U. +3.J.1j. +3.n.1I. +4.d.1Q. +4.K.1i. +c.T.1c. +c.Y.17. +i.c.1R. +j.a.1T. +0.+.13. +0.11.12. +0.17.Z. +0.1B.u. +0.1G.q. +0.1j.K. +0.1k.H. +0.1o.F. +0.1w.z. +0.A.1t. +0.B.1s. +0.C.1r. +0.D.1q. +0.E.1p. +0.f.1P. +0.g.1O. +0.h.1N. +0.i.1M. +0.j.1L. +0.k.1K. +0.L.1i. +0.N.1g. +0.O.1f. +0.o.1I. +0.P.1e. +0.p.1H. +0.s.1D. +0.t.1C. +0.U.1c. +0.v.1A. +0.V.1b. +0.W.1a. +0.w.1z. +0.X.19. +0.x.1y. +0.y.1x. +2.b.1T. +3.1l.G. +3.M.1h. +3.n.1J. +4.e.1Q. +4.S.1d. +c.Y.18. +i.c.1S. +j.a.1U. +0./.13. +0.+.14. +0.12.12. +0.1B.v. +0.1H.q. +0.1p.F. +0.1V.a. +0.1x.z. +0.B.1t. +0.D.1r. +0.E.1q. +0.G.1m. +0.g.1P. +0.h.1O. +0.i.1N. +0.j.1M. +0.k.1L. +0.L.1j. +0.l.1K. +0.O.1g. +0.o.1J. +0.P.1f. +0.p.1I. +0.Q.1e. +0.r.1E. +0.t.1D. +0.u.1C. +0.V.1c. +0.w.1A. +0.W.1b. +0.X.1a. +0.x.1z. +0.y.1y. +2.b.1U. +3.1l.H. +3.1u.A. +3.I.1k. +3.M.1i. +4.18.Z. +4.C.1s. +4.f.1Q. +c.T.1d. +c.Y.19. +0./.14. +0.+.15. +0.13.10. +0.19.Z. +0.1B.w. +0.1I.q. +0.1m.H. +0.1q.F. +0.1y.z. +0.A.1v. +0.C.1t. +0.D.1s. +0.G.1n. +0.h.1P. +0.i.1O. +0.j.1N. +0.k.1M. +0.l.1L. +0.m.1K. +0.P.1g. +0.p.1J. +0.Q.1f. +0.R.1e. +0.r.1F. +0.s.1E. +0.u.1D. +0.v.1C. +0.W.1c. +0.x.1A. +0.X.1b. +0.y.1z. +3.1l.I. +3.1u.B. +3.J.1k. +3.M.1j. +4.E.1r. +4.g.1Q. +4.U.1d. +c.Y.1a. +f.1V.b. +0./.15. +0.+.16. +0.13.11. +0.14.10. +0.1B.x. +0.1J.q. +0.1k.K. +0.1n.H. +0.1r.F. +0.1z.z. +0.B.1v. +0.d.1R. +0.D.1t. +0.G.1o. +0.h.1Q. +0.i.1P. +0.j.1O. +0.l.1M. +0.m.1L. +0.Q.1g. +0.R.1f. +0.r.1G. +0.s.1F. +0.t.1E. +0.v.1D. +0.w.1C. +0.X.1c. +0.y.1A. +3.1l.J. +3.1u.C. +3.I.1m. +3.n.1K. +4.1a.Z. +4.A.1w. +4.E.1s. +4.k.1N. +4.N.1h. +4.S.1e. +4.V.1d. +c.Y.1b. +i.c.1V. +0./.16. +0.+.17. +0.13.12. +0.14.11. +0.15.10. +0.1A.z. +0.1B.y. +0.1b.Z. +0.1o.H. +0.1s.F. +0.A.1x. +0.C.1v. +0.d.1S. +0.e.1R. +0.E.1t. +0.G.1p. +0.i.1Q. +0.j.1P. +0.k.1O. +0.L.1k. +0.l.1N. +0.m.1M. +0.o.1K. +0.R.1g. +0.r.1H. +0.s.1G. +0.t.1F. +0.w.1D. +0.x.1C. +3.1l.K. +3.1u.D. +3.I.1n. +3.J.1m. +3.n.1L. +4.B.1w. +4.N.1i. +4.O.1h. +4.S.1f. +4.u.1E. +4.W.1d. +c.T.1e. +c.Y.1c. +i.c.1W. +0./.17. +0.+.18. +0.14.12. +0.15.11. +0.16.10. +0.1c.Z. +0.1j.N. +0.1m.K. +0.A.1y. +0.B.1x. +0.D.1v. +0.e.1S. +0.f.1R. +0.G.1q. +0.H.1p. +0.k.1P. +0.l.1O. +0.m.1N. +0.o.1L. +0.p.1K. +0.r.1I. +0.s.1H. +0.t.1G. +0.U.1e. +0.u.1F. +0.x.1D. +0.y.1C. +3.1l.L. +3.1u.E. +3.I.1o. +3.J.1n. +3.M.1k. +3.n.1M. +4.1B.z. +4.1t.F. +4.C.1w. +4.j.1Q. +4.O.1i. +4.P.1h. +4.S.1g. +4.v.1E. +4.X.1d. +c.T.1f. +0.+.19. +0.15.12. +0.16.11. +0.17.10. +0.1C.z. +0.1K.q. +0.1n.K. +0.A.1z. +0.B.1y. +0.C.1x. +0.E.1v. +0.f.1S. +0.g.1R. +0.G.1r. +0.L.1m. +0.l.1P. +0.m.1O. +0.o.1M. +0.p.1L. +0.r.1J. +0.s.1I. +0.t.1H. +0.u.1G. +0.V.1e. +0.v.1F. +0.y.1D. +3.1l.M. +3.1u.F. +3.I.1p. +3.J.1o. +3.n.1N. +4./.18. +4.D.1w. +4.H.1q. +4.k.1Q. +4.O.1j. +4.P.1i. +4.Q.1h. +4.U.1f. +4.w.1E. +c.T.1g. +c.Y.1d. +i.c.1X. +0./.19. +0.+.1a. +0.13.13. +0.16.12. +0.17.11. +0.18.10. +0.1D.z. +0.1d.Z. +0.1j.P. +0.1L.q. +0.1o.K. +0.1v.F. +0.A.1A. +0.B.1z. +0.C.1y. +0.d.1V. +0.D.1x. +0.g.1S. +0.h.1R. +0.H.1r. +0.L.1n. +0.l.1Q. +0.m.1P. +0.o.1N. +0.p.1M. +0.R.1h. +0.s.1J. +0.t.1I. +0.U.1g. +0.u.1H. +0.V.1f. +0.v.1G. +0.W.1e. +0.w.1F. +3.I.1q. +3.J.1p. +3.M.1m. +3.n.1O. +4.E.1w. +4.G.1s. +4.Q.1i. +4.x.1E. +0./.1a. +0.+.1b. +0.13.14. +0.17.12. +0.18.11. +0.19.10. +0.1B.A. +0.1j.Q. +0.1k.N. +0.1M.q. +0.1w.F. +0.B.1A. +0.C.1z. +0.D.1y. +0.e.1V. +0.E.1x. +0.G.1t. +0.H.1s. +0.i.1R. +0.K.1p. +0.L.1o. +0.m.1Q. +0.o.1O. +0.p.1N. +0.t.1J. +0.u.1I. +0.V.1g. +0.v.1H. +0.W.1f. +0.w.1G. +0.X.1e. +0.x.1F. +0.y.1E. +3.I.1r. +3.J.1q. +3.M.1n. +3.n.1P. +4.d.1W. +4.h.1S. +4.R.1i. +4.S.1h. +0./.1b. +0.+.1c. +0.13.15. +0.18.12. +0.19.11. +0.1a.10. +0.1B.B. +0.1E.z. +0.1j.R. +0.1N.q. +0.1t.H. +0.1x.F. +0.A.1C. +0.C.1A. +0.D.1z. +0.E.1y. +0.f.1V. +0.i.1S. +0.j.1R. +0.K.1q. +0.L.1p. +0.O.1k. +0.o.1P. +0.p.1O. +0.r.1K. +0.u.1J. +0.v.1I. +0.W.1g. +0.w.1H. +0.X.1f. +0.x.1G. +0.y.1F. +3.1l.N. +3.1u.G. +3.I.1s. +3.J.1r. +3.M.1o. +3.n.1Q. +4.14.14. +4.e.1W. +4.S.1i. +c.T.1h. +c.Y.1e. +0./.1c. +0.14.15. +0.16.13. +0.19.12. +0.1a.11. +0.1b.10. +0.1B.C. +0.1F.z. +0.1k.P. +0.1m.N. +0.1O.q. +0.1y.F. +0.A.1D. +0.B.1C. +0.D.1A. +0.d.1X. +0.E.1z. +0.f.1W. +0.G.1v. +0.g.1V. +0.k.1R. +0.K.1r. +0.L.1q. +0.p.1P. +0.r.1L. +0.s.1K. +0.v.1J. +0.w.1I. +0.X.1g. +0.x.1H. +0.y.1G. +3.1l.O. +3.1u.H. +3.I.1t. +3.J.1s. +3.M.1p. +4.1e.Z. +4.j.1S. +4.o.1Q. +4.S.1j. +4.U.1h. +c.T.1i. +c.Y.1f. +0.+.1d. +0.15.15. +0.16.14. +0.17.13. +0.1a.12. +0.1b.11. +0.1B.D. +0.1c.10. +0.1f.Z. +0.1G.z. +0.1k.Q. +0.1n.N. +0.1P.q. +0.1z.F. +0.B.1D. +0.C.1C. +0.E.1A. +0.e.1X. +0.G.1w. +0.H.1v. +0.k.1S. +0.K.1s. +0.l.1R. +0.L.1r. +0.O.1m. +0.p.1Q. +0.r.1M. +0.s.1L. +0.t.1K. +0.w.1J. +0.x.1I. +0.y.1H. +3.1l.P. +3.1u.I. +3.J.1t. +3.M.1q. +4.g.1W. +4.h.1V. +4.U.1i. +4.V.1h. +c.T.1j. +c.Y.1g. +0.16.15. +0.17.14. +0.18.13. +0.1A.F. +0.1b.12. +0.1B.E. +0.1c.11. +0.1H.z. +0.1j.U. +0.1k.R. +0.1m.P. +0.1o.N. +0.1t.K. +0.A.1E. +0.C.1D. +0.D.1C. +0.f.1X. +0.G.1x. +0.h.1W. +0.l.1S. +0.L.1s. +0.m.1R. +0.O.1n. +0.r.1N. +0.s.1M. +0.t.1L. +0.u.1K. +0.x.1J. +0.y.1I. +3.1l.Q. +3.1u.J. +3.I.1v. +3.M.1r. +4./.1d. +4.H.1w. +4.i.1V. +4.q.1Q. +4.V.1i. +4.W.1h. +4.Z.1g. +0.13.19. +0.16.16. +0.17.15. +0.18.14. +0.1B.F. +0.1c.12. +0.1d.10. +0.1I.z. +0.1j.V. +0.1m.Q. +0.1n.P. +0.A.1F. +0.B.1E. +0.D.1D. +0.E.1C. +0.g.1X. +0.G.1y. +0.H.1x. +0.i.1W. +0.L.1t. +0.m.1S. +0.N.1p. +0.O.1o. +0.r.1O. +0.s.1N. +0.t.1M. +0.u.1L. +0.v.1K. +0.y.1J. +3.1l.R. +3.1u.K. +3.I.1w. +3.J.1v. +3.M.1s. +3.n.1R. +4.j.1V. +4.S.1k. +4.W.1i. +4.X.1h. +e.0.1Z. +0.+.1e. +0.13.1a. +0.14.19. +0.16.17. +0.18.15. +0.1C.F. +0.1d.11. +0.1j.W. +0.1J.z. +0.1m.R. +0.1n.Q. +0.1o.P. +0.A.1G. +0.B.1F. +0.C.1E. +0.E.1D. +0.G.1z. +0.h.1X. +0.H.1y. +0.K.1v. +0.N.1q. +0.o.1R. +0.r.1P. +0.s.1O. +0.t.1N. +0.u.1M. +0.v.1L. +0.w.1K. +3.1u.L. +3.I.1x. +3.J.1w. +3.M.1t. +3.n.1S. +4.1l.S. +4.j.1W. +4.k.1V. +4.O.1p. +4.X.1i. +c.T.1k. +c.Y.1h. +e.0.1+. +e.1.1Z. +0./.1e. +0.+.1f. +0.13.1b. +0.15.19. +0.17.17. +0.1d.12. +0.1D.F. +0.1h.Z. +0.1j.X. +0.1n.R. +0.1o.Q. +0.1p.P. +0.A.1H. +0.B.1G. +0.C.1F. +0.D.1E. +0.G.1A. +0.H.1z. +0.i.1X. +0.l.1V. +0.L.1v. +0.N.1r. +0.O.1q. +0.o.1S. +0.p.1R. +0.r.1Q. +0.s.1P. +0.t.1O. +0.U.1k. +0.u.1N. +0.v.1M. +0.w.1L. +0.x.1K. +3.1l.T. +3.1u.M. +3.I.1y. +3.J.1x. +4.14.1a. +4.16.18. +4.K.1w. +4.k.1W. +4.S.1m. +c.Y.1i. +e.0.1/. +e.1.1+. +e.2.1Z. +0./.1f. +0.+.1g. +0.10.1e. +0.16.19. +0.17.18. +0.1b.14. +0.1B.G. +0.1c.13. +0.1o.R. +0.1p.Q. +0.1q.P. +0.A.1I. +0.B.1H. +0.C.1G. +0.D.1F. +0.E.1E. +0.H.1A. +0.j.1X. +0.K.1x. +0.L.1w. +0.m.1V. +0.N.1s. +0.O.1r. +0.p.1S. +0.s.1Q. +0.t.1P. +0.u.1O. +0.V.1k. +0.v.1N. +0.w.1M. +0.x.1L. +0.y.1K. +3.1l.U. +3.I.1z. +3.J.1y. +3.M.1v. +4.15.1a. +4.1i.Z. +4.l.1W. +4.S.1n. +c.T.1m. +c.Y.1j. +e.0.20. +e.1.1/. +e.2.1+. +e.3.1Z. +f.1R.q. +0./.1g. +0.10.1f. +0.11.1e. +0.17.19. +0.1b.15. +0.1B.H. +0.1c.14. +0.1j.Z. +0.1K.z. +0.1p.R. +0.1q.Q. +0.1r.P. +0.1t.N. +0.A.1J. +0.B.1I. +0.C.1H. +0.D.1G. +0.E.1F. +0.G.1C. +0.k.1X. +0.K.1y. +0.L.1x. +0.m.1W. +0.O.1s. +0.q.1S. +0.t.1Q. +0.U.1m. +0.u.1P. +0.v.1O. +0.W.1k. +0.w.1N. +0.x.1M. +0.y.1L. +3.1l.V. +3.I.1A. +3.J.1z. +3.M.1w. +3.n.1V. +4.16.1a. +4.18.18. +4.1E.F. +4.S.1o. +c.T.1n. +e.0.21. +e.1.20. +e.2.1/. +e.3.1+. +e.4.1Z. +0.10.1g. +0.11.1f. +0.12.1e. +0.16.1b. +0.18.19. +0.1c.15. +0.1d.13. +0.1F.F. +0.1k.X. +0.1L.z. +0.1q.R. +0.1r.Q. +0.1s.P. +0.B.1J. +0.C.1I. +0.D.1H. +0.E.1G. +0.G.1D. +0.H.1C. +0.K.1z. +0.l.1X. +0.L.1y. +0.O.1t. +0.o.1V. +0.U.1n. +0.u.1Q. +0.V.1m. +0.v.1P. +0.w.1O. +0.x.1N. +0.y.1M. +3.1l.W. +3.1u.N. +3.I.1B. +3.J.1A. +3.M.1x. +3.n.1W. +4.17.1a. +4.S.1p. +c.T.1o. +e.0.22. +e.1.21. +e.2.20. +e.3.1/. +e.4.1+. +e.5.1Z. +0.+.1h. +0.11.1g. +0.12.1f. +0.17.1b. +0.18.1a. +0.19.19. +0.1c.16. +0.1d.14. +0.1G.F. +0.1M.z. +0.1r.R. +0.1s.Q. +0.1t.P. +0.C.1J. +0.D.1I. +0.E.1H. +0.H.1D. +0.K.1A. +0.L.1z. +0.m.1X. +0.N.1v. +0.p.1V. +0.r.1R. +0.U.1o. +0.V.1n. +0.v.1Q. +0.W.1m. +0.w.1P. +0.x.1O. +0.y.1N. +3.1l.X. +3.1u.O. +3.I.1C. +3.J.1B. +3.M.1y. +4.o.1W. +4.S.1q. +c.T.1p. +c.Y.1k. +e.0.23. +e.1.22. +e.2.21. +e.3.20. +e.4.1/. +e.5.1+. +e.6.1Z. +0.+.1i. +0.12.1g. +0.18.1b. +0.19.1a. +0.1B.K. +0.1c.17. +0.1d.15. +0.1H.F. +0.1k.Z. +0.1m.X. +0.1N.z. +0.1s.R. +0.A.1K. +0.D.1J. +0.E.1I. +0.L.1A. +0.O.1v. +0.p.1W. +0.q.1V. +0.s.1R. +0.U.1p. +0.V.1o. +0.W.1n. +0.w.1Q. +0.x.1P. +0.y.1O. +3.1l.Y. +3.1u.P. +3.I.1D. +3.J.1C. +3.M.1z. +3.n.1X. +4./.1h. +4.1t.Q. +4.G.1E. +4.N.1w. +4.r.1S. +4.S.1r. +c.T.1q. +e.0.24. +e.1.23. +e.2.22. +e.3.21. +e.4.20. +e.5.1/. +e.6.1+. +e.7.1Z. +i.c.1Y. +0.13.1e. +0.1b.19. +0.1B.L. +0.1d.16. +0.1E.H. +0.1I.F. +0.1j.+. +0.1n.X. +0.1O.z. +0.1t.R. +0.1v.P. +0.A.1L. +0.B.1K. +0.E.1J. +0.G.1F. +0.K.1C. +0.N.1x. +0.o.1X. +0.s.1S. +0.t.1R. +0.U.1q. +0.V.1p. +0.W.1o. +0.x.1Q. +0.y.1P. +3.1l.Z. +3.1u.Q. +3.J.1D. +3.M.1A. +4./.1i. +4.10.1h. +4.1a.1a. +4.1c.18. +4.O.1w. +4.q.1W. +4.S.1s. +c.T.1r. +c.Y.1m. +e.0.25. +e.1.24. +e.2.23. +e.3.22. +e.4.21. +e.5.20. +e.6.1/. +e.7.1+. +e.8.1Z. +0.13.1f. +0.14.1e. +0.1b.1a. +0.1c.19. +0.1d.17. +0.1j./. +0.1J.F. +0.1m.Z. +0.1o.X. +0.1P.z. +0.1v.Q. +0.1w.P. +0.A.1M. +0.B.1L. +0.C.1K. +0.G.1G. +0.H.1F. +0.K.1D. +0.L.1C. +0.N.1y. +0.O.1x. +0.p.1X. +0.t.1S. +0.u.1R. +0.U.1r. +0.V.1q. +0.W.1p. +0.y.1Q. +3.1u.R. +3.I.1E. +3.M.1B. +4.10.1i. +4.11.1h. +4.S.1t. +c.T.1s. +c.Y.1n. +e.0.26. +e.1.25. +e.2.24. +e.3.23. +e.4.22. +e.5.21. +e.6.20. +e.7.1/. +e.8.1+. +e.9.1Z. +0.13.1g. +0.15.1e. +0.1b.1b. +0.1c.1a. +0.1j.10. +0.1n.Z. +0.1Q.z. +0.1v.R. +0.1w.Q. +0.1x.P. +0.A.1N. +0.B.1M. +0.C.1L. +0.D.1K. +0.G.1H. +0.H.1G. +0.L.1D. +0.N.1z. +0.O.1y. +0.q.1X. +0.r.1V. +0.U.1s. +0.u.1S. +0.V.1r. +0.v.1R. +0.W.1q. +0.X.1p. +3.I.1F. +3.J.1E. +3.M.1C. +4.11.1i. +4.12.1h. +4.14.1f. +4.1d.18. +4.1u.S. +c.T.1t. +c.Y.1o. +e.0.27. +e.1.26. +e.2.25. +e.3.24. +e.4.23. +e.5.22. +e.6.21. +e.7.20. +e.8.1/. +e.9.1+. +e.a.1Z. +0.+.1k. +0.14.1g. +0.16.1e. +0.1c.1b. +0.1d.19. +0.1E.K. +0.1j.11. +0.1o.Z. +0.1w.R. +0.1x.Q. +0.1y.P. +0.A.1O. +0.B.1N. +0.C.1M. +0.D.1L. +0.d.1Y. +0.E.1K. +0.G.1I. +0.H.1H. +0.N.1A. +0.O.1z. +0.r.1W. +0.s.1V. +0.U.1t. +0.v.1S. +0.V.1s. +0.w.1R. +0.W.1r. +0.X.1q. +3.1u.T. +3.I.1G. +3.J.1F. +3.M.1D. +4.12.1i. +4.15.1f. +4.S.1v. +c.Y.1p. +e.1.27. +e.2.26. +e.3.25. +e.4.24. +e.5.23. +e.6.22. +e.7.21. +e.8.20. +e.9.1/. +e.a.1+. +e.b.1Z. +0./.1k. +0.15.1g. +0.16.1f. +0.17.1e. +0.1B.N. +0.1c.1c. +0.1j.12. +0.1p.Z. +0.1x.R. +0.1y.Q. +0.1z.P. +0.A.1P. +0.B.1O. +0.C.1N. +0.D.1M. +0.E.1L. +0.e.1Y. +0.G.1J. +0.H.1I. +0.K.1F. +0.L.1E. +0.O.1A. +0.s.1W. +0.V.1t. +0.W.1s. +0.x.1R. +0.X.1r. +3.1l.+. +3.1u.U. +3.I.1H. +3.J.1G. +4.1d.1a. +4.1K.F. +4.S.1w. +4.t.1V. +4.w.1S. +c.28.0. +c.T.1v. +c.Y.1q. +e.2.27. +e.3.26. +e.4.25. +e.5.24. +e.6.23. +e.7.22. +e.8.21. +e.9.20. +e.a.1/. +e.b.1+. +i.c.1Z. +0.+.1m. +0.16.1g. +0.18.1e. +0.1A.P. +0.1B.O. +0.1d.1b. +0.1k.10. +0.1L.F. +0.1q.Z. +0.1y.R. +0.1z.Q. +0.B.1P. +0.C.1O. +0.D.1N. +0.E.1M. +0.f.1Y. +0.H.1J. +0.K.1G. +0.L.1F. +0.N.1C. +0.r.1X. +0.t.1W. +0.U.1v. +0.W.1t. +0.X.1s. +0.y.1R. +3.1l./. +3.1u.V. +3.I.1I. +3.J.1H. +3.M.1E. +4.13.1h. +4.17.1f. +4.A.1Q. +4.S.1x. +4.u.1V. +4.x.1S. +c.28.1. +c.T.1w. +c.Y.1r. +e.3.27. +e.4.26. +e.5.25. +e.6.24. +e.7.23. +e.8.22. +e.9.21. +e.a.20. +e.b.1/. +i.c.1+. +0./.1m. +0.+.1n. +0.17.1g. +0.19.1e. +0.1A.Q. +0.1B.P. +0.1d.1c. +0.1k.11. +0.1M.F. +0.1r.Z. +0.1R.z. +0.1t.X. +0.1z.R. +0.B.1Q. +0.C.1P. +0.D.1O. +0.E.1N. +0.g.1Y. +0.K.1H. +0.L.1G. +0.N.1D. +0.O.1C. +0.s.1X. +0.u.1W. +0.V.1v. +0.y.1S. +3.1l.10. +3.1u.W. +3.I.1J. +3.J.1I. +3.M.1F. +4.13.1i. +4.14.1h. +4.18.1f. +4.S.1y. +4.U.1w. +4.v.1V. +c.28.2. +c.T.1x. +c.Y.1s. +e.4.27. +e.5.26. +e.6.25. +e.7.24. +e.8.23. +e.9.22. +e.a.21. +e.b.20. +i.c.1/. +0./.1n. +0.+.1o. +0.18.1g. +0.19.1f. +0.1a.1e. +0.1A.R. +0.1B.Q. +0.1C.P. +0.1j.13. +0.1k.12. +0.1m.10. +0.1S.z. +0.1s.Z. +0.C.1Q. +0.D.1P. +0.E.1O. +0.G.1K. +0.h.1Y. +0.K.1I. +0.L.1H. +0.O.1D. +0.t.1X. +0.U.1x. +0.V.1w. +0.v.1W. +0.w.1V. +0.W.1v. +3.1l.11. +3.1u.X. +3.J.1J. +3.M.1G. +4.14.1i. +4.15.1h. +4.1N.F. +4.S.1z. +c.28.3. +c.T.1y. +c.Y.1t. +e.5.27. +e.6.26. +e.7.25. +e.8.24. +e.9.23. +e.a.22. +e.b.21. +i.c.20. +0./.1o. +0.+.1p. +0.19.1g. +0.1b.1e. +0.1B.R. +0.1C.Q. +0.1D.P. +0.1E.N. +0.1j.14. +0.1m.11. +0.1n.10. +0.1O.F. +0.D.1Q. +0.d.1Z. +0.E.1P. +0.G.1L. +0.H.1K. +0.i.1Y. +0.K.1J. +0.L.1I. +0.u.1X. +0.U.1y. +0.V.1x. +0.w.1W. +0.X.1v. +3.1l.12. +3.1u.Y. +3.M.1H. +4.15.1i. +4.16.1h. +4.1a.1f. +4.1d.1d. +4.1t.Z. +4.S.1A. +4.W.1w. +4.x.1V. +c.T.1z. +e.4.28. +e.6.27. +e.7.26. +e.8.25. +e.9.24. +e.a.23. +e.b.22. +i.c.21. +0.+.1q. +0.1a.1g. +0.1c.1e. +0.1C.R. +0.1D.Q. +0.1j.15. +0.1m.12. +0.1n.11. +0.1o.10. +0.1P.F. +0.A.1R. +0.d.1+. +0.E.1Q. +0.e.1Z. +0.G.1M. +0.H.1L. +0.j.1Y. +0.L.1J. +0.N.1F. +0.O.1E. +0.U.1z. +0.v.1X. +0.V.1y. +0.W.1x. +0.x.1W. +3.1u.Z. +3.I.1K. +3.M.1I. +4./.1p. +4.16.1i. +4.17.1h. +4.1b.1f. +4.S.1B. +4.X.1w. +4.y.1V. +c.28.5. +c.T.1A. +c.Y.1v. +e.7.27. +e.8.26. +e.9.25. +e.a.24. +e.b.23. +i.c.22. +0./.1q. +0.+.1r. +0.1b.1g. +0.1D.R. +0.1E.P. +0.1j.16. +0.1k.13. +0.1n.12. +0.1o.11. +0.1p.10. +0.1v.Z. +0.A.1S. +0.B.1R. +0.d.1/. +0.e.1+. +0.f.1Z. +0.G.1N. +0.H.1M. +0.k.1Y. +0.N.1G. +0.O.1F. +0.U.1A. +0.V.1z. +0.w.1X. +0.W.1y. +0.X.1x. +0.y.1W. +3.I.1L. +3.J.1K. +3.M.1J. +4.17.1i. +4.18.1h. +4.1c.1f. +4.F.1Q. +4.S.1C. +c.28.6. +c.T.1B. +c.Y.1w. +e.8.27. +e.9.26. +e.a.25. +e.b.24. +f.1V.z. +i.c.23. +0./.1r. +0.+.1s. +0.19.1h. +0.1c.1g. +0.1d.1e. +0.1E.Q. +0.1F.P. +0.1j.17. +0.1k.14. +0.1o.12. +0.1p.11. +0.1q.10. +0.1W.z. +0.1w.Z. +0.B.1S. +0.C.1R. +0.d.20. +0.e.1/. +0.f.1+. +0.G.1O. +0.g.1Z. +0.H.1N. +0.K.1K. +0.l.1Y. +0.N.1H. +0.O.1G. +0.V.1A. +0.W.1z. +0.x.1X. +0.X.1y. +3.1l.13. +3.I.1M. +3.J.1L. +4.18.1i. +4.1B.U. +4.S.1D. +c.T.1C. +c.Y.1x. +e.7.28. +e.9.27. +e.a.26. +e.b.25. +i.c.24. +0./.1s. +0.+.1t. +0.19.1i. +0.1E.R. +0.1F.Q. +0.1G.P. +0.1j.18. +0.1k.15. +0.1m.13. +0.1p.12. +0.1q.11. +0.1r.10. +0.1x.Z. +0.C.1S. +0.D.1R. +0.d.21. +0.e.20. +0.f.1/. +0.g.1+. +0.G.1P. +0.H.1O. +0.h.1Z. +0.K.1L. +0.L.1K. +0.m.1Y. +0.N.1I. +0.O.1H. +0.U.1C. +0.W.1A. +0.X.1z. +0.y.1X. +3.1l.14. +3.I.1N. +3.J.1M. +4.1a.1h. +4.1B.V. +4.1d.1f. +c.28.8. +c.T.1D. +c.Y.1y. +e.a.27. +e.b.26. +i.c.25. +0./.1t. +0.1B.W. +0.1d.1g. +0.1F.R. +0.1G.Q. +0.1H.P. +0.1j.19. +0.1k.16. +0.1m.14. +0.1n.13. +0.1q.12. +0.1r.11. +0.1s.10. +0.1X.z. +0.1y.Z. +0.A.1V. +0.D.1S. +0.d.22. +0.E.1R. +0.e.21. +0.f.20. +0.g.1/. +0.H.1P. +0.i.1Z. +0.K.1M. +0.L.1L. +0.N.1J. +0.O.1I. +0.U.1D. +0.V.1C. +0.X.1A. +3.1l.15. +3.1u.+. +3.I.1O. +3.J.1N. +3.M.1K. +3.n.1Y. +4.1a.1i. +4.1b.1h. +4.G.1Q. +4.h.1+. +4.S.1E. +c.Y.1z. +e.9.28. +e.b.27. +i.c.26. +0.+.1v. +0.1B.X. +0.1e.1e. +0.1G.R. +0.1H.Q. +0.1I.P. +0.1j.1a. +0.1k.17. +0.1m.15. +0.1n.14. +0.1o.13. +0.1r.12. +0.1R.F. +0.1s.11. +0.1t.10. +0.1z.Z. +0.A.1W. +0.B.1V. +0.d.23. +0.E.1S. +0.e.22. +0.f.21. +0.g.20. +0.h.1/. +0.i.1+. +0.j.1Z. +0.K.1N. +0.L.1M. +0.O.1J. +0.o.1Y. +0.V.1D. +0.W.1C. +3.1l.16. +3.1u./. +3.I.1P. +3.J.1O. +3.M.1L. +4.1b.1i. +4.1c.1h. +4.H.1Q. +4.S.1F. +c.28.a. +c.T.1E. +c.Y.1A. +i.c.27. +0./.1v. +0.+.1w. +0.1A.Z. +0.1e.1f. +0.1H.R. +0.1I.Q. +0.1j.1b. +0.1k.18. +0.1m.16. +0.1n.15. +0.1o.14. +0.1p.13. +0.1s.12. +0.1t.11. +0.B.1W. +0.C.1V. +0.d.24. +0.e.23. +0.f.22. +0.g.21. +0.i.1/. +0.j.1+. +0.K.1O. +0.k.1Z. +0.L.1N. +0.P.1J. +0.p.1Y. +0.U.1E. +0.W.1D. +0.X.1C. +3.1l.17. +3.1u.10. +3.I.1Q. +3.J.1P. +3.M.1M. +4.1c.1i. +4.1S.F. +4.h.20. +4.S.1G. +c.28.b. +c.T.1F. +c.Y.1B. +0.+.1x. +0.1B.Z. +0.1e.1g. +0.1f.1f. +0.1I.R. +0.1j.1c. +0.1k.19. +0.1m.17. +0.1n.16. +0.1o.15. +0.1p.14. +0.1q.13. +0.1v.10. +0.A.1X. +0.D.1V. +0.d.25. +0.e.24. +0.f.23. +0.g.22. +0.h.21. +0.i.20. +0.j.1/. +0.k.1+. +0.K.1P. +0.L.1O. +0.l.1Z. +0.N.1K. +0.Q.1J. +0.q.1Y. +0.U.1F. +0.V.1E. +0.X.1D. +3.1l.18. +3.1u.11. +3.J.1Q. +3.M.1N. +4./.1w. +4.1d.1h. +4.1t.12. +4.C.1W. +4.S.1H. +c.T.1G. +c.Y.1C. +i.c.28. +0./.1x. +0.+.1y. +0.1C.Z. +0.1f.1g. +0.1k.1a. +0.1n.17. +0.1o.16. +0.1p.15. +0.1q.14. +0.1r.13. +0.1v.11. +0.1w.10. +0.B.1X. +0.d.26. +0.e.25. +0.f.24. +0.G.1R. +0.g.23. +0.h.22. +0.i.21. +0.j.20. +0.k.1/. +0.l.1+. +0.L.1P. +0.m.1Z. +0.N.1L. +0.O.1K. +0.R.1J. +0.U.1G. +0.V.1F. +0.W.1E. +3.1l.19. +3.1u.12. +3.M.1O. +4.1d.1i. +4.1m.18. +4.D.1W. +4.E.1V. +4.K.1Q. +4.S.1I. +c.T.1H. +c.Y.1D. +0./.1y. +0.+.1z. +0.1D.Z. +0.1E.X. +0.1j.1d. +0.1k.1b. +0.1m.19. +0.1n.18. +0.1o.17. +0.1p.16. +0.1q.15. +0.1r.14. +0.1s.13. +0.1v.12. +0.1w.11. +0.1x.10. +0.C.1X. +0.d.27. +0.e.26. +0.f.25. +0.g.24. +0.H.1R. +0.i.22. +0.j.21. +0.k.20. +0.l.1/. +0.L.1Q. +0.m.1+. +0.N.1M. +0.O.1L. +0.P.1K. +0.U.1H. +0.V.1G. +0.W.1F. +3.1l.1a. +3.M.1P. +3.n.1Z. +4.E.1W. +4.F.1V. +4.G.1S. +4.h.23. +4.S.1J. +c.T.1I. +e.1g.1g. +0./.1z. +0.+.1A. +0.1k.1c. +0.1m.1a. +0.1n.19. +0.1o.18. +0.1p.17. +0.1q.16. +0.1r.15. +0.1s.14. +0.1t.13. +0.1w.12. +0.1x.11. +0.1y.10. +0.D.1X. +0.e.27. +0.f.26. +0.g.25. +0.H.1S. +0.h.24. +0.i.23. +0.j.22. +0.k.21. +0.l.20. +0.m.1/. +0.N.1N. +0.O.1M. +0.o.1Z. +0.P.1L. +0.Q.1K. +0.r.1Y. +0.U.1I. +0.V.1H. +0.W.1G. +0.X.1F. +3.1l.1b. +3.I.1R. +3.M.1Q. +3.n.1+. +4.1e.1h. +4.F.1W. +c.T.1J. +c.Y.1E. +0./.1A. +0.1B.+. +0.1E.Z. +0.1m.1b. +0.1n.1a. +0.1o.19. +0.1p.18. +0.1q.17. +0.1r.16. +0.1s.15. +0.1x.12. +0.1y.11. +0.1z.10. +0.E.1X. +0.f.27. +0.g.26. +0.h.25. +0.i.24. +0.j.23. +0.k.22. +0.l.21. +0.m.20. +0.N.1O. +0.o.1+. +0.O.1N. +0.P.1M. +0.p.1Z. +0.Q.1L. +0.R.1K. +0.s.1Y. +0.U.1J. +0.V.1I. +0.W.1H. +0.X.1G. +3.1l.1c. +3.1u.13. +3.I.1S. +3.J.1R. +3.n.1/. +4.1e.1i. +4.1f.1h. +4.1t.14. +c.28.d. +c.Y.1F. +e.0.29. +0.+.1C. +0.1A.10. +0.1F.Z. +0.1h.1g. +0.1j.1e. +0.1m.1c. +0.1n.1b. +0.1o.1a. +0.1p.19. +0.1r.17. +0.1s.16. +0.1t.15. +0.1v.13. +0.1y.12. +0.1z.11. +0.F.1X. +0.g.27. +0.h.26. +0.i.25. +0.j.24. +0.K.1R. +0.k.23. +0.l.22. +0.m.21. +0.N.1P. +0.o.1/. +0.O.1O. +0.p.1+. +0.P.1N. +0.Q.1M. +0.q.1Z. +0.R.1L. +0.t.1Y. +0.V.1J. +0.W.1I. +0.X.1H. +3.1u.14. +3.J.1S. +4.1B./. +4.1f.1i. +4.1k.1d. +4.1q.18. +4.G.1V. +4.S.1K. +c.28.e. +c.Y.1G. +e.0.2a. +e.1.29. +e.n.20. +0./.1C. +0.+.1D. +0.1A.11. +0.1B.10. +0.1G.Z. +0.1i.1g. +0.1j.1f. +0.1n.1c. +0.1o.1b. +0.1p.1a. +0.1q.19. +0.1s.17. +0.1t.16. +0.1v.14. +0.1w.13. +0.1z.12. +0.G.1W. +0.H.1V. +0.h.27. +0.i.26. +0.j.25. +0.K.1S. +0.k.24. +0.L.1R. +0.l.23. +0.m.22. +0.N.1Q. +0.O.1P. +0.o.20. +0.p.1/. +0.P.1O. +0.q.1+. +0.Q.1N. +0.R.1M. +0.u.1Y. +0.W.1J. +0.X.1I. +3.1l.1d. +3.1u.15. +3.n.21. +3.S.1L. +4.1r.18. +c.28.f. +c.T.1K. +c.Y.1H. +e.1.2a. +e.2.29. +0./.1D. +0.1A.12. +0.1B.11. +0.1C.10. +0.1H.Z. +0.1j.1g. +0.1o.1c. +0.1p.1b. +0.1q.1a. +0.1r.19. +0.1t.17. +0.1v.15. +0.1w.14. +0.1x.13. +0.i.27. +0.j.26. +0.k.25. +0.L.1S. +0.l.24. +0.m.23. +0.o.21. +0.P.1P. +0.p.20. +0.q.1/. +0.Q.1O. +0.R.1N. +0.U.1K. +0.v.1Y. +0.X.1J. +3.1u.16. +3.I.1V. +3.M.1R. +3.n.22. +4.1m.1d. +4.1s.18. +4.H.1W. +4.O.1Q. +4.S.1M. +c.28.g. +c.T.1L. +c.Y.1I. +e.2.2a. +e.3.29. +0.1B.12. +0.1C.11. +0.1D.10. +0.1E.+. +0.1I.Z. +0.1k.1e. +0.1p.1c. +0.1q.1b. +0.1r.1a. +0.1s.19. +0.1v.16. +0.1w.15. +0.1x.14. +0.1y.13. +0.G.1X. +0.j.27. +0.k.26. +0.l.25. +0.m.24. +0.o.22. +0.p.21. +0.Q.1P. +0.q.20. +0.R.1O. +0.r.1Z. +0.U.1L. +0.V.1K. +0.w.1Y. +3.1u.17. +3.I.1W. +3.J.1V. +3.M.1S. +3.n.23. +3.S.1N. +4.1n.1d. +4.1t.18. +4.P.1Q. +c.28.h. +c.T.1M. +c.Y.1J. +e.0.2c. +e.1h.1h. +e.3.2a. +e.4.29. +0.+.1F. +0.1C.12. +0.1D.11. +0.1E./. +0.1k.1f. +0.1o.1d. +0.1q.1c. +0.1r.1b. +0.1t.19. +0.1v.17. +0.1w.16. +0.1x.15. +0.1y.14. +0.1z.13. +0.H.1X. +0.K.1V. +0.k.27. +0.l.26. +0.m.25. +0.o.23. +0.p.22. +0.Q.1Q. +0.q.21. +0.r.1+. +0.R.1P. +0.s.1Z. +0.U.1M. +0.V.1L. +0.W.1K. +0.x.1Y. +3.1l.1e. +3.1u.18. +3.J.1W. +3.n.24. +3.S.1O. +4.1h.1i. +4.1J.Z. +4.1s.1a. +c.28.i. +c.T.1N. +e.1.2c. +e.4.2a. +e.5.29. +0./.1F. +0.+.1G. +0.1A.13. +0.1D.12. +0.1E.10. +0.1j.1h. +0.1k.1g. +0.1m.1e. +0.1r.1c. +0.1s.1b. +0.1v.18. +0.1w.17. +0.1x.16. +0.1y.15. +0.1z.14. +0.K.1W. +0.L.1V. +0.l.27. +0.m.26. +0.N.1R. +0.o.24. +0.p.23. +0.q.22. +0.r.1/. +0.R.1Q. +0.s.1+. +0.t.1Z. +0.U.1N. +0.V.1M. +0.W.1L. +0.X.1K. +0.y.1Y. +3.1l.1f. +3.1u.19. +3.I.1X. +3.n.25. +3.S.1P. +4.1i.1i. +4.1p.1d. +4.1t.1a. +c.28.j. +c.T.1O. +e.2.2c. +e.5.2a. +e.6.29. +0./.1G. +0.+.1H. +0.1A.14. +0.1B.13. +0.1E.11. +0.1F.10. +0.1j.1i. +0.1m.1f. +0.1n.1e. +0.1s.1c. +0.1v.19. +0.1w.18. +0.1x.17. +0.1y.16. +0.1z.15. +0.L.1W. +0.m.27. +0.N.1S. +0.O.1R. +0.o.25. +0.p.24. +0.q.23. +0.r.20. +0.s.1/. +0.U.1O. +0.u.1Z. +0.V.1N. +0.W.1M. +0.X.1L. +0.z.1Y. +3.1l.1g. +3.1u.1a. +3.J.1X. +3.M.1V. +3.n.26. +4.1q.1d. +4.1t.1b. +4.S.1Q. +4.t.1+. +c.28.k. +c.T.1P. +c.Y.1K. +e.0.2d. +e.3.2c. +e.6.2a. +e.7.29. +0./.1H. +0.+.1I. +0.1A.15. +0.1B.14. +0.1C.13. +0.1E.12. +0.1F.11. +0.1G.10. +0.1j.1j. +0.1m.1g. +0.1n.1f. +0.1o.1e. +0.1t.1c. +0.1v.1a. +0.1w.19. +0.1x.18. +0.1y.17. +0.1z.16. +0.K.1X. +0.O.1S. +0.o.26. +0.P.1R. +0.p.25. +0.q.24. +0.r.21. +0.s.20. +0.U.1P. +0.V.1O. +0.v.1Z. +0.W.1N. +0.X.1M. +3.1u.1b. +3.M.1W. +3.n.27. +4.1K.Z. +4.1r.1d. +4.t.1/. +4.u.1+. +c.28.l. +c.T.1Q. +c.Y.1L. +e.1.2d. +e.4.2c. +e.7.2a. +e.8.29. +0./.1I. +0.+.1J. +0.1A.16. +0.1B.15. +0.1C.14. +0.1D.13. +0.1F.12. +0.1G.11. +0.1H.10. +0.1L.Z. +0.1n.1g. +0.1o.1f. +0.1p.1e. +0.1v.1b. +0.1w.1a. +0.1x.19. +0.1z.17. +0.L.1X. +0.o.27. +0.P.1S. +0.p.26. +0.Q.1R. +0.q.25. +0.r.22. +0.s.21. +0.u.1/. +0.v.1+. +0.V.1P. +0.W.1O. +0.w.1Z. +0.X.1N. +3.1u.1c. +4.1k.1h. +4.1s.1d. +4.1y.18. +4.t.20. +4.U.1Q. +c.28.m. +c.Y.1M. +e.2.2d. +e.5.2c. +e.8.2a. +e.9.29. +0./.1J. +0.1A.17. +0.1B.16. +0.1C.15. +0.1D.14. +0.1G.12. +0.1H.11. +0.1I.10. +0.1M.Z. +0.1o.1g. +0.1p.1f. +0.1q.1e. +0.1v.1c. +0.1w.1b. +0.1x.1a. +0.1y.19. +0.A.1Y. +0.N.1V. +0.p.27. +0.Q.1S. +0.q.26. +0.R.1R. +0.r.23. +0.s.22. +0.t.21. +0.u.20. +0.v.1/. +0.V.1Q. +0.W.1P. +0.X.1O. +0.x.1Z. +3.1l.1h. +3.M.1X. +3.n.28. +4.1k.1i. +4.1t.1d. +4.1z.18. +4.w.1+. +c.Y.1N. +e.3.2d. +e.6.2c. +e.9.2a. +e.a.29. +0.1B.17. +0.1C.16. +0.1D.15. +0.1E.13. +0.1H.12. +0.1I.11. +0.1J.10. +0.1j.1k. +0.1p.1g. +0.1q.1f. +0.1r.1e. +0.1w.1c. +0.1x.1b. +0.1y.1a. +0.1z.19. +0.B.1Y. +0.N.1W. +0.O.1V. +0.q.27. +0.R.1S. +0.r.24. +0.s.23. +0.u.21. +0.v.20. +0.w.1/. +0.W.1Q. +0.X.1P. +0.y.1Z. +3.1l.1i. +3.1u.1d. +3.S.1R. +4.1A.18. +4.1m.1h. +4.1N.Z. +4.t.22. +4.x.1+. +c.28.o. +c.Y.1O. +e.4.2d. +e.7.2c. +e.a.2a. +e.b.29. +0.+.1K. +0.11.1J. +0.1A.19. +0.1B.18. +0.1C.17. +0.1D.16. +0.1E.14. +0.1F.13. +0.1I.12. +0.1O.Z. +0.1q.1g. +0.1r.1f. +0.1s.1e. +0.1x.1c. +0.1y.1b. +0.1z.1a. +0.1Z.z. +0.C.1Y. +0.P.1V. +0.r.25. +0.s.24. +0.u.22. +0.v.21. +0.w.20. +0.x.1/. +0.y.1+. +3.1l.1j. +4.1m.1i. +4.1n.1h. +4.1v.1d. +4.O.1W. +4.S.1S. +4.t.23. +4.X.1Q. +c.28.p. +c.T.1R. +c.Y.1P. +e.5.2d. +e.8.2c. +e.b.2a. +i.c.29. +0./.1K. +0.+.1L. +0.1+.z. +0.1A.1a. +0.1B.19. +0.1C.18. +0.1D.17. +0.1E.15. +0.1F.14. +0.1G.13. +0.1J.12. +0.1j.1m. +0.1o.1h. +0.1P.Z. +0.1r.1g. +0.1s.1f. +0.1t.1e. +0.1y.1c. +0.1z.1b. +0.D.1Y. +0.N.1X. +0.Q.1V. +0.r.26. +0.s.25. +0.t.24. +0.U.1R. +0.u.23. +0.v.22. +0.w.21. +0.x.20. +0.y.1/. +4.1n.1i. +4.1w.1d. +4.P.1W. +c.28.q. +c.T.1S. +c.Y.1Q. +e.6.2d. +e.9.2c. +i.c.2a. +0./.1L. +0.+.1M. +0.1/.z. +0.1A.1b. +0.1B.1a. +0.1C.19. +0.1E.16. +0.1F.15. +0.1G.14. +0.1H.13. +0.1j.1n. +0.1K.10. +0.1k.1k. +0.1o.1i. +0.1s.1g. +0.1z.1c. +0.E.1Y. +0.O.1X. +0.Q.1W. +0.R.1V. +0.r.27. +0.s.26. +0.t.25. +0.u.24. +0.V.1R. +0.v.23. +0.w.22. +0.x.21. +0.y.20. +0.Z.1Q. +3.1u.1e. +4.1D.18. +4.1p.1h. +4.1t.1f. +4.1x.1d. +4.U.1S. +e.7.2d. +e.a.2c. +0./.1M. +0.+.1N. +0.1A.1c. +0.1B.1b. +0.1D.19. +0.1E.17. +0.1F.16. +0.1G.15. +0.1H.14. +0.1I.13. +0.1j.1o. +0.1K.11. +0.1L.10. +0.1t.1g. +0.1v.1e. +0.A.1Z. +0.F.1Y. +0.P.1X. +0.R.1W. +0.s.27. +0.t.26. +0.u.25. +0.V.1S. +0.v.24. +0.W.1R. +0.w.23. +0.x.22. +0.y.21. +3.1l.1k. +3.1u.1f. +3.S.1V. +4.1C.1a. +4.1p.1i. +4.1q.1h. +4.1y.1d. +5.c.2b. +e.8.2d. +e.b.2c. +f.20.z. +0./.1N. +0.+.1O. +0.13.1J. +0.1C.1b. +0.1D.1a. +0.1d.1z. +0.1F.17. +0.1G.16. +0.1H.15. +0.1I.14. +0.1j.1p. +0.1K.12. +0.1k.1m. +0.1L.11. +0.1M.10. +0.1v.1f. +0.1w.1e. +0.A.1+. +0.B.1Z. +0.d.29. +0.Q.1X. +0.t.27. +0.u.26. +0.v.25. +0.W.1S. +0.w.24. +0.X.1R. +0.x.23. +0.y.22. +3.1l.1l. +3.1u.1g. +4.1B.1c. +4.1E.18. +4.1q.1i. +4.1r.1h. +4.S.1W. +c.28.r. +c.T.1V. +e.21.z. +e.9.2d. +i.c.2c. +0./.1O. +0.+.1P. +0.14.1J. +0.1c.1C. +0.1d.1A. +0.1D.1b. +0.1E.19. +0.1F.18. +0.1G.17. +0.1H.16. +0.1I.15. +0.1j.1q. +0.1L.12. +0.1M.11. +0.1n.1k. +0.1v.1g. +0.1w.1f. +0.1x.1e. +0.22.z. +0.A.1/. +0.B.1+. +0.C.1Z. +0.e.29. +0.R.1X. +0.u.27. +0.v.26. +0.w.25. +0.X.1S. +0.x.24. +0.y.23. +3.1l.1m. +4.1N.10. +4.1r.1i. +4.1s.1h. +4.U.1V. +5.d.2a. +c.28.s. +c.T.1W. +c.Y.1R. +e.a.2d. +0./.1P. +0.+.1Q. +0.15.1J. +0.1B.1d. +0.1D.1c. +0.1E.1a. +0.1F.19. +0.1G.18. +0.1H.17. +0.1I.16. +0.1j.1r. +0.1M.12. +0.1m.1m. +0.1N.11. +0.1O.10. +0.1o.1k. +0.1R.Z. +0.1w.1g. +0.1x.1f. +0.1y.1e. +0.A.20. +0.B.1/. +0.C.1+. +0.D.1Z. +0.f.29. +0.G.1Y. +0.V.1V. +0.v.27. +0.w.26. +0.x.25. +0.y.24. +3.1l.1n. +4.1s.1i. +4.1t.1h. +4.S.1X. +4.U.1W. +5.e.2a. +c.28.t. +c.Y.1S. +e.b.2d. +f.23.z. +0.13.1K. +0.16.1J. +0.1d.1C. +0.1E.1b. +0.1F.1a. +0.1G.19. +0.1I.17. +0.1j.1s. +0.1k.1p. +0.1n.1m. +0.1O.11. +0.1P.10. +0.1x.1g. +0.1y.1f. +0.1z.1e. +0.24.z. +0.A.21. +0.B.20. +0.C.1/. +0.D.1+. +0.E.1Z. +0.g.29. +0.H.1Y. +0.W.1V. +0.w.27. +0.x.26. +0.y.25. +3.1l.1o. +3.1u.1h. +4./.1Q. +4.1H.18. +4.1N.12. +4.1S.Z. +4.1t.1i. +4.V.1W. +5.f.2a. +c.28.u. +c.T.1X. +i.c.2d. +0.13.1L. +0.14.1K. +0.17.1J. +0.1A.1e. +0.1d.1D. +0.1E.1c. +0.1F.1b. +0.1G.1a. +0.1H.19. +0.1j.1t. +0.1k.1q. +0.1n.1n. +0.1O.12. +0.1o.1m. +0.1P.11. +0.1y.1g. +0.1z.1f. +0.25.z. +0.A.22. +0.B.21. +0.C.20. +0.D.1/. +0.d.2c. +0.E.1+. +0.F.1Z. +0.U.1X. +0.W.1W. +0.X.1V. +0.x.27. +0.y.26. +3.1l.1p. +3.1u.1i. +3.I.1Y. +4.10.1Q. +4.1I.18. +4.1v.1h. +4.h.29. +5.c.2e. +5.g.2a. +c.28.v. +0.11.1Q. +0.13.1M. +0.14.1L. +0.15.1K. +0.18.1J. +0.1A.1f. +0.1F.1c. +0.1G.1b. +0.1H.1a. +0.1I.19. +0.1k.1r. +0.1m.1p. +0.1o.1n. +0.1P.12. +0.1z.1g. +0.26.z. +0.A.23. +0.B.22. +0.C.21. +0.D.20. +0.E.1/. +0.e.2c. +0.F.1+. +0.i.29. +0.V.1X. +0.X.1W. +0.y.27. +3.1l.1q. +3.1u.1j. +3.J.1Y. +4.1B.1e. +4.1v.1i. +4.1w.1h. +5.h.2a. +c.28.w. +c.Y.1V. +0.+.1R. +0.13.1N. +0.14.1M. +0.15.1L. +0.16.1K. +0.19.1J. +0.1A.1g. +0.1B.1f. +0.1C.1e. +0.1G.1c. +0.1H.1b. +0.1I.1a. +0.1j.1v. +0.1k.1s. +0.1m.1q. +0.1n.1p. +0.1o.1o. +0.27.z. +0.A.24. +0.B.23. +0.C.22. +0.D.21. +0.E.20. +0.F.1/. +0.f.2c. +0.K.1Y. +0.W.1X. +3.1l.1r. +4.12.1Q. +4.1E.1d. +4.1w.1i. +4.1x.1h. +4.j.29. +4.Z.1V. +5.i.2a. +c.28.x. +c.Y.1W. +0./.1R. +0.+.1S. +0.13.1O. +0.14.1N. +0.15.1M. +0.16.1L. +0.17.1K. +0.1a.1J. +0.1C.1f. +0.1D.1e. +0.1d.1F. +0.1H.1c. +0.1I.1b. +0.1j.1w. +0.1k.1t. +0.1m.1r. +0.1n.1q. +0.1o.1p. +0.A.25. +0.B.24. +0.C.23. +0.D.22. +0.d.2d. +0.E.21. +0.F.20. +0.G.1Z. +0.g.2c. +0.k.29. +0.L.1Y. +0.X.1X. +0.Z.1W. +3.1l.1s. +4.1B.1g. +4.1x.1i. +4.1y.1h. +5.c.2f. +5.j.2a. +c.28.y. +0./.1S. +0.10.1R. +0.13.1P. +0.14.1O. +0.15.1N. +0.16.1M. +0.17.1L. +0.18.1K. +0.1b.1J. +0.1C.1g. +0.1d.1G. +0.1I.1c. +0.1j.1x. +0.1m.1s. +0.1n.1r. +0.1o.1q. +0.1p.1p. +0.1y.1i. +0.A.26. +0.B.25. +0.C.24. +0.D.23. +0.E.22. +0.e.2d. +0.F.21. +0.H.1Z. +0.h.2c. +0.k.2a. +0.l.29. +3.1l.1t. +3.1u.1k. +3.M.1Y. +4.1D.1f. +4.1z.1h. +4.G.1+. +c.28.z. +c.Y.1X. +0.10.1S. +0.11.1R. +0.14.1P. +0.15.1O. +0.16.1N. +0.17.1M. +0.18.1L. +0.19.1K. +0.1c.1J. +0.1D.1g. +0.1d.1H. +0.1E.1e. +0.1j.1y. +0.1k.1v. +0.1m.1t. +0.1n.1s. +0.1o.1r. +0.1p.1q. +0.A.27. +0.B.26. +0.C.25. +0.D.24. +0.E.23. +0.F.22. +0.f.2d. +0.H.1+. +0.i.2c. +0.m.29. +0.Z.1X. +3.1u.1l. +3.I.1Z. +4.13.1Q. +4.1A.1h. +4.1z.1i. +4.G.1/. +5.l.2a. +0.+.1V. +0.11.1S. +0.12.1R. +0.15.1P. +0.16.1O. +0.17.1N. +0.18.1M. +0.19.1L. +0.1a.1K. +0.1B.1h. +0.1d.1I. +0.1F.1e. +0.1j.1z. +0.1n.1t. +0.1o.1s. +0.1p.1r. +0.1q.1q. +0.B.27. +0.C.26. +0.D.25. +0.E.24. +0.F.23. +0.G.20. +0.g.2d. +0.H.1/. +0.j.2c. +0.m.2a. +3.1l.1v. +3.1u.1m. +3.I.1+. +3.J.1Z. +3.n.29. +4.14.1Q. +4.1A.1i. +4.1E.1f. +4.1k.1w. +5.k.2b. +0.+.1W. +0.16.1P. +0.17.1O. +0.18.1N. +0.19.1M. +0.1a.1L. +0.1B.1i. +0.1b.1K. +0.1d.1J. +0.1E.1g. +0.1F.1f. +0.1G.1e. +0.1j.1A. +0.1k.1x. +0.1m.1v. +0.1o.1t. +0.1p.1s. +0.1q.1r. +0.C.27. +0.D.26. +0.E.25. +0.F.24. +0.G.21. +0.H.20. +0.h.2d. +0.K.1Z. +0.k.2c. +0.N.1Y. +0.o.29. +3.1l.1w. +3.1u.1n. +3.I.1/. +3.J.1+. +3.n.2a. +4./.1V. +4.12.1S. +4.15.1Q. +4.1C.1h. +c.28.A. +0.10.1V. +0.16.1Q. +0.17.1P. +0.18.1O. +0.19.1N. +0.1a.1M. +0.1b.1L. +0.1c.1K. +0.1F.1g. +0.1G.1f. +0.1H.1e. +0.1k.1y. +0.1n.1v. +0.1q.1s. +0.1r.1r. +0.D.27. +0.E.26. +0.F.25. +0.G.22. +0.H.21. +0.i.2d. +0.K.1+. +0.L.1Z. +0.l.2c. +0.O.1Y. +0.p.29. +3.1l.1x. +3.1u.1o. +3.I.20. +3.J.1/. +4./.1W. +4.1B.1j. +4.1C.1i. +4.1D.1h. +4.1m.1w. +4.1t.1p. +5.o.2a. +c.28.B. +0.+.1X. +0.11.1V. +0.13.1R. +0.18.1P. +0.19.1O. +0.1b.1M. +0.1c.1L. +0.1G.1g. +0.1H.1f. +0.1I.1e. +0.1j.1C. +0.1k.1z. +0.1m.1x. +0.1o.1v. +0.1r.1s. +0.E.27. +0.F.26. +0.G.23. +0.H.22. +0.j.2d. +0.K.1/. +0.L.1+. +0.m.2c. +0.P.1Y. +0.q.29. +3.1l.1y. +3.1u.1p. +3.I.21. +3.J.20. +3.M.1Z. +4.10.1W. +4.17.1Q. +4.1a.1N. +4.1D.1i. +4.1n.1w. +4.1t.1q. +5.p.2a. +c.28.C. +0./.1X. +0.11.1W. +0.12.1V. +0.13.1S. +0.14.1R. +0.19.1P. +0.1a.1O. +0.1b.1N. +0.1c.1M. +0.1d.1K. +0.1H.1g. +0.1j.1D. +0.1J.1e. +0.1k.1A. +0.1m.1y. +0.1n.1x. +0.1o.1w. +0.1s.1s. +0.1v.1p. +0.F.27. +0.G.24. +0.H.23. +0.K.20. +0.k.2d. +0.L.1/. +0.Q.1Y. +3.1l.1z. +3.1u.1q. +3.I.22. +3.J.21. +3.M.1+. +3.n.2c. +4.18.1Q. +4.1E.1h. +4.1I.1f. +4.1t.1r. +5.q.2a. +c.28.D. +0.10.1X. +0.15.1R. +0.19.1Q. +0.1a.1P. +0.1B.1k. +0.1b.1O. +0.1c.1N. +0.1d.1L. +0.1I.1g. +0.1J.1f. +0.1m.1z. +0.1n.1y. +0.1o.1x. +0.1t.1s. +0.1v.1q. +0.1w.1p. +0.G.25. +0.H.24. +0.K.21. +0.L.20. +0.l.2d. +0.o.2c. +0.R.1Y. +3.1l.1A. +3.1u.1r. +3.I.23. +3.J.22. +3.M.1/. +4.12.1W. +4.14.1S. +4.1E.1i. +4.1F.1h. +5.k.2e. +c.28.E. +0.11.1X. +0.15.1S. +0.16.1R. +0.1b.1P. +0.1c.1O. +0.1d.1M. +0.1j.1E. +0.1J.1g. +0.1k.1C. +0.1m.1A. +0.1n.1z. +0.1o.1y. +0.1v.1r. +0.1w.1q. +0.1x.1p. +0.G.26. +0.H.25. +0.K.22. +0.L.21. +0.m.2d. +0.N.1Z. +0.p.2c. +0.r.29. +3.1l.1B. +3.1u.1s. +3.I.24. +3.J.23. +3.M.20. +3.S.1Y. +4.1a.1Q. +4.1F.1i. +4.1G.1h. +4.1t.1t. +c.28.F. +0.12.1X. +0.13.1V. +0.16.1S. +0.17.1R. +0.1B.1m. +0.1c.1P. +0.1d.1N. +0.1j.1F. +0.1k.1D. +0.1K.1e. +0.1n.1A. +0.1o.1z. +0.1v.1s. +0.1w.1r. +0.1x.1q. +0.1y.1p. +0.G.27. +0.H.26. +0.K.23. +0.L.22. +0.N.1+. +0.O.1Z. +0.q.2c. +0.r.2a. +0.s.29. +3.1l.1C. +3.1u.1t. +3.I.25. +3.J.24. +3.M.21. +3.n.2d. +4.1b.1Q. +4.1G.1i. +4.1H.1h. +c.T.1Y. +0.13.1W. +0.18.1R. +0.1B.1n. +0.1d.1O. +0.1j.1G. +0.1L.1e. +0.1m.1C. +0.1o.1A. +0.1p.1z. +0.1w.1s. +0.1x.1r. +0.1y.1q. +0.H.27. +0.K.24. +0.L.23. +0.N.1/. +0.O.1+. +0.o.2d. +0.P.1Z. +0.s.2a. +0.U.1Y. +3.1l.1D. +3.1u.1u. +3.I.26. +3.J.25. +3.M.22. +4.14.1V. +4.17.1S. +4.1c.1Q. +4.1H.1i. +4.1I.1h. +4.1K.1f. +4.1t.1v. +4.t.29. +5.k.2f. +0.19.1R. +0.1B.1o. +0.1d.1P. +0.1E.1k. +0.1j.1H. +0.1L.1f. +0.1m.1D. +0.1M.1e. +0.1n.1C. +0.1p.1A. +0.1q.1z. +0.1x.1s. +0.1y.1r. +0.K.25. +0.L.24. +0.N.20. +0.O.1/. +0.P.1+. +0.p.2d. +0.Q.1Z. +0.t.2a. +0.V.1Y. +3.1u.1v. +3.I.27. +3.J.26. +3.M.23. +4.14.1W. +4.15.1V. +4.18.1S. +4.1I.1i. +4.1J.1h. +4.1K.1g. +4.1t.1w. +4.u.29. +7.r.2b. +c.28.G. +0.13.1X. +0.16.1V. +0.19.1S. +0.1a.1R. +0.1B.1p. +0.1j.1I. +0.1k.1F. +0.1L.1g. +0.1M.1f. +0.1n.1D. +0.1o.1C. +0.1q.1A. +0.1r.1z. +0.1v.1v. +0.1y.1s. +0.K.26. +0.L.25. +0.N.21. +0.O.20. +0.P.1/. +0.Q.1+. +0.q.2d. +0.R.1Z. +0.r.2c. +0.u.2a. +0.v.29. +0.W.1Y. +3.1l.1E. +3.1u.1w. +3.J.27. +3.M.24. +4.15.1W. +4.1d.1Q. +4.1J.1i. +4.1N.1e. +4.1t.1x. +7.s.2b. +c.28.H. +0.14.1X. +0.1a.1S. +0.1B.1q. +0.1b.1R. +0.1E.1m. +0.1j.1J. +0.1k.1G. +0.1M.1g. +0.1o.1D. +0.1O.1e. +0.1p.1C. +0.1r.1A. +0.1s.1z. +0.1w.1v. +0.K.27. +0.L.26. +0.N.22. +0.O.21. +0.P.20. +0.Q.1/. +0.R.1+. +0.s.2c. +0.X.1Y. +3.1l.1F. +3.1u.1x. +3.I.28. +3.M.25. +3.S.1Z. +4.16.1W. +4.17.1V. +4.1N.1f. +4.1t.1y. +4.w.29. +5.v.2a. +0.15.1X. +0.18.1V. +0.1B.1r. +0.1c.1R. +0.1E.1n. +0.1k.1H. +0.1m.1F. +0.1O.1f. +0.1p.1D. +0.1P.1e. +0.1q.1C. +0.1s.1A. +0.1t.1z. +0.1w.1w. +0.1x.1v. +0.L.27. +0.N.23. +0.O.22. +0.P.21. +0.Q.20. +0.R.1/. +0.t.2c. +3.1l.1G. +3.1u.1y. +3.J.28. +3.M.26. +4.17.1W. +4.1b.1S. +4.1K.1h. +4.1N.1g. +4.S.1+. +4.u.2b. +4.x.29. +5.w.2a. +c.T.1Z. +c.Y.1Y. +0.16.1X. +0.19.1V. +0.1E.1o. +0.1e.1Q. +0.1k.1I. +0.1L.1h. +0.1m.1G. +0.1n.1F. +0.1O.1g. +0.1P.1f. +0.1q.1D. +0.1r.1C. +0.1t.1A. +0.1y.1v. +0.N.24. +0.O.23. +0.P.22. +0.Q.21. +0.R.20. +0.r.2d. +0.U.1Z. +0.u.2c. +0.y.29. +0.Z.1Y. +3.1l.1H. +3.1u.1z. +3.M.27. +3.S.1/. +4.18.1W. +4.1B.1s. +4.1c.1S. +4.1K.1i. +4.1x.1w. +5.x.2a. +c.28.K. +c.T.1+. +0.17.1X. +0.19.1W. +0.1a.1V. +0.1B.1t. +0.1d.1R. +0.1j.1K. +0.1k.1J. +0.1L.1i. +0.1m.1H. +0.1n.1G. +0.1o.1F. +0.1P.1g. +0.1r.1D. +0.1s.1C. +0.1v.1z. +0.1x.1x. +0.29.z. +0.N.25. +0.O.24. +0.P.23. +0.Q.22. +0.R.21. +0.s.2d. +0.U.1+. +0.V.1Z. +0.v.2c. +3.1l.1I. +3.1u.1A. +3.S.20. +4.1E.1p. +4.1f.1Q. +4.1M.1h. +4.1y.1w. +4.r.2e. +5.y.2a. +c.28.L. +c.T.1/. +0.18.1X. +0.1d.1S. +0.1j.1L. +0.1m.1I. +0.1n.1H. +0.1o.1G. +0.1p.1F. +0.1s.1D. +0.1v.1A. +0.1w.1z. +0.1y.1x. +0.N.26. +0.O.25. +0.P.24. +0.Q.23. +0.R.22. +0.t.2d. +0.U.1/. +0.V.1+. +0.W.1Z. +0.w.2c. +3.1l.1J. +3.1u.1B. +3.M.28. +3.S.21. +4.1a.1W. +4.1b.1V. +4.1E.1q. +4.1M.1i. +4.1N.1h. +4.1t.1C. +4.s.2e. +5.2a.z. +c.T.20. +f.1Q.1g. +0.19.1X. +0.1B.1v. +0.1j.1M. +0.1m.1J. +0.1n.1I. +0.1O.1h. +0.1o.1H. +0.1p.1G. +0.1q.1F. +0.1t.1D. +0.1w.1A. +0.1x.1z. +0.1y.1y. +0.N.27. +0.O.26. +0.P.25. +0.Q.24. +0.R.23. +0.U.20. +0.u.2d. +0.V.1/. +0.W.1+. +0.X.1Z. +0.x.2c. +3.1u.1C. +4.1b.1W. +4.1c.1V. +4.1E.1r. +4.1N.1i. +4.S.22. +c.T.21. +0.+.1Y. +0.1a.1X. +0.1B.1w. +0.1E.1s. +0.1j.1N. +0.1k.1K. +0.1n.1J. +0.1O.1i. +0.1o.1I. +0.1p.1H. +0.1P.1h. +0.1q.1G. +0.1r.1F. +0.1v.1C. +0.1x.1A. +0.1y.1z. +0.A.29. +0.O.27. +0.P.26. +0.Q.25. +0.R.24. +0.U.21. +0.V.20. +0.v.2d. +0.W.1/. +0.X.1+. +0.y.2c. +3.1u.1D. +4.1c.1W. +4.r.2f. +4.S.23. +4.u.2e. +c.T.22. +c.Y.1Z. +f.1R.1e. +0./.1Y. +0.1b.1X. +0.1B.1x. +0.1d.1V. +0.1e.1S. +0.1E.1t. +0.1j.1O. +0.1k.1L. +0.1o.1J. +0.1p.1I. +0.1P.1i. +0.1q.1H. +0.1r.1G. +0.1s.1F. +0.1v.1D. +0.1w.1C. +0.1y.1A. +0.1z.1z. +0.2c.z. +0.B.29. +0.P.27. +0.Q.26. +0.R.25. +0.U.22. +0.V.21. +0.W.20. +0.w.2d. +0.X.1/. +0.Z.1Z. +3.1l.1K. +4.1h.1Q. +4.S.24. +4.s.2f. +5.A.2a. +c.28.N. +c.T.23. +c.Y.1+. +f.1R.1f. +0.10.1Y. +0.1A.1z. +0.1B.1y. +0.1c.1X. +0.1d.1W. +0.1j.1P. +0.1k.1M. +0.1m.1K. +0.1p.1J. +0.1q.1I. +0.1R.1g. +0.1r.1H. +0.1s.1G. +0.1t.1F. +0.1w.1D. +0.1x.1C. +0.C.29. +0.Q.27. +0.R.26. +0.U.23. +0.V.22. +0.W.21. +0.X.20. +0.x.2d. +0.Z.1+. +3.1l.1L. +3.1u.1E. +4.1i.1Q. +4.1S.1f. +5.B.2a. +c.28.O. +c.T.24. +c.Y.1/. +e.S.25. +0.11.1Y. +0.1A.1A. +0.1B.1z. +0.1E.1v. +0.1j.1Q. +0.1k.1N. +0.1m.1L. +0.1n.1K. +0.1q.1J. +0.1r.1I. +0.1S.1g. +0.1s.1H. +0.1x.1D. +0.1y.1C. +0.D.29. +0.R.27. +0.U.24. +0.V.23. +0.W.22. +0.X.21. +0.y.2d. +0.Z.1/. +3.1l.1M. +3.1u.1F. +3.S.26. +4.1t.1G. +4.u.2f. +5.C.2a. +c.28.P. +c.T.25. +c.Y.20. +0.1B.1A. +0.1d.1X. +0.1e.1V. +0.1k.1O. +0.1m.1M. +0.1n.1L. +0.1o.1K. +0.1r.1J. +0.1s.1I. +0.1t.1H. +0.1v.1F. +0.1y.1D. +0.1z.1C. +0.A.2c. +0.E.29. +0.U.25. +0.V.24. +0.W.23. +0.X.22. +0.Z.20. +3.1l.1N. +3.1u.1G. +4.12.1Y. +4.1E.1w. +4.2d.z. +5.D.2a. +c.28.Q. +c.T.26. +c.Y.21. +e.S.27. +0.+.1Z. +0.1A.1C. +0.1B.1B. +0.1e.1W. +0.1E.1x. +0.1f.1V. +0.1k.1P. +0.1m.1N. +0.1n.1M. +0.1o.1L. +0.1p.1K. +0.1R.1h. +0.1s.1J. +0.1t.1I. +0.1v.1G. +0.1w.1F. +0.1z.1D. +0.B.2c. +0.F.29. +0.U.26. +0.V.25. +0.W.24. +0.X.23. +0.Z.21. +3.1l.1O. +3.1u.1H. +5.E.2a. +c.28.R. +c.T.27. +c.Y.22. +0./.1Z. +0.+.1+. +0.1A.1D. +0.1B.1C. +0.1E.1y. +0.1m.1O. +0.1n.1N. +0.1o.1M. +0.1p.1L. +0.1q.1K. +0.1t.1J. +0.1v.1H. +0.1w.1G. +0.1x.1F. +0.C.2c. +0.U.27. +0.V.26. +0.W.25. +0.X.24. +0.Z.22. +3.1l.1P. +3.1u.1I. +3.S.28. +4.1f.1W. +4.1k.1Q. +4.1S.1h. +5.F.2a. +c.Y.23. +f.1R.1i. +f.1V.1g. +0./.1+. +0.+.1/. +0.10.1Z. +0.13.1Y. +0.1B.1D. +0.1C.1C. +0.1e.1X. +0.1E.1z. +0.1j.1R. +0.1m.1P. +0.1n.1O. +0.1o.1N. +0.1p.1M. +0.1q.1L. +0.1r.1K. +0.1v.1I. +0.1W.1g. +0.1w.1H. +0.1x.1G. +0.1y.1F. +0.A.2d. +0.D.2c. +0.V.27. +0.W.26. +0.X.25. +0.Z.23. +3.1l.1Q. +3.1u.1J. +4.1S.1i. +c.28.T. +c.Y.24. +0./.1/. +0.+.20. +0.10.1+. +0.11.1Z. +0.14.1Y. +0.1D.1C. +0.1E.1A. +0.1f.1X. +0.1j.1S. +0.1n.1P. +0.1o.1O. +0.1p.1N. +0.1q.1M. +0.1r.1L. +0.1s.1K. +0.1v.1J. +0.1w.1I. +0.1x.1H. +0.1y.1G. +0.1z.1F. +0.B.2d. +0.E.2c. +0.W.27. +0.X.26. +0.Z.24. +4.1m.1Q. +4.G.29. +c.28.U. +c.Y.25. +0./.20. +0.+.21. +0.10.1/. +0.11.1+. +0.12.1Z. +0.15.1Y. +0.1A.1F. +0.1D.1D. +0.1o.1P. +0.1p.1O. +0.1q.1N. +0.1r.1M. +0.1s.1L. +0.1t.1K. +0.1w.1J. +0.1X.1g. +0.1x.1I. +0.1y.1H. +0.1z.1G. +0.C.2d. +0.F.2c. +0.H.29. +0.X.27. +0.Z.25. +4.1B.1E. +4.1h.1V. +4.1n.1Q. +5.G.2a. +c.28.V. +c.Y.26. +0./.21. +0.+.22. +0.10.20. +0.11.1/. +0.12.1+. +0.16.1Y. +0.1A.1G. +0.1B.1F. +0.1E.1C. +0.1k.1R. +0.1o.1Q. +0.1p.1P. +0.1q.1O. +0.1r.1N. +0.1s.1M. +0.1t.1L. +0.1x.1J. +0.1y.1I. +0.1z.1H. +0.D.2d. +0.Z.26. +3.1u.1K. +3.I.29. +4.1h.1W. +4.1i.1V. +5.H.2a. +c.28.W. +c.Y.27. +0./.22. +0.+.23. +0.10.21. +0.11.20. +0.17.1Y. +0.1A.1H. +0.1B.1G. +0.1E.1D. +0.1F.1C. +0.1j.1V. +0.1k.1S. +0.1q.1P. +0.1r.1O. +0.1s.1N. +0.1t.1M. +0.1v.1K. +0.1y.1J. +0.1z.1I. +0.E.2d. +0.Z.27. +3.1l.1R. +3.1u.1L. +3.I.2a. +3.J.29. +4.12.1/. +4.1i.1W. +4.1p.1Q. +c.28.X. +0./.23. +0.+.24. +0.10.22. +0.11.21. +0.13.1Z. +0.18.1Y. +0.1A.1I. +0.1B.1H. +0.1F.1D. +0.1G.1C. +0.1h.1X. +0.1j.1W. +0.1m.1R. +0.1r.1P. +0.1s.1O. +0.1v.1L. +0.1w.1K. +0.1z.1J. +0.F.2d. +0.G.2c. +0.K.29. +3.1l.1S. +3.1u.1M. +4.12.20. +4.1q.1Q. +4.1t.1N. +5.J.2a. +c.28.Y. +0./.24. +0.+.25. +0.10.23. +0.11.22. +0.12.21. +0.13.1+. +0.14.1Z. +0.19.1Y. +0.1A.1J. +0.1B.1I. +0.1G.1D. +0.1H.1C. +0.1m.1S. +0.1n.1R. +0.1s.1P. +0.1t.1O. +0.1v.1M. +0.1w.1L. +0.1x.1K. +0.H.2c. +0.L.29. +3.1u.1N. +4.1i.1X. +4.1r.1Q. +5.K.2a. +c.28.Z. +e.1E.1E. +0./.25. +0.+.26. +0.10.24. +0.11.23. +0.13.1/. +0.14.1+. +0.15.1Z. +0.1a.1Y. +0.1B.1J. +0.1D.1H. +0.1E.1F. +0.1I.1C. +0.1j.1X. +0.1k.1V. +0.1n.1S. +0.1o.1R. +0.1t.1P. +0.1v.1N. +0.1w.1M. +0.1x.1L. +0.1y.1K. +0.L.2a. +3.1u.1O. +3.I.2c. +3.M.29. +4.12.22. +4.1s.1Q. +0./.26. +0.+.27. +0.10.25. +0.11.24. +0.13.20. +0.14.1/. +0.15.1+. +0.16.1Z. +0.1b.1Y. +0.1C.1J. +0.1D.1I. +0.1E.1G. +0.1F.1F. +0.1o.1S. +0.1p.1R. +0.1v.1O. +0.1w.1N. +0.1x.1M. +0.1y.1L. +0.1z.1K. +0.G.2d. +3.1l.1V. +3.1u.1P. +3.J.2c. +3.M.2a. +4.12.23. +4.1k.1W. +4.1t.1Q. +0./.27. +0.10.26. +0.11.25. +0.12.24. +0.13.21. +0.14.20. +0.15.1/. +0.16.1+. +0.17.1Z. +0.1A.1K. +0.1c.1Y. +0.1D.1J. +0.1E.1H. +0.1G.1F. +0.1m.1V. +0.1p.1S. +0.1q.1R. +0.1v.1P. +0.1w.1O. +0.1x.1N. +0.1y.1M. +0.1z.1L. +0.H.2d. +0.K.2c. +3.1l.1W. +3.1u.1Q. +0.10.27. +0.11.26. +0.12.25. +0.13.22. +0.14.21. +0.15.20. +0.16.1/. +0.17.1+. +0.18.1Z. +0.1A.1L. +0.1B.1K. +0.1E.1I. +0.1F.1H. +0.1G.1G. +0.1k.1X. +0.1n.1V. +0.1q.1S. +0.1r.1R. +0.1w.1P. +0.1x.1O. +0.1y.1N. +0.1z.1M. +0.L.2c. +0.N.29. +3.I.2d. +4.1m.1W. +4.1v.1Q. +c.28.+. +0.11.27. +0.12.26. +0.13.23. +0.14.22. +0.15.21. +0.16.20. +0.17.1/. +0.18.1+. +0.19.1Z. +0.1A.1M. +0.1B.1L. +0.1C.1K. +0.1d.1Y. +0.1E.1J. +0.1F.1I. +0.1G.1H. +0.1n.1W. +0.1o.1V. +0.1s.1R. +0.1x.1P. +0.1y.1O. +0.1z.1N. +0.O.29. +3.1l.1X. +3.J.2d. +3.M.2c. +4.1r.1S. +4.1w.1Q. +5.N.2a. +c.28./. +0.12.27. +0.13.24. +0.14.23. +0.15.22. +0.16.21. +0.17.20. +0.18.1/. +0.19.1+. +0.1A.1N. +0.1a.1Z. +0.1B.1M. +0.1C.1L. +0.1D.1K. +0.1F.1J. +0.1G.1I. +0.1H.1H. +0.1m.1X. +0.1o.1W. +0.1p.1V. +0.1s.1S. +0.1t.1R. +0.1x.1Q. +0.1y.1P. +0.1z.1O. +0.K.2d. +0.P.29. +5.O.2a. +c.28.10. +0.13.25. +0.14.24. +0.15.23. +0.16.22. +0.17.21. +0.18.20. +0.19.1/. +0.1a.1+. +0.1A.1O. +0.1B.1N. +0.1b.1Z. +0.1C.1M. +0.1D.1L. +0.1G.1J. +0.1H.1I. +0.1n.1X. +0.1p.1W. +0.1q.1V. +0.1y.1Q. +0.1z.1P. +0.L.2d. +0.Q.29. +3.1u.1R. +4.1t.1S. +5.P.2a. +c.28.11. +0.13.26. +0.14.25. +0.15.24. +0.16.23. +0.17.22. +0.18.21. +0.19.20. +0.1a.1/. +0.1A.1P. +0.1b.1+. +0.1B.1O. +0.1C.1N. +0.1c.1Z. +0.1D.1M. +0.1E.1K. +0.1e.1Y. +0.1H.1J. +0.1o.1X. +0.1r.1V. +0.1v.1R. +0.N.2c. +0.R.29. +3.1u.1S. +3.M.2d. +4.1I.1I. +4.1q.1W. +4.1z.1Q. +5.Q.2a. +c.28.12. +0.13.27. +0.14.26. +0.15.25. +0.16.24. +0.17.23. +0.18.22. +0.19.21. +0.1a.20. +0.1b.1/. +0.1B.1P. +0.1c.1+. +0.1C.1O. +0.1D.1N. +0.1E.1L. +0.1F.1K. +0.1f.1Y. +0.1I.1J. +0.1p.1X. +0.1s.1V. +0.1v.1S. +0.1w.1R. +0.O.2c. +4.1A.1Q. +4.1r.1W. +4.S.29. +5.R.2a. +0.14.27. +0.15.26. +0.16.25. +0.17.24. +0.18.23. +0.19.22. +0.1a.21. +0.1B.1Q. +0.1b.20. +0.1c.1/. +0.1C.1P. +0.1D.1O. +0.1d.1Z. +0.1E.1M. +0.1F.1L. +0.1G.1K. +0.1J.1J. +0.1q.1X. +0.1w.1S. +0.1x.1R. +0.P.2c. +3.S.2a. +4.1s.1W. +4.1t.1V. +c.T.29. +h.1g.1Y. +0.15.27. +0.16.26. +0.17.25. +0.18.24. +0.19.23. +0.1a.22. +0.1b.21. +0.1C.1Q. +0.1c.20. +0.1d.1+. +0.1D.1P. +0.1E.1N. +0.1F.1M. +0.1G.1L. +0.1H.1K. +0.1r.1X. +0.1x.1S. +0.1y.1R. +0.N.2d. +0.Q.2c. +0.U.29. +3.1u.1V. +4.1t.1W. +c.28.13. +c.T.2a. +0.16.27. +0.17.26. +0.18.25. +0.19.24. +0.1a.23. +0.1b.22. +0.1c.21. +0.1d.1/. +0.1E.1O. +0.1F.1N. +0.1G.1M. +0.1H.1L. +0.1I.1K. +0.1s.1X. +0.1v.1V. +0.1y.1S. +0.1z.1R. +0.O.2d. +0.R.2c. +0.V.29. +3.1u.1W. +4.1D.1Q. +5.U.2a. +c.28.14. +0.17.27. +0.18.26. +0.19.25. +0.1A.1R. +0.1a.24. +0.1b.23. +0.1c.22. +0.1d.20. +0.1E.1P. +0.1e.1Z. +0.1F.1O. +0.1G.1N. +0.1H.1M. +0.1h.1Y. +0.1I.1L. +0.1K.1J. +0.1t.1X. +0.1v.1W. +0.1w.1V. +0.P.2d. +0.W.29. +4.1z.1S. +4.S.2c. +5.V.2a. +c.28.15. +0.18.27. +0.19.26. +0.1a.25. +0.1B.1R. +0.1b.24. +0.1c.23. +0.1d.21. +0.1e.1+. +0.1F.1P. +0.1f.1Z. +0.1G.1O. +0.1H.1N. +0.1I.1M. +0.1i.1Y. +0.1L.1J. +0.1x.1V. +0.Q.2d. +0.X.29. +3.1u.1X. +4.1A.1S. +4.1E.1Q. +4.1w.1W. +5.W.2a. +c.28.16. +c.T.2c. +0.19.27. +0.1a.26. +0.1B.1S. +0.1b.25. +0.1C.1R. +0.1c.24. +0.1d.22. +0.1e.1/. +0.1f.1+. +0.1F.1Q. +0.1G.1P. +0.1H.1O. +0.1I.1N. +0.1j.1Y. +0.1M.1J. +0.1v.1X. +0.1x.1W. +0.1y.1V. +0.1Z.1g. +0.R.2d. +0.U.2c. +5.X.2a. +c.28.17. +c.Y.29. +0.1+.1g. +0.1a.27. +0.1b.26. +0.1C.1S. +0.1c.25. +0.1D.1R. +0.1d.23. +0.1e.20. +0.1f.1/. +0.1G.1Q. +0.1H.1P. +0.1I.1O. +0.1K.1K. +0.1w.1X. +0.1y.1W. +0.1z.1V. +0.V.2c. +0.Z.29. +3.S.2d. +4.1N.1J. +c.28.18. +c.Y.2a. +0.1/.1g. +0.1A.1V. +0.1b.27. +0.1c.26. +0.1d.24. +0.1e.21. +0.1f.20. +0.1I.1P. +0.1K.1L. +0.1O.1J. +0.1x.1X. +0.W.2c. +4.1D.1S. +4.1H.1Q. +4.1z.1W. +5.Z.2a. +c.28.19. +c.T.2d. +0.1c.27. +0.1d.25. +0.1E.1R. +0.1e.22. +0.1f.21. +0.1h.1Z. +0.1K.1M. +0.1k.1Y. +0.1L.1L. +0.1P.1J. +0.1y.1X. +0.U.2d. +0.X.2c. +4.1A.1W. +4.1B.1V. +4.1I.1Q. +c.28.1a. +f.20.1g. +0.1B.1W. +0.1C.1V. +0.1d.26. +0.1E.1S. +0.1e.23. +0.1F.1R. +0.1f.22. +0.1h.1+. +0.1i.1Z. +0.1K.1N. +0.1M.1L. +0.1z.1X. +0.V.2d. +3.1l.1Y. +4.1J.1Q. +c.28.1b. +c.Y.2c. +e.21.1g. +0.+.29. +0.1A.1X. +0.1C.1W. +0.1d.27. +0.1e.24. +0.1F.1S. +0.1f.23. +0.1G.1R. +0.1h.1/. +0.1i.1+. +0.1j.1Z. +0.1K.1O. +0.1L.1N. +0.1m.1Y. +0.22.1g. +0.W.2d. +0.Z.2c. +4.1D.1V. +4.1M.1M. +c.28.1c. +0./.29. +0.1B.1X. +0.1e.25. +0.1f.24. +0.1G.1S. +0.1H.1R. +0.1h.20. +0.1i.1/. +0.1j.1+. +0.1K.1P. +0.1L.1O. +0.1n.1Y. +0.X.2d. +4.1D.1W. +4.1M.1N. +5.+.2a. +f.23.1g. +0.10.29. +0.1C.1X. +0.1e.26. +0.1f.25. +0.1h.21. +0.1I.1R. +0.1i.20. +0.1j.1/. +0.1L.1P. +0.1M.1O. +0.1N.1N. +0.1o.1Y. +0.24.1g. +4.1E.1V. +4.1H.1S. +4.1K.1Q. +5./.2a. +c.28.1d. +c.Y.2d. +0.11.29. +0.1D.1X. +0.1e.27. +0.1F.1V. +0.1f.26. +0.1h.22. +0.1I.1S. +0.1i.21. +0.1J.1R. +0.1j.20. +0.1k.1Z. +0.1L.1Q. +0.1M.1P. +0.1p.1Y. +0.25.1g. +0.Z.2d. +4.1E.1W. +4.1N.1O. +5.10.2a. +0.+.2c. +0.12.29. +0.1F.1W. +0.1f.27. +0.1G.1V. +0.1h.23. +0.1i.22. +0.1j.21. +0.1k.1+. +0.1N.1P. +0.1O.1O. +0.1q.1Y. +0.26.1g. +3.1l.1Z. +4.1J.1S. +4.1M.1Q. +5./.2b. +5.11.2a. +0./.2c. +0.1E.1X. +0.1G.1W. +0.1H.1V. +0.1h.24. +0.1i.23. +0.1j.22. +0.1k.1/. +0.1m.1Z. +0.1P.1O. +0.1r.1Y. +0.27.1g. +3.1l.1+. +4.1N.1Q. +5.12.2a. +c.28.1e. +0.10.2c. +0.1F.1X. +0.1H.1W. +0.1h.25. +0.1i.24. +0.1j.23. +0.1K.1R. +0.1k.20. +0.1m.1+. +0.1n.1Z. +0.1O.1Q. +0.1P.1P. +0.1s.1Y. +3.1l.1/. +4.1I.1V. +c.28.1f. +0.+.2d. +0.11.2c. +0.13.29. +0.1G.1X. +0.1h.26. +0.1i.25. +0.1J.1V. +0.1j.24. +0.1K.1S. +0.1k.21. +0.1L.1R. +0.1m.1/. +0.1n.1+. +0.1o.1Z. +0.1P.1Q. +0.1t.1Y. +3.1l.20. +4.1I.1W. +c.28.1g. +0./.2d. +0.14.29. +0.1H.1X. +0.1h.27. +0.1i.26. +0.1j.25. +0.1k.22. +0.1L.1S. +0.1M.1R. +0.1m.20. +0.1n.1/. +0.1o.1+. +0.1p.1Z. +0.1Q.1Q. +3.1l.21. +3.1u.1Y. +4.12.2c. +4.1J.1W. +5.13.2a. +0.10.2d. +0.15.29. +0.1I.1X. +0.1i.27. +0.1j.26. +0.1k.23. +0.1M.1S. +0.1m.21. +0.1N.1R. +0.1n.20. +0.1o.1/. +0.1p.1+. +0.1q.1Z. +0.1v.1Y. +3.1l.22. +5./.2e. +5.14.2a. +e.0.2m. +0.11.2d. +0.16.29. +0.1J.1X. +0.1j.27. +0.1k.24. +0.1m.22. +0.1n.21. +0.1O.1R. +0.1o.20. +0.1p.1/. +0.1q.1+. +0.1r.1Z. +0.1w.1Y. +3.1l.23. +4.1K.1V. +4.1N.1S. +5.15.2a. +c.28.1h. +e.0.2n. +e.1.2m. +0.13.2c. +0.17.29. +0.1k.25. +0.1L.1V. +0.1m.23. +0.1n.22. +0.1O.1S. +0.1o.21. +0.1P.1R. +0.1p.20. +0.1q.1/. +0.1r.1+. +0.1s.1Z. +0.1x.1Y. +3.1l.24. +4.12.2d. +4.1K.1W. +5.16.2a. +c.28.1i. +e.1.2n. +e.2.2m. +0.14.2c. +0.18.29. +0.1k.26. +0.1L.1W. +0.1M.1V. +0.1m.24. +0.1n.23. +0.1o.22. +0.1P.1S. +0.1p.21. +0.1q.20. +0.1r.1/. +0.1R.1Q. +0.1s.1+. +0.1t.1Z. +0.1y.1Y. +2.0.2o. +3.1l.25. +5./.2f. +5.17.2a. +c.28.1j. +e.2.2n. +e.3.2m. +0.15.2c. +0.19.29. +0.1K.1X. +0.1k.27. +0.1m.25. +0.1n.24. +0.1o.23. +0.1p.22. +0.1q.21. +0.1r.20. +0.1s.1/. +0.1S.1Q. +0.1t.1+. +0.1z.1Y. +2.1.2o. +3.1l.26. +3.1u.1Z. +4.1M.1W. +4.1N.1V. +5.18.2a. +7.2p.0. +e.3.2n. +e.4.2m. +0.13.2d. +0.16.2c. +0.1A.1Y. +0.1a.29. +0.1L.1X. +0.1m.26. +0.1n.25. +0.1O.1V. +0.1o.24. +0.1p.23. +0.1q.22. +0.1r.21. +0.1s.20. +0.1t.1/. +0.1v.1Z. +2.2.2o. +3.1l.27. +3.1u.1+. +4.1N.1W. +5.19.2a. +7.2p.1. +e.4.2n. +e.5.2m. +0.14.2d. +0.17.2c. +0.1B.1Y. +0.1b.29. +0.1M.1X. +0.1m.27. +0.1n.26. +0.1O.1W. +0.1o.25. +0.1P.1V. +0.1p.24. +0.1q.23. +0.1r.22. +0.1s.21. +0.1t.20. +0.1v.1+. +0.1w.1Z. +2.3.2o. +3.1u.1/. +5.1a.2a. +7.2p.2. +c.28.1k. +e.5.2n. +e.6.2m. +0.15.2d. +0.18.2c. +0.1C.1Y. +0.1c.29. +0.1n.27. +0.1o.26. +0.1P.1W. +0.1p.25. +0.1q.24. +0.1R.1R. +0.1r.23. +0.1s.22. +0.1t.21. +0.1v.1/. +0.1w.1+. +0.1x.1Z. +2.4.2o. +3.1l.28. +3.1u.20. +4.1N.1X. +5.1b.2a. +5.c.2h. +7.2p.3. +e.6.2n. +e.7.2m. +f.1Q.1V. +0.16.2d. +0.19.2c. +0.1D.1Y. +0.1O.1X. +0.1o.27. +0.1p.26. +0.1q.25. +0.1R.1S. +0.1r.24. +0.1s.23. +0.1t.22. +0.1v.20. +0.1w.1/. +0.1x.1+. +0.1y.1Z. +2.5.2o. +3.1u.21. +5.1c.2a. +5.c.2i. +c.28.1m. +e.4.2p. +e.7.2n. +e.8.2m. +f.1Q.1W. +0.17.2d. +0.1a.2c. +0.1d.29. +0.1P.1X. +0.1p.27. +0.1q.26. +0.1r.25. +0.1S.1S. +0.1s.24. +0.1t.23. +0.1v.21. +0.1w.20. +0.1x.1/. +0.1y.1+. +0.1z.1Z. +2.6.2o. +3.1u.22. +5.c.2j. +7.2p.5. +c.28.1n. +e.8.2n. +e.9.2m. +0.18.2d. +0.1A.1Z. +0.1b.2c. +0.1E.1Y. +0.1Q.1X. +0.1q.27. +0.1r.26. +0.1s.25. +0.1t.24. +0.1v.22. +0.1w.21. +0.1x.20. +0.1y.1/. +0.1z.1+. +2.7.2o. +3.1u.23. +5.1d.2a. +5.c.2k. +7.2p.6. +c.28.1o. +e.9.2n. +e.a.2m. +0.19.2d. +0.1A.1+. +0.1B.1Z. +0.1c.2c. +0.1F.1Y. +0.1R.1V. +0.1r.27. +0.1s.26. +0.1t.25. +0.1v.23. +0.1w.22. +0.1x.21. +0.1y.20. +0.1z.1/. +2.8.2o. +3.1u.24. +5.c.2l. +c.28.1p. +e.7.2p. +e.a.2n. +e.b.2m. +0.1A.1/. +0.1a.2d. +0.1C.1Z. +0.1e.29. +0.1G.1Y. +0.1R.1W. +0.1S.1V. +0.1s.27. +0.1t.26. +0.1v.24. +0.1w.23. +0.1x.22. +0.1y.21. +0.1z.20. +2.9.2o. +3.1u.25. +4.1B.1+. +7.2p.8. +c.28.1q. +e.b.2n. +0.1A.20. +0.1B.1/. +0.1b.2d. +0.1C.1+. +0.1D.1Z. +0.1d.2c. +0.1e.2a. +0.1f.29. +0.1H.1Y. +0.1S.1W. +0.1t.27. +0.1v.25. +0.1w.24. +0.1x.23. +0.1y.22. +0.1z.21. +2.a.2o. +3.1u.26. +c.28.1r. +e.9.2p. +i.c.2n. +0.1A.21. +0.1B.20. +0.1C.1/. +0.1c.2d. +0.1D.1+. +0.1I.1Y. +0.1R.1X. +0.1v.26. +0.1w.25. +0.1x.24. +0.1y.23. +0.1z.22. +0.29.1g. +2.b.2o. +3.1u.27. +5.1f.2a. +7.2p.a. +c.28.1s. +0.1A.22. +0.1B.21. +0.1C.20. +0.1D.1/. +0.1E.1Z. +0.1J.1Y. +0.1S.1X. +0.1v.27. +0.1w.26. +0.1x.25. +0.1y.24. +0.1z.23. +5.2a.1g. +7.2p.b. +c.28.1t. +f.1V.1V. +0.1A.23. +0.1B.22. +0.1C.21. +0.1D.20. +0.1d.2d. +0.1E.1+. +0.1e.2c. +0.1F.1Z. +0.1V.1W. +0.1w.27. +0.1x.26. +0.1y.25. +0.1z.24. +3.1u.28. +7.2p.c. +0.1A.24. +0.1C.22. +0.1D.21. +0.1E.1/. +0.1F.1+. +0.1f.2c. +0.1G.1Z. +0.1h.29. +0.1x.27. +0.1y.26. +0.1z.25. +0.d.2n. +4.1B.23. +c.28.1v. +f.1W.1W. +0.1A.25. +0.1B.24. +0.1C.23. +0.1D.22. +0.1E.20. +0.1F.1/. +0.1G.1+. +0.1H.1Z. +0.1K.1Y. +0.1X.1V. +0.1y.27. +0.1z.26. +0.2c.1g. +0.e.2n. +4.1i.29. +5.1h.2a. +5.k.2h. +c.28.1w. +0.1A.26. +0.1B.25. +0.1C.24. +0.1D.23. +0.1E.21. +0.1e.2d. +0.1F.20. +0.1G.1/. +0.1H.1+. +0.1I.1Z. +0.1i.2a. +0.1j.29. +0.1L.1Y. +0.1X.1W. +0.1z.27. +0.f.2n. +5.k.2i. +c.28.1x. +0.1A.27. +0.1B.26. +0.1C.25. +0.1D.24. +0.1E.22. +0.1F.21. +0.1f.2d. +0.1G.20. +0.1H.1/. +0.1I.1+. +0.1J.1Z. +0.1j.2a. +0.1M.1Y. +0.g.2n. +5.1h.2b. +5.k.2j. +7.2p.d. +c.28.1y. +0.1B.27. +0.1C.26. +0.1D.25. +0.1E.23. +0.1F.22. +0.1G.21. +0.1H.20. +0.1h.2c. +0.1I.1/. +0.1J.1+. +0.1N.1Y. +0.1X.1X. +0.h.2n. +4.2d.1g. +5.k.2k. +7.2p.e. +c.28.1z. +0.1C.27. +0.1D.26. +0.1E.24. +0.1F.23. +0.1G.22. +0.1H.21. +0.1I.20. +0.1i.2c. +0.1J.1/. +0.1k.29. +0.1O.1Y. +0.i.2n. +5.k.2l. +7.2p.f. +c.28.1A. +0.1D.27. +0.1E.25. +0.1F.24. +0.1G.23. +0.1H.22. +0.1I.21. +0.1J.20. +0.1j.2c. +0.1K.1Z. +0.1k.2a. +0.1P.1Y. +0.j.2n. +3.1l.29. +7.2p.g. +c.28.1B. +0.1E.26. +0.1F.25. +0.1G.24. +0.1H.23. +0.1h.2d. +0.1I.22. +0.1J.21. +0.1K.1+. +0.1L.1Z. +0.1m.29. +0.1Q.1Y. +0.k.2n. +3.1l.2a. +7.2p.h. +c.28.1C. +0.1E.27. +0.1F.26. +0.1G.25. +0.1H.24. +0.1I.23. +0.1J.22. +0.1K.1/. +0.1L.1+. +0.1M.1Z. +0.1n.29. +0.l.2n. +4.1i.2d. +5.1h.2e. +5.1m.2a. +7.2p.i. +c.28.1D. +0.1F.27. +0.1G.26. +0.1H.25. +0.1I.24. +0.1J.23. +0.1j.2d. +0.1K.20. +0.1k.2c. +0.1L.1/. +0.1M.1+. +0.1n.2a. +0.1o.29. +0.m.2n. +4.1N.1Z. +7.2p.j. +j.n.2m. +0.1G.27. +0.1H.26. +0.1I.25. +0.1J.24. +0.1K.21. +0.1L.20. +0.1M.1/. +0.1N.1+. +0.1O.1Z. +0.1o.2a. +0.1p.29. +3.1l.2c. +3.n.2n. +7.2p.k. +7.r.2h. +c.28.1E. +0.1H.27. +0.1I.26. +0.1J.25. +0.1K.22. +0.1L.21. +0.1M.20. +0.1m.2c. +0.1O.1+. +0.1P.1Z. +0.1q.29. +0.o.2n. +4.1N.1/. +5.1h.2f. +5.1p.2a. +7.2p.l. +7.r.2i. +7.s.2h. +c.28.1F. +f.1R.1Y. +0.1I.27. +0.1J.26. +0.1K.23. +0.1k.2d. +0.1L.22. +0.1M.21. +0.1n.2c. +0.1O.1/. +0.1P.1+. +0.1Q.1Z. +0.1r.29. +0.1S.1Y. +0.p.2n. +4.1N.20. +4.r.2j. +4.s.2i. +5.1q.2a. +7.2p.m. +c.28.1G. +0.1J.27. +0.1K.24. +0.1L.23. +0.1M.22. +0.1O.20. +0.1o.2c. +0.1P.1/. +0.1Q.1+. +0.1s.29. +0.q.2n. +3.1l.2d. +4.1N.21. +4.s.2j. +5.1r.2a. +7.2p.n. +7.r.2k. +7.u.2h. +c.28.1H. +0.1K.25. +0.1L.24. +0.1M.23. +0.1m.2d. +0.1O.21. +0.1P.20. +0.1p.2c. +0.1Q.1/. +4.1N.22. +4.1t.29. +4.r.2l. +5.1s.2a. +7.2p.o. +7.s.2k. +7.u.2i. +c.28.1I. +0.1K.26. +0.1L.25. +0.1M.24. +0.1n.2d. +0.1O.22. +0.1P.21. +0.1Q.20. +0.1q.2c. +0.1t.2a. +0.1V.1Y. +3.1u.29. +4.1N.23. +4.s.2l. +7.2p.p. +7.u.2j. +c.28.1J. +0.1K.27. +0.1L.26. +0.1M.25. +0.1O.23. +0.1o.2d. +0.1P.22. +0.1Q.21. +0.1r.2c. +0.1v.29. +0.1W.1Y. +0.r.2n. +3.1u.2a. +4.1N.24. +7.2p.q. +7.u.2k. +f.1R.1Z. +0.1L.27. +0.1M.26. +0.1N.25. +0.1O.24. +0.1P.23. +0.1p.2d. +0.1R.1+. +0.1S.1Z. +0.1s.2c. +0.1w.29. +0.s.2n. +5.1t.2b. +5.1v.2a. +7.u.2l. +f.1Q.22. +0.1M.27. +0.1O.25. +0.1P.24. +0.1Q.23. +0.1q.2d. +0.1R.1/. +0.1S.1+. +0.1t.2c. +0.1X.1Y. +0.1x.29. +0.t.2n. +4.1N.26. +5.1w.2a. +c.28.1K. +0.1O.26. +0.1P.25. +0.1Q.24. +0.1r.2d. +0.1S.1/. +0.1y.29. +0.u.2n. +3.1u.2c. +4.1N.27. +5.1x.2a. +7.2p.r. +c.28.1L. +f.1R.20. +0.1O.27. +0.1P.26. +0.1Q.25. +0.1S.20. +0.1s.2d. +0.1V.1Z. +0.1v.2c. +0.1z.29. +0.v.2n. +5.1y.2a. +7.2p.s. +c.28.1M. +f.1R.21. +0.1A.29. +0.1P.27. +0.1Q.26. +0.1S.21. +0.1W.1Z. +0.1w.2c. +0.w.2n. +4.1t.2d. +5.1z.2a. +7.2p.t. +c.28.1N. +f.1R.22. +f.1V.1+. +0.1B.29. +0.1Q.27. +0.1R.23. +0.1S.22. +0.1V.1/. +0.1W.1+. +0.1x.2c. +0.x.2n. +3.1u.2d. +5.1A.2a. +5.1t.2e. +7.2p.u. +c.28.1O. +0.1C.29. +0.1S.23. +0.1v.2d. +0.1W.1/. +0.1X.1Z. +0.1y.2c. +0.y.2n. +5.1B.2a. +7.2p.v. +c.28.1P. +f.1R.24. +f.1V.20. +0.1D.29. +0.1R.25. +0.1S.24. +0.1V.21. +0.1W.20. +0.1w.2d. +0.1X.1+. +0.1z.2c. +0.2n.z. +2.A.2k. +5.1C.2a. +7.2p.w. +c.28.1Q. +0.1A.2c. +0.1R.26. +0.1S.25. +0.1W.21. +0.1X.1/. +0.1x.2d. +2.A.2l. +5.1D.2a. +5.1t.2f. +7.2p.x. +f.1V.22. +0.1B.2c. +0.1E.29. +0.1S.26. +0.1W.22. +0.1X.20. +0.1y.2d. +7.2p.y. +f.1R.27. +f.1V.23. +0.1C.2c. +0.1F.29. +0.1S.27. +0.1V.24. +0.1W.23. +0.1X.21. +0.1z.2d. +0.A.2n. +5.1E.2a. +7.2p.z. +0.1A.2d. +0.1D.2c. +0.1G.29. +0.1V.25. +0.1W.24. +0.1X.22. +0.B.2n. +5.1F.2a. +c.28.1R. +0.1B.2d. +0.1H.29. +0.1V.26. +0.1W.25. +0.1X.23. +0.C.2n. +5.1G.2a. +c.28.1S. +0.1C.2d. +0.1E.2c. +0.1I.29. +0.1V.27. +0.1W.26. +0.1X.24. +0.D.2n. +5.1H.2a. +7.2p.A. +0.1D.2d. +0.1F.2c. +0.1J.29. +0.1W.27. +0.1X.25. +0.E.2n. +5.1I.2a. +7.2p.B. +0.1G.2c. +0.1X.26. +0.1Y.1Y. +0.F.2n. +5.1J.2a. +7.2p.C. +c.28.1V. +0.1E.2d. +0.1H.2c. +0.1X.27. +7.2p.D. +c.28.1W. +0.1F.2d. +0.1I.2c. +0.1K.29. +7.2p.E. +0.1G.2d. +0.1J.2c. +0.1L.29. +0.G.2n. +5.1K.2a. +7.2p.F. +c.28.1X. +0.1H.2d. +0.1M.29. +0.H.2n. +5.1L.2a. +0.1I.2d. +0.1Z.1Y. +3.I.2n. +4.1N.29. +5.1M.2a. +0.1+.1Y. +0.1J.2d. +0.1K.2c. +0.1O.29. +3.J.2n. +5.1N.2a. +7.2p.G. +0.1/.1Y. +0.1L.2c. +0.1P.29. +0.K.2n. +5.1O.2a. +7.2p.H. +0.1M.2c. +0.1Q.29. +0.20.1Y. +0.L.2n. +5.1P.2a. +7.2p.I. +0.1K.2d. +0.1N.2c. +0.1Q.2a. +3.M.2n. +7.2p.J. +j.21.1Y. +0.1L.2d. +0.1O.2c. +0.1Z.1Z. +0.22.1Y. +7.2p.K. +0.1+.1Z. +0.1M.2d. +0.1P.2c. +0.23.1Y. +7.2p.L. +0.1/.1Z. +0.1+.1+. +0.1Q.2c. +0.24.1Y. +0.N.2n. +4.1N.2d. +7.2p.M. +f.1R.29. +0.1+.1/. +0.1O.2d. +0.20.1Z. +0.O.2n. +4.1S.29. +5.1R.2a. +f.25.1Y. +0.1/.1/. +0.1+.20. +0.1P.2d. +0.1S.2a. +0.26.1Y. +0.P.2n. +e.21.1Z. +0.1/.20. +0.1+.21. +0.1Q.2d. +0.22.1Z. +0.27.1Y. +0.Q.2n. +7.2p.N. +0.1/.21. +0.1+.22. +0.1R.2c. +0.20.20. +0.23.1Z. +0.R.2n. +7.2p.O. +f.1V.29. +j.S.2m. +0.1/.22. +0.1+.23. +0.1S.2c. +0.1W.29. +0.20.21. +0.24.1Z. +3.S.2n. +5.2q.0. +7.2p.P. +c.28.1Y. +f.1V.2a. +0.1/.23. +0.1+.24. +0.1Z.25. +0.20.22. +5.1W.2a. +5.2q.1. +7.2p.Q. +c.T.2n. +e.21.21. +0.1/.24. +0.1+.25. +0.1R.2d. +0.1X.29. +0.1Z.26. +0.20.23. +0.U.2n. +5.1V.2b. +5.2q.2. +7.2p.R. +e.21.22. +0.1/.25. +0.1+.26. +0.1S.2d. +0.1V.2c. +0.1X.2a. +0.1Z.27. +0.20.24. +0.22.22. +0.V.2n. +5.2q.3. +7.2p.S. +e.21.23. +0.1/.26. +0.1+.27. +0.1W.2c. +0.20.25. +0.22.23. +0.W.2n. +7.2p.T. +e.21.24. +0.1/.27. +0.20.26. +0.22.24. +0.X.2n. +5.2q.5. +7.2p.U. +c.28.1Z. +e.21.25. +f.23.23. +0.1V.2d. +0.1X.2c. +0.20.27. +0.22.25. +0.23.24. +5./.2h. +5.2q.6. +7.2p.V. +c.28.1+. +c.Y.2n. +e.21.26. +0.1W.2d. +0.22.26. +0.23.25. +0.24.24. +0.Z.2n. +5./.2i. +5.1V.2e. +7.2p.W. +c.28.1/. +e.21.27. +0.22.27. +0.23.26. +0.24.25. +5./.2j. +5.2q.8. +7.2p.X. +c.28.20. +0.1X.2d. +0.23.27. +0.24.26. +0.25.25. +5./.2k. +7.2p.Y. +c.28.21. +0.24.27. +4.26.25. +5./.2l. +5.1V.2f. +5.2q.a. +7.2p.Z. +c.28.22. +0.+.2n. +0.25.27. +4.26.26. +5.2q.b. +c.28.23. +0./.2n. +4.26.27. +5.2q.c. +c.28.24. +0.10.2n. +0.27.27. +c.28.25. +0.11.2n. +7.2p.+. +c.28.26. +0.12.2n. +7.2p./. +c.28.27. +0.29.1Y. +7.2p.10. +5.2a.1Y. +7.2p.11. +c.28.28. +0.13.2n. +7.2p.12. +0.14.2n. +0.15.2n. +0.2c.1Y. +0.16.2n. +0.29.1Z. +7.2p.13. +0.1+.29. +0.17.2n. +0.1Z.2a. +7.2p.14. +0.1/.29. +0.1+.2a. +0.18.2n. +4.2d.1Y. +7.2p.15. +0.19.2n. +0.20.29. +5.1/.2a. +7.2p.16. +0.1a.2n. +0.1Z.2c. +0.20.2a. +7.1+.2b. +7.2p.17. +e.21.29. +0.1+.2c. +0.1b.2n. +0.22.29. +2.1d.2k. +7.2p.18. +e.21.2a. +0.1/.2c. +0.1c.2n. +0.23.29. +2.1d.2l. +5.22.2a. +7.2p.19. +0.1Z.2d. +0.20.2c. +0.23.2a. +0.29.24. +7.2p.1a. +0.1+.2d. +0.1d.2n. +0.24.2a. +0.29.25. +7.22.2b. +7.2p.1b. +e.21.2c. +0.1/.2d. +0.22.2c. +0.25.2a. +0.29.26. +7.1+.2e. +7.2p.1c. +0.20.2d. +0.23.2c. +0.26.2a. +0.29.27. +5.1h.2h. +f.24.2b. +0.1e.2n. +0.24.2c. +5.1h.2i. +5.27.2a. +7.2p.1d. +e.21.2d. +0.1f.2n. +0.22.2d. +0.25.2c. +5.1h.2j. +7.1+.2f. +7.26.2b. +c.28.29. +0.23.2d. +0.26.2c. +0.2n.1g. +5.1h.2k. +7.22.2e. +c.28.2a. +0.24.2d. +0.27.2c. +5.1h.2l. +7.2p.1e. +0.25.2d. +7.2p.1f. +f.24.2e. +0.1h.2n. +0.26.2d. +7.22.2f. +7.2p.1g. +c.28.2c. +0.27.2d. +4.1i.2n. +7.26.2e. +j.1j.2m. +4.1j.2n. +5.2g.1O. +7.24.2f. +7.2p.1h. +c.28.2d. +f.2g.1P. +7.26.2f. +7.2p.1i. +f.2g.1Q. +0.1k.2n. +7.2p.1j. +3.1l.2n. +0.1m.2n. +0.1n.2n. +5.1t.2h. +7.2p.1k. +f.2g.1R. +0.1o.2n. +5.1t.2i. +7.2p.1l. +0.1p.2n. +5.1t.2j. +7.2p.1m. +0.1q.2n. +5.1t.2k. +7.2p.1n. +0.1r.2n. +0.29.29. +5.1t.2l. +5.2g.1V. +7.2p.1o. +0.1s.2n. +5.29.2a. +7.2p.1p. +f.2g.1W. +0.1t.2n. +7.2p.1q. +f.2a.2a. +3.1u.2n. +5.29.2b. +7.2p.1r. +f.2g.1X. +0.1v.2n. +0.29.2c. +7.2p.1s. +0.1w.2n. +7.2c.2a. +7.2p.1t. +0.1x.2n. +7.2p.1u. +0.1y.2n. +0.29.2d. +7.2p.1v. +0.1z.2n. +0.2a.2d. +5.29.2e. +7.2p.1w. +f.2c.2c. +0.1A.2n. +7.2p.1x. +0.1B.2n. +7.2p.1y. +0.1C.2n. +5.29.2f. +7.2p.1z. +f.2c.2d. +0.1D.2n. +7.2p.1A. +j.2a.2f. +7.2p.1B. +0.1E.2n. +0.2d.2d. +1.2f.2b. +7.2p.1C. +0.1F.2n. +7.2p.1D. +0.1G.2n. +0.1H.2n. +7.2p.1E. +0.1I.2n. +7.2p.1F. +0.1J.2n. +1.2f.2e. +7.2p.1G. +7.2p.1H. +7.2p.1I. +0.1K.2n. +1.2f.2f. +7.2p.1J. +0.1L.2n. +0.1M.2n. +0.1N.2n. +7.2p.1K. +0.1O.2n. +7.2p.1L. +0.1P.2n. +7.2p.1M. +0.1Q.2n. +7.2p.1N. +7.2p.1O. +5.1V.2h. +7.2p.1P. +8.2g.25. +7.2p.1Q. +8.2g.26. +f.1R.2n. +0.1S.2n. +5.1V.2k. +5.1V.2l. +7.2p.1R. +0.1V.2n. +7.2p.1S. +0.1W.2n. +0.1X.2n. +7.2p.1V. +7.2p.1W. +7.2p.1X. +5.2g.29. +f.2g.2a. +0.2n.1Y. +7.1+.2h. +8.2g.2c. +f.1+.2i. +7.1+.2j. +7.1+.2k. +7.2p.1Y. +7.1+.2l. +7.22.2h. +7.22.2i. +f.1Z.2n. +0.1+.2n. +7.22.2j. +7.24.2h. +0.1/.2n. +7.22.2k. +f.24.2i. +0.20.2n. +7.22.2l. +7.24.2j. +7.26.2h. +7.2p.1Z. +7.24.2k. +7.26.2i. +7.2p.1+. +e.21.2n. +0.22.2n. +7.24.2l. +7.26.2j. +7.2p.1/. +0.23.2n. +7.26.2k. +7.2p.20. +0.24.2n. +7.26.2l. +7.2p.21. +0.25.2n. +7.2p.22. +4.26.2n. +7.2p.23. +0.27.2n. +7.2p.24. +7.2p.25. +7.2p.26. +c.28.2n. +7.2p.27. +7.2p.28. +5.29.2h. +5.29.2i. +5.29.2j. +j.2a.2i. +5.29.2k. +5.29.2l. +0.29.2n. +f.2a.2n. +1.2n.2b. +7.2p.29. +0.2c.2n. +7.2p.2a. +1.2f.2h. +5.2q.1O. +1.2f.2i. +5.2q.1P. +0.2d.2n. +1.2f.2j. +5.2q.1Q. +7.2p.2c. +1.2f.2k. +1.2f.2l. +7.2p.2d. +5.2q.1R. +5.2q.1S. +5.2q.1V. +5.2q.1W. +5.2q.1X. +5.2q.1Y. +5.2q.1Z. +5.2q.1+. +5.2q.1/. +5.2q.20. +5.2q.21. +5.2q.22. +5.2q.23. +5.2q.25. +5.2q.26. +5.2q.27. +5.2q.28. +5.2q.29. +5.2q.2a. +5.2q.2b. +1.2n.2h. +1.2n.2i. +1.2n.2j. +1.2n.2k. +1.2n.2l. +0.2n.2n. +7.2p.2n. +7.2p.2p. +2.0.2r. +2.0.2s. +2.1.2r. +2.0.2t. +2.1.2s. +2.2.2r. +2.1.2t. +2.2.2s. +2.3.2r. +2.2.2t. +2.3.2s. +2.4.2r. +2.3.2t. +2.4.2s. +2.5.2r. +2.4.2t. +2.6.2r. +e.5.2s. +2.5.2t. +2.6.2s. +2.7.2r. +2.6.2t. +2.7.2s. +2.8.2r. +2.7.2t. +2.8.2s. +2.9.2r. +2.8.2t. +2.9.2s. +2.a.2r. +2.9.2t. +2.a.2s. +2.b.2r. +2.a.2t. +2.b.2s. +2.b.2t. +1.c.2u. +5.2q.2h. +2.c.2v. +5.2q.2i. +5.2q.2j. +5.2q.2k. +5.2q.2l. +5.2q.2n. +3.2y.0. +3.2y.1. +5.2p.2q. +3.2y.2. +1.k.2u. +3.2y.3. +2.k.2v. +e.4.2y. +i.c.2w. +3.2y.5. +3.2y.6. +e.7.2y. +0.d.2w. +3.2x.c. +3.2y.8. +0.e.2w. +e.9.2y. +0.f.2w. +3.2y.a. +0.g.2w. +3.2y.b. +0.h.2w. +1.r.2u. +3.2x.d. +3.2y.c. +0.i.2w. +1.s.2u. +2.r.2v. +3.2x.e. +8.2z.c. +0.j.2w. +2.s.2v. +3.2x.f. +8.2A.c. +0.k.2w. +1.u.2u. +3.2B.c. +3.2x.g. +0.l.2w. +2.u.2v. +3.2x.h. +3.2y.d. +a.2F.0. +i.c.2C. +0.m.2w. +3.2x.i. +3.2y.e. +8.2z.d. +a.2F.1. +3.2x.j. +3.2y.f. +3.n.2w. +8.2A.d. +8.2z.e. +a.2F.2. +0.o.2w. +3.2B.d. +3.2x.k. +3.2y.g. +8.2A.e. +8.2z.f. +a.2F.3. +g.2E.b. +i.c.2D. +0.p.2w. +3.2B.e. +3.2x.l. +3.2y.h. +4.d.2C. +8.2A.f. +8.2z.g. +e.4.2F. +f.2E.c. +0.q.2w. +3.2B.f. +3.2x.m. +3.2y.i. +4.e.2C. +8.2A.g. +8.2z.h. +a.2F.5. +3.2B.g. +3.2y.j. +3.n.2x. +4.f.2C. +8.2A.h. +8.2z.i. +a.2F.6. +0.d.2D. +3.2B.h. +3.2x.o. +3.2y.k. +4.g.2C. +8.2A.i. +8.2z.j. +e.7.2F. +0.e.2D. +0.r.2w. +2.A.2v. +3.2B.i. +3.2x.p. +3.2y.l. +4.h.2C. +8.2A.j. +8.2z.k. +a.2F.8. +g.2E.d. +0.f.2D. +0.s.2w. +3.2B.j. +3.2x.q. +3.2y.m. +4.i.2C. +8.2A.k. +8.2z.l. +e.9.2F. +g.2E.e. +0.g.2D. +0.j.2C. +0.t.2w. +3.2B.k. +3.n.2y. +8.2A.l. +8.2z.m. +a.2F.a. +g.2E.f. +0.h.2D. +0.u.2w. +3.2B.l. +3.2y.o. +4.k.2C. +8.2A.m. +8.2z.n. +a.2F.b. +g.2E.g. +0.i.2D. +0.l.2C. +0.v.2w. +3.2B.m. +3.2x.r. +3.2y.p. +8.2A.n. +8.2z.o. +a.2F.c. +g.2E.h. +0.j.2D. +0.m.2C. +0.w.2w. +3.2B.n. +3.2x.s. +3.2y.q. +8.2A.o. +8.2z.p. +g.2E.i. +0.k.2D. +0.x.2w. +3.2B.o. +3.2x.t. +3.n.2C. +8.2A.p. +8.2z.q. +g.2E.j. +i.c.2G. +0.l.2D. +0.y.2w. +3.2B.p. +3.2x.u. +4.o.2C. +8.2A.q. +g.2E.k. +0.m.2D. +0.p.2C. +2.2H.c. +3.2B.q. +3.2x.v. +3.2y.r. +a.2F.d. +f.2w.z. +g.2E.l. +3.2x.w. +3.2y.s. +3.n.2D. +4.q.2C. +8.2z.r. +a.2F.e. +g.2E.m. +0.d.2G. +0.o.2D. +3.2x.x. +3.2y.t. +8.2A.r. +8.2z.s. +a.2F.f. +g.2E.n. +0.A.2w. +0.e.2G. +0.p.2D. +3.2B.r. +3.2x.y. +3.2y.u. +8.2A.s. +8.2z.t. +a.2F.g. +g.2E.o. +0.B.2w. +0.f.2G. +0.q.2D. +0.r.2C. +2.2H.d. +3.2B.s. +3.2x.z. +3.2y.v. +8.2A.t. +8.2z.u. +a.2F.h. +g.2E.p. +i.c.2I. +0.C.2w. +0.g.2G. +0.s.2C. +2.2H.e. +3.2B.t. +3.2y.w. +8.2A.u. +8.2z.v. +a.2F.i. +g.2E.q. +0.D.2w. +0.h.2G. +0.t.2C. +2.2H.f. +3.2B.u. +3.2y.x. +8.2A.v. +8.2z.w. +a.2F.j. +0.E.2w. +0.i.2G. +0.r.2D. +0.u.2C. +2.2H.g. +3.2B.v. +3.2x.A. +3.2y.y. +8.2A.w. +8.2z.x. +a.2F.k. +0.j.2G. +0.s.2D. +0.v.2C. +2.2H.h. +3.2B.w. +3.2x.B. +3.2y.z. +4.d.2I. +4.F.2w. +8.2A.x. +8.2z.y. +a.2F.l. +g.2E.r. +0.k.2G. +0.t.2D. +0.w.2C. +2.2H.i. +3.2B.x. +3.2x.C. +4.e.2I. +8.2A.y. +8.2z.z. +a.2F.m. +g.2E.s. +0.l.2G. +0.u.2D. +0.x.2C. +2.2H.j. +3.2B.y. +3.2x.D. +3.n.2F. +4.f.2I. +8.2A.z. +a.2J.c. +g.2E.t. +0.G.2w. +0.m.2G. +0.y.2C. +2.2H.k. +3.2B.z. +3.2x.E. +3.2y.A. +4.g.2I. +4.v.2D. +a.2F.o. +g.2E.u. +0.H.2w. +0.w.2D. +2.2H.l. +3.2x.F. +3.2y.B. +3.n.2G. +4.h.2I. +8.2z.A. +a.2F.p. +f.2C.z. +g.2E.v. +0.o.2G. +0.x.2D. +2.2H.m. +3.2y.C. +3.I.2w. +4.i.2I. +8.2A.A. +8.2z.B. +a.2F.q. +g.2E.w. +0.p.2G. +0.y.2D. +2.2H.n. +3.2B.A. +3.2y.D. +3.J.2w. +4.j.2I. +8.2A.B. +8.2z.C. +a.2J.d. +g.2E.x. +i.c.2K. +0.2D.z. +0.K.2w. +0.q.2G. +2.2H.o. +3.2B.B. +3.2x.G. +3.2y.E. +4.A.2C. +4.k.2I. +8.2A.C. +8.2z.D. +a.2J.e. +g.2E.y. +0.B.2C. +0.L.2w. +2.2H.p. +3.2B.C. +3.2x.H. +3.2y.F. +4.l.2I. +8.2A.D. +8.2z.E. +a.2F.r. +a.2J.f. +g.2E.z. +2.2H.q. +3.2B.D. +3.I.2x. +3.M.2w. +4.C.2C. +4.m.2I. +8.2A.E. +8.2z.F. +a.2F.s. +a.2J.g. +0.A.2D. +0.D.2C. +0.d.2K. +0.r.2G. +3.2B.E. +3.J.2x. +3.n.2I. +8.2A.F. +a.2F.t. +a.2J.h. +0.B.2D. +0.e.2K. +0.s.2G. +3.2B.F. +3.2x.K. +3.2y.G. +4.E.2C. +4.o.2I. +a.2F.u. +a.2J.i. +g.2E.A. +0.C.2D. +0.f.2K. +0.N.2w. +0.t.2G. +2.2H.r. +3.2x.L. +3.2y.H. +4.F.2C. +4.p.2I. +8.2z.G. +a.2F.v. +a.2J.j. +g.2E.B. +i.c.2L. +0.D.2D. +0.g.2K. +0.O.2w. +0.u.2G. +2.2H.s. +3.I.2y. +3.M.2x. +4.q.2I. +8.2A.G. +8.2z.H. +a.2F.w. +a.2J.k. +g.2E.C. +0.E.2D. +0.h.2K. +0.P.2w. +0.v.2G. +2.2H.t. +3.2B.G. +3.J.2y. +8.2A.H. +8.2z.I. +a.2F.x. +a.2J.l. +g.2E.D. +0.G.2C. +0.i.2K. +0.Q.2w. +0.w.2G. +2.2H.u. +3.2B.H. +3.2y.K. +4.F.2D. +8.2A.I. +8.2z.J. +a.2F.y. +a.2J.m. +g.2E.E. +0.d.2L. +0.H.2C. +0.j.2K. +0.r.2I. +0.R.2w. +0.x.2G. +2.2H.v. +3.2B.I. +3.2x.N. +3.2y.L. +3.n.2J. +8.2A.J. +8.2z.K. +a.2F.z. +g.2E.F. +i.c.2M. +0.e.2L. +0.k.2K. +0.y.2G. +1./.2u. +2.2H.w. +3.2B.J. +3.2x.O. +3.I.2C. +3.M.2y. +3.S.2w. +4.s.2I. +8.2A.K. +8.2z.L. +a.2J.o. +0.f.2L. +0.l.2K. +2./.2v. +2.2H.x. +3.2B.K. +3.2x.P. +3.J.2C. +4.2G.z. +4.G.2D. +4.t.2I. +8.2A.L. +8.2z.M. +a.2J.p. +c.T.2w. +0.g.2L. +0.H.2D. +0.K.2C. +0.m.2K. +0.U.2w. +2.2H.y. +3.2B.L. +3.2x.Q. +4.u.2I. +8.2A.M. +a.2F.A. +a.2J.q. +g.2E.G. +0.h.2L. +0.L.2C. +0.V.2w. +2.2H.z. +3.2B.M. +3.2x.R. +3.2y.N. +3.I.2D. +3.n.2K. +4.d.2M. +4.v.2I. +a.2F.B. +g.2E.H. +i.c.2N. +0.A.2G. +0.i.2L. +0.o.2K. +0.W.2w. +3.2y.O. +3.J.2D. +3.M.2C. +3.S.2x. +4.e.2M. +4.w.2I. +8.2z.N. +a.2F.C. +g.2E.I. +i.c.2O. +0.B.2G. +0.j.2L. +0.K.2D. +0.p.2K. +0.X.2w. +3.2x.T. +3.2y.P. +4.f.2M. +4.x.2I. +8.2A.N. +8.2z.O. +a.2F.D. +a.2J.r. +a.2P.c. +g.2E.J. +0.C.2G. +0.k.2L. +0.L.2D. +0.q.2K. +2.2H.A. +3.2B.N. +3.2x.U. +3.2y.Q. +4.g.2M. +4.y.2I. +8.2A.O. +8.2z.P. +a.2F.E. +a.2J.s. +c.Y.2w. +g.2E.K. +l.2q.2q. +0.D.2G. +0.d.2N. +0.h.2M. +0.l.2L. +0.N.2C. +0.Z.2w. +2.2H.B. +3.2B.O. +3.2x.V. +3.2y.R. +3.M.2D. +8.2A.P. +8.2z.Q. +a.2F.F. +a.2J.t. +f.2I.z. +g.2E.L. +i.c.2Q. +0.d.2O. +0.E.2G. +0.e.2N. +0.m.2L. +2.2H.C. +3.2B.P. +3.2x.W. +3.S.2y. +4.i.2M. +4.O.2C. +8.2A.Q. +8.2z.R. +a.2J.u. +g.2E.M. +i.c.2R. +0.e.2O. +0.F.2G. +0.f.2N. +0.P.2C. +0.r.2K. +2.2H.D. +3.2B.Q. +3.2x.X. +3.2y.T. +3.n.2L. +4.j.2M. +8.2A.R. +8.2z.S. +a.2J.v. +a.2P.d. +0.f.2O. +0.g.2N. +0.N.2D. +0.o.2L. +0.s.2K. +2.2H.E. +3.2B.R. +3.2x.Y. +3.2y.U. +4.A.2I. +4.k.2M. +4.Q.2C. +8.2A.S. +8.2z.T. +a.2F.G. +a.2J.w. +a.2P.e. +i.c.2S. +0.+.2w. +0.d.2Q. +0.g.2O. +0.h.2N. +0.O.2D. +0.p.2L. +0.R.2C. +0.t.2K. +2.2H.F. +3.2B.S. +3.2x.Z. +3.2y.V. +4.B.2I. +4.l.2M. +8.2A.T. +8.2z.U. +a.2F.H. +a.2J.x. +a.2P.f. +g.2E.N. +0./.2w. +0.e.2Q. +0.G.2G. +0.h.2O. +0.i.2N. +0.m.2M. +0.P.2D. +0.q.2L. +0.u.2K. +3.2B.T. +3.2y.W. +3.I.2F. +3.S.2C. +4.C.2I. +4.d.2R. +8.2A.U. +8.2z.V. +a.2J.y. +a.2P.g. +a.2V.0. +g.2E.O. +0.10.2w. +0.f.2Q. +0.H.2G. +0.i.2O. +0.Q.2D. +0.v.2K. +3.2B.U. +3.2y.X. +3.J.2F. +3.n.2M. +4.D.2I. +4.e.2R. +4.j.2N. +8.2A.V. +8.2z.W. +a.2J.z. +a.2P.h. +a.2V.1. +c.T.2C. +g.2E.P. +0.11.2w. +0.d.2S. +0.g.2Q. +0.j.2O. +0.R.2D. +0.w.2K. +2.2H.G. +3.2B.V. +3.2y.Y. +3.I.2G. +4.E.2I. +4.f.2R. +4.k.2N. +4.o.2M. +4.U.2C. +8.2A.W. +8.2z.X. +a.2F.K. +a.2P.i. +a.2V.2. +g.2E.Q. +0.e.2S. +0.h.2Q. +0.k.2O. +0.l.2N. +0.r.2L. +0.x.2K. +2.2H.H. +3.2B.W. +3.2x.+. +3.2y.Z. +3.J.2G. +3.S.2D. +4.12.2w. +4.F.2I. +4.g.2R. +4.p.2M. +4.V.2C. +5.c.2T. +8.2A.X. +8.2z.Y. +a.2F.L. +a.2P.j. +a.2V.3. +g.2E.R. +0.f.2S. +0.i.2Q. +0.K.2G. +0.l.2O. +0.m.2N. +0.s.2L. +0.W.2C. +0.y.2K. +2.2H.I. +3.2B.X. +3.2x./. +3.M.2F. +4.h.2R. +4.q.2M. +8.2A.Y. +8.2z.Z. +a.2J.A. +a.2P.k. +c.T.2D. +e.4.2V. +g.2E.S. +0.g.2S. +0.L.2G. +0.m.2O. +0.t.2L. +0.U.2D. +0.X.2C. +2.2H.J. +3.2B.Y. +3.2x.10. +3.n.2N. +4.2K.z. +4.i.2R. +4.j.2Q. +8.2A.Z. +a.2J.B. +a.2P.l. +a.2V.5. +g.2E.T. +0.13.2w. +0.u.2L. +0.V.2D. +2.1d.2v. +2.2H.K. +3.2B.Z. +3.2x.11. +3.M.2G. +3.n.2O. +4.G.2I. +4.h.2S. +4.j.2R. +4.k.2Q. +4.o.2N. +a.2J.C. +a.2P.m. +a.2V.6. +c.Y.2C. +g.2E.U. +0.i.2S. +0.l.2Q. +0.o.2O. +0.p.2N. +0.r.2M. +0.v.2L. +0.W.2D. +0.Z.2C. +2.2H.L. +3.2x.12. +3.2y.+. +3.n.2P. +4.14.2w. +4.H.2I. +4.k.2R. +a.2F.N. +a.2J.D. +d.2U.c. +e.7.2V. +g.2E.V. +0.A.2K. +0.j.2S. +0.m.2Q. +0.p.2O. +0.w.2L. +0.X.2D. +2.2H.M. +3.2y./. +3.I.2I. +4.15.2w. +4.l.2R. +4.q.2N. +4.s.2M. +8.2z.+. +a.2F.O. +a.2J.E. +a.2P.o. +a.2V.8. +g.2E.W. +0.16.2w. +0.B.2K. +0.k.2S. +0.N.2G. +0.q.2O. +0.t.2M. +0.x.2L. +3.2y.10. +3.J.2I. +3.n.2Q. +4.m.2R. +8.2A.+. +8.2z./. +a.2F.P. +a.2J.F. +a.2P.p. +c.Y.2D. +e.9.2V. +g.2E.X. +0.C.2K. +0.l.2S. +0.O.2G. +0.o.2Q. +0.u.2M. +0.y.2L. +3.2B.+. +3.2x.13. +3.2y.11. +3.n.2R. +4.17.2w. +4.K.2I. +4.Z.2D. +8.2A./. +8.2z.10. +a.2F.Q. +a.2P.q. +a.2V.a. +g.2E.Y. +0.+.2C. +0.D.2K. +0.L.2I. +0.m.2S. +0.P.2G. +0.p.2Q. +0.r.2N. +0.v.2M. +2.2H.N. +3.2B./. +3.2x.14. +3.2y.12. +4.18.2w. +4.o.2R. +8.2A.10. +8.2z.11. +a.2F.R. +a.2V.b. +d.2U.d. +f.2L.z. +g.2E.Z. +0.19.2w. +0.E.2K. +0.Q.2G. +0.q.2Q. +0.r.2O. +0.s.2N. +0.w.2M. +2.2H.O. +3.2B.10. +3.2x.15. +3.M.2I. +3.n.2S. +3.S.2F. +4./.2C. +4.p.2R. +8.2A.11. +8.2z.12. +a.2J.G. +a.2V.c. +d.2U.e. +0.F.2K. +0.o.2S. +0.R.2G. +0.s.2O. +0.t.2N. +0.x.2M. +1.1h.2u. +2.2H.P. +3.2B.11. +3.2W.c. +3.2x.16. +4.10.2C. +4.1a.2w. +4.q.2R. +8.2A.12. +a.2F.T. +a.2J.H. +a.2P.r. +d.2U.f. +0.+.2D. +0.1b.2w. +0.A.2L. +0.p.2S. +0.t.2O. +0.u.2N. +0.y.2M. +2.1h.2v. +2.2H.Q. +3.2B.12. +3.2x.17. +3.2y.13. +3.I.2J. +3.S.2G. +4.11.2C. +5.c.2X. +5.k.2T. +a.2F.U. +a.2P.s. +d.2U.g. +0./.2D. +0.B.2L. +0.q.2S. +0.r.2Q. +0.u.2O. +0.v.2N. +2.2H.R. +3.2x.18. +3.2y.14. +3.J.2J. +4.12.2C. +4.1c.2w. +4.N.2I. +8.2z.13. +a.2F.V. +a.2P.t. +c.T.2G. +d.2U.h. +f.2M.z. +g.2E.+. +0.10.2D. +0.C.2L. +0.G.2K. +0.r.2R. +0.s.2Q. +0.U.2G. +0.v.2O. +0.w.2N. +2.2H.S. +3.2x.19. +3.2y.15. +4.O.2I. +8.2A.13. +8.2z.14. +a.2F.W. +a.2J.K. +a.2P.u. +a.2V.d. +d.2U.i. +g.2E./. +0.11.2D. +0.D.2L. +0.H.2K. +0.t.2Q. +0.V.2G. +0.w.2O. +0.x.2N. +2.2H.T. +3.2B.13. +3.2W.d. +3.2x.1a. +3.2y.16. +4.1d.2w. +4.P.2I. +4.s.2R. +8.2A.14. +8.2z.15. +a.2F.X. +a.2J.L. +a.2P.v. +a.2V.e. +d.2U.j. +g.2E.10. +0.13.2C. +0.E.2L. +0.r.2S. +0.u.2Q. +0.W.2G. +0.x.2O. +0.y.2N. +2.2H.U. +3.2B.14. +3.2W.e. +3.2x.1b. +3.2y.17. +3.I.2K. +3.M.2J. +4.12.2D. +4.A.2M. +4.Q.2I. +4.t.2R. +8.2A.15. +8.2z.16. +a.2F.Y. +a.2P.w. +a.2V.f. +a.2Y.c. +d.2U.k. +g.2E.11. +0.2N.z. +0.F.2L. +0.s.2S. +0.v.2Q. +0.X.2G. +0.y.2O. +2.2H.V. +3.2B.15. +3.2W.f. +3.2x.1c. +3.2y.18. +3.J.2K. +4.14.2C. +4.B.2M. +4.R.2I. +4.u.2R. +8.2A.16. +8.2z.17. +a.2F.Z. +a.2P.x. +a.2V.g. +d.2U.l. +g.2E.12. +0.1e.2w. +0.2O.z. +0.K.2K. +0.t.2S. +0.w.2Q. +2.2H.W. +3.2B.16. +3.2W.g. +3.2y.19. +3.S.2I. +4.15.2C. +4.C.2M. +4.v.2R. +8.2A.17. +8.2z.18. +a.2P.y. +a.2V.h. +c.Y.2G. +d.2U.m. +0.13.2D. +0.1f.2w. +0.L.2K. +0.u.2S. +0.x.2Q. +0.Z.2G. +2.2H.X. +3.2B.17. +3.2W.h. +3.2x.1d. +3.2y.1a. +4.16.2C. +4.D.2M. +4.w.2R. +8.2A.18. +8.2z.19. +9.c.2Z. +a.2J.N. +a.2P.z. +a.2V.i. +c.T.2I. +d.2U.n. +0.14.2D. +0.A.2N. +0.G.2L. +0.v.2S. +0.y.2Q. +2.2H.Y. +3.2B.18. +3.2W.i. +3.2y.1b. +3.M.2K. +4.17.2C. +4.E.2M. +4.U.2I. +4.x.2R. +8.2A.19. +8.2z.1a. +a.2J.O. +a.2V.j. +a.2Y.d. +d.2U.o. +f.2w.1g. +g.2E.13. +0.15.2D. +0.A.2O. +0.B.2N. +0.H.2L. +0.w.2S. +2.2H.Z. +3.2B.19. +3.2W.j. +3.2y.1c. +4.18.2C. +4.F.2M. +4.r.2T. +4.V.2I. +4.y.2R. +8.2A.1a. +8.2z.1b. +a.2F.+. +a.2J.P. +a.2V.k. +a.2Y.e. +d.2U.p. +f.2Q.z. +g.2E.14. +0.16.2D. +0.19.2C. +0.B.2O. +0.x.2S. +3.2B.1a. +3.2W.k. +3.2x.1e. +3.I.2L. +4.C.2N. +4.W.2I. +7.s.2T. +8.2A.1b. +8.2z.1c. +a.2F./. +a.2J.Q. +a.2P.A. +a.2V.l. +a.2Y.f. +d.2U.q. +f.2R.z. +g.2E.15. +0.+.2G. +0.17.2D. +0.1h.2w. +0.C.2O. +0.N.2K. +0.y.2S. +3.2B.1b. +3.2W.l. +3.2x.1f. +3.2y.1d. +3.J.2L. +4.1a.2C. +4.D.2N. +4.X.2I. +5.k.2X. +8.2A.1c. +a.2F.10. +a.2J.R. +a.2P.B. +a.2V.m. +a.2Y.g. +g.2E.16. +0./.2G. +0.18.2D. +0.2S.z. +0.A.2Q. +0.D.2O. +0.K.2L. +0.O.2K. +3.2B.1c. +3.2W.m. +3.2x.1g. +3.n.2V. +3.S.2J. +4.1b.2C. +4.1i.2w. +4.E.2N. +4.G.2M. +4.u.2T. +8.2z.1d. +a.2F.11. +a.2P.C. +a.2Y.h. +c.Y.2I. +g.2E.17. +0.10.2G. +0.19.2D. +0.1j.2w. +0.B.2Q. +0.E.2O. +0.L.2L. +0.P.2K. +1.1t.2u. +2.2H.+. +3.n.2W. +4.1c.2C. +4.A.2R. +4.F.2N. +4.H.2M. +4.Z.2I. +8.2+.c. +8.2A.1d. +a.2F.12. +a.2J.T. +a.2P.D. +a.2V.o. +a.2Y.i. +d.2U.r. +g.2E.18. +0.11.2G. +0.C.2Q. +0.F.2O. +0.Q.2K. +2.1t.2v. +2.2H./. +3.2B.1d. +3.2W.o. +3.2y.1e. +3.I.2M. +3.M.2L. +4.1a.2D. +4.B.2R. +a.2J.U. +a.2P.E. +a.2V.p. +a.2Y.j. +d.2U.s. +g.2E.19. +0.1b.2D. +0.A.2S. +0.D.2Q. +0.R.2K. +2.2H.10. +3.2W.p. +3.2x.1h. +3.2y.1f. +3.J.2M. +4.12.2G. +4.1d.2C. +4.C.2R. +8.2z.1e. +a.2J.V. +a.2P.F. +a.2V.q. +a.2Y.k. +d.2U.t. +g.2E.1a. +0.1c.2D. +0.1k.2w. +0.B.2S. +0.E.2Q. +0.G.2N. +0.K.2M. +2.2/.c. +2.2H.11. +3.2W.q. +3.2x.1i. +3.2y.1g. +3.S.2K. +4.D.2R. +8.2A.1e. +8.2z.1f. +a.2F.13. +a.2J.W. +a.2Y.l. +d.2U.u. +g.2E.1b. +0.+.2I. +0.C.2S. +0.F.2Q. +0.G.2O. +0.H.2N. +0.L.2M. +0.N.2L. +2.2H.12. +3.1l.2w. +3.2B.1e. +3.2x.1j. +4.E.2R. +8.2+.d. +8.2A.1f. +8.2z.1g. +a.2F.14. +a.2J.X. +a.2Y.m. +c.T.2K. +d.2U.v. +g.2E.1c. +0.13.2G. +0.1d.2D. +0.1m.2w. +0.D.2S. +0.H.2O. +0.O.2L. +0.U.2K. +3.2B.1f. +3.I.2N. +3.M.2M. +3.n.2Y. +4./.2I. +4.1e.2C. +4.F.2R. +8.2+.e. +8.2A.1g. +9.k.2Z. +a.2F.15. +a.2J.Y. +a.2P.G. +a.2V.r. +d.2U.w. +0.14.2G. +0.1n.2w. +0.E.2S. +0.P.2L. +0.V.2K. +3.2B.1g. +3.2W.r. +3.2y.1h. +3.I.2O. +3.J.2N. +4.10.2I. +4.1f.2C. +8.2+.f. +a.2F.16. +a.2J.Z. +a.2P.H. +a.2V.s. +a.2Y.o. +d.2U.x. +g.2E.1d. +0.15.2G. +0.1o.2w. +0.F.2S. +0.G.2Q. +0.K.2N. +0.Q.2L. +0.W.2K. +2.2/.d. +2.2H.13. +3.2W.s. +3.2x.1k. +3.2y.1i. +3.I.2P. +3.J.2O. +4.11.2I. +4.r.2X. +8.2+.g. +8.2z.1h. +a.2F.17. +a.2V.t. +a.2Y.p. +d.2U.y. +f.2C.1g. +i.c.30. +0.16.2G. +0.1e.2D. +0.1p.2w. +0.H.2Q. +0.K.2O. +0.L.2N. +0.R.2L. +0.X.2K. +2.2/.e. +2.2H.14. +3.1l.2x. +3.2W.t. +3.2y.1j. +3.J.2P. +4.12.2I. +4.G.2R. +4.N.2M. +7.s.2X. +8.2+.h. +8.2A.1h. +8.2z.1i. +a.2F.18. +a.2V.u. +a.2Y.q. +d.2U.z. +0.17.2G. +0.1f.2D. +0.1q.2w. +0.L.2O. +2.2/.f. +2.2H.15. +3.2B.1h. +3.2W.u. +3.2x.1m. +3.I.2Q. +3.M.2N. +3.S.2L. +4.H.2R. +4.O.2M. +8.2+.i. +8.2A.1i. +8.2z.1j. +a.2F.19. +a.2P.K. +a.2V.v. +c.Y.2K. +g.2E.1e. +0.18.2G. +0.1r.2w. +0.2D.1g. +0.G.2S. +0.Z.2K. +2.2/.g. +2.2H.16. +3.2B.1i. +3.2W.v. +3.2x.1n. +3.I.2R. +3.J.2Q. +3.M.2O. +4.1h.2C. +4.P.2M. +4.T.2L. +4.u.2X. +8.2+.j. +8.2A.1j. +a.2F.1a. +a.2J.+. +a.2P.L. +a.2V.w. +g.2E.1f. +0.19.2G. +0.1s.2w. +0.d.30. +0.H.2S. +0.K.2Q. +0.U.2L. +2.2/.h. +2.2H.17. +3.2B.1j. +3.2W.w. +3.2x.1o. +3.2y.1k. +3.J.2R. +3.M.2P. +4.13.2I. +4.1i.2C. +4.Q.2M. +8.2+.k. +a.2F.1b. +a.2J./. +a.2V.x. +a.2Y.r. +d.2U.A. +g.2E.1g. +0.1a.2G. +0.1j.2C. +0.e.30. +0.L.2Q. +0.N.2N. +0.R.2M. +0.V.2L. +2.2/.i. +2.2H.18. +3.1l.2y. +3.2W.x. +3.2x.1p. +3.I.2S. +4.14.2I. +4.1t.2w. +4.K.2R. +8.2+.l. +8.2z.1k. +a.2F.1c. +a.2J.10. +a.2V.y. +a.2Y.s. +d.2U.B. +0.1b.2G. +0.1h.2D. +0.f.30. +0.L.2R. +0.N.2O. +0.W.2L. +2.2/.j. +2.2H.19. +3.1u.2w. +3.2W.y. +3.2x.1q. +3.2y.1m. +3.J.2S. +3.M.2Q. +3.S.2M. +4.15.2I. +4.O.2N. +8.2+.m. +8.2A.1k. +8.2z.1l. +a.2J.11. +a.2V.z. +a.2Y.t. +d.2U.C. +0.+.2K. +0.1c.2G. +0.1v.2w. +0.g.30. +0.K.2S. +0.O.2O. +0.P.2N. +0.X.2L. +2.2/.k. +2.2H.1a. +3.2B.1k. +3.2W.z. +3.2x.1r. +3.2y.1n. +3.M.2R. +4.16.2I. +4.1i.2D. +8.2+.n. +8.2A.1l. +8.2z.1m. +9.r.2Z. +a.2F.1d. +a.2J.12. +a.2P.N. +a.2Y.u. +c.T.2M. +d.2U.D. +g.2E.1h. +0./.2K. +0.1j.2D. +0.1k.2C. +0.1w.2w. +0.h.30. +0.L.2S. +0.P.2O. +0.Q.2N. +2.2/.l. +2.2H.1b. +3.2B.1l. +3.2x.1s. +3.2y.1o. +4.17.2I. +4.U.2M. +8.2+.o. +8.2A.1m. +8.2z.1n. +9.s.2Z. +a.2P.O. +a.2Y.v. +c.Y.2L. +d.2U.E. +g.2E.1i. +0.10.2K. +0.1d.2G. +0.1x.2w. +0.i.30. +0.N.2Q. +0.Q.2O. +0.R.2N. +0.Z.2L. +1.31.c. +2.2/.m. +2.2H.1c. +3.1l.2C. +3.2B.1m. +3.2x.1t. +3.2y.1p. +3.M.2S. +4.18.2I. +4.V.2M. +8.2+.p. +8.2A.1n. +8.2z.1o. +a.2P.P. +a.2V.A. +a.2Y.w. +d.2U.F. +g.2E.1j. +0.11.2K. +0.19.2I. +0.1y.2w. +0.j.30. +0.O.2Q. +0.R.2O. +2.2/.n. +3.1u.2x. +3.2B.1n. +3.2W.A. +3.2y.1q. +3.S.2N. +4.1m.2C. +4.N.2R. +4.W.2M. +8.2+.q. +8.2A.1o. +8.2z.1p. +9.u.2Z. +a.2F.1e. +a.2J.13. +a.2P.Q. +a.2V.B. +a.2Y.x. +0.1k.2D. +0.1n.2C. +0.1z.2w. +0.k.30. +0.P.2Q. +2.2/.o. +2.2H.1d. +3.2B.1o. +3.2W.B. +3.2x.1v. +3.2y.1r. +3.S.2O. +4.12.2K. +4.1a.2I. +4.O.2R. +4.X.2M. +8.2A.1p. +8.2z.1q. +a.2F.1f. +a.2J.14. +a.2P.R. +a.2V.C. +a.2Y.y. +c.T.2N. +0.1A.2w. +0.1e.2G. +0.1o.2C. +0.l.30. +0.N.2S. +0.Q.2Q. +2.2/.p. +3.1l.2D. +3.2B.1p. +3.2W.C. +3.2x.1w. +3.2y.1s. +3.S.2P. +4.1b.2I. +4.P.2R. +4.U.2N. +8.2A.1q. +8.2z.1r. +a.2F.1g. +a.2J.15. +a.2V.D. +a.2Y.z. +c.T.2O. +c.Y.2M. +d.2U.G. +g.2E.1k. +0.+.2L. +0.1B.2w. +0.1f.2G. +0.1m.2D. +0.1p.2C. +0.m.30. +0.O.2S. +0.R.2Q. +0.U.2O. +0.Z.2M. +1.31.d. +2.2/.q. +3.2B.1q. +3.2W.D. +3.2x.1x. +3.2y.1t. +4.1c.2I. +4.Q.2R. +4.V.2N. +8.2+.r. +8.2A.1r. +8.2z.1s. +a.2J.16. +a.2P.T. +a.2V.E. +a.32.c. +d.2U.H. +g.2E.1l. +0./.2L. +0.13.2K. +0.1C.2w. +0.1n.2D. +0.1q.2C. +0.P.2S. +0.V.2O. +1.31.e. +2.2H.1e. +3.1u.2y. +3.2B.1r. +3.2W.E. +3.2x.1y. +3.n.30. +3.S.2Q. +4.2G.1g. +4.R.2R. +4.W.2N. +8.2+.s. +8.2A.1s. +8.2z.1t. +a.2J.17. +a.2P.U. +a.2V.F. +d.2U.I. +g.2E.1m. +0.10.2L. +0.14.2K. +0.1D.2w. +0.1o.2D. +0.o.30. +0.Q.2S. +0.W.2O. +1.31.f. +2.2H.1f. +3.2B.1s. +3.2W.F. +3.2x.1z. +3.2y.1v. +3.S.2R. +4.1d.2I. +4.1r.2C. +4.X.2N. +8.2+.t. +8.2A.1t. +8.2z.1u. +a.2F.1h. +a.2J.18. +a.2P.V. +a.2Y.A. +c.T.2Q. +d.2U.J. +g.2E.1n. +0.11.2L. +0.15.2K. +0.1p.2D. +0.p.30. +0.R.2S. +0.U.2Q. +0.X.2O. +1.31.g. +1.33.c. +2.2/.r. +2.2H.1g. +3.2B.1t. +3.2x.1A. +3.2y.1w. +4.1s.2C. +8.2+.u. +8.2A.1u. +8.2z.1v. +a.2F.1i. +a.2J.19. +a.2P.W. +a.2Y.B. +c.T.2R. +c.Y.2N. +d.2U.K. +g.2E.1o. +0.+.2M. +0.16.2K. +0.1E.2w. +0.1h.2G. +0.1q.2D. +0.q.30. +0.V.2Q. +0.Z.2N. +1.31.h. +2.2/.s. +3.2B.1u. +3.2x.1B. +3.2y.1x. +3.S.2S. +4.12.2L. +4.1t.2C. +4.U.2R. +8.2+.v. +8.2A.1v. +8.2z.1w. +a.2F.1j. +a.2J.1a. +a.2P.X. +a.2V.G. +a.2Y.C. +a.32.d. +c.Y.2O. +d.2U.L. +g.2E.1p. +i.c.34. +0.17.2K. +0.1F.2w. +0.1i.2G. +0.1r.2D. +0.W.2Q. +0.Z.2O. +1.31.i. +2.2/.t. +3.1u.2C. +3.2B.1v. +3.2W.G. +3.2x.1C. +3.2y.1y. +4./.2M. +4.1e.2I. +4.V.2R. +8.2+.w. +8.2A.1w. +8.2z.1x. +9.c.35. +a.2J.1b. +a.2P.Y. +a.2V.H. +a.2Y.D. +a.32.e. +c.T.2S. +d.2U.M. +g.2E.1q. +0.18.2K. +0.1G.2w. +0.1j.2G. +0.1s.2D. +0.1v.2C. +0.U.2S. +0.X.2Q. +1.31.j. +2.2/.u. +2.2H.1h. +3.2B.1w. +3.2W.H. +3.2x.1D. +3.2y.1z. +3.I.2V. +4.10.2M. +4.1f.2I. +4.W.2R. +8.2+.x. +8.2A.1x. +8.2z.1y. +9.c.36. +a.2J.1c. +a.2P.Z. +a.2Y.E. +a.32.f. +g.2E.1r. +0.13.2L. +0.19.2K. +0.1H.2w. +0.r.30. +0.V.2S. +1.31.k. +1.33.d. +2.2/.v. +2.2H.1i. +3.2B.1x. +3.2y.1A. +3.I.2W. +3.J.2V. +4.11.2M. +4.1t.2D. +4.1w.2C. +4.X.2R. +8.2+.y. +8.2A.1y. +8.2z.1z. +a.2F.1k. +a.2Y.F. +a.32.g. +c.Y.2Q. +f.2I.1g. +g.2E.1s. +0.+.2N. +0.14.2L. +0.1a.2K. +0.1x.2C. +0.d.34. +0.s.30. +0.W.2S. +0.Z.2Q. +1.31.l. +1.33.e. +2.2/.w. +2.2H.1j. +3.1l.2F. +3.1u.2D. +3.2B.1y. +3.2x.1E. +3.2y.1B. +3.J.2W. +4.12.2M. +4.1I.2w. +8.2+.z. +8.2A.1z. +8.2z.1A. +a.2J.1d. +a.2V.K. +a.32.h. +c.Y.2R. +d.2U.N. +g.2E.1t. +0.+.2O. +0.15.2L. +0.1b.2K. +0.1J.2w. +0.1k.2G. +0.1v.2D. +0.1y.2C. +0.e.34. +0.t.30. +0.X.2S. +1.31.m. +1.33.f. +2.2/.x. +3.2B.1z. +3.2W.K. +3.2x.1F. +3.2y.1C. +4./.2N. +4.Z.2R. +8.2A.1A. +8.2z.1B. +a.2F.1m. +a.2V.L. +a.32.i. +d.2U.O. +g.2E.1u. +0./.2O. +0.16.2L. +0.1c.2K. +0.1w.2D. +0.f.34. +0.u.30. +1.31.n. +1.33.g. +2.2/.y. +3.1l.2G. +3.2B.1A. +3.2W.L. +3.2x.1G. +3.2y.1D. +3.M.2V. +4.10.2N. +4.1h.2I. +4.1z.2C. +8.2A.1B. +8.2z.1C. +a.2F.1n. +a.2P.+. +a.2Y.G. +a.32.j. +c.Y.2S. +d.2U.P. +g.2E.1v. +i.c.37. +0.10.2O. +0.13.2M. +0.17.2L. +0.1m.2G. +0.1x.2D. +0.g.34. +0.v.30. +0.Z.2S. +1.31.o. +1.33.h. +2.2/.z. +2.2H.1k. +3.2B.1B. +3.2x.1H. +3.M.2W. +4.11.2N. +4.1A.2C. +4.1i.2I. +8.2+.A. +8.2A.1C. +8.2z.1D. +9.c.38. +a.2F.1o. +a.2J.1e. +a.2P./. +a.2Y.H. +a.32.k. +d.2U.Q. +g.2E.1w. +0.+.2Q. +0.11.2O. +0.18.2L. +0.1d.2K. +0.1j.2I. +0.1n.2G. +0.1y.2D. +0.h.34. +0.w.30. +1.31.p. +1.33.i. +2.2H.1l. +3.2B.1C. +3.2x.1I. +3.2y.1E. +3.I.2Y. +4.12.2N. +4.14.2M. +4.1B.2C. +4.1K.2w. +8.2+.B. +8.2A.1D. +a.2F.1p. +a.2J.1f. +a.2P.10. +a.32.l. +d.2U.R. +g.2E.1x. +0./.2Q. +0.+.2R. +0.19.2L. +0.1C.2C. +0.1L.2w. +0.1o.2G. +0.1z.2D. +0.i.34. +0.x.30. +1.31.q. +1.33.j. +2.2H.1m. +3.2B.1D. +3.2x.1J. +3.2y.1F. +3.J.2Y. +4.12.2O. +4.15.2M. +8.2+.C. +8.2z.1E. +a.2F.1q. +a.2J.1g. +a.2P.11. +a.2V.N. +a.32.m. +d.2U.S. +g.2E.1y. +0.10.2Q. +0.16.2M. +0.1A.2D. +0.1M.2w. +0.1p.2G. +0.j.34. +0.y.30. +1.1V.2u. +1.33.k. +2.2/.A. +2.2H.1n. +3.2W.N. +3.2y.1G. +3.n.32. +4./.2R. +4.1a.2L. +4.1D.2C. +8.2+.D. +8.2A.1E. +8.2z.1F. +a.2F.1r. +a.2P.12. +a.2V.O. +a.2Y.K. +c.37.d. +d.2U.T. +g.2E.1z. +i.c.39. +0.+.2S. +0.11.2Q. +0.13.2N. +0.1b.2L. +0.1e.2K. +0.1q.2G. +0.k.34. +1.33.l. +2.1V.2v. +2.2/.B. +2.2H.1o. +3.2B.1E. +3.2W.O. +3.2y.1H. +4.10.2R. +4.17.2M. +4.1B.2D. +4.1k.2I. +4.1N.2w. +8.2+.E. +8.2A.1F. +8.2z.1G. +9.c.3a. +a.2F.1s. +a.2V.P. +a.2Y.L. +a.32.o. +c.37.e. +d.2U.U. +f.30.z. +g.2E.1A. +0./.2S. +0.13.2O. +0.1C.2D. +0.1c.2L. +0.1f.2K. +0.1O.2w. +0.1r.2G. +0.l.34. +1.31.r. +1.33.m. +2.2/.C. +2.2H.1p. +3.1l.2I. +3.2B.1F. +3.2W.P. +3.2x.1K. +3.2y.1I. +3.M.2Y. +4.11.2R. +4.12.2Q. +4.14.2N. +4.18.2M. +4.1E.2C. +4.c.3b. +8.2+.F. +8.2A.1G. +8.2z.1H. +9.k.35. +a.2F.1t. +a.2J.1h. +a.2V.Q. +a.32.p. +d.2U.V. +e.0.3h. +f.37.f. +g.2E.1B. +0.10.2S. +0.14.2O. +0.19.2M. +0.1D.2D. +0.1P.2w. +0.1s.2G. +0.m.34. +1.31.s. +1.33.n. +2.2/.D. +2.2H.1q. +3.1u.2F. +3.2B.1G. +3.2W.Q. +3.2x.1L. +3.2y.1J. +4.12.2R. +4.15.2N. +4.1F.2C. +4.1m.2I. +4.2K.1g. +6.3i.0. +8.2A.1H. +8.2z.1I. +9.c.3c. +9.k.36. +a.2J.1i. +a.2P.13. +a.2V.R. +a.32.q. +d.2U.W. +e.1.3h. +f.37.g. +g.2E.1C. +0.11.2S. +0.15.2O. +0.1d.2L. +0.1Q.2w. +0.1t.2G. +0.A.30. +0.d.39. +1.31.t. +1.33.o. +2.2/.E. +2.2H.1r. +3.2B.1H. +3.2W.R. +3.2x.1M. +3.n.34. +3.S.2V. +4.16.2N. +4.1a.2M. +4.1G.2C. +4.1n.2I. +6.3i.1. +8.2A.1I. +8.2z.1J. +9.c.3d. +a.2F.1v. +a.2J.1j. +a.2P.14. +c.37.h. +d.2U.X. +e.2.3h. +g.2E.1D. +0.13.2Q. +0.16.2O. +0.1E.2D. +0.1o.2I. +0.B.30. +0.e.39. +0.o.34. +1.31.u. +1.33.p. +2.2/.F. +2.2H.1s. +3.1u.2G. +3.2B.1I. +3.2x.1N. +3.S.2W. +4.12.2S. +4.17.2N. +4.1b.2M. +4.1H.2C. +6.3i.2. +8.2+.G. +8.2A.1J. +a.2F.1w. +a.2P.15. +a.2V.T. +a.2Y.N. +c.3f.a. +d.2U.Y. +e.3.3h. +f.37.i. +i.c.3e. +0.14.2Q. +0.17.2O. +0.1F.2D. +0.1h.2K. +0.1v.2G. +0.C.30. +0.f.39. +0.p.34. +1.31.v. +1.33.q. +2.2H.1t. +3.2B.1J. +3.2W.T. +3.2x.1O. +3.2y.1K. +4.13.2R. +4.18.2N. +4.1c.2M. +4.1I.2C. +4.1p.2I. +6.3i.3. +8.2+.H. +a.2F.1x. +a.2P.16. +a.2V.U. +a.2Y.O. +a.32.r. +d.2U.Z. +e.4.3h. +f.37.j. +g.2E.1E. +0.15.2Q. +0.18.2O. +0.19.2N. +0.1e.2L. +0.1G.2D. +0.1i.2K. +0.1J.2C. +0.1w.2G. +0.D.30. +0.g.39. +0.q.34. +1.31.w. +2.2H.1u. +3.2W.U. +3.2x.1P. +3.2y.1L. +4.14.2R. +4.1q.2I. +5./.2T. +8.2+.I. +8.2z.1K. +a.2F.1y. +a.2J.1k. +a.2P.17. +a.2V.V. +a.2Y.P. +a.32.s. +e.0.3m. +e.4.3i. +e.5.3h. +f.37.k. +g.2E.1F. +i.c.3f. +0.13.2S. +0.16.2Q. +0.19.2O. +0.1f.2L. +0.1H.2D. +0.1j.2K. +0.1x.2G. +0.E.30. +0.h.39. +1.31.x. +2.2/.G. +2.2H.1v. +3.1l.2J. +3.2W.V. +3.2x.1Q. +3.2y.1M. +4.15.2R. +4.1a.2N. +4.1d.2M. +4.1r.2I. +6.3i.5. +8.2+.J. +8.2A.1K. +8.2z.1L. +9.k.38. +a.2F.1z. +a.2P.18. +a.2V.W. +a.2Y.Q. +a.32.t. +e.1.3m. +e.6.3h. +f.1R.2w. +f.37.l. +g.2E.1G. +0.14.2S. +0.17.2Q. +0.1a.2O. +0.1I.2D. +0.1y.2G. +0.d.3e. +0.F.30. +0.i.39. +1.31.y. +1.33.r. +2.1.3n. +2.2/.H. +2.2H.1w. +3.2B.1K. +3.2W.W. +3.2y.1N. +4.16.2R. +4.1b.2N. +4.1s.2I. +4.1S.2w. +6.3i.6. +8.2+.K. +8.2A.1L. +8.2z.1M. +a.2F.1A. +a.2J.1m. +a.2P.19. +a.2V.X. +a.2Y.R. +a.32.u. +e.2.3m. +e.7.3h. +f.2L.1g. +f.37.m. +g.2E.1H. +0.15.2S. +0.18.2Q. +0.1b.2O. +0.1J.2D. +0.1z.2G. +0.e.3e. +0.j.39. +0.r.34. +1.31.z. +1.33.s. +2.2.3n. +2.2/.I. +2.2H.1x. +3.2B.1L. +3.2W.X. +3.2y.1O. +3.n.37. +3.S.2Y. +4.17.2R. +4.1c.2N. +4.1K.2C. +4.1t.2I. +8.2+.L. +8.2A.1M. +8.2z.1N. +a.2F.1B. +a.2J.1n. +a.2P.1a. +a.2V.Y. +a.32.v. +d.2U.+. +e.3.3m. +e.7.3i. +e.8.3h. +g.2E.1I. +0.16.2S. +0.19.2Q. +0.1A.2G. +0.1c.2O. +0.1k.2K. +0.1L.2C. +0.f.3e. +0.k.39. +0.s.34. +1.33.t. +2.2/.J. +2.2H.1y. +2.3.3n. +3.1u.2I. +3.2B.1M. +3.2W.Y. +3.2y.1P. +4.18.2R. +4.1e.2M. +4.r.35. +6.3i.8. +8.2+.M. +8.2A.1N. +8.2z.1O. +a.2F.1C. +a.2J.1o. +a.2P.1b. +a.2V.Z. +a.2Y.T. +a.32.w. +c.37.o. +c.3f.d. +d.2U./. +e.0.3q. +e.4.3m. +e.9.3h. +g.2E.1J. +i.c.3g. +0.17.2S. +0.1a.2Q. +0.1B.2G. +0.1h.2L. +0.1M.2C. +0.1V.2w. +0.G.30. +0.g.3e. +0.l.39. +0.t.34. +1.33.u. +2.2/.K. +2.2H.1z. +2.4.3n. +3.1l.2K. +3.2B.1N. +3.2W.Z. +3.2x.1R. +3.2y.1Q. +4.19.2R. +4.1d.2N. +4.1f.2M. +4.1v.2I. +4.k.3a. +8.2A.1O. +8.2z.1P. +9.r.36. +9.s.35. +a.2F.1D. +a.2J.1p. +a.2P.1c. +a.2Y.U. +a.32.x. +c.37.p. +c.3f.e. +d.2U.10. +e.0.3r. +e.1.3q. +e.5.3m. +e.9.3i. +e.a.3h. +0.18.2S. +0.1b.2Q. +0.1C.2G. +0.1d.2O. +0.1K.2D. +0.1m.2K. +0.H.30. +0.m.39. +0.u.34. +1.31.A. +1.33.v. +2.2/.L. +2.2H.1A. +2.5.3n. +3.2B.1O. +3.2x.1S. +4.1a.2R. +4.1i.2L. +4.1N.2C. +4.1w.2I. +4.h.3e. +6.3i.a. +8.2A.1P. +8.2z.1Q. +9.k.3b. +9.s.36. +a.2J.1q. +a.2Y.V. +a.32.y. +c.3f.f. +d.2U.11. +e.1.3r. +e.2.3q. +e.6.3m. +e.b.3h. +f.1W.2w. +f.2M.1g. +f.37.q. +0.19.2S. +0.1c.2Q. +0.1D.2G. +0.1j.2L. +0.1L.2D. +0.1n.2K. +0.1O.2C. +0.i.3e. +0.v.34. +1.31.B. +1.33.w. +2.2/.M. +2.2H.1B. +2.6.3n. +3.2B.1P. +3.I.30. +3.n.39. +4.1b.2R. +4.1x.2I. +4.u.35. +6.3i.b. +8.2+.N. +8.2A.1Q. +9.k.3c. +a.2F.1E. +a.2J.1r. +a.2P.1d. +a.2Y.W. +a.32.z. +c.3f.g. +d.2U.12. +e.2.3r. +e.3.3q. +e.7.3m. +g.2E.1K. +i.c.3h. +0.1a.2S. +0.1e.2N. +0.1M.2D. +0.1o.2K. +0.1P.2C. +0.d.3g. +0.j.3e. +0.o.39. +0.w.34. +1.31.C. +1.33.x. +2.2H.1C. +2.7.3n. +3.2B.1Q. +3.J.30. +4.1c.2R. +4.1X.2w. +4.1y.2I. +4.u.36. +8.2+.O. +9.k.3d. +a.2F.1F. +a.2J.1s. +a.2V.+. +a.2Y.X. +c.3f.h. +e.3.3r. +e.4.3q. +e.8.3m. +g.2E.1L. +i.c.3i. +0.1b.2S. +0.1d.2Q. +0.1E.2G. +0.1e.2O. +0.1p.2K. +0.e.3g. +0.k.3e. +0.p.39. +0.x.34. +1.31.D. +1.33.y. +2.2H.1D. +2.8.3n. +3.2W.+. +3.2x.1V. +3.2y.1R. +4.1f.2N. +4.1h.2M. +4.1N.2D. +4.1z.2I. +4.K.30. +5.c.3j. +8.2+.P. +a.2F.1G. +a.2J.1t. +a.2V./. +a.2Y.Y. +c.37.r. +c.3f.i. +e.4.3r. +e.5.3q. +e.9.3m. +f.1Q.2C. +g.2E.1M. +0.1c.2S. +0.1F.2G. +0.1f.2O. +0.1k.2L. +0.1O.2D. +0.1q.2K. +0.2N.1g. +0.f.3g. +0.L.30. +0.l.3e. +0.q.39. +0.y.34. +1.31.E. +1.33.z. +2.2/.N. +2.9.3n. +3.1u.2J. +3.2W./. +3.2x.1W. +3.2y.1S. +4.1A.2I. +4.1d.2R. +4.1i.2M. +4.3f.j. +8.2+.Q. +8.2z.1R. +9.c.3k. +9.r.38. +a.2F.1H. +a.2P.1e. +a.2V.10. +a.2Y.Z. +d.2U.13. +e.32.A. +e.5.3r. +e.6.3q. +e.a.3m. +f.37.s. +g.2E.1N. +0.1G.2G. +0.1j.2M. +0.1P.2D. +0.1r.2K. +0.2O.1g. +0.34.z. +0.g.3g. +0.m.3e. +1.31.F. +2.2/.O. +2.2H.1E. +2.a.3n. +3.1l.2L. +3.2W.10. +3.M.30. +4.1B.2I. +4.d.3h. +5./.2X. +5.c.3l. +8.2+.R. +8.2A.1R. +8.2z.1S. +9.s.38. +a.2F.1I. +a.2J.1v. +a.2P.1f. +a.2V.11. +a.32.B. +c.3f.k. +d.2U.14. +e.6.3r. +e.7.3q. +e.b.3m. +f.37.t. +g.2E.1O. +0.1d.2S. +0.1H.2G. +0.1m.2L. +0.1Q.2D. +0.1s.2K. +0.h.3g. +2.2/.P. +2.2H.1F. +2.b.3n. +3.2B.1R. +3.2W.11. +3.2x.1X. +3.n.3e. +4.1C.2I. +4.1e.2Q. +4.e.3h. +6.3i.d. +8.2+.S. +8.2A.1S. +a.2F.1J. +a.2J.1w. +a.2P.1g. +a.2V.12. +a.32.C. +c.3f.l. +d.2U.15. +e.7.3r. +e.8.3q. +f.37.u. +g.2E.1P. +i.c.3m. +0.1f.2Q. +0.1I.2G. +0.1n.2L. +0.1t.2K. +0.i.3g. +0.o.3e. +0.r.39. +1.33.A. +1.c.3n. +2.2/.Q. +2.2H.1G. +3.2B.1S. +3.2W.12. +3.2y.1V. +4.1D.2I. +4.1e.2R. +4.1h.2N. +4.f.3h. +6.3i.e. +8.2+.T. +9.u.38. +a.2J.1x. +a.32.D. +c.3f.m. +d.2U.16. +e.8.3r. +e.9.3q. +f.1R.2C. +f.37.v. +g.2E.1Q. +0.1h.2O. +0.1J.2G. +0.1o.2L. +0.A.34. +0.g.3h. +0.j.3g. +0.N.30. +0.p.3e. +0.s.39. +1.31.G. +1.33.B. +2.2/.R. +2.2H.1H. +3.1u.2K. +3.2y.1W. +3.n.3f. +4.1f.2R. +4.1i.2N. +4.1k.2M. +4.1S.2C. +4.r.3a. +5.c.3o. +6.3i.f. +8.2+.U. +8.2z.1V. +a.2J.1y. +a.2Y.+. +a.32.E. +c.37.w. +d.2U.17. +e.9.3r. +e.a.3q. +f.2Q.1g. +0.1e.2S. +0.1i.2O. +0.1j.2N. +0.1p.2L. +0.1v.2K. +0.B.34. +0.k.3g. +0.O.30. +0.q.3e. +0.t.39. +1.31.H. +1.33.C. +2.2/.S. +2.2H.1I. +2.A.35. +3.1l.2M. +4.1E.2I. +4.h.3h. +4.r.3b. +4.s.3a. +6.3i.g. +8.2+.V. +8.2A.1V. +8.2z.1W. +a.2F.1K. +a.2J.1z. +a.2P.1h. +a.2V.13. +a.2Y./. +a.32.F. +c.3f.o. +d.2U.18. +e.a.3r. +e.b.3q. +e.c.3p. +f.2R.1g. +f.37.x. +0.1f.2S. +0.1j.2O. +0.1q.2L. +0.1R.2D. +0.1w.2K. +0.C.34. +0.d.3m. +0.l.3g. +0.P.30. +0.u.39. +1.1+.2u. +1.31.I. +1.33.D. +2.2/.T. +2.2H.1J. +3.2B.1V. +3.2W.13. +3.2y.1X. +4.1F.2I. +4.1m.2M. +4.i.3h. +6.3i.h. +8.2+.W. +8.2A.1W. +9.r.3c. +9.s.3b. +a.2F.1L. +a.2J.1A. +a.2P.1i. +a.2V.14. +a.2Y.10. +c.3f.p. +d.2U.19. +e.b.3r. +f.37.y. +i.c.3q. +0.1h.2Q. +0.1K.2G. +0.1r.2L. +0.1x.2K. +0.2S.1g. +0.D.34. +0.e.3m. +0.m.3g. +0.Q.30. +0.v.39. +1.31.J. +1.33.E. +2.1+.2v. +2.2/.U. +3.2B.1W. +3.2W.14. +4.1G.2I. +4.1n.2M. +4.1S.2D. +4.j.3h. +4.r.3d. +4.u.3a. +6.3i.i. +8.2+.X. +8.2z.1X. +9.s.3c. +a.2F.1M. +a.2J.1B. +a.2P.1j. +a.2V.15. +a.2Y.11. +c.37.z. +c.3f.q. +d.2U.1a. +f.1V.2C. +g.2E.1R. +i.c.3r. +0.1k.2N. +0.1L.2G. +0.1o.2M. +0.1s.2L. +0.1W.2C. +0.1y.2K. +0.E.34. +0.f.3m. +0.R.30. +0.r.3e. +0.w.39. +1.31.K. +1.33.F. +1.c.3s. +2.2/.V. +3.2W.15. +3.n.3g. +4.1H.2I. +4.1h.2R. +4.1i.2Q. +4.k.3h. +4.s.3d. +4.u.3b. +6.3i.j. +8.2+.Y. +8.2A.1X. +9./.2Z. +a.2F.1N. +a.2J.1C. +a.2V.16. +a.2Y.12. +a.32.G. +d.2U.1b. +g.2E.1S. +0.1j.2Q. +0.1k.2O. +0.1M.2G. +0.1z.2K. +0.F.34. +0.g.3m. +0.l.3h. +0.o.3g. +0.s.3e. +0.x.39. +1.31.L. +2.2/.W. +2.2H.1K. +3.1l.2N. +3.2B.1X. +3.2W.16. +3.S.30. +4.1I.2I. +4.1i.2R. +4.1p.2M. +4.1t.2L. +6.3i.k. +8.2+.Z. +9.u.3c. +a.2F.1O. +a.2J.1D. +a.2V.17. +a.32.H. +d.2U.1c. +0.1A.2K. +0.1h.2S. +0.1j.2R. +0.1X.2C. +0.h.3m. +0.m.3h. +0.p.3g. +0.y.39. +1.22.2u. +1.31.M. +2.2/.X. +2.2H.1L. +3.1l.2O. +3.1u.2L. +3.2W.17. +3.I.32. +4.1J.2I. +4.1m.2N. +4.1N.2G. +4.1q.2M. +4.d.3q. +4.t.3e. +4.u.3d. +5.k.3j. +6.3i.l. +a.2F.1P. +a.2P.1k. +a.2V.18. +c.3f.r. +c.T.30. +f.1V.2D. +f.37.A. +0.1B.2K. +0.1m.2O. +0.1n.2N. +0.1O.2G. +0.1v.2L. +0.i.3m. +0.q.3g. +0.U.30. +1.33.G. +2.2/.Y. +2.22.2v. +2.2H.1M. +2.A.38. +3.1l.2P. +3.2W.18. +3.J.32. +3.n.3h. +4.1i.2S. +4.1r.2M. +4.d.3r. +4.e.3q. +4.u.3e. +6.3i.m. +9.c.3t. +9.k.3k. +a.2F.1Q. +a.2J.1E. +a.2V.19. +a.2Y.13. +c.37.B. +c.3f.s. +d.2U.1d. +e.39.z. +f.1W.2D. +g.2E.1V. +0.1C.2K. +0.1j.2S. +0.1k.2Q. +0.1n.2O. +0.1o.2N. +0.1P.2G. +0.1w.2L. +0.2w.1Y. +0.j.3m. +0.o.3h. +0.V.30. +1.24.2u. +1.33.H. +2.2/.Z. +2.2H.1N. +3.2W.19. +3.n.3i. +4.1s.2M. +4.e.3r. +4.f.3q. +4.G.34. +4.v.3e. +5.c.3u. +5.k.3l. +a.2J.1F. +a.2P.1m. +a.2V.1a. +a.2Y.14. +a.32.K. +c.37.C. +c.3f.t. +g.2E.1W. +0.1D.2K. +0.1o.2O. +0.1p.2N. +0.1Q.2G. +0.1X.2D. +0.1x.2L. +0.f.3r. +0.H.34. +0.k.3m. +0.p.3h. +0.W.30. +0.w.3e. +1.31.N. +1.33.I. +2.24.2v. +2.2H.1O. +3.1l.2Q. +3.2W.1a. +4.1K.2I. +4.1k.2R. +4.1t.2M. +4.g.3q. +6.3i.o. +8.2+.+. +a.2J.1G. +a.2P.1n. +a.2V.1b. +a.2Y.15. +a.32.L. +c.37.D. +c.3f.u. +i.c.3v. +0.1L.2I. +0.1m.2Q. +0.1p.2O. +0.1y.2L. +0.A.39. +0.g.3r. +0.l.3m. +0.q.3h. +0.r.3g. +0.X.30. +1.26.2u. +1.31.O. +1.33.J. +1.k.3n. +2.2H.1P. +3.1l.2R. +3.1u.2M. +3.2W.1b. +3.I.34. +3.M.32. +4.1q.2N. +4.h.3q. +4.x.3e. +6.3i.p. +8.2+./. +9.c.3w. +a.2J.1H. +a.2P.1o. +a.2V.1c. +a.2Y.16. +c.37.E. +c.3f.v. +d.2U.1e. +g.2E.1X. +0.1E.2K. +0.1k.2S. +0.1n.2Q. +0.1q.2O. +0.1z.2L. +0.B.39. +0.h.3r. +0.m.3m. +0.s.3g. +1.31.P. +1.33.K. +2.26.2v. +2.2H.1Q. +3.2W.1c. +3.J.34. +4.1M.2I. +4.1m.2R. +4.1r.2N. +4.1v.2M. +4.c.3x. +4.i.3q. +4.y.3e. +5.1h.2T. +5.k.3o. +6.3i.q. +8.2+.10. +a.2F.1R. +a.2J.1I. +a.2P.1p. +a.2Y.17. +c.3f.w. +c.Y.30. +d.2U.1f. +f.37.F. +0.1A.2L. +0.1F.2K. +0.1o.2Q. +0.1r.2O. +0.3e.z. +0.C.39. +0.K.34. +0.t.3g. +0.Z.30. +1.31.Q. +1.33.L. +2.2/.+. +3.1l.2S. +3.2x.1Y. +3.n.3m. +4.1N.2I. +4.1n.2R. +4.1s.2N. +4.1w.2M. +4.i.3r. +4.j.3q. +4.k.3p. +8.2+.11. +a.2F.1S. +a.2J.1J. +a.2P.1q. +a.2V.1d. +a.2Y.18. +c.3f.x. +d.2U.1g. +i.c.3y. +0.1B.2L. +0.1G.2K. +0.1m.2S. +0.1O.2I. +0.1o.2R. +0.1p.2Q. +0.1R.2G. +0.1s.2O. +0.1Z.2w. +0.D.39. +0.d.3v. +0.L.34. +0.o.3m. +0.r.3h. +0.u.3g. +1.31.R. +1.33.M. +2.2/./. +2.c.3z. +3.2W.1d. +4.1t.2N. +4.1x.2M. +4.j.3r. +4.k.3q. +8.2+.12. +a.2P.1r. +a.2Y.19. +a.32.N. +c.3f.y. +0.1+.2w. +0.1C.2L. +0.1H.2K. +0.1n.2S. +0.1P.2I. +0.1q.2Q. +0.1S.2G. +0.1t.2O. +0.E.39. +0.e.3v. +0.p.3m. +0.s.3h. +0.v.3g. +1.31.S. +2.2/.10. +3.1u.2N. +3.M.34. +4.1p.2R. +4.1y.2M. +4.k.3r. +4.l.3q. +6.3i.r. +9.c.3A. +a.2P.1s. +a.2Y.1a. +a.32.O. +c.37.G. +c.3f.z. +0.1/.2w. +0.1D.2L. +0.1I.2K. +0.1o.2S. +0.1r.2Q. +0.1v.2N. +0.A.3e. +0.F.39. +0.f.3v. +0.l.3r. +0.q.3m. +0.w.3g. +1.31.T. +1.k.3s. +2.2/.11. +2.2H.1R. +3.1u.2O. +4.1q.2R. +4.1z.2M. +4.m.3q. +4.t.3h. +6.3i.s. +7.r.3j. +a.2F.1V. +a.2J.1K. +a.2P.1t. +a.2V.1e. +a.2Y.1b. +a.32.P. +d.2U.1h. +f.1Q.2I. +f.37.H. +0.+.30. +0.1J.2K. +0.1p.2S. +0.1s.2Q. +0.1v.2O. +0.20.2w. +0.B.3e. +0.d.3y. +0.g.3v. +0.m.3r. +0.x.3g. +1.31.U. +1.33.N. +2.2/.12. +2.2H.1S. +3.1u.2P. +3.2W.1e. +3.2y.1Y. +3.I.37. +3.n.3q. +4.1A.2M. +4.1r.2R. +4.1w.2N. +4.u.3h. +5.c.3B. +6.3i.t. +7.s.3j. +8.2+.13. +9.r.3k. +a.2F.1W. +a.2J.1L. +a.2V.1f. +a.2Y.1c. +a.32.Q. +d.2U.1i. +0./.30. +0.1B.2M. +0.1E.2L. +0.1q.2S. +0.1V.2G. +0.1w.2O. +0.1x.2N. +0.C.3e. +0.e.3y. +0.h.3v. +0.N.34. +0.v.3h. +0.y.3g. +1.31.V. +1.33.O. +2.3.3H. +3.2W.1f. +3.2x.1Z. +3.J.37. +3.n.3r. +4.1s.2R. +4.1t.2Q. +4.o.3q. +5.c.3C. +6.3i.u. +7.r.3l. +8.2+.14. +8.2z.1Y. +9.s.3k. +a.2J.1M. +a.2P.1v. +a.2V.1g. +a.32.R. +c.3f.A. +d.2U.1j. +e.21.2w. +0.10.30. +0.1F.2L. +0.1r.2S. +0.1W.2G. +0.1x.2O. +0.1y.2N. +0.D.3e. +0.f.3y. +0.G.39. +0.i.3v. +0.O.34. +0.r.3m. +0.w.3h. +0.z.3g. +1.31.W. +1.33.P. +2.c.3D. +3.1u.2Q. +3.2W.1g. +3.2x.1+. +3.S.32. +4.1C.2M. +4.1t.2R. +4.k.3t. +4.o.3r. +4.p.3q. +6.3i.v. +7.s.3l. +7.u.3j. +8.2+.15. +8.2A.1Y. +a.2F.1X. +a.2J.1N. +a.2P.1w. +a.2Y.1d. +b.3G.8. +b.3J.2. +c.37.K. +c.3f.B. +f.22.2w. +0.11.30. +0.1G.2L. +0.1K.2K. +0.1s.2S. +0.1v.2Q. +0.1y.2O. +0.23.2w. +0.E.3e. +0.g.3y. +0.H.39. +0.j.3v. +0.P.34. +0.p.3r. +0.s.3m. +1.31.X. +1.33.Q. +1.r.3n. +2.2/.13. +2.2H.1V. +3.1u.2R. +3.2B.1Y. +3.2x.1/. +4.1D.2M. +4.1z.2N. +4.q.3q. +4.u.3k. +4.x.3h. +5.k.3u. +6.3i.w. +8.2+.16. +9.c.3E. +a.2J.1O. +a.2P.1x. +a.32.T. +c.37.L. +c.3f.C. +f.1R.2I. +0.1H.2L. +0.1L.2K. +0.1t.2S. +0.1w.2Q. +0.1X.2G. +0.1z.2O. +0.24.2w. +0.2C.1Y. +0.F.3e. +0.h.3y. +0.k.3v. +0.Q.34. +0.q.3r. +0.t.3m. +0.y.3h. +1.31.Y. +1.33.R. +1.s.3n. +2.2/.14. +2.2H.1W. +3.2x.20. +3.I.39. +3.M.37. +4.12.30. +4.1A.2N. +4.1S.2I. +4.1v.2R. +6.3i.x. +7.r.3o. +7.u.3l. +8.2+.17. +9.c.3F. +a.2J.1P. +a.2P.1y. +a.2V.1h. +a.32.U. +c.3f.D. +d.2U.1k. +0.1A.2O. +0.1B.2N. +0.1I.2L. +0.1M.2K. +0.1x.2Q. +0.25.2w. +0.A.3g. +0.i.3y. +0.l.3v. +0.R.34. +0.u.3m. +1.31.Z. +1.33.S. +2.2/.15. +3.1u.2S. +3.2W.1h. +3.2x.21. +3.2y.1Z. +3.J.39. +4.1E.2M. +4.1w.2R. +4.k.3w. +4.r.3p. +4.s.3o. +6.3i.y. +6.3L.3. +8.2+.18. +a.2J.1Q. +a.2P.1z. +a.2V.1i. +a.2Y.1e. +a.32.V. +c.3f.E. +d.2U.1l. +f.3h.z. +0.1B.2O. +0.1J.2L. +0.1v.2S. +0.1y.2Q. +0.26.2w. +0.B.3g. +0.j.3y. +0.K.39. +0.m.3v. +0.r.3q. +0.v.3m. +1.33.T. +1.u.3n. +2.2/.16. +2.2H.1X. +3.2W.1i. +3.2x.22. +3.2y.1+. +3.S.34. +4.1C.2N. +4.1F.2M. +4.1N.2K. +4.1x.2R. +4.k.3x. +5.1h.2X. +6.3i.z. +8.2+.19. +8.2z.1Z. +9.s.3p. +a.2P.1A. +a.2V.1j. +a.2Y.1f. +a.32.W. +b.3G.c. +c.3f.F. +d.2U.1m. +0.13.30. +0.1C.2O. +0.1O.2K. +0.1V.2I. +0.1w.2S. +0.1z.2Q. +0.27.2w. +0.2D.1Y. +0.C.3g. +0.k.3y. +0.L.39. +0.r.3r. +0.w.3m. +1.33.U. +2.2/.17. +3.2W.1j. +3.2x.23. +3.2y.1/. +3.n.3v. +4.1D.2N. +4.1G.2M. +4.1y.2R. +4.G.3e. +4.s.3q. +4.u.3o. +8.2+.1a. +8.2A.1Z. +8.2z.1+. +a.2P.1B. +a.2Y.1g. +a.32.X. +c.37.N. +c.T.34. +d.2U.1n. +0.14.30. +0.1A.2Q. +0.1D.2O. +0.1P.2K. +0.1x.2S. +0.D.3g. +0.H.3e. +0.l.3y. +0.o.3v. +0.s.3r. +0.U.34. +0.x.3m. +1.33.V. +1.r.3s. +2.2/.18. +2.k.3z. +3.2B.1Z. +3.2x.24. +3.2y.20. +3.M.39. +4.1H.2M. +4.1z.2R. +4.A.3h. +4.t.3q. +8.2+.1b. +8.2A.1+. +8.2z.1/. +9.u.3p. +a.2P.1C. +a.32.Y. +c.37.O. +d.2U.1o. +f.1W.2I. +g.2E.1Y. +0.15.30. +0.1B.2Q. +0.1Q.2K. +0.1y.2S. +0.B.3h. +0.E.3g. +0.m.3y. +0.p.3v. +0.t.3r. +0.V.34. +0.y.3m. +1.31.+. +1.33.W. +1.s.3s. +2.2/.19. +3.2B.1+. +3.2x.25. +3.2y.21. +3.I.3e. +4.1A.2R. +4.1E.2N. +4.1I.2M. +4.1K.2L. +4.k.3A. +4.u.3q. +5.1t.2T. +6.3i.A. +8.2+.1c. +8.2A.1/. +8.2z.20. +a.2J.1R. +a.2P.1D. +a.2V.1k. +a.32.Z. +c.28.2w. +c.37.P. +c.3f.G. +d.2U.1p. +f.1Z.2C. +0.16.30. +0.1C.2Q. +0.1E.2O. +0.1F.2N. +0.1L.2L. +0.1X.2I. +0.1z.2S. +0.F.3g. +0.q.3v. +0.u.3r. +0.W.34. +1.31./. +1.33.X. +2.2/.1a. +3.1l.2V. +3.2B.1/. +3.2W.1k. +3.2x.26. +3.2y.22. +3.J.3e. +3.n.3y. +4.1B.2R. +4.1J.2M. +4.3f.H. +4.C.3h. +4.v.3q. +5.c.3H. +6.3i.B. +6.3m.z. +8.2A.20. +8.2z.21. +a.2J.1S. +a.2Y.1h. +b.3G.d. +c.37.Q. +d.2U.1q. +f.1+.2C. +0.1/.2C. +0.17.30. +0.1A.2S. +0.1D.2Q. +0.1F.2O. +0.1G.2N. +0.1M.2L. +0.D.3h. +0.K.3e. +0.N.39. +0.o.3y. +0.v.3r. +0.X.34. +1.31.10. +1.33.Y. +1.u.3s. +2.2/.1b. +3.1l.2W. +3.2B.20. +3.2x.27. +3.2y.23. +3.I.3f. +4.1C.2R. +4.r.3t. +4.w.3q. +5.c.3I. +5.k.3B. +6.3i.C. +8.2+.1d. +8.2A.21. +8.2z.22. +a.2P.1E. +a.2V.1m. +a.2Y.1i. +b.3G.e. +c.37.R. +d.2U.1r. +0.18.30. +0.1B.2S. +0.1G.2O. +0.1Z.2D. +0.L.3e. +0.O.39. +0.p.3y. +0.w.3r. +1.31.11. +1.33.Z. +2.2/.1c. +3.2B.21. +3.2W.1m. +3.2y.24. +3.J.3f. +3.S.37. +4.1D.2R. +4.1H.2N. +4.1N.2L. +4.E.3h. +4.s.3t. +4.x.3q. +5.k.3C. +6.3i.D. +7.r.3u. +8.2A.22. +8.2z.23. +a.2P.1F. +a.2V.1n. +a.2Y.1j. +b.3G.f. +b.3J.c. +b.3R.2. +c.Y.34. +d.2U.1s. +f.20.2C. +0.1+.2D. +0.19.30. +0.1C.2S. +0.1E.2Q. +0.1H.2O. +0.1O.2L. +0.1R.2K. +0.A.3m. +0.G.3g. +0.P.39. +0.q.3y. +0.r.3v. +0.x.3r. +1.29.2u. +1.31.12. +2.k.3D. +3.2B.22. +3.2W.1n. +3.2x.28. +3.2y.25. +3.3K.c. +3.M.3e. +4.1I.2N. +4.1K.2M. +4.3f.K. +4.F.3h. +4.y.3q. +4.Z.34. +5.1h.2Z. +6.3i.E. +7.s.3u. +8.2A.23. +8.2z.24. +a.2J.1V. +a.2P.1G. +a.2V.1o. +a.32.+. +b.3G.g. +c.37.T. +d.2U.1t. +g.2E.1Z. +j.21.2C. +0.1/.2D. +0.1D.2S. +0.1F.2Q. +0.1I.2O. +0.1J.2N. +0.1P.2L. +0.1S.2K. +0.3q.z. +0.B.3m. +0.H.3g. +0.Q.39. +0.s.3v. +0.y.3r. +2.2/.1d. +2.29.2v. +3.2B.23. +3.2W.1o. +3.2y.26. +4.1a.30. +4.1E.2R. +4.1L.2M. +4.u.3t. +6.3i.F. +6.3L.c. +8.2+.1e. +8.2A.24. +8.2z.25. +9.k.3E. +9.r.3w. +a.2J.1W. +a.2P.1H. +a.2V.1p. +a.32./. +b.3G.h. +c.37.U. +c.3f.L. +d.2U.1u. +f.22.2C. +g.2E.1+. +0.1b.30. +0.1G.2Q. +0.1J.2O. +0.23.2C. +0.C.3m. +0.R.39. +0.t.3v. +3.2B.24. +3.2W.1p. +3.2y.27. +3.3M.c. +3.I.3g. +3.M.3f. +4.1F.2R. +4.1M.2M. +4.s.3w. +4.u.3u. +6.3r.z. +8.2+.1f. +8.2A.25. +8.2z.26. +9.k.3F. +9.r.3x. +a.2P.1I. +a.2V.1q. +a.2Y.1k. +a.32.10. +b.3G.i. +c.37.V. +d.2U.1v. +f.1Q.2L. +f.20.2D. +g.2E.1/. +0.1c.30. +0.1E.2S. +0.1H.2Q. +0.24.2C. +0.D.3m. +0.N.3e. +0.r.3y. +0.u.3v. +1.31.13. +1.33.+. +3.1l.2Y. +3.2B.25. +3.2W.1q. +3.J.3g. +3.S.39. +4.1G.2R. +4.1N.2M. +4.G.3h. +4.s.3x. +8.2+.1g. +8.2A.26. +8.2z.27. +a.2F.1Y. +a.2J.1X. +a.2P.1J. +a.2V.1r. +a.32.11. +b.3G.j. +b.3J.d. +c.37.W. +d.2U.1w. +g.2E.20. +i.c.3N. +j.21.2D. +0.+.34. +0.1F.2S. +0.22.2D. +0.25.2C. +0.E.3m. +0.H.3h. +0.K.3g. +0.O.3e. +0.s.3y. +0.v.3v. +1.31.14. +1.33./. +2.2/.1e. +2.r.3z. +3.2B.26. +3.2W.1r. +3.2y.28. +3.3K.d. +4.1H.2R. +4.1I.2Q. +4.1K.2N. +4.1O.2M. +4.A.3q. +4.u.3w. +5.c.3O. +6.3i.G. +8.2A.27. +a.2V.1s. +a.2Y.1m. +a.32.12. +b.3G.k. +b.3J.e. +c.37.X. +c.T.39. +d.2U.1x. +f.1V.2K. +g.2E.21. +0./.34. +0.1d.30. +0.1G.2S. +0.1J.2Q. +0.1K.2O. +0.1L.2N. +0.1W.2K. +0.26.2C. +0.2G.1Y. +0.A.3r. +0.F.3m. +0.L.3g. +0.P.3e. +0.t.3y. +0.U.39. +0.w.3v. +1.31.15. +1.33.10. +2.2/.1f. +2.c.3P. +2.s.3z. +3.2B.27. +3.2W.1s. +3.3K.e. +3.I.3h. +4.1I.2R. +4.B.3q. +4.r.3A. +4.u.3x. +6.3i.H. +6.3L.d. +8.2z.28. +a.2V.1t. +a.2Y.1n. +b.3G.l. +b.3J.f. +c.37.Y. +c.3f.N. +d.2U.1y. +f.1P.2M. +f.23.2D. +g.2E.22. +0.10.34. +0.1H.2S. +0.1L.2O. +0.24.2D. +0.27.2C. +0.B.3r. +0.Q.3e. +0.u.3y. +0.V.39. +0.x.3v. +1.31.16. +1.33.11. +2.2/.1g. +3.1u.2V. +3.2W.1t. +3.3K.f. +3.3M.d. +3.I.3i. +3.J.3h. +3.M.3g. +4.1J.2R. +4.1M.2N. +4.C.3q. +6.3L.e. +8.2+.1h. +8.2A.28. +9./.35. +9.s.3A. +a.2P.1K. +a.2Y.1o. +b.3G.m. +b.3J.g. +c.37.Z. +c.3f.O. +d.2U.1z. +f.1Q.2M. +f.1R.2L. +g.2E.23. +0.11.34. +0.1I.2S. +0.1M.2O. +0.1X.2K. +0.d.3N. +0.K.3h. +0.R.3e. +0.v.3y. +0.W.39. +0.y.3v. +1.31.17. +1.33.12. +2.2H.1Y. +2.u.3z. +3.1u.2W. +3.2B.28. +3.3K.g. +3.3M.e. +3.J.3i. +4.1N.2N. +4.1S.2L. +4.C.3r. +4.D.3q. +5.1t.2X. +5.c.3Q. +6.3L.f. +7.r.3B. +8.2+.1i. +9./.36. +a.2P.1L. +a.2V.1v. +a.2Y.1p. +a.32.13. +b.3G.n. +b.3J.h. +c.3f.P. +d.2U.1A. +f.25.2D. +g.2E.24. +0.1e.30. +0.1J.2S. +0.1K.2Q. +0.1O.2N. +0.26.2D. +0.e.3N. +0.G.3m. +0.L.3h. +0.w.3y. +0.X.39. +0.z.3v. +1.31.18. +3.2W.1v. +3.3K.h. +3.3M.f. +3.S.3e. +4.12.34. +4.1N.2O. +4.D.3r. +4.E.3q. +5.k.3H. +5.r.3C. +6.3i.K. +6.3L.g. +7.s.3B. +8.2+.1j. +9.u.3A. +a.2F.1Z. +a.2P.1M. +a.2V.1w. +a.2Y.1q. +a.32.14. +b.3G.o. +b.3J.i. +c.28.2C. +c.3f.Q. +d.2U.1B. +g.2E.25. +0.1f.30. +0.1L.2Q. +0.1O.2O. +0.1P.2N. +0.27.2D. +0.f.3N. +0.H.3m. +0.N.3g. +0.x.3y. +1.31.19. +2.2/.1h. +2.r.3D. +3.2W.1w. +3.3K.i. +3.3M.g. +3.M.3h. +4.1K.2R. +4.E.3r. +4.F.3q. +5.k.3I. +5.s.3C. +6.3i.L. +6.3L.h. +a.2F.1+. +a.2P.1N. +a.2V.1x. +a.2Y.1r. +a.32.15. +b.3G.p. +b.3J.j. +b.3R.c. +c.3f.R. +c.T.3e. +c.Y.39. +d.2U.1C. +g.2E.26. +0.1M.2Q. +0.1P.2O. +0.1Q.2N. +0.1V.2L. +0.1Z.2G. +0.g.3N. +0.O.3g. +0.U.3e. +0.y.3y. +0.Z.39. +1.31.1a. +1.33.13. +2.2/.1i. +2.s.3D. +3.2W.1x. +3.3K.j. +3.3M.h. +3.I.3m. +3.M.3i. +3.S.3f. +4.1L.2R. +4.F.3r. +6.3L.i. +6.3S.c. +7.u.3B. +9.r.3E. +a.2F.1/. +a.2P.1O. +a.2V.1y. +a.2Y.1s. +a.32.16. +b.3G.q. +b.3J.k. +c.37.+. +d.2U.1D. +f.1R.2M. +f.30.1g. +g.2E.27. +0.1+.2G. +0.13.34. +0.1K.2S. +0.1Q.2O. +0.1W.2L. +0.2I.1Y. +0.A.3v. +0.h.3N. +0.P.3g. +0.V.3e. +1.2f.2u. +1.31.1b. +1.33.14. +2.2.3+. +2.2/.1j. +3.2W.1y. +3.3K.k. +3.3M.i. +3.J.3m. +4.1M.2R. +4.1N.2Q. +4.1S.2M. +5.c.3T. +5.u.3C. +6.3L.j. +6.3y.z. +8.2+.1k. +9.r.3F. +9.s.3E. +a.2F.20. +a.2P.1P. +a.2V.1z. +a.2Y.1t. +a.32.17. +b.3J.l. +c.28.2D. +c.37./. +c.3f.T. +0.1/.2G. +0.14.34. +0.1L.2S. +0.1O.2Q. +0.29.2w. +0.B.3v. +0.i.3N. +0.K.3m. +0.Q.3g. +0.W.3e. +1.31.1c. +1.33.15. +2.0.41. +2.2f.2v. +2.2H.1Z. +2.A.3w. +2.u.3D. +3.1u.2Y. +3.2W.1z. +3.3K.l. +3.3M.j. +4.1N.2R. +4.G.3q. +4.N.3h. +5.c.3U. +6.3L.k. +8.2+.1l. +9./.38. +9.s.3F. +a.2F.21. +a.2P.1Q. +a.2V.1A. +a.32.18. +b.3J.m. +c.37.10. +c.3f.U. +d.2U.1E. +g.2E.28. +0.15.34. +0.1h.30. +0.1M.2S. +0.1O.2R. +0.1P.2Q. +0.1X.2L. +0.20.2G. +0.C.3v. +0.G.3r. +0.j.3N. +0.L.3m. +0.R.3g. +0.X.3e. +1.33.16. +2.1.41. +2.2H.1+. +2.A.3x. +3.2W.1A. +3.3K.m. +3.3M.k. +4.H.3q. +4.O.3h. +5.2a.2w. +5.c.3V. +6.3i.N. +6.3L.l. +8.2+.1m. +9.u.3E. +a.2F.22. +a.2V.1B. +a.2Y.1v. +a.32.19. +b.3G.r. +b.3J.n. +b.3R.d. +c.37.11. +c.3f.V. +d.2U.1F. +0.+.39. +0.16.34. +0.1i.30. +0.1P.2R. +0.1V.2M. +0.A.3y. +0.D.3v. +0.H.3r. +0.k.3N. +0.P.3h. +1.31.1d. +1.33.17. +2.2.41. +2.2/.1k. +2.2H.1/. +3.2W.1B. +3.3K.n. +3.3M.l. +3.I.3q. +3.M.3m. +3.S.3g. +4.1N.2S. +5.c.3W. +6.3i.O. +6.3L.m. +6.3S.d. +8.2+.1n. +9.1t.2Z. +9.u.3F. +a.2F.23. +a.2V.1C. +a.2Y.1w. +a.32.1a. +b.3G.s. +b.3J.o. +b.3R.e. +c.37.12. +c.3f.W. +c.Y.3e. +d.2U.1G. +f.1Q.2Q. +f.1R.2N. +j.21.2G. +0./.39. +0.17.34. +0.1j.30. +0.1O.2S. +0.1Q.2R. +0.1W.2M. +0.22.2G. +0.B.3y. +0.E.3v. +0.l.3N. +0.Q.3h. +1.33.18. +2.2/.1l. +2.2H.20. +2.6.3+. +2.A.3z. +3.2W.1C. +3.3K.o. +3.3M.m. +3.I.3r. +3.J.3q. +3.n.3L. +4.1S.2N. +4.Z.3e. +5.c.3X. +5.k.3O. +6.3i.P. +6.3S.e. +8.2+.1o. +a.2F.24. +a.2V.1D. +a.2Y.1x. +a.32.1b. +b.3G.t. +b.3J.p. +b.3R.f. +c.3f.X. +c.T.3g. +d.2U.1H. +f.1R.2O. +0.10.39. +0.18.34. +0.1P.2S. +0.1S.2O. +0.23.2G. +0.C.3y. +0.F.3v. +0.m.3N. +0.R.3h. +0.U.3g. +1.33.19. +2.2/.1m. +2.2H.21. +2.A.3A. +2.k.3P. +3.2W.1D. +3.2x.29. +3.3K.p. +3.3M.n. +3.J.3r. +4./.3a. +4.K.3q. +6.3i.Q. +6.3L.o. +6.3S.f. +8.2+.1p. +a.2F.25. +a.2P.1R. +a.2Y.1y. +a.32.1c. +b.3G.u. +b.3J.q. +b.3R.g. +c.3f.Y. +d.2U.1I. +f.1Z.2I. +f.2c.2w. +0.11.39. +0.19.34. +0.1Q.2S. +0.1X.2M. +0.24.2G. +0.D.3y. +0.K.3r. +0.L.3q. +0.N.3m. +0.V.3g. +1.31.1e. +1.33.1a. +2.2/.1n. +2.2H.22. +3.2x.2a. +3.3K.q. +3.3M.o. +3.n.3N. +3.S.3h. +4./.3b. +6.3i.R. +6.3L.p. +6.3S.g. +7.r.3H. +8.2+.1q. +a.2F.26. +a.2J.1Y. +a.2P.1S. +a.2V.1E. +a.2Y.1z. +b.3G.v. +b.3R.h. +c.37.13. +c.3f.Z. +d.2U.1J. +f.1+.2I. +0.1a.34. +0.1k.30. +0.1V.2N. +0.25.2G. +0.E.3y. +0.L.3r. +0.O.3m. +0.o.3N. +0.W.3g. +1.31.1f. +1.33.1b. +2.2/.1o. +2.2H.23. +3.2W.1E. +3.3M.p. +3.M.3q. +3.S.3i. +4.12.39. +4.s.3H. +5.k.3Q. +6.3L.q. +6.3S.h. +7.r.3I. +8.2+.1r. +9./.3c. +a.2F.27. +a.2V.1F. +a.2Y.1A. +a.32.1d. +b.3G.w. +b.3R.i. +c.37.14. +c.T.3h. +f.1/.2I. +f.1R.2Q. +i.c.3Y. +0.+.3e. +0.1b.34. +0.1V.2O. +0.26.2G. +0.2w.2d. +0.F.3y. +0.G.3v. +0.P.3m. +0.p.3N. +0.U.3h. +0.X.3g. +1.31.1g. +1.33.1c. +2.2/.1p. +2.2H.24. +3.1l.30. +3.2W.1F. +3.3M.q. +3.M.3r. +4./.3d. +4.1S.2Q. +6.3i.T. +6.3S.i. +7.s.3I. +8.2+.1s. +a.2V.1G. +a.2Y.1B. +b.3G.x. +b.3J.r. +b.3R.j. +c.37.15. +f.1R.2R. +f.1W.2N. +f.20.2I. +0./.3e. +0.1c.34. +0.1m.30. +0.1W.2O. +0.27.2G. +0.H.3v. +0.Q.3m. +0.q.3N. +0.V.3h. +2.2/.1q. +2.2H.25. +2.b.3+. +3.2W.1G. +3.2x.2c. +3.2y.29. +3.3K.r. +4.1S.2R. +5.c.3Z. +6.3i.U. +6.3S.j. +7.u.3H. +8.2+.1t. +a.2F.28. +a.2P.1V. +a.2V.1H. +a.2Y.1C. +b.3G.y. +b.3J.s. +b.3R.k. +c.37.16. +c.Y.3g. +d.2U.1K. +j.21.2I. +0.10.3e. +0.13.39. +0.1n.30. +0.1X.2N. +0.2K.1Y. +0.R.3m. +0.W.3h. +0.Z.3g. +1.33.1d. +2.2/.1r. +2.2H.26. +3.2W.1H. +3.2y.2a. +3.3K.s. +3.I.3v. +4.N.3q. +5.c.3+. +6.3i.V. +6.3L.r. +6.3S.k. +7.u.3I. +8.2+.1u. +8.2z.29. +a.2P.1W. +a.2V.1I. +a.2Y.1D. +a.32.1e. +b.3G.z. +b.3J.t. +b.3R.l. +c.37.17. +c.3f.+. +d.2U.1L. +f.1R.2S. +f.22.2I. +0.11.3e. +0.14.39. +0.1d.34. +0.1o.30. +0.1S.2S. +0.1V.2Q. +0.1X.2O. +0.d.3Y. +0.G.3y. +0.X.3h. +1.31.1h. +2.2/.1s. +2.2H.27. +3.2W.1I. +3.3K.t. +3.3M.r. +3.J.3v. +3.S.3m. +4.N.3r. +4.O.3q. +5.c.3/. +5.k.3T. +6.3i.W. +6.3L.s. +6.3S.l. +8.2+.1v. +8.2A.29. +8.2z.2a. +a.2J.1Z. +a.2V.1J. +a.32.1f. +b.3J.u. +b.3R.m. +c.28.2G. +c.37.18. +c.3f./. +d.2U.1M. +f.23.2I. +0.12.3e. +0.15.39. +0.1p.30. +0.1W.2Q. +0.24.2I. +0.e.3Y. +0.H.3y. +0.K.3v. +0.r.3N. +1.31.1i. +2.1d.35. +2.2/.1t. +3.2B.29. +3.2W.1J. +3.2x.2d. +3.3K.u. +3.3M.s. +4.O.3r. +4.P.3q. +5.k.3U. +6.3i.X. +6.3L.t. +6.3S.m. +8.2+.1w. +8.2A.2a. +a.2J.1+. +a.2P.1X. +a.2Y.1E. +a.32.1g. +b.3J.v. +b.3R.n. +c.37.19. +c.3f.10. +c.Y.3h. +d.2U.1N. +f.1V.2R. +i.c.40. +0.16.39. +0.1q.30. +0.1W.2R. +0.25.2I. +0.f.3Y. +0.L.3v. +0.P.3r. +0.s.3N. +0.U.3m. +1.31.1j. +1.33.1e. +2.2/.1u. +2.2H.28. +3.2B.2a. +3.2y.2c. +3.3K.v. +3.3M.t. +3.I.3y. +3.n.3S. +4.Q.3q. +4.Z.3h. +5.c.41. +5.k.3V. +6.3i.Y. +6.3L.u. +7.r.3O. +8.2+.1x. +a.2J.1/. +a.2Y.1F. +b.3G.A. +b.3J.w. +b.3R.o. +c.3f.11. +d.2U.1O. +f.29.2C. +f.37.1a. +0.+.3g. +0.17.39. +0.1e.34. +0.1r.30. +0.1V.2S. +0.1X.2Q. +0.26.2I. +0.g.3Y. +0.Q.3r. +0.t.3N. +0.V.3m. +1.33.1f. +2.2/.1v. +2.r.3P. +3.3K.w. +3.3M.u. +3.J.3y. +3.M.3v. +4.R.3q. +4.s.3O. +5.2a.2C. +5.c.42. +5.k.3W. +6.3i.Z. +6.3L.v. +6.3S.o. +8.2+.1y. +8.2z.2c. +a.2J.20. +a.2V.1K. +a.2Y.1G. +b.3G.B. +b.3J.x. +b.3R.p. +c.37.1b. +c.3f.12. +d.2U.1P. +0./.3g. +0.13.3e. +0.18.39. +0.1f.34. +0.1s.30. +0.1W.2S. +0.1X.2R. +0.1Z.2K. +0.h.3Y. +0.K.3y. +0.R.3r. +0.u.3N. +0.W.3m. +1.33.1g. +1.c.43. +2.2/.1w. +2.s.3P. +3.2W.1K. +3.3K.x. +3.3M.v. +3.S.3q. +5.k.3X. +6.3L.w. +6.3S.p. +8.2+.1z. +8.2A.2c. +a.2J.21. +a.2V.1L. +a.2Y.1H. +a.32.1h. +b.3G.C. +b.3J.y. +b.3R.q. +c.37.1c. +d.2U.1Q. +f.27.2I. +0.1+.2K. +0.10.3g. +0.14.3e. +0.19.39. +0.1t.30. +0.29.2D. +0.2L.1Y. +0.34.1g. +0.d.40. +0.i.3Y. +0.L.3y. +0.v.3N. +0.X.3m. +1.31.1k. +2.2/.1x. +3.2B.2c. +3.2W.1L. +3.2y.2d. +3.3K.y. +3.3M.w. +4.S.3r. +5.c.44. +6.3L.x. +6.3S.q. +7.r.3Q. +7.u.3O. +8.2+.1A. +a.2J.22. +a.2V.1M. +a.2Y.1I. +a.32.1i. +b.3G.D. +b.3J.z. +c.T.3q. +0.+.3h. +0.1/.2K. +0.11.3g. +0.15.3e. +0.1X.2S. +0.e.40. +0.j.3Y. +0.N.3v. +0.w.3N. +1.31.1l. +2.2/.1y. +2.u.3P. +3.1u.30. +3.2W.1M. +3.3K.z. +3.3M.x. +3.M.3y. +4.1a.39. +4.U.3q. +5.2a.2D. +5.c.45. +6.3L.y. +7.s.3Q. +8.2+.1B. +8.2z.2d. +a.2J.23. +a.2V.1N. +a.2Y.1J. +a.32.1j. +b.3G.E. +c.28.2I. +c.37.1d. +c.3f.13. +c.T.3r. +c.Y.3m. +f.2c.2C. +g.2E.29. +0./.3h. +0.12.3g. +0.16.3e. +0.1b.39. +0.1v.30. +0.20.2K. +0.f.40. +0.k.3Y. +0.O.3v. +0.x.3N. +0.Z.3m. +1.31.1m. +1.33.1h. +2.1d.38. +2.2/.1z. +2.A.3I. +3.2W.1N. +3.3M.y. +4.U.3r. +4.V.3q. +5.c.46. +6.3i.+. +6.3L.z. +8.2+.1C. +8.2A.2d. +a.2J.24. +a.2V.1O. +b.3G.F. +b.3R.r. +c.3f.14. +g.2E.2a. +0.17.3e. +0.1c.39. +0.1h.34. +0.1w.30. +0.g.40. +0.l.3Y. +0.P.3v. +0.V.3r. +0.y.3N. +1.31.1n. +1.33.1i. +2.2/.1A. +3.2B.2d. +3.2W.1O. +3.3M.z. +4.10.3h. +4.W.3q. +5.1V.2T. +5.c.47. +6.3i./. +6.3R.s. +6.3S.r. +7.u.3Q. +8.2+.1D. +a.2J.25. +a.2V.1P. +b.3J.A. +c.3f.15. +d.2U.1R. +j.21.2K. +0.18.3e. +0.1i.34. +0.1x.30. +0.22.2K. +0.2C.2d. +0.2M.1Y. +0.h.40. +0.m.3Y. +0.N.3y. +0.Q.3v. +0.W.3r. +1.31.1o. +1.33.1j. +2.2/.1B. +3.2W.1P. +3.3K.A. +4.11.3h. +4.X.3q. +5./.3j. +5.1h.35. +5.c.48. +5.k.3Z. +6.3i.10. +6.3N.z. +6.3R.t. +6.3S.s. +7.r.3T. +a.2J.26. +a.2V.1Q. +a.2Y.1K. +a.32.1k. +b.3J.B. +c.3f.16. +d.2U.1S. +f.2c.2D. +f.37.1e. +0.12.3h. +0.13.3g. +0.19.3e. +0.1d.39. +0.1j.34. +0.1y.30. +0.1Z.2L. +0.23.2K. +0.i.40. +0.O.3y. +0.R.3v. +0.X.3r. +1.31.1p. +2.2/.1C. +3.1l.32. +3.2W.1Q. +3.3K.B. +3.n.3Y. +4.s.3T. +5.1h.36. +5.c.49. +5.k.3+. +6.3i.11. +6.3L.A. +6.3S.t. +7.r.3U. +8.2+.1E. +9./.3k. +a.2J.27. +a.2Y.1L. +b.3G.G. +b.3J.C. +b.3R.u. +c.3f.17. +c.Y.3q. +f.37.1f. +g.2E.2c. +0.+.3m. +0.1+.2L. +0.14.3g. +0.1a.3e. +0.1z.30. +0.24.2K. +0.j.40. +0.o.3Y. +0.P.3y. +1.31.1q. +1.c.4a. +2.2/.1D. +3.3K.C. +3.3M.A. +3.S.3v. +4.r.3V. +4.s.3U. +4.Z.3q. +5./.3l. +5.k.3/. +6.3i.12. +6.3L.B. +6.3S.u. +8.2+.1F. +a.2Y.1M. +a.32.1m. +b.3G.H. +b.3J.D. +b.3R.v. +c.37.1g. +c.3f.18. +c.Y.3r. +0./.3m. +0.1/.2L. +0.15.3g. +0.1A.30. +0.1b.3e. +0.25.2K. +0.A.3N. +0.k.40. +0.p.3Y. +0.Q.3y. +0.Z.3r. +1.31.1r. +1.33.1k. +1.c.4b. +3.3K.D. +3.3M.B. +4.2d.2D. +4.3f.19. +4.r.3W. +4.s.3V. +4.u.3T. +6.3L.C. +6.3S.v. +8.2+.1G. +a.2J.28. +a.2Y.1N. +a.32.1n. +b.3G.I. +b.3J.E. +b.3R.w. +c.T.3v. +d.2U.1V. +0.10.3m. +0.13.3h. +0.16.3g. +0.1B.30. +0.1c.3e. +0.1e.39. +0.1k.34. +0.20.2L. +0.26.2K. +0.2N.1Y. +0.B.3N. +0.l.40. +0.q.3Y. +0.R.3y. +0.U.3v. +1./.3n. +1.31.1s. +1.33.1l. +2.2/.1E. +2.A.3O. +3.3K.E. +3.3M.C. +5.k.41. +6.3L.D. +6.3S.w. +7.r.3X. +7.s.3W. +7.u.3U. +8.2+.1H. +a.2V.1R. +a.2Y.1O. +a.32.1o. +b.3G.J. +b.3J.F. +b.3R.x. +c.3f.1a. +d.2U.1W. +g.2E.2d. +0.11.3m. +0.17.3g. +0.1C.30. +0.1f.39. +0.27.2K. +0.2O.1Y. +0.C.3N. +0.m.40. +0.V.3v. +1.31.1t. +1.33.1m. +2.2/.1F. +2.A.3P. +3.1l.34. +3.2W.1R. +3.3K.F. +3.3M.D. +3.S.3y. +4.14.3h. +4.s.3X. +4.u.3V. +5./.3o. +5.k.42. +6.3i.13. +6.3L.E. +6.3S.x. +8.2+.1I. +a.2F.29. +a.2V.1S. +a.2Y.1P. +a.32.1p. +b.3G.K. +b.3R.y. +c.37.1h. +c.3f.1b. +f.1Z.2M. +j.21.2L. +0.+.3q. +0.1+.2M. +0.12.3m. +0.18.3g. +0.1D.30. +0.1d.3e. +0.1m.34. +0.22.2L. +0.D.3N. +0.W.3v. +1.31.1u. +1.33.1n. +1.k.43. +2.2/.1G. +3.2W.1S. +3.3M.E. +3.n.40. +4.15.3h. +4.u.3W. +5.1h.38. +6.3i.14. +6.3L.F. +6.3S.y. +6.4c.c. +8.2+.1J. +9./.3p. +a.2F.2a. +a.2P.1Y. +a.2Y.1Q. +a.32.1q. +b.3G.L. +b.3R.z. +c.37.1i. +c.3f.1c. +c.T.3y. +d.2U.1X. +e.39.1g. +0.+.3r. +0.16.3h. +0.19.3g. +0.1n.34. +0.23.2L. +0.29.2G. +0.E.3N. +0.o.40. +0.r.3Y. +0.U.3y. +0.X.3v. +1.31.1v. +1.33.1o. +2.2/.1H. +2.A.3Q. +3.3M.F. +4./.3q. +5.c.4d. +5.k.44. +6.3i.15. +6.3S.z. +7.u.3X. +a.32.1r. +b.3G.M. +b.3J.G. +c.28.2K. +f.1/.2M. +f.37.1j. +0./.3r. +0.1a.3g. +0.1E.30. +0.1o.34. +0.24.2L. +0.2Q.1Y. +0.F.3N. +0.p.40. +0.s.3Y. +0.V.3y. +1.31.1w. +1.33.1p. +2.2/.1I. +3.3K.G. +4.10.3q. +4.17.3h. +5.2a.2G. +5.k.45. +6.3i.16. +a.2V.1V. +a.32.1s. +b.3J.H. +c.3f.1d. +c.Y.3v. +f.20.2M. +0.10.3r. +0.13.3m. +0.1b.3g. +0.1e.3e. +0.1F.30. +0.1h.39. +0.1p.34. +0.25.2L. +0.q.40. +0.t.3Y. +0.W.3y. +0.Z.3v. +1./.3s. +1.31.1x. +1.33.1q. +2.2/.1J. +2.2H.29. +3.2W.1V. +3.3K.H. +4.11.3q. +4.18.3h. +5.c.4e. +5.k.46. +6.3i.17. +6.3L.G. +7.r.3Z. +8.2+.1K. +a.2F.2c. +a.2V.1W. +a.32.1t. +b.3J.I. +b.3R.A. +f.1Z.2N. +f.2R.1Y. +j.21.2M. +0.1+.2N. +0.11.3r. +0.14.3m. +0.19.3h. +0.1c.3g. +0.1f.3e. +0.1G.30. +0.1i.39. +0.1q.34. +0.1Z.2O. +0.22.2M. +0.26.2L. +0.u.3Y. +0.X.3y. +1.31.1y. +1.33.1r. +2.2H.2a. +3.1u.32. +3.2W.1W. +3.3K.I. +3.3M.G. +4.12.3q. +4.r.3+. +5.1h.3a. +5.1V.2X. +5.c.4f. +5.k.47. +6.3i.18. +6.3L.H. +6.3S.A. +6.4c.d. +7.s.3Z. +8.2+.1L. +a.2Y.1R. +b.3G.N. +b.3J.J. +b.3R.B. +c.37.1k. +0.1/.2N. +0.1+.2O. +0.15.3m. +0.1H.30. +0.1j.39. +0.1r.34. +0.23.2M. +0.27.2L. +0.2c.2G. +0.2S.1Y. +0.3e.1g. +0.G.3N. +0.v.3Y. +1.31.1z. +1.33.1s. +3.1l.37. +3.3K.J. +3.3M.H. +3.I.3L. +4.12.3r. +4.1a.3h. +5.1h.3b. +5.c.4g. +5.k.48. +6.3i.19. +6.3S.B. +6.4c.e. +7.r.3/. +7.s.3+. +8.2+.1M. +a.2P.1Z. +a.2V.1X. +a.2Y.1S. +a.32.1v. +b.3G.O. +b.3J.K. +b.3R.C. +c.3f.1e. +c.Y.3y. +0.1/.2O. +0.16.3m. +0.1d.3g. +0.1I.30. +0.1s.34. +0.20.2N. +0.24.2M. +0.H.3N. +0.r.40. +0.w.3Y. +0.Z.3y. +1.31.1A. +1.33.1t. +2.2/.1K. +3.2W.1X. +3.3K.K. +3.3M.I. +3.J.3L. +4./.3t. +4.1b.3h. +4.3f.1f. +5.1h.3c. +5.c.4h. +5.k.49. +6.3i.1a. +6.3S.C. +6.4c.f. +7.s.3/. +7.u.3Z. +8.2+.1N. +a.2F.2d. +a.2P.1+. +a.32.1w. +b.3G.P. +b.3J.L. +b.3R.D. +c.37.1m. +0.+.3v. +0.17.3m. +0.1J.30. +0.1t.34. +0.20.2O. +0.25.2M. +0.s.40. +0.x.3Y. +1.31.1B. +1.33.1u. +1.c.4i. +1.k.4a. +2.2/.1L. +2.2H.2c. +2.A.3V. +3.3K.L. +3.3M.J. +3.I.3N. +4.13.3q. +4.1c.3h. +4.u.3+. +5./.3u. +5.1h.3d. +6.3i.1b. +6.3L.K. +6.3S.D. +6.4c.g. +7.r.41. +8.2+.1O. +a.2P.1/. +a.32.1x. +b.3G.Q. +b.3J.M. +b.3R.E. +c.28.2L. +c.37.1n. +c.3f.1g. +f.1Z.2Q. +f.29.2I. +j.21.2N. +0./.3v. +0.1+.2Q. +0.18.3m. +0.1h.3e. +0.1k.39. +0.22.2N. +0.26.2M. +0.2G.2d. +0.t.40. +0.y.3Y. +1.31.1C. +1.33.1v. +1.k.4b. +2.2/.1M. +3.1u.34. +3.3K.M. +3.3M.K. +3.J.3N. +4.13.3r. +4.14.3q. +4.u.3/. +5.2a.2I. +6.3i.1c. +6.3L.L. +6.3S.E. +6.4c.h. +7.r.42. +7.s.41. +8.2+.1P. +9.1t.35. +a.2P.20. +a.2Y.1V. +a.32.1y. +b.3G.R. +b.3R.F. +c.37.1o. +f.1Z.2R. +i.c.4j. +j.21.2O. +0.1/.2Q. +0.10.3v. +0.19.3m. +0.1e.3g. +0.1i.3e. +0.1v.34. +0.22.2O. +0.23.2N. +0.K.3N. +0.u.40. +0.z.3Y. +1.31.1D. +1.33.1w. +1.r.43. +2.2/.1N. +2.A.3X. +3.1l.39. +3.3M.L. +3.M.3L. +4./.3w. +4.14.3r. +4.15.3q. +4.1d.3h. +4.1t.36. +4.s.42. +5.c.4k. +6.3S.F. +6.4c.i. +8.2+.1Q. +a.2P.21. +a.2Y.1W. +a.32.1z. +b.3G.S. +c.37.1p. +f.1+.2R. +f.27.2M. +0.+.3y. +0.11.3v. +0.15.3r. +0.1a.3m. +0.1f.3g. +0.1j.3e. +0.1K.30. +0.1m.39. +0.1w.34. +0.1Z.2S. +0.20.2Q. +0.23.2O. +0.24.2N. +0.L.3N. +0.v.40. +1.33.1x. +1.s.43. +2.2/.1O. +2.2H.2d. +2.c.4l. +3.3M.M. +4.16.3q. +6.3i.1d. +6.4c.j. +7.r.44. +7.u.41. +9./.3x. +a.2P.22. +a.32.1A. +b.3G.T. +b.3J.N. +c.37.1q. +c.3f.1h. +f.1/.2R. +0./.3y. +0.1+.2S. +0.12.3v. +0.1b.3m. +0.1L.30. +0.1n.39. +0.1x.34. +0.24.2O. +0.25.2N. +0.w.40. +1.31.1E. +1.33.1y. +2.2/.1P. +3.3K.N. +3.M.3N. +4.16.3r. +4.17.3q. +4.s.44. +4.u.42. +5.c.4m. +6.4c.k. +7.r.45. +a.2P.23. +a.2Y.1X. +a.32.1B. +b.3G.U. +b.3J.O. +b.3R.G. +c.28.2M. +c.37.1r. +c.3f.1i. +f.1V.2Z. +f.20.2R. +f.2c.2I. +h.1g.3g. +j.21.2Q. +0.1/.2S. +0.10.3y. +0.1c.3m. +0.1M.30. +0.1o.39. +0.1y.34. +0.22.2Q. +0.25.2O. +0.26.2N. +0.A.3Y. +0.d.4j. +0.x.40. +1.31.1F. +1.33.1z. +1.c.4n. +1.u.43. +2./.3z. +2.2/.1Q. +3.3K.O. +4.17.3r. +4.18.3q. +4.1e.3h. +4.s.45. +5.k.4d. +6.3L.N. +6.3S.G. +6.4c.l. +7.r.46. +a.2P.24. +a.32.1C. +b.3G.V. +b.3J.P. +b.3R.H. +c.37.1s. +c.3f.1j. +j.21.2R. +0.11.3y. +0.19.3q. +0.1k.3e. +0.1p.39. +0.1z.34. +0.20.2S. +0.23.2Q. +0.26.2O. +0.27.2N. +0.B.3Y. +0.e.4j. +0.y.40. +1.31.1G. +1.33.1A. +3.3K.P. +3.3M.N. +4./.3A. +4.18.3r. +4.1f.3h. +4.1N.30. +5.c.4o. +6.3i.1e. +6.3L.O. +6.3S.H. +6.4c.m. +7.r.47. +7.s.46. +7.u.44. +8.2+.1R. +a.2J.29. +a.2P.25. +a.32.1D. +b.3G.W. +b.3J.Q. +b.3R.I. +c.37.1t. +f.22.2R. +0.12.3y. +0.13.3v. +0.19.3r. +0.1A.34. +0.1d.3m. +0.1h.3g. +0.1O.30. +0.1q.39. +0.24.2Q. +0.27.2O. +0.2I.2d. +0.C.3Y. +0.f.4j. +0.N.3N. +0.z.40. +1.31.1H. +1.33.1B. +2.A.3Z. +3.1l.3e. +3.1u.37. +3.3K.Q. +3.3M.O. +3.I.3S. +3.n.4c. +4.1a.3q. +4.r.48. +4.u.45. +5.c.4p. +5.k.4e. +6.3i.1f. +6.3L.P. +7.s.47. +8.2+.1S. +9.1t.38. +a.2J.2a. +a.2P.26. +b.3G.X. +b.3J.R. +b.3R.J. +d.2U.1Y. +f.23.2R. +f.3h.1g. +j.21.2S. +0.14.3v. +0.1B.34. +0.1i.3g. +0.1m.3e. +0.1P.30. +0.1r.39. +0.22.2S. +0.24.2R. +0.25.2Q. +0.D.3Y. +0.g.4j. +0.O.3N. +1.31.1I. +1.33.1C. +2.A.3+. +3.3K.R. +3.3M.P. +3.J.3S. +4.1a.3r. +4.1b.3q. +4.s.48. +5./.3B. +5.k.4f. +6.3i.1g. +6.3L.Q. +6.4c.o. +7.r.49. +7.u.46. +a.2P.27. +a.32.1E. +b.3G.Y. +b.3J.S. +b.3R.K. +c.28.2N. +c.37.1v. +c.3f.1k. +0.15.3v. +0.1C.34. +0.1j.3g. +0.1n.3e. +0.1Q.30. +0.1s.39. +0.23.2S. +0.25.2R. +0.26.2Q. +0.E.3Y. +0.h.4j. +0.P.3N. +1.31.1J. +1.33.1D. +1.r.4a. +2.2/.1R. +2.A.3/. +3.1l.3f. +3.3K.S. +3.3M.Q. +4.1b.3r. +4.1c.3q. +5./.3C. +5.k.4g. +6.3L.R. +6.3S.K. +6.4c.p. +7.1+.2T. +7.s.49. +7.u.47. +a.32.1F. +b.3G.Z. +b.3J.T. +b.3R.L. +c.28.2O. +c.37.1w. +i.c.4q. +0.13.3y. +0.16.3v. +0.1D.34. +0.1e.3m. +0.1o.3e. +0.1t.39. +0.24.2S. +0.27.2Q. +0.29.2K. +0.A.40. +0.F.3Y. +0.i.4j. +0.Q.3N. +1.r.4b. +1.s.4a. +2./.3D. +2.2/.1S. +3.3K.T. +3.3M.R. +3.S.3L. +4.1c.3r. +4.1h.3h. +4.26.2R. +4.u.48. +5.c.4r. +5.k.4h. +6.3S.L. +6.4c.q. +8.2+.1V. +a.2J.2c. +a.2P.28. +a.32.1G. +b.3J.U. +b.3R.M. +c.37.1x. +c.3f.1m. +0.14.3y. +0.17.3v. +0.1f.3m. +0.1p.3e. +0.25.2S. +0.B.40. +0.j.4j. +0.R.3N. +1.33.1E. +1.k.4i. +1.s.4b. +3.1u.39. +3.3K.U. +3.3M.S. +3.M.3S. +4.1d.3q. +4.1i.3h. +4.1t.3a. +5.2a.2K. +5.c.4s. +6.3i.1h. +6.3L.T. +7.u.49. +8.2+.1W. +9./.3E. +a.32.1H. +b.3J.V. +c.37.1y. +c.3f.1n. +f.27.2R. +0.15.3y. +0.18.3v. +0.1E.34. +0.1j.3h. +0.1k.3g. +0.1q.3e. +0.1v.39. +0.26.2S. +0.C.40. +0.k.4j. +1.31.1K. +1.33.1F. +1.u.4a. +2.A.42. +3.3K.V. +3.3M.T. +3.S.3N. +4.1d.3r. +4.1t.3b. +5.1h.3j. +5.c.4t. +6.3i.1i. +6.3L.U. +6.3m.1g. +9./.3F. +a.2V.1Y. +a.32.1I. +b.3J.W. +c.28.2Q. +c.37.1z. +c.3f.1o. +d.2U.1Z. +0.16.3y. +0.19.3v. +0.1F.34. +0.1r.3e. +0.1w.39. +0.27.2S. +0.D.40. +0.d.4q. +0.G.3Y. +0.l.4j. +1.31.1L. +1.33.1G. +1.u.4b. +2.2/.1V. +2.A.43. +3.1l.3g. +3.2W.1Y. +3.3K.W. +3.3M.U. +4.1t.3c. +5.1h.3k. +5.k.4k. +6.3i.1j. +6.3L.V. +6.4c.r. +7.22.2T. +8.2+.1X. +a.2J.2d. +a.32.1J. +b.3G.+. +b.3J.X. +b.3R.N. +c.28.2R. +c.37.1A. +c.3f.1p. +c.T.3N. +d.2U.1+. +f.1R.30. +0.17.3y. +0.1a.3v. +0.1G.34. +0.1m.3g. +0.1s.3e. +0.1x.39. +0.E.40. +0.e.4q. +0.H.3Y. +0.m.4j. +0.U.3N. +1.31.1M. +1.33.1H. +2.2/.1W. +2.A.44. +2.k.4l. +3.3K.X. +3.3M.V. +4.1e.3q. +4.1S.30. +4.1t.3d. +5.1h.3l. +5.c.4u. +6.3L.W. +6.3S.N. +6.4c.s. +7.r.4d. +b.3G./. +b.3J.Y. +b.3R.O. +c.37.1B. +c.3f.1q. +d.2U.1/. +f.2c.2K. +0.18.3y. +0.1b.3v. +0.1e.3r. +0.1H.34. +0.1h.3m. +0.1k.3h. +0.1n.3g. +0.1t.3e. +0.1y.39. +0.F.40. +0.f.4q. +0.V.3N. +1.31.1N. +1.33.1I. +3.3K.Y. +3.3M.W. +3.I.3Y. +3.n.4j. +4.1f.3q. +5.c.4v. +5.k.4m. +6.3L.X. +6.3S.O. +6.4c.t. +7.s.4d. +b.3G.10. +b.3J.Z. +b.3R.P. +c.28.2S. +c.37.1C. +c.3f.1r. +d.2U.20. +f.24.2T. +0.19.3y. +0.1c.3v. +0.1f.3r. +0.1I.34. +0.1o.3g. +0.1z.39. +0.29.2L. +0.3q.1g. +0.g.4q. +0.o.4j. +0.W.3N. +1.1h.3n. +1.31.1O. +1.33.1J. +1.k.4n. +2.2/.1X. +2.A.46. +3.1l.3h. +3.1u.3e. +3.3K.Z. +3.3M.X. +3.J.3Y. +4.1i.3m. +5.c.4w. +6.3i.1k. +6.3L.Y. +6.3S.P. +6.4c.u. +7.r.4e. +a.32.1K. +b.3G.11. +b.3R.Q. +c.37.1D. +c.3f.1s. +d.2U.21. +0.1A.39. +0.1a.3y. +0.1J.34. +0.1j.3m. +0.1m.3h. +0.1p.3g. +0.1V.30. +0.1v.3e. +0.2K.2d. +0.h.4q. +0.K.3Y. +0.p.4j. +0.X.3N. +1.31.1P. +2.A.47. +3.1l.3i. +3.3M.Y. +5.1h.3o. +5.2a.2L. +5.k.4o. +6.3L.Z. +6.3r.1g. +6.3S.Q. +6.4c.v. +7.26.2T. +7.r.4f. +7.s.4e. +7.u.4d. +a.2V.1Z. +a.32.1L. +b.3G.12. +b.3R.R. +c.3f.1t. +d.2U.22. +0.1B.39. +0.1b.3y. +0.1d.3v. +0.1n.3h. +0.1q.3g. +0.1W.30. +0.1w.3e. +0.G.40. +0.i.4q. +0.L.3Y. +0.q.4j. +1.31.1Q. +3.1u.3f. +3.2W.1Z. +3.3M.Z. +5./.3H. +5.1h.3p. +5.2g.2w. +5.k.4p. +6.3i.1m. +6.3S.R. +6.4c.w. +7.r.4g. +7.s.4f. +9.c.4x. +a.2V.1+. +a.2Y.1Y. +a.32.1M. +b.3R.S. +c.37.1E. +c.Y.3N. +d.2U.23. +0.1C.39. +0.1c.3y. +0.1o.3h. +0.1r.3g. +0.1x.3e. +0.H.40. +0.j.4q. +0.Z.3N. +1.33.1K. +2.1d.3w. +3.2W.1+. +3.M.3Y. +3.S.3S. +4.1h.3q. +4.r.4h. +5./.3I. +5.c.4y. +6.3i.1n. +6.4c.x. +7.s.4g. +7.u.4e. +a.2V.1/. +a.32.1N. +b.3J.+. +b.3R.T. +c.37.1F. +c.3f.1v. +d.2U.24. +0.1D.39. +0.1K.34. +0.1k.3m. +0.1p.3h. +0.1s.3g. +0.1X.30. +0.1y.3e. +0.2c.2L. +0.k.4q. +1.33.1L. +1.r.4i. +2.1d.3x. +3.2W.1/. +3.3K.+. +3.I.40. +4.1h.3r. +4.1i.3q. +5.c.4z. +6.3i.1o. +6.3S.T. +6.4c.y. +7.1+.2X. +7.s.4h. +7.u.4f. +a.2V.20. +a.32.1O. +b.3G.13. +b.3J./. +b.3R.U. +c.37.1G. +c.3f.1w. +d.2U.25. +f.29.2M. +0.1d.3y. +0.1e.3v. +0.1j.3q. +0.1L.34. +0.1q.3h. +0.1t.3g. +0.1z.3e. +0.l.4q. +0.r.4j. +1.1h.3s. +1.33.1M. +1.s.4i. +3.1l.3m. +3.2W.20. +3.3K./. +3.J.40. +4.1i.3r. +5.2a.2M. +5.c.4A. +5.k.4r. +6.3i.1p. +6.3L.+. +6.3S.U. +6.4c.z. +7.u.4g. +a.2V.21. +a.32.1P. +b.3G.14. +b.3J.10. +b.3R.V. +c.37.1H. +c.3f.1x. +d.2U.26. +0.1A.3e. +0.1E.39. +0.1f.3v. +0.1j.3r. +0.1M.34. +0.1m.3m. +0.K.40. +0.m.4q. +0.N.3Y. +0.s.4j. +1.31.1R. +1.33.1N. +2.1d.3z. +3.1u.3g. +3.2W.21. +3.3K.10. +3.3M.+. +4.1r.3h. +4.u.4h. +5.2g.2x. +5.c.4B. +5.k.4s. +6.3i.1q. +6.3L./. +6.3S.V. +7.r.4k. +a.2V.22. +a.32.1Q. +b.3G.15. +b.3J.11. +b.3R.W. +c.37.1I. +c.3f.1y. +d.2U.27. +0.+.3N. +0.1F.39. +0.1n.3m. +0.1s.3h. +0.1v.3g. +0.2L.2d. +0.L.40. +0.O.3Y. +0.t.4j. +1.31.1S. +1.33.1O. +1.u.4i. +2.1d.3A. +2.r.4l. +3.2W.22. +3.3K.11. +3.3M./. +3.n.4q. +4.1B.3e. +4.1N.34. +5.c.4C. +5.k.4t. +6.3i.1r. +6.3L.10. +6.3S.W. +7.s.4k. +a.2V.23. +a.2Y.1Z. +b.3G.16. +b.3J.12. +b.3R.X. +c.3f.1z. +f.37.1J. +h.1g.3v. +0./.3N. +0.1C.3e. +0.1e.3y. +0.1G.39. +0.1O.34. +0.1o.3m. +0.1w.3g. +0.29.2N. +0.o.4q. +0.P.3Y. +0.u.4j. +1.33.1P. +2.s.4l. +3.2W.23. +3.3K.12. +3.3M.10. +3.M.40. +4.1k.3q. +4.1t.3h. +5.1h.3t. +5.c.4D. +6.3i.1s. +6.3L.11. +6.3S.X. +6.4c.A. +7.22.2X. +7.r.4m. +a.2V.24. +a.2Y.1+. +b.3G.17. +b.3R.Y. +c.3f.1A. +d.2U.28. +f.2c.2M. +0.10.3N. +0.1D.3e. +0.1f.3y. +0.1H.39. +0.1k.3r. +0.1P.34. +0.1p.3m. +0.1x.3g. +0.29.2O. +0.p.4q. +0.Q.3Y. +0.v.4j. +1.33.1Q. +1.r.4n. +2.A.4d. +2.c.4E. +3.1l.3q. +3.1u.3h. +3.2W.24. +3.3M.11. +4.s.4m. +5./.3O. +5.1h.3u. +5.2a.2N. +5.k.4u. +6.3i.1t. +6.3L.12. +6.3S.Y. +6.4c.B. +7.u.4k. +a.2V.25. +a.2Y.1/. +b.3G.18. +b.3R.Z. +c.3f.1B. +0.11.3N. +0.1h.3v. +0.1I.39. +0.1Q.34. +0.1q.3m. +0.1v.3h. +0.1y.3g. +0.q.4q. +0.R.3Y. +0.w.4j. +1.31.1V. +1.s.4n. +2./.3P. +2.u.4l. +3.1l.3r. +3.1u.3i. +3.2W.25. +3.3M.12. +4.1m.3q. +5.1t.3j. +5.2a.2O. +5.2g.2y. +5.c.4F. +5.k.4v. +6.3S.Z. +6.3y.1g. +6.4c.C. +7.24.2X. +7.r.4o. +a.2P.29. +a.2V.26. +a.2Y.20. +a.32.1R. +b.3G.19. +b.3J.13. +c.37.1K. +c.3f.1C. +0.1E.3e. +0.1i.3v. +0.1J.39. +0.1r.3m. +0.1z.3g. +0.2M.2d. +0.N.40. +0.x.4j. +1.31.1W. +3.2W.26. +3.3K.13. +3.S.3Y. +4.12.3N. +4.1m.3r. +4.1n.3q. +4.1w.3h. +5.1h.3w. +5.c.4G. +5.k.4w. +6.3i.1v. +6.4c.D. +7.r.4p. +7.s.4o. +7.u.4m. +8.2+.1Y. +8.2g.2z. +9.1+.2Z. +9.1t.3k. +a.2P.2a. +a.2V.27. +a.2Y.21. +a.32.1S. +b.3G.1a. +b.3J.14. +c.37.1L. +c.3f.1D. +0.1A.3g. +0.1F.3e. +0.1j.3v. +0.1n.3r. +0.1o.3q. +0.1s.3m. +0.1x.3h. +0.29.2Q. +0.O.40. +0.y.4j. +1.u.4n. +2.A.4f. +3.2W.27. +3.3K.14. +5./.3Q. +5.1h.3x. +5.1t.3l. +6.3i.1w. +6.3L.13. +6.4c.E. +7.26.2X. +7.s.4p. +8.2g.2A. +a.2Y.22. +b.3G.1b. +b.3J.15. +c.37.1M. +c.T.3Y. +f.2c.2N. +0.1B.3g. +0.1G.3e. +0.1h.3y. +0.1o.3r. +0.1y.3h. +0.2c.2O. +0.P.40. +0.r.4q. +0.U.3Y. +0.z.4j. +1.31.1X. +1.33.1R. +2.A.4g. +3.3K.15. +3.3M.13. +4.1p.3q. +4.1t.3m. +5.2a.2Q. +6.3i.1x. +6.3L.14. +6.4c.F. +7.u.4o. +9.k.4x. +a.2V.28. +a.2Y.23. +b.3G.1c. +b.3J.16. +b.3R.+. +c.3f.1E. +f.29.2R. +f.2g.2B. +f.37.1N. +0.13.3N. +0.1C.3g. +0.1H.3e. +0.1i.3y. +0.1K.39. +0.1p.3r. +0.Q.40. +0.s.4q. +0.V.3Y. +1.1t.3n. +1.33.1S. +2.1h.3z. +2.2/.1Y. +2.A.4h. +3.1u.3m. +3.2W.28. +3.3K.16. +3.3M.14. +4.1q.3q. +4.1z.3h. +5.2a.2R. +5.c.4H. +5.k.4y. +6.3i.1y. +6.3L.15. +6.3S.+. +7.r.4r. +7.u.4p. +a.2P.2c. +a.2Y.24. +a.32.1V. +b.3J.17. +b.3R./. +c.3f.1F. +f.1R.34. +f.2g.2C. +f.37.1O. +0.14.3N. +0.1D.3g. +0.1I.3e. +0.1j.3y. +0.1k.3v. +0.1L.39. +0.1q.3r. +0.1S.34. +0.1v.3m. +0.29.2S. +0.2N.2d. +0.R.40. +0.t.4q. +0.W.3Y. +3.3K.17. +3.3M.15. +4.1A.3h. +4.1r.3q. +5.1h.3A. +5.1t.3o. +5.c.4I. +5.k.4z. +6.3i.1z. +6.3L.16. +6.3S./. +7.r.4s. +7.s.4r. +9.22.2Z. +a.2Y.25. +a.32.1W. +b.3G.1d. +b.3J.18. +b.3R.10. +c.3f.1G. +f.37.1P. +0.15.3N. +0.1J.3e. +0.1M.39. +0.1w.3m. +0.2O.2d. +0.A.4j. +0.u.4q. +0.X.3Y. +3.1l.3v. +3.3K.18. +3.3M.16. +3.S.40. +4.1B.3h. +4.1r.3r. +4.1s.3q. +4.1t.3p. +5./.3T. +5.2a.2S. +5.c.4J. +5.k.4A. +6.3i.1A. +6.3L.17. +6.3S.10. +6.4c.G. +7.r.4t. +7.s.4s. +8.2+.1Z. +a.2Y.26. +b.3J.19. +b.3R.11. +c.37.1Q. +c.3f.1H. +f.2c.2Q. +0.16.3N. +0.1C.3h. +0.1E.3g. +0.1m.3v. +0.1s.3r. +0.1x.3m. +0.B.4j. +0.v.4q. +1.33.1V. +2.A.4k. +3.3K.19. +3.3M.17. +4.1N.39. +4.1t.3q. +4.s.4t. +5./.3U. +5.1h.3B. +5.c.4K. +5.k.4B. +6.3i.1B. +6.3L.18. +6.3S.11. +6.4c.H. +7.u.4r. +8.2+.1+. +9.24.2Z. +a.2P.2d. +a.2Y.27. +a.32.1X. +b.3J.1a. +b.3R.12. +c.3f.1I. +c.T.40. +c.Y.3Y. +f.2c.2R. +f.2g.2D. +0.17.3N. +0.1F.3g. +0.1k.3y. +0.1n.3v. +0.1O.39. +0.1V.34. +0.1y.3m. +0.30.1Y. +0.C.4j. +0.U.40. +0.w.4q. +0.Z.3Y. +1.33.1W. +2.A.4l. +3.1u.3q. +3.3M.18. +3.I.4c. +4.1D.3h. +4.1t.3r. +5./.3V. +5.1h.3C. +5.c.4L. +5.k.4C. +6.3i.1C. +6.3L.19. +6.3S.12. +7.r.4u. +7.u.4s. +8.2+.1/. +b.3G.1e. +b.3J.1b. +c.3f.1J. +e.3K.1a. +f.2g.2E. +0.18.3N. +0.1G.3g. +0.1K.3e. +0.1o.3v. +0.1P.39. +0.1W.34. +0.1z.3m. +0.2Q.2d. +0.D.4j. +0.V.40. +0.x.4q. +1.1t.3s. +2.1h.3D. +2.2/.1Z. +2.A.4m. +3.1l.3y. +3.1u.3r. +3.3K.1b. +3.3M.19. +3.J.4c. +4.1v.3q. +4.s.4u. +5./.3W. +5.c.4M. +5.k.4D. +6.3i.1D. +6.3L.1a. +7.r.4v. +7.u.4t. +8.2+.20. +9.26.2Z. +a.2Y.28. +b.3G.1f. +b.3J.1c. +f.1V.35. +f.2c.2S. +0.19.3N. +0.1A.3m. +0.1H.3g. +0.1L.3e. +0.1m.3y. +0.1p.3v. +0.1Q.39. +0.1v.3r. +0.2R.2d. +0.E.4j. +0.W.40. +0.y.4q. +1.33.1X. +2.1d.3I. +2.2/.1+. +2.A.4n. +2.k.4E. +3.3K.1c. +3.3M.1a. +4.1E.3h. +4.1w.3q. +5./.3X. +5.1h.3E. +5.29.2T. +5.c.4N. +6.3L.1b. +6.4c.K. +7.r.4w. +7.s.4v. +8.2+.21. +9.1V.36. +b.3G.1g. +b.3R.13. +c.37.1R. +0.1B.3m. +0.1F.3h. +0.1I.3g. +0.1M.3e. +0.1n.3y. +0.1q.3v. +0.1w.3r. +0.1X.34. +0.F.4j. +0.X.40. +2.2/.1/. +2.A.4o. +3.3M.1b. +4.1a.3N. +4.1x.3q. +4.u.4u. +5.1h.3F. +5.c.4O. +5.k.4F. +6.3i.1E. +6.3L.1c. +6.3S.13. +6.4c.L. +6.4q.z. +7.s.4w. +8.2+.22. +b.3J.1d. +b.3R.14. +c.3f.1K. +f.37.1S. +0.+.3Y. +0.1b.3N. +0.1C.3m. +0.1G.3h. +0.1J.3g. +0.1N.3e. +0.1o.3y. +0.1r.3v. +0.1x.3r. +0.2S.2d. +2.2/.20. +2.A.4p. +2.c.4P. +3.3K.1d. +3.3M.1c. +3.M.4c. +4.1t.3t. +4.1y.3q. +4.3f.1L. +5.k.4G. +6.3i.1F. +6.3S.14. +7.u.4v. +8.2+.23. +9.r.4x. +b.3R.15. +c.Y.40. +0./.3Y. +0.1c.3N. +0.1D.3m. +0.1O.3e. +0.1p.3y. +0.1s.3v. +0.1y.3r. +0.1Z.30. +0.Z.40. +2.2/.21. +4.1H.3h. +4.1z.3q. +5.1t.3u. +5.c.4Q. +6.3i.1G. +6.3L.1d. +6.3S.15. +7.r.4y. +7.u.4w. +8.2+.24. +9.s.4x. +b.3G.1h. +b.3R.16. +c.3f.1M. +0.1+.30. +0.10.3Y. +0.1P.3e. +0.1q.3y. +0.1t.3v. +0.A.4q. +0.G.4j. +2.2/.22. +3.3M.1d. +4.1A.3q. +4.1I.3h. +4.1z.3r. +4.3f.1N. +5.c.4R. +6.3i.1H. +6.3S.16. +7.r.4z. +7.s.4y. +8.2+.25. +b.3G.1i. +b.3J.1e. +b.3R.17. +c.37.1V. +d.2U.29. +f.1R.39. +0.1/.30. +0.11.3Y. +0.1E.3m. +0.1J.3h. +0.1K.3g. +0.1Q.3e. +0.1r.3y. +0.1S.39. +0.B.4q. +0.H.4j. +2.2/.23. +2.A.4r. +3.1u.3v. +3.3K.1e. +4.1A.3r. +4.1B.3q. +4.1d.3N. +4.1t.3w. +4.s.4z. +5./.3Z. +5.c.4S. +5.k.4H. +6.3i.1I. +6.3S.17. +6.4c.N. +7.r.4A. +8.2+.26. +9.1V.38. +9.u.4x. +b.3G.1j. +b.3J.1f. +b.3R.18. +c.3f.1O. +d.2U.2a. +f.37.1W. +0.12.3Y. +0.1B.3r. +0.1F.3m. +0.1L.3g. +0.1s.3y. +0.1v.3v. +0.20.30. +0.C.4q. +1.2n.2u. +2.1d.3O. +2.2/.24. +2.A.4s. +3.3K.1f. +3.I.4j. +4.1C.3q. +4.1t.3x. +5./.3+. +5.2g.2F. +5.k.4I. +6.3i.1J. +6.3L.1e. +6.3S.18. +6.4c.O. +7.r.4B. +7.s.4A. +7.u.4y. +8.2+.27. +b.3J.1g. +b.3R.19. +c.3f.1P. +0.+.40. +0.1G.3m. +0.1M.3g. +0.1t.3y. +0.1w.3v. +0.D.4q. +1.31.1Y. +2.1d.3P. +2.2/.25. +2.2n.2v. +2.A.4t. +2.c.4T. +3.3K.1g. +3.3M.1e. +3.J.4j. +4.1C.3r. +4.1D.3q. +4.3f.1Q. +4.u.4z. +5./.3/. +5.1h.3H. +5.k.4J. +6.3L.1f. +6.3S.19. +6.4c.P. +7.r.4C. +7.s.4B. +b.3R.1a. +c.37.1X. +e.21.30. +0./.40. +0.1e.3N. +0.1H.3m. +0.1N.3g. +0.1x.3v. +0.22.30. +0.E.4q. +0.K.4j. +2.0.4+. +2.1t.3z. +2.2/.26. +2.c.4U. +3.1u.3y. +3.3M.1f. +4.1D.3r. +4.1K.3h. +5.1h.3I. +5.k.4K. +6.3L.1g. +6.3S.1a. +6.4c.Q. +7.r.4D. +7.s.4C. +7.u.4A. +8.2+.28. +b.3G.1k. +b.3R.1b. +d.2U.2c. +f.1V.39. +f.2g.2G. +0.10.40. +0.13.3Y. +0.1f.3N. +0.1I.3m. +0.1L.3h. +0.1O.3g. +0.1v.3y. +0.1W.39. +0.1y.3v. +0.23.30. +0.F.4q. +0.L.4j. +2.1.4+. +2.1d.3Q. +2.2/.27. +2.r.4E. +3.3M.1g. +4.1E.3q. +4.1t.3A. +5./.41. +5.k.4L. +6.3i.1K. +6.3S.1b. +6.4c.R. +7.s.4D. +7.u.4B. +9.c.4V. +a.2V.29. +b.3G.1l. +b.3J.1h. +b.3R.1c. +f.1R.3e. +f.1V.3a. +0.11.40. +0.14.3Y. +0.1J.3m. +0.1M.3h. +0.1P.3g. +0.1S.3e. +0.1w.3y. +0.1z.3v. +0.24.30. +1.c.4W. +2.2.4+. +2.2H.2g. +2.A.4v. +2.s.4E. +3.2W.29. +3.3K.1h. +3.M.4j. +3.S.4c. +4.1E.3r. +4.1F.3q. +5./.42. +5.k.4M. +6.3i.1L. +6.3N.1g. +6.3S.1c. +7.r.4F. +7.u.4C. +a.2V.2a. +b.3G.1m. +b.3J.1i. +f.1V.3b. +0.12.40. +0.15.3Y. +0.1A.3v. +0.1F.3r. +0.1Q.3g. +0.1X.39. +0.1x.3y. +0.25.30. +1./.43. +1.2f.2T. +2.2/.28. +2.3.4+. +2.A.4w. +3.2W.2a. +3.3K.1i. +4.1G.3q. +4.1N.3h. +5.1t.3B. +5.29.2X. +5.k.4N. +6.3i.1M. +6.3L.1h. +6.4c.T. +7.r.4G. +7.s.4F. +7.u.4D. +9.1V.3c. +a.32.1Y. +b.3G.1n. +b.3J.1j. +b.3R.1d. +c.3f.1R. +d.2U.2d. +0.16.3Y. +0.1B.3v. +0.1G.3r. +0.1O.3h. +0.1y.3y. +0.26.30. +0.G.4q. +1.31.1Z. +2.4.4+. +2.u.4E. +3.3K.1j. +3.3M.1h. +4.1H.3q. +5./.44. +5.1t.3C. +5.k.4O. +6.3i.1N. +6.3L.1i. +6.3S.1d. +6.4c.U. +7.s.4G. +9.1V.3d. +b.3G.1o. +c.3f.1S. +0.17.3Y. +0.1C.3v. +0.1h.3N. +0.1H.3r. +0.1K.3m. +0.1P.3h. +0.1z.3y. +0.27.30. +0.H.4q. +0.N.4j. +1.31.1+. +2.1t.3D. +2.5.4+. +2.A.4x. +2.k.4P. +3.3M.1i. +4.1I.3q. +5./.45. +6.3i.1O. +6.3L.1j. +6.4c.V. +7.u.4F. +9.c.4X. +a.2V.2c. +b.3G.1p. +e.0.53. +f.1V.3e. +0.13.40. +0.18.3Y. +0.1A.3y. +0.1D.3v. +0.1I.3r. +0.1L.3m. +0.1W.3e. +0.O.4j. +1.31.1/. +1.33.1Y. +2.6.4+. +2.A.4y. +3.2W.2c. +3.3M.1j. +3.I.4q. +4.1i.3N. +4.1J.3q. +5./.46. +5.1h.3O. +5.k.4Q. +6.3i.1P. +6.4c.W. +7.r.4H. +7.u.4G. +9.1t.3E. +9.c.4Y. +b.3G.1q. +b.3J.1k. +b.3R.1e. +e.1.53. +f.1Q.3h. +f.2g.2I. +0.14.40. +0.19.3Y. +0.1B.3y. +0.1j.3N. +0.1M.3m. +0.1R.3g. +0.34.1Y. +0.P.4j. +1.31.20. +2.1d.3V. +2.1h.3P. +2.7.4+. +2.A.4z. +3.3K.1k. +3.J.4q. +4.1J.3r. +5./.47. +5.k.4R. +6.3i.1Q. +6.3S.1e. +6.4c.X. +7.r.4I. +7.s.4H. +9.1t.3F. +a.2Y.29. +b.3G.1r. +b.3J.1l. +b.3R.1f. +c.28.30. +c.3f.1V. +e.0.55. +e.2.53. +0.15.40. +0.1a.3Y. +0.1C.3y. +0.1E.3v. +0.1S.3g. +0.1X.3e. +0.K.4q. +0.Q.4j. +1.31.21. +2.8.4+. +3.3K.1l. +4.1N.3m. +5./.48. +5.k.4S. +6.3L.1k. +6.3S.1f. +6.4c.Y. +7.r.4J. +7.s.4I. +a.2V.2d. +a.2Y.2a. +a.32.1Z. +b.3G.1s. +b.3J.1m. +b.3R.1g. +c.3f.1W. +e.1.55. +e.3.53. +0.16.40. +0.1b.3Y. +0.1D.3y. +0.1F.3v. +0.1O.3m. +0.L.4q. +0.R.4j. +1.31.22. +1.c.4Z. +2.1d.3X. +2.9.4+. +2.A.4B. +3.1l.3L. +3.2W.2d. +3.3K.1m. +3.3M.1k. +4.1K.3q. +5./.49. +5.1h.3Q. +6.3S.1g. +6.4c.Z. +7.r.4K. +7.s.4J. +7.u.4H. +a.32.1+. +b.3G.1t. +b.3J.1n. +e.2.55. +e.4.53. +0.17.40. +0.1c.3Y. +0.1G.3v. +0.1k.3N. +0.1L.3q. +0.1P.3m. +0.2w.2n. +1./.4a. +1.31.23. +2.a.4+. +2.A.4C. +2.k.4T. +3.3K.1n. +3.3M.1l. +3.M.4q. +3.S.4j. +4.1K.3r. +5.29.2Z. +6.3L.1m. +6.58.0. +7.r.4L. +7.s.4K. +7.u.4I. +a.32.1/. +b.3G.1u. +b.3J.1o. +c.3f.1X. +e.3.55. +e.5.53. +f.1R.3h. +0.18.40. +0.1E.3y. +0.1H.3v. +0.1L.3r. +0.1Q.3m. +1./.4b. +1.31.24. +1.33.1Z. +2.A.4D. +2.b.4+. +2.k.4U. +3.1l.3N. +3.3K.1o. +3.3M.1m. +4.1M.3q. +4.1S.3h. +4.r.4M. +6.3i.1R. +6.3L.1n. +6.58.1. +7.s.4L. +7.u.4J. +a.2Y.2c. +a.32.20. +b.3G.1v. +b.3J.1p. +b.3R.1h. +c.T.4j. +e.4.55. +e.6.53. +f.1V.3g. +0.19.40. +0.1d.3Y. +0.1F.3y. +0.1I.3v. +0.1m.3N. +0.1M.3r. +0.1W.3g. +0.1Z.34. +0.U.4j. +1.31.25. +1.33.1+. +3.3K.1p. +3.3M.1n. +4.1N.3q. +4.c.4+. +4.s.4M. +5.k.4V. +6.3i.1S. +6.3L.1o. +6.3S.1h. +6.58.2. +7.r.4N. +7.u.4K. +a.32.21. +b.3G.1w. +b.3J.1q. +b.3R.1i. +e.5.55. +e.7.53. +f.2g.2J. +0.1+.34. +0.1a.40. +0.1G.3y. +0.1J.3v. +0.1n.3N. +0.N.4q. +0.V.4j. +1.31.26. +1.33.1/. +1.k.4W. +3.3K.1q. +3.3M.1o. +4.1N.3r. +4.1O.3q. +4.c.4/. +5.1h.3T. +5.1t.3H. +6.3L.1p. +6.3S.1i. +6.4c.+. +6.58.3. +7.2p.2w. +7.r.4O. +7.s.4N. +7.u.4L. +a.32.22. +b.3G.1x. +b.3J.1r. +b.3R.1j. +c.37.1Y. +e.6.55. +e.8.53. +0.1/.34. +0.1b.40. +0.1H.3y. +0.1o.3N. +0.1O.3r. +0.1X.3g. +0.O.4q. +0.W.4j. +1.2f.2X. +1.31.27. +1.33.20. +2.1d.3Z. +2.r.4P. +3.2x.2n. +3.3K.1r. +3.3M.1p. +4.c.50. +4.s.4O. +4.u.4M. +5.1h.3U. +5.1t.3I. +6.3L.1q. +6.3S.1j. +6.4c./. +9.1+.35. +a.2Y.2d. +a.32.23. +b.3G.1y. +b.3J.1s. +e.4.58. +e.7.55. +e.9.53. +f.1P.3q. +f.1V.3h. +0.1c.40. +0.1e.3Y. +0.1I.3y. +0.1p.3N. +0.1P.3r. +0.1W.3h. +0.P.4q. +0.X.4j. +1.33.21. +2.1d.3+. +2.s.4P. +3.3K.1s. +3.3M.1q. +5./.4d. +5.1h.3V. +5.c.51. +6.3i.1V. +6.3L.1r. +6.4c.10. +6.58.5. +7.r.4Q. +7.u.4N. +9.1+.36. +a.32.24. +b.3G.1z. +b.3J.1t. +e.8.55. +e.a.53. +f.1Q.3q. +f.1R.3m. +f.20.34. +0.1f.3Y. +0.1J.3y. +0.1K.3v. +0.1q.3N. +0.1S.3m. +0.Q.4q. +1.31.28. +1.33.22. +2.1d.3/. +3.3K.1t. +3.3M.1r. +4.r.4R. +4.u.4O. +5.1h.3W. +5.1V.3j. +5.c.52. +6.3i.1W. +6.3L.1s. +6.4c.11. +6.58.6. +7.s.4Q. +8.2+.29. +9.k.4X. +a.32.25. +b.3G.1A. +b.3J.1u. +b.3R.1k. +c.Y.4j. +e.9.55. +e.b.53. +f.1Q.3r. +f.2g.2K. +j.21.34. +0.1d.40. +0.1L.3v. +0.1r.3N. +0.1X.3h. +0.22.34. +0.R.4q. +0.Z.4j. +1.33.23. +2.0.5d. +2.A.4H. +2.u.4P. +3.3K.1u. +3.3M.1s. +4.s.4R. +5./.4e. +5.1h.3X. +6.3L.1t. +6.3S.1k. +6.4c.12. +7.2p.2x. +7.r.4S. +8.2+.2a. +9.k.4Y. +a.32.26. +b.3G.1B. +b.3J.1v. +b.3R.1l. +e.7.58. +e.a.55. +f.1V.3k. +f.39.1Y. +h.1g.3Y. +i.c.53. +0.1M.3v. +0.1s.3N. +1.33.24. +2.1.5d. +3.1l.3S. +3.1u.3L. +3.2y.2n. +3.3K.1v. +3.3M.1t. +3.S.4q. +5./.4f. +5.1V.3l. +5.c.54. +6.3i.1X. +6.58.8. +7.s.4S. +7.u.4Q. +9.22.35. +a.32.27. +b.3G.1C. +b.3J.1w. +b.3R.1m. +e.b.55. +f.23.34. +f.37.1Z. +0.1K.3y. +0.1N.3v. +0.1V.3m. +0.24.34. +1.33.25. +2.1d.42. +2.2.5d. +2.2/.29. +2.A.4J. +2.r.4T. +3.3K.1w. +3.3M.1u. +4.1t.3N. +5./.4g. +6.3L.1v. +6.3S.1m. +7.u.4R. +8.2z.2n. +9.22.36. +b.3G.1D. +b.3J.1x. +b.3R.1n. +c.37.1+. +c.T.4q. +e.9.58. +f.1R.3q. +i.c.55. +0.1e.40. +0.1h.3Y. +0.1L.3y. +0.1O.3v. +0.1W.3m. +0.25.34. +0.U.4q. +1.1V.3n. +1.33.26. +1.c.56. +1.k.4Z. +2.1d.43. +2.2/.2a. +2.3.5d. +2.A.4K. +2.r.4U. +2.s.4T. +3.1u.3N. +3.3K.1x. +3.3M.1v. +4.1S.3q. +5./.4h. +5.1t.3O. +6.3L.1w. +6.3S.1n. +6.4c.13. +6.58.a. +7.u.4S. +8.2+.2c. +8.2A.2n. +9.1+.38. +9.24.35. +a.32.28. +b.3J.1y. +b.3R.1o. +c.37.1/. +e.1R.3r. +0.+.4j. +0.1f.40. +0.1i.3Y. +0.1M.3y. +0.1P.3v. +0.1v.3N. +0.26.34. +0.d.53. +0.V.4q. +1.2f.2Z. +1.33.27. +2.1d.44. +2.1t.3P. +2.4.5d. +2.A.4L. +2.s.4U. +3.2B.2n. +3.3K.1y. +3.3M.1w. +4.1S.3r. +5.1V.3o. +5.c.57. +6.3L.1x. +6.3S.1o. +6.4c.14. +6.58.b. +7.2p.2y. +7.r.4V. +9.24.36. +b.3G.1E. +b.3J.1z. +b.3R.1p. +c.37.20. +e.3.5e. +0./.4j. +0.1j.3Y. +0.1N.3y. +0.1Q.3v. +0.1w.3N. +0.1X.3m. +0.1Z.39. +0.27.34. +0.2C.2n. +0.3e.1Y. +0.e.53. +0.W.4q. +1.r.4W. +2.5.5d. +2.A.4M. +2.u.4T. +3.3K.1z. +3.3M.1x. +5.1h.3Z. +6.3L.1y. +6.3S.1p. +6.4c.15. +7.s.4V. +8.2z.2p. +8.58.c. +9.1V.3p. +9.26.35. +b.3G.1F. +b.3J.1A. +b.3R.1q. +c.37.21. +e.4.5e. +f.2g.2L. +h.1g.40. +0.1+.39. +0.10.4j. +0.1O.3y. +0.1x.3N. +0.29.30. +0.X.4q. +1.33.28. +1.s.4W. +2.1d.46. +2.2/.2c. +2.6.5d. +2.A.4N. +2.c.59. +2.u.4U. +3.3K.1A. +3.3M.1y. +4.d.55. +4.f.53. +5./.4k. +5.1h.3+. +5.1t.3Q. +6.3L.1z. +6.3S.1q. +6.4c.16. +8.2+.2d. +8.2A.2p. +9.26.36. +9.k.4+. +b.3G.1G. +b.3J.1B. +b.3R.1r. +c.37.22. +f.1V.3q. +0.1/.39. +0.11.4j. +0.1P.3y. +0.1V.3r. +0.1y.3N. +0.g.53. +2./.4l. +2.1.5j. +2.1d.47. +2.7.5d. +2.A.4O. +2.c.5a. +3.3K.1B. +3.3M.1z. +4.3f.1Y. +4.e.55. +5.1h.3/. +5.2a.30. +6.3L.1A. +6.3S.1r. +6.4c.17. +7.2p.2B. +7.u.4V. +9.1+.3a. +9.22.38. +9.k.4/. +b.3G.1H. +b.3J.1C. +b.3R.1s. +c.28.34. +c.Y.4q. +f.1W.3q. +f.37.23. +0.1h.40. +0.1k.3Y. +0.1Q.3y. +0.1z.3N. +0.20.39. +0.2n.2D. +0.h.53. +0.Z.4q. +1.1V.3s. +1.u.4W. +2.2.5j. +2.8.5d. +2.A.4P. +2.c.5b. +3.3K.1C. +3.3M.1A. +4.12.4j. +4.f.55. +5./.4m. +6.3L.1B. +6.3S.1s. +6.4c.18. +7.2p.2C. +9.1+.3b. +9.k.50. +9.r.4X. +b.3G.1I. +b.3J.1D. +b.3R.1t. +c.37.24. +e.7.5e. +f.1W.3r. +0.1A.3N. +0.1i.40. +1./.4n. +2.2/.2d. +2.3.5j. +2.9.5d. +2.A.4Q. +3.1l.3Y. +3.3K.1D. +3.3M.1B. +4.1X.3q. +4.g.55. +4.i.53. +5.1h.41. +5.k.51. +6.3L.1C. +6.3S.1t. +6.4c.19. +6.58.d. +9.1+.3c. +9.24.38. +9.r.4Y. +9.s.4X. +b.3G.1J. +b.3R.1u. +e.8.5e. +f.1R.3v. +f.2g.2M. +f.37.25. +g.2E.2n. +j.21.39. +0.1B.3N. +0.1j.40. +0.1m.3Y. +0.1S.3v. +0.1Z.3e. +0.22.39. +2.4.5j. +2.A.4R. +2.a.5d. +3.1u.3S. +3.3M.1C. +4.1X.3r. +4.h.55. +4.j.53. +5./.4o. +5.1h.42. +5.1t.3T. +5.k.52. +6.3L.1D. +6.4c.1a. +6.58.e. +9.1+.3d. +9.s.4Y. +b.3J.1E. +b.3R.1v. +e.9.5e. +f.2c.30. +f.37.26. +0.1+.3e. +0.13.4j. +0.1C.3N. +0.1n.3Y. +0.1Y.3g. +0.23.39. +1.1h.43. +2.5.5j. +2.A.4S. +3.3K.1E. +3.3M.1D. +4.i.55. +4.k.53. +5./.4p. +5.1t.3U. +6.3S.1v. +6.4c.1b. +6.58.f. +7.2p.2D. +9.1V.3t. +9.22.3a. +9.26.38. +9.c.5c. +9.u.4X. +b.3J.1F. +b.3R.1w. +e.a.5e. +f.37.27. +0.+.4q. +0.1/.3e. +0.14.4j. +0.1D.3N. +0.1o.3Y. +0.24.39. +0.l.53. +1.r.4Z. +2.6.5j. +2.c.5d. +3.3K.1F. +4.j.55. +4.u.4Y. +5.1h.44. +5.1t.3V. +5.1V.3u. +5.k.54. +6.3L.1E. +6.3S.1w. +6.4c.1c. +6.58.g. +7.2p.2E. +9.22.3b. +b.3G.1K. +b.3J.1G. +b.3R.1x. +c.3f.1Z. +f.1R.3y. +0./.4q. +0.15.4j. +0.1k.40. +0.1p.3Y. +0.1S.3y. +0.1V.3v. +0.25.39. +0.30.2d. +0.m.53. +1.s.4Z. +2.0.5m. +2.7.5j. +2.A.4T. +3.3K.1G. +3.3M.1E. +4.3f.1+. +4.k.55. +5.1h.45. +5.1t.3W. +6.3L.1F. +6.3S.1x. +6.58.h. +9.22.3c. +9.24.3a. +b.3G.1L. +b.3J.1H. +b.3R.1y. +f.20.3e. +f.2g.2N. +f.37.28. +i.c.5e. +0.10.4q. +0.16.4j. +0.1E.3N. +0.1q.3Y. +0.1W.3v. +0.26.39. +0.3h.1Y. +1.k.56. +2.1.5m. +2.8.5j. +2.A.4U. +2.c.5f. +3.1l.40. +3.3K.1H. +3.3M.1F. +3.n.53. +4.l.55. +5./.4r. +5.1h.46. +5.1t.3X. +5.1V.3w. +6.3L.1G. +6.3S.1y. +6.4c.1d. +6.58.i. +9.22.3d. +9.24.3b. +b.3G.1M. +b.3J.1I. +b.3R.1z. +c.3f.1/. +f.2g.2O. +j.21.3e. +0.11.4q. +0.17.4j. +0.1F.3N. +0.1m.40. +0.1r.3Y. +0.22.3e. +0.27.39. +1.31.29. +1.u.4Z. +2.1d.4d. +2.2.5m. +2.9.5j. +3.3K.1I. +3.3M.1G. +4.m.55. +4.o.53. +5./.4s. +5.1h.47. +5.2g.2P. +5.c.5g. +5.k.57. +6.3i.1Y. +6.3L.1H. +6.3S.1z. +6.58.j. +9.1V.3x. +9.24.3c. +9.26.3a. +9.r.4+. +b.3G.1N. +b.3J.1J. +b.3R.1A. +c.3f.20. +0.18.4j. +0.1G.3N. +0.1n.40. +0.1s.3Y. +0.1V.3y. +0.1X.3v. +0.1Z.3g. +0.p.53. +1.31.2a. +1.c.5h. +2.3.5m. +2.A.4W. +2.a.5j. +3.3K.1J. +3.3M.1H. +3.n.55. +4.12.4q. +4.3f.21. +5./.4t. +5.1h.48. +6.3L.1I. +6.3S.1A. +8.58.k. +9.24.3d. +9.26.3b. +9.r.4/. +9.s.4+. +b.3G.1O. +b.3R.1B. +f.23.3e. +0.1+.3g. +0.19.4j. +0.1H.3N. +0.1o.40. +0.1t.3Y. +0.1W.3y. +0.24.3e. +2.1V.3z. +2.4.5m. +2.b.5j. +2.c.5i. +2.k.59. +3.3M.1I. +4.d.5e. +4.o.55. +4.q.53. +5.1h.49. +6.3L.1J. +6.3S.1B. +6.4c.1e. +6.58.l. +9.26.3c. +9.r.50. +9.s.4/. +a.2F.2n. +b.3G.1P. +b.3R.1C. +c.28.39. +c.3f.22. +f.2g.2Q. +0.1/.3g. +0.1a.4j. +0.1I.3N. +0.1p.40. +1.1h.4a. +2.1d.4f. +2.5.5m. +2.c.5j. +2.k.5a. +3.1u.3Y. +3.3M.1J. +4.e.5e. +4.p.55. +5./.4u. +5.2g.2R. +6.3S.1C. +6.4c.1f. +6.58.m. +7.r.51. +9.1V.3A. +9.26.3d. +9.s.50. +9.u.4+. +b.3G.1Q. +b.3J.1K. +b.3R.1D. +c.3f.23. +f.25.3e. +0.13.4q. +0.1b.4j. +0.1J.3N. +0.1q.40. +0.1v.3Y. +0.1X.3y. +0.1Z.3h. +0.26.3e. +0.2G.2n. +1.1h.4b. +1.31.2c. +2.1d.4g. +2.6.5m. +2.A.4X. +2.k.5b. +3.3K.1K. +4.f.5e. +4.q.55. +5./.4v. +5.1t.3Z. +6.3m.1Y. +6.3S.1D. +6.4c.1g. +6.58.n. +7.r.52. +7.s.51. +9.u.4/. +a.32.29. +b.3J.1L. +c.3f.24. +f.20.3g. +0.1+.3h. +0.14.4q. +0.1c.4j. +0.1r.40. +0.1w.3Y. +0.27.3e. +0.r.53. +2.1d.4h. +2.7.5m. +2.A.4Y. +3.3K.1L. +4.g.5e. +5./.4w. +5.1t.3+. +5.1V.3B. +6.3i.1Z. +6.3L.1K. +6.58.o. +7.2p.2F. +7.s.52. +9.c.5k. +9.u.50. +a.32.2a. +b.3J.1M. +b.3R.1E. +c.3f.25. +f.2g.2S. +j.21.3g. +0.1/.3h. +0.15.4q. +0.1s.40. +0.1x.3Y. +0.22.3g. +2.2H.2n. +2.8.5m. +3.3K.1M. +3.3M.1K. +4.h.5e. +4.s.53. +5.1t.3/. +5.1V.3C. +6.3i.1+. +6.3L.1L. +6.3S.1E. +6.58.p. +7.3f.26. +7.r.54. +7.u.51. +b.3J.1N. +b.3R.1F. +0.16.4q. +0.1d.4j. +0.1K.3N. +0.1t.40. +0.1y.3Y. +0.20.3h. +0.23.3g. +0.r.55. +0.t.53. +1.31.2d. +1.33.29. +2.1V.3D. +2.9.5m. +3.3K.1N. +3.3M.1L. +4./.4x. +4.i.5e. +6.3i.1/. +6.3L.1M. +6.3S.1F. +6.4c.1h. +6.58.q. +7.1+.3j. +7.2p.2G. +7.s.54. +7.u.52. +9.k.5c. +b.3G.1R. +b.3J.1O. +b.3R.1G. +c.28.3e. +c.3f.27. +0.17.4q. +0.1L.3N. +0.1z.3Y. +0.24.3g. +0.29.34. +0.3q.1Y. +0.u.53. +1.33.2a. +1.r.56. +2.1d.4k. +2.A.4Z. +2.a.5m. +2.c.5l. +2.k.5d. +3.1u.40. +3.3K.1O. +3.3M.1M. +4.j.5e. +4.s.55. +5./.4y. +5.1h.4d. +5.1t.41. +6.3i.20. +6.3L.1N. +6.3S.1G. +6.4c.1i. +9.1+.3k. +9.1V.3E. +a.32.2c. +b.3G.1S. +b.3J.1P. +b.3R.1H. +j.21.3h. +0.18.4q. +0.1A.3Y. +0.1M.3N. +0.1v.40. +0.1Z.3m. +0.22.3h. +0.t.55. +0.v.53. +1.s.56. +2.1d.4l. +2.2H.2p. +2.b.5m. +3.3K.1P. +3.3M.1N. +4.3f.28. +4.k.5e. +5./.4z. +5.1t.42. +5.29.35. +5.2a.34. +6.3i.21. +6.3L.1O. +6.3r.1Y. +6.3S.1H. +6.4c.1j. +7.1+.3l. +7.r.57. +7.u.54. +b.3J.1Q. +b.3R.1I. +f.1V.3F. +f.25.3g. +0.1+.3m. +0.19.4q. +0.1B.3Y. +0.1e.4j. +0.1w.40. +0.23.3h. +0.26.3g. +0.w.53. +1.1t.43. +2.1d.4m. +2.2g.2T. +2.k.5f. +3.3K.1Q. +3.3M.1O. +4.1N.3N. +4.l.5e. +4.u.55. +5./.4A. +5.1h.4e. +5.29.36. +5.2a.35. +6.3i.22. +6.3L.1P. +6.3S.1I. +6.58.r. +7.s.57. +b.3R.1J. +f.2I.2n. +0.1/.3m. +0.1a.4q. +0.1C.3Y. +0.1f.4j. +0.1O.3N. +0.1x.40. +0.24.3h. +0.27.3g. +0.v.55. +0.x.53. +1.1+.3n. +1.33.2c. +1.c.5n. +1.u.56. +2.1d.4n. +2.A.4+. +2.r.59. +3.3M.1P. +4.m.5e. +5./.4B. +5.1h.4f. +5.1t.44. +5.k.5g. +6.3i.23. +6.3L.1Q. +6.3S.1J. +6.58.s. +7.22.3j. +a.32.2d. +b.3G.1V. +0.1b.4q. +0.1D.3Y. +0.1P.3N. +0.1y.40. +0.20.3m. +0.25.3h. +0.2c.34. +0.y.53. +1.k.5h. +2.1d.4o. +2.A.4/. +2.r.5a. +2.s.59. +3.3M.1Q. +3.n.5e. +4.w.55. +5./.4C. +5.1h.4g. +5.1t.45. +5.c.5o. +6.3i.24. +6.4c.1k. +6.58.t. +7.1+.3o. +7.u.57. +9.22.3k. +b.3G.1W. +h.1g.4j. +0.1c.4q. +0.1Q.3N. +0.1z.40. +0.z.53. +2.1d.4p. +2.A.50. +2.c.5p. +2.k.5i. +2.r.5b. +2.s.5a. +3.1l.4c. +4.26.3h. +4.o.5e. +4.x.55. +5./.4D. +5.1h.4h. +5.1t.46. +6.3i.25. +6.58.u. +7.22.3l. +7.24.3j. +7.2p.2I. +9.1+.3p. +b.3J.1R. +b.3R.1K. +c.28.3g. +e.21.3m. +f.1Z.3q. +0.1A.40. +0.1E.3Y. +0.22.3m. +0.27.3h. +0.y.55. +1.1h.4i. +1.33.2d. +2./.4E. +2.A.51. +2.k.5j. +2.s.5b. +2.u.59. +3.3K.1R. +4.p.5e. +5.1t.47. +5.c.5q. +6.3i.26. +6.3S.1K. +6.4c.1m. +6.58.v. +9.24.3k. +b.3G.1X. +b.3J.1S. +b.3R.1L. +d.2U.2g. +f.1+.3q. +f.1Z.3r. +f.37.29. +0.1/.3q. +0.1+.3r. +0.1d.4q. +0.1F.3Y. +0.1h.4j. +0.1Y.3v. +0.23.3m. +0.34.2d. +1.22.3n. +2.A.52. +2.u.5a. +3.3K.1S. +4.1B.40. +4.55.z. +4.q.5e. +5./.4F. +5.1t.48. +5.1V.3H. +5.29.38. +6.3i.27. +6.3L.1R. +6.3S.1L. +6.4c.1n. +6.58.w. +7.24.3l. +7.26.3j. +7.37.2a. +b.3R.1M. +0.1/.3r. +0.1C.40. +0.1G.3Y. +0.1i.4j. +0.24.3m. +1.1+.3s. +2.1d.4r. +2.u.5b. +3.3M.1R. +4.A.53. +4.k.5k. +5./.4G. +5.1h.4k. +5.1t.49. +5.1V.3I. +6.3L.1S. +6.3S.1M. +6.4c.1o. +6.58.x. +7.22.3o. +9.26.3k. +9.r.5c. +a.2J.2n. +b.3R.1N. +c.28.3h. +f.20.3q. +0.1D.40. +0.1H.3Y. +0.1j.4j. +0.1R.3N. +0.20.3r. +0.25.3m. +0.B.53. +1.1t.4a. +1.24.3n. +2.1d.4s. +2.1h.4l. +2.A.54. +2.r.5d. +3.3M.1S. +6.3i.28. +6.3S.1N. +6.4c.1p. +6.58.y. +7.26.3l. +9.22.3p. +9.s.5c. +b.3J.1V. +b.3R.1O. +j.21.3q. +0.1e.4q. +0.1I.3Y. +0.1S.3N. +0.29.39. +0.C.53. +0.r.5e. +1.1t.4b. +1.c.5r. +2.1d.4t. +2.s.5d. +3.3K.1V. +4.26.3m. +4.A.55. +5.1h.4m. +6.3S.1O. +6.3y.1Y. +6.4c.1q. +6.58.z. +b.3J.1W. +b.3R.1P. +c.37.2c. +e.21.3r. +f.22.3q. +f.24.3o. +0.1E.40. +0.1f.4q. +0.1J.3Y. +0.22.3r. +0.27.3m. +0.D.53. +1.1h.4n. +1.26.3n. +2.A.56. +2.k.5l. +2.r.5f. +3.3K.1W. +4.B.55. +4.s.5e. +5./.4H. +5.29.3a. +5.2a.39. +6.3L.1V. +6.3S.1P. +6.4c.1r. +7.2p.2J. +9.1+.3t. +9.24.3p. +9.u.5c. +b.3R.1Q. +f.23.3q. +f.2g.2V. +i.c.5s. +0.1F.40. +0.1k.4j. +0.23.3r. +0.24.3q. +0.2K.2n. +1.22.3s. +1.2f.35. +2.A.57. +2.s.5f. +2.u.5d. +3.3M.1V. +4.C.55. +4.E.53. +4.t.5e. +5./.4I. +5.1h.4o. +5.29.3b. +6.3L.1W. +6.3S.1Q. +6.4c.1s. +6.4q.1g. +7.1+.3u. +7.26.3o. +7.r.5g. +b.3J.1X. +f.1Z.3v. +f.2g.2W. +0.1+.3v. +0.1G.40. +0.1V.3N. +0.24.3r. +1.2f.36. +1.r.5h. +2.1d.4v. +2.2g.2X. +3.1l.4j. +3.3K.1X. +3.3M.1W. +4.D.55. +4.F.53. +4.s.5g. +4.u.5e. +5./.4J. +5.1h.4p. +5.29.3c. +6.4c.1t. +6.58.A. +9.26.3p. +c.28.3m. +c.37.2d. +f.25.3q. +0.1/.3v. +0.1H.40. +0.1K.3Y. +0.1m.4j. +0.1W.3N. +0.26.3q. +0.2c.39. +1.24.3s. +1.k.5n. +1.s.5h. +2.1d.4w. +2.r.5i. +2.u.5f. +3.1u.4c. +4.E.55. +4.v.5e. +5./.4K. +5.1t.4d. +5.1V.3O. +5.29.3d. +6.3L.1X. +6.3r.25. +6.58.B. +9.1+.3w. +0.1h.4q. +0.1I.40. +0.1L.3Y. +0.1n.4j. +0.1Z.3y. +0.20.3v. +0.29.3e. +0.d.5s. +2.1V.3P. +2.A.5a. +2.r.5j. +2.s.5i. +3.3M.1X. +4.F.55. +4.u.5g. +4.w.5e. +5./.4L. +5.k.5o. +6.3r.26. +6.4c.1v. +6.58.C. +7.2p.2K. +9.1+.3x. +9.22.3t. +b.3R.1R. +f.27.3q. +0.1+.3y. +0.1i.4q. +0.1J.40. +0.1M.3Y. +0.1o.4j. +0.1X.3N. +0.e.5s. +0.G.53. +1.26.3s. +1.u.5h. +2.1d.4x. +2.A.5b. +2.c.5t. +2.k.5p. +2.s.5j. +4.x.5e. +5./.4M. +5.1h.4r. +5.1t.4e. +5.2a.3e. +6.3r.27. +6.3S.1R. +6.4c.1w. +6.58.D. +7.22.3u. +b.3R.1S. +j.21.3v. +0.1/.3y. +0.1j.4q. +0.1p.4j. +0.22.3v. +0.f.5s. +2.1+.3z. +2.1d.4y. +2.u.5i. +4.1N.3Y. +4.H.53. +4.r.5k. +4.y.5e. +5./.4N. +5.1h.4s. +5.1t.4f. +5.1V.3Q. +5.k.5q. +6.3S.1S. +6.4c.1x. +6.58.E. +9.24.3t. +c.28.3q. +c.3f.29. +f.2g.2Y. +f.39.2d. +0.1O.3Y. +0.1q.4j. +0.20.3y. +0.23.3v. +0.2L.2n. +0.g.5s. +1.2f.38. +2.1d.4z. +2.u.5j. +3.I.53. +4.G.55. +4.s.5k. +4.z.5e. +5./.4O. +5.1h.4t. +5.1t.4g. +6.4c.1y. +6.58.F. +9.1+.3A. +9.22.3w. +c.28.3r. +c.3f.2a. +f.24.3u. +0.1K.40. +0.1P.3Y. +0.1r.4j. +0.24.3v. +0.2c.3e. +2./.4P. +3.J.53. +4.H.55. +4.h.5s. +5.1t.4h. +6.4c.1z. +9.22.3x. +9.26.3t. +b.3R.1V. +j.21.3y. +0.1k.4q. +0.1L.40. +0.1Q.3Y. +0.1s.4j. +0.22.3y. +0.25.3v. +0.i.5s. +0.K.53. +1.1t.4i. +2.1d.4B. +2.2g.2Z. +2.A.5d. +2.r.5l. +3.I.55. +4.u.5k. +5./.4Q. +5.1h.4u. +6.3S.1V. +6.4c.1A. +7.1+.3B. +7.26.3u. +9.24.3w. +b.3G.1Y. +b.3R.1W. +i.c.5u. +0.1M.40. +0.1t.4j. +0.23.3y. +0.29.3g. +0.j.5s. +0.L.53. +1.k.5r. +2.1d.4C. +2.22.3z. +2.s.5l. +3.1l.4q. +3.J.55. +4.26.3v. +4.A.5e. +5./.4R. +5.1+.3C. +5.1h.4v. +5.1V.3T. +6.3S.1W. +6.4c.1B. +6.58.G. +7.2p.2L. +9.24.3x. +c.3f.2c. +0.1m.4q. +0.24.3y. +0.27.3v. +0.2M.2n. +0.k.5s. +1.2f.3a. +2.1+.3D. +2.1d.4D. +3.1u.4j. +3.M.53. +4.1N.40. +4.2d.3e. +4.B.5e. +4.K.55. +5./.4S. +5.1h.4w. +5.1t.4k. +5.1V.3U. +5.2a.3g. +6.4c.1C. +6.58.H. +9.22.3A. +9.26.3w. +b.3R.1X. +0.1n.4q. +0.1O.40. +0.1v.4j. +0.25.3y. +0.L.55. +0.l.5s. +1.2f.3b. +1.r.5n. +2.1t.4l. +2.24.3z. +2.A.5g. +2.u.5l. +4.C.5e. +5.1V.3V. +6.3S.1X. +6.4c.1D. +6.58.I. +9.1+.3E. +9.26.3x. +0.1o.4q. +0.1P.40. +0.1w.4j. +0.26.3y. +0.29.3h. +0.d.5u. +0.m.5s. +1.2f.3c. +1.s.5n. +2./.4T. +2.A.5h. +3.M.55. +4.D.5e. +5.1h.4x. +5.1t.4m. +5.1V.3W. +6.58.J. +7.22.3B. +7.r.5o. +9.1+.3F. +9.24.3A. +c.28.3v. +c.3f.2d. +f.1R.3Y. +0.1p.4q. +0.1Q.40. +0.1S.3Y. +0.1x.4j. +0.27.3y. +0.e.5u. +0.N.53. +1.1t.4n. +1.2f.3d. +2./.4U. +2.26.3z. +2.r.5p. +3.n.5s. +4.E.5e. +5.1h.4y. +5.1V.3X. +5.22.3C. +5.2a.3h. +6.3i.29. +6.4c.1E. +6.58.K. +7.2p.2M. +7.s.5o. +b.3G.1Z. +f.2c.3g. +0.1q.4q. +0.1y.4j. +0.2N.2n. +0.f.5u. +0.o.5s. +1.u.5n. +2.22.3D. +2.s.5p. +4.F.5e. +4.O.53. +5./.4V. +5.1h.4z. +5.1t.4o. +5.29.3j. +6.3i.2a. +6.4c.1F. +6.58.L. +7.24.3B. +7.r.5q. +8.2g.2+. +9.26.3A. +b.3G.1+. +b.3J.1Y. +0.1r.4q. +0.1z.4j. +0.2O.2n. +0.g.5u. +0.P.53. +0.p.5s. +1./.4W. +2.k.5t. +3.3K.1Y. +4.N.55. +4.u.5o. +5.1h.4A. +5.1t.4p. +5.24.3C. +5.29.3k. +6.4c.1G. +6.58.M. +7.s.5q. +9.22.3E. +b.3G.1/. +c.28.3y. +0.1A.4j. +0.1s.4q. +0.1V.3Y. +0.2d.3g. +0.h.5u. +0.Q.53. +0.q.5s. +2.1d.4H. +2.24.3D. +2.A.5k. +2.u.5p. +4.O.55. +5.1h.4B. +5.29.3l. +6.3L.1Y. +6.4c.1H. +7.26.3B. +9.22.3F. +a.2P.2n. +b.3G.20. +f.2c.3h. +0.1B.4j. +0.1t.4q. +0.1W.3Y. +0.29.3m. +0.i.5u. +0.R.53. +2.2/.2g. +2.c.5v. +3.3M.1Y. +4.G.5e. +4.P.55. +4.u.5q. +5.1h.4C. +5.26.3C. +6.3i.2c. +6.4c.1I. +7.2p.2N. +9.24.3E. +b.3G.21. +f.1R.40. +0.1C.4j. +0.1S.40. +0.2Q.2n. +0.j.5u. +1.29.3n. +1.r.5r. +2.1d.4J. +2.26.3D. +2.c.5w. +3.1u.4q. +3.S.53. +4.H.5e. +4.Q.55. +5.1h.4D. +5.1t.4r. +5.1V.3Z. +6.3N.1Y. +6.4c.1J. +6.58.N. +7.1+.3H. +7.2p.2O. +9./.4X. +9.24.3F. +b.3G.22. +f.2a.3m. +0.1D.4j. +0.1v.4q. +0.1X.3Y. +0.2d.3h. +0.k.5u. +0.r.5s. +1.s.5r. +2.1d.4K. +2.1h.4E. +3.I.5e. +4.R.55. +5.1t.4s. +5.1V.3+. +5.29.3o. +6.58.O. +7.1+.3I. +7.2p.2P. +9./.4Y. +9.26.3E. +9.c.5x. +b.3G.23. +b.3J.1Z. +c.T.53. +f.2R.2n. +0.1w.4q. +0.l.5u. +0.s.5s. +2.1d.4L. +3.3K.1Z. +3.J.5e. +3.S.55. +4.U.53. +5.1h.4F. +5.1t.4t. +5.1V.3/. +5.29.3p. +6.3i.2d. +6.58.P. +9.26.3F. +b.3G.24. +b.3J.1+. +0.1E.4j. +0.1V.40. +0.1x.4q. +0.29.3q. +0.2S.2n. +0.m.5u. +1.u.5r. +2.1d.4M. +3.3K.1+. +4.K.5e. +4.t.5s. +4.V.53. +5.1h.4G. +6.3L.1Z. +6.4c.1K. +6.58.Q. +7.2p.2Q. +9.c.5y. +b.3G.25. +b.3J.1/. +c.T.55. +f.2c.3m. +l.2g.30. +0.1F.4j. +0.1W.40. +0.1y.4q. +0.29.3r. +0.L.5e. +0.W.53. +1./.4Z. +2.1d.4N. +2.A.5n. +3.3K.1/. +3.3M.1Z. +3.n.5u. +4.U.55. +4.u.5s. +5.1t.4u. +5.1V.41. +5.2a.3q. +6.3L.1+. +6.4c.1L. +6.58.R. +7.22.3H. +7.2p.2R. +9.c.5z. +b.3G.26. +b.3J.20. +0.1G.4j. +0.1z.4q. +0.o.5u. +0.X.53. +1.29.3s. +2.1d.4O. +3.3K.20. +3.3M.1+. +3.M.5e. +4.V.55. +4.v.5s. +5.1t.4v. +5.1V.42. +5.2q.2w. +5.3r.2a. +6.3L.1/. +6.4c.1M. +6.58.S. +7.22.3I. +b.3G.27. +b.3J.21. +f.1Z.3N. +0.1+.3N. +0.1A.4q. +0.1H.4j. +0.1X.40. +0.p.5u. +1.1V.43. +2.1d.4P. +2.r.5t. +3.3K.21. +3.3M.1/. +4.W.55. +4.w.5s. +5.1h.4H. +5.1t.4w. +6.3L.20. +6.3m.2d. +6.4c.1N. +6.58.T. +7.24.3H. +7.2p.2S. +9.c.5A. +b.3J.22. +b.3R.1Y. +c.Y.53. +0.1/.3N. +0.1B.4q. +0.1I.4j. +0.q.5u. +0.Z.53. +1.2f.3j. +1.c.5B. +2.1d.4Q. +2.s.5t. +3.3K.22. +3.3M.20. +4.X.55. +4.x.5s. +5.1h.4I. +5.1V.44. +6.3L.21. +6.3S.1Y. +6.4c.1O. +6.58.U. +7.1+.3O. +7.24.3I. +9./.4+. +b.3G.28. +b.3J.23. +f.2c.3q. +0.1C.4q. +0.1J.4j. +0.20.3N. +1.2f.3k. +1.2n.2T. +2.1+.3P. +2.1d.4R. +3.3K.23. +3.3M.21. +4.1t.4x. +4.N.5e. +4.y.5s. +5.1h.4J. +5.1V.45. +5.29.3t. +6.3L.22. +6.3r.2c. +6.4c.1P. +6.58.V. +7.26.3H. +9./.4/. +b.3J.24. +c.Y.55. +0.1D.4q. +1.2f.3l. +2.1d.4S. +2.u.5t. +3.3K.24. +3.3M.22. +4.O.5e. +4.Z.55. +5.1h.4K. +5.1t.4y. +5.1V.46. +5.29.3u. +5.2q.2x. +6.3L.23. +6.4c.1Q. +6.58.W. +6.5s.z. +7.26.3I. +9./.50. +b.3J.25. +j.21.3N. +j.2a.3t. +0.22.3N. +0.29.3v. +0.2d.3q. +0.r.5u. +2.k.5v. +3.3K.25. +3.3M.23. +4.P.5e. +5./.51. +5.1h.4L. +5.1t.4z. +5.1V.47. +6.3L.24. +6.58.X. +7.1+.3Q. +9.c.5C. +b.3J.26. +0.+.53. +0.1E.4q. +0.1K.4j. +0.23.3N. +0.s.5u. +1.2f.3n. +2.1d.4T. +2.A.5r. +2.k.5w. +3.3K.26. +3.3M.24. +4.Q.5e. +5./.52. +5.1h.4M. +5.1t.4A. +5.1V.48. +5.29.3w. +5.2a.3v. +6.3L.25. +6.3r.2d. +6.58.Y. +7.22.3O. +9.c.5D. +b.3J.27. +b.3R.1Z. +0.1F.4q. +0.1L.4j. +0.24.3N. +0.A.5s. +0.t.5u. +1.2f.3o. +1.31.2g. +2.1d.4U. +2.22.3P. +3.3K.27. +3.3M.25. +4./.53. +4.R.5e. +5.1h.4N. +5.1t.4B. +5.1V.49. +5.29.3x. +6.3L.26. +6.3S.1Z. +6.58.Z. +b.3R.1+. +d.2U.2n. +e.k.5x. +j.2a.3w. +0.+.55. +0.1G.4q. +0.1M.4j. +0.25.3N. +0.29.3y. +0.B.5s. +0.u.5u. +1.1V.4a. +1.2f.3p. +2.c.5E. +3.3M.26. +3.S.5e. +4.10.53. +5./.54. +5.1h.4O. +5.1t.4C. +5.2a.3x. +5.2q.2y. +6.3L.27. +6.3S.1+. +6.4c.1R. +7.24.3O. +b.3J.28. +b.3R.1/. +0.1H.4q. +0.2a.3y. +0.C.5s. +0.v.5u. +1.1V.4b. +2.1d.4W. +2.1h.4P. +2.24.3P. +2.29.3z. +3.3K.28. +3.3M.27. +4./.55. +4.11.53. +4.1N.4j. +4.26.3N. +4.k.5y. +5.1t.4D. +6.3S.1/. +6.4c.1S. +7.1+.3T. +7.22.3Q. +8.2z.2q. +b.3R.20. +b.5F.c. +c.T.5e. +f.2c.3v. +0.1I.4q. +0.1O.4j. +0.1Y.3Y. +0.27.3N. +0.D.5s. +0.w.5u. +1./.56. +2.1t.4E. +3.5G.c. +4.10.55. +4.12.53. +4.26.3O. +4.U.5e. +5.1h.4Q. +5.29.3A. +6.3L.28. +6.3S.20. +7.1+.3U. +8.2A.2q. +9.k.5z. +b.3R.21. +d.2U.2p. +j.2a.3z. +0.1J.4q. +0.1P.4j. +0.E.5s. +0.x.5u. +1.2f.3s. +2.26.3P. +3.3M.28. +4.11.55. +4.2B.2q. +4.V.5e. +5./.57. +5.1h.4R. +5.1t.4F. +6.3S.21. +6.58.+. +7.1+.3V. +7.24.3Q. +b.3R.22. +f.2g.32. +j.2a.3A. +0.1Q.4j. +0.2c.3y. +0.2d.3v. +0.F.5s. +0.y.5u. +2.1d.4X. +2.A.5t. +4.12.55. +4.W.5e. +5.1h.4S. +5.1t.4G. +5.29.3B. +5.2q.2C. +6.3S.22. +6.4c.1V. +7.1+.3W. +8.58./. +9.k.5A. +a.2V.2n. +b.3R.23. +c.28.3N. +0.13.53. +0.5u.z. +1.k.5B. +2./.59. +2.1d.4Y. +2.c.5H. +3.2W.2n. +4.X.5e. +5.1V.4d. +5.29.3C. +6.3S.23. +6.4c.1W. +6.58.10. +7.1+.3X. +7.22.3T. +7.26.3Q. +b.3R.24. +b.5F.d. +0.1K.4q. +1.2f.3t. +1.2n.2X. +1.33.2g. +2./.5a. +2.1h.4T. +2.29.3D. +2.r.5v. +3.5G.d. +4.14.53. +5.2a.3C. +5.c.5I. +6.3S.24. +6.58.11. +7.22.3U. +b.3R.25. +b.5F.e. +c.Y.5e. +0.1L.4q. +0.1Y.40. +0.G.5s. +1.2f.3u. +2./.5b. +2.1h.4U. +2.r.5w. +2.s.5v. +3.5G.e. +4.13.55. +4.15.53. +4.Z.5e. +5.1t.4H. +5.1V.4e. +5.29.3E. +5.2g.34. +5.2q.2D. +6.3S.25. +6.3y.2d. +6.4c.1X. +6.58.12. +7.22.3V. +7.24.3T. +7.2p.2V. +b.3R.26. +b.5F.f. +f.1Z.3Y. +0.1+.3Y. +0.16.53. +0.1M.4q. +0.A.5u. +0.H.5s. +2.1d.4Z. +2.2g.35. +2.s.5w. +3.5G.f. +4.14.55. +4.k.5C. +5.1h.4V. +5.1t.4I. +5.1V.4f. +5.29.3F. +5.2E.2q. +6.3S.26. +7.22.3W. +7.24.3U. +7.2p.2W. +9.r.5x. +b.3R.27. +b.5F.g. +f.1R.4j. +0.1/.3Y. +0.1S.4j. +0.B.5u. +1.1h.4W. +1.2f.3w. +2.2g.36. +2.u.5v. +3.5G.g. +3.I.5s. +4.15.55. +4.17.53. +4.1N.4q. +5.1t.4J. +5.1V.4g. +6.3S.27. +7.22.3X. +7.24.3V. +7.26.3T. +9.k.5D. +9.s.5x. +b.5F.h. +j.2a.3F. +0.1O.4q. +0.20.3Y. +0.C.5u. +1.2f.3x. +2.u.5w. +3.5G.h. +3.J.5s. +4.16.55. +4.18.53. +4.r.5y. +5.1t.4K. +5.1V.4h. +6.58.13. +7.1+.3Z. +7.24.3W. +7.26.3U. +9./.5c. +a.2Y.2n. +b.3G.29. +b.3R.28. +b.5F.i. +0.+.5e. +0.19.53. +0.1P.4q. +0.D.5u. +0.K.5s. +1.1V.4i. +2./.5d. +2.1d.4+. +2.k.5E. +3.5G.i. +4.17.55. +4.s.5y. +5.1t.4L. +6.3S.28. +6.58.14. +7.24.3X. +7.26.3V. +9.r.5z. +9.u.5x. +b.3G.2a. +b.5F.j. +f.1+.3+. +j.21.3Y. +0.1Q.4q. +0.1V.4j. +0.1Z.40. +0.E.5u. +0.L.5s. +2.1d.4/. +2.2f.3z. +3.5G.j. +4./.5e. +4.18.55. +4.1a.53. +5.1h.4X. +5.1t.4M. +6.58.15. +7.1+.3/. +7.26.3W. +9.s.5z. +b.5F.k. +f.22.3Y. +0.1+.40. +0.19.55. +0.1W.4j. +0.23.3Y. +0.F.5u. +1.2f.3A. +1.2n.2Z. +2./.5f. +2.1d.50. +3.5G.k. +3.M.5s. +4.10.5e. +4.1b.53. +4.u.5y. +5.1h.4Y. +5.1t.4N. +5.1V.4k. +5.2g.37. +6.58.16. +7.26.3X. +7.2p.2Y. +9.r.5A. +b.5F.l. +0.1/.40. +0.24.3Y. +1.r.5B. +2.1d.51. +2.1V.4l. +2.2g.38. +3.5G.l. +4.11.5e. +4.1a.55. +4.1c.53. +4.u.5z. +5./.5g. +5.1t.4O. +5.29.3H. +6.58.17. +7.1+.41. +7.22.3Z. +9.s.5A. +b.3G.2c. +b.5F.m. +0.1X.4j. +0.20.40. +0.25.3Y. +1./.5h. +1.2f.3B. +1.s.5B. +2.1d.52. +2.1t.4P. +3.5G.m. +4.12.5e. +4.1b.55. +5.1V.4m. +5.29.3I. +6.58.18. +7.1+.42. +7.22.3+. +b.5F.n. +0.1d.53. +0.1R.4q. +0.G.5u. +0.N.5s. +1.1+.43. +1.1h.4Z. +1.1V.4n. +1.2f.3C. +2./.5i. +2.k.5H. +3.5G.n. +4.1c.55. +4.26.3Y. +4.u.5A. +5.1t.4Q. +5.2q.2F. +6.58.19. +7.22.3/. +7.24.3Z. +b.3J.29. +b.5F.o. +j.21.40. +0.1S.4q. +0.22.40. +0.27.3Y. +0.H.5u. +0.O.5s. +1.u.5B. +2./.5j. +2.1d.54. +2.2f.3D. +3.3K.29. +3.5G.o. +4.r.5C. +5.1t.4R. +5.1V.4o. +5.k.5I. +6.58.1a. +7.1+.44. +7.24.3+. +b.3G.2d. +b.3J.2a. +b.5F.p. +f.2g.39. +0.23.40. +0.P.5s. +1.2f.3E. +2.2g.3a. +2.A.5w. +3.3K.2a. +3.5G.p. +3.I.5u. +4.13.5e. +4.1d.55. +4.s.5C. +5.1t.4S. +5.1V.4p. +5.2q.2G. +6.3L.29. +6.58.1b. +7.1+.45. +7.22.41. +7.24.3/. +7.26.3Z. +9.r.5D. +b.5F.q. +0.1e.53. +0.24.40. +0.Q.5s. +1.2f.3F. +2.1d.56. +2.2g.3b. +2.A.5x. +3.3M.29. +3.5G.q. +3.J.5u. +4./.5k. +4.14.5e. +5.1h.4+. +6.3L.2a. +6.58.1c. +7.1+.46. +7.22.42. +7.26.3+. +8.2+.2n. +9.s.5D. +c.28.3Y. +0.1V.4q. +0.29.3N. +0.K.5u. +0.R.5s. +1.22.43. +2.1d.57. +2.1t.4T. +2.2g.3c. +2.2H.2q. +2.r.5E. +3.3M.2a. +4.15.5e. +4.1f.53. +4.u.5C. +5.1h.4/. +7.1+.47. +7.24.41. +7.26.3/. +b.3J.2c. +f.25.40. +0.1e.55. +0.1W.4q. +0.L.5u. +2.1t.4U. +2.2g.3d. +2.A.5y. +2.c.5J. +2.s.5E. +3.S.5s. +4.16.5e. +4.26.40. +5.1h.50. +5.1V.4r. +5.29.3O. +5.2a.3N. +6.3K.2c. +6.4c.1Y. +6.58.1d. +7.1+.48. +7.22.44. +7.24.42. +9.u.5D. +b.5F.r. +h.1g.53. +0.27.40. +1.24.43. +2./.5l. +2.2/.2n. +2.29.3P. +2.A.5z. +3.5G.r. +3.M.5u. +4.17.5e. +4.1f.55. +5.1h.51. +5.1t.4V. +5.1V.4s. +6.3L.2c. +7.1+.49. +7.22.45. +7.26.41. +8.2+.2p. +b.5F.s. +c.T.5s. +f.2g.3e. +0.1X.4q. +0.U.5s. +1.1+.4a. +1.1t.4W. +2.1d.5a. +2.c.5K. +2.u.5E. +3.3M.2c. +3.5G.s. +4.18.5e. +4.55.1g. +5.1h.52. +5.1V.4t. +7.22.46. +7.24.44. +7.26.42. +b.3J.2d. +b.5F.t. +0.19.5e. +0.V.5s. +1.1+.4b. +1.26.43. +2.1d.5b. +3.3K.2d. +3.5G.t. +4.1h.53. +5.29.3Q. +5.2q.2I. +6.58.1e. +7.22.47. +7.24.45. +b.5F.u. +c.28.40. +f.2c.3N. +f.2g.3f. +0.N.5u. +0.W.5s. +1./.5n. +1.2f.3H. +2.2/.2p. +2.A.5B. +2.r.5H. +3.5G.u. +4.1a.5e. +4.1i.53. +5.1h.54. +5.1V.4u. +6.3L.2d. +6.58.1f. +7.22.48. +7.24.46. +7.26.44. +b.5F.v. +0.1j.53. +0.30.2n. +0.O.5u. +0.X.5s. +1.2f.3I. +2.s.5H. +3.3M.2d. +3.5G.v. +4.1b.5e. +4.1h.55. +5./.5o. +5.1V.4v. +6.4c.1Z. +6.58.1g. +7.22.49. +7.24.47. +7.26.45. +7.r.5I. +9.1t.4X. +b.3R.29. +b.5F.w. +0.P.5u. +1.1h.56. +1.22.4a. +2./.5p. +3.5G.w. +4.1c.5e. +4.1i.55. +4.1t.4Y. +5.1V.4w. +6.3N.2d. +6.3S.29. +6.4c.1+. +7.24.48. +7.26.46. +7.s.5I. +b.3R.2a. +b.5F.x. +c.Y.5s. +0.1j.55. +0.Q.5u. +1.22.4b. +2.1d.5d. +2.u.5H. +3.5G.x. +4.Z.5s. +5./.5q. +5.1h.57. +5.29.3T. +6.3S.2a. +6.4c.1/. +7.1+.4d. +7.24.49. +7.26.47. +b.5F.y. +f.2g.3g. +0.1k.53. +0.1Y.4j. +0.R.5u. +1.24.4a. +2.A.5D. +3.5G.y. +4.1d.5e. +5.29.3U. +6.4c.20. +7.26.48. +7.2p.30. +7.u.5I. +8.58.1h. +9.1V.4x. +b.5F.z. +1.1t.4Z. +1.24.4b. +2.1h.59. +3.1l.53. +3.5G.z. +3.S.5u. +5.1V.4y. +5.29.3V. +5.2q.2J. +6.4c.21. +6.58.1i. +7.1+.4e. +7.26.49. +b.3R.2c. +1.26.4a. +2.1d.5g. +2.1h.5a. +2.A.5E. +4.1k.55. +4.1m.53. +5.1V.4z. +5.29.3W. +6.3S.2c. +6.4c.22. +6.58.1j. +7.1+.4f. +8.2g.3h. +c.T.5u. +j.2a.3V. +0.+.5s. +0.1n.53. +0.U.5u. +1./.5r. +1.26.4b. +1.2f.3O. +2.1d.5h. +2.1h.5b. +2.k.5J. +3.1l.55. +4.1e.5e. +5.1V.4A. +5.29.3X. +5.2g.3i. +6.4c.23. +7.1+.4g. +7.22.4d. +b.5F.A. +0./.5s. +0.1o.53. +0.V.5u. +2.2f.3P. +3.5G.A. +4.1f.5e. +4.1m.55. +5.1V.4B. +5.2g.3j. +6.4c.24. +7.1+.4h. +9.1t.4+. +b.3R.2d. +b.5F.B. +0.10.5s. +0.1p.53. +0.1Z.4j. +0.W.5u. +1.1+.4i. +2.2g.3k. +2.k.5K. +3.5G.B. +4.1n.55. +5.1V.4C. +5.2q.2K. +6.3S.2d. +6.4c.25. +6.58.1k. +7.22.4e. +9.1t.4/. +b.5F.C. +f.24.4d. +h.1g.5e. +0.1+.4j. +0.11.5s. +0.1o.55. +0.29.3Y. +0.X.5u. +1.2f.3Q. +1.31.2n. +3.5G.C. +4.1q.53. +4.1t.50. +5.1h.5c. +5.1V.4D. +6.4c.26. +6.58.1l. +7.22.4f. +b.5F.D. +0.1/.4j. +2.1d.5k. +2.1h.5d. +2.1V.4E. +3.5G.D. +4.12.5s. +4.1p.55. +4.1r.53. +5.1t.51. +5.2a.3Y. +6.4c.27. +6.58.1m. +7.1+.4k. +7.22.4g. +7.24.4e. +7.26.4d. +b.5F.E. +c.Y.5u. +f.2g.3m. +0.20.4j. +0.Z.5u. +2.1+.4l. +2.A.5I. +3.5G.E. +4.1h.5e. +4.1q.55. +4.1s.53. +5.1t.52. +5.1V.4F. +5.29.3Z. +6.4q.1Y. +6.58.1n. +7.22.4h. +b.5F.F. +f.24.4f. +1.22.4i. +1.31.2p. +2./.5t. +2.1h.5f. +2.2g.3o. +3.5G.F. +4.1i.5e. +4.1r.55. +4.1t.53. +5.1V.4G. +5.29.3+. +6.4c.28. +6.58.1o. +7.1+.4m. +7.24.4g. +7.26.4e. +e.21.4j. +0.13.5s. +0.1j.5e. +0.22.4j. +1.1+.4n. +1.2f.3T. +2.2g.3p. +3.1u.53. +4.1s.55. +4.26.4f. +5.1h.5g. +5.29.3/. +6.58.1p. +7.24.4h. +a.32.2n. +e.1t.54. +f.2c.3Y. +0.14.5s. +0.1v.53. +0.23.4j. +0.29.40. +1.1h.5h. +1.24.4i. +1.2f.3U. +4.1t.55. +5.2g.3q. +5.2q.2L. +6.58.1q. +7.1+.4o. +7.22.4k. +7.26.4g. +b.5F.G. +0.+.5u. +0.15.5s. +0.24.4j. +1.1t.56. +1.2f.3V. +2.1h.5i. +2.22.4l. +2.r.5J. +3.1u.55. +3.5G.G. +4.1w.53. +5.1V.4H. +5.29.41. +5.2a.40. +6.58.1r. +7.1+.4p. +7.26.4h. +b.5F.H. +f.2g.3r. +0./.5u. +0.16.5s. +0.1x.53. +0.1Z.4q. +0.25.4j. +0.2d.3Y. +1.26.4i. +1.2f.3W. +1.33.2n. +2.1d.5n. +2.1h.5j. +2.s.5J. +3.5G.H. +4.1k.5e. +4.1v.55. +5.1t.57. +5.1V.4I. +5.29.42. +6.58.1s. +7.22.4m. +7.24.4k. +7.2p.32. +b.5F.I. +0.1+.4q. +0.10.5u. +0.17.5s. +0.1y.53. +0.34.2n. +1.22.4n. +1.29.43. +1.2f.3X. +2.24.4l. +2.r.5K. +3.1l.5e. +3.5G.I. +4.1w.55. +4.26.4j. +5.1V.4J. +6.5L.c. +8.58.1t. +b.5F.J. +0.1/.4q. +0.11.5u. +0.18.5s. +0.27.4j. +1.2n.35. +2.1t.59. +2.s.5K. +2.u.5J. +3.5G.J. +4.1m.5e. +4.1x.55. +4.1z.53. +5.1h.5k. +5.1V.4K. +5.29.44. +5.2q.2M. +6.58.1u. +7.1+.4r. +7.22.4o. +7.24.4m. +7.26.4k. +b.5F.K. +f.2c.40. +0.19.5s. +0.20.4q. +1.24.4n. +1.2n.36. +1.33.2p. +2.1t.5a. +2.26.4l. +2.2g.3t. +2.c.5M. +3.5G.K. +4.12.5u. +4.1A.53. +4.1n.5e. +4.1y.55. +5.1V.4L. +5.29.45. +6.58.1v. +7.1+.4s. +7.22.4p. +b.5F.L. +0.1a.5s. +0.1B.53. +0.1o.5e. +2.1t.5b. +2.c.5N. +2.u.5K. +3.5G.L. +4.1z.55. +5.1V.4M. +5.29.46. +6.58.1w. +7.1+.4t. +7.24.4o. +7.26.4m. +7.2p.34. +b.5F.M. +c.28.4j. +f.2g.3u. +j.21.4q. +0.1b.5s. +0.1C.53. +0.22.4q. +0.2d.40. +1.26.4n. +2.1h.5l. +3.5G.M. +4.1A.55. +4.1p.5e. +5.1V.4N. +5.29.47. +6.58.1x. +6.5L.d. +7.24.4p. +f.2g.3v. +0.13.5u. +0.1B.55. +0.1c.5s. +0.23.4q. +1.2f.3Z. +2.2g.3w. +4.1D.53. +4.1q.5e. +5.1V.4O. +5.29.48. +5.2q.2N. +6.58.1y. +6.5L.e. +7.1+.4u. +7.22.4r. +7.26.4o. +9.c.5O. +0.14.5u. +0.1C.55. +0.24.4q. +1.2f.3+. +2.1d.5r. +2.1V.4P. +2.2g.3x. +4.1r.5e. +5.29.49. +5.2q.2O. +6.58.1z. +6.5L.f. +7.1+.4v. +7.22.4s. +7.26.4p. +9.1t.5c. +9.c.5P. +b.5F.N. +f.37.2n. +0.15.5u. +0.25.4q. +1.1h.5n. +1.29.4a. +1.2f.3/. +1.2n.38. +2.1t.5d. +3.5G.N. +4.1D.55. +4.1d.5s. +4.1E.53. +4.1s.5e. +5.1V.4Q. +5.2P.2q. +6.58.1A. +6.5L.g. +7.1+.4w. +7.22.4t. +b.5F.O. +f.24.4r. +f.2g.3y. +0.16.5u. +0.1F.53. +0.26.4q. +1.29.4b. +2./.5v. +3.5G.O. +4.1t.5e. +5.1h.5o. +5.1V.4R. +5.c.5Q. +6.58.1B. +6.5L.h. +7.24.4s. +b.5F.P. +0.17.5u. +0.1G.53. +0.27.4q. +1.2f.41. +2./.5w. +2.1h.5p. +2.1t.5f. +2.2g.3A. +2.A.5J. +3.1u.5e. +3.5G.P. +4.1E.55. +5.1V.4S. +5.2q.2Q. +6.58.1C. +6.5L.i. +7.22.4u. +7.24.4t. +7.26.4r. +7.2p.37. +9.1+.4x. +b.5F.Q. +0.18.5u. +0.1e.5s. +1.2f.42. +3.5G.Q. +4.1F.55. +4.1H.53. +4.1v.5e. +5.1h.5q. +5.1t.5g. +5.2q.2R. +6.58.1D. +6.5L.j. +7.1+.4y. +7.22.4v. +7.26.4s. +9./.5x. +b.5F.R. +f.39.2n. +0.19.5u. +0.1f.5s. +1.1t.5h. +1.2f.43. +1.2n.3a. +2.1V.4T. +2.A.5K. +3.5G.R. +4.1G.55. +4.1I.53. +4.1w.5e. +6.4c.29. +6.5L.k. +7.1+.4z. +7.22.4w. +7.24.4u. +7.26.4t. +9.c.5R. +b.5F.S. +c.28.4q. +0.1a.5u. +1.2f.44. +2.1d.5t. +2.1t.5i. +2.1V.4U. +3.5G.S. +4./.5y. +4.1H.55. +4.1J.53. +4.1x.5e. +5.29.4d. +5.2q.2S. +6.4c.2a. +6.58.1E. +6.5L.l. +6.5s.1g. +7.1+.4A. +7.24.4v. +b.5F.T. +0.1b.5u. +1.2f.45. +1.2n.3c. +2.1t.5j. +2.k.5M. +3.5G.T. +4.1I.55. +4.1y.5e. +5.1V.4V. +6.58.1F. +6.5L.m. +7.1+.4B. +7.24.4w. +7.26.4u. +7.2p.39. +9./.5z. +9.22.4x. +b.5F.U. +0.1c.5u. +1.1h.5r. +1.1V.4W. +1.2f.46. +1.2n.3d. +2.2g.3E. +2.k.5N. +3.5G.U. +3.n.5L. +4.1J.55. +4.1z.5e. +5.29.4e. +6.58.1G. +7.1+.4C. +7.22.4y. +7.26.4v. +b.5F.V. +0.1h.5s. +0.2n.3e. +1.2f.47. +2.2g.3F. +3.5G.V. +4.1A.5e. +4.1K.53. +4.1t.5k. +5.29.4f. +6.4c.2c. +6.58.1H. +6.5L.o. +7.1+.4D. +7.22.4z. +7.26.4w. +9./.5A. +9.24.4x. +b.5F.W. +0.1d.5u. +0.1L.53. +1./.5B. +1.2f.48. +2.1+.4E. +2.c.5S. +3.5G.W. +4.1B.5e. +4.1i.5s. +4.k.5O. +5.29.4g. +6.58.1I. +6.5L.p. +7.22.4A. +7.24.4y. +b.5F.X. +0.1j.5s. +0.1M.53. +1.2f.49. +3.5G.X. +4.1C.5e. +4.1K.55. +5.29.4h. +5.2g.3G. +5.2q.2T. +6.58.1J. +6.5L.q. +7.1+.4F. +7.22.4B. +7.24.4z. +9.26.4x. +9.k.5P. +b.5F.Y. +c.3f.2n. +f.1V.4X. +1.29.4i. +1.2f.4a. +2.1t.5l. +3.5G.Y. +4.1D.5e. +4.1L.55. +4.1N.53. +6.4c.2d. +7.1+.4G. +7.22.4C. +7.24.4A. +7.26.4y. +7.2p.3e. +b.5F.Z. +f.1V.4Y. +0.1e.5u. +0.1O.53. +0.29.4j. +1.2f.4b. +3.5G.Z. +4./.5C. +4.1M.55. +5.k.5Q. +7.22.4D. +7.24.4B. +7.26.4z. +0.1f.5u. +0.1k.5s. +0.1P.53. +0.2a.4j. +2.1h.5t. +2.22.4E. +4.1E.5e. +4.1N.55. +5.29.4k. +6.58.1K. +6.5L.r. +7.24.4C. +7.26.4A. +7.2p.3f. +9./.5D. +0.1O.55. +0.1Q.53. +0.2n.3g. +0.5u.1g. +1.1t.5n. +1.1V.4Z. +2.29.4l. +3.1l.5s. +4.1F.5e. +6.58.1L. +6.5L.s. +7.1+.4H. +7.22.4F. +7.26.4B. +d.2U.2q. +f.24.4D. +f.2g.3H. +0.1m.5s. +0.1P.55. +2./.5E. +2.24.4E. +2.r.5M. +4.1G.5e. +4.k.5R. +5.1t.5o. +5.29.4m. +6.58.1M. +6.5L.t. +7.1+.4I. +7.22.4G. +7.26.4C. +b.5F.+. +f.2g.3I. +0.1n.5s. +0.1Q.55. +1.29.4n. +1.2f.4d. +2.1t.5p. +2.r.5N. +2.s.5M. +3.5G.+. +4.1H.5e. +5.2g.3J. +5.c.5T. +6.58.1N. +6.5L.u. +7.1+.4J. +7.24.4F. +7.26.4D. +b.5F./. +f.2c.4j. +0.1h.5u. +0.1o.5s. +0.3h.2n. +2.26.4E. +2.s.5N. +3.5G./. +4.1I.5e. +5.1t.5q. +5.29.4o. +6.58.1O. +6.5L.v. +7.1+.4K. +7.24.4G. +7.2p.3g. +b.5F.10. +f.1V.4+. +f.2g.3K. +0.1i.5u. +0.1p.5s. +1.2f.4e. +2.u.5M. +3.5G.10. +4.1J.5e. +4.r.5O. +5.29.4p. +6.3i.2n. +6.58.1P. +6.5L.w. +7.1+.4L. +7.22.4H. +7.26.4F. +9.1V.4/. +b.5F.11. +f.1R.53. +f.2g.3L. +0.1j.5u. +0.1q.5s. +0.2d.4j. +1.2f.4f. +1.2n.3j. +2.1d.5w. +2.u.5N. +3.5G.11. +4.1S.53. +5.2g.3M. +5.2q.2V. +6.58.1Q. +6.5L.x. +7.1+.4M. +7.22.4I. +7.26.4G. +9.r.5P. +9.s.5O. +b.5F.12. +f.1V.50. +0.1r.5s. +0.29.4q. +1.2f.4g. +1.2n.3k. +2./.5H. +2.1d.5x. +2.k.5S. +3.5G.12. +5.1V.51. +5.2g.3N. +6.5L.y. +7.1+.4N. +7.22.4J. +7.24.4H. +7.2p.3h. +9.s.5P. +f.1R.55. +f.2W.2q. +0.1s.5s. +1.1t.5r. +1.2f.4h. +1.2n.3l. +4.1K.5e. +4.1S.55. +4.u.5O. +5./.5I. +5.1V.52. +5.29.4r. +5.2a.4q. +5.2q.2X. +6.5L.z. +7.1+.4O. +7.22.4K. +7.24.4I. +7.2p.3i. +7.r.5Q. +f.2g.3O. +0.1k.5u. +0.1L.5e. +0.1t.5s. +0.1V.53. +1.2f.4i. +2.1+.4P. +2.1d.5y. +5.29.4s. +6.3m.2n. +7.22.4L. +7.24.4J. +7.26.4H. +7.s.5Q. +9.u.5P. +b.5F.13. +0.1W.53. +1.2n.3n. +2.1d.5z. +3.1l.5u. +3.1u.5s. +3.5G.13. +4.1M.5e. +5.1V.54. +5.29.4t. +6.58.1R. +7.1+.4Q. +7.22.4M. +7.24.4K. +7.26.4I. +b.5F.14. +0.1m.5u. +0.1V.55. +0.1v.5s. +0.2c.4q. +1.2f.4k. +1.2n.3o. +3.5G.14. +4.1N.5e. +6.58.1S. +6.5L.A. +7.1+.4R. +7.22.4N. +7.24.4L. +7.26.4J. +7.u.5Q. +9.r.5R. +b.5F.15. +f.2g.3Q. +0.1n.5u. +0.1O.5e. +0.1w.5s. +0.1X.53. +1.1V.56. +1.2n.3p. +2.2f.4l. +3.5G.15. +5.29.4u. +5.2q.2Y. +5.c.5U. +6.5L.B. +7.1+.4S. +7.22.4O. +7.24.4M. +7.26.4K. +9.s.5R. +b.5F.16. +f.1W.55. +0.1o.5u. +0.1P.5e. +0.1x.5s. +0.3q.2n. +1.2f.4m. +2.1d.5B. +2.1h.5v. +2.22.4P. +2.A.5M. +3.5G.16. +5.1V.57. +5.29.4v. +5.2g.3R. +6.5L.C. +7.24.4N. +7.26.4L. +b.5F.17. +0.1p.5u. +0.1X.55. +0.1y.5s. +1.2f.4n. +2.1+.4T. +2.1h.5w. +2.1t.5t. +2.A.5N. +3.5G.17. +5.29.4w. +5.c.5V. +5.k.5T. +6.3r.2n. +6.4q.2d. +6.5L.D. +7.22.4Q. +7.24.4O. +7.26.4M. +8.58.1V. +9.u.5R. +b.5F.18. +f.1Q.5e. +f.2g.3S. +0.1q.5u. +0.1z.5s. +1.2f.4o. +1.2n.3s. +2.1+.4U. +2.1V.59. +2.24.4P. +3.5G.18. +5.1h.5x. +5.2q.2Z. +6.58.1W. +6.5L.E. +7.22.4R. +7.26.4N. +b.5F.19. +f.2g.3T. +0.1A.5s. +0.1r.5u. +1.2f.4p. +2.1V.5a. +2.A.5O. +2.r.5S. +3.5G.19. +5.29.4x. +5.2g.3U. +6.5L.F. +7.1+.4V. +7.22.4S. +7.24.4Q. +7.26.4O. +7.2p.3q. +b.5F.1a. +0.1s.5u. +1.1+.4W. +2.1d.5D. +2.1V.5b. +2.26.4P. +2.A.5P. +2.s.5S. +3.5G.1a. +4.1B.5s. +5.1h.5y. +5.29.4y. +5.2g.3V. +6.58.1X. +7.24.4R. +7.2p.3r. +b.5F.1b. +0.1C.5s. +0.1R.5e. +0.1t.5u. +1.2n.3t. +2.22.4T. +2.c.5W. +3.5G.1b. +5.1h.5z. +5.29.4z. +5.2g.3W. +7.24.4S. +7.26.4Q. +b.5F.1c. +0.1D.5s. +1.2f.4r. +1.2n.3u. +2.1d.5E. +2.22.4U. +2.A.5Q. +2.c.5X. +2.u.5S. +3.1u.5u. +3.5G.1c. +4.1S.5e. +5.29.4A. +6.5L.G. +7.26.4R. +0.1v.5u. +0.2n.3v. +1.2f.4s. +2.24.4T. +2.c.5Y. +5.1h.5A. +5.1V.5c. +5.29.4B. +6.5L.H. +7.22.4V. +7.26.4S. +9.1+.4X. +b.5F.1d. +0.1E.5s. +0.1w.5u. +1.1h.5B. +1.22.4W. +1.2f.4t. +1.2n.3w. +2.1V.5d. +2.24.4U. +3.5G.1d. +3.I.5L. +5.29.4C. +5.c.5Z. +8.2+.2q. +9.1+.4Y. +0.1F.5s. +0.1V.5e. +0.1x.5u. +1.2n.3x. +2.26.4T. +2.c.5+. +3.J.5L. +5.29.4D. +5.2g.3Y. +7.24.4V. +0.1G.5s. +0.1W.5e. +0.1y.5u. +1.24.4W. +1.2f.4u. +2./.5J. +2.1V.5f. +2.26.4U. +2.29.4E. +5.k.5U. +6.3y.2n. +6.5L.K. +7.2p.3v. +7.r.5T. +b.5F.1e. +0.1H.5s. +0.1z.5u. +1.1+.4Z. +1.2f.4v. +2.2/.2q. +2.2n.3z. +3.5G.1e. +4.s.5T. +5.1h.5C. +5.1V.5g. +5.29.4F. +6.5L.L. +7.26.4V. +9.22.4X. +b.5F.1f. +0.1A.5u. +0.1I.5s. +0.1X.5e. +1.1V.5h. +1.26.4W. +1.2f.4w. +1.2n.3A. +2./.5K. +2.1d.5I. +3.5G.1f. +3.M.5L. +5.1h.5D. +5.29.4G. +5.k.5V. +9.22.4Y. +b.5F.1g. +f.2g.3+. +0.1B.5u. +0.1J.5s. +0.1Y.53. +2.1V.5i. +3.5G.1g. +4.u.5T. +7.2p.3y. +9.24.4X. +f.2g.3/. +j.2a.4G. +0.1C.5u. +1.2f.4x. +1.2n.3B. +2.1.65. +2.1h.5E. +2.1t.5v. +2.1V.5j. +2.c.5/. +9.1+.4+. +9.24.4Y. +f.2g.40. +0.1D.5u. +0.55.1Y. +1.22.4Z. +1.2f.4y. +1.2n.3C. +2.1t.5w. +2.2.65. +2.c.60. +5.29.4H. +5.2q.30. +6.5L.N. +9.1+.4/. +9.26.4X. +b.5F.1h. +f.2g.41. +0.1K.5s. +1.2f.4z. +2.2n.3D. +2.3.65. +2.k.5W. +3.5G.1h. +5.29.4I. +5.2g.42. +5.c.61. +6.5L.O. +9.1+.50. +9.1t.5x. +9.26.4Y. +b.5F.1i. +f.1V.5k. +0.1E.5u. +0.1L.5s. +1.24.4Z. +1.2f.4A. +1.2n.3E. +2.4.65. +2.k.5X. +3.5G.1i. +5.29.4J. +5.c.62. +6.5L.P. +7.1+.51. +b.5F.1j. +0.1F.5u. +0.1M.5s. +0.1Z.53. +1.2f.4B. +1.2n.3F. +2.k.5Y. +3.5G.1j. +4.1t.5y. +5.29.4K. +6.5L.Q. +7.1+.52. +8.58.1Y. +9.22.4+. +e.5.65. +f.2g.44. +0.1+.53. +0.1G.5u. +0.1N.5s. +1.26.4Z. +1.2f.4C. +2.1h.5H. +2.1V.5l. +2.6.65. +4.r.5U. +5.29.4L. +5.k.5Z. +6.5L.R. +9.1t.5z. +9.22.4/. +0.1/.53. +0.1H.5u. +0.1O.5s. +1.2f.4D. +2.7.65. +2.c.63. +2.k.5+. +3.S.5L. +4.s.5U. +5.1h.5I. +5.29.4M. +7.1+.54. +9.22.50. +9.24.4+. +b.3G.2n. +b.5F.1k. +f.1Z.55. +0.1I.5u. +0.1P.5s. +0.20.53. +2.2f.4E. +2.8.65. +3.5G.1k. +5.29.4N. +5.2g.47. +6.5L.T. +7.22.51. +7.r.5V. +9.1t.5A. +9.24.4/. +b.5F.1l. +f.1+.55. +0.1/.55. +0.1J.5u. +0.1Q.5s. +1.1+.56. +1.1t.5B. +1.1V.5n. +1.2f.4F. +2.9.65. +3.5G.1l. +4.s.5V. +4.u.5U. +5.29.4O. +6.5L.U. +7.22.52. +9.24.50. +9.26.4+. +b.5F.1m. +e.21.53. +f.2g.48. +0.20.55. +0.22.53. +1.2f.4G. +2.29.4P. +2.a.65. +2.c.64. +3.5G.1m. +5.1V.5o. +5.2g.49. +6.58.1Z. +6.5L.V. +7.1+.57. +7.24.51. +9.26.4/. +b.3G.2p. +b.5F.1n. +0.23.53. +1.2n.3H. +1.31.2q. +2.1V.5p. +2.b.65. +3.5G.1n. +4.u.5V. +5.29.4Q. +6.5L.W. +7.22.54. +8.58.1+. +9.26.50. +b.5F.1o. +f.24.52. +j.21.55. +0.1K.5u. +0.24.53. +1.2n.3I. +2.1+.59. +2.k.5/. +2.r.5W. +3.5G.1o. +4.1t.5C. +4.26.51. +5.1V.5q. +5.29.4R. +5.c.65. +6.58.1/. +6.5L.X. +b.5F.1p. +f.22.55. +0.1L.5u. +0.23.55. +0.25.53. +1.22.56. +1.2f.4H. +2.1+.5a. +2.k.60. +2.r.5X. +2.s.5W. +3.5G.1p. +4.1Y.5e. +4.26.52. +5.29.4S. +6.58.20. +6.5L.Y. +7.24.54. +9.1t.5D. +b.3J.2n. +b.5F.1q. +f.1R.5s. +0.1M.5u. +0.1S.5s. +0.24.55. +1.2f.4I. +2.1+.5b. +2.1d.5J. +2.c.66. +2.r.5Y. +2.s.5X. +3.3K.2n. +3.5G.1q. +4.26.53. +5.k.61. +6.58.21. +6.5L.Z. +7.22.57. +b.5F.1r. +0.1N.5u. +0.25.55. +0.27.53. +1.24.56. +1.2f.4J. +2.1t.5E. +2.29.4T. +2.s.5Y. +2.u.5W. +3.5G.1r. +4.r.5Z. +5.32.2q. +5.c.67. +5.k.62. +6.3L.2n. +7.26.54. +8.58.22. +b.5F.1s. +f.2g.4c. +0.1O.5u. +1.1V.5r. +1.2f.4K. +2.1d.5K. +2.22.59. +2.29.4U. +2.c.68. +2.r.5+. +2.u.5X. +3.3M.2n. +3.5G.1s. +4.26.55. +4.s.5Z. +6.58.23. +7.24.57. +b.3J.2p. +b.5F.1t. +f.2g.4d. +0.1P.5u. +1.26.56. +1.2f.4L. +2.22.5a. +2.s.5+. +2.u.5Y. +3.5G.1t. +5.29.4V. +6.3N.2n. +7.2p.3K. +8.58.24. +9.1+.5c. +b.5F.1u. +c.28.53. +f.1V.5s. +f.27.55. +0.1Q.5u. +0.1W.5s. +1.29.4W. +1.2f.4M. +1.2n.3O. +1.33.2q. +2.1+.5d. +2.22.5b. +2.24.59. +2.k.63. +3.5G.1u. +4.u.5Z. +6.58.25. +6.5L.+. +7.26.57. +7.2p.3L. +b.5F.1v. +f.1Z.5e. +f.2g.4e. +1.2f.4N. +2.24.5a. +2.2n.3P. +2.A.5V. +2.u.5+. +3.5G.1v. +5.2q.34. +5.c.69. +6.5L./. +7.2p.3M. +8.58.26. +b.5F.1w. +c.28.55. +f.1+.5e. +f.2g.4f. +0.1X.5s. +1.2f.4O. +2.1+.5f. +2.1t.5H. +2.24.5b. +2.26.59. +3.5G.1w. +5.2q.35. +5.c.6a. +6.5L.10. +7.2p.3N. +8.58.27. +b.5F.1x. +f.1/.5e. +f.2g.4g. +0.20.5e. +1.2n.3Q. +2./.5M. +2.26.5a. +2.2f.4P. +2.k.64. +2.r.5/. +3.5G.1x. +5.1t.5I. +5.29.4X. +5.2q.36. +6.5L.11. +7.1+.5g. +9.22.5c. +b.5F.1y. +f.2g.4h. +0.1R.5u. +1.1+.5h. +1.2f.4Q. +1.c.6b. +2./.5N. +2.1h.5J. +2.1V.5t. +2.22.5d. +2.26.5b. +2.r.60. +2.s.5/. +3.5G.1y. +5.29.4Y. +6.5L.12. +8.58.28. +b.5F.1z. +j.21.5e. +0.1S.5u. +1.2f.4R. +2.1+.5i. +2.A.5W. +2.s.60. +3.5G.1z. +5.k.65. +7.r.61. +9.24.5c. +b.3R.2n. +b.5F.1A. +f.22.5e. +f.2g.4j. +0.23.5e. +1.2f.4S. +2.1+.5j. +2.1h.5K. +2.22.5f. +2.24.5d. +2.u.5/. +3.5G.1A. +4.s.61. +6.3S.2n. +7.r.62. +9./.5O. +b.5F.1B. +f.2g.4k. +0.24.5e. +1.29.4Z. +1.2n.3T. +2.A.5Y. +2.k.66. +2.u.60. +3.5G.1B. +5.2q.37. +6.5L.13. +7.22.5g. +7.s.62. +9./.5P. +9.26.5c. +b.5F.1C. +0.1V.5u. +0.25.5e. +1.22.5h. +1.2n.3U. +2.24.5f. +2.26.5d. +2.2f.4T. +2.A.5Z. +3.5G.1C. +4.u.61. +5.2q.38. +5.k.67. +6.5L.14. +9.1+.5k. +b.3R.2p. +b.5F.1D. +f.2g.4m. +0.1W.5u. +1.2n.3V. +2.22.5i. +2.2f.4U. +2.k.68. +2.r.63. +3.5G.1D. +4.26.5e. +4.u.62. +5./.5Q. +6.5L.15. +7.24.5g. +7.2p.3S. +1.24.5h. +1.2f.4V. +1.2n.3W. +2.22.5j. +2.26.5f. +2.s.63. +5.29.4+. +6.5L.16. +b.5F.1E. +f.27.5e. +f.2g.4o. +0.1X.5u. +1.2f.4W. +1.2n.3X. +2.1+.5l. +2.24.5i. +3.5G.1E. +5.29.4/. +5.2q.39. +6.5L.17. +7.26.5g. +b.5F.1F. +f.2g.4p. +1.26.5h. +2.24.5j. +2.r.64. +2.u.63. +3.5G.1F. +5.29.50. +5.2q.3a. +5.c.6c. +5.k.69. +6.5L.18. +9./.5R. +9.22.5k. +b.5F.1G. +c.28.5e. +j.2a.4/. +2.26.5i. +2.s.64. +3.5G.1G. +5.29.51. +5.2q.3b. +5.k.6a. +6.5L.19. +b.5F.1H. +f.2g.4q. +0.2n.3Y. +1.1+.5n. +1.2f.4X. +2.26.5j. +2.A.5/. +3.5G.1H. +5.29.52. +5.2q.3c. +6.5L.1a. +7.r.65. +9.24.5k. +b.5F.1I. +0.29.53. +1.2f.4Y. +1.k.6b. +2.22.5l. +2.A.60. +2.u.64. +3.5G.1I. +5.2q.3d. +6.5L.1b. +7.1+.5o. +7.s.65. +b.5F.1J. +1.2n.3Z. +2.1+.5p. +2.A.61. +2.r.66. +3.5G.1J. +5.29.54. +5.2q.3e. +6.5L.1c. +6.5s.1Y. +9.26.5k. +f.2a.53. +1.2n.3+. +2./.5S. +2.1V.5v. +2.24.5l. +2.s.66. +4.r.67. +7.1+.5q. +7.2p.3Y. +7.u.65. +f.29.55. +1.22.5n. +1.29.56. +1.2f.4Z. +1.2n.3/. +2.1t.5J. +2.1V.5w. +2.r.68. +4.s.67. +5.2a.55. +5.2q.3f. +6.5L.1d. +b.5F.1K. +0.2n.40. +2.26.5l. +2.s.68. +2.u.66. +3.5G.1K. +5.29.57. +7.22.5o. +9.1V.5x. +b.5F.1L. +f.2c.53. +f.2g.4v. +1.24.5n. +1.2n.41. +2.1d.5M. +2.1t.5K. +2.22.5p. +2.c.6d. +3.5G.1L. +4.u.67. +5.2g.4w. +8.58.29. +b.5F.1M. +1.1+.5r. +1.2f.4+. +1.2n.42. +2.1d.5N. +2.29.59. +2.c.6e. +2.u.68. +3.5G.1M. +6.5L.1e. +7.22.5q. +7.24.5o. +7.r.69. +8.58.2a. +b.5F.1N. +f.1V.5y. +f.1Z.5s. +f.2c.55. +0.1+.5s. +0.2d.53. +1.26.5n. +1.2f.4/. +1.2n.43. +2.24.5p. +2.29.5a. +2.2g.4x. +3.5G.1N. +5.2q.3g. +6.5L.1f. +7.2p.40. +7.r.6a. +7.s.69. +9.1V.5z. +b.5F.1O. +0.1/.5s. +1.2f.50. +1.2n.44. +2.1d.5O. +2.29.5b. +3.5G.1O. +5.c.6f. +5.k.6c. +6.5L.1g. +7.24.5q. +7.26.5o. +7.s.6a. +b.5F.1P. +f.2g.4y. +0.20.5s. +0.2d.55. +1.2f.51. +1.2n.45. +1.r.6b. +2.1d.5P. +2.26.5p. +3.5G.1P. +5./.5T. +5.c.6g. +6.58.2c. +7.u.69. +9.1V.5A. +b.5F.1Q. +0.5u.1Y. +1.1V.5B. +1.22.5r. +1.2f.52. +1.2n.46. +1.s.6b. +3.5G.1Q. +7.26.5q. +7.u.6a. +e.21.5s. +f.2g.4A. +l.2q.3h. +1.2n.47. +2.1d.5Q. +5.29.5c. +5.2q.3i. +6.5L.1h. +f.22.5s. +1.24.5r. +1.2f.54. +1.2n.48. +1.u.6b. +2.1+.5t. +2.29.5d. +5.2q.3j. +6.58.2d. +6.5L.1i. +f.23.5s. +j.2a.5c. +0.24.5s. +1.2n.49. +1.c.6h. +2.1h.5M. +5.2q.3k. +6.5L.1j. +b.5F.1R. +f.1V.5C. +f.29.5e. +f.2g.4D. +0.25.5s. +1.26.5r. +1.2f.56. +1.2n.4a. +2.1h.5N. +2.29.5f. +3.5G.1R. +5.2a.5e. +5.2q.3l. +9.1V.5D. +b.5F.1S. +0.1Z.5u. +1.2f.57. +1.2n.4b. +3.5G.1S. +4.26.5s. +5.29.5g. +5.2q.3m. +f.2g.4F. +0.1+.5u. +0.27.5s. +1.29.5h. +1.2f.58. +1.c.6i. +2.1V.5E. +2.22.5t. +2.k.6d. +5.1h.5O. +6.5L.1k. +0.1/.5u. +1.c.6j. +2.29.5i. +2.2f.59. +2.k.6e. +3.1l.5L. +5.1h.5P. +5.2q.3o. +7.r.6c. +b.5F.1V. +f.2c.5e. +0.20.5u. +2.24.5t. +2.29.5j. +2.2f.5a. +2.c.6k. +3.5G.1V. +4.s.6c. +5./.5U. +5.2q.3p. +6.4c.2n. +6.5L.1m. +b.5F.1W. +c.28.5s. +1.2n.4d. +1.c.6l. +2.2f.5b. +3.5G.1W. +5.1h.5Q. +5.2g.4H. +5.2q.3q. +5.k.6f. +6.5L.1n. +j.21.5u. +0.22.5u. +0.2d.5e. +2.26.5t. +2.A.6b. +4.u.6c. +5./.5V. +5.29.5k. +5.2q.3r. +5.k.6g. +6.5L.1o. +b.5F.1X. +f.2g.4I. +0.23.5u. +1.2n.4e. +2.1V.5H. +3.5G.1X. +6.5L.1p. +7.2p.4c. +j.2a.5k. +0.24.5u. +1.2f.5c. +1.2n.4f. +2.c.6m. +5.1h.5R. +5.1V.5I. +6.5L.1q. +0.25.5u. +1.2n.4g. +2.29.5l. +2.2f.5d. +6.5L.1r. +1.2n.4h. +1.k.6h. +2./.5W. +4.26.5u. +5.2q.3t. +6.5L.1s. +0.27.5u. +1.2n.4i. +2./.5X. +2.1+.5v. +2.2f.5f. +2.r.6d. +5.2q.3u. +6.5L.1t. +0.2n.4j. +1.29.5n. +1.2f.5g. +2./.5Y. +2.1+.5w. +2.r.6e. +2.s.6d. +3.1u.5L. +5.2q.3v. +1.2f.5h. +1.2n.4k. +1.k.6i. +2.1h.5S. +2.1t.5M. +2.s.6e. +5./.5Z. +5.29.5o. +5.2q.3w. +6.5L.1v. +9.1+.5x. +c.28.5u. +1.k.6j. +2./.5+. +2.1t.5N. +2.29.5p. +2.2f.5i. +2.2n.4l. +2.u.6d. +4.r.6f. +5.2g.4Q. +5.2q.3x. +6.5L.1w. +1.2n.4m. +2.22.5v. +2.2f.5j. +2.k.6k. +2.u.6e. +4.s.6f. +5.29.5q. +5.2q.3y. +6.5L.1x. +7.2p.4j. +7.r.6g. +9.1+.5y. +f.2g.4R. +1.2n.4n. +1.k.6l. +2.22.5w. +6.5L.1y. +7.s.6g. +9.1+.5z. +9.1t.5O. +1.2f.5k. +1.2n.4o. +2.24.5v. +4.u.6f. +5.2q.3A. +6.5L.1z. +9.1t.5P. +9.22.5x. +1.2n.4p. +2.24.5w. +4.u.6g. +6.5L.1A. +9.1+.5A. +1.1+.5B. +1.29.5r. +1.r.6h. +2./.5/. +2.26.5v. +2.k.6m. +5.1t.5Q. +5.2q.3B. +6.5L.1B. +9.22.5y. +9.24.5x. +1.s.6h. +2./.60. +2.26.5w. +2.2f.5l. +5.1h.5T. +5.2q.3C. +6.4q.2n. +6.5L.1C. +9.22.5z. +b.5F.1Y. +f.29.5s. +1.2n.4r. +3.5G.1Y. +5./.61. +6.5L.1D. +9.24.5y. +9.26.5x. +f.2a.5s. +1.2n.4s. +1.r.6i. +1.u.6h. +5./.62. +5.2q.3E. +9.1+.5C. +9.1t.5R. +9.22.5A. +9.24.5z. +1.22.5B. +1.2f.5n. +1.2n.4t. +1.5s.2b. +1.r.6j. +1.s.6i. +2.1d.5V. +2.1V.5J. +5.2q.3F. +6.5L.1E. +7.2p.4q. +9.1+.5D. +9.26.5y. +0.2c.5s. +1.2f.5o. +1.s.6j. +2.2g.4X. +2.r.6k. +6.5L.1F. +9.24.5A. +9.26.5z. +1.24.5B. +1.2n.4u. +1.r.6l. +1.u.6i. +2./.63. +2.1+.5E. +2.1V.5K. +2.29.5t. +2.2f.5p. +2.2g.4Y. +2.s.6k. +5.3G.2q. +6.5L.1G. +b.5F.1Z. +1.2f.5q. +1.2n.4v. +1.s.6l. +1.u.6j. +3.5G.1Z. +6.5L.1H. +9.22.5C. +9.26.5A. +b.5F.1+. +1.26.5B. +1.2n.4w. +2.1d.5W. +2.1t.5S. +2.u.6k. +3.5G.1+. +4.2d.5s. +6.5L.1I. +9.22.5D. +b.5F.1/. +1.5s.2e. +1.u.6l. +2./.64. +2.r.6m. +3.5G.1/. +6.5L.1J. +9.24.5C. +b.5F.20. +0.29.5u. +1.2n.4x. +2.1d.5Y. +2.22.5E. +2.s.6m. +3.5G.20. +5.1h.5U. +5.2q.3H. +9.24.5D. +b.5F.21. +1.2f.5r. +1.2n.4y. +2.1+.5H. +2.1d.5Z. +3.5G.21. +5./.65. +5.2a.5u. +5.2q.3I. +9.26.5C. +b.5F.22. +1.2n.4z. +1.5s.2f. +2.24.5E. +2.2g.4+. +2.u.6m. +3.5G.22. +5.1h.5V. +5.3J.2q. +6.5L.1K. +7.1+.5I. +9.26.5D. +b.5F.23. +1.2n.4A. +2./.66. +3.5G.23. +6.3K.2q. +6.5L.1L. +b.5F.24. +e.2g.4/. +1.2n.4B. +1.4G.2i. +2.26.5E. +2.2g.50. +3.5G.24. +5./.67. +5.2q.3L. +6.5L.1M. +b.5F.25. +f.2c.5u. +1.2n.4C. +1.4G.2j. +2./.68. +2.22.5H. +3.5G.25. +5.1t.5T. +5.2g.51. +6.3M.2q. +6.5L.1N. +b.5F.26. +e.0.6o. +1.2n.4D. +2.1h.5W. +2.A.6k. +3.5G.26. +5.2g.52. +5.2q.3N. +6.5L.1O. +7.22.5I. +b.5F.27. +e.0.6p. +e.1.6o. +0.5u.2d. +2.1d.5/. +2.1h.5X. +2.24.5H. +2.2f.5t. +2.2n.4E. +3.5G.27. +5.2g.53. +5.2q.3O. +6.5L.1P. +e.1.6p. +e.2.6o. +1.2n.4F. +2.1d.60. +2.1h.5Y. +5./.69. +6.5L.1Q. +7.24.5I. +b.5F.28. +e.2.6p. +e.3.6o. +f.2g.54. +1.2n.4G. +2.1d.61. +2.26.5H. +2.29.5v. +3.5G.28. +5./.6a. +5.1h.5Z. +e.3.6p. +e.4.6o. +f.2g.55. +2.1h.5+. +2.29.5w. +2.A.6m. +5.2q.3Q. +7.26.5I. +e.4.6p. +e.5.6o. +1./.6b. +5.29.5x. +e.5.6p. +e.6.6o. +1.2n.4H. +5.3R.2q. +6.5L.1R. +e.0.6u. +e.6.6p. +e.7.6o. +f.2g.58. +j.2a.5x. +1.2n.4I. +5.29.5y. +5.2q.3S. +6.5L.1S. +e.0.6v. +e.1.6u. +e.7.6p. +e.8.6o. +1.2n.4J. +5.1t.5U. +5.29.5z. +5.2q.3T. +e.1.6v. +e.2.6u. +e.8.6p. +e.9.6o. +j.2a.5y. +1.2n.4K. +2.1h.5/. +5.2q.3U. +e.2.6v. +e.3.6u. +e.9.6p. +e.a.6o. +j.2a.5z. +1.2n.4L. +2.1h.60. +5.1t.5V. +5.29.5A. +5.2q.3V. +6.5L.1V. +9.c.6n. +e.3.6v. +e.4.6u. +e.a.6p. +e.b.6o. +1.29.5B. +1.2n.4M. +2.1+.5J. +5.1h.61. +5.2q.3W. +6.5L.1W. +e.4.6v. +e.5.6u. +e.b.6p. +i.c.6o. +j.2a.5A. +1.2n.4N. +2.1V.5M. +2.2g.5c. +5.1h.62. +5.2q.3X. +e.5.6v. +e.6.6u. +i.c.6p. +1.2n.4O. +1.c.6q. +2.1+.5K. +2.1V.5N. +5./.6c. +6.5L.1X. +e.6.6v. +e.7.6u. +2.1t.5W. +2.2f.5v. +2.2n.4P. +5.29.5C. +e.7.6v. +e.8.6u. +f.2g.5e. +0.d.6o. +1.2n.4Q. +2.1h.63. +2.1t.5X. +2.22.5J. +2.2f.5w. +5.1V.5O. +5.29.5D. +5.2q.3Y. +9.c.6r. +e.8.6v. +e.9.6u. +0.d.6p. +0.e.6o. +1.2f.5x. +1.2n.4R. +2.1t.5Y. +9.1V.5P. +e.9.6v. +e.a.6u. +i.c.6s. +0.e.6p. +0.f.6o. +1.2n.4S. +2.22.5K. +2.24.5J. +2.29.5E. +4.b.6u. +5.1t.5Z. +5.2q.3Z. +9.c.6t. +e.a.6v. +0.f.6p. +0.g.6o. +1.2f.5y. +2.1h.64. +2.1t.5+. +5.1V.5Q. +5.2q.3+. +b.5F.29. +e.b.6v. +i.c.6u. +0.g.6p. +0.h.6o. +1.2f.5z. +2.24.5K. +2.26.5J. +2.2n.4T. +3.5G.29. +5.2q.3/. +b.5F.2a. +i.c.6v. +0.d.6s. +0.h.6p. +0.i.6o. +2.2n.4U. +3.5G.2a. +5.1h.65. +5.2q.40. +0.e.6s. +0.i.6p. +0.j.6o. +1.2f.5A. +1.2n.4V. +2./.6d. +2.26.5K. +2.2g.5k. +5.1V.5R. +5.2q.41. +9.k.6n. +0.f.6s. +0.j.6p. +0.k.6o. +1.2f.5B. +1.2n.4W. +1.c.6w. +2./.6e. +2.1d.6b. +2.1h.66. +2.29.5H. +4.d.6u. +5.2q.42. +b.5F.2c. +0.d.6v. +0.g.6s. +0.k.6p. +0.l.6o. +2.1t.5/. +3.5G.2c. +4.e.6u. +5.1h.67. +5.29.5I. +h.c.6x. +0.e.6v. +0.h.6s. +0.l.6p. +0.m.6o. +1.k.6q. +2.1h.68. +2.1t.60. +4.f.6u. +5./.6f. +5.2q.44. +e.0.6E. +e.c.6y. +0.f.6v. +0.i.6s. +0.m.6p. +1.2f.5C. +1.2n.4X. +3.n.6o. +4.g.6u. +5./.6g. +5.1t.61. +5.2q.45. +b.5F.2d. +e.1.6E. +0.g.6v. +0.j.6s. +0.o.6o. +1.2f.5D. +1.2n.4Y. +2.1V.5S. +3.5G.2d. +3.n.6p. +4.h.6u. +5.1t.62. +5.2q.46. +9.k.6r. +e.2.6E. +0.h.6v. +0.k.6s. +0.o.6p. +0.p.6o. +4.i.6u. +5.1h.69. +5.2q.47. +e.3.6E. +e.5L.1Y. +h.d.6x. +0.i.6v. +0.l.6s. +0.p.6p. +0.q.6o. +2.2f.5E. +4.j.6u. +5.1h.6a. +5.2q.48. +9.k.6t. +e.4.6E. +f.2g.5o. +h.e.6x. +0.j.6v. +0.m.6s. +0.q.6p. +1./.6h. +1.2n.4Z. +2.1t.63. +4.k.6u. +5.2q.49. +8.6z.c. +e.5.6E. +h.f.6x. +0.k.6v. +1.1h.6b. +3.n.6s. +4.l.6u. +5.2g.5q. +9.r.6n. +e.6.6E. +h.g.6x. +0.l.6v. +0.o.6s. +0.r.6o. +4.m.6u. +9.c.6A. +9.s.6n. +e.7.6E. +h.h.6x. +0.m.6v. +0.p.6s. +0.r.6p. +0.s.6o. +1./.6i. +1.2n.4+. +2.1t.64. +6.5L.1Z. +e.8.6E. +e.n.6u. +h.i.6x. +i.c.6B. +0.s.6p. +0.t.6o. +1./.6j. +1.2n.4/. +1.k.6w. +1.r.6q. +2.2f.5H. +3.n.6v. +4.o.6u. +6.5L.1+. +6.6s.q. +8.6z.d. +9.u.6n. +e.9.6E. +h.j.6x. +0.o.6v. +0.t.6p. +0.u.6o. +1.2f.5I. +1.2n.50. +1.s.6q. +2./.6k. +4.p.6u. +5.1t.65. +5.4c.2q. +6.5L.1/. +8.6z.e. +9.c.6C. +e.a.6E. +h.k.6x. +0.p.6v. +0.u.6p. +0.v.6o. +1./.6l. +1.2n.51. +2.1+.5M. +4.b.6E. +4.q.6u. +4.r.6r. +5.2q.4d. +5.c.6D. +6.5L.20. +8.6z.f. +9.k.6y. +e.0.6J. +f.2g.5s. +h.l.6x. +0.d.6B. +0.q.6v. +0.r.6s. +0.v.6p. +0.w.6o. +1.2n.52. +1.u.6q. +2.0.6K. +2.1+.5N. +2.1t.66. +6.5L.21. +8.6z.g. +9.s.6r. +e.1.6J. +h.m.6x. +i.c.6E. +0.2n.53. +0.e.6B. +0.s.6s. +0.w.6p. +0.x.6o. +1.6L.0. +2.1.6K. +5.1t.67. +5.2q.4e. +6.5L.22. +8.6z.h. +9.r.6t. +e.2.6J. +h.n.6x. +0.f.6B. +0.r.6u. +0.t.6s. +0.x.6p. +0.y.6o. +1.2n.54. +1.6L.1. +2./.6m. +2.1t.68. +2.2.6K. +2.29.5J. +5.1h.6c. +5.2q.4f. +5.c.6F. +6.5L.23. +8.6z.i. +9.1+.5O. +9.s.6t. +9.u.6r. +e.3.6J. +h.o.6x. +0.55.2n. +0.g.6B. +0.r.6v. +0.u.6s. +0.y.6p. +1.6L.2. +2.22.5M. +2.3.6K. +4.s.6u. +5.2q.4g. +6.5L.24. +6.6o.z. +8.6z.j. +9.1+.5P. +e.4.6J. +h.p.6x. +0.s.6v. +0.t.6u. +0.v.6s. +1.2n.56. +1.6L.3. +2.22.5N. +2.29.5K. +4.d.6E. +4.h.6B. +4.u.6t. +5.2q.4h. +6.5L.25. +6.6p.z. +7.2p.53. +8.6z.k. +e.5.6J. +h.q.6x. +0.i.6B. +0.t.6v. +0.u.6u. +0.w.6s. +1.2n.57. +1.6L.4. +2.24.5M. +4.e.6E. +5.1t.69. +5.c.6G. +6.5L.26. +7.1+.5Q. +8.6z.l. +e.6.6J. +0.A.6o. +0.j.6B. +0.u.6v. +0.v.6u. +0.x.6s. +1.6L.5. +1.r.6w. +2.24.5N. +2.6.6K. +4.f.6E. +5.1t.6a. +5.2q.4j. +6.58.2n. +6.5L.27. +7.2p.55. +8.6z.m. +9.22.5O. +9.k.6A. +e.7.6J. +0.A.6p. +0.B.6o. +0.g.6E. +0.k.6B. +0.v.6v. +0.y.6s. +1.6L.6. +1.s.6w. +2.26.5M. +2.2n.59. +2.7.6K. +4.w.6u. +5.2q.4k. +8.6z.n. +9.22.5P. +e.8.6J. +h.r.6x. +k.6N.0. +0.B.6p. +0.C.6o. +0.h.6E. +0.l.6B. +0.w.6v. +0.x.6u. +1.1t.6b. +1.6L.7. +2.26.5N. +2.2n.5a. +2.8.6K. +4.r.6y. +6.5L.28. +6.6s.z. +8.6z.o. +9.1+.5R. +9.24.5O. +e.9.6J. +f.2g.5u. +h.s.6x. +0.C.6p. +0.D.6o. +0.m.6B. +0.x.6v. +0.y.6u. +1.6L.8. +1.u.6w. +2.2n.5b. +2.9.6K. +4.i.6E. +5.2q.4m. +7.22.5Q. +8.58.2p. +8.6z.p. +9.24.5P. +9.k.6C. +9.s.6y. +e.a.6J. +h.t.6x. +i.c.6H. +0.6u.z. +0.D.6p. +0.E.6o. +0.y.6v. +1.6L.9. +2.1h.6d. +2.a.6K. +3.n.6B. +4.j.6E. +5.k.6D. +8.6z.q. +9.26.5O. +9.c.6I. +e.b.6J. +h.u.6x. +0.6v.z. +0.A.6s. +0.E.6p. +0.F.6o. +0.o.6B. +1.6L.a. +2.1h.6e. +2.b.6K. +4.k.6E. +5.2q.4o. +7.24.5Q. +9.26.5P. +9.u.6y. +e.0.6O. +h.v.6x. +i.c.6J. +0.B.6s. +0.F.6p. +0.p.6B. +1.2n.5c. +1.6L.b. +2.1V.5W. +4.l.6E. +5.2q.4p. +9.22.5R. +9.c.6K. +e.1.6O. +h.w.6x. +0.C.6s. +0.d.6H. +0.q.6B. +0.x.6x. +1.6L.c. +2.1+.5S. +2.1V.5X. +2.2f.5J. +2.2n.5d. +4.A.6u. +4.m.6E. +5.1h.6f. +5.k.6F. +7.26.5Q. +8.6z.r. +e.0.6P. +e.2.6O. +0.A.6v. +0.D.6s. +0.e.6H. +0.G.6o. +2.1V.5Y. +3.n.6E. +4.B.6u. +5.1h.6g. +5.2q.4q. +8.6z.s. +9.24.5R. +e.1.6P. +e.3.6O. +f.2n.5e. +h.y.6x. +0.B.6v. +0.d.6J. +0.E.6s. +0.f.6H. +0.G.6p. +0.H.6o. +2.1d.6k. +2.2f.5K. +2.2n.5f. +4.C.6u. +4.o.6E. +4.r.6A. +5.1V.5Z. +5.2q.4r. +8.6z.t. +e.2.6P. +e.4.6O. +h.z.6x. +0.C.6v. +0.e.6J. +0.g.6H. +0.H.6p. +0.p.6E. +0.r.6B. +1.2n.5g. +2.1V.5+. +3.I.6o. +4.D.6u. +5.1t.6c. +5.2q.4s. +5.k.6G. +6.6s.F. +8.6z.u. +9.26.5R. +9.s.6A. +e.3.6P. +e.5.6O. +0.D.6v. +0.f.6J. +0.h.6H. +0.s.6B. +1.2n.5h. +1.6L.d. +2.22.5S. +3.I.6p. +3.J.6o. +4.E.6u. +4.q.6E. +5.2q.4t. +7.2p.5e. +8.6z.v. +e.4.6P. +e.6.6O. +i.c.6M. +0.E.6v. +0.g.6J. +0.i.6H. +0.K.6o. +1.1h.6h. +1.6L.e. +2.2n.5i. +3.J.6p. +4.F.6u. +4.t.6B. +8.6z.w. +9.r.6C. +9.u.6A. +e.5.6P. +e.7.6O. +h.A.6x. +0.F.6v. +0.G.6s. +0.h.6J. +0.j.6H. +0.K.6p. +0.L.6o. +1.6L.f. +2.1d.6m. +2.24.5S. +2.2g.5x. +2.2n.5j. +4.u.6B. +5.2q.4u. +7.r.6D. +8.6z.x. +9.s.6C. +e.6.6P. +e.8.6O. +h.B.6x. +k.6N.c. +0.H.6s. +0.i.6J. +0.k.6H. +0.L.6p. +0.r.6E. +1.6L.g. +3.M.6o. +4.v.6B. +5.2q.4v. +7.1+.5T. +7.s.6D. +8.6z.y. +e.7.6P. +e.9.6O. +h.C.6x. +0.d.6M. +0.G.6u. +0.l.6H. +0.s.6E. +0.w.6B. +1.1h.6i. +1.2n.5k. +1.6L.h. +2.0.6Q. +2.1V.5/. +2.26.5S. +2.2g.5y. +3.I.6s. +3.M.6p. +4.j.6J. +5.2q.4w. +8.6z.z. +9.k.6I. +9.u.6C. +e.8.6P. +e.a.6O. +h.D.6x. +0.e.6M. +0.G.6v. +0.k.6J. +0.m.6H. +0.t.6E. +1.1h.6j. +1.6L.i. +2.1.6Q. +2.1V.60. +2.2g.5z. +3.6R.0. +3.J.6s. +4.H.6u. +4.x.6B. +7.r.6F. +7.u.6D. +e.9.6P. +e.b.6O. +h.E.6x. +0.f.6M. +0.H.6v. +0.K.6s. +0.l.6J. +0.N.6o. +0.u.6E. +1.6L.j. +2.1h.6k. +2.2.6Q. +3.6R.1. +3.I.6u. +3.n.6H. +4.y.6B. +5.1V.61. +5.2q.4x. +7.s.6F. +9.k.6K. +e.0.6S. +e.a.6P. +h.F.6x. +i.c.6O. +0.g.6M. +0.L.6s. +0.m.6J. +0.N.6p. +0.o.6H. +0.O.6o. +0.v.6E. +1.1h.6l. +1.6L.k. +2.1t.6d. +2.2g.5A. +2.2n.5l. +2.3.6Q. +3.6R.2. +3.I.6v. +3.J.6u. +5.1V.62. +5.2q.4y. +6.5L.29. +6.6B.z. +7.22.5T. +8.6z.A. +e.1.6S. +e.b.6P. +0.h.6M. +0.O.6p. +0.p.6H. +0.P.6o. +0.w.6E. +1.6L.l. +2.1t.6e. +2.4.6Q. +3.6R.3. +3.J.6v. +3.M.6s. +4.K.6u. +4.r.6G. +4.u.6F. +5.2q.4z. +6.5L.2a. +8.6z.B. +e.2.6S. +e.n.6J. +i.c.6P. +0.i.6M. +0.K.6v. +0.L.6u. +0.o.6J. +0.P.6p. +0.q.6H. +0.Q.6o. +0.x.6E. +1.6L.m. +2.29.5M. +2.5.6Q. +4.s.6G. +5.2q.4A. +7.24.5T. +8.6z.C. +e.3.6S. +e.4.6R. +h.G.6x. +0.A.6B. +0.d.6O. +0.j.6M. +0.L.6v. +0.p.6J. +0.Q.6p. +0.R.6o. +0.y.6E. +1.2n.5n. +1.6L.n. +2.1h.6m. +2.1V.63. +2.29.5N. +2.6.6Q. +3.6R.5. +3.M.6u. +5.1t.6f. +5.2q.4B. +8.6z.D. +e.4.6S. +h.H.6x. +0.B.6B. +0.e.6O. +0.k.6M. +0.N.6s. +0.R.6p. +1.2n.5o. +1.6L.o. +2.2g.5C. +2.7.6Q. +3.6R.6. +3.M.6v. +3.S.6o. +4.q.6J. +5.1t.6g. +5.2q.4C. +6.5L.2c. +6.6E.z. +7.26.5T. +7.u.6G. +8.6z.E. +e.5.6S. +h.I.6x. +0.C.6B. +0.d.6P. +0.f.6O. +0.l.6M. +0.O.6s. +0.r.6H. +1.5s.2h. +1.6L.p. +2.2g.5D. +2.2n.5p. +2.8.6Q. +3.S.6p. +5.29.5O. +5.2q.4D. +7.1+.5U. +8.6z.F. +c.T.6o. +e.6.6S. +e.7.6R. +h.J.6x. +0.D.6B. +0.e.6P. +0.g.6O. +0.m.6M. +0.P.6s. +0.s.6H. +0.U.6o. +1.2n.5q. +1.5s.2i. +1.6L.q. +2.1V.64. +2.9.6Q. +2.k.6N. +3.6R.8. +4.N.6u. +5.29.5P. +9.r.6I. +c.T.6p. +e.7.6S. +h.K.6x. +0.E.6B. +0.f.6P. +0.h.6O. +0.N.6v. +0.Q.6s. +0.r.6J. +0.t.6H. +0.U.6p. +0.V.6o. +1.5s.2j. +2.a.6Q. +3.n.6M. +4.A.6E. +4.O.6u. +5.2q.4F. +6.5L.2d. +7.1+.5V. +9.s.6I. +e.8.6S. +e.9.6R. +h.L.6x. +j.2a.5P. +0.F.6B. +0.g.6P. +0.i.6O. +0.o.6M. +0.O.6v. +0.R.6s. +0.s.6J. +0.u.6H. +0.V.6p. +0.W.6o. +1.1t.6h. +1.5s.2k. +3.6R.a. +3.M.6x. +4.B.6E. +4.P.6u. +5.1V.65. +5.29.5Q. +5.2q.4G. +8.6z.G. +9.r.6K. +e.9.6S. +f.2g.5F. +0.h.6P. +0.j.6O. +0.p.6M. +0.P.6v. +0.t.6J. +0.v.6H. +0.W.6p. +0.X.6o. +1.5s.2l. +1.6L.r. +3.6R.b. +4.C.6E. +4.Q.6u. +4.S.6s. +4.u.6I. +5.c.6Q. +7.22.5U. +8.6z.H. +9.s.6K. +e.a.6S. +f.2g.5G. +0.i.6P. +0.k.6O. +0.q.6M. +0.Q.6v. +0.u.6J. +0.w.6H. +0.X.6p. +1.2n.5r. +1.6L.s. +2.1V.66. +3.6R.c. +4.D.6E. +4.R.6u. +8.6z.I. +c.T.6s. +c.Y.6o. +e.b.6S. +0.j.6P. +0.l.6O. +0.R.6v. +0.U.6s. +0.v.6J. +0.x.6H. +0.Z.6o. +1.1t.6i. +1.6L.t. +2.1+.5W. +3.S.6u. +4.E.6E. +4.G.6B. +5.1V.67. +5.29.5R. +5.2q.4H. +6.5s.2n. +7.22.5V. +7.24.5U. +8.6z.J. +9.u.6K. +c.Y.6p. +h.N.6x. +0.H.6B. +0.k.6P. +0.m.6O. +0.V.6s. +0.w.6J. +0.y.6H. +0.Z.6p. +1.1t.6j. +1.6L.u. +2.1+.5X. +2.1V.68. +3.S.6v. +4.F.6E. +5.2q.4I. +5.c.6T. +8.6z.K. +c.T.6u. +h.O.6x. +0.l.6P. +0.r.6M. +0.W.6s. +0.x.6J. +0.z.6H. +1.6L.v. +2.1+.5Y. +2.1t.6k. +2.2f.5M. +3.I.6B. +3.n.6O. +4.U.6u. +5.2q.4J. +7.24.5V. +7.26.5U. +8.6z.L. +c.T.6v. +h.P.6x. +0.m.6P. +0.o.6O. +0.s.6M. +0.U.6v. +0.X.6s. +0.y.6J. +1.1t.6l. +1.6L.w. +2.2f.5N. +3.6R.d. +3.6V.8. +3.J.6B. +4.V.6u. +5.2q.4K. +7.1+.5Z. +7.2p.5s. +8.6z.M. +h.Q.6x. +0.+.6o. +0.G.6E. +0.K.6B. +0.p.6O. +0.t.6M. +0.V.6v. +1.6L.x. +2.1+.5+. +2.22.5W. +3.6R.e. +3.n.6P. +4.6J.z. +4.W.6u. +5.1V.69. +5.2q.4L. +7.26.5V. +9./.6n. +c.Y.6s. +h.R.6x. +k.6N.r. +0./.6o. +0.+.6p. +0.A.6H. +0.H.6E. +0.L.6B. +0.o.6P. +0.q.6O. +0.u.6M. +0.W.6v. +1.2f.5O. +1.6L.y. +2.22.5X. +2.29.5S. +2.2n.5t. +3.6R.f. +3.6V.a. +3.S.6x. +4.X.6u. +5.1V.6a. +5.2q.4M. +6.6s.Z. +k.6N.s. +0./.6p. +0.10.6o. +0.B.6H. +0.p.6P. +0.v.6M. +0.X.6v. +1.2f.5P. +1.6L.z. +2.1t.6m. +2.22.5Y. +2.24.5W. +3.6R.g. +3.6V.b. +3.I.6E. +3.M.6B. +5.2q.4N. +5.c.6U. +8.6z.N. +c.T.6x. +c.Y.6u. +0.10.6p. +0.11.6o. +0.A.6J. +0.C.6H. +0.q.6P. +0.w.6M. +0.Z.6u. +1./.6q. +1.1V.6b. +2.24.5X. +2.u.6N. +3.6R.h. +3.6V.c. +3.J.6E. +5.2q.4O. +7.22.5Z. +8.6z.O. +c.Y.6v. +h.U.6x. +0.11.6p. +0.B.6J. +0.D.6H. +0.K.6E. +0.r.6O. +0.x.6M. +0.Z.6v. +1.2f.5Q. +2.22.5+. +2.24.5Y. +2.26.5W. +3.6R.i. +4.12.6o. +5.c.6W. +8.6z.P. +h.V.6x. +0.+.6s. +0.5u.2n. +0.C.6J. +0.E.6H. +0.L.6E. +0.N.6B. +0.s.6O. +0.y.6M. +1.6L.A. +2.1+.5/. +2.26.5X. +3.6R.j. +4.12.6p. +5.2q.4Q. +5.c.6X. +5.k.6Q. +7.24.5Z. +8.6z.Q. +9./.6r. +h.W.6x. +0./.6s. +0.D.6J. +0.F.6H. +0.O.6B. +0.r.6P. +0.t.6O. +1.6L.B. +2.1+.60. +2.24.5+. +2.26.5Y. +3.6R.k. +3.M.6E. +5.2q.4R. +5.c.6Y. +6.6M.z. +8.6z.R. +h.X.6x. +0.+.6u. +0.10.6s. +0.13.6o. +0.E.6J. +0.P.6B. +0.s.6P. +0.u.6O. +1.2f.5R. +1.6L.C. +3.6R.l. +3.6V.d. +5.2q.4S. +5.c.6Z. +7.1+.61. +7.26.5Z. +8.6z.S. +9./.6t. +c.Y.6x. +0.+.6v. +0.11.6s. +0.13.6p. +0.14.6o. +0.F.6J. +0.Q.6B. +0.t.6P. +0.v.6O. +1.6L.D. +2.26.5+. +3.6R.m. +3.6V.e. +4./.6u. +5.29.5T. +5.k.6T. +7.1+.62. +7.2p.5u. +8.6z.T. +h.Z.6x. +0./.6v. +0.14.6p. +0.15.6o. +0.A.6M. +0.G.6H. +0.R.6B. +0.u.6P. +0.w.6O. +1.6L.E. +2.22.5/. +3.6V.f. +3.n.6R. +4.10.6u. +4.12.6s. +4.N.6E. +8.6z.U. +0.10.6v. +0.15.6p. +0.16.6o. +0.B.6M. +0.H.6H. +0.v.6P. +0.x.6O. +1.6L.F. +2.22.60. +3.6R.o. +3.6V.g. +3.S.6B. +4.11.6u. +4.O.6E. +8.6z.V. +b.6+.c. +0.11.6v. +0.16.6p. +0.C.6M. +0.G.6J. +0.w.6P. +0.y.6O. +2.1+.63. +2.24.5/. +2.A.6N. +3.6R.p. +3.6V.h. +3.I.6H. +4.12.6u. +4.P.6E. +5.1V.6c. +5.2q.4V. +5.c.6/. +7.22.61. +8.6z.W. +c.T.6B. +n.17.6o. +0.12.6v. +0.13.6s. +0.18.6o. +0.D.6M. +0.H.6J. +0.U.6B. +0.x.6P. +1./.6w. +2.24.60. +2.2f.5S. +3.6R.q. +3.6V.i. +3.J.6H. +4.Q.6E. +5.c.70. +6.6O.z. +7.22.62. +8.6z.X. +h.+.6x. +n.17.6p. +0.14.6s. +0.18.6p. +0.19.6o. +0.E.6M. +0.K.6H. +0.R.6E. +0.V.6B. +0.y.6P. +1.6L.G. +2.26.5/. +3.6V.j. +3.I.6J. +5.c.71. +5.k.6U. +7.24.61. +8.6z.Y. +h./.6x. +0.15.6s. +0.19.6p. +0.1a.6o. +0.6P.z. +0.F.6M. +0.L.6H. +0.W.6B. +1.6L.H. +2.1+.64. +2.26.60. +2.2n.5v. +3.6V.k. +3.J.6J. +3.S.6E. +4./.6y. +4.13.6u. +5.c.72. +7.24.62. +7.r.6Q. +8.6z.Z. +b.6+.d. +h.10.6x. +0.13.6v. +0.16.6s. +0.1a.6p. +0.1b.6o. +0.A.6O. +0.K.6J. +0.X.6B. +1.6L.I. +2.22.63. +2.2n.5w. +3.6R.r. +3.6V.l. +3.M.6H. +4.14.6u. +5.2q.4X. +5.c.73. +5.k.6W. +7.26.61. +7.s.6Q. +b.6+.e. +c.T.6E. +h.11.6x. +0.14.6v. +0.17.6s. +0.1b.6p. +0.1c.6o. +0.B.6O. +0.L.6J. +1.2n.5x. +1.6L.J. +3.6R.s. +3.6V.m. +4.15.6u. +4.U.6E. +5.2q.4Y. +5.c.74. +5.k.6X. +7.1+.65. +7.26.62. +b.6+.f. +c.Y.6B. +h.12.6x. +0.15.6v. +0.18.6s. +0.1c.6p. +0.A.6P. +0.C.6O. +0.G.6M. +1.6L.K. +2.24.63. +3.6R.t. +3.M.6J. +3.n.6V. +4.16.6u. +4.V.6E. +4.Z.6B. +5.29.5U. +5.c.75. +5.k.6Y. +7.r.6T. +7.u.6Q. +b.6+.g. +d.71.d. +0.16.6v. +0.19.6s. +0.1d.6o. +0.B.6P. +0.D.6O. +0.H.6M. +0.N.6H. +0.W.6E. +1.2n.5y. +1.6L.L. +2.1+.66. +2.22.64. +3.6R.u. +3.6V.o. +4.17.6u. +5.c.76. +5.k.6Z. +7.s.6T. +8.6z.+. +b.6+.h. +d.71.e. +0.17.6v. +0.1a.6s. +0.1d.6p. +0.C.6P. +0.E.6O. +0.O.6H. +0.X.6E. +1.2f.5T. +1.2n.5z. +1.6L.M. +2.1V.6d. +2.26.63. +3.6R.v. +3.6V.p. +3.I.6M. +4.18.6u. +5.29.5V. +5.c.77. +7.1+.67. +8.6z./. +b.6+.i. +d.71.f. +h.13.6x. +0.18.6v. +0.19.6u. +0.1b.6s. +0.D.6P. +0.F.6O. +0.N.6J. +0.P.6H. +2.1+.68. +2.1V.6e. +2.24.64. +3.6R.w. +3.6V.q. +3.J.6M. +5.c.78. +7.22.65. +7.u.6T. +8.6z.10. +b.6+.j. +c.Y.6E. +d.71.g. +h.14.6x. +0.+.6B. +0.19.6v. +0.1c.6s. +0.1e.6o. +0.E.6P. +0.K.6M. +0.O.6J. +0.Q.6H. +0.Z.6E. +1.2n.5A. +3.6R.x. +4./.6A. +4.1a.6u. +5.c.79. +8.6z.11. +b.6+.k. +d.71.h. +h.15.6x. +0./.6B. +0.1a.6v. +0.1e.6p. +0.1f.6o. +0.F.6P. +0.L.6M. +0.P.6J. +0.R.6H. +1.6L.N. +2.22.66. +2.26.64. +3.6R.y. +4.1b.6u. +5.1V.6f. +5.2q.4+. +5.c.7a. +5.k.6/. +7.24.65. +7.r.6U. +8.6z.12. +b.6+.l. +d.71.i. +h.16.6x. +0.10.6B. +0.1b.6v. +0.1d.6s. +0.1f.6p. +0.G.6O. +0.Q.6J. +1.6L.O. +2.29.5W. +3.6R.z. +3.6V.r. +3.M.6M. +3.S.6H. +4.1c.6u. +4.s.6U. +5.1V.6g. +5.2q.4/. +5.c.7b. +5.k.70. +6.6o.1g. +7.1+.69. +7.22.67. +b.6+.m. +d.71.j. +h.17.6x. +0.11.6B. +0.1c.6v. +0.H.6O. +0.R.6J. +1.6L.P. +2.22.68. +2.24.66. +2.29.5X. +3.6V.s. +5.2q.50. +5.c.7c. +6.6p.1g. +7.1+.6a. +7.26.65. +7.r.6W. +9./.6C. +b.6+.n. +c.T.6H. +d.71.k. +h.18.6x. +0.+.6E. +0.G.6P. +0.U.6H. +1.2n.5C. +1.6L.Q. +2.29.5Y. +3.6V.t. +3.I.6O. +3.S.6J. +4.12.6B. +4.1d.6u. +4.r.6X. +5./.6D. +5.1h.6n. +5.2q.51. +5.c.7d. +5.k.72. +7.24.67. +7.s.6W. +7.u.6U. +8.6z.13. +b.6+.o. +d.71.l. +h.19.6x. +0.1d.6v. +0.1h.6o. +0.2w.2w. +0.H.6P. +0.N.6M. +0.V.6H. +1.1+.6b. +1.2n.5D. +1.6L.R. +2.24.68. +2.26.66. +3.6R.A. +3.6V.u. +3.J.6O. +4./.6E. +5.29.5Z. +5.2q.52. +5.c.7e. +5.k.73. +6.6s.1e. +7.r.6Y. +7.s.6X. +8.6z.14. +b.6+.p. +c.T.6J. +d.71.m. +h.1a.6x. +0.1h.6p. +0.K.6O. +0.O.6M. +0.U.6J. +0.W.6H. +1.1V.6h. +1.6L.S. +2.29.5+. +3.6R.B. +3.6V.v. +3.I.6P. +4.10.6E. +4.1i.6o. +4.s.6Y. +4.u.6W. +5.2q.53. +5.c.7f. +5.k.74. +6.6s.1f. +7.22.69. +7.26.67. +7.r.6Z. +8.6z.15. +b.6+.q. +d.71.n. +h.1b.6x. +0.13.6B. +0.1i.6p. +0.1j.6o. +0.L.6O. +0.P.6M. +0.V.6J. +0.X.6H. +1.1h.6q. +1.2f.5U. +1.6L.T. +2.26.68. +2.2n.5E. +3.6R.C. +3.6V.w. +3.J.6P. +4.11.6E. +4.1e.6u. +4.u.6X. +5./.6F. +5.2q.54. +5.c.7g. +5.k.75. +6.6s.1g. +7.22.6a. +7.s.6Z. +8.6z.16. +d.71.o. +h.1c.6x. +0.14.6B. +0.1e.6v. +0.1j.6p. +0.K.6P. +0.Q.6M. +0.W.6J. +1.6L.U. +3.6R.D. +3.6V.x. +3.M.6O. +4.12.6E. +4.1f.6u. +4.u.6Y. +5.2q.55. +5.c.7h. +5.k.76. +7.24.69. +8.6z.17. +b.5F.2n. +c.Y.6H. +d.71.p. +0.15.6B. +0.1f.6v. +0.6u.1g. +0.L.6P. +0.R.6M. +0.X.6J. +0.Z.6H. +1.1V.6i. +1.22.6b. +1.2f.5V. +1.6L.V. +3.2x.2w. +3.5G.2n. +3.6R.E. +3.6V.y. +4.u.6Z. +5.1h.6r. +5.k.77. +7.24.6a. +8.6z.18. +b.6+.r. +d.71.q. +h.1d.6x. +0.16.6B. +0.1k.6o. +0.6v.1g. +1.1V.6j. +1.6L.W. +3.6R.F. +3.6V.z. +3.M.6P. +3.S.6M. +5./.6G. +5.2q.57. +5.c.7i. +5.k.78. +6.6s.1h. +7.26.69. +7.r.6/. +8.6z.19. +b.6+.s. +c.Y.6J. +0.13.6E. +0.17.6B. +0.1k.6p. +0.N.6O. +0.Z.6J. +1.24.6b. +1.6L.X. +2.1V.6k. +2.29.5/. +2.c.7j. +3.1l.6o. +5.1h.6t. +5.k.79. +6.6s.1i. +7.26.6a. +7.r.70. +7.s.6/. +8.58.2q. +8.6z.1a. +b.2p.5F. +b.6+.t. +c.T.6M. +0.18.6B. +0.1j.6s. +0.1m.6o. +0.O.6O. +0.U.6M. +1.1V.6l. +1.6L.Y. +2.29.60. +2.2n.5H. +3.1l.6p. +4.14.6E. +4.1h.6u. +4.s.70. +5.c.7k. +5.k.7a. +7.2p.5G. +8.6z.1b. +b.6+.u. +d.71.r. +h.1e.6x. +0.+.6H. +0.19.6B. +0.1h.6v. +0.1m.6p. +0.1n.6o. +0.N.6P. +0.P.6O. +0.V.6M. +1.26.6b. +1.2n.5I. +1.6L.Z. +2.2f.5W. +3.2x.2x. +3.2y.2w. +3.6R.G. +3.6V.A. +4.15.6E. +4.1i.6u. +5.29.61. +5.c.7l. +5.k.7b. +7.1+.6c. +7.r.72. +7.u.6/. +8.6z.1c. +b.6+.v. +d.71.s. +h.1f.6x. +j.2a.60. +0./.6H. +0.1a.6B. +0.1i.6v. +0.1j.6u. +0.1n.6p. +0.1o.6o. +0.O.6P. +0.Q.6O. +0.W.6M. +2.2f.5X. +3.6R.H. +3.6V.B. +4.16.6E. +5.29.62. +5.c.7m. +5.k.7c. +7.r.73. +7.s.72. +7.u.70. +8.2z.2w. +b.6+.w. +d.71.t. +f.2g.5L. +h.1g.6x. +0.+.6J. +0.10.6H. +0.1b.6B. +0.1j.6v. +0.1k.6s. +0.1o.6p. +0.1p.6o. +0.P.6P. +0.R.6O. +0.X.6M. +2.1V.6m. +2.2f.5Y. +3.6V.C. +3.I.6R. +4.17.6E. +5.c.7n. +5.k.7d. +7.r.74. +7.s.73. +8.2A.2w. +8.6z.1d. +9./.6I. +b.6+.x. +d.71.u. +0./.6J. +0.11.6H. +0.1c.6B. +0.1p.6p. +0.1q.6o. +0.Q.6P. +1.1h.6w. +1.2f.5Z. +3.1l.6s. +3.2B.2w. +3.6V.D. +3.J.6R. +3.S.6O. +4.18.6E. +4.s.74. +5.c.7o. +5.k.7e. +7.r.75. +7.u.72. +b.6+.y. +c.Y.6M. +d.71.v. +0.10.6J. +0.19.6E. +0.1m.6s. +0.1q.6p. +0.1r.6o. +0.2C.2w. +0.R.6P. +0.Z.6M. +1.6L.+. +2.29.63. +2.2f.5+. +3.2y.2x. +3.6R.K. +3.6V.E. +4.12.6H. +4.1k.6u. +5.2q.5c. +5.k.7f. +7.22.6c. +7.r.76. +7.s.75. +7.u.73. +9./.6K. +b.6+.z. +c.T.6O. +d.71.w. +h.1h.6x. +0.11.6J. +0.1d.6B. +0.1k.6v. +0.1n.6s. +0.1r.6p. +0.1s.6o. +0.U.6O. +1.6L./. +3.1l.6u. +3.6R.L. +3.6V.F. +3.S.6P. +4.1a.6E. +4.1t.6n. +4.s.76. +5.1h.6y. +5.k.7g. +7.r.77. +7.u.74. +8.2z.2x. +8.6z.1e. +d.71.x. +h.1i.6x. +0.1o.6s. +0.1s.6p. +0.1t.6o. +0.V.6O. +1.6L.10. +2.2g.5O. +3.1l.6v. +3.M.6R. +4.12.6J. +4.1b.6E. +4.1m.6u. +4.r.78. +4.s.77. +4.T.6P. +4.u.75. +5.2q.5e. +5.c.7p. +5.k.7h. +7.24.6c. +8.2A.2x. +8.6z.1f. +d.71.y. +h.1j.6x. +0.13.6H. +0.1m.6v. +0.1p.6s. +0.1t.6p. +0.U.6P. +0.W.6O. +1.6L.11. +2.29.64. +2.2g.5P. +3.1u.6o. +3.2B.2x. +4.1c.6E. +4.1n.6u. +4.r.79. +5.c.7q. +7.s.78. +7.u.76. +8.6z.1g. +b.6+.A. +d.71.z. +f.2w.2D. +0.+.6M. +0.14.6H. +0.1e.6B. +0.1n.6v. +0.1o.6u. +0.1q.6s. +0.1v.6o. +0.V.6P. +0.X.6O. +1.1t.6q. +1.6L.12. +2.1+.6d. +2.A.6/. +3.1u.6p. +3.2x.2C. +3.2y.2y. +3.6V.G. +4.r.7a. +4.s.79. +4.u.77. +5.2q.5g. +5.c.7r. +5.k.7i. +7.26.6c. +b.6+.B. +g.2E.2w. +0./.6M. +0.13.6J. +0.15.6H. +0.1f.6B. +0.1o.6v. +0.1r.6s. +0.1v.6p. +0.1w.6o. +0.W.6P. +2.1+.6e. +2.2f.5/. +2.k.7j. +3.6R.N. +3.6V.H. +4.1d.6E. +4.1p.6u. +4.r.7b. +4.s.7a. +5.29.65. +7.u.78. +8.2z.2y. +b.6+.C. +c.Y.6O. +h.1k.6x. +0.10.6M. +0.16.6H. +0.1p.6v. +0.1s.6s. +0.1w.6p. +0.1x.6o. +0.X.6P. +0.Z.6O. +2.2f.60. +3.6R.O. +3.I.6V. +4.14.6J. +4.1q.6u. +4.1t.6r. +4.r.7c. +4.s.7b. +4.u.79. +5.c.7s. +5.k.7k. +6.6B.1g. +8.2A.2y. +8.2z.2z. +8.6z.1h. +b.6+.D. +d.71.A. +h.1l.6x. +0.11.6M. +0.17.6H. +0.1q.6v. +0.1t.6s. +0.1x.6p. +0.1y.6o. +1.2f.61. +1.6L.13. +2./.6N. +2.29.66. +2.A.72. +3.2B.2y. +3.2x.2D. +3.6R.P. +3.J.6V. +4.15.6J. +4.1r.6u. +4.r.7d. +4.s.7c. +4.u.7a. +5.k.7l. +8.2A.2z. +8.6z.1i. +b.6+.E. +c.Y.6P. +d.71.B. +e.0.7A. +f.1+.6f. +h.1m.6x. +i.c.7t. +0.16.6J. +0.18.6H. +0.1r.6v. +0.1y.6p. +0.1z.6o. +0.Z.6P. +1.2f.62. +1.6L.14. +2.22.6d. +2.2g.5R. +3.1u.6s. +3.2y.2C. +3.6R.Q. +3.6V.K. +4.12.6M. +4.1e.6E. +4.1s.6u. +4.s.7d. +4.u.7b. +5.1h.6A. +5.29.67. +5.k.7m. +7.r.7e. +8.2A.2A. +8.2z.2B. +8.6z.1j. +9.1t.6t. +b.6+.F. +d.71.C. +e.1.7A. +f.1+.6g. +g.2E.2x. +h.1n.6x. +0.17.6J. +0.19.6H. +0.1A.6o. +0.1h.6B. +0.1s.6v. +0.1v.6s. +0.1z.6p. +1.6L.15. +2.22.6e. +2.29.68. +3.6R.R. +3.6V.L. +4.1f.6E. +4.1t.6u. +4.s.7e. +4.u.7c. +5.2q.5k. +5.c.7u. +5.k.7n. +7.r.7f. +8.2A.2B. +8.2z.2C. +d.71.D. +e.2.7A. +h.1o.6x. +0.+.6O. +0.18.6J. +0.1A.6p. +0.1B.6o. +0.1i.6B. +0.1t.6v. +0.1w.6s. +1.6L.16. +2.24.6d. +2.A.75. +3.1u.6u. +3.2B.2B. +3.M.6V. +3.S.6R. +4.1a.6H. +4.u.7d. +5.k.7o. +6.6E.1g. +7.r.7g. +7.s.7f. +8.2A.2C. +d.71.E. +e.3.7A. +h.1p.6x. +0./.6O. +0.13.6M. +0.19.6J. +0.1b.6H. +0.1B.6p. +0.1C.6o. +0.1j.6B. +0.1x.6s. +0.d.7t. +1.6L.17. +2.24.6e. +2.2f.63. +2.2n.5J. +2.A.76. +3.1u.6v. +3.2B.2C. +3.2y.2D. +3.6R.T. +4.1v.6u. +5.1h.6C. +5.c.7v. +7.22.6f. +7.r.7h. +7.s.7g. +7.u.7e. +8.6z.1k. +b.6+.G. +d.71.F. +e.4.7A. +h.1q.6x. +0.+.6P. +0.10.6O. +0.14.6M. +0.1a.6J. +0.1c.6H. +0.1C.6p. +0.1D.6o. +0.1v.6v. +0.1y.6s. +0.2C.2C. +0.e.7t. +1.1+.6h. +1.6L.18. +2.26.6d. +2.A.77. +3.6R.U. +4.1w.6u. +4.s.7h. +5.1h.6D. +5.29.69. +5.c.7w. +7.22.6g. +7.u.7f. +8.2z.2D. +8.6z.1l. +a.2F.2w. +b.6+.H. +e.5.7A. +g.2E.2y. +h.1r.6x. +0./.6P. +0.11.6O. +0.15.6M. +0.1D.6p. +0.1w.6v. +0.1z.6s. +0.f.7t. +1.1t.6w. +1.6L.19. +2.26.6e. +2.2n.5K. +2.A.78. +3.6R.V. +3.6V.N. +4.1b.6J. +4.1h.6E. +4.1x.6u. +5.29.6a. +5.c.7x. +5.k.7p. +7.24.6f. +7.r.7i. +7.u.7g. +8.2A.2D. +8.2z.2E. +8.6z.1m. +b.6+.I. +e.6.7A. +h.1s.6x. +0.10.6P. +0.16.6M. +0.1A.6s. +0.1c.6J. +0.1d.6H. +0.1E.6o. +0.1k.6B. +0.1x.6v. +0.2w.2G. +0.g.7t. +1.6L.1a. +2.2f.64. +2.r.7j. +3.2B.2D. +3.6R.W. +3.6V.O. +4.12.6O. +4.1i.6E. +4.1y.6u. +5.k.7q. +7.24.6g. +7.s.7i. +7.u.7h. +8.2A.2E. +8.6z.1n. +b.6+.J. +d.71.G. +e.7.7A. +h.1t.6x. +0.11.6P. +0.17.6M. +0.1B.6s. +0.1E.6p. +0.1F.6o. +0.1j.6E. +0.1y.6v. +0.h.7t. +1.1+.6i. +1.29.6b. +1.6L.1b. +2.A.7a. +2.s.7j. +3.1l.6B. +3.6R.X. +3.6V.P. +4.1z.6u. +4.26.6f. +5.1h.6F. +5.k.7r. +7.r.7k. +8.6z.1o. +9.1t.6y. +b.6+.K. +d.71.H. +e.8.7A. +f.2C.2D. +h.1u.6x. +0.18.6M. +0.1C.6s. +0.1d.6J. +0.1F.6p. +0.1G.6o. +0.1m.6B. +0.1z.6v. +0.i.7t. +1.1+.6j. +1.22.6h. +1.2f.65. +1.6L.1c. +2.2H.2w. +3.2x.2F. +3.6R.Y. +3.6V.Q. +4.12.6P. +4.1A.6u. +4.26.6g. +4.u.7i. +5.2q.5o. +5.c.7y. +7.r.7l. +7.s.7k. +8.6z.1p. +b.6+.L. +d.71.I. +e.9.7A. +g.2E.2C. +h.1v.6x. +0.13.6O. +0.19.6M. +0.1A.6v. +0.1B.6u. +0.1D.6s. +0.1e.6H. +0.1G.6p. +0.1H.6o. +0.1n.6B. +0.j.7t. +2.1+.6k. +2.A.7c. +2.u.7j. +3.6R.Z. +3.6V.R. +4.r.7m. +5.k.7s. +7.s.7l. +8.6z.1q. +b.6+.M. +d.71.J. +e.a.7A. +h.1w.6x. +0.14.6O. +0.1a.6M. +0.1B.6v. +0.1f.6H. +0.1H.6p. +0.1I.6o. +0.1o.6B. +0.2D.2D. +0.k.7t. +1.1+.6l. +1.24.6h. +1.6L.1d. +2.2f.66. +2.A.7d. +3.2x.2G. +3.S.6V. +4.1C.6u. +4.1k.6E. +4.s.7m. +4.u.7k. +5.1h.6G. +5.2q.5q. +7.r.7n. +8.6z.1r. +9.c.7z. +d.71.K. +e.b.7A. +h.1x.6x. +0.13.6P. +0.15.6O. +0.1b.6M. +0.1C.6v. +0.1e.6J. +0.1E.6s. +0.1I.6p. +0.1J.6o. +0.1p.6B. +0.l.7t. +1.22.6i. +1.2f.67. +2.A.7e. +3.1l.6E. +3.6V.T. +4.1D.6u. +7.r.7o. +7.s.7n. +7.u.7l. +8.6z.1s. +d.71.L. +g.2E.2D. +h.1g.6H. +h.1y.6x. +i.c.7A. +0.14.6P. +0.16.6O. +0.1c.6M. +0.1D.6v. +0.1f.6J. +0.1F.6s. +0.1J.6p. +0.1q.6B. +0.2I.2w. +0.m.7t. +1.22.6j. +1.26.6h. +2.2f.68. +2.2H.2x. +2.A.7f. +3.2y.2F. +3.6V.U. +4.1m.6E. +4.u.7m. +5.k.7u. +7.s.7o. +8.6z.1t. +b.6+.N. +d.71.M. +g.2E.2E. +h.1z.6x. +0.15.6P. +0.17.6O. +0.1G.6s. +0.1r.6B. +1.24.6i. +1.6L.1e. +2.1+.6m. +2.22.6k. +3.6R.+. +3.6V.V. +3.n.7t. +4.1E.6u. +4.1n.6E. +4.6J.1g. +4.u.7n. +5./.6Q. +8.2z.2F. +8.6z.1u. +b.6+.O. +h.1A.6x. +0.16.6P. +0.18.6O. +0.1d.6M. +0.1E.6v. +0.1h.6H. +0.1H.6s. +0.1K.6o. +0.1o.6E. +0.1s.6B. +0.o.7t. +1.22.6l. +1.24.6j. +1.6L.1f. +2.A.7h. +3.2y.2G. +3.6R./. +3.6V.W. +4.1F.6u. +4.1t.6A. +5.k.7v. +7.r.7p. +7.u.7o. +8.2A.2F. +8.6z.1v. +b.6+.P. +h.1B.6x. +0.17.6P. +0.19.6O. +0.1F.6v. +0.1I.6s. +0.1K.6p. +0.1L.6o. +0.1p.6E. +0.1t.6B. +0.d.7A. +0.p.7t. +1.26.6i. +1.2f.69. +1.6L.1g. +2.24.6k. +3.2B.2F. +3.6R.10. +3.6V.X. +4.1G.6u. +4.1i.6H. +4.s.7p. +5.1h.6I. +5.29.6c. +5.2q.5s. +5.c.7B. +5.k.7w. +7.r.7q. +8.2z.2G. +8.6z.1w. +b.6+.Q. +d.71.N. +h.1C.6x. +0.18.6P. +0.1G.6v. +0.1j.6H. +0.1J.6s. +0.1L.6p. +0.1M.6o. +0.e.7A. +0.q.7t. +1.24.6l. +1.26.6j. +1.2f.6a. +2.1d.6N. +2.2H.2y. +2.A.7i. +3.1u.6B. +3.2x.2I. +3.6R.11. +3.6V.Y. +4.1a.6O. +4.1h.6J. +4.1H.6u. +4.1q.6E. +5./.6T. +5.c.7C. +5.k.7x. +7.r.7r. +7.s.7q. +8.2A.2G. +8.6z.1x. +a.2F.2C. +b.6+.R. +d.71.O. +h.1D.6x. +0.19.6P. +0.1b.6O. +0.1e.6M. +0.1H.6v. +0.1M.6p. +0.1v.6B. +0.f.7A. +2.22.6m. +2.26.6k. +2.2H.2z. +2.A.7j. +3.2B.2G. +3.6R.12. +3.6V.Z. +4.1i.6J. +4.1I.6u. +4.1N.6o. +4.1r.6E. +5.1h.6K. +5.c.7D. +7.s.7r. +7.u.7p. +8.6z.1y. +9.1t.6C. +b.6+.S. +d.71.P. +0.1c.6O. +0.1f.6M. +0.1I.6v. +0.1j.6J. +0.1O.6o. +0.1w.6B. +0.2C.2G. +0.g.7A. +1.26.6l. +1.2f.6b. +1.6L.1h. +2.2H.2A. +2.A.7k. +4.1a.6P. +4.1J.6u. +4.1N.6p. +4.1s.6E. +5.1t.6D. +5.c.7E. +7.r.7s. +7.u.7q. +8.6z.1z. +a.2J.2w. +b.6+.T. +d.71.Q. +h.1E.6x. +0.1b.6P. +0.1J.6v. +0.1k.6H. +0.1K.6s. +0.1O.6p. +0.1P.6o. +0.1x.6B. +0.h.7A. +0.r.7t. +1.6L.1i. +2.24.6m. +2.2H.2B. +4.1t.6E. +5.k.7y. +6.6M.1g. +7.s.7s. +7.u.7r. +8.6z.1A. +a.2F.2D. +b.6+.U. +d.71.R. +h.1F.6x. +0.1c.6P. +0.1d.6O. +0.1L.6s. +0.1P.6p. +0.1Q.6o. +0.1y.6B. +0.i.7A. +0.s.7t. +1.6L.1j. +2.2H.2C. +2.A.7m. +3.1l.6H. +3.1u.6E. +3.2y.2I. +3.6R.13. +8.6z.1B. +b.6+.V. +d.71.S. +g.2E.2F. +h.1G.6x. +0.1k.6J. +0.1m.6H. +0.1M.6s. +0.1Q.6p. +0.1v.6E. +0.1z.6B. +0.j.7A. +0.t.7t. +2.26.6m. +2.A.7n. +3.6R.14. +3.6V.+. +4.1K.6u. +4.2G.2D. +5./.6U. +5.1t.6F. +5.c.7F. +7.r.7u. +7.u.7s. +8.2z.2I. +8.6z.1C. +9.k.7z. +b.6+.W. +d.71.T. +h.1H.6x. +0.1A.6B. +0.1d.6P. +0.1h.6M. +0.1K.6v. +0.1L.6u. +0.1n.6H. +0.2w.2K. +0.k.7A. +0.u.7t. +2.A.7o. +3.1l.6J. +3.2x.2J. +3.6R.15. +3.6V./. +4.1N.6s. +4.1w.6E. +7.s.7u. +8.2A.2I. +8.6z.1D. +b.6+.X. +d.71.U. +g.2E.2G. +h.1I.6x. +0.1e.6O. +0.1i.6M. +0.1L.6v. +0.1m.6J. +0.1o.6H. +0.1O.6s. +0.l.7A. +0.v.7t. +1.6L.1k. +2.29.6d. +2.2H.2D. +3.2B.2I. +3.6R.16. +3.6V.10. +4.1B.6B. +4.1M.6u. +4.1x.6E. +4.r.7v. +5./.6W. +5.c.7G. +b.6+.Y. +d.71.V. +h.1J.6x. +0.1C.6B. +0.1f.6O. +0.1j.6M. +0.1M.6v. +0.1n.6J. +0.1p.6H. +0.1P.6s. +0.m.7A. +0.w.7t. +1.6L.1l. +2.1h.6N. +2.29.6e. +2.2H.2E. +3.6R.17. +3.6V.11. +4.1N.6u. +4.1y.6E. +4.s.7v. +5./.6X. +5.1t.6G. +5.2q.5u. +5.c.7H. +7.r.7w. +7.u.7u. +8.6z.1E. +b.6+.Z. +d.71.W. +f.1R.6o. +f.2C.2I. +0.1D.6B. +0.1e.6P. +0.1N.6v. +0.1o.6J. +0.1O.6u. +0.1q.6H. +0.1S.6o. +0.x.7t. +1.6L.1m. +2.A.7p. +3.6R.18. +3.6V.12. +3.n.7A. +4.1z.6E. +5./.6Y. +5.c.7I. +6.5L.2n. +6.6O.1g. +6.6s.1Q. +7.r.7x. +7.s.7w. +8.6z.1F. +d.71.X. +f.1R.6p. +0.1f.6P. +0.1O.6v. +0.1p.6J. +0.1P.6u. +0.1r.6H. +0.1S.6p. +0.o.7A. +0.y.7t. +1.2f.6c. +1.6L.1n. +3.2x.2K. +3.2y.2J. +3.6R.19. +4.1A.6E. +4.u.7v. +5./.6Z. +5.29.6f. +5.c.7J. +5.k.7B. +7.s.7x. +8.6z.1G. +d.71.Y. +h.1K.6x. +0.1B.6E. +0.1E.6B. +0.1k.6M. +0.1P.6v. +0.1q.6J. +0.1s.6H. +0.6P.1g. +0.p.7A. +0.z.7t. +1.6L.1o. +2.2n.5M. +3.6R.1a. +5.1V.6n. +5.29.6g. +5.k.7C. +7.u.7w. +8.2z.2J. +8.6z.1H. +d.71.Z. +f.1Q.6u. +f.2I.2D. +h.1L.6x. +0.1C.6E. +0.1F.6B. +0.1h.6O. +0.1Q.6v. +0.1r.6J. +0.1t.6H. +0.1V.6o. +0.2L.2w. +0.q.7A. +1.6L.1p. +2.2n.5N. +3.1l.6M. +3.6R.1b. +3.6V.13. +5.k.7D. +7.2p.5L. +7.r.7y. +7.u.7x. +8.2A.2J. +8.6z.1I. +a.2F.2F. +b.6+.+. +g.2E.2I. +h.1M.6x. +j.2a.6g. +0.1G.6B. +0.1i.6O. +0.1m.6M. +0.1s.6J. +0.1V.6p. +0.1W.6o. +1.6L.1q. +2.A.7s. +3.1u.6H. +3.2B.2J. +3.6R.1c. +3.6V.14. +4.1D.6E. +5.k.7E. +6.6s.1R. +7.s.7y. +8.6z.1J. +9.1t.6I. +b.6+./. +h.1N.6x. +0.1H.6B. +0.1h.6P. +0.1j.6O. +0.1n.6M. +0.1v.6H. +0.1W.6p. +0.A.7t. +1.1V.6q. +1.2n.5O. +1.6L.1r. +3.2y.2K. +3.6V.15. +4.1t.6J. +5./.6/. +6.6s.1S. +9.r.7z. +a.2F.2G. +a.2J.2C. +b.6+.10. +h.1O.6x. +0.1I.6B. +0.1i.6P. +0.1o.6M. +0.1w.6H. +0.1X.6o. +0.B.7t. +0.r.7A. +1.29.6h. +1.2n.5P. +1.6L.1s. +3.1u.6J. +3.6R.1d. +3.6V.16. +4.1E.6E. +4.s.7z. +5./.70. +7.u.7y. +8.2z.2K. +9.1t.6K. +b.6+.11. +d.71.+. +f.1R.6u. +h.1P.6x. +0.1J.6B. +0.1j.6P. +0.1p.6M. +0.1v.6J. +0.1x.6H. +0.1X.6p. +0.2G.2G. +0.2M.2w. +0.C.7t. +0.s.7A. +2.2H.2F. +2.A.7u. +3.2x.2L. +3.6V.17. +4.1F.6E. +4.1S.6u. +5./.71. +5.1V.6r. +5.k.7F. +8.2A.2K. +8.6z.1K. +9.1t.6L. +b.6+.12. +f.1R.6v. +h.1Q.6x. +0.1k.6O. +0.1q.6M. +0.1S.6v. +0.1w.6J. +0.1y.6H. +0.D.7t. +0.t.7A. +1.2n.5Q. +1.6L.1u. +3.2B.2K. +3.6V.18. +4.1G.6E. +5./.72. +6.6s.1V. +8.6z.1L. +9.u.7z. +a.2J.2D. +d.71.10. +0.1r.6M. +0.1x.6J. +0.1z.6H. +0.2C.2K. +0.E.7t. +0.u.7A. +1.29.6i. +1.6L.1v. +2.2f.6d. +2.2H.2G. +2.A.7v. +3.1l.6O. +3.6R.1e. +3.6V.19. +4.1H.6E. +5./.73. +5.k.7G. +6.6s.1W. +8.6z.1M. +9.1V.6t. +d.71.11. +g.2E.2J. +0.1A.6H. +0.1K.6B. +0.1k.6P. +0.1m.6O. +0.1s.6M. +0.1y.6J. +0.F.7t. +0.v.7A. +1.29.6j. +1.6L.1w. +2.2f.6e. +3.6R.1f. +3.6V.1a. +4.1I.6E. +5./.74. +5.2q.5x. +5.k.7H. +7.r.7B. +8.6z.1N. +b.6+.13. +d.71.12. +f.1V.6u. +0.1L.6B. +0.1n.6O. +0.1t.6M. +0.1V.6v. +0.1z.6J. +0.2N.2w. +0.w.7A. +1.2n.5R. +1.6L.1x. +2.29.6k. +2.2H.2H. +2.A.7x. +3.1l.6P. +3.2x.2M. +3.2y.2L. +3.6R.1g. +3.6V.1b. +4.1B.6H. +4.1J.6E. +4.r.7C. +5./.75. +5.k.7I. +6.6s.1X. +7.s.7B. +8.6z.1O. +a.2F.2I. +b.6+.14. +f.1R.6x. +f.1W.6u. +0.1A.6J. +0.1C.6H. +0.1M.6B. +0.1m.6P. +0.1o.6O. +0.1W.6v. +0.2K.2D. +0.2w.2O. +0.x.7A. +1.29.6l. +1.2f.6f. +1.6L.1y. +2.c.7K. +3.1u.6M. +3.6V.1c. +4.s.7C. +5./.76. +5.2q.5y. +5.k.7J. +7.r.7D. +8.2z.2L. +8.6z.1P. +b.6+.15. +h.1S.6x. +0.1B.6J. +0.1D.6H. +0.1N.6B. +0.1n.6P. +0.1p.6O. +0.1v.6M. +0.1X.6u. +0.2I.2G. +0.G.7t. +0.y.7A. +1.2f.6g. +1.6L.1z. +2.1t.6N. +5./.77. +5.1h.6Q. +5.2q.5z. +7.r.7E. +7.s.7D. +7.u.7B. +8.2A.2L. +8.6z.1Q. +a.2P.2w. +b.6+.16. +d.71.13. +g.2E.2K. +0.1C.6J. +0.1O.6B. +0.1o.6P. +0.1q.6O. +0.1w.6M. +0.1X.6v. +0.7A.z. +0.H.7t. +1.1V.6w. +1.6L.1A. +3.2B.2L. +3.6R.1h. +3.6V.1d. +4.1K.6E. +4.s.7E. +5./.78. +7.u.7C. +b.6+.17. +d.71.14. +0.1E.6H. +0.1L.6E. +0.1P.6B. +0.1p.6P. +0.1r.6O. +0.1V.6x. +0.1x.6M. +0.2C.2L. +0.2Q.2w. +1.6L.1B. +2.29.6m. +2.2H.2I. +3.2x.2N. +3.2y.2M. +3.6R.1i. +3.I.7t. +4.1D.6J. +5./.79. +5.2q.5A. +7.u.7D. +b.6+.18. +d.71.15. +j.0.7N. +0.1F.6H. +0.1Q.6B. +0.1q.6P. +0.1s.6O. +0.1y.6M. +0.2R.2w. +1.6L.1C. +2.1.7N. +2.2n.5S. +3.2x.2O. +3.6R.1j. +3.J.7t. +4.1M.6E. +4.r.7F. +5./.7a. +5.1h.6T. +7.u.7E. +8.2z.2M. +9.1V.6y. +b.6+.19. +d.71.16. +f.1W.6x. +0.1G.6H. +0.1r.6P. +0.1t.6O. +0.1z.6M. +0.A.7A. +0.K.7t. +1.2f.6h. +1.6L.1D. +2.2.7N. +3.6V.1e. +4.1E.6J. +4.1N.6E. +5./.7b. +5.c.7L. +7.s.7F. +8.2A.2M. +8.6z.1R. +a.2F.2J. +a.2P.2x. +b.6+.1a. +d.71.17. +0.1A.6M. +0.1F.6J. +0.1H.6H. +0.1O.6E. +0.1s.6P. +0.2w.2S. +0.B.7A. +0.L.7t. +2.3.7N. +2.c.7M. +3.1u.6O. +3.2B.2M. +3.6V.1f. +4.r.7G. +5./.7c. +8.6z.1S. +b.6+.1b. +d.71.18. +f.2L.2D. +h.1X.6x. +0.1B.6M. +0.1G.6J. +0.1I.6H. +0.1P.6E. +0.1t.6P. +0.1v.6O. +0.2C.2M. +0.C.7A. +1.6L.1E. +2.4.7N. +3.2x.2Q. +3.2y.2N. +3.6R.1k. +3.6V.1g. +3.M.7t. +4.s.7G. +4.u.7F. +5./.7d. +5.2q.5C. +7.r.7H. +a.2J.2G. +b.6+.1c. +d.71.19. +f.2I.2I. +g.2E.2L. +0.1C.6M. +0.1H.6J. +0.1J.6H. +0.1Q.6E. +0.1R.6B. +0.1w.6O. +0.D.7A. +1.2f.6i. +1.6L.1F. +2.5.7N. +3.1l.6R. +3.1u.6P. +3.2x.2R. +3.2y.2O. +5./.7e. +5.2q.5D. +6.6o.1Y. +7.r.7I. +7.s.7H. +8.2z.2N. +d.71.1a. +0.1D.6M. +0.1I.6J. +0.1S.6B. +0.1v.6P. +0.1x.6O. +0.E.7A. +1.2f.6j. +1.6L.1G. +2.2H.2J. +2.6.7N. +2.A.7B. +3.6R.1m. +4.r.7J. +4.s.7I. +4.u.7G. +5./.7f. +5.1h.6U. +6.6p.1Y. +8.2A.2N. +8.2z.2O. +8.6z.1V. +a.2F.2K. +a.2P.2y. +b.6+.1d. +d.71.1b. +0.1J.6J. +0.1w.6P. +0.1y.6O. +0.F.7A. +0.N.7t. +1.6L.1H. +2.1d.6/. +2.2f.6k. +2.2g.65. +2.7.7N. +2.A.7C. +3.2B.2N. +3.2x.2S. +3.6R.1n. +3.6V.1h. +4.s.7J. +5./.7g. +7.u.7H. +8.2A.2O. +8.2z.2P. +8.6z.1W. +d.71.1c. +f.2M.2D. +0.1E.6M. +0.1K.6H. +0.1x.6P. +0.1z.6O. +0.2C.2N. +0.2G.2K. +0.O.7t. +1.2f.6l. +1.2n.5T. +1.6L.1I. +2.8.7N. +2.A.7D. +2.k.7K. +3.2B.2O. +3.2y.2Q. +3.6R.1o. +3.6V.1i. +5./.7h. +5.1h.6W. +5.5F.2q. +7.u.7I. +8.2A.2P. +f.1V.6A. +g.2E.2M. +0.1A.6O. +0.1F.6M. +0.1L.6H. +0.1R.6E. +0.1y.6P. +0.2C.2O. +0.P.7t. +1.6L.1J. +2.9.7N. +3.2B.2P. +3.2y.2R. +3.6R.1p. +3.6V.1j. +4.5G.2q. +4.u.7J. +5.1h.6X. +8.2z.2Q. +8.6z.1X. +b.6+.1e. +d.71.1d. +f.1V.6B. +0.1B.6O. +0.1G.6M. +0.1M.6H. +0.1W.6B. +0.1Z.6o. +0.1z.6P. +0.G.7A. +0.Q.7t. +2.0.7R. +2.1d.72. +2.2g.67. +2.2H.2K. +2.a.7N. +3.6R.1q. +4.1K.6J. +4.1S.6E. +5./.7i. +5.1h.6Y. +6.6s.1Y. +8.2A.2Q. +8.2z.2R. +9.1+.6n. +a.2J.2I. +a.2P.2C. +b.6+.1f. +0.1+.6o. +0.1A.6P. +0.1C.6O. +0.1H.6M. +0.1L.6J. +0.1Z.6p. +0.2N.2D. +0.H.7A. +0.R.7t. +2./.7j. +2.1.7R. +2.2f.6m. +2.b.7N. +3.2B.2Q. +3.2y.2S. +3.6R.1r. +4.1N.6H. +5.1h.6Z. +8.2A.2R. +9.1V.6C. +b.6+.1g. +0.1/.6o. +0.1+.6p. +0.1B.6P. +0.1D.6O. +0.1I.6M. +0.1M.6J. +0.1O.6H. +0.1X.6B. +0.2C.2Q. +0.2O.2D. +0.6u.1Y. +1.6L.1K. +2.1.7S. +2.2.7R. +2.A.7F. +3.2B.2R. +3.6R.1s. +3.6V.1k. +3.I.7A. +3.S.7t. +5./.7k. +5.1t.6Q. +5.1V.6D. +8.2z.2S. +a.2F.2L. +d.2U.2w. +d.71.1e. +g.2E.2N. +0.1/.6p. +0.1C.6P. +0.1J.6M. +0.1P.6H. +0.1V.6E. +0.20.6o. +0.6v.1Y. +1.1+.6q. +1.6L.1L. +2.1d.75. +2.2.7S. +2.3.7R. +3.1l.6V. +3.6R.1t. +3.J.7A. +4.1N.6J. +5./.7l. +5.2q.5I. +5.k.7L. +8.2A.2S. +a.2P.2D. +c.T.7t. +d.71.1f. +f.2C.2R. +g.2E.2O. +0.1D.6P. +0.1E.6O. +0.1O.6J. +0.1Q.6H. +0.20.6p. +0.2I.2K. +0.2L.2G. +0.K.7A. +0.U.7t. +1.6L.1M. +2.1d.76. +2.3.7S. +2.4.7R. +2.A.7G. +2.c.7O. +2.k.7M. +3.1u.6R. +3.2B.2S. +3.6V.1m. +5./.7m. +9.22.6n. +b.6+.1h. +d.71.1g. +e.21.6o. +f.1W.6E. +g.2E.2P. +0.1F.6O. +0.1P.6J. +0.22.6o. +0.2C.2S. +0.L.7A. +0.V.7t. +1.6L.1N. +2.1d.77. +2.2g.6a. +2.4.7S. +2.5.7R. +2.A.7H. +3.6R.1v. +3.6V.1n. +5./.7n. +5.1h.6/. +5.1t.6T. +5.1V.6F. +6.6s.1Z. +9.1+.6r. +b.6+.1i. +e.21.6p. +f.2Q.2D. +0.1E.6P. +0.1G.6O. +0.1K.6M. +0.1Q.6J. +0.22.6p. +0.23.6o. +0.W.7t. +1.6L.1O. +2.1d.78. +2.2H.2L. +2.5.7S. +2.c.7P. +3.6R.1w. +3.6V.1o. +3.M.7A. +4.1X.6E. +5./.7o. +5.1h.70. +6.6s.1+. +9.24.6n. +a.2F.2M. +a.2J.2J. +b.6+.1j. +d.2U.2x. +e.0.7V. +e.6.7R. +f.2R.2D. +g.2E.2Q. +0.1F.6P. +0.1H.6O. +0.1L.6M. +0.23.6p. +0.24.6o. +0.X.7t. +1.22.6q. +1.2n.5U. +1.6L.1P. +2.0.7W. +2.6.7S. +2.7.7R. +2.r.7K. +3.6R.1x. +3.6V.1p. +5.1h.71. +6.6s.1/. +9.1+.6t. +a.2V.2w. +e.1.7V. +f.1Z.6u. +g.2E.2R. +h.1Y.6x. +0.1+.6u. +0.1G.6P. +0.1I.6O. +0.1M.6M. +0.1Z.6v. +0.24.6p. +0.25.6o. +0.2M.2G. +0.2S.2D. +1.6L.1Q. +2.1.7W. +2.1d.7a. +2.7.7S. +2.8.7R. +2.s.7K. +3.2W.2w. +3.6R.1y. +3.6V.1q. +5.1h.72. +5.1V.6G. +6.6s.20. +9.26.6n. +c.Y.7t. +d.71.1i. +e.2.7V. +f.1R.6H. +0.1+.6v. +0.1H.6P. +0.1J.6O. +0.1S.6H. +0.25.6p. +0.26.6o. +0.N.7A. +0.Z.7t. +1.24.6q. +1.2n.5V. +2.2.7W. +2.8.7S. +2.9.7R. +3.6R.1z. +3.6V.1r. +4.1N.6M. +5./.7p. +5.1h.73. +6.6s.21. +9.22.6r. +b.6+.1k. +d.71.1j. +e.3.7V. +f.1/.6u. +g.2E.2S. +0.1/.6v. +0.1I.6P. +0.1O.6M. +0.20.6u. +0.26.6p. +0.27.6o. +0.2L.2I. +0.O.7A. +2.1d.7c. +2.2H.2M. +2.3.7W. +2.9.7S. +2.a.7R. +2.c.7Q. +2.u.7K. +3.6R.1A. +3.6V.1s. +5./.7q. +5.1h.74. +5.1t.6U. +6.6s.22. +a.2F.2N. +a.2J.2K. +b.6+.1l. +d.2U.2y. +e.4.7V. +f.1R.6J. +0.1J.6P. +0.1P.6M. +0.1S.6J. +0.20.6v. +0.27.6p. +0.P.7A. +1.26.6q. +2.1d.7d. +2.4.7W. +2.a.7S. +2.b.7R. +3.2x.2V. +3.6R.1B. +5./.7r. +5.1h.75. +6.6s.23. +6.6V.1t. +9.22.6t. +9.24.6r. +a.2F.2O. +b.6+.1m. +d.2U.2z. +e.21.6u. +e.5.7V. +0.1K.6O. +0.1Q.6M. +0.1V.6H. +0.2N.2G. +0.Q.7A. +1.1+.6w. +1.6L.1R. +2.1d.7e. +2.5.7W. +2.b.7S. +3.1u.6V. +3.2W.2x. +3.6R.1C. +5.1h.76. +5.1t.6W. +6.6s.24. +7.r.7L. +a.2P.2F. +b.6+.1n. +c.28.6o. +d.2U.2A. +d.71.1k. +e.21.6v. +e.6.7V. +f.22.6u. +h.1Z.6x. +0.+.7t. +0.1L.6O. +0.1W.6H. +0.22.6v. +0.23.6u. +0.2O.2G. +0.R.7A. +1.6L.1S. +1.c.7S. +2.1d.7f. +2.2n.5W. +2.6.7W. +2.r.7M. +3.6R.1D. +3.6V.1v. +5./.7s. +5.1h.77. +5.1t.6X. +6.6s.25. +7.s.7L. +8.6z.1Y. +9.1V.6I. +9.24.6t. +9.26.6r. +a.2Y.2w. +b.6+.1o. +c.28.6p. +d.2U.2B. +d.71.1l. +e.7.7V. +h.1+.6x. +0./.7t. +0.1K.6P. +0.1M.6O. +0.1V.6J. +0.23.6v. +0.24.6u. +0.2I.2M. +0.2K.2K. +2.2H.2N. +2.2n.5X. +2.7.7W. +2.s.7M. +3.6V.1w. +3.S.7A. +5.1h.78. +5.1t.6Y. +6.6s.26. +9.1+.6y. +a.2F.2Q. +a.2P.2G. +b.6+.1p. +d.2U.2C. +d.71.1m. +e.8.7V. +h.1/.6x. +0.10.7t. +0.1L.6P. +0.1W.6J. +0.1X.6H. +0.24.6v. +0.25.6u. +2.1d.7h. +2.2H.2O. +2.2n.5Y. +2.8.7W. +2.k.7O. +3.2y.2V. +3.6R.1E. +3.6V.1x. +4.1N.6O. +5.1h.79. +5.1t.6Z. +5.c.7T. +6.6s.27. +7.u.7L. +9.1V.6K. +9.26.6t. +a.2F.2R. +b.6+.1q. +c.T.7A. +d.71.1n. +e.9.7V. +h.20.6x. +0.11.7t. +0.1M.6P. +0.1O.6O. +0.25.6v. +0.2Q.2G. +0.U.7A. +1.22.6w. +1.2n.5Z. +1.6L.1V. +2.0.7Y. +2.2H.2P. +2.9.7W. +2.u.7M. +3.2W.2y. +3.6R.1F. +3.6V.1y. +4.26.6u. +5./.7u. +5.1h.7a. +6.6B.1Y. +8.2z.2V. +a.2J.2L. +b.6+.1r. +d.71.1o. +e.21.6x. +e.a.7V. +f.1R.6M. +0.1P.6O. +0.1S.6M. +0.1X.6J. +0.26.6v. +0.2R.2G. +0.V.7A. +1.6L.1W. +1.c.7U. +2.1.7Y. +2.1d.7i. +2.2n.5+. +2.a.7W. +2.k.7P. +3.2x.2Y. +3.6R.1G. +3.6V.1z. +4.12.7t. +4.1N.6P. +5.1h.7b. +8.2A.2V. +8.2z.2W. +a.2F.2S. +b.6+.1s. +c.28.6s. +d.2U.2D. +d.71.1p. +e.0.7Z. +e.b.7V. +f.27.6u. +h.22.6x. +0.1O.6P. +0.1Q.6O. +0.27.6v. +0.2N.2I. +0.W.7A. +1.24.6w. +2.1d.7j. +2.2.7Y. +2.2H.2Q. +2.A.7K. +2.b.7W. +3.2B.2V. +3.6R.1H. +3.6V.1A. +5./.7v. +5.1h.7c. +8.2A.2W. +8.6z.1Z. +9.22.6y. +b.6+.1t. +d.2U.2E. +d.71.1q. +e.1.7Z. +h.23.6x. +i.c.7V. +0.1P.6P. +0.2G.2S. +0.2I.2O. +0.X.7A. +1.6L.1X. +2.1d.7k. +2.2H.2R. +2.3.7Y. +3.2B.2W. +3.6R.1I. +3.6V.1B. +5./.7w. +5.1h.7d. +5.1t.6/. +8.6z.1+. +a.2V.2C. +b.6+.1u. +c.28.6u. +d.71.1r. +e.2.7Z. +h.24.6x. +0.13.7t. +0.1V.6M. +0.25.6x. +0.2L.2K. +1.26.6w. +2.4.7Y. +3.2W.2C. +3.6R.1J. +3.6V.1C. +5./.7x. +5.1h.7e. +5.1t.70. +8.6z.1/. +9.24.6y. +a.2J.2M. +a.2P.2I. +b.6+.1v. +c.28.6v. +c.Y.7A. +d.71.1s. +e.3.7Z. +f.1Q.6P. +0.14.7t. +0.1W.6M. +0.1Z.6B. +0.Z.7A. +1.c.7X. +2.1d.7m. +2.2H.2S. +2.k.7Q. +3.2y.2Y. +3.6V.1D. +5.1h.7f. +5.1t.71. +8.6z.20. +9.1+.6A. +b.6+.1w. +e.4.7Z. +h.26.6x. +0.1+.6B. +0.15.7t. +0.2Q.2I. +0.d.7V. +2.1d.7n. +2.1V.6N. +2.2n.5/. +2.6.7Y. +5.1h.7g. +5.1t.72. +8.2+.2w. +8.2z.2Y. +8.6z.21. +9.26.6y. +a.2V.2D. +b.6+.1x. +d.71.1u. +e.5.7Z. +f.1R.6O. +h.27.6x. +0.1/.6B. +0.16.7t. +0.1X.6M. +0.e.7V. +2.1d.7o. +2.2n.60. +2.7.7Y. +3.2W.2D. +3.6R.1K. +3.6V.1E. +4.1S.6O. +5./.7y. +5.1h.7h. +5.1t.73. +8.2A.2Y. +8.6z.22. +b.6+.1y. +d.71.1v. +e.6.7Z. +f.2I.2R. +g.2E.2V. +0.17.7t. +0.20.6B. +0.2M.2K. +0.f.7V. +1.2n.61. +1.k.7S. +2.8.7Y. +2.A.7M. +2.r.7O. +3.2B.2Y. +3.6R.1L. +3.6V.1F. +5.1t.74. +8.6z.23. +9.1+.6C. +a.2J.2N. +b.6+.1z. +d.71.1w. +e.7.7Z. +f.1R.6P. +g.2E.2W. +h.28.6x. +0.+.7A. +0.18.7t. +0.1S.6P. +0.2I.2S. +0.g.7V. +1.2n.62. +2.2/.2w. +2.9.7Y. +2.s.7O. +3.6R.1M. +3.6V.1G. +4./.7z. +5.1h.7i. +5.1t.75. +7.1+.6D. +9.22.6A. +9.24.6z. +a.2J.2O. +a.2Y.2C. +b.6+.1A. +d.71.1x. +e.8.7Z. +f.1Z.6E. +j.21.6B. +0./.7A. +0.1+.6E. +0.19.7t. +0.22.6B. +0.2L.2L. +0.h.7V. +2.1d.7p. +2.1h.7j. +2.a.7Y. +2.r.7P. +3.6R.1N. +3.6V.1H. +5.1t.76. +5.k.7T. +8.2+.2x. +8.6z.25. +a.2P.2J. +b.6+.1B. +d.2U.2F. +d.71.1y. +e.9.7Z. +f.1V.6O. +0.1/.6E. +0.10.7A. +0.1a.7t. +0.1W.6O. +0.i.7V. +2.b.7Y. +2.s.7P. +2.u.7O. +3.6R.1O. +3.6V.1I. +5.1h.7k. +5.1t.77. +5.29.6n. +8.6z.26. +9.24.6A. +b.6+.1C. +d.71.1z. +e.a.7Z. +f.23.6B. +0.11.7A. +0.1b.7t. +0.1V.6P. +0.20.6E. +0.24.6B. +0.29.6o. +0.2N.2K. +0.j.7V. +1.c.7Y. +1.k.7U. +2.2n.63. +3.6R.1P. +3.6V.1J. +5.1h.7l. +5.1t.78. +7.1+.6F. +8.6z.27. +a.2J.2Q. +a.2Y.2D. +b.6+.1D. +d.2U.2G. +d.71.1A. +e.b.7Z. +f.22.6C. +j.2a.6n. +0.1c.7t. +0.1W.6P. +0.1X.6O. +0.1Y.6H. +0.25.6B. +0.29.6p. +0.2a.6o. +0.2O.2K. +0.2w.30. +0.k.7V. +2.2/.2x. +2.u.7P. +3.6R.1Q. +4.12.7A. +5.1h.7m. +5.1t.79. +7.22.6D. +9.26.6A. +a.2J.2R. +d.71.1B. +e.21.6E. +g.2E.2Y. +i.c.7Z. +0.22.6E. +0.26.6B. +0.2a.6p. +0.2L.2M. +0.l.7V. +1.29.6q. +2.1d.7s. +2.2H.2U. +2.r.7Q. +5./.7B. +5.1h.7n. +5.1t.7a. +8.2+.2y. +8.6z.28. +9.24.6C. +a.2P.2K. +b.6+.1E. +d.71.1C. +0.1d.7t. +0.23.6E. +0.27.6B. +0.m.7V. +2.2n.64. +2.s.7Q. +3.6V.1K. +4.1X.6P. +4.6J.1Y. +5./.7C. +5.1h.7o. +5.1t.7b. +7.24.6D. +8.2+.2z. +a.2J.2S. +a.2V.2F. +b.6+.1F. +d.71.1D. +f.1+.6G. +0.13.7A. +0.24.6E. +0.2c.6o. +0.2Q.2K. +1.k.7X. +3.2W.2F. +3.6V.1L. +3.n.7V. +5./.7D. +5.1t.7c. +5.29.6r. +7.22.6F. +8.2+.2A. +9.26.6C. +b.6+.1G. +0.14.7A. +0.25.6E. +0.2c.6p. +0.2R.2K. +0.o.7V. +1.2n.65. +1.6L.1Y. +1.r.7S. +2.1d.7u. +2.2/.2y. +2.u.7Q. +3.2x.30. +3.6R.1R. +3.6V.1M. +4.d.7Z. +5./.7E. +5.1t.7d. +5.c.7+. +6.6s.29. +7.26.6D. +8.2+.2B. +a.2V.2G. +b.6+.1H. +c.28.6B. +d.71.1E. +j.2a.6r. +0.15.7A. +0.1e.7t. +0.1Z.6H. +0.26.6E. +0.2N.2L. +0.p.7V. +1.s.7S. +2.2/.2z. +3.2W.2G. +3.6R.1S. +3.6V.1N. +4.e.7Z. +5.1h.7p. +5.1t.7e. +5.29.6t. +5.6s.2a. +7.24.6F. +8.2+.2C. +b.6+.1I. +d.2U.2I. +d.71.1F. +f.2M.2M. +0.1+.6H. +0.16.7A. +0.1f.7t. +0.27.6E. +0.2K.2S. +0.2L.2O. +0.q.7V. +2.1d.7v. +2.2/.2A. +2.2H.2V. +2.2n.66. +2.A.7O. +3.6V.1O. +4.f.7Z. +4.r.7T. +5.1h.7q. +5.1t.7f. +6.6o.2d. +7.22.6G. +b.6+.1J. +d.71.1G. +f.29.6u. +j.2a.6t. +0.1/.6H. +0.17.7A. +0.29.6v. +1.2n.67. +1.u.7S. +2.2/.2B. +2.2H.2W. +3.6V.1P. +4.g.7Z. +4.s.7T. +5./.7F. +5.1h.7r. +5.1t.7g. +5.1V.6Q. +5.2a.6u. +6.6p.2d. +7.26.6F. +9.1+.6I. +a.2P.2L. +d.71.1H. +f.1Z.6J. +h.1g.7t. +0.1+.6J. +0.18.7A. +0.20.6H. +1.r.7U. +2.1d.7x. +2.2/.2C. +2.2n.68. +2.A.7P. +3.2y.30. +3.6R.1V. +3.6V.1Q. +4.h.7Z. +5.1t.7h. +5.2a.6v. +5.c.7/. +6.6M.1Y. +6.6s.2c. +7.24.6G. +8.2+.2D. +a.2Y.2F. +c.28.6E. +d.71.1I. +0.1/.6J. +0.19.7A. +0.2N.2M. +0.2Q.2L. +0.r.7V. +1.2f.6n. +1.31.2w. +1.6L.1Z. +1.s.7U. +3.6R.1W. +4.i.7Z. +5./.7G. +5.1h.7s. +7.u.7T. +8.2+.2E. +8.2z.30. +9.1+.6K. +b.6+.1K. +d.71.1J. +j.21.6H. +0.1a.7A. +0.1h.7t. +0.20.6J. +0.2M.2O. +0.s.7V. +1.29.6w. +1.6L.1+. +1.c.80. +1.k.7Y. +4.j.7Z. +5./.7H. +5.1t.7i. +5.1V.6T. +6.6v.2b. +7.26.6G. +8.2A.30. +a.2V.2I. +a.2Y.2G. +b.6+.1L. +f.22.6H. +f.2c.6u. +f.2L.2R. +0.1b.7A. +0.1i.7t. +0.23.6H. +0.2c.6v. +0.t.7V. +1.2n.69. +1.6L.1/. +1.u.7U. +2.1t.7j. +2.2/.2D. +3.2B.30. +3.2W.2I. +3.6R.1X. +4.k.7Z. +5./.7I. +5.5L.2q. +6.6s.2d. +9.22.6I. +a.2P.2M. +b.6+.1M. +d.2U.2J. +h.29.6x. +j.21.6J. +0.1c.7A. +0.1j.7t. +0.22.6J. +0.24.6H. +0.2C.30. +0.2L.2S. +0.u.7V. +1.2f.6q. +1.2n.6a. +1.6L.20. +1.r.7X. +2.2/.2E. +2.2H.2Y. +2.A.7Q. +3.6V.1R. +4.l.7Z. +5./.7J. +5.1h.7u. +5.1t.7k. +5.29.6y. +5.2a.6x. +b.6+.1N. +d.71.1K. +0.1Z.6M. +0.23.6J. +0.25.6H. +0.2d.6u. +0.2N.2N. +0.2Q.2M. +0.v.7V. +1.31.2x. +1.6L.21. +1.s.7X. +3.6V.1S. +4.m.7Z. +5.1t.7l. +9.22.6K. +9.24.6I. +a.32.2w. +b.6+.1O. +d.71.1L. +0.1+.6M. +0.1d.7A. +0.24.6J. +0.2N.2O. +0.6v.2d. +0.w.7V. +1.2f.6r. +1.2n.6b. +1.6L.22. +3.n.7Z. +4.26.6H. +5.1h.7v. +5.1t.7m. +6.6O.1Y. +7.6x.2b. +b.6+.1P. +d.71.1M. +f.2M.2R. +0.1/.6M. +0.1k.7t. +0.25.6J. +0.27.6H. +0.x.7V. +1.6L.23. +1.u.7X. +4.2O.2O. +4.o.7Z. +5.1h.7w. +5.1t.7n. +5.1V.6U. +5.k.7+. +6.6v.2e. +9.24.6K. +9.26.6I. +a.2P.2N. +b.6+.1Q. +d.2U.2K. +d.71.1N. +f.30.2D. +h.2c.6x. +0.20.6M. +0.2M.2S. +0.6P.1Y. +0.y.7V. +1.2f.6t. +1.33.2w. +1.6L.24. +3.1l.7t. +3.6V.1V. +4.26.6J. +4.p.7Z. +5.1h.7x. +5.1t.7o. +5.2q.5O. +a.2P.2O. +a.2V.2J. +a.2Y.2I. +d.71.1O. +g.2E.30. +k.6N.1+. +0.1e.7A. +0.1m.7t. +0.27.6J. +0.2Q.2N. +0.2w.34. +1.31.2y. +1.6L.25. +2.A.7T. +3.2W.2J. +3.6V.1W. +4.q.7Z. +5.1V.6W. +5.2q.5P. +5.c.81. +8.2+.2F. +8.6z.29. +9.26.6K. +a.2P.2P. +a.32.2x. +c.28.6H. +d.71.1P. +f.7V.z. +j.21.6M. +0.1f.7A. +0.1n.7t. +0.22.6M. +0.2N.2R. +0.2Q.2O. +1.31.2z. +1.6L.26. +1.c.82. +2.1d.7B. +5.1V.6X. +6.6v.2f. +8.6z.2a. +d.71.1Q. +h.2d.6x. +0.1o.7t. +0.23.6M. +0.7A.1g. +1.31.2A. +1.6L.27. +1.c.83. +1.r.7Y. +2.1d.7C. +3.6V.1X. +5.1h.7y. +5.1t.7p. +5.1V.6Y. +5.29.6A. +5.2q.5Q. +5.k.7/. +7.6x.2e. +8.2+.2G. +a.2P.2Q. +b.6+.1R. +c.28.6J. +f.1Z.6O. +f.2R.2O. +0.1+.6O. +0.1p.7t. +0.24.6M. +0.29.6B. +0.2N.2S. +0.A.7V. +0.r.7Z. +1.31.2B. +1.33.2x. +1.s.7Y. +2.1d.7D. +2.2/.2F. +2.22.6N. +5.1t.7q. +5.1V.6Z. +5.c.84. +8.6z.2b. +a.2P.2R. +a.2V.2K. +b.6+.1S. +0.1/.6O. +0.1q.7t. +0.1Z.6P. +0.25.6M. +0.2O.2S. +0.2Q.2Q. +0.B.7V. +1.2f.6w. +1.31.2C. +1.6L.28. +1.k.80. +2.2H.2+. +3.2W.2K. +3.2x.34. +4.s.7Z. +5.1h.7z. +5.1t.7r. +5.2a.6B. +6.85.c. +8.6z.2c. +a.32.2y. +d.2U.2L. +0.1+.6P. +0.1h.7A. +0.1r.7t. +0.20.6O. +0.C.7V. +1.2n.6c. +1.c.86. +1.u.7Y. +2.2/.2G. +2.24.6N. +4.26.6M. +4.t.7Z. +5.29.6C. +5.2q.5R. +7.6x.2f. +8.2z.32. +a.2P.2S. +a.2Y.2J. +d.71.1R. +f.2Q.2R. +0.1/.6P. +0.1i.7A. +0.1s.7t. +0.27.6M. +0.D.7V. +1.2f.6y. +2./.7K. +4.u.7Z. +5.1t.7s. +5.29.6D. +8.2A.32. +b.6+.1V. +d.71.1S. +e.21.6O. +f.2R.2R. +f.37.2w. +0.1j.7A. +0.20.6P. +0.22.6O. +0.29.6E. +0.2c.6B. +0.2Q.2S. +0.E.7V. +0.v.7Z. +1.31.2D. +1.33.2y. +2.1d.7F. +2.26.6N. +2.2H.2/. +3.2B.32. +4.1t.7t. +5.1V.6/. +7.r.7+. +8.6z.2d. +a.2F.30. +b.6+.1W. +0.23.6O. +0.F.7V. +1.31.2E. +1.33.2z. +3.1u.7t. +3.2y.34. +3.6R.1Y. +4.s.7+. +4.w.7Z. +5.1V.70. +5.c.87. +6.85.d. +8.2+.2I. +8.6z.2e. +a.32.2C. +c.28.6M. +d.2U.2M. +e.21.6P. +f.2a.6E. +f.2R.2S. +0.1v.7t. +0.22.6P. +0.24.6O. +0.30.2G. +1.33.2A. +2.1d.7G. +4.x.7Z. +5.1h.7B. +5.1t.7u. +5.29.6F. +5.c.88. +6.85.e. +8.2z.34. +a.2V.2L. +a.2Y.2K. +b.6+.1X. +d.71.1V. +0.1k.7A. +0.1w.7t. +0.23.6P. +0.25.6O. +0.2S.2S. +0.y.7Z. +1.33.2B. +2.1d.7H. +3.2W.2L. +3.2x.37. +4.u.7+. +5.1h.7C. +5.1V.72. +5.c.89. +6.6B.2d. +6.85.f. +8.2A.34. +d.71.1W. +f.39.2w. +0.1x.7t. +0.24.6P. +0.26.6O. +0.G.7V. +1.33.2C. +1.6g.2h. +2.2/.2I. +2.2H.30. +3.1l.7A. +3.2B.34. +5./.7L. +5.1h.7D. +5.1t.7v. +5.1V.73. +5.c.8a. +5.k.81. +6.85.g. +7.r.7/. +8.6z.2f. +a.32.2D. +f.2c.6E. +f.7Z.z. +0.1m.7A. +0.1y.7t. +0.25.6P. +0.27.6O. +0.2C.34. +0.H.7V. +1.6g.2i. +1.k.82. +2./.7M. +5.1h.7E. +5.1t.7w. +5.1V.74. +5.29.6G. +5.c.8b. +6.85.h. +7.s.7/. +d.2U.2N. +d.71.1X. +g.2E.32. +0.1n.7A. +0.1z.7t. +0.26.6P. +1.2f.6A. +1.6g.2j. +1.k.83. +1.r.80. +2.2n.6d. +2.A.7Y. +3.6R.1Z. +3.I.7V. +5.1t.7x. +5.1V.75. +5.c.8c. +6.85.i. +7.1+.6Q. +a.2V.2M. +d.2U.2O. +0.1A.7t. +0.1o.7A. +0.27.6P. +0.2d.6E. +1.33.2D. +1.6g.2k. +1.s.80. +2.2n.6e. +3.2W.2M. +3.2x.39. +3.2y.37. +3.6R.1+. +3.J.7V. +4.A.7Z. +5.1V.76. +5.c.8d. +5.k.84. +6.85.j. +7.u.7/. +8.2+.2J. +c.28.6O. +d.2U.2P. +0.1B.7t. +0.1p.7A. +0.2I.30. +0.34.2D. +0.K.7V. +1.33.2E. +1.6g.2l. +3.6R.1/. +4.B.7Z. +5.1h.7F. +5.1V.77. +5.c.8e. +6.85.k. +8.2z.37. +a.2Y.2L. +f.2w.3e. +0.1C.7t. +0.1q.7A. +0.29.6H. +0.L.7V. +1.2f.6C. +1.2n.6f. +1.31.2F. +1.u.80. +3.6R.20. +3.6V.1Y. +4.C.7Z. +5.1t.7y. +5.1V.78. +5.c.8f. +6.85.l. +7.1+.6T. +8.2A.37. +c.28.6P. +d.2U.2Q. +e.k.86. +g.2E.34. +0.1D.7t. +0.1r.7A. +1.2f.6D. +2.2/.2J. +2.c.8g. +3.2B.37. +3.6R.21. +3.M.7V. +4.3f.2w. +4.D.7Z. +5.1h.7G. +5.1V.79. +5.29.6I. +5.2a.6H. +5.2q.5T. +6.85.m. +7.22.6Q. +a.2V.2N. +d.2U.2R. +0.1s.7A. +1.31.2G. +2.c.8h. +3.2W.2N. +3.2y.39. +3.6R.22. +3.n.85. +4.1t.7z. +4.E.7Z. +5.1h.7H. +5.1V.7a. +8.2+.2K. +a.2V.2O. +f.29.6J. +f.37.2C. +j.2a.6I. +0.1E.7t. +0.1t.7A. +3.2W.2O. +3.2x.3e. +3.6R.23. +4.F.7Z. +5.1h.7I. +5.1V.7b. +5.29.6K. +5.2a.6J. +5.k.87. +6.85.o. +8.2z.39. +a.2P.2V. +a.2Y.2M. +d.2U.2S. +f.24.6Q. +0.1F.7t. +0.N.7V. +1.2f.6F. +1.6L.29. +2.2H.31. +3.1u.7A. +3.2W.2P. +3.6R.24. +5.1h.7J. +5.1V.7c. +5.k.88. +6.85.p. +7.22.6T. +7.r.81. +8.2A.39. +a.32.2F. +f.2c.6H. +j.2a.6K. +0.1G.7t. +0.1v.7A. +0.2w.3g. +0.O.7V. +1.2n.6h. +1.6L.2a. +1.r.82. +2.2/.2K. +3.2B.39. +3.2x.3f. +3.6R.25. +3.6V.1Z. +5.1V.7d. +5.c.8i. +5.k.89. +6.85.q. +7.1+.6U. +7.26.6Q. +7.s.81. +a.2J.30. +a.2V.2Q. +c.37.2D. +0.1H.7t. +0.1w.7A. +0.P.7V. +1.r.83. +1.s.82. +3.2W.2Q. +3.6R.26. +3.6V.1+. +4.G.7Z. +5.1V.7e. +5.k.8a. +a.2V.2R. +a.32.2G. +f.24.6T. +f.25.6S. +f.2C.39. +f.2c.6J. +g.2E.37. +0.1I.7t. +0.1x.7A. +0.2d.6H. +0.Q.7V. +1.2f.6G. +1.33.2F. +1.6L.2b. +1.s.83. +2./.7O. +2.1d.7K. +3.2W.2R. +3.2y.3e. +3.6R.27. +3.6V.1/. +4.H.7Z. +5.1t.7B. +5.1V.7f. +5.c.8j. +5.k.8b. +7.1+.6W. +7.r.84. +7.u.81. +a.2Y.2N. +b.6+.1Y. +0.1J.7t. +0.1y.7A. +0.29.6M. +0.2w.3h. +0.R.7V. +1.2n.6i. +1.31.2I. +1.6L.2c. +1.u.82. +2.2H.32. +3.6V.20. +3.I.7Z. +4.s.84. +5.1t.7C. +5.1V.7g. +5.k.8c. +6.85.r. +7.1+.6X. +7.26.6T. +8.2+.2L. +8.2z.3e. +a.2F.34. +a.2V.2S. +a.2Y.2O. +0.1z.7A. +0.2d.6J. +0.30.2K. +1.2n.6j. +1.33.2G. +1.c.8k. +1.u.83. +2./.7P. +3.2W.2S. +3.2x.3g. +3.2y.3f. +3.6R.28. +3.6V.21. +3.J.7Z. +3.S.7V. +5.1t.7D. +5.1V.7h. +5.k.8d. +6.3i.2w. +6.85.s. +7.1+.6Y. +7.22.6U. +8.2A.3e. +a.2P.2Y. +e.r.86. +f.2a.6M. +f.39.2D. +0.1A.7A. +0.2G.34. +1.c.8l. +2.29.6N. +2.2n.6k. +3.2B.3e. +3.6V.22. +4.K.7Z. +4.u.84. +5.1t.7E. +5.2q.5U. +5.k.8e. +6.85.t. +7.1+.6Z. +8.2z.3f. +c.T.7V. +d.71.1Y. +e.s.86. +g.2E.39. +0.1B.7A. +0.1K.7t. +0.L.7Z. +0.U.7V. +1.2n.6l. +1.6L.2d. +2.2/.2L. +2.2H.33. +3.6V.23. +5.1V.7i. +5.k.8f. +6.85.u. +7.22.6W. +7.24.6U. +8.2A.3f. +a.2Y.2Q. +f.2C.3e. +j.2a.6N. +0.1C.7A. +0.1L.7t. +0.2c.6M. +0.V.7V. +1.2f.6I. +1.6L.2e. +2.1V.7j. +2.2H.34. +2.k.8g. +3.2B.3f. +3.2x.3h. +3.6V.24. +3.M.7Z. +4.r.87. +5.2q.5V. +6.85.v. +7.22.6X. +8.2+.2M. +a.2Y.2R. +a.32.2I. +b.6+.1Z. +d.2U.2U. +e.u.86. +0.1D.7A. +0.1M.7t. +0.W.7V. +1.6N.2b. +2./.7Q. +2.1d.7M. +2.k.8h. +3.2x.3i. +3.2y.3g. +3.6V.25. +4.s.87. +5.1t.7F. +5.1V.7k. +5.c.8m. +6.3m.2w. +6.85.w. +7.22.6Y. +7.24.6W. +7.26.6U. +7.r.88. +b.6+.1+. +c.3f.2C. +0.29.6O. +0.2D.3e. +0.X.7V. +1.2f.6K. +1.31.2J. +2.2n.6m. +3.6V.26. +4.1N.7t. +5.1V.7l. +5.c.8n. +6.85.x. +7.1+.6/. +7.22.6Z. +7.24.6X. +7.r.89. +7.s.88. +8.2z.3g. +a.2F.37. +a.2Y.2S. +b.6+.1/. +0.1E.7A. +0.1O.7t. +0.2L.30. +1.2f.6L. +1.33.2I. +1.c.8o. +2.1h.7K. +2.2/.2M. +3.6V.27. +4.N.7Z. +4.u.87. +5.1t.7G. +5.1V.7m. +6.6M.2d. +6.85.y. +7.1+.70. +7.24.6Y. +7.26.6W. +7.r.8a. +7.s.89. +8.2A.3g. +b.6+.20. +c.Y.7V. +d.71.1Z. +f.2a.6O. +g.2E.3e. +0.1F.7A. +0.1P.7t. +0.29.6P. +0.2I.34. +0.Z.7V. +1./.7S. +1.c.8p. +3.2B.3g. +3.2y.3h. +4.O.7Z. +4.r.8b. +5.1t.7H. +5.1V.7n. +5.2g.6o. +5.k.8i. +6.85.z. +7.24.6Z. +7.26.6X. +7.s.8a. +7.u.88. +8.2+.2N. +b.6+.21. +c.37.2G. +c.3f.2D. +d.71.1+. +0.1G.7A. +0.1Q.7t. +0.2C.3g. +3.2x.3m. +3.2y.3i. +3.6V.28. +4.P.7Z. +4.s.8b. +4.u.89. +5.1t.7I. +5.1V.7o. +5.2a.6P. +5.2g.6p. +7.1+.72. +7.26.6Y. +7.r.8c. +8.2+.2O. +8.2z.3h. +b.6+.22. +d.2U.2V. +d.71.1/. +f.2w.3q. +g.2E.3f. +0.1H.7A. +0.2c.6O. +1.31.2K. +1.6N.2e. +1.c.8q. +2.2H.37. +2.A.84. +4.Q.7Z. +4.s.8c. +5./.7T. +5.1t.7J. +5.k.8j. +6.3r.2w. +7.1+.73. +7.22.6/. +7.26.6Z. +7.r.8d. +7.u.8a. +8.2+.2P. +8.2A.3h. +8.2z.3i. +a.2F.39. +a.32.2J. +b.6+.23. +d.2U.2W. +d.71.20. +0.1I.7A. +0.2M.30. +0.R.7Z. +2.2/.2N. +3.2B.3h. +4.s.8d. +4.u.8b. +5.2q.5Z. +5.6P.2b. +6.85.A. +7.1+.74. +7.22.70. +7.r.8e. +8.2A.3i. +b.6+.24. +d.71.21. +0.+.7V. +0.1J.7A. +0.2C.3h. +0.2c.6P. +0.2D.3g. +1./.7U. +1.k.8k. +2.2/.2O. +2.A.86. +3.2B.3i. +3.S.7Z. +4.u.8c. +5.1h.7L. +5.1V.7p. +6.85.B. +7.22.71. +7.24.6/. +7.r.8f. +7.s.8e. +8.2+.2Q. +b.6+.25. +f.1+.75. +f.39.2G. +0./.7V. +0.1R.7t. +0.2d.6O. +1.33.2J. +1.6N.2f. +1.k.8l. +2.1h.7M. +2.2/.2P. +2.r.8g. +3.2x.3q. +3.2y.3m. +4.s.8f. +4.u.8d. +5.1V.7q. +5.2g.6s. +6.3i.2C. +6.85.C. +7.1+.76. +7.22.72. +7.24.70. +8.2+.2R. +b.6+.26. +c.T.7Z. +d.71.23. +g.2E.3g. +0.10.7V. +0.1S.7t. +2.2H.39. +2.r.8h. +2.s.8g. +3.2x.3r. +4.U.7Z. +5.1V.7r. +6.85.D. +7.1+.77. +7.22.73. +7.26.6/. +7.u.8e. +8.2z.3m. +a.2J.34. +a.2V.2V. +a.32.2K. +b.6+.27. +d.71.24. +e.2g.6t. +f.37.2I. +0.11.7V. +0.1K.7A. +0.2N.30. +0.3h.2D. +0.6P.2d. +2.2/.2Q. +2.s.8h. +3.2W.2V. +4.u.8f. +4.V.7Z. +5.2g.6u. +6.85.E. +7.1+.78. +7.22.74. +7.24.72. +7.26.70. +8.2+.2S. +8.2A.3m. +9.c.8r. +a.2F.3e. +d.2U.2Y. +d.71.25. +0.1L.7A. +0.2O.30. +0.2w.3v. +1./.7X. +1.31.2L. +2.1d.7O. +2.2/.2R. +2.u.8g. +3.2B.3m. +3.2W.2W. +4.12.7V. +4.W.7Z. +5.1V.7s. +5.29.6Q. +5.6P.2e. +5.c.8s. +5.k.8m. +6.3i.2D. +6.85.F. +7.22.75. +7.24.73. +7.26.71. +8.2g.6v. +b.6+.28. +f.1+.79. +g.2E.3h. +0.1M.7A. +0.1V.7t. +1.33.2K. +2.A.89. +2.u.8h. +3.2y.3q. +3.6R.29. +4.2G.3e. +4.X.7Z. +5.k.8n. +6.3m.2C. +7.1+.7a. +7.22.76. +7.24.74. +7.26.72. +7.r.8i. +a.2F.3f. +a.2P.30. +d.71.27. +g.2E.3i. +0.1W.7t. +0.2I.39. +0.2K.34. +1.k.8o. +2.1d.7P. +2.2/.2S. +2.A.8a. +3.2y.3r. +3.6R.2a. +4.1N.7A. +7.1+.7b. +7.22.77. +7.24.75. +7.26.73. +7.s.8i. +8.2z.3q. +c.Y.7Z. +0.13.7V. +0.1O.7A. +0.2Q.30. +0.2w.3y. +1.k.8p. +2.2H.3e. +4.Z.7Z. +5.1V.7u. +5.29.6T. +5.2q.61. +5.6P.2f. +6.85.G. +7.1+.7c. +7.22.78. +7.26.74. +7.3f.2G. +7.r.8j. +8.2A.3q. +8.2z.3r. +d.71.28. +f.24.76. +0.14.7V. +0.1P.7A. +0.1X.7t. +0.2R.30. +1.31.2M. +3.2B.3q. +3.2x.3v. +5.2q.62. +6.3m.2D. +6.6R.2b. +6.85.H. +7.1+.7d. +7.22.79. +7.24.77. +7.26.75. +7.s.8j. +7.u.8i. +8.2A.3r. +a.2J.37. +a.2V.2Y. +a.32.2L. +f.2g.6x. +0.15.7V. +0.1Q.7A. +1.k.8q. +1.r.8k. +2.1t.7K. +2.2g.6y. +2.2H.3f. +3.2B.3r. +3.2W.2Y. +3.6R.2c. +3.I.85. +5.1V.7v. +7.1+.7e. +7.22.7a. +7.24.78. +7.26.76. +a.2F.3g. +f.2C.3q. +g.2E.3m. +0.16.7V. +0.30.2S. +1.r.8l. +1.s.8k. +2.1d.7Q. +2.A.8e. +3.J.85. +5.1V.7w. +5.c.8t. +6.3r.2C. +7.1+.7f. +7.22.7b. +7.24.79. +7.26.77. +7.u.8j. +0.+.7Z. +0.17.7V. +0.2G.3g. +1./.7Y. +1.33.2L. +1.s.8l. +3.2x.3y. +5.1V.7x. +6.85.K. +7.1+.7g. +7.22.7c. +7.24.7a. +7.26.78. +f.2I.3e. +0.18.7V. +0.2L.34. +0.3q.2D. +1.31.2N. +1.u.8k. +2.0.8A. +2.1h.7O. +3.2y.3v. +3.6R.2d. +4./.7Z. +5.29.6U. +6.85.L. +7.1+.7h. +7.22.7d. +7.24.7b. +7.26.79. +9.c.8u. +a.2F.3h. +a.2J.39. +a.32.2M. +c.37.2K. +d.2U.2+. +0.19.7V. +0.1R.7A. +1.31.2O. +1.u.8l. +2.1.8A. +2.2H.3g. +3.6V.29. +3.M.85. +4.10.7Z. +5.c.8v. +6.3r.2D. +6.6R.2e. +7.22.7e. +7.24.7c. +7.26.7a. +7.r.8m. +8.2z.3v. +a.2F.3i. +c.3f.2I. +g.2E.3q. +0.1a.7V. +0.1S.7A. +0.2G.3h. +1.31.2P. +2.1h.7P. +2.2.8A. +3.6V.2a. +4.11.7Z. +4.k.8r. +5.1t.7L. +5.1V.7y. +5.29.6W. +5.c.8w. +7.1+.7i. +7.22.7f. +7.26.7b. +7.r.8n. +7.s.8m. +8.2A.3v. +8.2g.6z. +a.2Y.2Y. +f.24.7d. +g.2E.3r. +0.1b.7V. +1.2f.6Q. +1.33.2M. +1.r.8o. +2.1+.7j. +2.1d.7T. +2.1t.7M. +2.2/.2U. +2.3.8A. +3.2B.3v. +3.2y.3y. +4.12.7Z. +5.29.6X. +5.k.8s. +6.3i.2G. +7.22.7g. +7.24.7e. +7.26.7c. +7.s.8n. +j.2a.6W. +0.1c.7V. +0.2C.3v. +0.2M.34. +0.39.2K. +1.31.2Q. +1.r.8p. +1.s.8o. +2.2g.6A. +2.2H.3h. +2.4.8A. +5./.7+. +5.1V.7z. +5.29.6Y. +5.2q.65. +6.6R.2f. +6.85.N. +7.1+.7k. +7.22.7h. +7.24.7f. +7.26.7d. +7.u.8m. +8.2z.3y. +a.32.2N. +j.2a.6X. +0.2I.3g. +1.31.2R. +1.c.8x. +1.s.8p. +2.2H.3i. +2.5.8A. +3.6V.2c. +5.29.6Z. +6.85.O. +7.1+.7l. +7.24.7g. +7.26.7e. +7.u.8n. +8.2+.2V. +8.2A.3y. +a.2F.3m. +a.2J.3e. +a.32.2O. +b.3G.2w. +f.1V.7A. +f.2g.6B. +j.2a.6Y. +0.1d.7V. +0.1W.7A. +1.2f.6T. +1.r.8q. +1.u.8o. +2.1h.7Q. +2.6.8A. +3.2B.3y. +4.13.7Z. +6.85.P. +7.1+.7m. +7.22.7i. +7.24.7h. +7.26.7f. +8.2+.2W. +a.2P.32. +f.37.2L. +j.2a.6Z. +0.2C.3y. +0.2D.3v. +1.31.2S. +1.33.2N. +1.s.8q. +1.u.8p. +2.22.7j. +2.2g.6C. +2.7.8A. +4.14.7Z. +5.2q.67. +5.c.8y. +6.3m.2G. +6.85.Q. +7.1+.7n. +7.26.7g. +a.2J.3f. +d.2U.30. +0.1X.7A. +0.2I.3h. +0.2N.34. +1.33.2O. +2.2/.2V. +2.8.8A. +3.6V.2d. +4.15.7Z. +5./.7/. +6.85.R. +7.1+.7o. +7.22.7k. +7.24.7i. +7.26.7h. +a.32.2Q. +b.6+.29. +g.2E.3v. +0.1e.7V. +0.2O.34. +1.1h.7S. +1.33.2P. +1.u.8q. +2.2/.2W. +2.24.7j. +2.2H.3m. +2.9.8A. +4.16.7Z. +4.2K.3e. +4.S.85. +5.1V.7B. +5.29.6/. +5.2g.6E. +5.k.8t. +6.3i.2I. +7.22.7l. +9.c.8z. +a.2F.3q. +a.32.2R. +b.3G.2x. +b.6+.2a. +0.1f.7V. +0.1Y.7t. +0.2L.39. +1./.80. +2.a.8A. +4.17.7Z. +5.1V.7C. +5.29.70. +6.3y.2D. +6.85.T. +7.22.7m. +7.24.7k. +7.26.7i. +a.2F.3r. +a.2P.34. +f.37.2M. +j.2a.6/. +1.2f.6U. +1.33.2Q. +2.26.7j. +2.b.8A. +4.18.7Z. +4.2G.3q. +4.r.8r. +5.1h.7T. +5.1V.7D. +5.29.71. +5.2q.69. +5.k.8u. +6.85.U. +7.1+.7p. +7.22.7n. +7.24.7l. +8.2+.2Y. +a.2J.3g. +a.32.2S. +b.3J.2w. +c.3f.2K. +f.7V.1g. +g.2E.3y. +j.2a.70. +0.19.7Z. +0.2Q.34. +1.33.2R. +3.3K.2w. +5.1V.7E. +5.29.72. +5.2q.6a. +5.k.8v. +6.3r.2G. +6.85.V. +7.1+.7q. +7.22.7o. +7.24.7m. +7.26.7k. +7.r.8s. +9.s.8r. +a.2V.30. +b.6+.2c. +d.71.2a. +0.2R.34. +1.1h.7U. +1.2f.6W. +2.2H.3q. +3.2W.30. +4.1a.7Z. +5.29.73. +5.k.8w. +6.3L.2w. +6.3m.2I. +6.85.W. +7.1+.7r. +7.24.7n. +7.26.7l. +7.s.8s. +b.3G.2y. +0.1h.7V. +0.2M.39. +1.2f.6X. +1.33.2S. +2.1t.7O. +2.2/.2Y. +2.2H.3r. +2.A.8o. +3.3M.2w. +4.1b.7Z. +5.29.74. +6.85.X. +7.24.7o. +7.26.7m. +9.u.8r. +a.2J.3h. +e.2g.6G. +f.37.2N. +j.2a.73. +0.1i.7V. +0.1Z.7t. +0.2w.3N. +0.34.2S. +1.2f.6Y. +2.A.8p. +4.1c.7Z. +4.2K.3g. +5.1V.7F. +5.29.75. +6.85.Y. +7.1+.7s. +7.22.7p. +7.26.7n. +7.u.8s. +a.2J.3i. +b.3J.2x. +b.6+.2d. +c.37.2O. +d.71.2c. +f.2L.3e. +0.1+.7t. +0.1j.7V. +1.2f.6Z. +1.31.2U. +1.k.8x. +2.1d.7Y. +2.1t.7P. +3.3K.2x. +5.29.76. +6.85.Z. +7.22.7q. +7.26.7o. +a.2F.3v. +a.2P.37. +b.3G.2B. +0.1/.7t. +1.1h.7X. +3.2x.3L. +4.1d.7Z. +5./.81. +5.1V.7G. +5.29.77. +7.22.7r. +7.24.7p. +b.3G.2C. +c.3f.2L. +f.2I.3q. +0.20.7t. +0.2G.3v. +0.2N.39. +1./.82. +3.3M.2x. +4.2K.3h. +5.1V.7H. +5.29.78. +5.2g.6H. +5.k.8y. +6.3r.2I. +7.1+.7u. +7.24.7q. +7.r.8t. +a.2Y.30. +d.71.2d. +f.37.2Q. +0.1k.7V. +0.39.2O. +1./.83. +1.2n.6n. +2.2g.6I. +3.2x.3N. +4.26.7p. +5.1V.7I. +5.29.79. +6.3i.2K. +7.22.7s. +7.24.7r. +7.s.8t. +a.2F.3y. +a.2J.3m. +b.3J.2y. +e.21.7t. +f.2M.3e. +f.37.2R. +0.22.7t. +1.2f.6/. +2.1t.7Q. +2.2H.3v. +3.1l.7V. +3.3K.2y. +4.1e.7Z. +5./.84. +5.1V.7J. +5.29.7a. +6.6o.2n. +6.85.+. +7.1+.7v. +7.26.7q. +7.r.8u. +8.2+.2+. +8.2z.3J. +9.k.8z. +a.2P.39. +b.3G.2D. +d.2U.32. +f.2g.6J. +0.1m.7V. +0.23.7t. +0.2L.3g. +1.2f.70. +1.31.2V. +3.2y.3L. +4.1f.7Z. +4.3f.2M. +4.r.8v. +5.29.7b. +5.c.8B. +6.3y.2G. +6.6p.2n. +6.85./. +7.1+.7w. +7.24.7s. +7.26.7r. +7.s.8u. +7.u.8t. +8.2A.3J. +8.2z.3K. +b.3G.2E. +b.3R.2w. +c.37.2S. +e.2g.6K. +0.1n.7V. +0.24.7t. +0.2Q.39. +0.7A.1Y. +1.2f.71. +1.2n.6q. +1.31.2W. +1.6L.2g. +3.3M.2y. +4.s.8v. +5.29.7c. +5.2q.6c. +6.3S.2w. +6.85.10. +7.1+.7x. +7.22.7u. +7.6v.2h. +7.r.8w. +8.2A.3K. +8.2z.3L. +b.3J.2B. +e./.86. +f.7Z.1g. +0.1o.7V. +0.2N.3e. +1.1t.7S. +1.2f.72. +1.33.2U. +2.2/.2+. +2.2H.3y. +2.A.8s. +3.2B.3K. +3.2y.3N. +5.29.7d. +6.3m.2K. +6.6v.2i. +6.85.11. +7.26.7s. +7.2p.6o. +7.s.8w. +7.u.8u. +8.2A.3L. +8.2z.3M. +a.2J.3q. +b.3J.2C. +f.25.7t. +f.39.2R. +0.1p.7V. +0.26.7t. +0.2I.3v. +0.2L.3h. +0.2O.3e. +1.1h.7Y. +1.2f.73. +1.2n.6r. +3.2B.3L. +3.3K.2C. +4.u.8v. +5.29.7e. +6.85.12. +7.22.7v. +7.24.7u. +7.2p.6p. +7.6v.2j. +8.2A.3M. +8.2z.3N. +a.2J.3r. +d.2U.34. +0.1q.7V. +0.27.7t. +0.2M.3g. +0.39.2S. +1.2f.74. +1.r.8x. +3.2B.3M. +4.1h.7Z. +5./.87. +5.1t.7T. +5.29.7f. +6.3i.2L. +6.3L.2C. +6.6s.2n. +7.1+.7y. +7.22.7w. +7.6v.2k. +7.u.8w. +8.2A.3N. +a.2P.3e. +a.32.2V. +b.3R.2x. +c.3f.2N. +0.1r.7V. +1.2f.75. +1.2n.6t. +1.7d.2b. +1.s.8x. +2.2/.2/. +3.2B.3N. +3.2W.32. +3.3M.2C. +4.1i.7Z. +4.3f.2O. +5./.88. +5.29.7g. +6.3S.2x. +6.6v.2l. +7.22.7x. +7.24.7v. +7.26.7u. +7.6x.2h. +b.3J.2D. +f.2g.6M. +0.1j.7Z. +0.1s.7V. +0.2C.3N. +0.2I.3y. +0.6u.2n. +0.7A.1Z. +1.1t.7U. +1.2f.76. +1.31.2Y. +3.3K.2D. +4.2K.3q. +5./.89. +5.29.7h. +6.85.13. +7.24.7w. +7.6x.2i. +7.r.8y. +8.2+.30. +9.1+.7z. +a.2P.3f. +b.3J.2E. +c.28.7t. +f.2Q.3e. +i.c.8C. +j.2a.7g. +0.1+.7A. +0.1t.7V. +0.6v.2n. +1.2f.77. +1.33.2V. +1.u.8x. +5./.8a. +6.3L.2D. +6.3r.2K. +6.85.14. +7.24.7x. +7.26.7v. +7.2p.6s. +7.6x.2j. +7.s.8y. +f.2M.3h. +f.2R.3e. +g.2E.3K. +j.2a.7h. +0.1/.7A. +0.2N.3g. +1.2f.78. +1.33.2W. +1.7g.2b. +3.1u.7V. +3.3M.2D. +4.r.8z. +5./.8b. +5.1h.7+. +5.29.7i. +6.3i.2M. +6.3m.2L. +6.85.15. +7.22.7y. +7.26.7w. +7.6x.2k. +a.2V.34. +b.3G.2F. +b.3R.2y. +c.3f.2Q. +g.2E.3L. +0.1v.7V. +0.20.7A. +0.2O.3g. +0.2S.3e. +0.2w.3Y. +1.2f.79. +2.2/.30. +2.29.7j. +3.2W.34. +4.1k.7Z. +4.3f.2R. +5./.8c. +6.3N.2D. +6.3S.2y. +6.85.16. +7.26.7x. +7.2p.6u. +7.6x.2l. +7.u.8y. +8.2z.3R. +9.s.8z. +a.2J.3v. +d.2U.37. +g.2E.3M. +0.1w.7V. +1.1t.7X. +1.2f.7a. +2.1V.7K. +3.1l.7Z. +4.d.8C. +5./.8d. +5.29.7k. +6.85.17. +7.24.7y. +7.2p.6v. +8.2A.3R. +8.2z.3S. +9.22.7z. +a.2P.3g. +a.32.2Y. +b.3G.2G. +e.21.7A. +g.2E.3N. +0.1x.7V. +0.22.7A. +0.2N.3h. +1.2f.7b. +4.1m.7Z. +4.e.8C. +4.u.8z. +5./.8e. +5.29.7l. +5.2g.6O. +5.k.8B. +6.85.18. +7.1+.7B. +8.2A.3S. +8.6z.2h. +b.3R.2B. +c.3f.2S. +h.2n.6x. +0.1y.7V. +0.23.7A. +0.2L.3q. +0.2O.3h. +0.2Q.3g. +1.2f.7c. +1.2n.6y. +2.2H.3G. +2.A.8w. +3.2B.3S. +4.1n.7Z. +4.f.8C. +5./.8f. +5.1h.7/. +5.29.7m. +5.2q.6f. +6.3i.2N. +6.3m.2M. +6.85.19. +7.1+.7C. +7.26.7y. +8.6z.2i. +9.24.7z. +a.2J.3y. +b.3R.2C. +0.1o.7Z. +0.1z.7V. +0.2K.3v. +0.2R.3g. +0.30.30. +0.7A.24. +1.2f.7d. +1.33.2Y. +2./.8g. +3.2x.3Y. +4.g.8C. +5.29.7n. +5.2q.6g. +6.3i.2O. +6.3r.2L. +6.3S.2C. +6.85.1a. +7.1+.7D. +8.2g.6P. +8.6z.2j. +a.2P.3h. +d.2U.39. +j.2a.7m. +0.1A.7V. +0.2w.40. +0.7A.25. +1.1h.80. +1.2f.7e. +2./.8h. +4.1p.7Z. +4.h.8C. +5.29.7o. +6.85.1b. +7.1+.7E. +8.6z.2k. +9.26.7z. +a.2P.3i. +a.2V.37. +a.2Y.34. +b.3J.2F. +h.2p.6x. +0.1B.7V. +0.2Q.3h. +0.2S.3g. +0.7A.26. +1.2f.7f. +1.31.2+. +3.2W.37. +3.3K.2F. +4.1q.7Z. +4.i.8C. +5.1V.7L. +6.85.1c. +7.22.7B. +8.6z.2l. +b.3R.2D. +j.2a.7o. +0.1C.7V. +0.7A.27. +1.2f.7g. +2.1d.84. +2.1V.7M. +4.1r.7Z. +4.j.8C. +6.3i.2Q. +6.3m.2N. +6.3S.2D. +6.3y.2K. +7.22.7C. +a.2F.3L. +b.3G.2I. +b.3J.2G. +b.3R.2E. +f.2M.3q. +f.2R.3h. +0.1D.7V. +1.1t.7Y. +1.2f.7h. +3.2y.3Y. +3.3K.2G. +3.3M.2F. +4.1s.7Z. +4.k.8C. +5./.8i. +5.29.7p. +6.3i.2R. +6.3m.2O. +6.3r.2M. +6.85.1d. +7.1+.7F. +7.22.7D. +7.24.7B. +8.6z.2n. +g.2E.3S. +0.2S.3h. +2.1d.86. +2.2/.31. +2.2H.3J. +3.2x.40. +4.1t.7Z. +4.l.8C. +5.29.7q. +6.3L.2G. +7.22.7E. +8.2z.3Y. +a.2F.3N. +a.2P.3m. +a.2V.39. +c.28.7A. +d.2U.3e. +f.24.7C. +0.1E.7V. +0.2L.3v. +1.2f.7i. +1.2n.6A. +2.2H.3K. +2.A.8z. +3.1u.7Z. +3.2W.39. +3.3M.2G. +4.m.8C. +5./.8j. +5.29.7r. +5.c.8D. +6.3i.2S. +7.1+.7G. +7.24.7D. +7.26.7B. +8.2+.32. +8.2A.3Y. +0.1F.7V. +0.2G.3N. +0.2N.3q. +2.2f.7j. +2.2H.3L. +3.2B.3Y. +3.n.8C. +4.1v.7Z. +6.3m.2Q. +6.6B.2n. +6.85.1e. +7.1+.7H. +7.24.7E. +7.26.7C. +7.r.8B. +8.6z.2p. +a.2Y.37. +d.2U.3f. +0.1G.7V. +0.2C.3Y. +1./.8k. +1.2f.7k. +2.2H.3M. +4.1w.7Z. +4.2O.3q. +4.o.8C. +4.s.8B. +5.1h.81. +5.29.7s. +6.3m.2R. +6.3r.2N. +6.85.1f. +7.1+.7I. +7.22.7F. +7.26.7D. +0.1H.7V. +0.1x.7Z. +0.29.7t. +0.2L.3y. +1./.8l. +1.1h.82. +1.2f.7l. +1.2n.6C. +1.31.30. +1.33.2+. +2.2/.32. +2.2H.3N. +3.2y.40. +4.p.8C. +5.1t.7+. +6.3r.2O. +6.85.1g. +7.1+.7J. +7.26.7E. +8.2g.6R. +a.2P.3q. +b.3G.2J. +b.3J.2I. +0.1I.7V. +0.1y.7Z. +0.2M.3v. +1.1h.83. +1.2f.7m. +1.2n.6D. +2.1d.89. +3.3K.2I. +4.q.8C. +4.u.8B. +5.2a.7t. +6.3m.2S. +7.22.7G. +7.24.7F. +7.2p.6B. +8.2+.34. +8.2z.40. +a.2P.3r. +a.2V.3e. +0.1J.7V. +0.2D.3Y. +1.2f.7n. +2.1d.8a. +3.2W.3e. +4.1z.7Z. +5.1h.84. +5.29.7u. +6.3L.2I. +6.6E.2n. +7.22.7H. +8.2A.40. +a.2Y.39. +b.3R.2F. +d.2U.3g. +f.2Q.3q. +1.2f.7o. +2.2/.33. +3.2B.40. +3.3M.2I. +4.1A.7Z. +4.7t.2b. +5./.8m. +6.3r.2Q. +6.3S.2F. +6.85.1h. +7.22.7I. +7.24.7G. +7.26.7F. +a.2V.3f. +f.2R.3q. +g.2E.3Y. +j.2a.7u. +0.1B.7Z. +0.2c.7t. +0.2I.3N. +0.2M.3y. +0.r.8C. +1.2n.6F. +2.2/.34. +3.2W.3f. +5./.8n. +5.1t.7/. +5.29.7v. +6.3r.2R. +6.85.1i. +7.22.7J. +7.24.7H. +a.32.30. +b.3G.2K. +b.3R.2G. +e.1h.86. +f.2C.40. +0.1K.7V. +0.2N.3v. +0.2S.3q. +1./.8o. +2.1V.7O. +4.1C.7Z. +4.s.8C. +5.29.7w. +6.3S.2G. +6.85.1j. +7.24.7I. +7.26.7G. +7.2p.6E. +d.2U.3h. +j.2a.7v. +0.1L.7V. +0.2O.3v. +0.t.8C. +1./.8p. +1.1t.80. +1.2f.7p. +2.1d.8e. +2.2H.3R. +4.1D.7Z. +5.29.7x. +6.3r.2S. +6.4c.2w. +7.24.7J. +7.26.7H. +b.3J.2J. +d.2U.3i. +0.1M.7V. +0.2D.40. +0.2d.7t. +0.u.8C. +1.2f.7q. +1.2n.6G. +1.33.30. +2.1V.7P. +2.2H.3S. +3.3K.2J. +5.1h.87. +7.26.7I. +8.2+.37. +a.2P.3v. +a.2V.3g. +a.2Y.3e. +d.7v.2b. +j.2a.7x. +0.2N.3y. +0.30.34. +0.v.8C. +1./.8q. +1.2f.7r. +3.2W.3g. +4.1E.7Z. +4.1N.7V. +5.1h.88. +5.k.8D. +6.85.1k. +7.26.7J. +9.7t.2e. +a.2J.3L. +f.2g.6V. +g.2E.40. +0.1O.7V. +0.2O.3y. +0.2Q.3v. +0.w.8C. +1.31.31. +1.6L.2h. +2.A.8B. +2.c.8E. +3.1l.85. +3.3M.2J. +4.1F.7Z. +5.1h.89. +5.29.7y. +a.2Y.3f. +0.1P.7V. +0.2R.3v. +0.x.8C. +1.2f.7s. +1.6L.2i. +2.2/.37. +2.6J.2k. +4.1G.7Z. +5.1h.8a. +6.4c.2x. +6.85.1m. +a.2J.3N. +a.2P.3y. +a.2V.3h. +b.3G.2L. +b.3J.2K. +b.3R.2I. +d.2U.3m. +j.2a.7y. +0.1Q.7V. +0.2n.6H. +0.y.8C. +1.6L.2j. +2.0.8K. +2.1+.7K. +2.1V.7Q. +2.6J.2l. +3.2W.3h. +3.3K.2K. +4.1H.7Z. +5.1h.8b. +5.29.7z. +6.3S.2I. +6.85.1n. +8.2+.39. +9.7t.2f. +a.2F.3Y. +a.2V.3i. +0.2Q.3y. +0.2S.3v. +0.7A.29. +1.2n.6I. +1.6L.2k. +2.1.8K. +3.2W.3i. +4.1I.7Z. +4.z.8C. +5.1h.8c. +6.3L.2K. +6.85.1o. +d.7v.2e. +0.2G.3Y. +0.2R.3y. +0.6J.2n. +0.7A.2a. +1.2f.7u. +1.31.32. +1.6L.2l. +2.2.8K. +3.3M.2K. +4.1J.7Z. +5.1h.8d. +5.1t.81. +6.85.1p. +9./.8r. +a.2Y.3g. +0.2K.3N. +0.2w.4j. +1.1t.82. +1.1V.7S. +1.2n.6K. +2.2/.39. +2.3.8K. +5./.8s. +5.1h.8e. +6.4c.2y. +6.85.1q. +7.2p.6H. +b.3G.2M. +c.37.30. +d.2U.3q. +1.1t.83. +1.2f.7v. +1.6L.2n. +1.6N.2h. +2.22.7K. +2.2H.3Y. +2.c.8F. +4.A.8C. +5.1h.8f. +6.3y.2S. +6.85.1r. +8.2z.4c. +9.7A.2b. +a.2V.3m. +d.2U.3r. +e.4.8K. +f.1R.7V. +f.2g.6+. +0.1S.7V. +0.7A.2c. +1.2f.7w. +1.6N.2i. +2.1h.8g. +2.2g.6/. +2.5.8K. +3.2W.3m. +4.1K.7Z. +4.B.8C. +5.1t.84. +5.1V.7T. +5.29.7B. +6.85.1s. +7.1+.7L. +7.2p.6J. +8.2+.3e. +8.2A.4c. +a.2F.40. +a.2Y.3h. +b.3J.2L. +b.3R.2J. +1.2f.7x. +1.31.34. +1.6N.2j. +2.1+.7M. +2.1h.8h. +2.24.7K. +2.2g.70. +2.6.8K. +3.2B.4c. +3.3K.2L. +4.1L.7Z. +4.C.8C. +5.29.7C. +6.3S.2J. +6.85.1t. +7.r.8D. +a.2Y.3i. +a.32.32. +1.6L.2p. +1.6N.2k. +3.1u.85. +3.2x.4j. +4.1M.7Z. +4.2G.40. +4.D.8C. +5.29.7D. +5.c.8G. +6.3L.2L. +6.4c.2C. +7.s.8D. +8.2+.3f. +b.3G.2N. +d.71.2g. +e.1t.86. +e.7.8K. +f.39.30. +o.1V.7U. +0.2I.3Y. +0.7A.2d. +1.6N.2l. +2.2/.3e. +2.26.7K. +2.8.8K. +3.3M.2L. +4.1N.7Z. +4.E.8C. +5.29.7E. +5.c.8H. +6.6M.2n. +6.85.1v. +a.2V.3q. +b.3G.2O. +f.1V.7V. +0.1O.7Z. +0.1W.7V. +0.2L.3N. +1.2f.7y. +1.33.32. +2.2H.40. +2.k.8E. +3.2W.3q. +4.F.8C. +5./.8t. +5.1h.8i. +6.85.1w. +7.22.7L. +7.u.8D. +9.7A.2e. +a.2V.3r. +b.3G.2P. +b.3J.2M. +b.3R.2K. +e.9.8K. +0.1P.7Z. +2.2/.3f. +2.22.7M. +2.2n.6N. +3.2W.3r. +3.3K.2M. +5.1t.87. +5.6P.2h. +5.c.8I. +6.3S.2K. +6.4c.2D. +6.85.1x. +a.2Y.3m. +a.32.34. +d.2U.3v. +e.a.8K. +0.1X.7V. +0.2w.4q. +1.1V.7X. +1.2f.7z. +2.1d.8o. +2.2g.75. +2.b.8K. +3.2y.4j. +5./.8u. +5.1h.8j. +5.1t.88. +5.29.7F. +5.6P.2i. +5.c.8J. +6.3L.2M. +6.85.1y. +7.24.7L. +7.2p.6M. +8.2+.3g. +b.3G.2Q. +f.1Q.7Z. +g.2E.4c. +1.31.37. +1.33.33. +2.1d.8p. +2.24.7M. +3.3M.2M. +4.7A.2f. +4.G.8C. +5./.8v. +5.1t.89. +5.6P.2j. +5.c.8K. +6.85.1z. +8.2z.4j. +b.3G.2R. +f.30.3e. +0.2M.3N. +1.1h.8k. +1.33.34. +4.H.8C. +5./.8w. +5.1t.8a. +5.29.7G. +5.6P.2k. +6.85.1A. +7.26.7L. +8.2A.4j. +b.3J.2N. +d.2U.3y. +f.2I.40. +0.34.34. +1.1h.8l. +2.2/.3g. +2.26.7M. +3.2B.4j. +3.3K.2N. +3.I.8C. +5.1t.8b. +5.29.7H. +5.6P.2l. +6.6O.2n. +6.85.1B. +8.2+.3h. +a.2J.3Y. +a.2Y.3q. +b.3G.2S. +b.3J.2O. +c.3f.30. +0.2C.4j. +2.k.8F. +3.2x.4q. +3.3K.2O. +3.J.8C. +5.1t.8c. +5.29.7I. +6.3L.2N. +6.85.1C. +8.2+.3i. +a.2V.3v. +a.2Y.3r. +b.3R.2L. +e.3J.2P. +f.1R.7Z. +0.6P.2n. +1./.8x. +1.2f.7B. +1.31.39. +3.2W.3v. +3.3K.2P. +3.3M.2N. +4.1S.7Z. +4.K.8C. +5.1t.8d. +5.29.7J. +6.3L.2O. +6.3S.2L. +6.85.1D. +a.32.37. +0.2N.3N. +0.L.8C. +1.2f.7C. +2.1+.7O. +2.2/.3h. +3.3M.2O. +5.1h.8m. +5.1t.8e. +7.2p.6O. +a.2P.3L. +b.3J.2Q. +0.2D.4j. +0.2K.3Y. +0.2O.3N. +0.30.3g. +1.1V.7Y. +1.2f.7D. +2.2/.3i. +2.4G.2v. +2.r.8E. +3.3K.2Q. +3.3M.2P. +3.M.8C. +5./.8y. +5.1h.8n. +5.1t.8f. +5.k.8G. +6.85.1E. +a.2V.3y. +b.3J.2R. +1.1h.8o. +1.2f.7E. +1.33.37. +2.1+.7P. +2.1t.8g. +2.s.8E. +3.2W.3y. +3.2y.4q. +3.3K.2R. +5.k.8H. +6.3L.2Q. +6.4c.2F. +6.6R.2h. +6.85.1F. +7.2p.6P. +8.2+.3m. +a.2J.40. +a.2P.3N. +b.3R.2M. +f.1V.7Z. +g.2E.4j. +1.1h.8p. +2.1d.8s. +2.1t.8h. +3.3M.2Q. +6.3L.2R. +6.3S.2M. +6.6R.2i. +6.85.1G. +8.2z.4q. +9./.8z. +a.32.39. +b.3J.2S. +c.37.34. +f.1W.7Z. +0.2Q.3N. +0.30.3h. +1.31.3e. +2.22.7O. +2.u.8E. +3.3K.2S. +3.3M.2R. +4.N.8C. +5.k.8I. +6.4c.2G. +6.6R.2j. +6.85.1H. +8.2A.4q. +a.2Y.3v. +0.2R.3N. +1.1h.8q. +1.2f.7F. +2.2/.3m. +3.2B.4q. +4.1X.7Z. +4.O.8C. +5.k.8J. +6.3i.30. +6.3L.2S. +6.6R.2k. +6.85.1I. +0.2C.4q. +0.2K.40. +1.31.3f. +1.33.39. +2.1+.7Q. +2.22.7P. +2.24.7O. +2.2H.4c. +3.3M.2S. +4.P.8C. +5.1t.8i. +5.1V.7+. +5.k.8K. +6.6R.2l. +6.85.1J. +8.2+.3q. +b.3R.2N. +d.2U.3G. +0.2L.3Y. +0.2S.3N. +0.39.34. +1.2f.7G. +1.2n.6Q. +2.r.8F. +4.Q.8C. +6.3S.2N. +8.2+.3r. +a.2Y.3y. +b.3R.2O. +0.7V.1Y. +1.2f.7H. +1.c.8L. +2.24.7P. +2.26.7O. +2.s.8F. +3.6R.2n. +4.R.8C. +5.1t.8j. +6.3S.2O. +a.32.3e. +b.3R.2P. +1.1+.7S. +1.2f.7I. +1.c.8M. +2.2/.3q. +3.S.8C. +6.3m.30. +6.4q.2D. +6.85.1K. +a.2P.3S. +f.37.37. +1.1t.8k. +1.2f.7J. +1.2n.6T. +1.31.3g. +1.c.8N. +2.2/.3r. +2.22.7Q. +2.26.7P. +2.29.7K. +2.u.8F. +5.1h.8r. +5.1V.7/. +6.4c.2I. +6.85.1L. +7.r.8G. +a.2F.4j. +a.32.3f. +b.3R.2Q. +c.T.8C. +g.2E.4q. +0.2M.3Y. +1.1t.8l. +1.33.3e. +4.r.8H. +4.s.8G. +4.U.8C. +5.1h.8s. +6.3S.2Q. +6.85.1M. +7.1+.7T. +7.2p.6R. +b.3G.2V. +b.3R.2R. +0.2G.4j. +0.34.3e. +1.1V.80. +2.24.7Q. +4.s.8H. +4.V.8C. +6.3S.2R. +6.85.1N. +b.3G.2W. +d.2U.3J. +f.2L.40. +0.1Z.7V. +0.30.3q. +1.1+.7U. +1.22.7S. +1.31.3h. +1.33.3f. +2.1d.8w. +4.u.8G. +4.W.8C. +6.85.1O. +7.r.8I. +8.2+.3v. +b.3R.2S. +c.37.39. +d.2U.3K. +0.1+.7V. +1.31.3i. +2.26.7Q. +2.2H.4j. +4.r.8J. +4.s.8I. +4.u.8H. +4.X.8C. +5.1t.8m. +5.2q.6o. +6.3r.30. +6.3S.2S. +6.85.1P. +a.32.3g. +c.3f.34. +d.2U.3L. +0.1/.7V. +0.2N.3Y. +1.24.7S. +1.2n.6U. +1.c.8O. +4.r.8K. +4.s.8J. +5./.8B. +5.1t.8n. +5.29.7L. +5.2q.6p. +6.85.1Q. +7.22.7T. +c.Y.8C. +d.2U.3M. +0.20.7V. +0.Z.8C. +1.1t.8o. +1.c.8P. +2.2/.3v. +2.29.7M. +3.6V.2n. +4.2O.3Y. +4.s.8K. +4.u.8I. +6.4c.2J. +8.2+.3y. +d.2U.3N. +f.2M.40. +0.39.39. +1.1+.7X. +1.1t.8p. +1.22.7U. +1.26.7S. +1.2n.6W. +1.33.3g. +4.u.8J. +5.1h.8t. +7.24.7T. +a.2P.3Y. +a.32.3h. +b.3G.2Y. +b.3J.2V. +j.21.7V. +0.22.7V. +0.2I.4j. +0.34.3g. +1.2n.6X. +1.31.3m. +1.c.8Q. +3.3K.2V. +4.u.8K. +a.2F.4q. +a.32.3i. +c.37.3e. +e.3J.2W. +0.23.7V. +0.2Q.3Y. +0.7Z.1Y. +1.1t.8q. +1.24.7U. +1.2n.6Y. +1.k.8L. +2.2/.3y. +3.3K.2W. +5.1h.8u. +5.1V.81. +5.2q.6s. +6.85.1R. +7.26.7T. +7.2p.6V. +a.2V.3L. +0.+.8C. +0.24.7V. +0.2G.4q. +0.2N.40. +0.2R.3Y. +0.30.3v. +1.1V.82. +1.2n.6Z. +1.33.3h. +1.k.8M. +2.1d.8z. +3.2W.3L. +3.3M.2V. +5.1h.8v. +5.2q.6t. +5.c.8R. +6.4c.2K. +6.85.1S. +c.37.3f. +0.25.7V. +0.34.3h. +1.1V.83. +1.22.7X. +1.26.7U. +1.33.3i. +1.k.8N. +2.2f.7K. +3.3M.2W. +4./.8C. +4.2O.40. +5.1h.8w. +5.2q.6u. +a.2V.3N. +0.2S.3Y. +1.31.3q. +2.2H.4q. +3.2W.3N. +4.10.8C. +4.26.7V. +5.1V.84. +5.2q.6v. +6.3i.34. +9.c.8S. +a.2P.40. +a.32.3m. +d.2U.3R. +f.2g.7t. +f.39.3e. +0.27.7V. +1.24.7X. +1.31.3r. +4.11.8C. +6.3y.30. +6.85.1V. +b.6+.2n. +d.2U.3S. +e.3J.2Y. +1.1+.7Y. +1.1h.8x. +1.1V.86. +1.2n.6/. +2.A.8I. +3.3K.2Y. +4.12.8C. +4.1t.8r. +6.85.1W. +a.2J.4j. +c.37.3g. +c.3f.39. +f.1Z.7Z. +f.2Q.40. +0.1+.7Z. +1.26.7X. +1.2n.70. +1.33.3m. +5.1t.8s. +5.c.8T. +a.2Y.3L. +c.28.7V. +f.2R.40. +0.1/.7Z. +0.2I.4q. +1.2f.7L. +1.k.8O. +3.3M.2Y. +4.2q.6x. +5.1h.8y. +6.3m.34. +6.4c.2L. +6.85.1X. +8.2+.3G. +a.32.3q. +b.6+.2p. +d.71.2n. +0.20.7Z. +0.2S.40. +0.3e.3e. +1.2n.72. +1.k.8P. +2.29.7O. +2.2f.7M. +4.13.8C. +5.1V.87. +5.2q.6y. +a.2Y.3N. +a.32.3r. +b.3R.2V. +c.37.3h. +0.2K.4j. +1.22.7Y. +1.2n.73. +1.r.8L. +4.14.8C. +5.1h.8z. +5.1V.88. +6.3S.2V. +b.3R.2W. +f.37.3i. +f.39.3g. +j.21.7Z. +1.2n.74. +1.31.3v. +1.33.3q. +1.k.8Q. +1.r.8M. +1.s.8L. +2.2/.3G. +2.29.7P. +3.2W.3S. +4.15.8C. +5.1V.89. +7.1+.7+. +c.3f.3e. +d.71.2p. +f.22.7Z. +0.16.8C. +0.23.7Z. +1.24.7Y. +1.2n.75. +1.33.3r. +1.r.8N. +1.s.8M. +5.1V.8a. +5.c.8U. +6.4c.2M. +f.34.3q. +0.24.7Z. +1.2n.76. +1.s.8N. +1.u.8L. +4.17.8C. +5.1t.8t. +5.1V.8b. +5.k.8R. +6.3r.34. +c.3f.3f. +d.2U.3Y. +f.39.3h. +0.25.7Z. +1.26.7Y. +1.31.3y. +1.7d.2h. +1.u.8M. +2.1d.8B. +4.18.8C. +5.1V.8c. +6.3i.39. +8.2+.3J. +a.2J.4q. +c.37.3m. +0.19.8C. +0.26.7Z. +0.3e.3g. +1.2n.78. +1.7d.2i. +1.u.8N. +2.29.7Q. +2.2g.7z. +4.k.8S. +5.1t.8u. +5.1V.8d. +7.1+.7/. +7.22.7+. +8.2+.3K. +a.32.3v. +b.3G.30. +b.3R.2Y. +0.27.7Z. +0.2L.4j. +1.2n.79. +1.7d.2j. +4.1a.8C. +5./.8D. +5.1t.8v. +5.1V.8e. +5.2q.6A. +6.3S.2Y. +6.4c.2N. +8.2+.3L. +1.1+.80. +1.2n.7a. +1.7d.2k. +1.7g.2h. +1.c.8V. +1.r.8O. +2.2/.3J. +4.1b.8C. +5.1t.8w. +5.1V.8f. +5.2q.6B. +6.4c.2O. +7.24.7+. +8.2+.3M. +c.3f.3g. +0.2K.4q. +1.29.7S. +1.2n.7b. +1.33.3v. +1.7d.2l. +1.7g.2i. +1.r.8P. +1.s.8O. +2.1V.8g. +2.2/.3K. +4.1c.8C. +5.k.8T. +6.3m.39. +6.4c.2P. +8.2+.3N. +a.2V.3Y. +a.32.3y. +c.28.7Z. +d.2U.40. +f.37.3q. +f.3h.3e. +0.34.3v. +1.2n.7c. +1.7g.2j. +1.s.8P. +2.1V.8h. +2.2/.3L. +3.2W.3Y. +6.3i.3e. +7.22.7/. +7.26.7+. +9.c.8W. +c.37.3r. +0.1d.8C. +0.2M.4j. +1.1t.8x. +1.2n.7d. +1.7g.2k. +1.r.8Q. +1.u.8O. +2.2/.3M. +2.2f.7O. +5.29.7T. +5.2q.6D. +6.4c.2Q. +c.3f.3h. +0.3g.3g. +1.22.80. +1.2n.7e. +1.33.3y. +1.7g.2l. +1.s.8Q. +1.u.8P. +2.2/.3N. +5.2q.6E. +6.4c.2R. +7.24.7/. +b.3J.30. +c.3f.3i. +1.2n.7f. +2.2f.7P. +3.3K.30. +5.1h.8B. +5.1t.8y. +5.1V.8i. +6.3y.34. +6.85.1Y. +7.r.8R. +f.39.3q. +0.29.7V. +1.24.80. +1.2n.7g. +1.u.8Q. +4.1e.8C. +5.2q.6F. +5.c.8X. +5.k.8U. +6.3L.30. +6.3m.3e. +6.3r.39. +6.4c.2S. +7.26.7/. +7.s.8R. +a.2V.40. +0.2L.4q. +0.2N.4j. +0.3h.3g. +1.2n.7h. +1.31.3G. +3.2W.40. +3.3M.30. +4.1f.8C. +4.r.8S. +5.1V.8j. +5.c.8Y. +7.1+.81. +8.2+.3R. +9.1t.8z. +a.2Y.3Y. +f.2a.7V. +0.30.3N. +1.1+.82. +1.26.80. +4.2O.4j. +4.s.8S. +6.3i.3g. +7.u.8R. +8.2+.3S. +c.3f.3m. +f.37.3v. +h.1g.8C. +1.1+.83. +1.1V.8k. +1.29.7X. +1.2n.7i. +2.2f.7Q. +5.2q.6G. +a.2P.4j. +0.2c.7V. +0.3h.3h. +0.3q.3e. +1.1V.8l. +2.2/.3R. +2.2n.7j. +4.u.8S. +6.85.1Z. +7.1+.84. +7.r.8T. +0.2M.4q. +0.2Q.4j. +0.2w.53. +1.2n.7k. +1.k.8V. +2./.8E. +2.2/.3S. +4.1h.8C. +6.3i.3h. +6.3r.3e. +6.85.1+. +7.22.81. +7.s.8T. +b.3G.32. +c.37.3y. +0.2R.4j. +0.39.3v. +1.1+.86. +1.22.82. +1.2f.7S. +1.2n.7l. +4.1i.8C. +6.3i.3i. +6.3m.3g. +6.85.1/. +a.2Y.40. +c.3f.3q. +0.1j.8C. +0.2w.55. +0.7V.2d. +1.22.83. +1.31.3J. +2.c.8Z. +4.k.8W. +5.1V.8m. +5.2q.6H. +6.85.20. +7.u.8T. +c.3f.3r. +f.24.81. +0.2S.4j. +1.24.82. +1.2f.7T. +1.2n.7n. +1.31.3K. +1.33.3G. +5.1V.8n. +5.2q.6I. +6.85.21. +7.22.84. +b.3R.30. +0.2N.4q. +0.39.3y. +1.1V.8o. +1.24.83. +1.2n.7o. +1.31.3L. +3.2x.53. +5.2q.6J. +5.c.8+. +6.3m.3h. +6.3S.30. +6.85.22. +7.1+.87. +7.26.81. +7.r.8U. +b.3G.34. +d.2U.4c. +0.2O.4q. +0.3q.3g. +1.1V.8p. +1.22.86. +1.26.82. +1.29.7Y. +1.2f.7U. +1.31.3M. +4.1k.8C. +4.s.8U. +5.2q.6K. +6.3i.3m. +6.58.2w. +6.85.23. +7.1+.88. +7.24.84. +8.2+.3Y. +0.3e.3v. +1.26.83. +1.31.3N. +1.6L.2q. +2./.8F. +3.1l.8C. +3.2x.55. +5.k.8X. +6.3r.3g. +6.85.24. +7.1+.89. +9.7t.2h. +a.2P.4q. +b.3J.32. +f.29.7Z. +1.1V.8q. +1.24.86. +1.2n.7p. +3.3K.32. +4.1m.8C. +4.7t.2i. +5.2a.7Z. +5.k.8Y. +6.85.25. +7.1+.8a. +7.26.84. +7.u.8U. +0.2Q.4q. +1.2n.7q. +2.2/.3Y. +3.2y.53. +4.1n.8C. +4.7t.2j. +5.1t.8B. +5.c.8/. +6.85.26. +7.1+.8b. +7.22.87. +a.32.3L. +c.3f.3v. +f.3h.3q. +0.1o.8C. +0.2R.4q. +1.26.86. +1.2f.7X. +1.2n.7r. +1.33.3J. +1.r.8V. +3.3M.32. +5./.8G. +6.3i.3q. +6.3m.3m. +6.3r.3h. +6.3y.3e. +6.4c.2V. +6.85.27. +7.1+.8c. +7.22.88. +8.2z.53. +8.58.2x. +9.7t.2k. +1.33.3K. +1.s.8V. +3.2W.4c. +3.2y.55. +4.1p.8C. +5./.8H. +5.29.7+. +5.2q.6M. +6.3i.3r. +7.1+.8d. +7.22.89. +7.24.87. +8.2+.40. +8.2A.53. +9.7t.2l. +a.32.3N. +b.3G.37. +b.3J.34. +d.7v.2h. +f.2c.7Z. +0.2S.4q. +1.33.3L. +1.c.90. +3.2B.53. +3.3K.34. +4.1q.8C. +6.85.28. +7.1+.8e. +7.22.8a. +7.24.88. +8.2z.55. +9.r.8W. +c.3f.3y. +d.7v.2i. +0.2C.53. +0.2n.7t. +0.30.3Y. +0.3g.3v. +1.31.3R. +1.33.3M. +1.u.8V. +4.1r.8C. +5./.8I. +6.3L.34. +7.1+.8f. +7.22.8b. +7.24.89. +7.26.87. +8.2A.55. +9.1V.8r. +9.s.8W. +d.2U.4j. +d.7v.2j. +0.2w.5e. +1.31.3S. +1.33.3N. +2.1+.8g. +2.2/.40. +2.k.8Z. +3.2B.55. +3.3M.34. +4.1s.8C. +4.2d.7Z. +5./.8J. +5.1h.8D. +5.1V.8s. +6.3m.3q. +7.22.8c. +7.24.8a. +7.26.88. +8.58.2y. +d.7v.2k. +0.34.3N. +1.2n.7u. +2.1+.8h. +4.1t.8C. +4.u.8W. +5./.8K. +5.29.7/. +6.3r.3m. +7.22.8d. +7.24.8b. +7.26.89. +8.58.2z. +b.3G.39. +d.7v.2l. +f.2C.55. +0.2D.53. +0.3h.3v. +3.1u.8C. +5.k.8+. +6.3y.3g. +6.4c.2Y. +7.22.8e. +7.24.8c. +7.26.8a. +7.2p.7t. +7.r.8X. +8.58.2A. +j.2a.7/. +1.29.80. +1.2f.7Y. +1.c.91. +4.1v.8C. +4.s.8X. +5.2q.6O. +6.3i.3v. +7.22.8f. +7.24.8d. +7.26.8b. +7.r.8Y. +8.58.2B. +b.3J.37. +b.3R.32. +g.2E.53. +0.30.40. +0.3q.3q. +2.22.8g. +3.2x.5e. +3.3K.37. +4.1w.8C. +4.55.2D. +4.s.8Y. +6.58.2C. +7.1+.8i. +7.24.8e. +7.26.8c. +a.2V.4j. +a.32.3S. +0.1x.8C. +1.2n.7x. +1.c.92. +2.22.8h. +3.2W.4j. +4.u.8X. +5.2q.6P. +6.3L.37. +6.3r.3q. +6.3y.3h. +7.24.8f. +7.26.8d. +9.7A.2h. +g.2E.55. +0.1y.8C. +1.33.3R. +1.c.93. +2.24.8g. +3.3M.37. +5.1V.8t. +5.k.8/. +6.3i.3y. +6.3r.3r. +7.1+.8j. +7.26.8e. +7.u.8Y. +9.7A.2i. +b.3G.3e. +1.33.3S. +1.c.94. +2.24.8h. +4.1z.8C. +6.3m.3v. +7.26.8f. +8.58.2D. +9.7A.2j. +b.3J.39. +b.3R.34. +c.37.3N. +d.2U.4q. +1.1+.8k. +1.2f.7+. +1.2n.7y. +1.31.3Y. +2.26.8g. +3.2y.5e. +3.3K.39. +4.1A.8C. +5.1V.8u. +6.3S.34. +7.22.8i. +8.58.2E. +9.7A.2k. +b.3G.3f. +1.1+.8l. +1.5s.2u. +1.k.90. +2.26.8h. +2.A.8W. +2.r.8Z. +4.1B.8C. +5.1V.8v. +6.3L.39. +8.2z.5e. +9.7A.2l. +0.1C.8C. +1.2n.7z. +2.5s.2v. +2.s.8Z. +3.3M.39. +5.1V.8w. +5.29.81. +6.3m.3y. +7.22.8j. +7.24.8i. +8.2A.5e. +a.2Y.4j. +0.3q.3v. +0.7A.2n. +1.29.82. +3.2B.5e. +4.1D.8C. +7.r.8+. +8.2+.4c. +a.2F.53. +f.39.3N. +j.2a.81. +0.2C.5e. +1.22.8k. +1.29.83. +1.2f.7/. +2.1h.8E. +2.u.8Z. +4.s.8+. +5.c.95. +6.3r.3v. +7.1+.8m. +7.24.8j. +7.26.8i. +a.2V.4q. +a.32.3Y. +b.3G.3g. +b.3J.3e. +0.2G.53. +1.1V.8x. +1.22.8l. +1.31.40. +3.2W.4q. +3.3K.3e. +4.1E.8C. +5.29.84. +5.2q.6Q. +7.1+.8n. +a.2F.55. +b.3R.37. +1./.8L. +1.1+.8o. +1.24.8k. +1.2f.80. +1.k.91. +2.2/.4c. +2.A.8Y. +4.1F.8C. +4.u.8+. +5.1t.8D. +6.3L.3e. +6.3S.37. +6.3y.3q. +6.6R.2q. +6.85.29. +7.26.8j. +7.2p.7A. +b.3J.3f. +j.2a.84. +0.2D.5e. +0.2G.55. +1./.8M. +1.1+.8p. +1.24.8l. +1.29.86. +1.2n.7B. +1.33.3Y. +2.2H.53. +3.3K.3f. +3.3M.3e. +4.1G.8C. +4.r.8/. +5.1V.8y. +6.3r.3y. +6.85.2a. +b.3G.3h. +0.34.3Y. +1./.8N. +1.26.8k. +1.2n.7C. +1.c.96. +1.k.92. +2.1d.8I. +4.1H.8C. +4.s.8/. +5.2q.6T. +6.3L.3f. +6.3N.3e. +7.22.8m. +8.58.2F. +b.3G.3i. +g.2E.5e. +1.1+.8q. +1.26.8l. +1.2n.7D. +1.k.93. +2.2H.55. +3.3M.3f. +4.1I.8C. +7.22.8n. +9.1V.8z. +a.32.40. +b.3R.39. +1.22.8o. +1.2n.7E. +1.k.94. +1.r.90. +2.1h.8F. +4.1J.8C. +4.3f.3N. +4.u.8/. +5.29.87. +6.3S.39. +6.3v.3v. +6.4c.30. +6.58.2G. +6.85.2c. +7.24.8m. +a.2Y.4q. +b.3J.3g. +0.2I.53. +1.22.8p. +1.s.90. +3.3K.3g. +5.29.88. +7.24.8n. +8.2+.4j. +j.2a.87. +k.98.0. +1.24.8o. +1.33.40. +2.2H.58. +5.29.89. +6.3L.3g. +7.26.8m. +b.3G.3m. +0.2I.55. +0.34.40. +1./.8O. +1.22.8q. +1.24.8p. +1.2f.81. +1.2n.7F. +1.4G.2T. +1.u.90. +3.3M.3g. +4.1K.8C. +5.1h.8G. +5.29.8a. +5.2q.6U. +6.3y.3v. +6.85.2d. +7.26.8n. +b.3J.3h. +f.2w.5s. +0.1L.8C. +1./.8P. +1.26.8o. +1.2f.82. +2.2/.4j. +3.3K.3h. +5.1h.8H. +5.29.8b. +6.3N.3g. +6.6V.2q. +9.1+.8r. +b.3J.3i. +b.3R.3e. +c.37.3Y. +j.2a.8a. +1.24.8q. +1.26.8p. +1.2f.83. +1.2n.7G. +1.r.91. +3.3K.3i. +4.1M.8C. +5.29.8c. +5.2q.6W. +5.k.95. +6.3S.3e. +7.1+.8s. +e.3L.3h. +j.2a.8b. +1./.8Q. +1.2f.84. +1.2n.7H. +1.s.91. +3.3M.3h. +4.1N.8C. +5.1h.8I. +5.29.8d. +5.2q.6X. +6.3L.3i. +6.3y.3y. +6.58.2I. +a.2F.5e. +b.3G.3q. +b.3R.3f. +0.1O.8C. +1.26.8q. +1.2n.7I. +1.c.97. +1.r.92. +3.2x.5s. +3.3M.3i. +5.1h.8J. +5.29.8e. +5.2q.6Y. +6.3N.3h. +6.3S.3f. +a.2J.53. +b.3G.3r. +0.1P.8C. +0.2G.5e. +0.30.4j. +1.2f.86. +1.2n.7J. +1.r.93. +1.s.92. +1.u.91. +2.1t.8E. +5./.8R. +5.1h.8K. +5.29.8f. +5.2q.6Z. +6.3i.3N. +9.22.8r. +b.3J.3m. +f.39.3Y. +j.2a.8e. +0.1Q.8C. +1.31.4c. +1.k.96. +1.r.94. +1.s.93. +2.29.8g. +3.3K.3m. +7.22.8s. +8.2+.4q. +a.2J.55. +c.37.40. +j.2a.8f. +1.s.94. +1.u.92. +2.29.8h. +2.2H.5e. +4./.8S. +6.3L.3m. +9.24.8r. +b.3R.3g. +0.2K.53. +1.2f.87. +1.u.93. +3.2y.5s. +3.3M.3m. +5.1V.8B. +5.6+.2q. +6.3S.3g. +7.1+.8t. +7.24.8s. +0.2w.5u. +1.2f.88. +1.u.94. +2.2/.4q. +5.2q.6/. +6.3m.3N. +8.2z.5s. +8.58.2J. +9.26.8r. +b.3J.3q. +0.2K.55. +0.39.40. +0.3e.3Y. +1.2f.89. +3.3K.3q. +5./.8T. +5.29.8i. +5.2q.70. +6.4c.32. +7.1+.8u. +7.26.8s. +8.2A.5s. +b.3G.3v. +b.3J.3r. +b.3R.3h. +f.1R.8C. +k.98.c. +0.1S.8C. +0.2I.5e. +1.2f.8a. +1.4G.2X. +1.c.99. +2.1t.8F. +3.2B.5s. +3.3K.3r. +4.r.95. +5.2g.7V. +5.2q.71. +6.3L.3q. +6.3S.3h. +7.1+.8v. +b.3R.3i. +1.2f.8b. +3.3M.3q. +5.29.8j. +5.2q.72. +6.3L.3r. +6.3S.3i. +7.1+.8w. +7.22.8t. +7.s.95. +c.3f.3Y. +f.2C.5s. +0.30.4q. +1.2f.8c. +1.33.4c. +3.2x.5u. +3.3M.3r. +5.2q.73. +6.3N.3q. +6.58.2K. +b.3G.3y. +0.1V.8C. +0.2L.53. +1.29.8k. +1.2f.8d. +1.31.4j. +1.c.9a. +4.u.95. +5.1t.8G. +5.2q.74. +6.3r.3N. +6.4c.34. +7.22.8u. +7.24.8t. +0.3e.40. +1.1+.8x. +1.29.8l. +1.2f.8e. +1.k.97. +1.r.96. +2.c.9b. +5./.8U. +5.1t.8H. +5.2q.75. +6.5s.2D. +7.22.8v. +b.3R.3m. +f.1W.8C. +0.2L.55. +0.3g.3Y. +1.2f.8f. +1.s.96. +2.2n.7K. +5.2q.76. +6.3S.3m. +7.22.8w. +7.24.8u. +7.26.8t. +b.3J.3v. +g.2E.5s. +0.1X.8C. +2.2f.8g. +2.A.94. +3.2y.5u. +3.3K.3v. +5.1t.8I. +5.2q.77. +7.1+.8y. +7.24.8v. +a.2J.5e. +c.3f.40. +0.2M.53. +1.1h.8L. +1.u.96. +2.2f.8h. +5.1t.8J. +5.29.8m. +5.2q.78. +6.3L.3v. +7.24.8w. +7.26.8u. +8.2z.5u. +9.c.9c. +a.32.4j. +0.3h.3Y. +1.1h.8M. +1.22.8x. +3.3M.3v. +5.1t.8K. +5.29.8n. +5.2q.79. +6.58.2L. +7.26.8v. +8.2A.5u. +9.1+.8z. +b.3J.3y. +b.3R.3q. +0.2M.55. +1./.8V. +1.1h.8N. +1.29.8o. +3.2B.5u. +3.3K.3y. +5.2q.7a. +5.c.9d. +6.3i.3Y. +6.3N.3v. +6.3S.3q. +6.4c.37. +7.26.8w. +b.3R.3r. +0.2C.5u. +0.3g.40. +1.24.8x. +1.29.8p. +1.2f.8i. +1.2n.7L. +1.33.4j. +4.2K.5e. +5.2q.7b. +6.3L.3y. +6.3S.3r. +7.22.8y. +0.2N.53. +0.34.4j. +1.31.4q. +1.k.99. +2.2n.7M. +3.3M.3y. +5.2g.7Z. +5.2q.7c. +9./.8W. +0.2O.53. +1.26.8x. +1.29.8q. +1.2f.8j. +5.2q.7d. +6.3y.3N. +6.58.2M. +7.24.8y. +9.22.8z. +b.3G.3G. +0.2N.55. +0.3h.40. +0.5u.2D. +1.r.97. +2.c.9e. +5.2q.7e. +6.3m.3Y. +6.4c.39. +a.2F.5s. +a.2P.53. +0.2O.55. +1.1h.8O. +1.2f.8k. +1.k.9a. +1.s.97. +6.3i.40. +7.26.8y. +9.24.8z. +g.2E.5u. +0.2Q.53. +1.1h.8P. +1.2f.8l. +2.k.9b. +4.2G.5s. +5./.8X. +5.2q.7g. +a.2P.55. +a.32.4q. +b.3R.3v. +0.2L.5e. +1.u.97. +5./.8Y. +5.1V.8D. +5.2q.7h. +6.3S.3v. +6.58.2N. +9.26.8z. +f.2R.53. +0.2Q.55. +0.3q.3Y. +1.1h.8Q. +2.2H.5s. +5.29.8r. +6.58.2O. +f.37.4j. +0.2S.53. +1.2f.8m. +1.33.4q. +5.29.8s. +5.2q.7i. +6.3m.40. +6.3r.3Y. +6.4c.3e. +8.58.2P. +9.k.9c. +b.3G.3J. +b.3R.3y. +f.2R.55. +0.34.4q. +1.2f.8n. +5.1h.8R. +6.3S.3y. +b.3G.3K. +k.98.r. +0.1Y.8C. +0.2M.5e. +0.2S.55. +1.2f.8o. +1.r.99. +4.c.9f. +5.k.9d. +6.4c.3f. +6.58.2Q. +7.1+.8B. +b.3G.3L. +k.98.s. +0.39.4j. +1.1t.8L. +1.2f.8p. +1.s.99. +5.1h.8S. +6.58.2R. +b.3G.3M. +f.2I.5s. +0.3q.40. +1.1t.8M. +2./.8Z. +2.2n.7O. +2.c.9g. +5.2q.7m. +a.2F.5u. +b.3G.3N. +b.5F.2w. +k.98.u. +1.1t.8N. +1.2f.8q. +1.r.9a. +1.u.99. +3.5G.2w. +5.2q.7n. +6.3r.40. +6.58.2S. +0.2G.5u. +0.2N.5e. +1.s.9a. +2.2n.7P. +2.k.9e. +2.r.9b. +5./.8+. +5.1h.8T. +5.29.8t. +5.2q.7o. +6.3v.3Y. +6.4c.3g. +7.22.8B. +b.3J.3J. +2.s.9b. +4.2O.5e. +b.3J.3K. +f.1Z.8C. +f.37.4q. +0.1+.8C. +0.3e.4j. +1.u.9a. +2.2H.5u. +3.3K.3K. +5.29.8u. +7.24.8B. +a.2P.5e. +b.3J.3L. +b.5F.2x. +0.1/.8C. +2.u.9b. +3.3K.3L. +3.5G.2x. +5.29.8v. +5.2q.7p. +6.3y.3Y. +6.4c.3h. +9.r.9c. +a.2J.5s. +b.3J.3M. +d.2U.53. +0.20.8C. +0.2Q.5e. +1.1t.8O. +1.2f.8r. +2.1d.8W. +2.1V.8E. +2.2n.7Q. +3.3M.3K. +4.s.9c. +5./.8/. +5.29.8w. +5.2q.7q. +6.3L.3L. +6.4c.3i. +7.26.8B. +b.3G.3R. +b.3J.3N. +c.3f.4j. +0.39.4q. +1.1t.8P. +1.2f.8s. +3.3K.3N. +3.3M.3L. +4.r.9d. +5.1h.8U. +5.2q.7r. +5.c.9h. +6.3v.40. +b.3G.3S. +d.2U.55. +f.2R.5e. +j.21.8C. +0.2I.5u. +3.3M.3M. +6.3L.3N. +7.s.9d. +9.u.9c. +b.5F.2y. +f.22.8C. +0.23.8C. +0.2S.5e. +1.1t.8Q. +1.29.8x. +1.2n.7S. +3.3M.3N. +3.5G.2y. +4.2K.5s. +4.k.9f. +5.2q.7s. +8.2z.5F. +0.24.8C. +0.3g.4j. +4.u.9d. +5.2q.7t. +6.3N.3N. +6.3y.40. +6.4c.3m. +8.2A.5F. +8.2z.5G. +a.2V.53. +d.2U.58. +0.25.8C. +1.2n.7T. +2.1d.8Y. +2.k.9g. +2.r.9e. +3.2B.5F. +3.2W.53. +5.1t.8R. +5.29.8y. +8.2A.5G. +1.1h.8V. +2.1V.8F. +2.s.9e. +3.2B.5G. +4.26.8C. +5.2q.7u. +6.4q.3e. +a.2V.55. +b.3R.3J. +b.5F.2C. +0.27.8C. +0.3h.4j. +1.2f.8t. +1.2n.7U. +2.A.9b. +3.2W.55. +3.5G.2C. +4.1t.8S. +5.29.8z. +b.3J.3S. +b.3R.3K. +0.7V.2n. +1./.91. +2.u.9e. +3.3K.3S. +5.1h.8W. +5.2q.7v. +6.3i.4j. +6.4c.3q. +a.2J.5u. +b.3R.3L. +c.3f.4q. +j.2a.8z. +1.2f.8u. +5.1V.8G. +5.2q.7w. +6.3S.3L. +6.4c.3r. +8.58.2V. +b.3G.3Y. +b.3R.3M. +b.5F.2D. +c.28.8C. +f.2L.5s. +1./.92. +1.2f.8v. +1.4G.35. +1.8z.2b. +3.3M.3S. +3.5G.2D. +5.1t.8T. +5.2q.7x. +8.58.2W. +a.2Y.53. +b.3R.3N. +f.1V.8H. +1./.93. +1.2f.8w. +1.2n.7X. +6.3S.3N. +7.1+.8D. +7.2p.7V. +g.2E.5G. +0.2K.5u. +1./.94. +5.1h.8X. +5.1V.8I. +5.k.9h. +6.3m.4j. +6.4q.3g. +9.r.9f. +a.2Y.55. +d.2U.5e. +5.1h.8Y. +5.1V.8J. +5.2q.7y. +9.s.9f. +f.2M.5s. +1.2f.8x. +2.r.9g. +5.1V.8K. +b.3G.40. +2.s.9g. +4.u.9f. +5.1t.8U. +5.2q.7z. +6.4c.3v. +6.4q.3h. +7.22.8D. +8.58.2Y. +9.c.9i. +b.3J.3Y. +0.3q.4j. +1.2f.8y. +3.3K.3Y. +5.2q.7A. +6.3i.4q. +b.3R.3R. +0.2N.5s. +2.u.9g. +5./.95. +6.3L.3Y. +6.3r.4j. +7.24.8D. +a.2V.5e. +b.3R.3S. +0.2L.5u. +0.2O.5s. +1.2f.8z. +1.2n.7Y. +3.2W.5e. +3.3M.3Y. +6.3S.3S. +6.4c.3y. +b.5F.2F. +0.7Z.2n. +2.1h.8Z. +3.5G.2F. +5.29.8B. +6.3N.3Y. +7.26.8D. +8.2+.53. +a.2P.5s. +1.1t.8V. +5.2q.7B. +6.3m.4q. +b.3J.40. +b.5F.2G. +j.2a.8B. +1./.96. +1.4G.3b. +3.3K.40. +3.5G.2G. +5.1h.8+. +5.2q.7C. +7.r.9h. +8.2+.55. +f.2Q.5s. +0.2M.5u. +1.4G.3c. +2.2/.53. +2.2H.5F. +5.2q.7D. +6.3L.40. +7.2p.7Z. +7.s.9h. +9.1t.8W. +f.2R.5s. +1.2n.7+. +2.1+.8E. +2.2H.5G. +3.3M.40. +5.2q.7E. +6.3v.4j. +a.2Y.5e. +0.2S.5s. +2.2/.55. +6.3N.40. +6.4q.3q. +7.u.9h. +8.2+.58. +2.A.9g. +5.1h.8/. +6.3r.4q. +b.3R.3Y. +f.29.8C. +0.2N.5u. +0.30.53. +2.c.9j. +5.1t.8X. +5.2a.8C. +5.2q.7F. +6.3S.3Y. +6.3y.4j. +9.k.9i. +b.5F.2I. +0.2O.5u. +1.1V.8L. +1.2n.7/. +2.2/.58. +2.22.8E. +2.c.9k. +3.5G.2I. +5.1t.8Y. +0.30.55. +1.1h.90. +1.1V.8M. +5.2q.7G. +a.2P.5u. +b.3G.4c. +1.1V.8N. +1.2n.80. +1.5s.2T. +2.1+.8F. +2.1d.94. +2.24.8E. +5.2q.7H. +f.2c.8C. +0.2Q.5u. +1./.97. +1.2f.8B. +5.2q.7I. +b.3R.40. +0.2R.5u. +2.26.8E. +5.2q.7J. +6.3S.40. +6.4q.3v. +6.58.30. +0.2d.8C. +7.1+.8G. +8.2+.5e. +b.5F.2J. +0.2S.5u. +1.1h.91. +2.1t.8Z. +2.22.8F. +3.5G.2J. +7.1+.8H. +d.2U.5s. +0.3Y.3Y. +1.1V.8O. +6.3y.4q. +b.3J.4c. +1.1h.92. +1.1V.8P. +1.31.53. +2.2/.5e. +2.24.8F. +3.3K.4c. +5.1t.8+. +7.1+.8I. +9.r.9i. +1.1h.93. +1.2n.81. +1.4G.3k. +6.4c.3L. +6.5L.2w. +7.22.8G. +9.s.9i. +b.3G.4j. +b.5F.2K. +f.1+.8J. +1./.99. +1.1h.94. +1.1V.8Q. +1.2n.82. +1.31.55. +2.26.8F. +2.k.9j. +3.3M.4c. +3.5G.2K. +7.1+.8K. +f.22.8H. +1.2n.83. +2.k.9k. +5.29.8D. +6.4c.3N. +9.u.9i. +a.2V.5s. +f.24.8G. +0.30.5e. +1.2n.84. +2.2g.8r. +3.2W.5s. +5.1t.8/. +5.1V.8R. +6.40.3Y. +7.22.8I. +7.24.8H. +a.32.53. +1.31.58. +1.5s.2X. +6.5L.2x. +6.85.2n. +7.22.8J. +7.26.8G. +1.2n.86. +2./.9b. +4.26.8H. +7.22.8K. +7.24.8I. +9.1V.8S. +a.32.55. +1.1t.90. +1.33.53. +5.1h.95. +7.24.8J. +b.3J.4j. +b.5F.2L. +d.2U.5u. +0.34.53. +3.3K.4j. +3.5G.2L. +7.24.8K. +7.26.8I. +7.2p.85. +1.2n.87. +1.33.55. +1.4G.3s. +4./.9c. +5.1V.8T. +6.3L.4j. +6.40.40. +6.5L.2y. +7.26.8J. +8.58.32. +a.2Y.5s. +b.3G.4q. +b.3R.4c. +1.2n.88. +3.3M.4j. +6.4c.3S. +7.26.8K. +8.2z.5L. +f.34.55. +1.1h.96. +1.2n.89. +2.2g.8t. +2.r.9j. +5./.9d. +6.3N.4j. +8.2A.5L. +b.5F.2M. +1.1t.91. +1.2n.8a. +1.33.58. +1.5s.2Z. +2.r.9k. +2.s.9j. +3.2B.5L. +3.5G.2M. +5.2q.7L. +a.2V.5u. +1.2n.8b. +1.31.5e. +2.s.9k. +3.2W.5u. +6.58.34. +6.5L.2C. +1.1t.92. +1.2f.8D. +1.2n.8c. +2.u.9j. +5.1V.8U. +f.37.53. +1.1+.8L. +1.1t.93. +1.2n.8d. +2./.9e. +2.29.8E. +2.2g.8w. +2.u.9k. +b.3J.4q. +b.5F.2N. +1.1+.8M. +1.1t.94. +1.2n.8e. +3.3K.4q. +3.5G.2N. +6.5L.2D. +b.5F.2O. +c.37.55. +1.1+.8N. +1.2n.8f. +3.5G.2O. +6.3L.4q. +a.32.5e. +b.3R.4j. +b.5F.2P. +g.2E.5L. +2.2n.8g. +2.4G.3z. +3.3M.4q. +3.5G.2P. +6.3S.4j. +6.4c.3Y. +8.2+.5s. +a.2Y.5u. +f.39.53. +1.1V.8V. +1.22.8L. +2.2n.8h. +6.4q.3N. +6.58.37. +b.5F.2Q. +1.1h.97. +1.22.8M. +1.33.5e. +3.5G.2Q. +b.5F.2R. +f.39.55. +1.22.8N. +1.24.8L. +2.2/.5s. +2.29.8F. +3.5G.2R. +5.1t.95. +f.1V.8W. +f.34.5e. +1.1+.8O. +1.24.8M. +1.2n.8i. +4./.9f. +b.5F.2S. +e.2g.8z. +0.3e.53. +1.1+.8P. +1.24.8N. +1.26.8L. +2.1d.9b. +2.4G.3D. +3.5G.2S. +6.4c.40. +6.58.39. +1.26.8M. +1.2n.8j. +2./.9g. +5.29.8G. +5.c.9l. +1.1+.8Q. +1.1t.96. +1.26.8N. +4.55.3e. +5.1V.8X. +5.29.8H. +6.5L.2F. +b.3R.4q. +c.3f.53. +f.30.5s. +0.4j.3Y. +1.22.8O. +2.2f.8E. +5.1V.8Y. +6.3S.4q. +k.98.1h. +1.1h.99. +1.22.8P. +1.2n.8l. +5.29.8I. +6.5L.2G. +7.1+.8R. +8.2+.5u. +c.37.5e. +c.3f.55. +1.24.8O. +5.29.8J. +5.c.9m. +8.58.3e. +0.3g.53. +1.22.8Q. +1.24.8P. +2.2H.5L. +5.29.8K. +9.1+.8S. +1.1h.9a. +1.26.8O. +1.2n.8m. +2.2/.5u. +8.58.3f. +0.39.5e. +0.4j.40. +0.55.3g. +1.24.8Q. +1.26.8P. +1.2n.8n. +2.1h.9b. +5./.9h. +7.22.8R. +d.2U.5F. +0.3h.53. +1.2n.8o. +2.1V.8Z. +2.2f.8F. +7.1+.8T. +d.2U.5G. +1.26.8Q. +1.2n.8p. +6.3i.53. +6.5L.2I. +7.24.8R. +9.22.8S. +0.30.5u. +1.1t.97. +1.31.5s. +5.1h.9c. +5.1V.8+. +5.c.9n. +6.4q.3Y. +8.58.3g. +f.3h.55. +1.2f.8G. +1.2n.8q. +5.2q.7T. +5.c.9o. +5.k.9l. +6.3i.55. +7.26.8R. +9.24.8S. +0.3e.5e. +1.2f.8H. +5.1h.9d. +5.c.9p. +7.22.8T. +b.5F.2V. +3.5G.2V. +5.c.9q. +6.3m.53. +6.4c.4c. +6.58.3h. +7.1+.8U. +9.26.8S. +b.5F.2W. +1.2f.8I. +1.4G.3O. +3.5G.2W. +5.1V.8/. +5.2q.7V. +5.c.9r. +7.24.8T. +8.58.3i. +a.32.5s. +c.3f.5e. +1.2f.8J. +2.4G.3P. +5.c.9s. +5.k.9m. +6.3m.55. +6.4q.40. +6.5L.2J. +1.2f.8K. +1.2n.8r. +2.1h.9e. +5.c.9t. +7.26.8T. +f.2g.8C. +0.3q.53. +1.1t.99. +1.1V.90. +1.2n.8s. +1.33.5s. +2.1d.9g. +5.c.9u. +7.22.8U. +0.34.5s. +1.1+.8V. +1.29.8L. +4.3g.5e. +5.c.9v. +6.3r.53. +6.58.3m. +b.5F.2Y. +0.3q.55. +1.29.8M. +1.31.5u. +1.5s.35. +3.5G.2Y. +5.c.9w. +6.5L.2K. +f.24.8U. +1.1t.9a. +1.29.8N. +1.5s.36. +5.c.9x. +6.3r.55. +7.r.9l. +9.1+.8W. +0.3h.5e. +1.4G.3T. +2.1t.9b. +5.c.9y. +5.k.9n. +6.4c.4j. +7.26.8U. +7.s.9l. +9./.9i. +1.1V.91. +1.22.8V. +1.4G.3U. +5.c.9z. +5.k.9o. +6.3i.5e. +6.58.3q. +1.2n.8t. +5.1h.9f. +5.k.9p. +6.58.3r. +7.u.9l. +a.32.5u. +1.1V.92. +1.24.8V. +1.4G.3W. +4.1t.9c. +5.k.9q. +6.3v.53. +7.1+.8X. +7.r.9m. +9.22.8W. +c.37.5s. +1.1V.93. +1.29.8O. +1.2n.8u. +1.4G.3X. +1.5s.38. +2.1h.9g. +5.2q.7Z. +5.k.9r. +6.5L.2L. +7.1+.8Y. +7.s.9m. +0.55.3v. +1.1V.94. +1.26.8V. +1.29.8P. +1.2n.8v. +1.33.5u. +5.1t.9d. +5.k.9s. +6.3m.5e. +9.24.8W. +0.34.5u. +5.k.9t. +6.3y.53. +7.u.9m. +8.2+.5F. +1.29.8Q. +1.8z.2h. +5.k.9u. +7.22.8X. +8.2+.5G. +9.26.8W. +f.39.5s. +0.4j.4j. +1.2f.8L. +1.5s.3a. +1.8z.2i. +5.2q.7+. +5.k.9v. +6.3y.55. +6.4c.4q. +6.5L.2M. +7.22.8Y. +8.58.3v. +0.3q.5e. +1.2f.8M. +1.2n.8x. +1.4G.3Z. +1.5s.3b. +1.8z.2j. +2.1t.9e. +2.2/.5F. +5.29.8R. +5.k.9w. +7.24.8X. +7.r.9n. +1.2f.8N. +1.5s.3c. +1.8z.2k. +2.1+.8Z. +2.2/.5G. +5.1V.95. +5.k.9x. +6.3r.5e. +7.24.8Y. +7.r.9o. +7.s.9n. +1.5s.3d. +1.8z.2l. +2./.9j. +5.1h.9h. +5.29.8S. +5.k.9y. +6.58.3y. +7.26.8X. +7.r.9p. +7.s.9o. +2./.9k. +5.2q.7/. +5.k.9z. +6.5L.2N. +6.5s.3e. +7.1+.8+. +7.26.8Y. +7.r.9q. +7.s.9p. +7.u.9n. +c.37.5u. +j.2a.8S. +1.2n.8z. +1.4G.41. +6.5L.2O. +7.r.9r. +7.s.9q. +7.u.9o. +b.5F.30. +1.1V.96. +2.22.8Z. +3.5G.30. +5.29.8T. +7.r.9s. +7.s.9r. +7.u.9p. +a.2P.5L. +c.3f.5s. +e.8S.2b. +1.2f.8O. +1.4G.43. +7.r.9t. +7.s.9s. +7.u.9q. +0.39.5u. +1.2f.8P. +1.4G.44. +2.24.8Z. +4.1t.9f. +6.3v.5e. +6.4q.4j. +6.5L.2Q. +7.1+.8/. +7.22.8+. +7.r.9u. +7.s.9t. +7.u.9r. +b.3G.53. +6.5L.2R. +7.r.9v. +7.s.9u. +7.u.9s. +1.2f.8Q. +2.1t.9g. +2.26.8Z. +6.5s.3g. +7.24.8+. +7.r.9w. +7.s.9v. +7.u.9t. +b.3G.55. +1.1+.90. +1.4G.47. +1.8S.2e. +5.29.8U. +6.3y.5e. +6.5L.2S. +7.r.9x. +7.s.9w. +7.u.9u. +1.2f.8R. +1.4G.48. +2.A.9n. +7.22.8/. +7.26.8+. +7.r.9y. +7.s.9x. +7.u.9v. +0.5u.3e. +2.A.9o. +5.2q.81. +7.r.9z. +7.s.9y. +7.u.9w. +8.58.3G. +f.3h.5s. +1.31.5F. +1.4G.4a. +2.A.9p. +6.3i.5s. +7.s.9z. +7.u.9x. +b.3J.53. +f.24.8/. +1.1V.97. +1.22.90. +1.31.5G. +1.4G.4b. +1.5s.3j. +2.A.9q. +3.3K.53. +4.3f.5u. +7.u.9y. +1.1+.91. +1.29.8V. +1.5s.3k. +2.A.9r. +5.2q.84. +6.3L.53. +6.4q.4q. +7.26.8/. +7.u.9z. +b.3J.55. +1.24.90. +1.2f.8T. +1.2n.8B. +1.5s.3l. +2.A.9s. +3.3K.55. +3.3M.53. +5.1h.9i. +5.1t.9h. +1.1+.92. +2.A.9t. +5.29.8W. +6.3L.55. +6.3m.5s. +6.3N.53. +b.5F.32. +0.5u.3g. +1.1+.93. +1.26.90. +1.5s.3n. +2.A.9u. +3.3M.55. +3.5G.32. +8.58.3J. +1.1+.94. +1.22.91. +1.5s.3o. +2.A.9v. +6.3N.55. +8.58.3K. +d.2U.5L. +1.33.5F. +1.4G.4e. +1.5s.3p. +2.2g.8G. +2.A.9w. +5.2q.87. +5.c.9A. +8.58.3L. +b.3G.5e. +0.3q.5s. +0.5u.3h. +1.1V.99. +1.22.92. +1.24.91. +1.2f.8U. +1.33.5G. +1.4G.4f. +2.A.9x. +5.29.8X. +5.2q.88. +8.58.3M. +b.5F.34. +0.2n.8C. +1.22.93. +2.A.9y. +3.5G.34. +5.29.8Y. +5.2q.89. +6.3i.5u. +6.3r.5s. +6.58.3N. +j.2a.8X. +1.22.94. +1.24.92. +1.26.91. +1.4G.4h. +1.5s.3s. +2.A.9z. +5.2q.8a. +b.3R.53. +1.1V.9a. +1.24.93. +2.6g.2v. +5.2q.8b. +6.3S.53. +6.5L.2V. +7.1+.95. +1.24.94. +1.26.92. +2.1V.9b. +3.2W.5L. +5.2q.8c. +7.2p.8C. +b.3R.55. +1.26.93. +1.5s.3t. +5.2q.8d. +6.3m.5u. +6.3S.55. +b.3J.5e. +1.26.94. +1.5s.3u. +2.1h.9j. +2.4G.4l. +3.3K.5e. +5.2q.8e. +b.5F.37. +1.1+.96. +1.2f.8W. +1.4G.4m. +2.1h.9k. +2.29.8Z. +3.5G.37. +5.2q.8f. +6.3L.5e. +6.5s.3v. +7.22.95. +8.58.3R. +9.1V.9c. +9.c.9B. +1.5s.3w. +3.3M.5e. +8.58.3S. +9.c.9C. +0.5u.3q. +1.5s.3x. +5.1V.9d. +5.29.8+. +6.3N.5e. +6.5L.2Y. +7.24.95. +9.1t.9i. +9.c.9D. +1.4G.4p. +5.k.9A. +6.3r.5u. +6.3y.5s. +9.c.9E. +b.5F.39. +1.22.96. +1.2f.8X. +2.5s.3z. +3.5G.39. +6.53.3Y. +7.26.95. +1.2f.8Y. +1.5s.3A. +5.2q.8i. +5.c.9F. +0.55.3Y. +1.24.96. +1.4G.4r. +2.1V.9e. +5.29.8/. +1.2n.8D. +1.5s.3B. +5./.9l. +5.2q.8j. +1.26.96. +1.5s.3C. +b.3R.5e. +b.5F.3e. +0.5u.3v. +1.1+.97. +1.29.90. +2.5s.3D. +3.5G.3e. +5.c.9G. +6.3S.5e. +6.53.40. +8.58.3Y. +1.4G.4u. +1.5s.3E. +4.c.9H. +b.5F.3f. +0.55.40. +1.5s.3F. +2.2f.8Z. +3.5G.3f. +5./.9m. +5.k.9B. +8.2+.5L. +5.k.9C. +6.3y.5u. +7.r.9A. +1.22.97. +1.2f.8+. +2.1t.9j. +4.c.9I. +5.2q.8m. +5.k.9D. +7.s.9A. +9.1V.9f. +b.3G.5s. +1.29.91. +2.1t.9k. +2.2/.5L. +5.2q.8n. +5.k.9E. +8.58.40. +9.c.9J. +b.5F.3g. +1.24.97. +2.1V.9g. +3.5G.3g. +7.u.9A. +k.98.1+. +1.1+.99. +1.29.92. +5.k.9F. +0.5e.3Y. +1.26.97. +1.29.93. +1.4G.4A. +1.5s.3H. +5./.9n. +b.5F.3h. +1.29.94. +1.4G.4B. +1.5s.3I. +3.5G.3h. +5./.9o. +6.5L.30. +b.5F.3i. +1.1+.9a. +2.2n.8E. +3.5G.3i. +5./.9p. +b.3J.5s. +1.22.99. +1.2f.90. +1.4G.4D. +2.1+.9b. +3.3K.5s. +5./.9q. +5.k.9G. +7.r.9B. +2.4G.4E. +5./.9r. +5.k.9H. +6.3L.5s. +7.r.9C. +7.s.9B. +k.98.24. +0.5e.40. +1.24.99. +2.2g.8R. +3.3M.5s. +5./.9s. +5.1V.9h. +5.c.9K. +7.r.9D. +7.s.9C. +b.3G.5u. +b.5F.3m. +1.22.9a. +1.4G.4G. +3.5G.3m. +5./.9t. +5.29.95. +5.2q.8r. +6.3N.5s. +6.4c.53. +7.r.9E. +7.s.9D. +7.u.9B. +9.1+.9c. +k.98.26. +1.26.99. +1.5s.3O. +2.22.9b. +2.2g.8S. +5./.9u. +5.2q.8s. +5.k.9I. +7.s.9E. +7.u.9C. +1.24.9a. +1.2f.91. +2.2n.8F. +2.5s.3P. +5./.9v. +5.k.9J. +6.4c.55. +7.1+.9d. +7.r.9F. +7.u.9D. +2.24.9b. +7.s.9F. +7.u.9E. +b.5F.3q. +e./.9w. +1.26.9a. +1.29.96. +1.2f.92. +1.31.5L. +1.5s.3Q. +3.5G.3q. +5./.9x. +9.22.9c. +b.5F.3r. +1.2f.93. +1.2n.8G. +2.26.9b. +3.5G.3r. +5./.9y. +5.c.9L. +7.u.9F. +8.58.4c. +b.3J.5u. +1.2f.94. +1.2n.8H. +2.1+.9e. +3.3K.5u. +5./.9z. +7.22.9d. +7.r.9G. +9.24.9c. +b.3R.5s. +5.2q.8t. +6.3L.5u. +6.3S.5s. +7.r.9H. +7.s.9G. +1.2n.8I. +1.4G.4M. +1.5s.3T. +3.3M.5u. +6.53.4j. +7.s.9H. +9.26.9c. +a.32.5L. +f.24.9d. +1.2n.8J. +1.4G.4N. +1.5s.3U. +5.1h.9l. +5.2q.8u. +6.3N.5u. +7.u.9G. +0.55.4j. +1.2n.8K. +1.5s.3V. +2.22.9e. +5.2q.8v. +5.c.9M. +5.k.9K. +7.26.9d. +7.r.9I. +7.u.9H. +b.5F.3v. +1.2f.95. +1.33.5L. +1.5s.3W. +2.1d.9n. +2.4G.4P. +3.5G.3v. +5.2q.8w. +5.c.9N. +7.r.9J. +7.s.9I. +1.5s.3X. +2.1d.9o. +2.24.9e. +4.s.9J. +6.5L.34. +1.29.97. +2.1d.9p. +5.1h.9m. +6.4c.5e. +7.u.9I. +8.58.4j. +9.1+.9f. +9.1V.9i. +b.5F.3y. +2.1d.9q. +2.26.9e. +3.5G.3y. +7.u.9J. +1.2f.96. +2.1+.9g. +2.1d.9r. +6.5s.3Y. +b.3R.5u. +2.1d.9s. +2.2g.8W. +2.4G.4T. +4.c.9O. +5.2q.8y. +5.k.9L. +6.3S.5u. +6.4q.53. +1.5s.3Z. +2.1d.9t. +2.4G.4U. +4.c.9P. +9.22.9f. +1.5s.3+. +2.1d.9u. +5.2q.8z. +6.4q.55. +6.5L.37. +1.4G.4W. +1.5s.3/. +2.1d.9v. +2.22.9g. +5.1h.9n. +5.c.9Q. +7.r.9K. +9.24.9f. +1.29.99. +2.1d.9w. +5.1h.9o. +6.5s.40. +7.s.9K. +j.2a.98. +0.5e.4j. +1.5s.41. +2.1d.9x. +2.24.9g. +5.1h.9p. +5.k.9M. +6.58.4q. +9.26.9f. +1.4G.4X. +1.5s.42. +1.98.2b. +2.1d.9y. +5.1h.9q. +5.k.9N. +6.5L.39. +7.1+.9h. +7.u.9K. +1.29.9a. +1.2n.8L. +1.5s.43. +2.1d.9z. +2.26.9g. +5.1h.9r. +5.1t.9l. +b.3G.5F. +0.5u.3Y. +1.2f.97. +1.2n.8M. +1.5s.44. +2.1V.9j. +2.29.9b. +5.1h.9s. +b.3G.5G. +1.2n.8N. +1.5s.45. +2.1V.9k. +5.1h.9t. +7.r.9L. +1.5s.46. +5.1h.9u. +7.22.9h. +7.s.9L. +1.5s.47. +5.1h.9v. +5.1t.9m. +5.29.9c. +5.k.9O. +6.5L.3e. +1.5s.48. +5.k.9P. +7.24.9h. +7.u.9L. +e.1h.9w. +0.5u.40. +1.5s.49. +5.1h.9x. +5.29.9d. +6.4q.5e. +6.5L.3f. +b.3J.5F. +1.2n.8O. +1.5s.4a. +5.1h.9y. +5.2q.8B. +5.k.9Q. +7.26.9h. +7.r.9M. +b.3J.5G. +b.5F.3K. +1.2f.99. +1.2n.8P. +1.5s.4b. +1.6v.2u. +3.5G.3K. +5.1h.9z. +7.r.9N. +7.s.9M. +b.5F.3L. +2.6v.2v. +2.8S.2i. +3.5G.3L. +7.s.9N. +b.5F.3M. +1.2n.8Q. +1.4G.52. +1.8S.2j. +2.29.9e. +3.5G.3M. +5.1t.9n. +6.5L.3g. +7.u.9M. +b.5F.3N. +1.2f.9a. +3.5G.3N. +5./.9A. +5.1t.9o. +6.4c.5s. +7.u.9N. +0.2w.6o. +1.2n.8R. +1.5s.4d. +2.2f.9b. +5.1t.9p. +0.2w.6p. +5.1t.9q. +5.2q.8C. +5.r.9O. +6.5L.3h. +9.1+.9i. +1.2n.8S. +1.4G.56. +1.5s.4e. +5.1t.9r. +5.r.9P. +5.s.9O. +6.5L.3i. +1.2f.9c. +1.4G.57. +1.5s.4f. +5.1t.9s. +5.s.9P. +1.4G.58. +1.5s.4g. +2.c.9R. +3.2x.6o. +5.1t.9t. +5.u.9O. +7.r.9Q. +b.3R.5F. +1.2f.9d. +1.2n.8T. +1.5s.4h. +2.4G.59. +3.2x.6p. +5.1t.9u. +5.29.9f. +5.u.9P. +6.6s.2w. +7.s.9Q. +9.22.9i. +b.3R.5G. +b.5F.3S. +1.5s.4i. +1.6z.2u. +2.4G.5a. +3.5G.3S. +5.1t.9v. +j.2a.9f. +0.2w.6u. +2.29.9g. +2.4G.5b. +2.6z.2v. +5./.9B. +5.1t.9w. +6.5s.4j. +7.u.9Q. +9.24.9i. +0.2w.6v. +1.5s.4k. +3.2y.6o. +5./.9C. +5.1t.9x. +6.4c.5u. +2.2f.9e. +2.5s.4l. +3.2x.6s. +3.2y.6p. +5./.9D. +5.1t.9y. +8.2z.6o. +9.26.9i. +1.2n.8U. +1.4G.5c. +1.5s.4m. +2.6B.2v. +5./.9E. +5.1t.9z. +6.5L.3q. +8.2A.6o. +8.2z.6p. +1.5s.4n. +2.4G.5d. +3.2B.6o. +3.2x.6u. +5.c.9S. +6.5L.3r. +8.2A.6p. +0.2C.6o. +1.5s.4o. +2.1+.9j. +3.2B.6p. +3.2x.6v. +5./.9F. +5.c.9T. +h.2w.6x. +0.2C.6p. +1.5s.4p. +2.1+.9k. +2.4G.5f. +3.2y.6s. +b.5F.3Y. +3.5G.3Y. +5.29.9h. +5.2q.8D. +8.2z.6s. +1.2n.8V. +2.k.9R. +3.2y.6u. +6.4q.5s. +6.6o.2D. +8.2A.6s. +0.5u.4j. +1.2f.9f. +1.5s.4r. +2.22.9j. +2.4G.5i. +3.2B.6s. +3.2x.6x. +3.2y.6v. +5./.9G. +5.c.9U. +6.6p.2D. +8.2z.6u. +g.2E.6o. +1.2n.8W. +1.5s.4s. +1.6g.2T. +2.22.9k. +2.4G.5j. +5./.9H. +6.5L.3v. +6.6s.2C. +8.2A.6u. +8.2z.6v. +g.2E.6p. +1.5s.4t. +2.24.9j. +2.2f.9g. +3.2B.6u. +8.2A.6v. +8.6z.2w. +b.5F.40. +0.2C.6u. +2.24.9k. +3.2B.6v. +3.5G.40. +5.c.9V. +0.2C.6v. +1.5s.4u. +2.26.9j. +3.2y.6x. +5./.9I. +6.5L.3y. +6.6s.2D. +1.2n.8X. +1.5s.4v. +2.26.9k. +5./.9J. +5.c.9W. +8.2z.6x. +f.2w.6B. +g.2E.6s. +0.6u.2D. +1.2n.8Y. +1.5s.4w. +2.4G.5l. +5.1V.9l. +5.k.9S. +8.2A.6x. +8.6z.2x. +0.6v.2D. +5.c.9X. +5.k.9T. +g.2E.6u. +h.2B.6x. +0.2C.6x. +1.5s.4x. +2.r.9R. +5.1h.9A. +6.4q.5u. +a.2F.6o. +g.2E.6v. +0.2w.6E. +1.4G.5n. +1.5s.4y. +2.6J.2v. +2.s.9R. +3.2x.6B. +a.2F.6p. +0.2G.6o. +1.5s.4z. +1.6L.2u. +5.1V.9m. +8.6z.2y. +0.2G.6p. +1.5s.4A. +2.4G.5p. +2.6L.2v. +2.u.9R. +5.k.9U. +8.6z.2z. +h.2D.6x. +1.5s.4B. +1.6g.2X. +2.2H.6o. +2.2n.8Z. +5.29.9i. +8.6z.2A. +h.2E.6x. +1.5s.4C. +2.2H.6p. +3.2x.6E. +3.2y.6B. +5./.9K. +8.6z.2B. +a.2F.6s. +j.2a.9i. +1.2n.8+. +1.5s.4D. +5.k.9V. +8.2z.6B. +8.6z.2C. +b.3G.5L. +1.9i.2b. +2.5s.4E. +6.6s.2G. +7.r.9S. +8.2A.6B. +a.2F.6u. +0.2I.6o. +1.4G.5r. +1.5s.4F. +1.6N.2u. +3.2B.6B. +5.1h.9B. +5.1V.9n. +5.k.9W. +7.r.9T. +7.s.9S. +a.2F.6v. +b.5F.4c. +0.2G.6u. +0.2I.6p. +1.5s.4G. +2.2H.6s. +2.6N.2v. +3.2y.6E. +3.5G.4c. +5.1h.9C. +5.1V.9o. +6.53.53. +7.s.9T. +8.6z.2D. +f.2C.6B. +0.2G.6v. +0.2w.6H. +1.2n.8/. +5.1h.9D. +5.1V.9p. +5.k.9X. +7.u.9S. +8.2z.6E. +8.6z.2E. +0.55.53. +1.6g.2Z. +2.2H.6u. +2.A.9R. +5./.9L. +5.1h.9E. +5.1V.9q. +5.2q.8G. +7.u.9T. +8.2A.6E. +0.2w.6J. +1.5s.4H. +2.2H.6v. +3.2B.6E. +5.1V.9r. +5.2q.8H. +6.6B.2D. +7.r.9U. +a.2F.6x. +b.3J.5L. +0.2C.6E. +1.2n.90. +1.5s.4I. +3.3K.5L. +4.55.55. +5.1h.9F. +5.1V.9s. +6.6s.2I. +7.s.9U. +g.2E.6B. +1.5s.4J. +1.6L.2w. +1.6P.2u. +2.4G.5t. +3.2x.6H. +5.1t.9A. +5.1V.9t. +5.2q.8I. +6.5L.3L. +8.58.53. +a.2J.6o. +h.2G.6x. +0.2I.6u. +1.2f.9i. +1.5s.4K. +2.29.9j. +2.6P.2v. +3.3M.5L. +5.1V.9u. +5.2q.8J. +7.r.9V. +7.u.9U. +a.2J.6p. +0.2I.6v. +1.5s.4L. +2.29.9k. +2.2H.6x. +3.2x.6J. +5./.9M. +5.1V.9v. +5.2q.8K. +6.5L.3N. +6.6E.2D. +7.s.9V. +8.58.55. +b.5F.4j. +1.5s.4M. +3.5G.4j. +5./.9N. +5.1h.9G. +5.1V.9w. +7.r.9W. +g.2E.6E. +0.2K.6o. +0.2w.6M. +1.2n.91. +1.5s.4N. +1.6L.2x. +3.2y.6H. +5.1h.9H. +5.1V.9x. +7.s.9W. +7.u.9V. +8.6z.2F. +e.2g.9c. +0.2K.6p. +1.5s.4O. +5.1V.9y. +6.58.58. +7.r.9X. +8.2z.6H. +a.2J.6s. +1.2n.92. +2.5s.4P. +3.2y.6J. +5.1V.9z. +7.s.9X. +7.u.9W. +8.2A.6H. +8.6z.2G. +h.2I.6x. +1.2n.93. +1.5s.4Q. +3.2B.6H. +5.1h.9I. +7.1+.9l. +8.2z.6J. +a.2F.6B. +a.2J.6u. +0.2C.6H. +0.5e.53. +1.2n.94. +1.5s.4R. +1.6L.2y. +2.2H.6z. +3.2x.6M. +5./.9O. +5.1h.9J. +5.1t.9B. +7.u.9X. +8.2A.6J. +a.2J.6v. +b.3R.5L. +1.5s.4S. +1.6L.2z. +1.6R.2u. +3.2B.6J. +4.2G.6B. +5./.9P. +5.1t.9C. +6.5L.3S. +6.6s.2K. +0.2C.6J. +0.2L.6o. +0.2w.6O. +0.55.5e. +1.6L.2A. +2.6R.2v. +5.1t.9D. +b.5F.4q. +0.2D.6H. +0.2K.6u. +0.2L.6p. +1.6L.2B. +2.2H.6B. +2.5s.4T. +3.5G.4q. +5./.9Q. +5.1t.9E. +7.1+.9m. +7.22.9l. +a.2F.6E. +0.2K.6v. +0.2w.6P. +1.6L.2C. +2.2f.9j. +2.5s.4U. +3.2y.6M. +8.6z.2I. +a.2J.6x. +g.2E.6H. +1.2n.95. +1.5s.4V. +2.2f.9k. +2.4G.5v. +4.2G.6E. +4.6J.2D. +5.1t.9F. +7.24.9l. +8.2z.6M. +8.58.5e. +0.2M.6o. +1.5s.4W. +2.4G.5w. +3.2x.6O. +8.2A.6M. +g.2E.6J. +0.2M.6p. +1.6L.2D. +2.2H.6E. +3.2B.6M. +6.6s.2L. +7.22.9m. +7.26.9l. +f.2I.6B. +0.2C.6M. +1.6L.2E. +3.2x.6P. +5.1h.9K. +h.2K.6x. +0.2L.6u. +1.2n.96. +1.4G.5y. +1.5s.4X. +2.2g.9f. +5.1t.9G. +6.5L.3Y. +7.1+.9n. +f.24.9m. +0.2L.6v. +0.2N.6o. +1.4G.5z. +1.5s.4Y. +3.2y.6O. +5.1t.9H. +7.1+.9o. +8.6z.2J. +0.2I.6E. +0.2N.6p. +0.2O.6o. +6.6M.2D. +6.6s.2M. +7.1+.9p. +7.26.9m. +8.2z.6O. +0.2O.6p. +1.4G.5A. +3.2y.6P. +7.1+.9q. +8.2A.6O. +a.2F.6H. +a.2P.6o. +g.2E.6M. +0.2M.6u. +0.5e.5e. +1.4G.5B. +1.5s.4Z. +3.2B.6O. +3.6R.2w. +5.1t.9I. +7.1+.9r. +7.22.9n. +8.2z.6P. +a.2J.6B. +a.2P.6p. +0.2C.6O. +0.2G.6H. +0.2M.6v. +0.2Q.6o. +5.1h.9L. +5.1t.9J. +6.5L.40. +7.1+.9s. +7.22.9o. +8.2A.6P. +8.6z.2K. +a.2F.6J. +h.2L.6x. +0.2Q.6p. +0.2R.6o. +3.2B.6P. +6.6s.2N. +7.1+.9t. +7.22.9p. +7.24.9n. +0.2C.6P. +0.2G.6J. +0.2R.6p. +1.5s.4+. +1.6L.2F. +2.2H.6H. +6.6s.2O. +7.1+.9u. +7.22.9q. +7.24.9o. +0.2N.6u. +0.2S.6o. +1.4G.5D. +1.5s.4/. +3.6R.2x. +4.2K.6B. +6.6O.2D. +7.1+.9v. +7.22.9r. +7.24.9p. +7.26.9n. +a.2J.6E. +a.2P.6s. +0.2N.6v. +0.2S.6p. +1.2n.97. +1.5s.50. +1.6L.2G. +1.98.2h. +2.2g.9h. +2.2H.6J. +4.2O.6u. +7.1+.9w. +7.22.9s. +7.24.9q. +7.26.9o. +g.2E.6O. +h.2M.6x. +0.2O.6v. +0.6P.2D. +1.5s.51. +1.98.2i. +2.4G.5E. +5.1h.9M. +5.2q.8R. +6.6s.2Q. +7.1+.9x. +7.22.9t. +7.24.9r. +7.26.9p. +a.2P.6u. +0.2I.6H. +1.5s.52. +1.6g.35. +1.98.2j. +2.2H.6L. +5.1h.9N. +6.6s.2R. +7.1+.9y. +7.22.9u. +7.24.9s. +7.26.9q. +8.6z.2L. +a.2F.6M. +a.2P.6v. +g.2E.6P. +0.2K.6E. +0.2Q.6u. +1.6g.36. +1.98.2k. +3.6R.2y. +3.6V.2w. +5.2q.8S. +6.5s.53. +7.1+.9z. +7.22.9v. +7.24.9t. +7.26.9r. +0.2G.6M. +0.2I.6J. +0.2Q.6v. +1.5s.54. +1.98.2l. +5.1t.9K. +6.6s.2S. +7.22.9w. +7.24.9u. +7.26.9s. +8.2z.6R. +f.2R.6u. +h.2N.6x. +2./.9R. +4.55.5s. +7.22.9x. +7.24.9v. +7.26.9t. +8.2A.6R. +f.2L.6B. +f.2R.6v. +h.2O.6x. +0.2S.6u. +1.5s.56. +1.6L.2I. +2.2H.6M. +2.4G.5H. +3.2B.6R. +5.2q.8T. +7.22.9y. +7.24.9w. +7.26.9u. +8.6z.2M. +a.2P.6x. +0.6v.2S. +1.2n.99. +1.5s.57. +3.6R.2C. +3.6V.2x. +5.1h.9O. +7.22.9z. +7.24.9x. +7.26.9v. +1.5s.58. +1.6g.38. +5.1h.9P. +5.1V.9A. +5.29.9l. +7.24.9y. +7.26.9w. +a.2F.6O. +a.2J.6H. +d.2U.6o. +h.2Q.6x. +0.2L.6E. +0.2M.6B. +2.5s.59. +7.24.9z. +7.26.9x. +d.2U.6p. +h.2R.6x. +0.2G.6O. +0.2I.6M. +1.2n.9a. +2.5s.5a. +3.6R.2D. +5.1h.9Q. +5.1t.9L. +6.4c.5L. +7.26.9y. +8.6z.2N. +a.2F.6P. +a.2J.6J. +b.6+.2w. +0.2S.6x. +2.2n.9b. +2.5s.5b. +3.6V.2y. +5.2q.8U. +7.26.9z. +8.6z.2O. +g.2E.6R. +0.2G.6P. +0.2K.6H. +0.5u.53. +1.6g.3a. +1.6L.2J. +2.2H.6O. +5./.9S. +5.29.9m. +6.6v.2T. +8.2z.6V. +8.6z.2P. +0.2M.6E. +0.2N.6B. +1.6g.3b. +1.7d.2u. +5./.9T. +8.2A.6V. +a.2V.6o. +d.2U.6s. +d.71.2w. +0.2K.6J. +0.2O.6B. +0.5u.55. +1.2n.9c. +1.5s.5c. +1.6g.3c. +2.2g.9i. +2.2H.6P. +3.2B.6V. +3.2W.6o. +8.6z.2Q. +a.2V.6p. +b.6+.2x. +1.6g.3d. +2.5s.5d. +3.2W.6p. +3.6V.2C. +5.1t.9M. +8.6z.2R. +a.2P.6B. +d.2U.6u. +1.2n.9d. +1.6L.2K. +1.7g.2u. +5.1t.9N. +5.1V.9B. +6.5s.5e. +7.6x.2T. +a.2J.6M. +d.2U.6v. +f.2I.6O. +0.2N.6E. +2.5s.5f. +2.7g.2v. +5./.9U. +5.1V.9C. +6.58.5u. +8.6z.2S. +d.71.2x. +f.2Q.6B. +0.2I.6P. +0.2L.6H. +0.2O.6E. +1.5s.5g. +3.6V.2D. +5.1V.9D. +5.29.9n. +5.2q.8W. +6.5L.4j. +a.2V.6s. +b.6+.2y. +f.2R.6B. +1.2f.9l. +1.5s.5h. +3.2W.6s. +3.6R.2F. +5.1V.9E. +5.29.9o. +8.2z.6+. +a.2P.6E. +a.2Y.6o. +g.2E.6V. +0.2K.6M. +0.2L.6J. +2.2n.9e. +2.5s.5i. +5./.9V. +5.29.9p. +6.6B.2S. +8.2A.6+. +a.2V.6u. +a.2Y.6p. +d.2U.6x. +0.2Q.6E. +2.5s.5j. +3.2W.6u. +3.6R.2G. +5.1t.9O. +5.1V.9F. +5.29.9q. +a.2V.6v. +b.6+.2B. +d.71.2y. +0.2M.6H. +0.2R.6E. +1.6L.2L. +2.1d.9R. +2.4G.5J. +3.2W.6v. +5./.9W. +5.1t.9P. +5.29.9r. +5.2q.8X. +8.6z.2T. +a.2J.6O. +b.6+.2C. +d.71.2z. +1.2f.9m. +1.5s.5k. +2.2H.6R. +5.29.9s. +5.2q.8Y. +7.6v.2X. +d.71.2A. +0.2M.6J. +0.2S.6E. +2.4G.5K. +5./.9X. +5.1t.9Q. +5.29.9t. +a.2J.6P. +a.2Y.6s. +d.71.2B. +0.5u.5e. +5.1V.9G. +5.29.9u. +d.71.2C. +h.2V.6x. +m.6+.2D. +0.2K.6O. +0.2L.6M. +0.2N.6H. +1.6L.2M. +2.5s.5l. +5.1V.9H. +5.29.9v. +6.5L.4q. +a.2Y.6u. +b.6+.2E. +d.2U.6z. +h.2W.6x. +1.2n.9f. +3.6R.2I. +3.6V.2F. +4.2O.6H. +5.29.9w. +7.6x.2X. +a.2Y.6v. +0.2K.6P. +0.2N.6J. +1.6g.3k. +5.29.9x. +8.2+.6o. +a.2P.6H. +d.71.2D. +0.2O.6J. +1.2f.9n. +1.5s.5n. +2.2n.9g. +3.6V.2G. +5.1V.9I. +5.29.9y. +8.2+.6p. +d.2U.6B. +d.71.2E. +0.2M.6M. +0.2Q.6H. +1.2f.9o. +1.5s.5o. +1.6L.2N. +2.1h.9R. +5.1V.9J. +5.29.9z. +9.6v.2Z. +a.2P.6J. +0.2R.6H. +1.2f.9p. +1.6g.3n. +1.6L.2O. +2.2/.6o. +2.2H.6V. +2.5s.5p. +5.2q.8+. +7.1+.9A. +8.6z.2V. +a.2Y.6x. +0.2L.6O. +0.2Q.6J. +1.2f.9q. +1.5s.5q. +1.6g.3o. +1.6L.2P. +2.2/.6p. +8.6z.2W. +0.2S.6H. +1.2f.9r. +1.6g.3p. +1.7t.2u. +3.6R.2J. +8.2+.6s. +8.6z.2X. +d.2U.6E. +f.2R.6J. +0.2L.6P. +0.2N.6M. +1.2f.9s. +1.6L.2Q. +2.7t.2v. +9.6x.2Z. +a.2V.6B. +b.6+.2F. +0.2O.6M. +0.2S.6J. +0.30.6o. +1.2f.9t. +1.6L.2R. +3.2W.6B. +3.6V.2I. +5.2q.8/. +7.22.9A. +8.2+.6u. +0.30.6p. +1.2f.9u. +1.5s.5r. +1.6g.3s. +2.2/.6s. +8.2+.6v. +a.2P.6M. +b.5F.53. +b.6+.2G. +f.2M.6O. +1.2f.9v. +1.6L.2S. +1.7v.2u. +3.5G.53. +3.6R.2K. +5.1h.9S. +6.5s.5s. +7.24.9A. +8.6z.2Y. +d.71.2F. +0.2M.6P. +0.2Q.6M. +1.2f.9w. +2.2/.6u. +2.2H.6+. +2.7v.2v. +5.1h.9T. +5.1V.9K. +a.2V.6E. +b.5F.55. +0.2R.6M. +1.2f.9x. +1.6g.3t. +2.2/.6v. +3.2W.6E. +3.5G.55. +7.1+.9B. +7.26.9A. +d.71.2G. +0.2N.6O. +1.2f.9y. +1.6g.3u. +6.6s.30. +7.1+.9C. +8.2+.6x. +8.6z.2Z. +a.2Y.6B. +0.2O.6O. +0.2S.6M. +1.2f.9z. +2.2H.71. +3.6V.2J. +7.1+.9D. +8.58.5F. +0.2N.6P. +0.30.6u. +1.6g.3w. +1.6L.2T. +2.5s.5t. +5.1h.9U. +7.1+.9E. +8.58.5G. +a.2P.6O. +b.6+.2I. +d.2U.6H. +0.2O.6P. +0.30.6v. +1.6g.3x. +2.2/.6x. +3.6R.2L. +7.22.9B. +0.2Q.6O. +1.31.6o. +1.9i.2h. +2.1t.9R. +5.1V.9L. +7.1+.9F. +7.22.9C. +a.2P.6P. +a.2Y.6E. +d.2U.6J. +0.2w.7t. +1.31.6p. +1.7A.2u. +1.9i.2i. +2.6g.3z. +3.6V.2K. +5.1h.9V. +7.22.9D. +7.24.9B. +d.71.2I. +f.2R.6O. +0.2Q.6P. +0.5u.5s. +1.6g.3A. +1.6L.2U. +1.9i.2j. +2.4G.5M. +2.7A.2v. +7.22.9E. +7.24.9C. +8.2+.6z. +0.2R.6P. +0.2S.6O. +1.9i.2k. +2.4G.5N. +3.6R.2M. +5.1h.9W. +7.24.9D. +7.26.9B. +a.2V.6H. +h.30.6x. +1.6g.3B. +1.6N.2T. +1.9i.2l. +3.2W.6H. +7.1+.9G. +7.22.9F. +7.24.9E. +7.26.9C. +a.32.6o. +b.6+.2J. +0.6P.2S. +1.31.6s. +1.4G.5O. +1.6g.3C. +2.2/.6z. +3.2x.7t. +5.1h.9X. +5.1V.9M. +7.1+.9H. +7.26.9D. +8.2+.6B. +a.2V.6J. +a.32.6p. +b.5F.5e. +1.2n.9i. +1.4G.5P. +2.6g.3D. +3.2W.6J. +3.5G.5e. +5.1V.9N. +7.24.9F. +7.26.9E. +d.2U.6M. +1.31.6u. +1.33.6o. +1.6g.3E. +1.6L.2V. +3.6R.2N. +3.6V.2L. +5.1t.9S. +d.71.2J. +0.34.6o. +1.31.6v. +1.33.6p. +1.4G.5Q. +1.6g.3F. +1.6L.2W. +2.2/.6B. +3.6R.2O. +5.1t.9T. +5.2q.95. +7.1+.9I. +7.22.9G. +7.26.9F. +b.6+.2K. +0.34.6p. +1.6L.2X. +3.2y.7t. +3.6R.2P. +7.1+.9J. +7.22.9H. +8.2+.6E. +8.6z.30. +a.2Y.6H. +a.32.6s. +5.6P.2T. +7.24.9G. +8.2z.7t. +0.5u.5u. +2.5s.5v. +3.6R.2Q. +3.6V.2M. +5.1V.9O. +7.24.9H. +8.2A.7t. +a.2V.6M. +a.2Y.6J. +a.32.6u. +d.71.2K. +1.31.6x. +1.33.6s. +2.2/.6E. +2.5s.5w. +3.2B.7t. +3.2W.6M. +3.6R.2R. +5.1t.9U. +5.1V.9P. +7.22.9I. +7.26.9G. +a.32.6v. +d.2U.6O. +f.30.6B. +0.2C.7t. +0.7A.2w. +1.5s.5x. +1.6L.2Y. +6.6s.34. +7.22.9J. +7.26.9H. +1.33.6u. +1.6g.3H. +3.6R.2S. +5.1V.9Q. +5.29.9A. +7.24.9I. +b.6+.2L. +c.37.6o. +d.2U.6P. +0.34.6u. +1.33.6v. +1.5s.5y. +1.6g.3I. +1.6N.2X. +3.6V.2N. +5.1t.9V. +c.37.6p. +f.24.9J. +0.2D.7t. +0.30.6E. +0.34.6v. +1.5s.5z. +1.6L.2Z. +2.4G.5S. +3.6V.2O. +7.26.9I. +a.32.6x. +2.2n.9j. +3.2x.7A. +3.6V.2P. +5.1t.9W. +7.1+.9K. +7.26.9J. +9.6v.35. +a.2V.6O. +a.2Y.6M. +d.71.2L. +g.2E.7t. +0.39.6o. +1.31.6z. +1.5s.5A. +2.2n.9k. +3.2W.6O. +8.2+.6H. +9.6v.36. +b.6+.2M. +0.39.6p. +1.33.6x. +1.5s.5B. +3.6V.2Q. +5.1t.9X. +6.6R.2T. +a.2V.6P. +c.37.6s. +3.2W.6P. +3.6V.2R. +8.2+.6J. +h.34.6x. +1.31.6B. +1.6g.3O. +2.2/.6H. +3.2y.7A. +5.6P.2X. +7.22.9K. +9.6x.35. +c.37.6u. +d.71.2M. +1.5s.5C. +1.6L.2+. +1.6N.2Z. +2.6g.3P. +3.6V.2S. +5.29.9B. +8.2z.7A. +8.6z.32. +9.6x.36. +b.6+.2N. +f.37.6v. +1.4G.5T. +1.5s.5D. +2.2/.6J. +5.29.9C. +6.6o.3e. +6.6s.39. +7.1+.9L. +7.24.9K. +8.2A.7A. +9.6v.38. +a.2Y.6O. +b.6+.2O. +d.2U.6R. +1.6g.3Q. +2.2g.9n. +3.2B.7A. +5.29.9D. +6.6p.3e. +b.6+.2P. +0.30.6H. +0.7A.2C. +1.2f.9A. +1.31.6E. +1.33.6z. +2.2/.6L. +2.2g.9o. +2.5s.5E. +5.29.9E. +7.26.9K. +a.2F.7t. +a.2Y.6P. +a.32.6B. +c.3f.6o. +d.71.2N. +f.39.6u. +0.39.6v. +2.2g.9p. +8.2+.6M. +8.6z.34. +b.5F.5s. +b.6+.2Q. +c.3f.6p. +d.71.2O. +h.37.6x. +0.2G.7t. +0.30.6J. +2.2g.9q. +3.5G.5s. +5.29.9F. +7.22.9L. +8.6z.35. +9.6v.3a. +9.6x.38. +b.6+.2R. +d.71.2P. +0.7A.2D. +1.33.6B. +1.6g.3T. +2.2g.9r. +3.6R.2V. +6.6s.3e. +7.1+.9M. +8.6z.36. +9.6P.2Z. +9.6v.3b. +0.34.6B. +1.6g.3U. +1.6L.30. +2.2/.6M. +2.2g.9s. +2.2H.7t. +3.6R.2W. +6.6o.3g. +7.1+.9N. +7.24.9L. +9.6v.3c. +a.32.6E. +b.6+.2S. +d.71.2Q. +g.2E.7A. +0.6u.3e. +1.6g.3V. +2.2g.9t. +2.5s.5H. +4.3f.6s. +6.6p.3g. +6.6R.2X. +9.6v.3d. +d.71.2R. +h.39.6x. +0.6v.3e. +1.5s.5I. +1.6g.3W. +2.2g.9u. +5.29.9G. +7.26.9L. +9.6x.3a. +d.2U.6V. +1.33.6E. +1.6g.3X. +2.2g.9v. +5.29.9H. +6.6o.3h. +7.22.9M. +8.2+.6O. +8.6z.37. +9.6x.3b. +c.3f.6u. +d.71.2S. +0.2I.7t. +0.30.6M. +0.34.6E. +1.2f.9B. +2.2g.9w. +6.3i.6o. +6.6p.3h. +7.22.9N. +8.6z.38. +9.6x.3c. +c.3f.6v. +1.2f.9C. +1.31.6H. +2.1V.9R. +2.2g.9x. +3.6R.2Y. +5.1+.9O. +6.3i.6p. +6.5L.53. +6.6s.3g. +7.24.9M. +8.2+.6P. +9.6x.3d. +1.2f.9D. +2.2/.6O. +2.2g.9y. +5.1+.9P. +5.29.9I. +5.2q.9c. +7.24.9N. +b.5F.5u. +c.37.6B. +h.3e.6x. +0.6u.3g. +1.2f.9E. +1.31.6J. +2.2g.9z. +3.5G.5u. +3.6V.2V. +5.29.9J. +6.5L.55. +7.26.9M. +8.6z.39. +0.6v.3g. +1.6g.3Z. +2.2/.6P. +3.6V.2W. +5.2q.9d. +6.3m.6o. +6.6R.2Z. +6.6s.3h. +7.1+.9Q. +7.26.9N. +8.6z.3a. +a.2F.7A. +h.3f.6x. +1.2f.9F. +1.6g.3+. +1.6L.31. +2.4G.5W. +5.22.9O. +6.3i.6s. +6.3m.6p. +8.6z.3b. +a.32.6H. +d.2U.6+. +0.30.6O. +0.3h.6u. +0.7A.2G. +1.6g.3/. +2.4G.5X. +5.22.9P. +8.58.5L. +8.6z.3c. +a.2J.7t. +f.37.6E. +f.39.6B. +0.6v.3h. +2.4G.5Y. +5.24.9O. +6.3i.6u. +8.6z.3d. +a.32.6J. +0.30.6P. +1.33.6H. +1.4G.5Z. +1.6g.41. +2.2H.7A. +5.1V.9S. +5.24.9P. +6.3i.6v. +6.6o.3q. +7.22.9Q. +8.6z.3e. +d.71.2U. +h.3g.6x. +1.2f.9G. +1.31.6M. +1.6g.42. +1.6L.32. +2.4G.5+. +3.6V.2Y. +5.1V.9T. +5.26.9O. +6.3r.6o. +6.6p.3q. +6.6s.3m. +6.6v.3j. +f.34.6H. +0.2K.7t. +0.39.6E. +1.2f.9H. +1.33.6J. +1.6g.43. +5.26.9P. +6.3r.6p. +7.24.9Q. +8.2+.6R. +8.6z.3f. +9.6v.3k. +b.6+.2V. +0.34.6J. +1.6g.44. +5.29.9K. +6.3m.6u. +6.6B.3e. +6.6v.3l. +b.6+.2W. +h.3h.6x. +0.7A.2I. +1.6g.45. +1.6L.33. +2.6J.35. +6.3m.6v. +7.26.9Q. +h.3i.6x. +1.2f.9I. +1.6g.46. +1.6L.34. +1.6v.3n. +2.2/.6R. +2.5s.5J. +5.1V.9U. +6.6s.3q. +7.6x.3j. +a.32.6M. +c.3f.6B. +d.71.2V. +1.2f.9J. +1.6g.47. +1.6L.35. +2.4G.5/. +6.5L.5e. +6.6s.3r. +6.6v.3o. +8.6z.3g. +9.6x.3k. +d.71.2W. +0.6u.3q. +1.31.6O. +1.6g.48. +1.6L.36. +2.4G.60. +2.5s.5K. +5.2q.9f. +6.6E.3e. +6.6o.3v. +7.6x.3l. +9.6v.3p. +f.37.6H. +0.2L.7t. +0.6v.3q. +1.33.6M. +1.6g.49. +1.7d.2T. +5.1V.9V. +6.3r.6u. +6.6p.3v. +b.6+.2Y. +h.3m.6x. +0.34.6M. +1.2n.9l. +1.31.6P. +1.6g.4a. +3.6R.30. +5.29.9L. +6.3r.6v. +6.6B.3g. +8.6z.3h. +c.37.6J. +c.3f.6E. +1.6g.4b. +1.6v.3s. +2.6J.38. +5.1V.9W. +6.3y.6o. +7.6x.3o. +8.2+.6V. +8.6z.3i. +a.2J.7A. +1.6L.37. +1.7g.2T. +6.3y.6p. +8.6z.3j. +9.6x.3p. +a.32.6O. +d.71.2Y. +f.39.6H. +0.2M.7t. +1.6L.38. +1.6N.35. +2.4G.63. +5.1V.9X. +6.6B.3h. +6.6s.3v. +8.6z.3k. +h.3q.6x. +0.39.6J. +1.2n.9m. +1.6N.36. +2.2/.6V. +6.3i.6B. +6.6E.3g. +8.6z.3l. +9.6v.3t. +a.32.6P. +h.3r.6x. +0.6u.3v. +0.7A.2K. +1.2f.9K. +1.33.6O. +1.6g.4d. +5.29.9M. +6.6v.3u. +8.6z.3m. +0.34.6O. +0.6v.3v. +1.6L.39. +1.6z.3n. +2.4G.64. +5.29.9N. +6.6s.3y. +c.37.6M. +0.2N.7t. +0.3h.6E. +1.33.6P. +1.6g.4e. +1.6L.3a. +2.1+.9R. +4.3e.6H. +5.2q.9h. +8.6z.3o. +9.6v.3w. +0.2O.7t. +0.34.6P. +1.6g.4f. +1.6L.3b. +3.6V.30. +6.3i.6E. +6.3m.6B. +6.3y.6u. +8.2+.6+. +8.6z.3p. +9.6v.3x. +9.6x.3t. +1.31.6R. +1.6g.4g. +1.6L.3c. +1.6N.38. +4.6J.3e. +6.3y.6v. +7.6x.3u. +8.6z.3q. +9.6P.35. +a.2P.7t. +b.5F.5F. +c.3f.6H. +0.39.6M. +1.2n.9n. +1.6g.4h. +1.6L.3d. +1.7d.2X. +2.4G.66. +2.6v.3z. +8.6z.3r. +9.6P.36. +b.5F.5G. +h.3v.6x. +0.2Q.7t. +0.7A.2L. +1.2f.9L. +1.2n.9o. +1.6g.4i. +1.6L.3e. +1.6z.3s. +2.2/.6+. +2.22.9R. +3.5G.5G. +4.3f.6J. +5.29.9O. +9.6v.3A. +9.6x.3w. +b.3G.6o. +d.71.2+. +0.2R.7t. +1.2n.9p. +2.4G.68. +5.29.9P. +6.3m.6E. +6.6B.3q. +9.6x.3x. +b.3G.6p. +c.37.6O. +j.2a.9O. +1.2n.9q. +1.6g.4k. +1.6L.3f. +1.6N.3a. +1.7g.2X. +2.24.9R. +3.6R.32. +4.3g.6H. +6.3r.6B. +7.6v.3B. +h.3y.6x. +j.2a.9P. +0.2S.7t. +1.2n.9r. +1.6N.3b. +2.2/.71. +2.2g.9A. +2.6g.4l. +5.29.9Q. +5.6v.3C. +7.1+.9S. +8.6z.3t. +c.37.6P. +0.6J.3g. +0.7A.2M. +1.2n.9s. +1.4G.69. +1.6g.4m. +1.6N.3c. +2.26.9R. +2.6v.3D. +6.6M.3e. +6.6P.38. +7.1+.9T. +8.6z.3u. +9.6x.3A. +b.6+.30. +j.2a.9Q. +0.3h.6H. +0.3q.6E. +1.2f.9M. +1.2n.9t. +1.33.6R. +1.6g.4n. +1.6N.3d. +8.6z.3v. +9.6v.3E. +b.3G.6s. +f.39.6O. +1.2f.9N. +1.2n.9u. +1.31.6V. +1.6g.4o. +1.6L.3g. +1.7d.2Z. +3.6R.34. +6.3i.6H. +6.3r.6E. +7.6x.3B. +8.6z.3w. +9.6v.3F. +b.3J.6o. +c.3f.6M. +0.3h.6J. +1.2n.9v. +1.6g.4p. +3.3K.6o. +4.6x.3C. +6.5L.5s. +7.22.9S. +8.6z.3x. +9.6R.35. +b.3G.6u. +b.3J.6p. +d.71.30. +f.39.6P. +0.7A.2N. +1.2n.9w. +3.3K.6p. +6.3i.6J. +6.3L.6o. +6.6B.3v. +6.6P.3a. +6.6R.36. +7.1+.9U. +7.22.9T. +8.6z.3y. +9.7t.2T. +b.3G.6v. +0.2w.7V. +0.7A.2O. +1.2n.9x. +1.6L.3h. +1.7g.2Z. +2.5s.5M. +2.6z.3z. +3.3M.6o. +6.3L.6p. +6.6P.3b. +7.24.9S. +9.6x.3E. +1.2n.9y. +1.6g.4r. +1.6L.3i. +2.5s.5N. +3.3M.6p. +3.6V.32. +5.2q.9i. +6.3m.6H. +6.3N.6o. +6.6M.3g. +6.6O.3e. +7.24.9T. +8.6z.3A. +9.6P.3c. +9.6x.3F. +a.2P.7A. +1.2f.9O. +1.2n.9z. +1.6g.4s. +1.6L.3j. +6.3N.6p. +6.3y.6B. +6.6P.3d. +7.1+.9V. +7.26.9S. +b.3J.6s. +0.6P.3e. +0.7A.2Q. +1.2f.9P. +1.5s.5O. +1.6g.4t. +1.6L.3k. +2.6B.3z. +3.3K.6s. +3.6R.37. +6.3m.6J. +6.6E.3v. +6.6v.3H. +7.22.9U. +7.26.9T. +8.6z.3B. +c.3f.6O. +d.2U.7t. +d.7v.2T. +h.3G.6x. +0.7A.2R. +1.31.6+. +1.33.6V. +1.5s.5P. +1.6L.3l. +3.2x.7V. +6.3L.6s. +6.6M.3h. +6.6R.38. +7.1+.9W. +7.6v.3I. +8.6z.3C. +b.3J.6u. +0.3q.6H. +1.2f.9Q. +1.6g.4u. +1.6L.3m. +2.6z.3D. +3.3K.6u. +3.3M.6s. +3.6V.34. +6.3i.6M. +7.24.9U. +b.3J.6v. +c.3f.6P. +0.7A.2S. +1.4G.6c. +1.5s.5Q. +1.6g.4v. +1.6L.3n. +3.3K.6v. +6.3L.6u. +6.3r.6H. +6.3y.6E. +6.6s.3N. +7.1+.9X. +7.22.9V. +8.6z.3E. +0.6J.3q. +1.31.71. +1.6g.4w. +1.6L.3o. +3.3M.6u. +3.6R.39. +6.3L.6v. +6.5L.5u. +6.6O.3g. +7.26.9U. +7.6x.3H. +8.6z.3F. +b.3R.6o. +1.6L.3p. +1.6N.3j. +2.6B.3D. +3.2y.7V. +3.3M.6v. +6.3N.6u. +6.3r.6J. +6.3S.6o. +6.6R.3a. +7.22.9W. +7.24.9V. +7.6x.3I. +a.2V.7t. +b.3R.6p. +b.6+.32. +0.6P.3g. +0.6v.3N. +1.5s.5R. +1.6g.4x. +1.6L.3q. +1.6N.3k. +3.2W.7t. +6.3m.6M. +6.3S.6p. +8.2z.7V. +8.6z.3G. +9.6R.3b. +h.3J.6x. +0.3h.6O. +1.6g.4y. +1.6L.3r. +1.6N.3l. +4.7t.2X. +6.6R.3c. +6.6v.3O. +7.22.9X. +7.26.9V. +8.2A.7V. +f.24.9W. +h.3K.6x. +0.2w.7Z. +1.33.6+. +1.6g.4z. +1.6L.3s. +2.2g.9G. +2.6v.3P. +3.2B.7V. +3.6V.37. +6.3i.6O. +6.6R.3d. +9.7A.2T. +d.71.32. +h.3L.6x. +0.6P.3h. +1.6g.4A. +1.6N.3n. +2.2g.9H. +3.6R.3e. +6.3v.6H. +7.26.9W. +b.3G.6B. +b.3R.6s. +b.6+.34. +f.24.9X. +f.2C.7V. +h.3M.6x. +1.6g.4B. +1.6N.3o. +6.3i.6P. +6.3S.6s. +6.6M.3q. +7.6v.3Q. +8.6z.3H. +h.3N.6x. +1.33.71. +1.6g.4C. +1.6L.3t. +1.6N.3p. +2.29.9R. +2.4G.6d. +2.5s.5S. +3.6R.3f. +4.6J.3v. +5.6P.3j. +6.3r.6M. +7.26.9X. +7.6x.3O. +8.6z.3I. +a.2Y.7t. +b.3R.6u. +d.7v.2X. +0.7V.2D. +1.6g.4D. +1.6L.3u. +2.2g.9I. +2.4G.6e. +2.6J.3w. +3.2x.7Z. +3.6V.39. +6.3m.6O. +6.3S.6u. +6.3y.6H. +6.6P.3k. +8.6z.3J. +b.3R.6v. +d.2U.7A. +d.71.34. +1.6L.3v. +2.6g.4E. +2.6J.3x. +5.6P.3l. +6.3S.6v. +6.6o.3Y. +8.6z.3K. +b.3G.6E. +g.2E.7V. +1.6g.4F. +1.6L.3w. +1.6N.3s. +4.7t.2Z. +6.3m.6P. +6.3y.6J. +6.6p.3Y. +7.6v.3T. +7.6x.3Q. +8.6z.3L. +1.6g.4G. +1.6L.3x. +1.6P.3n. +2.6J.3z. +3.6R.3g. +7.6v.3U. +8.6z.3M. +b.3J.6B. +b.6+.37. +1.6L.3y. +2.6J.3A. +3.2y.7Z. +3.3K.6B. +5.6P.3o. +6.6O.3q. +7.6v.3V. +8.6z.3N. +h.3R.6x. +1.6N.3t. +2.6L.3z. +3.6V.3e. +6.3L.6B. +6.3r.6O. +6.6M.3v. +6.6v.3W. +8.2z.7Z. +8.6z.3O. +9.6P.3p. +a.2V.7A. +h.3S.6x. +0.6P.3q. +1.5s.5T. +1.6g.4H. +1.6L.3A. +1.6N.3u. +2.6z.3P. +3.2W.7A. +3.3M.6B. +3.6R.3h. +5.29.9S. +6.6o.40. +6.6s.3Y. +7.6v.3X. +7.6x.3T. +8.2A.7Z. +d.71.37. +d.7v.2Z. +1.6g.4I. +3.2B.7Z. +3.6R.3i. +3.6V.3f. +5.29.9T. +6.3r.6P. +6.6B.3N. +6.6p.40. +7.6x.3U. +9.7A.2X. +b.6+.39. +e.3J.6E. +0.6u.3Y. +1.6g.4J. +1.6L.3B. +1.6N.3w. +1.6P.3s. +3.3K.6E. +6.3y.6M. +6.6R.3j. +7.6x.3V. +8.2+.7t. +8.6z.3Q. +f.2C.7Z. +0.6v.3Y. +1.6g.4K. +1.6L.3C. +1.6N.3x. +2.2g.9K. +2.6B.3P. +6.3L.6E. +7.6x.3W. +9.6R.3k. +a.2F.7V. +1.4G.6i. +1.6g.4L. +2.2f.9R. +2.6L.3D. +3.3M.6E. +6.6R.3l. +7.6x.3X. +8.6z.3R. +b.3G.6H. +d.71.39. +0.2G.7V. +1.4G.6j. +1.6g.4M. +1.6L.3E. +2.2/.7t. +2.6N.3z. +3.6R.3m. +3.6V.3g. +5.29.9U. +6.3N.6E. +6.6O.3v. +6.6P.3t. +6.6s.40. +7.6v.3Z. +8.6z.3S. +a.2Y.7A. +f.7Z.2D. +1.6g.4N. +1.6L.3F. +1.6N.3A. +1.6R.3n. +2.4G.6k. +5.6P.3u. +6.6v.3+. +8.6z.3T. +b.3G.6J. +b.6+.3e. +e.2E.7Z. +0.6P.3v. +0.6u.40. +1.4G.6l. +1.6g.4O. +1.7d.35. +2.2H.7V. +6.6R.3o. +6.6v.3/. +8.6z.3U. +b.3R.6B. +h.6x.3Y. +0.6v.40. +1.6L.3G. +1.6N.3B. +1.7d.36. +2.6g.4P. +3.6V.3h. +5.29.9V. +6.3S.6B. +6.3y.6O. +6.6P.3w. +8.6z.3V. +9.6R.3p. +9.7A.2Z. +b.6+.3f. +0.30.7t. +1.2n.9A. +1.5s.5U. +1.6g.4Q. +1.6N.3C. +3.6R.3q. +3.6V.3i. +7.6v.41. +7.6x.3Z. +8.6z.3W. +9.6P.3x. +d.71.3e. +j.2a.9V. +1.6g.4R. +1.7g.35. +2.4G.6m. +2.6N.3D. +3.6R.3r. +5.29.9W. +6.3y.6P. +6.6v.42. +7.6x.3+. +8.6z.3X. +b.3J.6H. +b.5F.5L. +1.5s.5V. +1.6g.4S. +1.6N.3E. +1.6R.3s. +1.6v.43. +1.7g.36. +2.6J.3I. +2.6P.3z. +3.3K.6H. +3.5G.5L. +6.85.2w. +7.6x.3/. +b.3R.6E. +d.71.3f. +f.2I.7V. +1.6L.3H. +1.6N.3F. +5.29.9X. +6.3L.6H. +6.3S.6E. +6.6P.3A. +6.6v.44. +b.3G.6M. +b.3J.6J. +b.6+.3g. +h.6x.40. +1.6L.3I. +1.7d.38. +2.6g.4T. +3.3K.6J. +3.3M.6H. +3.6V.3m. +6.6v.45. +7.6x.41. +8.6z.3Y. +1.6L.3J. +2.6g.4U. +5.6P.3B. +6.3L.6J. +6.3N.6H. +6.4c.6o. +6.6R.3t. +7.6v.46. +7.6x.42. +8.2+.7A. +a.2F.7Z. +1.6g.4V. +1.6L.3K. +2.5s.5W. +3.2x.85. +3.3M.6J. +5.6P.3C. +6.4c.6p. +6.6R.3u. +6.6v.47. +8.6z.3Z. +b.6+.3h. +d.71.3g. +1.6g.4W. +1.6L.3L. +1.7g.38. +2.5s.5X. +2.6P.3D. +3.6R.3v. +4.2G.7Z. +6.3N.6J. +6.6B.3Y. +6.6v.48. +7.6x.44. +8.6z.3+. +b.6+.3i. +1.2n.9B. +1.6L.3M. +1.7d.3a. +2.2/.7A. +2.5s.5Y. +2.6J.3O. +3.6V.3q. +6.6P.3E. +7.6v.49. +7.6x.45. +8.6z.3/. +9.6R.3w. +a.2J.7V. +1.2n.9C. +1.31.7t. +1.5s.5Z. +1.6L.3N. +1.6N.3H. +1.6v.4a. +1.7d.3b. +2.2H.7Z. +2.6J.3P. +3.6V.3r. +6.6P.3F. +7.6x.46. +8.6z.40. +9.6R.3x. +b.3G.6O. +b.3J.6M. +d.71.3h. +1.2n.9D. +1.6g.4X. +1.6L.3O. +1.6N.3I. +1.6v.4b. +1.7d.3c. +2.5s.5+. +3.2y.85. +3.3K.6M. +3.6R.3y. +6.4c.6s. +7.6x.47. +8.6z.41. +d.71.3i. +1.2n.9E. +1.6g.4Y. +1.7d.3d. +1.7g.3a. +2.6J.3Q. +2.6L.3P. +2.6R.3z. +6.3L.6M. +6.6E.3Y. +7.6x.48. +8.2z.85. +8.6z.42. +b.3G.6P. +b.3R.6H. +b.6+.3m. +0.2K.7V. +0.7A.30. +1.6z.43. +1.7g.3b. +3.3M.6M. +6.3S.6H. +6.4c.6u. +6.6B.40. +6.6R.3A. +7.6x.49. +8.2A.85. +1.2n.9F. +1.6L.3Q. +1.7g.3c. +3.2B.85. +6.3N.6M. +6.4c.6v. +6.6o.4j. +8.6z.44. +a.32.7t. +b.3R.6J. +f.2I.7Z. +1.6g.4Z. +1.7g.3d. +6.3S.6J. +6.6p.4j. +6.6R.3B. +6.85.2C. +7.6v.4d. +8.6z.45. +d.71.3m. +1.6L.3R. +2.5s.5/. +3.6V.3v. +5.2q.9l. +5.6P.3H. +5.6R.3C. +8.6z.46. +b.3J.6O. +b.6+.3q. +1.33.7t. +1.6L.3S. +1.6N.3O. +2.5s.60. +2.6R.3D. +3.3K.6O. +5.6P.3I. +6.6E.40. +7.6v.4e. +8.6z.47. +b.6+.3r. +0.34.7t. +1.2n.9G. +1.5s.61. +1.6g.4+. +1.6L.3T. +2.6J.3V. +2.6N.3P. +6.3L.6O. +6.6v.4f. +6.85.2D. +8.6z.48. +9.6R.3E. +b.3J.6P. +h.4c.6x. +0.2L.7V. +1.2n.9H. +1.5s.62. +1.6g.4/. +1.6L.3U. +3.3K.6P. +3.3M.6O. +3.6V.3y. +4.7t.35. +6.6s.4j. +7.6v.4g. +7.6x.4d. +8.6z.49. +9.6R.3F. +d.71.3q. +g.2E.85. +1.6g.50. +1.6L.3V. +1.6N.3Q. +1.6z.4a. +2.6J.3X. +4.7t.36. +5.2q.9m. +6.3L.6P. +6.3N.6O. +7.6v.4h. +a.2J.7Z. +b.3R.6M. +d.71.3r. +0.6H.3Y. +0.6u.4j. +1.6g.51. +1.6L.3W. +1.6v.4i. +1.6z.4b. +3.3M.6P. +6.3S.6M. +7.6x.4e. +b.3G.6R. +0.6P.3N. +0.6v.4j. +1.2n.9I. +1.31.7A. +1.6g.52. +1.6L.3X. +2.5s.63. +6.4q.6o. +7.6x.4f. +0.6J.3Y. +1.2n.9J. +5.6P.3O. +6.4q.6p. +6.6v.4k. +7.6x.4g. +b.6+.3v. +d.7v.35. +f.2M.7V. +0.2K.7Z. +1.6g.54. +1.6N.3T. +1.7d.3j. +2.6P.3P. +2.6v.4l. +7.6x.4h. +8.6z.4c. +c.37.7t. +d.7v.36. +1.6L.3Y. +1.6N.3U. +1.7d.3k. +2.5s.64. +2.6J.3Z. +6.6R.3H. +7.6v.4m. +8.6z.4d. +9.7t.38. +0.6H.40. +1.6g.56. +1.6N.3V. +1.6v.4n. +1.7d.3l. +2.6J.3+. +5.2q.9n. +5.6P.3Q. +6.6R.3I. +a.32.7A. +b.3R.6O. +b.6+.3y. +d.71.3v. +h.6x.4j. +0.2N.7V. +1.5s.65. +1.6g.57. +1.6L.3Z. +1.6N.3W. +1.7g.3j. +2.6J.3/. +5.2q.9o. +6.3S.6O. +6.4c.6B. +6.6s.4q. +7.6v.4o. +7.6x.4k. +8.6z.4e. +a.2F.85. +b.3J.6R. +0.2O.7V. +0.39.7t. +0.6J.40. +1.6g.58. +1.6L.3+. +1.6N.3X. +1.7d.3n. +1.7g.3k. +3.3K.6R. +5.2q.9p. +7.6v.4p. +8.6z.4f. +b.3R.6P. +1.33.7A. +1.6L.3/. +1.7d.3o. +1.7g.3l. +2.5s.66. +2.6g.59. +3.6R.3L. +4.7t.3a. +5.2q.9q. +6.3S.6P. +6.4q.6u. +6.6M.3Y. +6.85.2G. +7.6x.4m. +8.6z.4g. +a.2P.7V. +b.3G.6V. +d.71.3y. +d.7v.38. +0.6v.4q. +0.7A.34. +1.5s.67. +1.6L.40. +1.7d.3p. +2.6g.5a. +2.6J.42. +3.3M.6R. +5.2q.9r. +5.6P.3T. +8.6z.4h. +9.7t.3b. +f.2L.7Z. +0.2Q.7V. +1.2n.9K. +1.6L.41. +1.6z.4i. +1.7g.3n. +2.2H.85. +2.5s.68. +2.6g.5b. +2.6J.43. +3.6R.3N. +4.7A.35. +4.7t.3c. +5.2q.9s. +5.6P.3U. +6.4c.6E. +7.6v.4r. +7.6x.4o. +1.6L.42. +1.7g.3o. +2.6J.44. +5.2q.9t. +5.6P.3V. +6.6R.3O. +7.6v.4s. +7.6x.4p. +8.6z.4j. +9.7A.36. +9.7t.3d. +f.2R.7V. +0.3e.7t. +1.6L.43. +1.6N.3Z. +1.7d.3s. +1.7g.3p. +2.6R.3P. +5.2q.9u. +5.6P.3W. +7.6v.4t. +8.6z.4k. +d.7v.3a. +0.2S.7V. +1.5s.69. +1.6g.5c. +1.6L.44. +1.6N.3+. +2.6J.46. +2.6z.4l. +5.2q.9v. +5.6P.3X. +6.6M.40. +d.7v.3b. +f.2M.7Z. +h.4q.6x. +1.5s.6a. +1.6L.45. +1.6N.3/. +2.6g.5d. +2.6J.47. +5.2q.9w. +6.6B.4j. +6.6O.3Y. +6.6R.3Q. +6.6v.4u. +6.85.2I. +7.6x.4r. +8.6z.4m. +c.3f.7t. +d.7v.3c. +e.3J.6V. +1.6L.46. +1.6z.4n. +1.7d.3t. +1.7g.3s. +3.3K.6V. +5.2q.9x. +7.6v.4v. +7.6x.4s. +b.3G.6+. +d.7v.3d. +f.37.7A. +0.6P.3Y. +1.2n.9L. +1.5s.6b. +1.6L.47. +1.6N.41. +1.7d.3u. +2.6B.4l. +2.6g.5f. +3.6V.3L. +5.2q.9y. +7.6v.4w. +7.6x.4t. +8.6z.4o. +9.7A.38. +b.3R.6R. +0.2N.7Z. +1.6g.5g. +1.6L.48. +1.6N.42. +3.3M.6V. +3.6R.3S. +5.2q.9z. +8.6z.4p. +0.2O.7Z. +0.3g.7t. +1.6g.5h. +1.6L.49. +1.6N.43. +1.7d.3w. +1.7g.3t. +3.6V.3N. +5.6P.3Z. +6.6E.4j. +6.6R.3T. +7.6x.4u. +9.6v.4x. +d.71.3G. +0.7A.39. +1.6L.4a. +1.6N.44. +1.7d.3x. +1.7g.3u. +2.6g.5i. +5.6P.3+. +6.4c.6H. +6.6O.40. +6.6R.3U. +7.6v.4y. +7.6x.4v. +8.6z.4q. +a.2P.7Z. +1.6L.4b. +1.6N.45. +2.6g.5j. +5.6P.3/. +6.6R.3V. +6.6v.4z. +7.6x.4w. +8.6z.4r. +9.7A.3a. +a.2J.85. +0.3h.7t. +0.6P.40. +1.2n.9M. +1.6N.46. +1.7g.3w. +2.2g.9S. +6.4c.6J. +7.6v.4A. +8.6z.4s. +9.6R.3W. +9.7A.3b. +b.6+.3J. +f.2Q.7Z. +1.2n.9N. +1.6g.5k. +1.6N.47. +1.7d.3A. +1.7g.3x. +2.2g.9T. +2.6J.4d. +5.6P.41. +6.3i.7t. +6.6B.4q. +6.6R.3X. +7.6v.4B. +8.6z.4t. +9.6x.4x. +9.7A.3c. +b.6+.3K. +d.2U.7V. +f.2R.7Z. +1.6L.4c. +1.6N.48. +1.8z.2u. +5.6P.42. +7.6v.4C. +7.6x.4y. +9.7A.3d. +9.7t.3j. +b.6+.3L. +e.5L.5L. +0.2S.7Z. +0.7A.3e. +1.5s.6c. +1.6L.4d. +1.6N.49. +1.6P.43. +1.7d.3B. +2.7g.3z. +2.8z.2v. +6.6v.4D. +6.85.2K. +7.6x.4z. +8.6z.4u. +9.7t.3k. +b.3R.6V. +b.6+.3M. +d.71.3J. +1.4G.6t. +1.6N.4a. +1.7d.3C. +1.7g.3A. +2.6g.5l. +2.6J.4f. +2.6v.4E. +3.6R.3Y. +3.6V.3S. +5.6P.44. +7.6x.4A. +8.6z.4v. +9.7t.3l. +b.6+.3N. +d.71.3K. +1.6L.4e. +1.6N.4b. +2.2g.9U. +2.6J.4g. +5.6P.45. +6.3m.7t. +6.4q.6E. +7.6v.4F. +7.6x.4B. +8.6z.4w. +c.3f.7A. +d.71.3L. +0.6H.4j. +1.2n.9O. +1.6L.4f. +1.7d.3E. +1.7g.3B. +1.7t.3n. +2.6J.4h. +5.6P.46. +6.4c.6M. +7.6v.4G. +7.6x.4C. +9.6R.3Z. +a.2V.7V. +d.71.3M. +d.7v.3j. +1.2n.9P. +1.6g.5n. +1.6L.4g. +1.7d.3F. +1.7g.3C. +3.2W.7V. +5.6P.47. +6.6R.3+. +7.6x.4D. +8.6z.4x. +9.7t.3o. +d.71.3N. +d.7v.3k. +0.6J.4j. +1.6g.5o. +1.6L.4h. +2.2g.9V. +2.7g.3D. +5.6P.48. +6.6R.3/. +8.6z.4y. +9.7t.3p. +d.7v.3l. +0.3q.7t. +0.7A.3g. +1.2n.9Q. +1.6L.4i. +1.6N.4d. +1.7g.3E. +2.6g.5p. +2.6J.4k. +3.6R.40. +5.6P.49. +6.85.2L. +7.6v.4H. +7.6x.4F. +8.6z.4z. +1.6g.5q. +1.6L.4j. +1.6P.4a. +1.7g.3F. +1.7v.3n. +2.2g.9W. +2.6J.4l. +6.3r.7t. +6.6R.41. +7.6v.4I. +7.6x.4G. +8.6z.4A. +b.6+.3R. +1.4G.6y. +1.6L.4k. +1.6N.4e. +1.6P.4b. +1.7t.3s. +2.5s.6d. +2.6J.4m. +6.6R.42. +7.6v.4J. +8.6z.4B. +b.6+.3S. +d.7v.3o. +0.7A.3h. +1.6N.4f. +1.6R.43. +2.2g.9X. +2.5s.6e. +2.6J.4n. +2.6L.4l. +3.6V.3Y. +6.4c.6O. +7.6v.4K. +8.6z.4C. +a.2Y.7V. +d.2U.7Z. +d.7v.3p. +1.6L.4m. +1.6N.4g. +1.7d.3H. +2.6J.4o. +6.3i.7A. +6.6R.44. +6.85.2M. +7.6v.4L. +7.6x.4H. +8.6z.4D. +d.71.3R. +1.5s.6f. +1.6g.5r. +1.6L.4n. +1.6N.4h. +1.7d.3I. +2.6J.4p. +2.6z.4E. +6.4c.6P. +6.4q.6H. +6.6M.4j. +6.6v.4M. +7.6x.4I. +9.6R.45. +9.7A.3j. +9.7t.3t. +d.71.3S. +1.5s.6g. +1.6L.4o. +1.6N.4i. +1.7v.3s. +5.6P.4d. +6.6v.4N. +7.6x.4J. +8.6z.4F. +9.6R.46. +9.7A.3k. +9.7t.3u. +0.7t.3v. +1.6L.4p. +1.7g.3H. +6.4q.6J. +6.6R.47. +6.6v.4O. +7.6x.4K. +8.6z.4G. +9.7A.3l. +0.7A.3m. +1.6N.4k. +1.7g.3I. +2.6B.4E. +2.6J.4r. +2.6v.4P. +3.6V.40. +5.6P.4e. +6.6R.48. +6.85.2N. +7.6x.4L. +9.7t.3w. +a.2V.7Z. +1.6L.4q. +1.7A.3n. +2.6J.4s. +2.6N.4l. +3.2W.7Z. +5.6P.4f. +6.85.2O. +7.6v.4Q. +7.6x.4M. +9.6R.49. +9.7t.3x. +d.7v.3t. +1.5s.6h. +1.6L.4r. +1.6N.4m. +1.6R.4a. +2.6J.4t. +5.6P.4g. +6.3y.7t. +7.6v.4R. +7.6x.4N. +8.6z.4H. +9.7A.3o. +a.2P.85. +b.6+.3Y. +d.7v.3u. +1.6L.4s. +1.6N.4n. +1.6R.4b. +1.7d.3O. +2.6g.5t. +2.7t.3z. +5.6P.4h. +6.6O.4j. +7.6v.4S. +7.6x.4O. +8.6z.4I. +9.7A.3p. +0.7A.3q. +1.6L.4t. +1.6N.4o. +1.6P.4i. +6.85.2Q. +8.2+.7V. +8.6z.4J. +9.7t.3A. +d.7v.3w. +0.6P.4j. +0.7A.3r. +1.5s.6i. +1.6N.4p. +2.6J.4v. +2.6v.4T. +6.4q.6M. +6.85.2R. +7.6x.4Q. +8.6z.4K. +d.71.3Y. +d.7v.3x. +1.5s.6j. +1.6L.4u. +1.7A.3s. +1.7d.3Q. +1.7g.3O. +2.6J.4w. +2.6v.4U. +3.6R.4c. +5.6P.4k. +7.6x.4R. +8.6z.4L. +9.7t.3B. +a.2Y.7Z. +1.6L.4v. +2.2/.7V. +2.5s.6k. +2.6P.4l. +2.7g.3P. +2.7v.3z. +5.2q.9A. +5.7t.3C. +6.6R.4d. +6.85.2S. +7.6v.4V. +7.6x.4S. +8.6z.4M. +b.6+.40. +1.4G.6F. +1.5s.6l. +1.6L.4w. +1.6v.4W. +2.6J.4x. +2.7t.3D. +5.6P.4m. +8.6z.4N. +d.7v.3A. +e.6N.4r. +1.6P.4n. +1.7g.3Q. +2.6J.4y. +8.6z.4O. +9.6R.4e. +9.7A.3t. +9.7t.3E. +e.6N.4s. +1.6L.4x. +1.6N.4t. +1.7d.3T. +2.6J.4z. +2.6z.4P. +5.6P.4o. +6.6R.4f. +9.7A.3u. +9.7t.3F. +d.71.40. +d.7v.3B. +0.30.7V. +0.7A.3v. +1.4G.6G. +1.6L.4y. +1.7d.3U. +2.5s.6m. +5.6P.4p. +6.4q.6O. +6.6R.4g. +7.6x.4V. +8.6z.4Q. +9.6v.4X. +d.7v.3C. +1.6L.4z. +1.6N.4u. +1.7d.3V. +2.2n.9R. +2.6J.4B. +2.7v.3D. +6.6R.4h. +8.6z.4R. +9.6v.4Y. +9.7A.3w. +b.3G.7t. +0.6P.4q. +1.6L.4A. +1.6N.4v. +1.6R.4i. +1.7d.3W. +1.7g.3T. +2.6B.4P. +2.6J.4C. +8.6z.4S. +9.7A.3x. +d.7v.3E. +0.7A.3y. +1.6L.4B. +1.6N.4w. +1.7d.3X. +1.7g.3U. +2.6J.4D. +3.6R.4j. +3.6V.4c. +5.6P.4r. +d.7v.3F. +0.2w.8C. +1.6L.4C. +1.6v.4Z. +1.7g.3V. +2.6z.4T. +2.7A.3z. +5.6P.4s. +6.6o.53. +8.2+.7Z. +9.6R.4k. +9.6x.4X. +1.4G.6I. +1.6L.4D. +1.6N.4x. +1.7g.3W. +2.6g.5v. +2.6R.4l. +2.6z.4U. +4.7t.3H. +5.6P.4t. +6.6p.53. +9.6x.4Y. +9.7A.3A. +d.2U.85. +1.6N.4y. +1.7g.3X. +2.6g.5w. +2.6L.4E. +6.6o.55. +8.6z.4V. +9.6R.4m. +9.7t.3I. +1.6g.5x. +1.6L.4F. +1.6N.4z. +1.6R.4n. +1.6z.4W. +2.2/.7Z. +2.6B.4T. +5.6P.4u. +6.6p.55. +9.6v.4+. +9.7A.3B. +b.3J.7t. +1.6L.4G. +1.6N.4A. +1.7d.3Z. +2.6B.4U. +3.2x.8C. +3.3K.7t. +5.6P.4v. +5.7A.3C. +6.6R.4o. +9.6v.4/. +1.31.7V. +1.6N.4B. +1.7d.3+. +2.6J.4H. +2.7A.3D. +5.2q.9F. +5.6P.4w. +6.3L.7t. +6.58.6o. +6.6R.4p. +6.6s.53. +9.6v.50. +d.7v.3H. +1.6N.4C. +1.7d.3/. +3.3M.7t. +6.58.6p. +6.6v.51. +8.6z.4X. +9.7A.3E. +a.2V.85. +b.6+.4c. +d.7v.3I. +0.30.7Z. +0.6u.53. +1.6L.4H. +1.7g.3Z. +2.6J.4J. +3.2W.85. +3.6R.4q. +3.6V.4j. +6.3N.7t. +6.6s.55. +6.6v.52. +8.6z.4Y. +9.6P.4x. +9.6x.4+. +9.7A.3F. +e.6N.4D. +0.6v.53. +1.6L.4I. +1.7d.41. +1.7g.3+. +2.6J.4K. +2.6N.4E. +3.2y.8C. +5.6P.4y. +9.6R.4r. +9.6x.4/. +9.7t.3O. +0.6u.55. +1.6L.4J. +1.6N.4F. +1.7d.42. +1.7g.3/. +2.6J.4L. +2.7t.3P. +5.2q.9G. +5.6P.4z. +6.6v.54. +8.2z.8C. +9.6R.4s. +9.6x.50. +a.32.7V. +b.3G.7A. +d.71.4c. +0.6v.55. +1.6L.4K. +1.6N.4G. +1.6z.4Z. +1.7d.43. +2.6J.4M. +5.2q.9H. +5.6P.4A. +6.58.6s. +7.6x.51. +8.2A.8C. +9.6R.4t. +1.6L.4L. +1.6v.56. +1.7d.44. +1.7g.41. +2.6J.4N. +3.2B.8C. +4.7t.3Q. +5.6P.4B. +7.6x.52. +0.2C.8C. +1.33.7V. +1.6L.4M. +1.7d.45. +1.7g.42. +2.6J.4O. +5.6P.4C. +6.58.6u. +7.6v.57. +9.6R.4u. +a.2Y.85. +d.7v.3O. +h.6x.53. +0.34.7V. +1.6L.4N. +1.6N.4H. +1.7d.46. +1.7g.43. +2.6J.4P. +2.7v.3P. +5.2q.9I. +5.6P.4D. +6.6o.5e. +7.6x.54. +8.58.6v. +8.6z.4+. +9.6R.4v. +9.7A.3H. +b.3R.7t. +0.55.6x. +1.6L.4O. +1.6N.4I. +1.7d.47. +1.7g.44. +2.6J.4Q. +2.6P.4E. +2.6v.59. +6.3S.7t. +6.6p.5e. +8.6z.4/. +9.6R.4w. +9.7A.3I. +b.6+.4j. +0.2D.8C. +1.6N.4J. +1.7d.48. +1.7g.45. +2.6g.5E. +2.6J.4R. +2.6L.4P. +2.6v.5a. +3.6V.4q. +5.6P.4F. +8.6z.50. +9.7t.3T. +b.3J.7A. +d.7v.3Q. +1.31.7Z. +1.6L.4Q. +1.6N.4K. +1.7d.49. +1.7g.46. +2.6J.4S. +2.6v.5b. +3.3K.7A. +5.6P.4G. +7.6x.57. +8.6z.51. +9.6R.4x. +9.7t.3U. +g.2E.8C. +1.6L.4R. +1.6N.4L. +1.7d.4a. +1.7g.47. +6.3L.7A. +8.58.6x. +8.6z.52. +9.6R.4y. +9.7t.3V. +d.71.4j. +1.6L.4S. +1.6N.4M. +1.7d.4b. +1.7g.48. +2.6J.4T. +3.3M.7A. +6.6R.4z. +6.6s.5e. +8.6z.53. +9.7t.3W. +0.7A.3N. +1.6N.4N. +1.7g.49. +2.6J.4U. +4.7t.3X. +5.6P.4H. +6.6R.4A. +8.6z.54. +9.6v.5c. +c.37.7V. +d.7v.3T. +0.6u.5e. +1.6N.4O. +1.7g.4a. +2.6g.5H. +2.6L.4T. +2.6v.5d. +5.6P.4I. +6.6R.4B. +8.6z.55. +9.7A.3O. +a.32.7Z. +d.7v.3U. +0.6v.5e. +1.6z.56. +1.7g.4b. +2.6J.4W. +2.6L.4U. +2.6N.4P. +2.7A.3P. +5.6P.4J. +6.6B.53. +8.2+.85. +9.6R.4C. +d.7v.3V. +0.7t.3Y. +1.6L.4V. +1.6N.4Q. +1.7d.4d. +2.6v.5f. +5.2q.9K. +5.6P.4K. +6.6R.4D. +8.6z.57. +b.6+.4q. +d.7v.3W. +0.39.7V. +1.33.7Z. +1.6L.4W. +1.6N.4R. +2.6R.4E. +5.6P.4L. +6.6B.55. +6.6v.5g. +8.58.6z. +9.6x.5c. +9.7A.3Q. +d.7v.3X. +1.6N.4S. +1.6v.5h. +1.7d.4e. +2.2/.85. +2.6J.4X. +2.6z.59. +4.7t.3Z. +5.6P.4M. +9.6R.4F. +a.2F.8C. +f.34.7Z. +1.7d.4f. +1.7g.4d. +2.6J.4Y. +2.6v.5i. +2.6z.5a. +5.6P.4N. +6.6E.53. +9.6R.4G. +9.7t.3+. +b.3R.7A. +d.71.4q. +h.6x.5e. +0.2G.8C. +1.6L.4X. +1.7d.4g. +2.6N.4T. +2.6v.5j. +2.6z.5b. +5.6P.4O. +6.3S.7A. +6.58.6B. +9.7t.3/. +0.55.6E. +0.7t.40. +1.4G.6T. +1.6L.4Y. +1.7d.4h. +1.7g.4e. +2.6B.59. +2.6N.4U. +2.6P.4P. +7.6x.5g. +9.7A.3T. +1.6N.4V. +1.7d.4i. +1.7g.4f. +2.2H.8C. +2.6B.5a. +2.6J.4Z. +4.7t.41. +5.2q.9L. +5.6P.4Q. +6.6R.4H. +9.6v.5k. +9.7A.3U. +d.7v.3Z. +f.7V.3e. +i.85.30. +1.7g.4g. +2.6B.5b. +5.6P.4R. +8.6z.5c. +9.6R.4I. +9.7A.3V. +9.7t.42. +d.7v.3+. +e.6N.4W. +1.6L.4Z. +1.7d.4k. +1.7g.4h. +1.7t.43. +2.6z.5d. +5.58.6E. +5.6P.4S. +9.6R.4J. +9.7A.3W. +c.3f.7V. +d.7v.3/. +f.37.7Z. +1.7g.4i. +2.6J.4+. +2.6v.5l. +8.6z.5e. +9.6R.4K. +9.7A.3X. +9.7t.44. +0.2I.8C. +1.6N.4X. +1.7d.4m. +2.6J.4/. +2.6P.4T. +2.6z.5f. +9.6R.4L. +9.6x.5k. +9.7t.45. +d.7v.41. +1.5s.6n. +1.6L.4+. +1.6N.4Y. +1.7d.4n. +1.7g.4k. +2.6B.5d. +2.6J.50. +2.6P.4U. +8.6z.5g. +9.6R.4M. +9.7t.46. +d.7v.42. +0.6H.53. +0.7A.3Y. +0.7V.3g. +1.4G.6W. +1.6L.4/. +1.6v.5n. +1.6z.5h. +1.7d.4o. +1.7v.43. +2.6J.51. +2.7g.4l. +5.6P.4V. +6.6B.5e. +6.6o.5s. +9.6R.4N. +9.7t.47. +f.39.7Z. +1.6L.50. +1.6P.4W. +1.7d.4p. +1.7g.4m. +2.6B.5f. +2.6J.52. +2.6z.5i. +6.6p.5s. +6.6R.4O. +6.6v.5o. +9.7t.48. +d.7v.44. +0.55.6H. +0.6J.53. +1.5s.6q. +1.6L.51. +1.6N.4Z. +1.7g.4n. +2.6g.5J. +2.6R.4P. +2.6v.5p. +2.6z.5j. +9.7A.3Z. +9.7t.49. +d.7v.45. +0.7V.3h. +1.31.85. +1.4G.6Z. +1.6L.52. +1.7g.4o. +1.7t.4a. +2.6J.54. +6.6v.5q. +9.6R.4Q. +9.7A.3+. +d.7v.46. +0.6J.55. +1.5s.6r. +1.6L.53. +1.7d.4r. +1.7g.4p. +1.7t.4b. +2.6B.5i. +2.6g.5K. +6.3i.7V. +6.6E.5e. +6.6P.4X. +6.6R.4R. +8.6z.5k. +9.7A.3/. +a.2J.8C. +d.7v.47. +0.7A.40. +1.6L.54. +1.6N.4+. +1.7d.4s. +2.6B.5j. +2.6J.56. +5.2q.9O. +6.6R.4S. +6.6s.5s. +7.6x.5o. +8.58.6H. +9.6P.4Y. +d.7v.48. +f.7Z.3e. +1.5s.6t. +1.6L.55. +1.6N.4/. +1.7d.4t. +2.6J.57. +5.2q.9P. +9.7A.41. +d.7v.49. +0.6u.5s. +1.4G.6/. +1.6L.56. +1.6N.50. +1.6v.5r. +1.7g.4r. +1.7v.4a. +2.6R.4T. +2.6z.5l. +6.4c.7t. +6.58.6J. +7.6x.5q. +9.7A.42. +a.32.85. +c.3f.7Z. +0.2K.8C. +0.6v.5s. +1.4G.70. +1.6L.57. +1.6N.51. +1.6P.4Z. +1.7A.43. +1.7d.4u. +1.7g.4s. +1.7v.4b. +2.6R.4U. +4.7t.4d. +5.2q.9Q. +6.3m.7V. +6.6M.53. +0.5u.6o. +1.4G.71. +1.6L.58. +1.6N.52. +1.7d.4v. +1.7g.4t. +2.6J.5a. +6.6R.4V. +9.7A.44. +0.5u.6p. +1.33.85. +1.4G.72. +1.6R.4W. +1.6z.5n. +1.7d.4w. +2.6B.5l. +2.6J.5b. +2.6L.59. +6.6M.55. +9.7A.45. +9.7t.4e. +0.7Z.3g. +1.4G.73. +1.5s.6w. +1.6N.54. +1.7g.4u. +2.6L.5a. +4.7t.4f. +6.6P.4+. +6.85.34. +8.6z.5o. +9.7A.46. +1.4G.74. +1.7d.4x. +1.7g.4v. +2.6L.5b. +2.6z.5p. +6.6P.4/. +9.7A.47. +9.7t.4g. +d.7v.4d. +f.7V.3q. +h.5s.6x. +0.6H.5e. +1.4G.75. +1.5s.6y. +1.6N.56. +1.7d.4y. +1.7g.4w. +2.6v.5t. +6.3r.7V. +6.58.6M. +6.6P.50. +6.6R.4X. +8.6z.5q. +9.7A.48. +9.7t.4h. +0.2L.8C. +0.3h.7Z. +1.4G.76. +1.6N.57. +1.7d.4z. +1.7t.4i. +2.6J.5d. +5.6P.51. +6.6O.53. +6.6R.4Y. +6.6s.5u. +9.7A.49. +d.7v.4e. +0.6J.5e. +0.7t.4j. +1.4G.77. +1.6L.5c. +1.6N.58. +1.7A.4a. +1.7d.4A. +1.7g.4x. +2.6B.5p. +5.6P.52. +6.3i.7Z. +d.7v.4f. +0.5u.6u. +0.6P.53. +1.4G.78. +1.7A.4b. +1.7d.4B. +1.7g.4y. +2.6L.5d. +2.6N.59. +6.6O.55. +9.7t.4k. +d.7v.4g. +0.6v.5u. +1.4G.79. +1.6L.5e. +1.6z.5r. +1.7d.4C. +1.7g.4z. +2.6J.5g. +2.6N.5a. +2.7t.4l. +5.6P.54. +6.85.37. +d.7v.4h. +k.6R.4Z. +0.2M.8C. +0.6P.55. +1.4G.7a. +1.7d.4D. +1.7g.4A. +1.7v.4i. +2.6J.5h. +2.6L.5f. +2.6N.5b. +4.7t.4m. +8.6z.5s. +0.7V.3v. +1.4G.7b. +1.6L.5g. +1.6P.56. +1.7g.4B. +1.7t.4n. +6.3m.7Z. +6.4c.7A. +6.58.6O. +1.4G.7c. +1.5s.6A. +1.6L.5h. +1.7d.4F. +1.7g.4C. +5.6P.57. +6.6R.4+. +9.7A.4d. +9.7t.4o. +d.7v.4k. +e.8S.2u. +0.5u.6x. +1.6N.5c. +1.7d.4G. +1.7g.4D. +2.6L.5i. +2.7v.4l. +2.8S.2v. +6.6B.5s. +6.6M.5e. +6.85.39. +8.58.6P. +9.6R.4/. +9.7t.4p. +0.2N.8C. +1.4G.7e. +2.6J.5k. +2.6L.5j. +2.6N.5d. +2.6P.59. +2.7g.4E. +6.3y.7V. +6.6R.50. +9.7A.4e. +d.7v.4m. +0.2O.8C. +0.3q.7Z. +1.4G.7f. +1.5s.6C. +1.7g.4F. +1.7v.4n. +2.6P.5a. +2.6z.5t. +6.4q.7t. +6.6R.51. +9.7A.4f. +1.5s.6D. +1.6L.5k. +1.7d.4H. +1.7g.4G. +2.6N.5f. +2.6P.5b. +6.3r.7Z. +6.6R.52. +9.7A.4g. +9.7t.4r. +a.2P.8C. +d.7v.4o. +1.4G.7h. +1.6N.5g. +1.7d.4I. +3.6R.53. +6.6E.5s. +9.7A.4h. +9.7t.4s. +d.7v.4p. +0.2Q.8C. +1.6N.5h. +1.7A.4i. +1.7d.4J. +2.6B.5t. +2.6g.5M. +2.6v.5v. +6.6R.54. +6.85.3e. +9.7t.4t. +0.2R.8C. +0.7A.4j. +1.4G.7i. +1.5s.6F. +1.7d.4K. +1.7g.4H. +1.8z.2X. +2.6g.5N. +2.6L.5l. +2.6N.5i. +2.6v.5w. +3.6R.55. +6.6O.5e. +8.6z.5u. +9.6P.5c. +1.6R.56. +1.7d.4L. +1.7g.4I. +2.4G.7j. +2.6J.5n. +2.6N.5j. +2.6P.5d. +4.7t.4u. +6.85.3f. +9.6v.5x. +9.7A.4k. +d.7v.4r. +0.2S.8C. +0.6P.5e. +1.4G.7k. +1.7d.4M. +1.7g.4J. +2.7A.4l. +6.6R.57. +9.7t.4v. +d.7v.4s. +0.7Z.3v. +1.4G.7l. +1.5s.6G. +1.6L.5n. +1.6N.5k. +1.7d.4N. +1.7g.4K. +2.6P.5f. +6.6B.5u. +8.58.6R. +9.6v.5y. +9.7A.4m. +9.7t.4w. +d.7v.4t. +1.6L.5o. +1.7A.4n. +1.7d.4O. +1.7g.4L. +2.6R.59. +5.6P.5g. +9.6v.5z. +1.6P.5h. +1.7g.4M. +2.6L.5p. +2.6R.5a. +4.7t.4x. +6.85.3g. +9.6x.5x. +9.7A.4o. +b.3G.7V. +d.7v.4u. +1.4G.7o. +1.6L.5q. +1.7d.4Q. +1.7g.4N. +2.6N.5l. +2.6P.5i. +2.6R.5b. +3.6V.53. +6.3y.7Z. +9.6v.5A. +9.7A.4p. +9.7t.4y. +b.5F.6o. +d.7v.4v. +0.5u.6E. +1.6v.5B. +1.7d.4R. +1.7g.4O. +1.8z.2Z. +2.6P.5j. +3.5G.6o. +6.5s.6H. +9.6x.5y. +9.7t.4z. +b.5F.6p. +d.7v.4w. +0.7A.4q. +1.5s.6I. +1.6g.5R. +1.7d.4S. +2.6J.5r. +2.7g.4P. +3.5G.6p. +3.6V.55. +4.7t.4A. +5.2q.9S. +6.85.3h. +9.6x.5z. +0.6J.5s. +1.6N.5n. +1.7g.4Q. +2.6z.5v. +5.2q.9T. +6.6P.5k. +6.6R.5c. +6.85.3i. +9.7A.4r. +9.7t.4B. +d.7v.4x. +1.4G.7q. +1.5s.6K. +1.6L.5r. +1.6N.5o. +1.7g.4R. +2.6R.5d. +2.6z.5w. +9.6v.5C. +9.6x.5A. +9.7A.4s. +9.7t.4C. +d.7v.4y. +1.4G.7r. +1.6L.5s. +1.7g.4S. +2.6N.5p. +3.6R.5e. +8.58.6V. +8.6z.5x. +9.6v.5D. +9.7A.4t. +9.7t.4D. +b.3J.7V. +b.5F.6s. +d.2U.8C. +d.7v.4z. +1.6N.5q. +1.7d.4V. +2.6B.5v. +2.6P.5l. +2.6R.5f. +2.7t.4E. +3.3K.7V. +3.5G.6s. +d.7v.4A. +1.4G.7s. +1.7d.4W. +2.6B.5w. +2.6g.5S. +2.6v.5E. +2.7g.4T. +5.2q.9U. +6.3L.7V. +6.85.3m. +8.6z.5y. +9.6R.5g. +9.7A.4u. +9.7t.4F. +b.5F.6u. +b.6+.53. +d.7v.4B. +1.6R.5h. +2.6J.5t. +2.7g.4U. +3.3M.7V. +3.5G.6u. +8.6z.5z. +9.6x.5C. +9.7A.4v. +9.7t.4G. +b.5F.6v. +d.7v.4C. +1.6P.5n. +1.7g.4V. +2.6R.5i. +3.5G.6v. +6.3N.7V. +6.6M.5s. +9.6x.5D. +9.7A.4w. +b.6+.55. +d.7v.4D. +0.5u.6H. +1.4G.7u. +1.6N.5r. +1.7d.4X. +1.7g.4W. +2.6L.5t. +2.6R.5j. +2.7v.4E. +5.2q.9V. +5.6P.5o. +8.6z.5A. +a.2V.8C. +b.3G.7Z. +d.71.53. +1.6z.5B. +1.7d.4Y. +2.5s.6N. +2.6P.5p. +3.2W.8C. +6.85.3q. +9.7A.4x. +9.7t.4H. +d.7v.4F. +0.5u.6J. +2.6v.5H. +5.2q.9W. +5.6P.5q. +6.6R.5k. +6.85.3r. +8.58.6+. +9.7A.4y. +9.7t.4I. +d.71.55. +d.7v.4G. +h.5F.6x. +1.4G.7w. +1.7g.4X. +3.6V.5e. +6.6v.5I. +9.7A.4z. +9.7t.4J. +h.5G.6x. +1.6g.5T. +1.6L.5u. +1.7d.4Z. +1.7g.4Y. +5.2q.9X. +8.6z.5C. +9.7A.4A. +9.7t.4K. +2.6R.5l. +6.6O.5s. +8.6z.5D. +9.7A.4B. +9.7t.4L. +b.3R.7V. +d.71.58. +d.7v.4H. +1.6P.5r. +2.6N.5t. +6.3S.7V. +9.7A.4C. +9.7t.4M. +a.2Y.8C. +b.3J.7Z. +d.7v.4I. +0.6P.5s. +1.4G.7y. +1.7d.4+. +1.7g.4Z. +2.6z.5E. +3.3K.7Z. +7.6x.5I. +9.7A.4D. +9.7t.4N. +d.7v.4J. +0.5u.6M. +1.6B.5D. +1.6R.5n. +1.7d.4/. +2.7A.4E. +4.7t.4O. +6.3L.7Z. +6.85.3v. +8.6z.5F. +d.7v.4K. +1.4G.7z. +1.7d.50. +2.7t.4P. +3.3M.7Z. +6.6R.5o. +8.6z.5G. +9.7A.4F. +d.7v.4L. +1.7d.51. +1.7g.4+. +2.6B.5E. +2.6R.5p. +6.3N.7Z. +9.7A.4G. +9.7t.4Q. +b.6+.5e. +d.7v.4M. +1.7d.52. +1.7g.4/. +4.7t.4R. +6.85.3y. +9.6R.5q. +b.5F.6B. +d.7v.4N. +1.7g.50. +2.6J.5w. +2.6P.5t. +2.6z.5H. +3.5G.6B. +4.7t.4S. +d.7v.4O. +1.6g.5U. +1.7d.54. +1.7g.51. +2.6J.5x. +2.6L.5v. +2.7v.4P. +8.6z.5I. +9.7A.4H. +d.71.5e. +0.5u.6O. +0.7V.3Y. +1.4G.7B. +1.7g.52. +2.6L.5w. +2.7t.4T. +9.7A.4I. +d.7v.4Q. +1.4G.7C. +1.5s.6Q. +1.6g.5V. +1.6L.5x. +1.6R.5r. +1.7d.56. +2.6B.5H. +2.6J.5y. +2.7t.4U. +8.2+.8C. +9.7A.4J. +b.5F.6E. +d.7v.4R. +0.6P.5u. +1.7d.57. +1.7g.54. +2.6J.5z. +3.5G.6E. +3.6R.5s. +4.7t.4V. +9.7A.4K. +b.3R.7Z. +d.7v.4S. +1.4G.7E. +1.6L.5y. +1.7d.58. +1.7t.4W. +6.3S.7Z. +9.7A.4L. +1.5s.6T. +1.6L.5z. +1.7g.56. +2.2/.8C. +2.6v.5J. +2.7v.4T. +9.7A.4M. +0.7V.40. +1.7g.57. +2.6g.5W. +2.6J.5B. +2.6N.5v. +2.7v.4U. +9.7A.4N. +1.4G.7F. +1.6L.5A. +1.7g.58. +2.6g.5X. +2.6N.5w. +2.6v.5K. +4.7t.4X. +9.7A.4O. +d.7v.4V. +1.6L.5B. +1.6N.5x. +1.7v.4W. +2.6g.5Y. +2.6R.5t. +2.7A.4P. +2.7g.59. +4.7t.4Y. +b.3G.85. +0.30.8C. +1.4G.7G. +1.8z.35. +2.7g.5a. +9.7A.4Q. +1.5s.6U. +1.6N.5y. +1.7d.5c. +1.8z.36. +2.6g.5+. +2.6J.5D. +2.7g.5b. +9.7A.4R. +1.4G.7I. +1.6L.5C. +1.6N.5z. +1.7t.4Z. +3.6V.5s. +9.7A.4S. +b.5F.6H. +d.7v.4X. +0.7Z.3Y. +1.5s.6W. +1.6L.5D. +2.6J.5E. +2.6P.5v. +3.5G.6H. +3.6R.5u. +d.7v.4Y. +1.5s.6X. +1.6N.5A. +1.7g.5c. +2.6P.5w. +2.7A.4T. +b.5F.6J. +1.5s.6Y. +1.6N.5B. +1.7d.5g. +2.6L.5E. +2.7A.4U. +2.7g.5d. +3.5G.6J. +9.6P.5x. +9.7t.4+. +b.3J.85. +1.5s.6Z. +1.6L.5F. +1.7d.5h. +1.7v.4Z. +1.8z.38. +2.6g.5/. +2.6z.5J. +3.3K.85. +9.7A.4V. +9.7t.4/. +1.6L.5G. +1.7A.4W. +2.6g.60. +2.7g.5f. +6.85.3L. +9.6P.5y. +9.7t.50. +0.7Z.40. +1.6g.61. +1.6N.5C. +1.7g.5g. +2.6z.5K. +3.3M.85. +4.7t.51. +9.6P.5z. +1.31.8C. +1.6g.62. +1.6N.5D. +1.7g.5h. +2.6B.5J. +2.6J.5I. +6.85.3N. +9.7t.52. +b.6+.5s. +d.7v.4+. +0.7t.53. +1.5s.6/. +1.7d.5k. +2.6L.5H. +2.7g.5i. +6.5L.6o. +6.6P.5A. +9.7A.4X. +b.5F.6M. +d.7v.4/. +1.5s.70. +1.6L.5I. +1.6P.5B. +2.6B.5K. +2.6N.5E. +2.7g.5j. +3.5G.6M. +3.6V.5u. +6.4c.7V. +6.5L.6p. +9.7A.4Y. +9.7t.54. +d.7v.50. +0.55.7t. +1.8z.3c. +2.6g.63. +2.6R.5v. +d.71.5s. +d.7v.51. +1.5s.72. +1.7g.5k. +1.7t.56. +2.6R.5w. +a.32.8C. +d.7v.52. +1.5s.73. +1.7A.4Z. +2.4G.7K. +4.7t.57. +6.6R.5x. +9.6P.5C. +1.5s.74. +2.6g.64. +6.5L.6s. +8.58.7t. +9.6P.5D. +b.3R.85. +d.7v.54. +1.33.8C. +1.5s.75. +1.7d.5n. +2.6N.5H. +2.7g.5l. +2.7t.59. +6.3S.85. +9.6R.5y. +b.5F.6O. +1.5s.76. +1.6g.65. +1.6N.5I. +1.7d.5o. +1.7v.56. +2.6P.5E. +2.7t.5a. +3.5G.6O. +6.5L.6u. +9.6R.5z. +9.7A.4+. +f.34.8C. +1.5s.77. +2.7t.5b. +6.5L.6v. +9.7A.4/. +b.5F.6P. +b.6+.5u. +d.7v.57. +0.7V.4j. +1.5s.78. +1.7d.5q. +1.7g.5n. +2.6g.66. +3.5G.6P. +9.6R.5A. +9.7A.50. +d.7v.58. +1.5s.79. +1.6g.67. +1.6R.5B. +1.7g.5o. +2.4G.7M. +2.6v.5M. +2.7v.59. +9.7A.51. +1.5s.7a. +2.6g.68. +2.6v.5N. +2.7g.5p. +2.7v.5a. +9.7A.52. +9.7t.5c. +d.71.5u. +0.7A.53. +1.5s.7b. +1.7g.5q. +2.6P.5H. +2.7t.5d. +2.7v.5b. +6.4c.7Z. +h.5L.6x. +0.7t.5e. +1.5s.7c. +1.7d.5r. +1.98.2u. +5.6P.5I. +9.6R.5C. +9.6v.5O. +9.7A.54. +c.37.8C. +0.7A.55. +1.5s.7d. +1.6g.69. +2.6J.5J. +2.7t.5f. +2.98.2v. +6.85.3Y. +9.6R.5D. +9.6v.5P. +1.5s.7e. +1.6g.6a. +1.7A.56. +1.8z.3j. +4.7t.5g. +d.7v.5c. +1.5s.7f. +1.7g.5r. +1.7t.5h. +1.8z.3k. +2.6J.5K. +2.6L.5J. +2.6R.5E. +2.7v.5d. +7.6v.5Q. +9.7A.57. +0.39.8C. +1.5s.7g. +1.6g.6b. +1.8z.3l. +2.7t.5i. +6.4q.7V. +6.58.7A. +9.6x.5O. +b.5F.6R. +1.5s.7h. +2.6L.5K. +2.7A.59. +2.7t.5j. +2.7v.5f. +3.5G.6R. +8.6z.5L. +9.6x.5P. +1.8z.3n. +2.7A.5a. +6.85.40. +9.6v.5R. +d.7v.5g. +0.7Z.4j. +1.5s.7i. +1.7v.5h. +1.8z.3o. +2.6z.5M. +2.7A.5b. +7.6x.5Q. +9.7t.5k. +1.8z.3p. +2.4G.7O. +2.5s.7j. +2.6R.5H. +2.6z.5N. +2.7v.5i. +6.5L.6B. +0.3e.8C. +1.5s.7k. +2.6N.5J. +2.7g.5t. +2.7v.5j. +6.6R.5I. +1.5s.7l. +2.4G.7P. +2.6B.5M. +2.7t.5l. +8.6z.5O. +9.6x.5R. +9.7A.5c. +1.5s.7m. +1.8z.3s. +2.6B.5N. +2.6N.5K. +2.6v.5S. +2.7A.5d. +8.6z.5P. +c.3f.8C. +d.7v.5k. +0.7A.5e. +1.5s.7n. +1.6g.6c. +6.5L.6E. +b.5F.6V. +1.5s.7o. +1.6B.5O. +1.7t.5n. +2.7A.5f. +3.5G.6V. +8.6z.5Q. +1.6B.5P. +1.8z.3t. +2.4G.7Q. +2.7v.5l. +9.7A.5g. +9.7t.5o. +0.3g.8C. +1.7A.5h. +1.8z.3u. +2.6P.5J. +2.7t.5p. +6.4q.7Z. +1.5s.7p. +1.6B.5Q. +2.7A.5i. +4.7t.5q. +8.6z.5R. +1.5s.7q. +1.7v.5n. +1.8z.3w. +2.6P.5K. +2.7A.5j. +0.3h.8C. +1.5s.7r. +1.8z.3x. +6.6v.5T. +d.7v.5o. +1.4G.7T. +2.7v.5p. +6.3i.8C. +9.7A.5k. +b.6+.5F. +1.5s.7s. +1.7t.5r. +2.6g.6d. +2.8z.3z. +6.4c.85. +b.6+.5G. +d.7v.5q. +1.4G.7U. +1.7d.5x. +1.8z.3A. +2.6g.6e. +2.6z.5S. +6.5L.6H. +6.5s.7t. +2.7A.5l. +2.7g.5v. +7.6x.5T. +d.71.5F. +1.5s.7u. +1.6g.6f. +1.7d.5y. +1.8S.2X. +1.8z.3B. +2.7g.5w. +6.3m.8C. +6.5L.6J. +d.71.5G. +1.6g.6g. +1.7d.5z. +1.7g.5x. +1.7v.5r. +1.8z.3C. +2.6B.5S. +2.6R.5J. +1.5s.7v. +1.6L.5L. +1.7A.5n. +2.6J.5M. +2.8z.3D. +1.5s.7w. +1.7d.5A. +1.7g.5y. +1.8z.3E. +2.6J.5N. +2.6R.5K. +2.7t.5t. +9.7A.5o. +0.3q.8C. +1.5s.7x. +1.7d.5B. +1.7g.5z. +1.8z.3F. +2.6L.5M. +2.7A.5p. +6.6v.5U. +1.6g.6h. +2.6J.5O. +2.6L.5N. +6.3r.8C. +6.85.4j. +8.6z.5T. +9.7A.5q. +1.7g.5A. +2.6J.5P. +6.5L.6M. +6.6v.5V. +0.5u.7t. +1.5s.7y. +1.6L.5O. +1.7d.5C. +1.7g.5B. +2.7v.5t. +1.6g.6i. +1.6L.5P. +1.7d.5D. +2.6J.5Q. +7.6x.5U. +1.4G.7Y. +1.5s.7z. +1.6g.6j. +1.7A.5r. +0.7A.5s. +1.6L.5Q. +1.7g.5C. +1.8z.3H. +2.6g.6k. +2.6N.5M. +2.6v.5W. +7.6x.5V. +1.6g.6l. +1.7g.5D. +1.8z.3I. +2.6N.5N. +2.6v.5X. +6.3v.8C. +2.6v.5Y. +6.5L.6O. +1.6L.5R. +1.6N.5O. +2.7g.5E. +6.6v.5Z. +6.85.4q. +1.5s.7B. +1.6N.5P. +2.6g.6m. +2.6v.5+. +6.3y.8C. +6.5L.6P. +8.6z.5U. +1.5s.7C. +2.7A.5t. +1.5s.7D. +1.6N.5Q. +1.7d.5I. +2.6P.5M. +2.7t.5v. +8.6z.5V. +1.5s.7E. +1.8z.3O. +2.6P.5N. +2.7t.5w. +7.6x.5Z. +2.6L.5S. +2.7g.5H. +2.8z.3P. +9.7t.5x. +0.7A.5u. +1.6N.5R. +1.7g.5I. +2.6v.5/. +6.6P.5O. +1.5s.7F. +1.8z.3Q. +2.6v.60. +2.6z.5W. +2.7v.5v. +6.6P.5P. +9.7t.5y. +1.9i.2u. +2.6z.5X. +2.7v.5w. +6.6v.61. +9.7t.5z. +1.5s.7G. +2.6z.5Y. +2.9i.2v. +5.6P.5Q. +6.6v.62. +d.7v.5x. +1.5s.7H. +2.6B.5W. +3.6R.5L. +8.6z.5Z. +9.7t.5A. +1.5s.7I. +1.7t.5B. +1.8z.3T. +2.6B.5X. +2.6N.5S. +2.6z.5+. +b.3G.8C. +d.7v.5y. +1.5s.7J. +1.6L.5T. +1.8z.3U. +2.6B.5Y. +2.6R.5M. +2.6v.63. +7.6x.61. +9.6P.5R. +d.7v.5z. +1.8z.3V. +2.6R.5N. +7.6x.62. +1.8z.3W. +2.6B.5+. +9.7t.5C. +d.7v.5A. +1.4G.82. +1.7v.5B. +1.8z.3X. +2.6v.64. +2.7A.5v. +9.6R.5O. +9.7t.5D. +1.4G.83. +2.6z.5/. +2.7A.5w. +9.6R.5P. +1.4G.84. +2.6P.5S. +2.6z.60. +2.7t.5E. +7.6v.65. +9.7A.5x. +b.3J.8C. +1.6N.5T. +3.3K.8C. +3.6V.5L. +8.6z.61. +9.6R.5Q. +b.5F.7t. +d.7v.5C. +2.6B.5/. +2.6v.66. +3.5G.7t. +6.3L.8C. +8.6z.62. +9.7A.5y. +d.7v.5D. +0.7V.53. +1.8z.3Z. +2.6B.60. +3.3M.8C. +7.6v.67. +9.7A.5z. +1.6L.5U. +1.8z.3+. +2.6J.5V. +2.6v.68. +2.7g.5J. +2.7v.5E. +6.3N.8C. +7.6x.65. +9.6R.5R. +0.7V.55. +1.8z.3/. +2.6z.63. +2.7t.5H. +9.7A.5A. +1.4G.88. +1.6L.5V. +1.7A.5B. +2.5s.7K. +2.7g.5K. +9.7t.5I. +1.8z.41. +5.6P.5T. +7.6v.69. +h.6x.67. +1.8z.42. +2.6B.63. +2.6J.5W. +2.6z.64. +6.58.7V. +6.6v.6a. +b.6+.5L. +1.8z.43. +2.6R.5S. +2.7v.5H. +9.7A.5C. +1.6N.5U. +1.6v.6b. +1.8z.44. +2.6J.5Y. +2.6L.5W. +8.6z.65. +9.7A.5D. +b.3R.8C. +d.7v.5I. +1.4G.8d. +1.5s.7L. +1.8z.45. +2.6B.64. +2.6J.5Z. +2.6L.5X. +6.3S.8C. +7.6x.69. +d.71.5L. +1.6N.5V. +1.8z.46. +2.5s.7M. +2.6L.5Y. +2.6z.66. +2.7A.5E. +7.6x.6a. +1.4G.8f. +1.6L.5Z. +1.8z.47. +8.6z.67. +b.5F.7A. +0.7Z.53. +1.8z.48. +2.4G.8g. +2.6L.5+. +2.6z.68. +3.5G.7A. +1.8z.49. +2.4G.8h. +2.6B.66. +0.7V.5e. +0.7Z.55. +1.8z.4a. +2.6N.5W. +5.6P.5U. +9.6R.5T. +1.8z.4b. +2.6B.68. +2.6J.5/. +2.6N.5X. +2.7A.5H. +8.6z.69. +2.6J.60. +2.6N.5Y. +5.6P.5V. +6.6v.6c. +8.6z.6a. +9.7A.5I. +0.8C.3Y. +1.6N.5Z. +2.6J.61. +2.6L.5/. +6.58.7Z. +1.4G.8j. +1.6B.69. +1.6z.6b. +1.8S.3c. +2.6L.60. +2.6N.5+. +1.6g.6n. +1.6L.61. +1.8z.4d. +2.7t.5J. +1.6L.62. +2.6P.5W. +7.6x.6c. +1.8z.4e. +2.5s.7O. +2.6P.5X. +2.7t.5K. +0.8C.40. +1.6g.6q. +1.8z.4f. +2.6P.5Y. +1.8z.4g. +2.5s.7P. +2.6L.63. +2.6N.5/. +2.7v.5J. +5.6P.5Z. +9.6R.5U. +1.4G.8m. +1.6g.6r. +1.8z.4h. +2.6N.60. +2.6P.5+. +2.6v.6d. +1.4G.8n. +1.6N.61. +1.8z.4i. +1.98.2T. +2.6v.6e. +2.7v.5K. +9.6R.5V. +f.7Z.5e. +1.4G.8o. +1.6g.6t. +1.6N.62. +1.7d.5O. +2.6L.64. +2.7g.5M. +8.6z.6c. +1.7d.5P. +1.8z.4k. +2.5s.7Q. +2.7g.5N. +6.6v.6f. +1.6L.65. +2.8z.4l. +6.6v.6g. +1.6B.6c. +1.7d.5Q. +1.7g.5O. +1.8z.4m. +2.6N.63. +2.6P.5/. +2.6R.5W. +1.5s.7S. +1.7g.5P. +1.8z.4n. +2.6L.66. +2.6P.60. +2.6R.5X. +1.6g.6w. +1.6L.67. +1.8S.3j. +1.8z.4o. +2.6R.5Y. +2.7A.5J. +5.6P.61. +7.6x.6f. +1.5s.7T. +1.6v.6h. +1.7d.5R. +1.7g.5Q. +1.8S.3k. +1.8z.4p. +2.6L.68. +2.6N.64. +5.6P.62. +6.6R.5Z. +7.6x.6g. +1.6g.6y. +1.8S.3l. +2.6R.5+. +2.7A.5K. +6.85.53. +1.5s.7U. +1.6N.65. +2.6z.6d. +0.7V.5s. +1.6L.69. +1.6v.6i. +1.7g.5R. +1.8z.4r. +2.6P.63. +2.6z.6e. +6.85.55. +1.6L.6a. +1.6v.6j. +1.8z.4s. +1.98.2X. +2.6J.6b. +2.6N.66. +6.4c.8C. +1.6N.67. +1.8z.4t. +2.6B.6d. +2.6v.6k. +8.6z.6f. +1.5s.7X. +1.6L.6b. +1.6v.6l. +2.6B.6e. +2.6N.68. +2.6P.64. +2.6R.5/. +8.58.85. +8.6z.6g. +1.8z.4u. +2.6R.60. +1.6g.6A. +1.8z.4v. +2.7g.5S. +5.6P.65. +6.6R.61. +1.4G.8t. +1.6N.69. +1.8z.4w. +2.6v.6m. +6.5L.7t. +6.6R.62. +1.6N.6a. +1.6z.6h. +2.6P.66. +1.4G.8u. +1.6g.6C. +1.8z.4x. +1.98.2Z. +2.7t.5M. +2.8S.3t. +5.6P.67. +0.5u.7V. +0.8C.4j. +1.6g.6D. +1.6N.6b. +1.7d.5T. +1.8z.4y. +2.6P.68. +2.6R.63. +2.7t.5N. +2.8S.3u. +1.5s.7Y. +1.6z.6i. +1.8z.4z. +0.7Z.5s. +1.6L.6c. +1.6z.6j. +1.8z.4A. +6.85.5e. +9.7t.5O. +1.6g.6F. +1.7g.5T. +1.8z.4B. +2.6R.64. +2.6z.6k. +2.7v.5M. +5.6P.69. +9.7t.5P. +1.6B.6i. +1.6z.6l. +1.8z.4C. +2.7v.5N. +5.6P.6a. +1.8z.4D. +2.8S.3z. +6.6R.65. +9.7t.5Q. +1.4G.8y. +1.5s.7+. +1.6g.6G. +1.6P.6b. +2.6B.6k. +2.8z.4E. +d.7v.5O. +1.8z.4F. +2.6R.66. +2.6z.6m. +d.7v.5P. +1.6N.6c. +1.8z.4G. +6.4q.8C. +9.6R.67. +9.7t.5R. +1.7d.5U. +2.6R.68. +6.5L.7A. +d.7v.5Q. +1.5s.7/. +2.6B.6m. +2.6L.6d. +2.8S.3D. +0.5u.7Z. +1.6g.6I. +1.7d.5V. +1.8z.4H. +2.6L.6e. +2.7A.5M. +1.5s.80. +1.7g.5U. +1.8S.3F. +1.8z.4I. +2.7A.5N. +6.6R.69. +d.7v.5R. +1.6L.6f. +1.8z.4J. +2.7t.5S. +9.6R.6a. +1.6L.6g. +1.7g.5V. +1.8z.4K. +5.6P.6c. +9.7A.5O. +1.6R.6b. +1.8z.4L. +9.7A.5P. +1.8z.4M. +2.6N.6d. +1.8z.4N. +2.6N.6e. +2.7v.5S. +9.7A.5Q. +1.6L.6h. +1.7d.5Z. +1.8z.4O. +2.7g.5W. +1.5s.81. +1.6N.6f. +2.7g.5X. +2.8z.4P. +1.5s.82. +1.6N.6g. +1.8z.4Q. +2.7g.5Y. +4.7t.5T. +9.7A.5R. +1.5s.83. +1.6L.6i. +1.7g.5Z. +1.8z.4R. +2.6J.6k. +1.5s.84. +1.6L.6j. +1.8z.4S. +2.6P.6d. +2.7g.5+. +2.6L.6k. +2.6P.6e. +6.85.5s. +9.6R.6c. +1.5s.86. +1.6L.6l. +1.6N.6h. +2.8z.4T. +b.5F.7V. +d.7v.5T. +1.9i.2T. +2.6J.6m. +2.7A.5S. +2.8z.4U. +3.5G.7V. +5.6P.6f. +1.7d.61. +1.8z.4V. +2.8S.3P. +5.6P.6g. +1.5s.87. +1.6N.6i. +1.7d.62. +1.8z.4W. +2.6L.6m. +2.7g.5/. +6.6o.6o. +1.5s.88. +1.6N.6j. +2.7g.60. +6.6o.6p. +1.5s.89. +1.7g.61. +2.6N.6k. +4.7t.5U. +6.6p.6p. +1.5s.8a. +1.6N.6l. +1.6P.6h. +1.7g.62. +1.8z.4X. +1.98.35. +1.5s.8b. +1.8z.4Y. +1.98.36. +2.6R.6d. +4.7t.5V. +1.5s.8c. +1.8S.3T. +2.6R.6e. +6.6s.6o. +6.85.5u. +9.7A.5T. +1.5s.8d. +1.6P.6i. +2.6N.6m. +2.7g.63. +6.6s.6p. +d.7v.5U. +1.5s.8e. +1.6g.6Q. +1.6P.6j. +1.8z.4Z. +2.8S.3V. +6.6o.6u. +6.6R.6f. +9.6v.6n. +0.6v.6o. +1.5s.8f. +1.7d.65. +1.8S.3W. +2.6P.6k. +2.7t.5W. +6.6p.6u. +6.6R.6g. +b.5F.7Z. +d.7v.5V. +0.6v.6p. +1.6P.6l. +1.8S.3X. +1.98.38. +1.9i.2X. +2.5s.8g. +2.7g.64. +2.7t.5X. +3.5G.7Z. +1.6g.6T. +1.6v.6q. +1.8z.4+. +2.5s.8h. +2.7t.5Y. +6.6s.6s. +1.7d.67. +1.7g.65. +1.8z.4/. +4.7t.5Z. +9.6x.6n. +1.6R.6h. +1.8z.50. +2.6P.6m. +2.7t.5+. +2.7v.5W. +6.6s.6u. +9.6v.6r. +h.6o.6x. +1.5s.8i. +1.8z.51. +1.98.3a. +2.7g.66. +2.7v.5X. +6.6s.6v. +h.6p.6x. +0.6u.6u. +1.7g.67. +1.8S.3Z. +1.8z.52. +1.98.3b. +2.7v.5Y. +9.6v.6t. +9.7A.5U. +0.6v.6u. +1.5s.8j. +1.6g.6U. +1.6R.6i. +1.7d.69. +1.8S.3+. +1.98.3c. +2.7g.68. +d.7v.5Z. +1.6R.6j. +1.7d.6a. +1.8z.54. +1.98.3d. +1.9i.2Z. +2.7v.5+. +6.6v.6v. +9.6x.6r. +9.7A.5V. +1.5s.8k. +2.6R.6k. +2.7t.5/. +8.6z.6n. +h.6s.6x. +1.5s.8l. +1.6g.6X. +1.6R.6l. +1.7d.6b. +1.7g.69. +1.8S.41. +1.8z.56. +2.7t.60. +8.6z.6o. +9.6x.6t. +1.6g.6Y. +1.6v.6w. +1.7g.6a. +1.8S.42. +1.8z.57. +8.6z.6p. +9.7t.61. +h.6u.6x. +0.6v.6x. +1.6g.6Z. +1.6z.6q. +1.8z.58. +2.7A.5W. +9.7t.62. +1.5s.8m. +1.7g.6b. +1.8S.44. +2.6R.6m. +2.7A.5X. +2.7v.5/. +2.8z.59. +6.6B.6o. +9.6v.6y. +1.5s.8n. +1.8S.45. +2.7A.5Y. +2.7v.60. +2.8z.5a. +6.6B.6p. +8.6z.6r. +1.5s.8o. +1.8S.46. +2.7t.63. +2.8z.5b. +8.6z.6s. +9.7A.5Z. +d.7v.61. +1.5s.8p. +1.6g.6/. +2.4G.8E. +2.7A.5+. +8.6z.6t. +d.7v.62. +h.6x.6x. +1.6g.70. +1.8S.48. +6.6o.6E. +8.6z.6u. +9.6x.6y. +1.5s.8q. +1.7d.6c. +1.8z.5c. +2.7t.64. +6.6p.6E. +6.6s.6B. +8.6z.6v. +1.8S.4a. +2.7v.63. +2.8z.5d. +1.8S.4b. +1.98.3j. +6.6B.6u. +9.6v.6A. +9.7t.65. +0.6v.6B. +1.6z.6w. +1.7g.6c. +1.98.3k. +2.7A.5/. +2.8z.5f. +b.5F.85. +1.8z.5g. +1.98.3l. +2.4G.8F. +2.7A.60. +2.7t.66. +2.7v.64. +3.5G.85. +6.6s.6E. +9.6x.6z. +1.5s.8r. +1.6g.76. +1.8z.5h. +8.6z.6y. +9.6v.6C. +9.7A.61. +9.7t.67. +0.6u.6E. +1.5s.8s. +1.98.3n. +2.7t.68. +2.8z.5i. +7.6v.6D. +9.6x.6A. +9.7A.62. +d.7v.65. +0.6v.6E. +0.8C.53. +1.4G.8G. +1.98.3o. +2.8z.5j. +h.6B.6x. +1.4G.8H. +1.8S.4e. +1.98.3p. +2.7v.66. +6.6o.6H. +0.55.8C. +1.8S.4f. +1.8z.5k. +2.7A.63. +6.6p.6H. +6.6v.6F. +9.6x.6C. +9.7t.69. +d.7v.67. +1.4G.8I. +1.6g.7b. +2.7v.68. +6.6o.6J. +7.6x.6D. +8.6z.6z. +9.7t.6a. +1.4G.8J. +1.6L.6n. +1.7d.6f. +1.98.3s. +2.7g.6d. +6.6p.6J. +h.6E.6x. +1.4G.8K. +1.5s.8t. +1.6L.6o. +1.7d.6g. +1.7t.6b. +2.7A.64. +2.7g.6e. +2.8z.5l. +6.58.8C. +6.6v.6G. +8.6z.6A. +1.6g.7e. +1.6L.6p. +6.5L.7V. +6.6s.6H. +7.6x.6F. +8.6z.6B. +d.7v.69. +1.5s.8u. +1.6g.7f. +1.6L.6q. +1.7g.6f. +1.98.3t. +9.7A.65. +d.7v.6a. +0.6u.6H. +1.5s.8v. +1.7g.6g. +1.8z.5n. +1.98.3u. +2.8S.4l. +6.6s.6J. +8.6z.6C. +0.6v.6H. +1.5s.8w. +1.6g.7h. +1.6L.6r. +1.7d.6h. +1.7v.6b. +1.8S.4m. +1.8z.5o. +2.7A.66. +6.6B.6B. +6.6o.6M. +7.6x.6G. +8.6z.6D. +0.6u.6J. +1.6L.6s. +1.6N.6n. +1.8S.4n. +1.98.3w. +2.8z.5p. +6.6p.6M. +8.6z.6E. +9.6v.6I. +9.7A.67. +0.6v.6J. +1.6g.7i. +1.6L.6t. +1.8z.5q. +1.98.3x. +2.7A.68. +1.5s.8x. +1.6L.6u. +1.7d.6i. +1.7g.6h. +2.6g.7j. +8.6z.6F. +9.6v.6K. +0.8C.5e. +1.6g.7k. +1.6L.6v. +1.6N.6q. +1.7d.6j. +2.98.3z. +6.6B.6E. +9.7t.6c. +h.6H.6x. +1.5s.8y. +1.98.3A. +1.9i.35. +6.6s.6M. +9.6x.6I. +9.7A.69. +1.6B.6F. +1.6N.6r. +1.7d.6l. +1.7g.6i. +1.8z.5r. +1.9i.36. +6.6o.6O. +8.6z.6G. +9.7A.6a. +h.6J.6x. +1.5s.8z. +1.6g.7n. +1.6L.6w. +1.7g.6j. +1.98.3B. +6.6M.6u. +6.6P.6n. +6.6p.6O. +9.6x.6K. +0.6P.6o. +0.6v.6M. +1.6g.7o. +1.6L.6x. +1.6N.6t. +1.7A.6b. +1.98.3C. +2.7g.6k. +6.5L.7Z. +6.6E.6E. +d.7v.6c. +0.6P.6p. +1.6L.6y. +1.7g.6l. +2.98.3D. +1.6P.6q. +1.8S.4u. +1.98.3E. +8.6z.6H. +k.6N.6v. +1.8S.4v. +1.98.3F. +1.9i.38. +6.6s.6O. +8.6z.6I. +1.6g.7q. +2.7g.6m. +2.7t.6d. +2.8z.5t. +6.6P.6r. +8.6z.6J. +h.6M.6x. +1.6g.7r. +1.6N.6w. +2.7t.6e. +6.6B.6H. +6.6O.6u. +6.6s.6P. +8.6z.6K. +0.6v.6O. +6.6P.6t. +8.6z.6L. +0.6P.6u. +1.6N.6y. +1.8S.4y. +1.9i.3a. +6.6B.6J. +9.7t.6f. +0.6v.6P. +1.6L.6A. +1.9i.3b. +2.7v.6d. +6.6R.6n. +9.7A.6c. +9.7t.6g. +1.6L.6B. +1.8S.4A. +1.98.3H. +1.9i.3c. +2.7v.6e. +3.6R.6o. +6.6E.6H. +1.8S.4B. +1.98.3I. +1.9i.3d. +3.6R.6p. +8.6z.6M. +h.6O.6x. +0.6J.6E. +1.4G.8Q. +1.5s.8B. +1.6L.6C. +1.6P.6w. +1.6R.6q. +1.8S.4C. +d.7v.6f. +1.6L.6D. +1.6N.6z. +1.7t.6h. +d.7v.6g. +h.6P.6x. +1.6L.6E. +2.8S.4E. +6.6B.6M. +6.6P.6y. +6.6R.6r. +1.6g.7x. +1.6N.6A. +3.6R.6s. +1.6L.6F. +1.7t.6i. +2.6B.6N. +9.6R.6t. +1.7t.6j. +1.7v.6h. +1.98.3O. +2.7A.6d. +3.6R.6u. +7.6v.6Q. +8.6z.6O. +1.6g.7y. +1.6N.6C. +2.7A.6e. +2.7t.6k. +2.8z.5v. +2.98.3P. +3.6R.6v. +3.6V.6o. +6.5s.8C. +6.6M.6E. +0.6H.6H. +1.6L.6G. +1.6N.6D. +1.7t.6l. +2.8z.5w. +3.6V.6p. +8.6z.6P. +1.6g.7z. +1.7v.6i. +1.8z.5x. +1.98.3Q. +6.6B.6O. +6.6v.6T. +9.7A.6f. +0.6J.6H. +1.6R.6w. +1.7v.6j. +6.6P.6A. +7.6x.6Q. +9.7A.6g. +1.6N.6F. +1.8z.5y. +2.7t.6m. +2.7v.6k. +6.5L.85. +6.6B.6P. +h.6R.6x. +0.6J.6J. +1.6L.6H. +1.7v.6l. +1.8z.5z. +1.9i.3j. +3.6V.6s. +6.6R.6y. +1.6L.6I. +1.98.3T. +1.9i.3k. +6.6O.6E. +7.6x.6T. +9.6P.6C. +1.6g.7B. +1.6L.6J. +1.6N.6G. +1.7A.6h. +1.8z.5A. +1.98.3U. +1.9i.3l. +3.6V.6u. +5.6P.6D. +6.6v.6U. +b.6+.6o. +0.6P.6E. +1.6L.6K. +1.8z.5B. +1.98.3V. +2.7v.6m. +3.6V.6v. +b.6+.6p. +0.5u.8C. +1.6L.6L. +1.98.3W. +1.9i.3n. +2.8S.4P. +6.6M.6H. +6.6v.6W. +8.6z.6Q. +1.7A.6i. +1.98.3X. +1.9i.3o. +5.6P.6F. +6.6v.6X. +8.6z.6R. +d.71.6o. +1.7A.6j. +1.8z.5C. +1.9i.3p. +6.6M.6J. +6.6v.6Y. +7.6x.6U. +d.71.6p. +1.6N.6I. +1.8z.5D. +2.7A.6k. +6.6R.6A. +6.6v.6Z. +8.6z.6T. +b.6+.6s. +h.6V.6x. +1.5s.8D. +1.6L.6M. +1.7A.6l. +2.6J.6N. +3.6R.6B. +5.6P.6G. +7.6x.6W. +1.6N.6K. +1.9i.3s. +2.8S.4T. +2.8z.5E. +7.6x.6X. +b.6+.6u. +1.98.3Z. +2.6L.6N. +2.8S.4U. +6.6O.6H. +6.6R.6C. +7.6x.6Y. +b.6+.6v. +d.71.6s. +1.6g.7H. +1.8S.4V. +1.98.3+. +2.7A.6m. +6.6R.6D. +7.6v.6/. +7.6x.6Z. +0.6P.6H. +1.4G.8X. +1.6g.7I. +1.8S.4W. +1.98.3/. +1.9i.3t. +3.6R.6E. +6.6M.6M. +6.6O.6J. +7.6v.70. +8.6z.6U. +d.71.6u. +1.6g.7J. +1.9i.3u. +6.6P.6I. +6.6S.6E. +7.6v.71. +8.6z.6V. +0.6P.6J. +1.6L.6O. +1.98.41. +2.8z.5H. +6.6R.6F. +7.6v.72. +8.6z.6W. +h.6+.6x. +1.8S.4X. +1.8z.5I. +1.98.42. +1.9i.3w. +6.6P.6K. +7.6v.73. +7.6x.6/. +8.6z.6X. +1.6L.6P. +1.7d.6n. +1.98.43. +1.9i.3x. +2.6N.6N. +3.6V.6B. +7.6v.74. +7.6x.70. +8.6z.6Y. +1.98.44. +6.6R.6G. +6.6v.75. +7.6x.71. +8.6z.6Z. +1.98.45. +2.9i.3z. +6.6M.6O. +6.6v.76. +7.6x.72. +1.7d.6q. +1.7g.6n. +1.8S.4Z. +1.98.46. +1.9i.3A. +2.4G.8Z. +6.6v.77. +7.6x.73. +0.6P.6M. +1.98.47. +2.5s.8E. +3.6V.6E. +6.6v.78. +7.6x.74. +8.6z.6+. +1.4G.8+. +1.7d.6r. +1.98.48. +1.9i.3B. +3.6R.6H. +6.6v.79. +7.6x.75. +8.6z.6/. +1.7g.6q. +1.98.49. +1.9i.3C. +2.6P.6N. +2.8S.4+. +6.6v.7a. +7.6x.76. +8.6z.70. +9.6R.6I. +1.7d.6t. +1.98.4a. +2.9i.3D. +3.6R.6J. +7.6x.77. +8.6z.71. +b.6+.6B. +m.6v.7b. +1.6L.6Q. +1.7g.6r. +1.98.4b. +1.9i.3E. +2.6g.7K. +2.8S.50. +6.6O.6O. +6.6v.7c. +7.6x.78. +8.6z.72. +9.6R.6K. +1.6L.6R. +1.9i.3F. +6.6v.7d. +7.6x.79. +8.6z.73. +0.6P.6O. +1.7g.6t. +2.5s.8F. +6.6v.7e. +7.6x.7a. +8.6z.74. +d.71.6B. +1.6B.72. +1.6L.6T. +7.6v.7f. +7.6x.7b. +8.6z.75. +b.6+.6E. +0.6P.6P. +1.6B.73. +1.7d.6w. +1.98.4d. +6.6v.7g. +7.6x.7c. +8.6z.76. +b.5F.8C. +1.5s.8G. +1.6B.74. +1.6g.7L. +3.5G.8C. +3.6R.6M. +3.6V.6H. +6.6v.7h. +7.6x.7d. +8.6z.77. +1.5s.8H. +1.6B.75. +1.6N.6Q. +1.7d.6y. +1.8S.56. +1.98.4e. +2.6g.7M. +2.8z.5J. +7.6x.7e. +8.6z.78. +d.71.6E. +1.7g.6w. +1.98.4f. +1.9i.3H. +2.6R.6N. +3.6V.6J. +6.6v.7i. +7.6x.7f. +8.6z.79. +1.5s.8I. +1.6B.77. +1.6L.6U. +1.98.4g. +1.9i.3I. +2.6v.7j. +2.8z.5K. +7.6x.7g. +8.6z.7a. +1.5s.8J. +1.6L.6V. +1.6N.6T. +1.7g.6y. +1.98.4h. +2.8S.59. +6.6v.7k. +7.6x.7h. +8.6z.7b. +1.5s.8K. +1.6B.79. +1.6L.6W. +1.98.4i. +2.8S.5a. +7.6v.7l. +8.6z.7c. +1.6B.7a. +1.6L.6X. +2.8S.5b. +3.6R.6O. +6.6v.7m. +7.6x.7i. +8.6z.7d. +1.4G.93. +1.6L.6Y. +1.98.4k. +5.6P.6Q. +6.6v.7n. +8.6z.7e. +9.7t.6n. +b.6+.6H. +1.4G.94. +1.6B.7c. +1.6L.6Z. +1.7d.6A. +2.98.4l. +3.6R.6P. +3.6V.6M. +6.6o.7t. +7.6v.7o. +7.6x.7k. +8.6z.7f. +1.6N.6U. +1.98.4m. +1.9i.3O. +2.8S.5c. +6.6p.7t. +7.6x.7l. +8.6z.7g. +b.6+.6J. +1.7t.6q. +1.98.4n. +2.6J.6/. +2.8S.5d. +2.9i.3P. +5.6P.6T. +7.6x.7m. +8.6z.7h. +d.71.6H. +1.6L.6+. +1.6N.6W. +1.7d.6C. +1.7g.6A. +1.98.4o. +7.6v.7p. +7.6x.7n. +d.7v.6n. +1.6L.6/. +1.6N.6X. +1.7d.6D. +1.98.4p. +1.9i.3Q. +2.6g.7O. +2.8S.5f. +7.6v.7q. +7.6x.7o. +8.6z.7i. +9.7t.6r. +d.71.6J. +1.4G.95. +1.6B.7h. +1.6L.70. +1.6N.6Y. +2.6J.72. +2.6z.7j. +6.6s.7t. +6.6v.7r. +1.6L.71. +1.6N.6Z. +1.7g.6C. +1.7v.6q. +2.6g.7P. +3.6V.6O. +8.6z.7k. +9.7t.6t. +0.6u.7t. +1.6B.7i. +1.6L.72. +1.7d.6F. +1.7g.6D. +1.98.4r. +2.8S.5i. +5.6P.6U. +7.6v.7s. +7.6x.7p. +8.6z.7l. +b.6+.6M. +0.6v.7t. +1.6L.73. +1.98.4s. +1.9i.3T. +2.6B.7j. +2.6J.75. +2.8S.5j. +3.6V.6P. +6.6R.6Q. +7.6x.7q. +8.6z.7m. +d.7v.6r. +1.4G.96. +1.6L.74. +1.98.4t. +1.9i.3U. +2.6J.76. +3.6R.6R. +5.6P.6W. +7.6x.7r. +8.6z.7n. +1.5s.8L. +1.6B.7l. +1.6L.75. +1.6N.6/. +1.7d.6G. +1.7g.6F. +1.9i.3V. +2.6g.7Q. +2.6J.77. +2.8S.5k. +5.6P.6X. +6.6v.7u. +8.6z.7o. +d.71.6M. +d.7v.6t. +1.5s.8M. +1.6L.76. +1.6N.70. +1.7t.6w. +1.98.4u. +1.9i.3W. +2.6J.78. +5.6P.6Y. +6.6S.6S. +7.6x.7s. +9.6R.6T. +9.7A.6n. +0.7A.6o. +1.5s.8N. +1.6L.77. +1.6N.71. +1.98.4v. +1.9i.3X. +5.6P.6Z. +6.6v.7v. +h.7t.6x. +0.7A.6p. +1.6g.7S. +1.6L.78. +1.6N.72. +1.7g.6G. +1.98.4w. +2.6J.7a. +2.8S.5l. +7.6v.7w. +8.6z.7p. +9.7t.6y. +b.6+.6O. +1.6L.79. +1.6N.73. +1.7A.6q. +7.6v.7x. +7.6x.7u. +8.6z.7q. +1.6g.7T. +1.6L.7a. +1.6N.74. +1.7d.6I. +1.7v.6w. +1.98.4x. +2.6J.7c. +8.6z.7r. +b.6+.6P. +1.6L.7b. +1.6N.75. +1.8S.5n. +1.98.4y. +2.6J.7d. +5.6P.6/. +6.6R.6U. +7.6x.7v. +9.7A.6r. +d.71.6O. +1.5s.8O. +1.6g.7U. +1.6L.7c. +1.6N.76. +1.7d.6K. +1.98.4z. +1.9i.3Z. +2.6J.7e. +2.8z.5M. +3.6R.6V. +5.6P.70. +6.6s.7A. +6.6v.7y. +7.6x.7w. +8.6z.7s. +d.7v.6y. +1.5s.8P. +1.6L.7d. +1.6N.77. +1.7g.6I. +1.98.4A. +1.9i.3+. +2.6J.7f. +2.8S.5p. +2.8z.5N. +5.6P.71. +6.6R.6W. +7.6x.7x. +8.6z.7t. +9.7A.6t. +0.7A.6u. +1.6L.7e. +1.6N.78. +1.98.4B. +1.9i.3/. +5.6P.72. +6.6R.6X. +9.6v.7z. +0.7A.6v. +1.5s.8Q. +1.6B.7s. +1.6L.7f. +1.6N.79. +1.7g.6K. +1.8z.5O. +1.98.4C. +2.6J.7h. +5.6P.73. +6.6R.6Y. +8.6z.7u. +9.7t.6A. +1.6g.7X. +1.6N.7a. +1.7g.6L. +1.8z.5P. +1.98.4D. +1.9i.41. +5.6P.74. +6.6B.7t. +6.6R.6Z. +7.6x.7y. +1.5s.8R. +1.6L.7h. +1.6N.7b. +1.9i.42. +2.6J.7i. +2.98.4E. +5.6P.75. +8.6z.7v. +1.6B.7u. +1.6N.7c. +1.7A.6w. +1.8S.5r. +1.8z.5Q. +1.98.4F. +1.9i.43. +2.6J.7j. +5.6P.76. +8.6z.7w. +9.6x.7z. +9.7t.6C. +1.5s.8S. +1.6L.7i. +1.6N.7d. +1.98.4G. +1.9i.44. +2.6J.7k. +4.7t.6D. +5.6P.77. +7.6v.7B. +8.6z.7x. +b.6+.6R. +d.7v.6A. +h.7A.6x. +1.4G.99. +1.6N.7e. +1.9i.45. +2.6L.7j. +3.6V.6V. +5.6P.78. +6.6E.7t. +7.6v.7C. +9.6R.6/. +9.7A.6y. +1.6B.7w. +1.6L.7k. +1.6N.7f. +1.8z.5R. +1.9i.46. +2.6J.7m. +5.6P.79. +7.6v.7D. +9.6R.70. +1.5s.8T. +1.6L.7l. +1.6N.7g. +1.98.4H. +1.9i.47. +2.6J.7n. +5.6P.7a. +7.6v.7E. +8.6z.7y. +9.6R.71. +9.7t.6F. +d.7v.6C. +1.6g.7Y. +1.6L.7m. +1.6N.7h. +1.98.4I. +1.9i.48. +2.6J.7o. +5.6P.7b. +7.6x.7B. +9.6R.72. +d.7v.6D. +1.6L.7n. +1.98.4J. +1.9i.49. +2.4G.9b. +2.8S.5t. +5.6P.7c. +7.6x.7C. +8.6z.7z. +9.6R.73. +1.6L.7o. +1.6N.7i. +1.98.4K. +1.9i.4a. +4.7t.6G. +5.6P.7d. +6.6v.7F. +7.6x.7D. +8.6z.7A. +9.6R.74. +1.98.4L. +1.9i.4b. +2.6J.7p. +2.6N.7j. +2.8z.5S. +5.6P.7e. +6.6R.75. +7.6x.7E. +d.7v.6F. +1.5s.8U. +1.6B.7z. +1.6N.7k. +1.98.4M. +5.6P.7f. +6.6v.7G. +9.6R.76. +9.7A.6A. +b.6+.6V. +0.7A.6B. +1.6g.7+. +1.6L.7p. +1.6N.7l. +1.98.4N. +5.6P.7g. +6.5L.8C. +7.6v.7H. +9.6R.77. +0.7t.6H. +1.6L.7q. +1.6N.7m. +1.98.4O. +5.6P.7h. +6.6R.78. +7.6v.7I. +7.6x.7F. +8.6z.7B. +d.7v.6G. +1.6L.7r. +1.6N.7n. +1.9i.4d. +2.6J.7s. +2.98.4P. +6.6v.7J. +8.6z.7C. +9.6R.79. +9.7A.6C. +9.7t.6I. +d.71.6V. +0.6J.7t. +1.6N.7o. +1.98.4Q. +5.6P.7i. +6.6R.7a. +7.6x.7G. +8.6z.7D. +9.7A.6D. +0.7A.6E. +1.5s.8V. +1.6B.7B. +1.6L.7s. +1.98.4R. +1.9i.4e. +2.6P.7j. +6.6R.7b. +7.6x.7H. +8.6z.7E. +9.7t.6K. +1.6B.7C. +1.6L.7t. +1.7d.6Q. +1.8z.5T. +1.98.4S. +1.9i.4f. +2.4G.9e. +2.6J.7u. +5.6P.7k. +7.6x.7I. +9.6R.7c. +1.5s.8W. +1.6g.80. +1.6N.7p. +1.9i.4g. +5.6P.7l. +6.6R.7d. +7.6x.7J. +9.7A.6F. +b.6+.6+. +d.7v.6I. +1.6B.7E. +1.6L.7u. +1.6N.7q. +1.9i.4h. +2.6J.7v. +2.98.4T. +5.6P.7m. +8.6z.7F. +9.6R.7e. +1.6N.7r. +1.7d.6T. +1.7g.6Q. +1.9i.4i. +2.98.4U. +5.6P.7n. +9.6R.7f. +d.7v.6K. +1.98.4V. +2.6J.7x. +2.8S.5v. +5.6P.7o. +6.6M.7t. +8.6z.7G. +9.6R.7g. +9.7A.6G. +d.71.6+. +d.7v.6L. +1.5s.8X. +1.6B.7F. +1.6L.7w. +1.6N.7s. +1.98.4W. +1.9i.4k. +2.8S.5w. +8.6z.7H. +9.6R.7h. +1.5s.8Y. +1.6L.7x. +1.7g.6T. +2.7t.6N. +2.9i.4l. +8.6z.7I. +1.6B.7G. +1.9i.4m. +5.6P.7p. +8.6z.7J. +9.6R.7i. +d.71.71. +0.7A.6H. +1.6g.81. +1.6N.7u. +1.7d.6U. +1.8S.5y. +1.98.4X. +1.9i.4n. +2.6R.7j. +2.6v.7K. +5.6P.7q. +1.6g.82. +1.6L.7y. +1.8S.5z. +1.8z.5U. +1.98.4Y. +1.9i.4o. +2.4G.9g. +5.6P.7r. +6.6R.7k. +9.7A.6I. +0.7A.6J. +1.6g.83. +1.7d.6W. +1.9i.4p. +2.7v.6N. +6.6O.7t. +9.6R.7l. +1.6L.7z. +1.6N.7w. +1.7d.6X. +1.7g.6U. +1.8S.5A. +1.8z.5V. +5.6P.7s. +6.6R.7m. +9.7A.6K. +0.6P.7t. +1.6L.7A. +1.6N.7x. +1.7d.6Y. +1.8S.5B. +1.98.4Z. +2.5s.8Z. +6.6R.7n. +1.7d.6Z. +1.7g.6W. +1.9i.4r. +6.6v.7L. +9.6R.7o. +1.5s.8+. +1.7g.6X. +1.9i.4s. +2.6J.7B. +2.6v.7M. +5.6P.7u. +1.6N.7y. +1.7g.6Y. +1.8S.5C. +1.98.4+. +1.9i.4t. +2.6J.7C. +2.8z.5W. +0.7A.6M. +1.6L.7B. +1.7g.6Z. +1.8S.5D. +1.98.4/. +2.6J.7D. +2.8z.5X. +5.6P.7v. +9.6R.7p. +1.6g.88. +1.6L.7C. +1.6N.7z. +1.7d.6/. +1.98.50. +1.9i.4u. +2.8z.5Y. +5.6P.7w. +6.6R.7q. +7.6x.7L. +1.5s.8/. +1.6L.7D. +1.7d.70. +1.8z.5Z. +1.98.51. +1.9i.4v. +2.6z.7K. +2.7A.6N. +2.8S.5E. +5.6P.7x. +6.6R.7r. +1.6L.7E. +1.7d.71. +1.98.52. +1.9i.4w. +2.8z.5+. +1.7d.72. +1.7g.6/. +2.6J.7F. +4.7t.6Q. +9.6R.7s. +1.5s.90. +1.7d.73. +1.7g.70. +1.98.54. +1.9i.4x. +2.6B.7K. +3.6R.7t. +5.6P.7y. +0.7A.6O. +1.6g.8d. +1.6L.7F. +1.6N.7B. +1.7d.74. +1.7g.71. +1.9i.4y. +2.6J.7G. +1.6g.8e. +1.6N.7C. +1.7d.75. +1.7g.72. +1.98.56. +1.9i.4z. +2.6J.7H. +2.8S.5H. +6.6P.7z. +6.6R.7u. +8.6z.7L. +9.7t.6T. +0.7A.6P. +1.6g.8f. +1.6L.7G. +1.6N.7D. +1.7d.76. +1.7g.73. +1.8S.5I. +1.98.57. +1.9i.4A. +2.6z.7M. +2.8z.5/. +d.7v.6Q. +1.6L.7H. +1.6N.7E. +1.7d.77. +1.7g.74. +1.98.58. +1.9i.4B. +2.6g.8g. +2.6v.7O. +2.8z.60. +6.6R.7v. +1.5s.91. +1.6L.7I. +1.7d.78. +1.7g.75. +1.8z.61. +1.9i.4C. +2.6g.8h. +2.98.59. +6.6R.7w. +1.6L.7J. +1.7d.79. +1.7g.76. +1.8z.62. +1.9i.4D. +2.6B.7M. +2.6v.7P. +2.98.5a. +6.6R.7x. +d.7v.6T. +1.5s.92. +1.6N.7F. +1.7d.7a. +1.7g.77. +2.98.5b. +2.9i.4E. +5.6P.7B. +9.7t.6U. +1.5s.93. +1.7d.7b. +1.7g.78. +1.9i.4F. +3.6V.7t. +5.6P.7C. +1.4G.9i. +1.5s.94. +1.6N.7G. +1.7d.7c. +1.7g.79. +2.8z.63. +5.6P.7D. +6.6R.7y. +9.7t.6W. +1.6g.8j. +1.6N.7H. +1.7d.7d. +1.7g.7a. +1.98.5c. +2.6v.7Q. +5.6P.7E. +6.6o.7V. +9.7t.6X. +1.6N.7I. +1.7d.7e. +1.7g.7b. +2.98.5d. +6.6p.7V. +9.6R.7z. +9.7A.6Q. +9.7t.6Y. +d.7v.6U. +1.6N.7J. +1.7d.7f. +1.7g.7c. +1.9i.4H. +2.8z.64. +3.6R.7A. +9.7t.6Z. +1.6v.7S. +1.7d.7g. +1.9i.4I. +2.98.5f. +5.6P.7F. +d.7v.6W. +1.5s.95. +1.7d.7h. +1.7g.7e. +1.8z.65. +1.98.5g. +1.9i.4J. +2.6z.7O. +9.7A.6T. +d.7v.6X. +1.7g.7f. +1.98.5h. +1.9i.4K. +2.6J.7K. +5.6P.7G. +6.6s.7V. +7.6v.7T. +b.6+.7t. +d.7v.6Y. +1.6g.8m. +1.7d.7i. +1.7g.7g. +1.9i.4L. +2.6z.7P. +2.8z.66. +2.98.5i. +5.6P.7H. +9.6R.7B. +9.7t.6/. +d.7v.6Z. +0.7V.6u. +1.6g.8n. +1.6v.7U. +1.7g.7h. +1.8z.67. +1.9i.4M. +2.6B.7O. +2.6L.7K. +2.98.5j. +5.6P.7I. +9.6R.7C. +9.7t.70. +0.6v.7V. +1.5s.96. +1.6g.8o. +1.7d.7k. +1.9i.4N. +2.8S.5J. +2.8z.68. +5.6P.7J. +9.6R.7D. +9.7t.71. +1.7d.7l. +1.7g.7i. +1.98.5k. +1.9i.4O. +2.6B.7P. +7.6x.7T. +9.6R.7E. +9.7A.6U. +9.7t.72. +1.7d.7m. +2.4G.9j. +2.6z.7Q. +2.7g.7j. +2.8S.5K. +2.9i.4P. +3.6V.7A. +9.7t.73. +d.7v.6/. +1.6v.7X. +1.7d.7n. +1.7g.7k. +1.8z.69. +1.9i.4Q. +2.4G.9k. +2.6J.7M. +4.7A.6W. +6.6o.7Z. +9.7t.74. +d.7v.70. +1.6L.7L. +1.7d.7o. +1.7g.7l. +1.8z.6a. +1.9i.4R. +2.98.5l. +4.7A.6X. +6.6p.7Z. +6.6R.7F. +9.7t.75. +d.7v.71. +h.7V.6x. +1.6z.7S. +1.7g.7m. +1.9i.4S. +2.6B.7Q. +2.6L.7M. +2.6N.7K. +4.7A.6Y. +9.7t.76. +d.7v.72. +1.7g.7n. +1.8z.6b. +4.7A.6Z. +4.7t.77. +6.6R.7G. +d.7v.73. +1.7d.7p. +1.7g.7o. +1.98.5n. +2.9i.4T. +8.6z.7T. +9.6R.7H. +9.7t.78. +d.7v.74. +1.7d.7q. +1.98.5o. +2.9i.4U. +6.6s.7Z. +9.6R.7I. +9.7t.79. +m.7v.75. +1.5s.97. +1.6z.7U. +1.7d.7r. +1.9i.4V. +2.98.5p. +6.6R.7J. +9.7t.7a. +b.6+.7A. +d.7v.76. +0.6u.7Z. +1.6N.7L. +1.6v.7Y. +1.7g.7p. +1.98.5q. +1.9i.4W. +4.7A.6/. +4.7t.7b. +8.6z.7V. +d.7v.77. +0.6v.7Z. +1.7d.7s. +1.7g.7q. +2.6N.7M. +2.6P.7K. +4.7A.70. +9.7t.7c. +d.7v.78. +1.7g.7r. +4.7A.71. +9.7t.7d. +d.7v.79. +1.6z.7X. +1.9i.4X. +4.7A.72. +4.7t.7e. +6.6B.7V. +d.7v.7a. +1.7d.7u. +1.7g.7s. +1.8z.6c. +1.98.5r. +1.9i.4Y. +2.6J.7O. +4.7A.73. +9.7t.7f. +d.7v.7b. +1.6g.8t. +4.7A.74. +6.6v.7+. +9.7t.7g. +d.7v.7c. +h.7Z.6x. +1.5s.99. +2.6J.7P. +2.6L.7O. +4.7A.75. +5.6P.7L. +9.7t.7h. +m.7v.7d. +0.7V.6E. +1.7d.7w. +1.7g.7u. +1.9i.4Z. +2.6P.7M. +9.7A.76. +d.7v.7e. +1.7d.7x. +2.6L.7P. +9.7A.77. +9.7t.7i. +d.7v.7f. +1.5s.9a. +2.7t.7j. +7.6v.7/. +7.6x.7+. +9.7A.78. +d.7v.7g. +1.6z.7Y. +1.7g.7w. +1.9i.4+. +2.5s.9b. +2.6J.7Q. +2.6R.7K. +2.98.5t. +9.7A.79. +9.7t.7k. +d.7v.7h. +1.6v.80. +1.7d.7y. +1.7g.7x. +1.9i.4/. +8.6z.7Z. +9.7A.7a. +9.7t.7l. +1.9i.50. +2.6L.7Q. +2.6N.7O. +2.8z.6d. +4.7t.7m. +9.7A.7b. +d.7v.7i. +1.5s.9c. +1.7d.7z. +1.9i.51. +2.7v.7j. +2.8z.6e. +4.7t.7n. +7.6x.7/. +9.7A.7c. +1.6g.8y. +1.7g.7y. +1.9i.52. +2.6N.7P. +2.8S.5M. +6.6B.7Z. +6.85.6o. +9.7A.7d. +9.7t.7o. +d.7v.7k. +1.5s.9d. +1.6L.7S. +1.8z.6f. +2.6J.7T. +2.8S.5N. +6.6R.7L. +6.85.6p. +8.6z.7+. +9.7A.7e. +d.7v.7l. +0.7V.6H. +1.7g.7z. +1.9i.54. +2.6R.7M. +9.7A.7f. +d.7v.7m. +1.6L.7T. +1.8S.5O. +9.7A.7g. +9.7t.7p. +d.7v.7n. +0.7V.6J. +0.7Z.6E. +1.7d.7B. +1.8S.5P. +1.9i.56. +2.6N.7Q. +2.6P.7O. +4.7t.7q. +6.6v.81. +9.7A.7h. +d.7v.7o. +1.6L.7U. +1.6v.82. +1.7d.7C. +1.9i.57. +2.5s.9e. +6.85.6s. +8.6z.7/. +9.7t.7r. +1.6L.7V. +1.6v.83. +1.7d.7D. +1.8S.5Q. +1.8z.6h. +1.9i.58. +2.6P.7P. +9.7A.7i. +1.6N.7S. +1.6z.80. +1.7d.7E. +1.7g.7B. +2.7A.7j. +2.9i.59. +6.6v.84. +6.85.6u. +9.7t.7s. +d.7v.7p. +0.7t.7t. +1.7g.7C. +2.9i.5a. +6.85.6v. +7.6x.81. +9.7A.7k. +d.7v.7q. +1.6L.7X. +1.6N.7T. +1.6v.86. +1.7g.7D. +1.8z.6i. +2.98.5v. +2.9i.5b. +9.7A.7l. +d.7v.7r. +1.7d.7F. +1.7g.7E. +1.8z.6j. +2.6P.7Q. +2.98.5w. +6.6M.7V. +9.7A.7m. +9.7t.7u. +1.6N.7U. +1.98.5x. +2.8z.6k. +7.6x.84. +9.7A.7n. +d.7v.7s. +1.5s.9f. +1.7d.7G. +1.8z.6l. +1.9i.5c. +4.7t.7v. +6.6v.87. +9.7A.7o. +h.85.6x. +0.7Z.6H. +1.6P.7S. +1.7d.7H. +1.7g.7F. +1.98.5y. +2.6R.7O. +2.9i.5d. +6.6v.88. +9.7t.7w. +1.7d.7I. +1.98.5z. +2.5s.9g. +2.6J.7Y. +2.8S.5S. +6.6v.89. +8.6z.81. +9.7t.7x. +d.7v.7u. +0.6J.7Z. +1.6N.7X. +1.6z.82. +1.7d.7J. +1.7g.7G. +2.6R.7P. +2.8z.6m. +2.9i.5f. +4.7A.7p. +5.6P.7T. +7.6v.8a. +0.7V.6O. +1.6L.7Y. +1.6z.83. +1.7g.7H. +1.98.5A. +1.9i.5g. +6.6v.8b. +7.6x.87. +9.7A.7q. +d.7v.7v. +1.6L.7Z. +1.6P.7U. +1.7g.7I. +1.98.5B. +1.9i.5h. +6.6v.8c. +7.6x.88. +8.6z.84. +9.7A.7r. +9.7t.7y. +d.7v.7w. +0.6P.7V. +1.7g.7J. +2.9i.5i. +6.6v.8d. +7.6x.89. +8.6z.85. +d.7v.7x. +1.6z.86. +2.6R.7Q. +2.9i.5j. +4.7A.7s. +7.6v.8e. +7.6x.8a. +9.7t.7z. +0.7A.7t. +1.6B.84. +1.98.5C. +6.6v.8f. +7.6x.8b. +1.4G.9n. +1.5s.9h. +1.6L.7+. +1.6P.7X. +1.98.5D. +1.9i.5k. +2.6v.8g. +6.6M.7Z. +6.85.6B. +7.6x.8c. +d.7v.7y. +1.4G.9o. +1.6N.7Y. +1.6R.7S. +2.6v.8h. +7.6x.8d. +8.6z.87. +9.7A.7u. +1.4G.9p. +2.98.5E. +7.6x.8e. +8.6z.88. +d.7v.7z. +1.4G.9q. +2.9i.5l. +7.6x.8f. +8.6z.89. +9.6R.7T. +9.7A.7v. +9.7t.7B. +1.4G.9r. +1.6B.87. +1.6L.7/. +4.7A.7w. +6.85.6E. +7.6v.8i. +8.6z.8a. +9.7t.7C. +1.4G.9s. +1.6R.7U. +8.6z.8b. +9.7A.7x. +9.7t.7D. +1.4G.9t. +1.6B.89. +1.6L.80. +1.6N.7+. +1.9i.5n. +3.6R.7V. +6.6O.7Z. +7.6v.8j. +8.6z.8c. +9.7t.7E. +1.4G.9u. +1.6B.8a. +1.6P.7Y. +1.9i.5o. +2.98.5H. +8.6z.8d. +d.7v.7B. +0.6P.7Z. +1.4G.9v. +1.6B.8b. +1.6v.8k. +1.98.5I. +2.7g.7K. +2.9i.5p. +7.6x.8i. +8.6z.8e. +9.7A.7y. +d.7v.7C. +1.4G.9w. +1.6B.8c. +1.6R.7X. +1.6v.8l. +1.9i.5q. +8.6z.8f. +9.7t.7F. +d.7v.7D. +1.4G.9x. +1.6N.7/. +1.7d.7L. +2.6z.8g. +7.6x.8j. +9.7A.7z. +d.7v.7E. +1.4G.9y. +2.6z.8h. +9.7t.7G. +l.7A.7A. +1.4G.9z. +1.6N.80. +5.6P.7+. +7.6v.8m. +9.7t.7H. +1.6L.81. +1.7g.7L. +1.9i.5r. +2.6B.8g. +6.85.6H. +7.6v.8n. +9.7t.7I. +d.7v.7F. +1.5s.9i. +1.6L.82. +1.6v.8o. +2.6B.8h. +2.6J.84. +2.7g.7M. +3.6V.7V. +8.6z.8i. +9.7t.7J. +1.6L.83. +1.6v.8p. +2.8S.5W. +6.85.6J. +9.7A.7B. +d.7v.7G. +1.6L.84. +1.6R.7Y. +2.6J.86. +2.8S.5X. +5.6P.7/. +7.6x.8m. +8.6z.8j. +9.7A.7C. +d.7v.7H. +1.6L.85. +1.6v.8q. +2.8S.5Y. +3.6R.7Z. +7.6x.8n. +9.7A.7D. +d.7v.7I. +1.6L.86. +1.6P.80. +1.6z.8k. +9.7A.7E. +d.7v.7J. +1.6B.8j. +1.6N.81. +1.6z.8l. +2.8S.5+. +2.9i.5t. +1.6N.82. +1.6L.87. +1.6N.83. +2.6J.89. +6.6R.7+. +6.85.6M. +9.7A.7F. +b.6+.7V. +1.6L.88. +1.6N.84. +2.6J.8a. +8.6z.8m. +9.6v.8r. +1.6L.89. +2.98.5J. +7.6v.8s. +8.6z.8n. +9.7A.7G. +1.6L.8a. +1.6N.86. +1.6z.8o. +2.6g.8E. +2.8S.5/. +9.7A.7H. +d.71.7V. +1.6L.8b. +1.6z.8p. +2.5s.9j. +2.7g.7O. +2.7t.7K. +2.8S.60. +2.98.5K. +3.6V.7Z. +5.6P.81. +9.6R.7/. +9.7A.7I. +1.6L.8c. +1.6P.82. +2.5s.9k. +2.6J.8e. +9.6x.8r. +9.7A.7J. +1.6L.8d. +1.6N.87. +1.6P.83. +1.6z.8q. +2.7g.7P. +6.85.6O. +7.6x.8s. +k.6R.80. +1.6L.8e. +1.6N.88. +5.6P.84. +1.6L.8f. +1.6N.89. +2.7v.7K. +6.85.6P. +7.6v.8t. +1.6N.8a. +1.6P.86. +2.6g.8F. +2.6L.8g. +2.8S.63. +9.7t.7L. +1.6N.8b. +1.7d.7S. +1.8z.6n. +2.6L.8h. +2.7g.7Q. +2.7t.7M. +7.6v.8u. +1.6N.8c. +2.9i.5v. +6.6v.8v. +8.6z.8r. +b.6+.7Z. +1.6g.8G. +1.6N.8d. +1.7d.7T. +2.8S.64. +2.9i.5w. +5.6P.87. +6.6v.8w. +7.6x.8t. +8.6z.8s. +1.6g.8H. +1.6L.8i. +1.6N.8e. +1.7g.7S. +1.8z.6q. +1.9i.5x. +5.6P.88. +6.6R.81. +d.7v.7L. +1.6N.8f. +1.8S.65. +2.7v.7M. +5.6P.89. +7.6x.8u. +d.71.7Z. +k.6R.82. +1.6g.8I. +1.6L.8j. +1.6v.8x. +1.7g.7T. +1.8z.6r. +1.9i.5y. +2.6N.8g. +5.6P.8a. +h.6x.8v. +k.6R.83. +1.6g.8J. +1.9i.5z. +2.6N.8h. +2.7A.7K. +2.8S.66. +5.6P.8b. +6.6R.84. +7.6x.8w. +1.6g.8K. +1.6L.8k. +1.7g.7U. +1.8S.67. +1.8z.6t. +3.6R.85. +5.6P.8c. +6.6v.8y. +1.6L.8l. +1.7d.7X. +1.9i.5A. +2.8S.68. +5.6P.8d. +8.6z.8t. +k.6R.86. +1.6N.8i. +1.9i.5B. +5.6P.8e. +9.6v.8z. +5.6P.8f. +8.6z.8u. +1.6L.8m. +1.6N.8j. +1.7g.7X. +1.8S.69. +2.6J.8o. +2.6P.8g. +2.7t.7O. +7.6x.8y. +8.6z.8v. +9.6R.87. +9.7A.7L. +1.6L.8n. +1.8S.6a. +1.8z.6w. +1.9i.5C. +2.6J.8p. +2.6P.8h. +2.7A.7M. +8.6z.8w. +9.6R.88. +1.6B.8u. +1.6L.8o. +1.6N.8k. +1.9i.5D. +2.7t.7P. +6.6R.89. +9.6x.8z. +1.6L.8p. +1.6N.8l. +1.8S.6b. +1.8z.6y. +6.6R.8a. +1.6z.8x. +1.7d.7Y. +2.7v.7O. +2.9i.5E. +3.6V.85. +5.6P.8i. +9.6R.8b. +1.6L.8q. +2.98.5M. +6.6R.8c. +1.4G.9B. +1.6N.8m. +2.7t.7Q. +2.7v.7P. +2.98.5N. +5.6P.8j. +8.6z.8y. +9.6R.8d. +1.4G.9C. +1.6N.8n. +1.7g.7Y. +9.6R.8e. +1.4G.9D. +1.6N.8o. +1.6P.8k. +1.98.5O. +6.6o.8C. +6.6R.8f. +8.6z.8z. +1.4G.9E. +1.6N.8p. +1.6P.8l. +1.7d.7+. +1.7t.7S. +1.98.5P. +2.6J.8s. +2.6R.8g. +2.9i.5H. +6.6p.8C. +1.6L.8r. +1.8z.6A. +1.9i.5I. +2.6R.8h. +2.7v.7Q. +7.6v.8B. +1.4G.9F. +1.6L.8s. +1.6N.8q. +1.8S.6c. +1.98.5Q. +9.7t.7T. +b.6+.85. +1.7g.7+. +2.7A.7O. +5.6P.8m. +1.7d.7/. +1.7t.7U. +1.7v.7S. +1.8z.6C. +5.6P.8n. +6.6R.8i. +6.6s.8C. +0.7V.7t. +1.6P.8o. +1.8z.6D. +1.98.5R. +2.7A.7P. +7.6x.8B. +d.71.85. +0.6u.8C. +1.4G.9G. +1.6P.8p. +1.7d.80. +9.6R.8j. +d.7v.7T. +0.6v.8C. +1.4G.9H. +1.5s.9l. +1.6g.8O. +1.6N.8r. +1.7g.7/. +1.6L.8t. +1.6N.8s. +1.6P.8q. +1.7t.7X. +1.7v.7U. +1.8z.6F. +k.6R.8k. +1.7g.80. +2.7A.7Q. +k.6R.8l. +1.4G.9I. +1.6g.8Q. +1.6L.8u. +2.6J.8w. +2.8S.6d. +2.98.5S. +1.4G.9J. +1.5s.9m. +1.6L.8v. +1.8z.6G. +2.8S.6e. +8.6z.8B. +h.6x.8C. +1.6L.8w. +1.7A.7S. +1.7v.7X. +6.6R.8m. +1.7d.81. +6.6R.8n. +9.6P.8r. +1.6N.8t. +1.6R.8o. +1.7d.82. +5.6P.8s. +9.7A.7T. +1.6L.8x. +1.6R.8p. +1.7d.83. +1.7t.7Y. +0.7Z.7t. +1.6N.8u. +1.7A.7U. +1.7d.84. +1.7g.81. +1.8z.6I. +2.9i.5J. +0.7A.7V. +1.5s.9n. +1.6g.8T. +1.6L.8y. +1.6N.8v. +1.7g.82. +1.98.5T. +2.6J.8z. +8.6z.8C. +k.6R.8q. +1.5s.9o. +1.6N.8w. +1.7d.86. +1.7g.83. +1.8z.6K. +2.9i.5K. +1.4G.9K. +1.5s.9p. +1.7g.84. +1.7v.7Y. +1.8z.6L. +1.5s.9q. +1.7A.7X. +4.7t.7+. +5.6P.8t. +6.6B.8C. +6.6v.8D. +1.5s.9r. +1.6N.8x. +1.7d.87. +1.7g.86. +1.8S.6i. +1.5s.9s. +1.7d.88. +1.8S.6j. +5.6P.8u. +9.6R.8r. +1.5s.9t. +1.6N.8y. +1.7d.89. +2.8S.6k. +5.6P.8v. +9.6R.8s. +1.5s.9u. +1.7d.8a. +1.7g.87. +1.8S.6l. +5.6P.8w. +6.6E.8C. +7.6x.8D. +9.7t.7/. +d.7v.7+. +1.4G.9L. +1.5s.9v. +1.6N.8z. +1.7d.8b. +1.7g.88. +1.5s.9w. +1.7d.8c. +1.7g.89. +1.7t.80. +1.98.5U. +1.5s.9x. +1.6g.8V. +1.6P.8x. +1.7A.7Y. +1.7d.8d. +1.7g.8a. +2.8S.6m. +0.7A.7Z. +1.5s.9y. +1.7d.8e. +1.7g.8b. +1.98.5V. +d.7v.7/. +1.5s.9z. +1.6g.8W. +1.7d.8f. +1.7g.8c. +2.6J.8B. +5.6P.8y. +9.6R.8t. +1.4G.9M. +1.7g.8d. +1.7v.80. +8.6z.8D. +1.4G.9N. +1.6L.8B. +1.7g.8e. +6.6P.8z. +6.6R.8u. +1.7g.8f. +2.6v.8E. +2.98.5W. +6.6R.8v. +9.7A.7+. +0.6H.8C. +1.6g.8X. +2.7g.8g. +2.98.5X. +6.6R.8w. +9.7t.81. +1.7d.8i. +1.7t.82. +2.7g.8h. +2.98.5Y. +0.6J.8C. +1.7t.83. +1.98.5Z. +1.4G.9O. +1.6R.8x. +1.7d.8j. +2.98.5+. +9.7A.7/. +9.7t.84. +1.4G.9P. +1.6L.8C. +1.6N.8B. +1.7g.8i. +6.85.7t. +d.7v.81. +1.7A.80. +1.7d.8k. +1.7t.86. +1.7v.82. +2.6v.8F. +6.6R.8y. +1.4G.9Q. +1.7d.8l. +1.7g.8j. +1.7v.83. +1.8z.6Q. +2.6g.8Z. +2.9i.5M. +6.6R.8z. +d.7v.84. +2.98.5/. +2.9i.5N. +6.6M.8C. +6.6v.8G. +9.7t.87. +1.6g.8+. +1.7d.8m. +1.7g.8l. +1.7v.86. +1.8z.6T. +2.6z.8E. +2.98.60. +4.7t.88. +6.6v.8H. +1.7d.8n. +1.98.61. +1.9i.5O. +5.6P.8B. +9.7t.89. +1.7d.8o. +1.98.62. +1.9i.5P. +6.6v.8I. +9.7t.8a. +1.7d.8p. +1.7g.8m. +2.6B.8E. +6.6v.8J. +7.6x.8G. +9.7A.81. +9.7t.8b. +d.7v.87. +1.6g.8/. +1.7A.82. +1.7g.8n. +1.9i.5Q. +7.6v.8K. +7.6x.8H. +9.7t.8c. +d.7v.88. +1.7A.83. +1.7d.8q. +1.7g.8o. +1.8z.6U. +2.98.63. +4.7t.8d. +6.6O.8C. +d.7v.89. +1.7g.8p. +2.6z.8F. +7.6x.8I. +9.7A.84. +9.7t.8e. +d.7v.8a. +0.6P.8C. +1.6g.90. +1.8z.6W. +1.9i.5R. +6.85.7A. +7.6x.8J. +9.7t.8f. +d.7v.8b. +1.6L.8D. +1.7A.86. +1.7g.8q. +1.8z.6X. +2.7t.8g. +2.98.64. +7.6x.8K. +d.7v.8c. +1.8z.6Y. +2.6B.8F. +2.7t.8h. +8.6z.8G. +d.7v.8d. +1.7d.8r. +1.8z.6Z. +1.98.65. +6.6R.8B. +8.6z.8H. +d.7v.8e. +1.7d.8s. +9.7A.87. +d.7v.8f. +1.6B.8G. +1.6g.91. +2.7v.8g. +2.98.66. +2.9i.5S. +8.6z.8I. +9.7A.88. +9.7t.8i. +1.6B.8H. +1.7g.8r. +1.98.67. +2.7v.8h. +8.6z.8J. +9.7A.89. +1.6g.92. +1.6N.8D. +1.7g.8s. +1.8z.6/. +2.98.68. +8.6z.8K. +9.7A.8a. +9.7t.8j. +1.5s.9A. +1.6g.93. +1.8z.70. +9.7A.8b. +1.6g.94. +1.7t.8k. +1.8z.71. +3.6R.8C. +9.7A.8c. +d.7v.8i. +1.6v.8L. +1.7d.8t. +1.7t.8l. +1.8z.72. +1.98.69. +2.4G.9R. +9.7A.8d. +1.6v.8M. +1.8z.73. +1.98.6a. +9.7A.8e. +d.7v.8j. +1.6v.8N. +1.7d.8u. +1.8z.74. +1.9i.5T. +9.7A.8f. +1.7d.8v. +1.7g.8t. +1.7v.8k. +1.8z.75. +1.98.6b. +2.6L.8E. +2.7A.8g. +5.6P.8D. +9.7t.8m. +1.6g.95. +1.7d.8w. +1.7v.8l. +1.8z.76. +2.7A.8h. +9.7t.8n. +1.7g.8u. +1.7t.8o. +1.8z.77. +1.5s.9B. +1.7g.8v. +1.7t.8p. +1.8z.78. +1.5s.9C. +1.6v.8O. +1.7d.8x. +1.7g.8w. +1.8z.79. +3.6V.8C. +9.7A.8i. +d.7v.8m. +1.5s.9D. +1.6g.96. +1.6v.8P. +1.7t.8q. +1.8z.7a. +d.7v.8n. +1.5s.9E. +1.7d.8y. +1.7v.8o. +1.8z.7b. +2.6L.8F. +2.6N.8E. +9.7A.8j. +1.6v.8Q. +1.6z.8L. +1.7g.8x. +1.7v.8p. +1.8z.7c. +1.5s.9F. +1.6z.8M. +1.7A.8k. +1.7d.8z. +1.98.6c. +1.9i.5U. +1.6L.8G. +1.6z.8N. +1.7A.8l. +1.7g.8y. +1.7v.8q. +1.8S.6t. +1.8z.7e. +6.6v.8R. +9.6R.8D. +1.6L.8H. +1.8z.7f. +1.9i.5V. +2.6J.8I. +9.7t.8r. +1.8z.7g. +9.6v.8S. +9.7t.8s. +b.6+.8C. +1.5s.9G. +1.6L.8I. +1.8z.7h. +2.6N.8F. +2.6P.8E. +9.7A.8m. +f.7V.7V. +1.5s.9H. +1.6L.8J. +7.6x.8R. +9.7A.8n. +1.6g.97. +1.6L.8K. +1.6z.8O. +1.7A.8o. +1.8z.7i. +2.9i.5W. +6.6v.8T. +d.71.8C. +d.7v.8r. +1.6N.8G. +1.6z.8P. +1.7A.8p. +2.8z.7j. +2.9i.5X. +9.6x.8S. +d.7v.8s. +1.5s.9I. +1.6N.8H. +1.8S.6y. +1.8z.7k. +2.98.6d. +2.9i.5Y. +1.5s.9J. +1.6z.8Q. +1.7A.8q. +1.8z.7l. +1.9i.5Z. +2.98.6e. +4.7t.8t. +1.6N.8I. +1.8z.7m. +2.6P.8F. +2.9i.5+. +7.6x.8T. +1.6N.8J. +1.8z.7n. +1.98.6f. +7.6v.8U. +8.6z.8R. +9.7t.8u. +1.6B.8Q. +1.6N.8K. +1.7d.8B. +1.8z.7o. +1.98.6g. +9.7t.8v. +1.6g.99. +2.6R.8E. +5.6P.8G. +8.6z.8S. +9.7t.8w. +d.7v.8t. +5.6P.8H. +9.7A.8r. +1.7g.8B. +1.8z.7p. +2.9i.5/. +7.6x.8U. +9.7A.8s. +d.7v.8u. +f.7V.7Z. +1.6g.9a. +1.6v.8V. +1.7t.8x. +1.8z.7q. +1.98.6h. +2.9i.60. +5.6P.8I. +8.6z.8T. +d.7v.8v. +1.5s.9K. +1.8z.7r. +1.9i.61. +2.6g.9b. +5.6P.8J. +d.7v.8w. +1.9i.62. +4.7t.8y. +5.6P.8K. +9.6v.8W. +1.6L.8L. +1.8z.7s. +1.98.6i. +2.6R.8F. +1.6g.9c. +1.6L.8M. +1.7v.8x. +1.98.6j. +4.7t.8z. +1.6L.8N. +2.98.6k. +2.9i.63. +8.6z.8U. +9.7A.8t. +1.6g.9d. +1.8S.6F. +1.8z.7u. +1.98.6l. +6.6R.8G. +6.6v.8X. +9.6x.8W. +d.7v.8y. +1.5s.9L. +6.6R.8H. +6.6v.8Y. +9.7A.8u. +2.9i.64. +9.7A.8v. +d.7v.8z. +1.6N.8L. +1.8S.6G. +1.8z.7w. +2.98.6m. +9.6R.8I. +9.7A.8w. +1.6L.8O. +1.6N.8M. +1.6z.8V. +1.8z.7x. +1.9i.65. +2.6g.9e. +7.6x.8X. +9.6R.8J. +f.7Z.7Z. +1.6L.8P. +1.6N.8N. +7.6x.8Y. +9.6R.8K. +1.5s.9M. +1.7A.8x. +2.9i.66. +8.6z.8W. +1.5s.9N. +1.6L.8Q. +1.8z.7y. +1.9i.67. +2.6v.8Z. +1.8S.6I. +2.9i.68. +9.7A.8y. +1.6J.8S. +1.6L.8R. +1.6P.8L. +1.7d.8D. +1.8z.7z. +7.6v.8+. +1.6N.8O. +1.6P.8M. +1.8S.6K. +8.6z.8X. +9.7A.8z. +9.7t.8B. +1.6g.9f. +1.6L.8S. +1.6N.8P. +1.6P.8N. +1.9i.69. +8.6z.8Y. +1.5s.9O. +1.7g.8D. +1.9i.6a. +1.5s.9P. +1.6N.8Q. +2.6g.9g. +6.6v.8/. +7.6x.8+. +1.6L.8T. +1.8z.7B. +1.9i.6b. +6.85.7V. +d.7v.8B. +1.5s.9Q. +1.6N.8R. +1.8z.7C. +0.7t.8C. +1.6P.8O. +1.6v.90. +1.8z.7D. +1.6P.8P. +1.8z.7E. +2.6z.8Z. +2.8S.6N. +7.6x.8/. +k.6R.8L. +1.6L.8U. +1.6P.8Q. +8.6z.8+. +k.6R.8M. +1.6g.9h. +1.6N.8T. +1.8z.7F. +2.6B.8Z. +k.6R.8N. +1.6v.91. +5.6P.8R. +1.6B.8+. +1.8z.7G. +1.9i.6c. +9.7A.8B. +1.6v.92. +1.8z.7H. +6.6P.8S. +8.6z.8/. +1.6L.8V. +1.6v.93. +1.8z.7I. +2.6J.8W. +2.7g.8E. +1.6N.8U. +1.6v.94. +1.8z.7J. +6.85.7Z. +k.6R.8O. +1.6L.8W. +1.6z.90. +5.6P.8T. +k.6R.8P. +0.7A.8C. +1.6R.8Q. +2.6J.8Y. +9.7t.8D. +1.6L.8X. +1.6N.8V. +1.7d.8G. +2.7g.8F. +2.9i.6d. +6.6v.95. +9.6R.8R. +1.6L.8Y. +1.6z.91. +1.7d.8H. +2.9i.6e. +5.6P.8U. +1.6N.8W. +9.6R.8S. +1.6z.92. +1.7d.8I. +1.7g.8G. +1.9i.6f. +d.7v.8D. +1.6g.9i. +1.6v.96. +1.6z.93. +1.7d.8J. +1.7g.8H. +7.6x.95. +1.6z.94. +1.7d.8K. +2.5s.9R. +6.6R.8T. +1.6N.8X. +1.6P.8V. +1.7g.8I. +2.8z.7K. +1.6N.8Y. +1.7g.8J. +2.6L.8Z. +1.7g.8K. +1.9i.6h. +6.6P.8W. +1.6L.8+. +1.98.6n. +6.6R.8U. +8.6z.95. +1.8S.6W. +1.8z.7L. +1.9i.6i. +2.7t.8E. +9.7A.8D. +1.5s.9S. +1.9i.6j. +2.8z.7M. +5.6P.8X. +1.5s.9T. +1.6L.8/. +1.6v.97. +1.98.6q. +2.6N.8Z. +2.9i.6k. +5.6P.8Y. +1.6z.96. +1.9i.6l. +1.6N.8+. +1.6R.8V. +1.98.6r. +2.6g.9j. +2.7v.8E. +1.6L.90. +2.6g.9k. +6.85.85. +1.5s.9U. +1.98.6t. +2.7t.8F. +2.9i.6m. +9.6R.8W. +1.7d.8L. +e.8S.6/. +1.6N.8/. +1.7d.8M. +2.6P.8Z. +e.8S.70. +k.98.6v. +1.5s.9V. +1.6v.99. +1.7d.8N. +9.7t.8G. +e.8S.71. +1.6L.91. +1.7g.8L. +2.7v.8F. +5.6P.8+. +9.6R.8X. +9.7t.8H. +e.8S.72. +1.5s.9W. +1.6N.90. +1.7g.8M. +1.98.6w. +9.6R.8Y. +e.8S.73. +1.6L.92. +1.6v.9a. +1.6z.97. +1.7g.8N. +1.8S.74. +2.6J.94. +2.7A.8E. +2.8z.7O. +9.7t.8I. +1.5s.9X. +1.6L.93. +1.98.6y. +2.6v.9b. +9.7t.8J. +d.7v.8G. +e.8S.75. +1.6L.94. +1.7d.8O. +2.8z.7P. +4.7t.8K. +5.6P.8/. +d.7v.8H. +1.7d.8P. +1.8S.77. +1.6N.91. +2.8S.78. +9.6v.9c. +d.7v.8I. +1.6P.90. +1.7d.8Q. +1.7g.8O. +2.6R.8Z. +d.7v.8J. +1.6N.92. +1.7g.8P. +1.98.6z. +2.7A.8F. +2.8z.7Q. +6.6v.9d. +d.7v.8K. +1.6L.95. +1.6N.93. +1.6z.99. +1.7d.8R. +2.8S.7b. +6.6R.8+. +1.6N.94. +1.7g.8Q. +1.8S.7c. +1.98.6A. +9.6x.9c. +1.8z.7S. +9.7A.8G. +1.6P.91. +1.6z.9a. +1.7g.8R. +2.6v.9e. +7.6x.9d. +9.7A.8H. +1.6L.96. +1.8z.7T. +1.98.6C. +2.6z.9b. +6.6R.8/. +1.6P.92. +1.7d.8T. +1.98.6D. +2.8S.7g. +9.7A.8I. +1.6N.95. +1.6P.93. +1.8z.7U. +9.7A.8J. +e.8S.7h. +1.6P.94. +2.6B.9b. +8.6z.9c. +9.7A.8K. +k.6R.90. +1.7g.8T. +1.7t.8L. +1.8S.7i. +1.98.6F. +1.7t.8M. +2.8S.7j. +8.6z.9d. +1.6N.96. +1.7d.8U. +1.7t.8N. +1.8z.7X. +9.6v.9f. +1.8S.7l. +1.98.6G. +1.7v.8L. +2.6v.9g. +5.6P.95. +k.6R.91. +1.6L.97. +1.7g.8U. +1.7v.8M. +1.8S.7n. +2.6z.9e. +1.7v.8N. +9.6x.9f. +k.6R.92. +1.6R.93. +1.7t.8O. +1.6P.96. +1.6R.94. +1.7t.8P. +1.98.6I. +2.6B.9e. +1.7d.8W. +1.8S.7p. +1.8z.7Y. +1.6g.9l. +1.7g.8V. +1.7t.8Q. +1.8S.7q. +1.98.6K. +1.6N.97. +1.7v.8O. +1.8S.7r. +1.98.6L. +7.6v.9h. +1.6L.99. +1.7A.8L. +1.7g.8W. +1.7v.8P. +8.6z.9f. +9.7t.8R. +1.7A.8M. +1.7d.8X. +1.8S.7s. +6.6R.95. +1.6g.9m. +1.7A.8N. +1.7d.8Y. +1.7v.8Q. +1.8z.7+. +2.6J.9b. +2.6z.9g. +9.7t.8S. +1.6L.9a. +7.6x.9h. +1.7g.8X. +1.9i.6n. +2.6L.9b. +d.7v.8R. +e.8S.7u. +0.7V.8C. +1.6P.97. +1.6R.96. +1.7g.8Y. +2.6B.9g. +2.98.6N. +4.7t.8T. +1.6N.99. +1.8z.7/. +e.8S.7v. +1.6L.9c. +1.7A.8O. +1.9i.6q. +e.8S.7w. +1.6g.9n. +1.7A.8P. +1.8z.80. +2.8S.7x. +1.6g.9o. +1.6L.9d. +1.6N.9a. +1.9i.6r. +8.6z.9h. +d.7v.8T. +1.6g.9p. +1.7A.8Q. +1.7d.8+. +2.6N.9b. +4.7t.8U. +1.6g.9q. +1.9i.6t. +2.7g.8Z. +e.8S.7y. +k.98.6P. +1.6g.9r. +1.6P.99. +9.7A.8R. +1.6g.9s. +1.6N.9c. +1.7g.8+. +1.8S.7z. +2.6L.9e. +9.6v.9i. +1.6g.9t. +1.6R.97. +9.7A.8S. +d.7v.8U. +1.6g.9u. +1.6N.9d. +1.6P.9a. +1.7t.8V. +1.8z.81. +0.7Z.8C. +1.6g.9v. +1.8z.82. +1.9i.6w. +2.6P.9b. +1.6g.9w. +1.7d.90. +1.8z.83. +4.7A.8T. +9.6x.9i. +9.7t.8W. +1.6g.9x. +1.8S.7B. +1.8z.84. +1.9i.6y. +1.6g.9y. +1.7v.8V. +1.8S.7C. +1.98.6Q. +2.6N.9e. +9.6P.9c. +1.6g.9z. +1.6L.9f. +1.7g.90. +1.8S.7D. +1.8z.86. +2.6J.9g. +k.6R.98. +1.8S.7E. +5.6P.9d. +9.7t.8X. +d.7v.8W. +k.6R.99. +1.7d.91. +1.98.6T. +2.6L.9g. +4.7A.8U. +4.7t.8Y. +1.8z.87. +8.6z.9i. +1.6R.9a. +1.7d.92. +1.8S.7F. +1.8z.88. +2.6v.9j. +1.7d.93. +1.7g.91. +1.8z.89. +1.9i.6A. +2.6P.9e. +2.6R.9b. +2.6v.9k. +d.7v.8X. +1.6N.9f. +1.7d.94. +1.8S.7G. +1.8z.8a. +d.7v.8Y. +1.7A.8V. +1.7g.92. +1.8z.8b. +1.98.6U. +1.7g.93. +1.8S.7I. +1.8z.8c. +1.9i.6C. +2.6N.9g. +2.7t.8Z. +9.6R.9c. +1.6L.9h. +1.7g.94. +1.8z.8d. +1.98.6W. +1.9i.6D. +9.7A.8W. +1.8z.8e. +1.98.6X. +6.6R.9d. +9.7t.8+. +1.7d.95. +1.8z.8f. +1.98.6Y. +1.98.6Z. +1.9i.6F. +2.7v.8Z. +2.8z.8g. +6.6P.9f. +2.8z.8h. +9.7A.8X. +1.7g.95. +2.6P.9g. +2.6R.9e. +2.6z.9j. +4.7t.8/. +9.7A.8Y. +d.7v.8+. +1.6N.9h. +1.7d.96. +1.9i.6G. +2.6z.9k. +1.8z.8i. +1.98.6/. +1.7t.90. +1.98.70. +2.6B.9j. +6.85.8C. +1.7g.96. +1.8z.8j. +1.98.71. +2.6B.9k. +d.7v.8/. +1.98.72. +1.8z.8k. +1.98.73. +1.9i.6I. +2.7A.8Z. +2.8S.7K. +1.7v.90. +1.8z.8l. +1.98.74. +5.6P.9h. +9.6R.9f. +1.7t.91. +1.98.75. +1.9i.6K. +9.7A.8+. +1.98.76. +1.9i.6L. +2.6R.9g. +1.7d.97. +1.7t.92. +1.8z.8m. +1.98.77. +1.7t.93. +1.8z.8n. +1.98.78. +1.7t.94. +1.7v.91. +1.8z.8o. +1.98.79. +2.8S.7M. +9.7A.8/. +1.7g.97. +1.8z.8p. +1.98.7a. +1.7v.92. +1.98.7b. +1.7A.90. +1.7v.93. +1.8z.8q. +1.98.7c. +2.9i.6N. +1.7v.94. +1.98.7d. +9.6R.9h. +1.7d.99. +1.98.7e. +9.7t.95. +1.98.7f. +1.98.7g. +1.7A.91. +1.7d.9a. +1.7g.99. +1.8z.8r. +1.98.7h. +2.6L.9j. +1.7t.96. +1.8z.8s. +2.6L.9k. +9.6P.9i. +d.7v.95. +1.6g.9B. +1.7A.92. +1.98.7i. +1.6g.9C. +1.7A.93. +1.7g.9a. +2.8S.7O. +2.98.7j. +7.6v.9l. +1.6g.9D. +1.7A.94. +1.7d.9c. +1.98.7k. +2.7g.9b. +1.6g.9E. +1.7v.96. +1.98.7l. +2.8S.7P. +1.7d.9d. +1.98.7m. +2.6N.9j. +1.6g.9F. +1.7g.9c. +1.8z.8t. +1.98.7n. +2.6N.9k. +7.6v.9m. +7.6x.9l. +1.98.7o. +1.7g.9d. +1.8z.8u. +1.9i.6Q. +2.8S.7Q. +9.7A.95. +1.7t.97. +1.8z.8v. +9.6R.9i. +1.6g.9G. +1.8z.8w. +1.98.7p. +7.6x.9m. +1.6g.9H. +1.98.7q. +1.9i.6T. +2.6P.9j. +1.7A.96. +1.98.7r. +2.6P.9k. +2.7g.9e. +7.6v.9n. +8.6z.9l. +1.7v.97. +1.8S.7T. +1.8z.8x. +7.6v.9o. +1.6g.9I. +1.98.7s. +7.6v.9p. +1.6B.9l. +1.6g.9J. +7.6v.9q. +1.7d.9f. +1.7t.99. +1.9i.6U. +7.6v.9r. +7.6x.9n. +8.6z.9m. +1.98.7u. +7.6v.9s. +7.6x.9o. +1.9i.6W. +7.6v.9t. +7.6x.9p. +1.6B.9m. +1.7g.9f. +1.7t.9a. +1.98.7v. +1.9i.6X. +2.8S.7X. +7.6v.9u. +7.6x.9q. +1.7v.99. +1.98.7w. +1.9i.6Y. +2.6R.9j. +2.7t.9b. +7.6v.9v. +7.6x.9r. +1.7A.97. +1.98.7x. +1.9i.6Z. +2.6R.9k. +2.7g.9g. +7.6v.9w. +7.6x.9s. +7.6v.9x. +7.6x.9t. +8.6z.9n. +1.6g.9K. +1.7v.9a. +7.6v.9y. +7.6x.9u. +8.6z.9o. +9.7t.9c. +1.98.7y. +2.7v.9b. +7.6v.9z. +7.6x.9v. +8.6z.9p. +1.6B.9n. +1.9i.6/. +4.7t.9d. +7.6x.9w. +8.6z.9q. +1.6B.9o. +1.98.7z. +1.9i.70. +2.8S.7Y. +7.6x.9x. +8.6z.9r. +1.6B.9p. +1.9i.71. +7.6x.9y. +8.6z.9s. +d.7v.9c. +1.6B.9q. +1.7A.99. +1.7g.9h. +1.9i.72. +7.6x.9z. +8.6z.9t. +1.6B.9r. +1.6g.9L. +1.8z.8B. +1.9i.73. +2.7t.9e. +8.6z.9u. +d.7v.9d. +1.6B.9s. +1.9i.74. +8.6z.9v. +1.6B.9t. +1.7A.9a. +1.98.7B. +1.9i.75. +8.6z.9w. +1.6B.9u. +1.6L.9l. +1.98.7C. +1.9i.76. +2.7A.9b. +8.6z.9x. +1.6B.9v. +1.98.7D. +1.9i.77. +2.7v.9e. +8.6z.9y. +1.6B.9w. +1.6g.9M. +1.98.7E. +1.9i.78. +8.6z.9z. +1.6B.9x. +1.6g.9N. +1.9i.79. +9.7A.9c. +e.8S.7/. +1.6B.9y. +1.6L.9m. +1.9i.7a. +9.7t.9f. +1.98.7F. +1.9i.7b. +9.7A.9d. +1.6N.9l. +1.9i.7c. +2.7t.9g. +1.7d.9i. +1.98.7G. +1.6g.9O. +1.98.7H. +1.9i.7e. +2.6J.9n. +d.7v.9f. +1.6g.9P. +1.98.7I. +1.9i.7f. +2.6J.9o. +2.7A.9e. +1.6L.9n. +1.6N.9m. +1.7g.9i. +1.98.7J. +2.6J.9p. +2.7v.9g. +1.6g.9Q. +1.6L.9o. +1.9i.7h. +2.6J.9q. +1.6L.9p. +2.6J.9r. +2.8S.81. +5.6P.9l. +1.6L.9q. +1.9i.7i. +2.6J.9s. +9.7t.9h. +1.6L.9r. +2.6J.9t. +2.9i.7j. +1.6L.9s. +1.9i.7k. +2.6J.9u. +e.8S.84. +1.6L.9t. +1.6N.9n. +1.8z.8D. +1.9i.7l. +2.6J.9v. +5.6P.9m. +9.7A.9f. +1.6L.9u. +1.6N.9o. +1.8S.86. +1.9i.7m. +2.6J.9w. +d.7v.9h. +1.6L.9v. +1.6N.9p. +1.9i.7n. +2.6J.9x. +2.7A.9g. +1.6L.9w. +1.6N.9q. +1.9i.7o. +2.6J.9y. +1.6L.9x. +1.6N.9r. +2.6J.9z. +2.8S.87. +7.6v.9A. +0.8C.8C. +1.6L.9y. +1.6N.9s. +1.8S.88. +2.7g.9j. +2.98.7K. +6.6R.9l. +1.6L.9z. +1.6N.9t. +1.9i.7p. +2.7g.9k. +5.6P.9n. +1.6N.9u. +1.8S.8a. +1.9i.7q. +5.6P.9o. +1.6N.9v. +1.9i.7r. +5.6P.9p. +7.6x.9A. +1.6N.9w. +4.7A.9h. +5.6P.9q. +6.6R.9m. +1.6N.9x. +1.8S.8d. +1.98.7L. +1.9i.7s. +5.6P.9r. +1.6N.9y. +2.98.7M. +5.6P.9s. +9.7t.9i. +1.6N.9z. +1.8S.8f. +2.8z.8E. +5.6P.9t. +7.6v.9B. +1.9i.7u. +2.8S.8g. +5.6P.9u. +7.6v.9C. +2.8S.8h. +5.6P.9v. +7.6v.9D. +8.6z.9A. +2.6g.9R. +5.6P.9w. +6.6R.9n. +7.6v.9E. +d.7v.9i. +1.9i.7w. +5.6P.9x. +6.6R.9o. +7.6x.9B. +1.8S.8i. +1.9i.7x. +5.6P.9y. +6.6R.9p. +7.6v.9F. +7.6x.9C. +2.8z.8F. +5.6P.9z. +6.6R.9q. +7.6x.9D. +1.8S.8j. +6.6R.9r. +7.6x.9E. +1.9i.7y. +6.6R.9s. +1.8S.8k. +1.8z.8G. +6.6R.9t. +7.6v.9G. +7.6x.9F. +1.6g.9S. +1.8S.8l. +1.8z.8H. +1.9i.7z. +2.7t.9j. +2.98.7O. +6.6R.9u. +7.6v.9H. +8.6z.9B. +1.6g.9T. +2.7t.9k. +6.6R.9v. +8.6z.9C. +9.7A.9i. +1.8z.8I. +2.98.7P. +6.6R.9w. +8.6z.9D. +1.6B.9B. +1.8z.8J. +6.6R.9x. +7.6v.9I. +7.6x.9G. +8.6z.9E. +1.6B.9C. +1.8S.8n. +1.8z.8K. +2.7v.9j. +6.6R.9y. +6.6v.9J. +7.6x.9H. +1.6B.9D. +1.6g.9U. +1.9i.7B. +2.7v.9k. +6.6R.9z. +8.6z.9F. +1.6B.9E. +1.8S.8p. +1.9i.7C. +2.98.7Q. +1.9i.7D. +7.6x.9I. +1.6B.9F. +1.6g.9V. +1.8S.8q. +1.9i.7E. +7.6x.9J. +1.98.7S. +8.6z.9G. +1.6g.9W. +8.6z.9H. +1.98.7T. +1.9i.7F. +1.6B.9G. +1.6g.9X. +1.6L.9A. +2.7A.9j. +7.6v.9K. +1.6B.9H. +1.7d.9l. +1.8S.8r. +1.9i.7G. +2.7A.9k. +8.6z.9I. +1.8S.8s. +1.9i.7H. +8.6z.9J. +1.9i.7I. +1.6B.9I. +1.7g.9l. +1.9i.7J. +7.6x.9K. +1.6B.9J. +1.7d.9m. +1.8z.8L. +1.98.7X. +1.6N.9A. +1.8z.8M. +7.6v.9L. +1.8z.8N. +1.6L.9B. +1.7g.9m. +1.6L.9C. +1.6L.9D. +1.8S.8u. +7.6x.9L. +8.6z.9K. +1.6L.9E. +1.7d.9n. +1.8S.8v. +7.6v.9M. +1.7d.9o. +1.8S.8w. +1.8z.8O. +1.98.7Y. +5.6P.9A. +7.6v.9N. +1.6B.9K. +1.6L.9F. +1.7d.9p. +1.8z.8P. +1.6N.9B. +1.7d.9q. +1.7g.9n. +1.6N.9C. +1.7d.9r. +1.7g.9o. +1.8S.8x. +1.8z.8Q. +2.9i.7K. +7.6x.9M. +1.6N.9D. +1.7d.9s. +1.7g.9p. +7.6x.9N. +8.6z.9L. +1.6L.9G. +1.6N.9E. +1.7d.9t. +1.7g.9q. +1.8z.8R. +1.98.7+. +5.6v.9O. +1.6L.9H. +1.7d.9u. +1.7g.9r. +5.6v.9P. +1.6B.9L. +1.6N.9F. +1.7d.9v. +1.7g.9s. +2.8S.8z. +1.7d.9w. +1.7g.9t. +1.9i.7L. +5.6P.9B. +7.6v.9Q. +1.6L.9I. +1.7d.9x. +1.7g.9u. +1.98.7/. +2.9i.7M. +4.6x.9O. +5.6P.9C. +8.6z.9M. +9.6R.9A. +9.7t.9l. +1.6L.9J. +1.7d.9y. +1.7g.9v. +1.8z.8T. +4.6x.9P. +5.6P.9D. +8.6z.9N. +1.6N.9G. +1.7d.9z. +1.7g.9w. +1.98.80. +5.6P.9E. +1.6B.9M. +1.6N.9H. +1.7g.9x. +7.6x.9Q. +1.6B.9N. +1.7g.9y. +5.6P.9F. +9.7t.9m. +d.7v.9l. +1.7g.9z. +1.6N.9I. +1.8z.8U. +8.6z.9O. +1.6N.9J. +8.6z.9P. +5.6P.9G. +9.6R.9B. +d.7v.9m. +1.6B.9O. +1.6L.9K. +1.98.81. +5.6P.9H. +8.6z.9Q. +9.6R.9C. +1.6B.9P. +1.98.82. +4.7t.9n. +9.6R.9D. +e.8S.8B. +1.8z.8V. +1.98.83. +2.9i.7O. +9.6R.9E. +9.7t.9o. +1.6B.9Q. +1.98.84. +4.7A.9l. +5.6P.9I. +9.7t.9p. +1.8z.8W. +2.9i.7P. +4.7t.9q. +5.6P.9J. +6.6R.9F. +1.98.86. +9.7t.9r. +d.7v.9n. +1.6L.9L. +1.6N.9K. +4.7t.9s. +d.7v.9o. +4.7A.9m. +4.7t.9t. +d.7v.9p. +1.8z.8X. +1.98.87. +2.9i.7Q. +6.6R.9G. +9.7t.9u. +d.7v.9q. +1.8z.8Y. +1.98.88. +6.6R.9H. +9.7t.9v. +d.7v.9r. +1.98.89. +2.6v.9R. +9.7t.9w. +d.7v.9s. +1.6L.9M. +1.98.8a. +1.9i.7S. +9.7t.9x. +d.7v.9t. +1.6L.9N. +1.6N.9L. +1.98.8b. +5.6P.9K. +6.6R.9I. +9.7t.9y. +d.7v.9u. +1.98.8c. +1.9i.7T. +4.7A.9n. +9.6R.9J. +9.7t.9z. +d.7v.9v. +1.98.8d. +4.7A.9o. +d.7v.9w. +1.98.8e. +1.9i.7U. +2.8z.8Z. +4.7A.9p. +d.7v.9x. +1.98.8f. +4.7A.9q. +d.7v.9y. +1.6L.9O. +1.6N.9M. +1.8z.8+. +2.98.8g. +4.7A.9r. +7.6v.9S. +d.7v.9z. +1.6L.9P. +1.6N.9N. +2.98.8h. +4.7A.9s. +5.6P.9L. +7.6v.9T. +1.9i.7X. +4.7A.9t. +1.6L.9Q. +1.8S.8D. +2.6z.9R. +4.7A.9u. +1.7d.9A. +1.8z.8/. +1.98.8i. +4.7A.9v. +6.6R.9K. +7.6x.9S. +4.7A.9w. +7.6v.9U. +7.6x.9T. +1.6N.9O. +1.98.8j. +2.6B.9R. +4.7A.9x. +5.6P.9M. +1.6N.9P. +1.7g.9A. +1.8z.90. +4.7A.9y. +5.6P.9N. +1.98.8k. +4.7A.9z. +7.6v.9V. +1.6N.9Q. +1.98.8l. +1.9i.7Y. +7.6x.9U. +7.6v.9W. +8.6z.9S. +9.6R.9L. +8.6z.9T. +1.7d.9B. +1.8z.91. +1.98.8m. +5.6P.9O. +7.6v.9X. +7.6x.9V. +1.7d.9C. +1.98.8n. +5.6P.9P. +1.7d.9D. +1.8z.92. +1.98.8o. +1.9i.7+. +7.6x.9W. +1.7d.9E. +1.7g.9B. +1.8z.93. +1.98.8p. +2.8S.8E. +5.6P.9Q. +6.6R.9M. +8.6z.9U. +1.7g.9C. +1.8z.94. +6.6R.9N. +7.6x.9X. +1.7d.9F. +1.7g.9D. +1.98.8q. +1.7g.9E. +1.9i.7/. +8.6z.9V. +1.7g.9F. +1.9i.80. +8.6z.9W. +1.7d.9G. +1.8z.95. +2.6J.9R. +2.8S.8F. +6.6R.9O. +1.7d.9H. +1.98.8r. +6.6R.9P. +8.6z.9X. +1.98.8s. +2.6L.9R. +1.7g.9G. +1.8S.8G. +6.6R.9Q. +9.7t.9A. +1.7d.9I. +1.7g.9H. +1.8S.8H. +1.8z.96. +1.7d.9J. +1.9i.81. +1.7g.9I. +1.9i.82. +d.7v.9A. +1.7g.9J. +1.98.8t. +1.9i.83. +2.6N.9R. +1.6L.9S. +1.9i.84. +1.6L.9T. +1.98.8u. +1.98.8v. +1.9i.86. +9.7t.9B. +1.98.8w. +9.7t.9C. +1.7d.9K. +9.7t.9D. +1.6L.9U. +1.9i.87. +2.6P.9R. +9.7t.9E. +1.6N.9S. +1.98.8x. +1.9i.88. +9.7A.9A. +d.7v.9B. +1.6N.9T. +1.7g.9K. +1.9i.89. +9.7t.9F. +d.7v.9C. +1.6L.9V. +1.98.8y. +1.9i.8a. +d.7v.9D. +1.9i.8b. +d.7v.9E. +1.6L.9W. +1.7d.9L. +1.98.8z. +1.9i.8c. +1.6N.9U. +1.8z.99. +1.9i.8d. +9.7t.9G. +d.7v.9F. +1.6L.9X. +1.8S.8L. +1.9i.8e. +5.6P.9S. +9.7t.9H. +1.7g.9L. +1.8S.8M. +1.9i.8f. +5.6P.9T. +1.6N.9V. +1.8S.8N. +1.8z.9a. +2.6R.9R. +2.9i.8g. +9.7A.9B. +1.7d.9M. +2.8z.9b. +2.9i.8h. +4.7t.9I. +9.7A.9C. +d.7v.9G. +1.6N.9W. +1.7d.9N. +9.7A.9D. +9.7t.9J. +d.7v.9H. +5.6P.9U. +9.7A.9E. +1.6N.9X. +1.7g.9M. +1.8z.9c. +1.9i.8i. +1.7g.9N. +4.7A.9F. +d.7v.9I. +1.8z.9d. +1.9i.8j. +5.6P.9V. +d.7v.9J. +e.8S.8P. +1.7d.9O. +9.6R.9S. +1.7d.9P. +1.8S.8Q. +1.98.8B. +1.9i.8k. +5.6P.9W. +9.6R.9T. +1.9i.8l. +9.7A.9G. +1.7d.9Q. +1.7g.9O. +1.8S.8R. +2.8z.9e. +5.6P.9X. +9.7A.9H. +9.7t.9K. +1.7g.9P. +1.8S.8S. +1.9i.8m. +9.6R.9U. +1.7g.9Q. +1.9i.8n. +9.7A.9I. +1.9i.8o. +9.7A.9J. +d.7v.9K. +1.9i.8p. +9.6R.9V. +9.7t.9L. +1.8z.9f. +1.9i.8q. +9.6R.9W. +2.8z.9g. +9.6R.9X. +1.8S.8U. +d.7v.9L. +9.7t.9M. +1.9i.8r. +9.7A.9K. +9.7t.9N. +1.9i.8s. +2.8S.8V. +d.7v.9M. +d.7v.9N. +1.8S.8W. +1.98.8D. +4.7t.9O. +9.7A.9L. +9.7t.9P. +1.9i.8t. +9.7t.9Q. +1.9i.8u. +d.7v.9O. +e.8S.8X. +1.8S.8Y. +1.9i.8v. +d.7v.9P. +1.9i.8w. +2.7g.9R. +9.7A.9M. +9.7A.9N. +d.7v.9Q. +1.9i.8x. +1.7d.9S. +1.7d.9T. +2.8S.8Z. +2.98.8E. +9.7A.9O. +1.7g.9S. +1.8S.8+. +1.9i.8z. +9.7A.9P. +1.7g.9T. +1.7d.9U. +9.7A.9Q. +1.7d.9V. +1.7g.9U. +2.98.8F. +1.7d.9W. +1.7g.9V. +1.98.8G. +1.7d.9X. +1.98.8H. +1.7g.9W. +2.7t.9R. +1.98.8I. +2.8z.9j. +1.7g.9X. +1.98.8J. +1.9i.8B. +2.8z.9k. +1.98.8K. +2.7v.9R. +9.7t.9S. +9.7t.9T. +d.7v.9S. +e.8S.95. +2.7A.9R. +9.7t.9U. +d.7v.9T. +1.8S.96. +4.7t.9V. +1.98.8L. +d.7v.9U. +1.98.8M. +9.7t.9W. +1.98.8N. +4.7A.9S. +9.7t.9X. +d.7v.9V. +4.7A.9T. +1.9i.8D. +d.7v.9W. +1.98.8O. +d.7v.9X. +1.8S.97. +1.98.8P. +4.7A.9U. +1.98.8Q. +4.7A.9V. +1.98.8R. +4.7A.9W. +1.98.8S. +1.8S.99. +4.7A.9X. +1.98.8T. +2.9i.8E. +1.8z.9l. +2.8S.9b. +1.8z.9m. +1.98.8U. +2.9i.8F. +1.8S.9d. +1.9i.8G. +1.98.8V. +1.9i.8H. +1.8z.9n. +2.8S.9e. +1.8z.9o. +1.98.8W. +1.9i.8I. +1.8z.9p. +1.9i.8J. +1.8z.9q. +1.9i.8K. +1.8z.9r. +1.8z.9s. +1.98.8X. +1.8z.9t. +1.98.8Y. +1.8z.9u. +1.8z.9v. +1.8z.9w. +2.8S.9g. +1.8z.9x. +1.8z.9y. +1.8z.9z. +2.98.8Z. +1.98.8+. +1.9i.8L. +e.8S.9h. +1.9i.8M. +1.98.8/. +1.9i.8N. +1.98.90. +1.9i.8O. +1.9i.8P. +1.98.91. +1.9i.8Q. +1.98.92. +1.9i.8R. +1.98.93. +1.8S.9i. +1.98.94. +1.9i.8T. +1.98.95. +1.9i.8U. +1.98.96. +1.8z.9A. +2.8S.9j. +2.8S.9k. +1.9i.8V. +1.9i.8W. +1.8z.9B. +1.98.97. +1.8z.9C. +1.9i.8X. +1.8z.9D. +1.9i.8Y. +1.8z.9E. +1.8z.9F. +1.98.99. +2.9i.8Z. +1.8z.9G. +1.8z.9H. +1.98.9a. +1.9i.8+. +2.98.9b. +1.8z.9I. +1.8z.9J. +1.98.9c. +1.9i.8/. +1.98.9d. +1.9i.90. +2.98.9e. +1.8z.9K. +1.9i.91. +1.9i.92. +1.9i.93. +1.8S.9l. +1.9i.94. +1.98.9f. +1.8z.9L. +2.98.9g. +1.8S.9m. +1.9i.95. +1.8z.9M. +1.8z.9N. +1.9i.96. +1.8S.9n. +1.98.9h. +1.8S.9o. +1.8S.9p. +1.8S.9q. +1.8z.9O. +1.8S.9r. +1.8z.9P. +1.8S.9s. +1.8S.9t. +1.8z.9Q. +1.8S.9u. +1.8S.9v. +1.9i.97. +1.8S.9w. +1.8S.9x. +1.8S.9y. +1.8S.9z. +1.98.9i. +1.9i.99. +1.9i.9a. +2.9i.9b. +1.9i.9c. +1.9i.9d. +2.98.9j. +2.8z.9R. +2.98.9k. +2.9i.9e. +1.8z.9S. +1.8z.9T. +1.9i.9f. +2.9i.9g. +1.8z.9U. +1.8S.9A. +1.8z.9V. +1.8z.9W. +1.8z.9X. +1.8S.9B. +1.8S.9C. +1.8S.9D. +1.8S.9E. +1.8S.9F. +1.8S.9G. +1.8S.9H. +1.98.9l. +1.9i.9i. +1.8S.9I. +1.8S.9J. +1.98.9m. +1.98.9n. +1.98.9o. +2.9i.9j. +1.8S.9K. +1.98.9p. +2.9i.9k. +1.98.9q. +1.98.9r. +1.98.9s. +1.98.9t. +1.98.9u. +1.8S.9L. +1.98.9v. +1.98.9w. +1.98.9x. +1.98.9y. +1.98.9z. +1.8S.9M. +1.8S.9N. +e.8S.9O. +e.8S.9P. +e.8S.9Q. +1.9i.9l. +1.9i.9m. +1.98.9A. +2.8S.9R. +1.9i.9n. +1.9i.9o. +1.9i.9p. +1.9i.9q. +1.98.9B. +1.9i.9r. +1.98.9C. +1.9i.9s. +2.8S.9S. +1.98.9D. +1.9i.9t. +2.8S.9T. +1.98.9E. +1.9i.9u. +1.9i.9v. +1.98.9F. +1.9i.9w. +1.9i.9x. +2.8S.9U. +1.9i.9y. +1.9i.9z. +1.98.9G. +2.8S.9V. +1.98.9H. +2.8S.9W. +1.98.9I. +2.8S.9X. +1.98.9J. +1.98.9K. +1.98.9L. +1.98.9M. +1.98.9N. +1.9i.9A. +1.98.9O. +1.98.9P. +1.98.9Q. +1.9i.9B. +1.9i.9C. +1.9i.9D. +1.9i.9E. +1.9i.9F. +1.9i.9G. +1.9i.9H. +1.9i.9I. +1.9i.9J. +2.98.9R. +1.9i.9K. +1.98.9S. +1.98.9T. +1.9i.9L. +1.98.9U. +1.98.9V. +1.9i.9M. +1.9i.9N. +1.98.9W. +1.98.9X. +1.9i.9O. +1.9i.9P. +1.9i.9Q. +2.9i.9R. +1.9i.9S. +1.9i.9T. +1.9i.9U. +1.9i.9V. +1.9i.9W. +1.9i.9X. +2.9Y.0. +2.9Y.1. +2.9Y.2. +2.9Y.3. +2.9Y.4. +2.9Y.5. +2.9Y.6. +2.9Y.7. +2.9Y.8. +2.9Y.9. +2.9Y.a. +2.9Y.b. +2.9Y.c. +2.9Y.d. +2.9Y.e. +2.9Y.f. +2.9Y.g. +2.9Y.h. +2.9Y.i. +2.9Y.j. +2.9Y.k. +2.9Y.l. +2.9Y.m. +2.9Y.n. +2.9Y.o. +2.9Y.p. +2.9Y.q. +2.9Y.r. +2.9Y.s. +2.9Y.t. +2.9Y.u. +2.9Y.v. +2.9Y.w. +2.9Y.x. +2.9Y.y. +2.9Y.z. +2.9Y.A. +2.9Y.B. +2.9Y.C. +2.9Y.D. +2.9Y.E. +2.9Y.F. +2.9Y.G. +2.9Y.H. +2.9Y.I. +2.9Y.J. +2.9Y.K. +2.9Y.L. +2.9Y.M. +2.9Y.N. +2.9Y.O. +2.9Y.P. +2.9Y.Q. +2.9Y.R. +2.9Y.S. +2.9Y.T. +2.9Y.U. +2.9Y.V. +2.9Y.W. +2.9Y.X. +2.9Y.Y. +2.9Y.Z. +2.9Y.+. +2.9Y./. +2.9Y.10. +2.9Y.11. +2.9Y.12. +2.9Y.13. +2.9Y.14. +2.9Y.15. +2.9Y.16. +2.9Y.17. +2.9Y.18. +2.9Y.19. +2.9Y.1a. +2.9Y.1b. +2.9Y.1c. +2.9Y.1d. +2.9Y.1e. +2.9Y.1f. +2.9Y.1g. +2.9Y.1h. +2.9Y.1i. +2.9Y.1j. +2.9Y.1k. +2.9Y.1l. +2.9Y.1m. +2.9Y.1n. +2.9Y.1o. +2.9Y.1p. +2.9Y.1q. +2.9Y.1r. +2.9Y.1s. +2.9Y.1t. +2.9Y.1u. +2.9Y.1v. +2.9Y.1w. +2.9Y.1x. +2.9Y.1y. +2.9Y.1z. +2.9Y.1A. +2.9Y.1B. +2.9Y.1C. +2.9Y.1D. +2.9Y.1E. +2.9Y.1F. +2.9Y.1G. +2.9Y.1H. +2.9Y.1I. +2.9Y.1J. +2.9Y.1K. +2.9Y.1L. +2.9Y.1M. +2.9Y.1N. +2.9Y.1O. +2.9Y.1P. +2.9Y.1Q. +2.9Y.1R. +2.9Y.1S. +2.9Y.1V. +2.9Y.1W. +2.9Y.1X. +2.9Y.1Y. +2.9Y.1Z. +2.9Y.1+. +2.9Y.1/. +2.9Y.20. +2.9Y.21. +2.9Y.22. +2.9Y.23. +2.9Y.24. +2.9Y.25. +2.9Y.26. +2.9Y.27. +2.9Y.28. +2.9Y.29. +2.9Y.2a. +2.9Y.2c. +2.9Y.2d. +2.9Y.2g. +2.9Y.2m. +2.9Y.2n. +2.9Y.2p. +2.9Y.2q. +2.9Y.2w. +2.9Y.2x. +2.9Y.2y. +2.9Y.2z. +2.9Y.2A. +2.9Y.2B. +2.9Y.2C. +2.9Y.2D. +2.9Y.2E. +2.9Y.2F. +2.9Y.2G. +2.9Y.2H. +2.9Y.2I. +2.9Y.2J. +2.9Y.2K. +2.9Y.2L. +2.9Y.2M. +2.9Y.2N. +2.9Y.2O. +2.9Y.2P. +2.9Y.2Q. +2.9Y.2R. +2.9Y.2S. +2.9Y.2U. +2.9Y.2V. +2.9Y.2W. +2.9Y.2Y. +2.9Y.2+. +2.9Y.2/. +2.9Y.30. +2.9Y.31. +2.9Y.32. +2.9Y.33. +2.9Y.34. +2.9Y.35. +2.9Y.37. +2.9Y.39. +2.9Y.3e. +2.9Y.3f. +2.9Y.3g. +2.9Y.3h. +2.9Y.3i. +2.9Y.3m. +2.9Y.3q. +2.9Y.3r. +2.9Y.3t. +2.9Y.3v. +2.9Y.3x. +2.9Y.3y. +2.9Y.3C. +2.9Y.3G. +2.9Y.3J. +2.9Y.3K. +2.9Y.3L. +2.9Y.3M. +2.9Y.3N. +2.9Y.3R. +2.9Y.3S. +2.9Y.3Y. +2.9Y.40. +2.9Y.4c. +2.9Y.4j. +2.9Y.4q. +2.9Y.53. +2.9Y.55. +2.9Y.58. +2.9Y.5e. +2.9Y.5s. +2.9Y.5u. +2.9Y.5F. +2.9Y.5G. +2.9Y.5L. +2.9Y.6o. +2.9Y.6p. +2.9Y.6s. +2.9Y.6u. +2.9Y.6v. +2.9Y.6x. +2.9Y.6z. +2.9Y.6B. +2.9Y.6E. +2.9Y.6H. +2.9Y.6J. +2.9Y.6L. +2.9Y.6M. +2.9Y.6O. +2.9Y.6P. +2.9Y.6R. +2.9Y.6V. +2.9Y.6+. +2.9Y.71. +2.9Y.7t. +2.9Y.7A. +2.9Y.7V. +2.9Y.7Z. +2.9Y.85. +2.9Y.8C. +2.9Y.9Y. +`; diff --git a/packages/frontend/src/scripts/emojiKitchen/emojiMixer.ts b/packages/frontend/src/scripts/emojiKitchen/emojiMixer.ts new file mode 100644 index 0000000000..a573ec56bd --- /dev/null +++ b/packages/frontend/src/scripts/emojiKitchen/emojiMixer.ts @@ -0,0 +1,84 @@ +import * as data from './emojiData.js'; + +const mixEmojiUrl = (r, c) => { + let padZeros = r < 20220500; // Revisions before 0522 had preceding zeros + c[0] = c[0].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u"); + c[1] = c[1].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u"); + return `https://www.gstatic.com/android/keyboard/emojikitchen/${r}/u${c[0]}/u${c[0]}_u${c[1]}.png`; +}; + +const convertBase = (value, from_base, to_base) => { + value = value.toString(); + var range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/'.split(''); + var from_range = range.slice(0, from_base); + var to_range = range.slice(0, to_base); + + var dec_value = value.split('').reverse().reduce(function (carry, digit, index) { + if (from_range.indexOf(digit) === -1) throw new Error('Invalid digit `' + digit + '` for base ' + from_base + '.'); + return carry += from_range.indexOf(digit) * (Math.pow(from_base, index)); + }, 0); + + var new_value = ''; + while (dec_value > 0) { + new_value = to_range[dec_value % to_base] + new_value; + dec_value = (dec_value - (dec_value % to_base)) / to_base; + } + return new_value || '0'; +}; + +const emojiSplit = String.fromCodePoint(0x200d); +const hexEncodeEmoji = (chr) => { + if (chr.length === 3) return hexEncodeEmoji(chr.slice(0, 2)) + '-' + hexEncodeEmoji(chr.slice(2, chr.length)); + else if (chr.length === 2) { + const hi = chr.charCodeAt(0); + const lo = chr.charCodeAt(1); + if (0xD800 <= hi && hi < 0xDC00 && 0xDC00 <= lo && lo < 0xE000) { + return (0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)).toString(16); + } + return hi.toString(16) + '-' + lo.toString(16); + } + else if (chr.length === 1) { + return chr.charCodeAt(0).toString(16); + } + else { + const sp = chr.split(emojiSplit); + if (sp.length !== 2) return ''; + return hexEncodeEmoji(sp[0]) + '-200d-' + hexEncodeEmoji(sp[1]); + } +}; + +const pairsMatchingMap = match => { + const mv = match[0]; + let [d, c1, c2] = mv.split('.'); + c1 = data.points[convertBase(c1, 64, 10)]; + c2 = data.points[convertBase(c2, 64, 10)]; + d = data.revisions[convertBase(d, 64, 10)]; + + return mixEmojiUrl(d, [c1, c2]); +}; + +const fixedEncodeIndex = (emoji) => { + const e = hexEncodeEmoji(emoji); + let ei = data.points.indexOf(e); + if (ei === -1) { + ei = data.points.indexOf(e + "-fe0f"); + if (ei === -1) throw new Error('no match emoji'); + } + + return ei; +} + +export const mixEmoji = (emoji1, emoji2) => { + try { + const encordedEmoji1 = convertBase(fixedEncodeIndex(emoji1), 10, 64); + const encordedEmoji2 = convertBase(fixedEncodeIndex(emoji2), 10, 64); + return [ + ...data.pairs.matchAll(new RegExp("^.*\\." + encordedEmoji1 + "\\." + encordedEmoji2 + "\\.$", "gm")), + ...data.pairs.matchAll(new RegExp("^.*\\." + encordedEmoji2 + "\\." + encordedEmoji1 + "\\.$", "gm")) + ].map(pairsMatchingMap).pop(); + } + catch { + console.error('convert failed.', hexEncodeEmoji(emoji1), hexEncodeEmoji(emoji2)); + return; + } +}; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 3d2b8b1621..cf565492d4 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -74,7 +74,34 @@ async function deleteFile(file: Misskey.entities.DriveFile) { fileId: file.id, }); } +async function MultideleteFile(files: Misskey.entities.DriveFile[] | null) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveMultiFileDeleteConfirm', { name: files.length }), + }); + if (canceled) return; + files.forEach((e)=>{ + misskeyApi('drive/files/delete', { + fileId: e.id, + }); + }) +} +function isSensitive(files: Misskey.entities.DriveFile[] | null ,sensitive:boolean) { + files.forEach((e)=>{ + misskeyApi('drive/files/update', { + fileId: e.id, + isSensitive: sensitive, + }).catch(err => { + os.alert({ + type: 'error', + title: i18n.ts.error, + text: err.message, + }); + }); + }) + +} export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { const isImage = file.type.startsWith('image/'); let menu; @@ -138,3 +165,22 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss return menu; } +export function getDriveMultiFileMenu(files: string[] & boolean): MenuItem[] { + let menu; + menu = [{ + text: i18n.ts.unmarkAsSensitive, + icon: 'ti ti-eye', + action: () => isSensitive(files,false), + },{ + text: i18n.ts.markAsSensitive, + icon: 'ti ti-eye-exclamation', + action: () => isSensitive(files,true), + }, { + text: i18n.ts.delete, + icon: 'ti ti-trash', + danger: true, + action: () => MultideleteFile(files), + }]; + + return menu; +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index a68a3ac202..060f860305 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -132,13 +132,10 @@ export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): Men icon: 'ti ti-exclamation-circle', text, action: (): void => { - const localUrl = `${url}/notes/${note.id}`; - let noteInfo = ''; - if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`; - noteInfo += `Local Note: ${localUrl}\n`; + const u = note.url ?? note.uri ?? `${url}/notes/${note.id}`; os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: note.user, - initialComment: `${noteInfo}-----\n`, + initialNoteId: note.id, }, {}, 'closed'); }, }; @@ -209,6 +206,10 @@ export function getNoteMenu(props: { }); } + function edit(): void { + os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel, updateMode: true }); + } + function toggleFavorite(favorite: boolean): void { claimAchievement('noteFavorited1'); os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { @@ -425,6 +426,11 @@ export function getNoteMenu(props: { ), ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ { type: 'divider' }, + appearNote.userId === $i.id && $i.policies.canEditNote ? { + icon: 'ti ti-edit', + text: i18n.ts.edit, + action: edit, + } : undefined, appearNote.userId === $i.id ? { icon: 'ti ti-edit', text: i18n.ts.deleteAndEdit, diff --git a/packages/frontend/src/scripts/get-timeline-available.ts b/packages/frontend/src/scripts/get-timeline-available.ts new file mode 100644 index 0000000000..30197441ea --- /dev/null +++ b/packages/frontend/src/scripts/get-timeline-available.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; +export const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); +export const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index cbef119c27..3e70a5b67e 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -81,6 +81,18 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } + async function toggleBlockAndMute() { + if (user.isBlocking && user.isMuted || !user.isBlocking && !user.isMuted) { + toggleMute(); + toggleBlock(); + } + if (user.isMuted && !user.isBlocking) { + toggleMute(); + } else if (!user.isMuted && user.isBlocking) { + toggleBlock(); + } + } + async function toggleWithReplies() { os.apiWithDialog('following/update', { userId: user.id, @@ -327,6 +339,10 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, + }, { + icon: 'ti ti-user-off', + text: user.isBlocking || user.isMuted ? i18n.ts.unblock + '&' + i18n.ts.unmute : i18n.ts.block + '&' + i18n.ts.mute, + action: toggleBlockAndMute, }]); if (user.isFollowed) { diff --git a/packages/frontend/src/scripts/note-drafts.ts b/packages/frontend/src/scripts/note-drafts.ts new file mode 100644 index 0000000000..67abc004a3 --- /dev/null +++ b/packages/frontend/src/scripts/note-drafts.ts @@ -0,0 +1,99 @@ +import * as Misskey from 'misskey-js'; +import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; +import type { DeleteScheduleEditorModelValue } from '@/components/MkDeleteScheduleEditor.vue'; +import { miLocalStorage } from '@/local-storage.js'; +import { get as idbGet, set as idbSet } from '@/scripts/idb-proxy.js'; + +export type NoteDraft = { + updatedAt: Date; + type: keyof NoteKeys; + uniqueId: string; + auxId: string | null; + data: { + text: string; + useCw: boolean; + cw: string | null; + visibility: (typeof Misskey.noteVisibilities)[number]; + localOnly: boolean; + files: Misskey.entities.DriveFile[]; + poll: PollEditorModelValue | null; + scheduledNoteDelete: DeleteScheduleEditorModelValue | null; + }; +}; + +type NoteKeys = { + note: () => unknown, + reply: (replyId: string) => unknown, + quote: (renoteId: string) => unknown, + channel: (channelId: string) => unknown, +} + +export async function migrate(userId: string) { + const raw = miLocalStorage.getItem('drafts'); + if (!raw) return; + + const drafts = JSON.parse(raw) as Record<string, NoteDraft>; + const keys = Object.keys(drafts); + const newDrafts: Record<string, NoteDraft> = {}; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const [type, id] = key.split(':'); + if (type === 'note' && id !== userId) continue; + const keyType = type === 'renote' ? 'quote' : type as keyof NoteKeys; + const keyId = type === 'note' ? null : id; + const uniqueId = Date.now().toString() + i.toString(); + const newKey = getKey(keyType, uniqueId, keyId as string); + newDrafts[newKey] = { + ...drafts[key], + uniqueId, + type: keyType, + auxId: keyId, + }; + delete drafts[key]; + } + + if (Object.keys(newDrafts).length === 0) return; + await idbSet(`drafts::${userId}`, newDrafts); + miLocalStorage.setItem('drafts', JSON.stringify(drafts)); +} + +function getKey<T extends keyof NoteKeys>(type: T, uniqueId: string, ...args: Parameters<NoteKeys[T]>) { + let key = `${type}:${uniqueId}`; + for (const arg of args) { + if (arg != null) key += `:${arg}`; + } + return key; +} + +export async function getAll(userId: string) { + const drafts = await idbGet(`drafts::${userId}`); + return (drafts ?? {}) as Record<string, NoteDraft | undefined>; +} + +export async function get<T extends keyof NoteKeys>(type: T, userId: string, uniqueId: string, ...args: Parameters<NoteKeys[T]>) { + const key = getKey(type, uniqueId, ...args); + const draft = await getAll(userId); + return draft[key] ?? null; +} + +export async function set<T extends keyof NoteKeys>(type: T, userId: string, uniqueId: string, draft: NoteDraft['data'], ...args: Parameters<NoteKeys[T]>) { + const drafts = await getAll(userId); + const key = getKey(type, uniqueId, ...args); + drafts[key] = { + updatedAt: new Date(), + type, + uniqueId, + auxId: args[0] ?? null, + data: JSON.parse(JSON.stringify(draft)) as NoteDraft['data'], + }; + console.log(drafts); + await idbSet(`drafts::${userId}`, drafts); +} + +export async function remove<T extends keyof NoteKeys>(type: T, userId: string, uniqueId: string, ...args: Parameters<NoteKeys[T]>) { + const drafts = await getAll(userId); + const key = getKey(type, uniqueId, ...args); + delete drafts[key]; + await idbSet(`drafts::${userId}`, drafts); +} diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts index 109ea1fcc3..740be17f1b 100644 --- a/packages/frontend/src/scripts/nyaize.ts +++ b/packages/frontend/src/scripts/nyaize.ts @@ -3,25 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -const enRegex1 = /(?<=n)a/gi; -const enRegex2 = /(?<=morn)ing/gi; -const enRegex3 = /(?<=every)one/gi; -const koRegex1 = /[나-낳]/g; -const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; -const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; - export function nyaize(text: string): string { return text // ja-JP .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') // en-US - .replace(enRegex1, x => x === 'A' ? 'YA' : 'ya') - .replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan') - .replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan') + .replace(/na/gi, (match) => match === 'NA' ? 'NYA' : 'nya') + .replace(/morning/gi, (match) => match === 'MORNING' ? 'MORNYAN' : 'mornyan') + .replace(/everyone/gi, (match) => match === 'EVERYONE' ? 'EVERYNYAN' : 'everynyan') // ko-KR - .replace(koRegex1, match => String.fromCharCode( + .replace(/[나-낳]/g, match => String.fromCharCode( match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), )) - .replace(koRegex2, '다냥') - .replace(koRegex3, '냥'); + .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') + .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); } diff --git a/packages/frontend/src/scripts/uhoize.ts b/packages/frontend/src/scripts/uhoize.ts new file mode 100644 index 0000000000..ffe799d1a8 --- /dev/null +++ b/packages/frontend/src/scripts/uhoize.ts @@ -0,0 +1,44 @@ +export function uhoize(str) { + const punctuation = ['。', '!', '?', '.', '!', '?']; + let lines = str.split('\n'); + let voice = 'ウホ'; + return lines.map(line => { + if (Math.floor(Math.random() * 2) === 0) { + voice = 'ウホッ' + } else { + voice = 'ウホ' + } + let lastChar = line.trim().slice(-1); + if (punctuation.includes(lastChar)) { + let lineWithoutPunctuation = line.trim().slice(0, -1); + return lineWithoutPunctuation + voice + lastChar; + } else { + return line + voice; + } + }).join('\n'); +} +/* +export function uhoize(text) { + const gorillaNoises = ['ウホ', 'ウホホ', 'ウホッ']; + let result = ''; + let noiseIndex = 0; + for (let i = 0; i < text.length; i++) { + if (!(/[、。.,\/#!$%\^&\*;:{}=\-_`~()]/.test(text[i]))) { + if (/[^\x00-\x7F]/.test(text[i])) { + noiseIndex = Math.floor(Math.random() * 3); + const japaneseNoises = ['ウホ', 'ウホホ', 'ウホッ']; + result += japaneseNoises[noiseIndex]; + } else { + noiseIndex = Math.floor(Math.random() * 2); + const englishNoises = ['uho', 'uhoho']; + result += englishNoises[noiseIndex]; + } + }else{ + result += text[i]; + } + if (text.length*1.3 < result.length) return result + + } + return result; +} +*/ diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 4ab5bc1697..0142c10f42 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -39,6 +39,13 @@ export function useNoteCapture(props: { if ($i && (body.userId === $i.id)) { note.value.myReaction = reaction; + console.log(note.value.myReactions); + if (!note.value.myReactions) { + note.value.myReactions = []; + note.value.myReactions.push(reaction); + } else if (!note.value.myReactions.includes(reaction)) { + note.value.myReactions.push(reaction); + } } break; } @@ -55,6 +62,7 @@ export function useNoteCapture(props: { if ($i && (body.userId === $i.id)) { note.value.myReaction = null; + note.value.myReactions = note.value.myReactions.filter(r => r !== reaction); } break; } @@ -75,6 +83,13 @@ export function useNoteCapture(props: { break; } + case 'updated': { + note.value.updatedAt = new Date().toISOString(); + note.value.cw = body.cw; + note.value.text = body.text; + break; + } + case 'deleted': { props.isDeletedRef.value = true; break; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index b5b8593f5e..5a3b307d85 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -9,6 +9,8 @@ import { miLocalStorage } from './local-storage.js'; import type { SoundType } from '@/scripts/sound.js'; import { Storage } from '@/pizzax.js'; import { hemisphere } from '@/scripts/intl-const.js'; +import { isLocalTimelineAvailable, isGlobalTimelineAvailable } from '@/scripts/get-timeline-available.js'; +import { instance } from '@/instance.js'; interface PostFormAction { title: string, @@ -52,13 +54,13 @@ export type SoundStore = { volume: number; } - export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; export const noteViewInterruptors: NoteViewInterruptor[] = []; export const notePostInterruptors: NotePostInterruptor[] = []; export const pageViewInterruptors: PageViewInterruptor[] = []; +export const { bannerDark, bannerLight, iconDark, iconLight } = instance; // TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) // あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない @@ -88,10 +90,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: true, }, - rememberNoteVisibility: { - where: 'account', - default: false, - }, defaultNoteVisibility: { where: 'account', default: 'public' as (typeof Misskey.noteVisibilities)[number], @@ -100,6 +98,25 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, + defaultHomeNoteLocalOnly: { + where: 'account', + default: false, + }, defaultFollowersNoteLocalOnly: { + where: 'account', + default: false, + }, + draftSavingBehavior: { + where: 'account', + default: 'auto' as 'auto' | 'manual', + }, + rememberNoteVisibility: { + where: 'account', + default: false, + }, + rememberReactionAcceptance: { + where: 'account', + default: false, + }, uploadFolder: { where: 'account', default: null as string | null, @@ -108,6 +125,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 'yyyy-MM-dd HH-mm-ss [{{number}}]', }, + disableNoteDrafting: { + where: 'account', + default: false, + }, keepOriginalUploading: { where: 'account', default: false, @@ -124,6 +145,74 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: [], }, + reactions1: { + where: 'account', + default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], + }, + pinnedEmojis1: { + where: 'account', + default: [], + }, + reactions2: { + where: 'account', + default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], + }, + pinnedEmojis2: { + where: 'account', + default: [], + }, + reactions3: { + where: 'account', + default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], + }, + pinnedEmojis3: { + where: 'account', + default: [], + }, + reactions4: { + where: 'account', + default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], + }, + pinnedEmojis4: { + where: 'account', + default: [], + }, + reactions5: { + where: 'account', + default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], + }, + pinnedEmojis5: { + where: 'account', + default: [], + }, + pickerProfileName: { + where: 'account', + default: 'default', + }, + pickerProfileName1: { + where: 'account', + default: '1', + }, + pickerProfileName2: { + where: 'account', + default: '2', + }, + pickerProfileName3: { + where: 'account', + default: '3', + }, + pickerProfileName4: { + where: 'account', + default: '4', + }, + pickerProfileName5: { + where: 'account', + default: '5', + }, + pickerProfileDefault: { + where: 'account', + default: 1, + }, reactionAcceptance: { where: 'account', default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, @@ -146,8 +235,25 @@ export const defaultStore = markRaw(new Storage('base', { 'search', '-', 'ui', + 'cacheclear', ], }, + timelineHeader: { + where: 'deviceAccount', + default: [ + 'home', + ...(isLocalTimelineAvailable ? [ + 'local', + 'social', + ] : []), + ...(isGlobalTimelineAvailable ? [ + 'global', + ] : []), + 'lists', + 'antennas', + 'channels', + ] as TimelineHeaderItem[], + }, visibility: { where: 'deviceAccount', default: 'public' as (typeof Misskey.noteVisibilities)[number], @@ -190,6 +296,7 @@ export const defaultStore = markRaw(new Storage('base', { withRenotes: true, withSensitive: true, onlyFiles: false, + withCw: false, }, }, }, @@ -197,7 +304,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'deviceAccount', default: [] as Misskey.entities.UserList[], }, - + pinnedChannels: { + where: 'deviceAccount', + default: [] as Misskey.entities.Channel[], + }, overridedDeviceKind: { where: 'device', default: null as null | 'smartphone' | 'tablet' | 'desktop', @@ -230,6 +340,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + alwaysShowPlayer: { + where: 'device', + default: true, + }, + alwaysExpandTweet: { + where: 'device', + default: false, + }, enableQuickAddMfmFunction: { where: 'device', default: false, @@ -242,6 +360,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + enableDataSaverMode: { + where: 'device', + default: false, + }, + enableCellularWithDataSaver: { + where: 'device', + default: false, + }, disableShowingAnimatedImages: { where: 'device', default: window.matchMedia('(prefers-reduced-motion)').matches, @@ -278,6 +404,22 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + topBarNameShown: { + where: 'device', + default: false, + }, + showHomeTimeline: { + where: 'device', + default: true, + }, + showLocalTimeline: { + where: 'device', + default: true, + }, + showSocialTimeline: { + where: 'device', + default: true, + }, showGapBetweenNotesInTimeline: { where: 'device', default: false, @@ -286,6 +428,26 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + gamingMode: { + where: 'device', + default: true, + }, + gamingType: { + where: 'device', + default: 'dark', + }, + indicatorCounterToggle: { + where: 'device', + default: 'true', + }, + bannerUrl: { + where: 'device', + default: bannerDark, + }, + iconUrl: { + where: 'device', + default: iconDark, + }, instanceTicker: { where: 'device', default: 'remote' as 'none' | 'remote' | 'always', @@ -310,6 +472,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: [] as string[], }, + enablehanntenn: { + where: 'device', + default: false, + }, recentlyUsedUsers: { where: 'device', default: [] as string[], @@ -350,6 +516,46 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 3, }, + specifiedColor: { + where: 'device', + default: '#FFFF64', + }, + followerColor: { + where: 'device', + default: '#FF00FF', + }, + homeColor: { + where: 'device', + default: '#00FFFF', + }, + localOnlyColor: { + where: 'device', + default: '#2b2c41', + }, + numberOfGamingSpeed: { + where: 'device', + default: 44, + }, + remoteLocalTimeline: { + where: 'device', + default: [], + }, + onlyAndWithSave: { + where: 'device', + default: false, + }, + onlyFiles: { + where: 'device', + default: false, + }, + withReplies: { + where: 'device', + default: true, + }, + withRenotes: { + where: 'device', + default: true, + }, showNoteActionsOnlyHover: { where: 'device', default: false, @@ -358,6 +564,18 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + showMediaTimeline: { + where: 'device', + default: true, + }, + showGlobalTimeline: { + where: 'device', + default: true, + }, + showVisibilityColor: { + where: 'device', + default: false, + }, reactionsDisplaySize: { where: 'device', default: 'medium' as 'small' | 'medium' | 'large', @@ -406,6 +624,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, + hideMutedNotes: { + where: 'device', + default: false, + }, disableStreamingTimeline: { where: 'device', default: false, @@ -522,6 +744,7 @@ interface Watcher { */ import lightTheme from '@/themes/l-light.json5'; import darkTheme from '@/themes/d-green-lime.json5'; +import { TimelineHeaderItem } from '@/timeline-header.js'; export class ColdDeviceStorage { public static default = { diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index fb4dd9e6f1..63e1b5a6b3 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -12,7 +12,7 @@ --marginHalf: 10px; --margin: var(--marginFull); - + --gamingspeed: 43s; // switch dynamically --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); @@ -164,6 +164,14 @@ rt { height: 1em; border-radius: 100%; background: currentColor; + &.gamingdark{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + -moz-animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + } + } ._indicateCounter { diff --git a/packages/frontend/src/timeline-header.ts b/packages/frontend/src/timeline-header.ts new file mode 100644 index 0000000000..ee978115d5 --- /dev/null +++ b/packages/frontend/src/timeline-header.ts @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { computed, reactive, ref } from 'vue'; +import { i18n } from '@/i18n.js'; +import { + antennasCache, + userChannelFollowingsCache, + userChannelsCache, + userFavoriteListsCache, + userListsCache, +} from '@/cache.js'; +import { isLocalTimelineAvailable, isGlobalTimelineAvailable } from '@/scripts/get-timeline-available.js'; +import { defaultStore } from '@/store.js'; +import { $i } from '@/account.js'; + +export type TimelineHeaderItem = + 'home' | + 'local' | + 'social' | + 'global' | + 'lists' | + 'antennas' | + 'channels' | + `list:${string}` | + `channel:${string}` | + `antenna:${string}` | + 'media' | + `customTimeline:${string}`; + +type TimelineHeaderItemsDef = { + title: string; + icon: string; + iconOnly?: boolean; // わからん +} + +const lists = await userListsCache.fetch(); +const userChannels = await userChannelsCache.fetch(); +const userChannelFollowings = await userChannelFollowingsCache.fetch(); +const userFavoriteLists = await userFavoriteListsCache.fetch(); +const antenna = await antennasCache.fetch(); + +export const timelineHeaderItemDef = reactive<Partial<Record<TimelineHeaderItem, TimelineHeaderItemsDef>>>({ + home: { + title: i18n.ts._timelines.home, + icon: 'ti ti-home', + iconOnly: true, + }, + ...(isLocalTimelineAvailable ? { + local: { + title: i18n.ts._timelines.local, + icon: 'ti ti-planet', + iconOnly: true, + }, + social: { + title: i18n.ts._timelines.social, + icon: 'ti ti-universe', + iconOnly: true, + } } : {}), + ...(isGlobalTimelineAvailable ? { global: { + title: i18n.ts._timelines.global, + icon: 'ti ti-whirl', + iconOnly: true, + } } : {}), + lists: { + icon: 'ti ti-list', + title: i18n.ts.lists, + iconOnly: true, + }, + antennas: { + icon: 'ti ti-antenna', + title: i18n.ts.antennas, + iconOnly: true, + }, + channels: { + icon: 'ti ti-device-tv', + title: i18n.ts.channel, + iconOnly: true, + }, + ...lists.reduce((acc, l) => { + acc['list:' + l.id] = { + title: i18n.ts.lists + ':' + l.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), + ...userChannels.reduce((acc, l) => { + acc['channel:' + l.id] = { + title: i18n.ts.channel + ':' + l.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), + ...userChannelFollowings.reduce((acc, l) => { + acc['channel:' + l.id] = { + title: i18n.ts.channel + ':' + l.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), + ...userFavoriteLists.reduce((acc, l) => { + acc['channel:' + l.id] = { + title: i18n.ts.channel + ':' + l.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), + ...antenna.reduce((acc, l) => { + acc['antenna:' + l.id] = { + title: i18n.ts.antennas + ':' + l.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), + ...defaultStore.reactiveState.remoteLocalTimeline.value.reduce((acc, t) => { + acc['remoteLocalTimeline:' + t.host.replace('https://', '')] = { + title: 'remoteLocaltimeline:' + t.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), + +}); + diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 76692a0490..037698eebc 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -25,8 +25,13 @@ function toolsMenuItems(): MenuItem[] { }, { type: 'link', to: '/clicker', - text: '🍪👈', + text: '●👈', icon: 'ti ti-cookie', + }, { + type: 'link', + to: '/bubble-game', + text: i18n.ts.bubbleGame, + icon: 'ti ti-apple', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index 24318900c4..15bace455a 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -5,58 +5,170 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div :class="$style.root"> <div :class="$style.top"> - <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> + <div :class="$style.banner" :style="{ backgroundImage: `url(${ bannerUrl })` }"></div> <button class="_button" :class="$style.instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> + <img :src="iconUrl" alt="" :class="$style.instanceIcon"/> </button> </div> <div :class="$style.middle"> - <MkA :class="$style.item" :activeClass="$style.active" to="/" exact> - <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> + <MkA + :class="[$style.item, { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" + :activeClass="$style.active" to="/" exact + > + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ + i18n.ts.timeline + }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" :class="$style.divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"> - <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> - <i v-else class="_indicatorCircle"></i> - </span> + <component + :is="navbarItemDef[item].to ? 'MkA' : 'button'" + v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" + :class="[$style.item, { [$style.active]: gaming === '' && navbarItemDef[item].active, [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" + :activeClass="$style.active" :to="navbarItemDef[item].to" + v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}" + > + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span + :class="$style.itemText" + >{{ navbarItemDef[item].title }}</span> + <span + v-if="navbarItemDef[item].indicated" + :class="[$style.itemIndicator,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'}]" + > + <span v-if="navbarItemDef[item].indicateValue && indicatorCounterToggle" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span><i + v-else class="_indicatorCircle" + ></i></span> </component> </template> <div :class="$style.divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" :class="$style.item" :activeClass="$style.active" to="/admin"> - <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> + <MkA + v-if="$i.isAdmin || $i.isModerator" + :class="[$style.item, { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" + :activeClass="$style.active" to="/admin" + > + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span + :class="$style.itemText" + >{{ i18n.ts.controlPanel }}</span> </MkA> - <button :class="$style.item" class="_button" @click="more"> - <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> + <button + :class="[$style.item, { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" + class="_button" @click="more" + > + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ + i18n.ts.more + }}</span> + <span + v-if="otherMenuItemIndicated" + :class="[$style.itemIndicator,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'}]" + ><i + class="_indicatorCircle" + ></i></span> </button> - <MkA :class="$style.item" :activeClass="$style.active" to="/settings"> - <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> + <MkA + :class="[$style.item, { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" + :activeClass="$style.active" to="/settings" + > + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ + i18n.ts.settings + }}</span> </MkA> </div> <div :class="$style.bottom"> - <button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post"> - <i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span> + <button + class="_button" + :class="[$style.post ,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light',}]" + data-cy-open-post-form @click="os.post" + > + <i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ + i18n.ts.note + }}</span> </button> <button class="_button" :class="$style.account" @click="openAccountMenu"> - <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" class="_nowrap" :user="$i"/> + <MkAvatar :user="$i" :class="$style.avatar"/> + <MkAcct :class="$style.acct" class="_nowrap" :user="$i"/> </button> </div> </div> </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, toRef } from 'vue'; +import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue'; import { openInstanceMenu } from './common.js'; -import * as os from '@/os.js'; +import * as os from '@/os'; import { navbarItemDef } from '@/navbar.js'; -import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { defaultStore } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; +import { $i, openAccountMenu as openAccountMenu_ } from '@/account'; +import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store'; +import { i18n } from '@/i18n'; +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); +const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle')); +let bannerUrl = ref(defaultStore.state.bannerUrl); +let iconUrl = ref(defaultStore.state.iconUrl); +function hexToRgb(hex) { + // 16進数のカラーコードから "#" を除去 + hex = hex.replace(/^#/, ''); + + // 16進数をRGBに変換 + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return `${r},${g},${b}`; +} + +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +document.documentElement.style.setProperty('--homeColor', hexToRgb(defaultStore.state.homeColor)); +document.documentElement.style.setProperty('--followerColor', hexToRgb(defaultStore.state.followerColor)); +document.documentElement.style.setProperty('--specifiedColor', hexToRgb(defaultStore.state.specifiedColor)); +document.documentElement.style.setProperty('--localOnlyColor', hexToRgb(defaultStore.state.localOnlyColor)); +document.documentElement.style.setProperty('--gamingspeed', defaultStore.state.numberOfGamingSpeed + 's'); + +let gaming = ref(); +if (darkMode.value) { + bannerUrl.value = bannerDark; + iconUrl.value = iconDark; +} else { + bannerUrl.value = bannerLight; + iconUrl.value = iconLight; +} +watch(darkMode, () => { + if (darkMode.value) { + bannerUrl.value = bannerDark; + iconUrl.value = iconDark; + } else { + bannerUrl.value = bannerLight; + iconUrl.value = iconLight; + } +}); +// gaming.valueに新しい値を代入する +if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; +} else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; +} else { + gaming.value = ''; +} + +watch(darkMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +watch(gamingMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); const menu = toRef(defaultStore.state, 'menu'); const otherMenuItemIndicated = computed(() => { for (const def in navbarItemDef) { @@ -73,181 +185,301 @@ function openAccountMenu(ev: MouseEvent) { } function more() { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, { - }, 'closed'); + os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, {}, 'closed'); } </script> <style lang="scss" module> .root { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .top { - position: sticky; - top: 0; - z-index: 1; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + position: sticky; + top: 0; + z-index: 1; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); } .banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center center; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + -webkit-mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.75) 20%); + mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.75) 20%); } .instance { - position: relative; - display: block; - text-align: center; - width: 100%; + position: relative; + display: block; + text-align: center; + width: 100%; } .instanceIcon { - display: inline-block; - width: 38px; - aspect-ratio: 1; + display: inline-block; + width: 38px; + aspect-ratio: 1; } .bottom { - position: sticky; - bottom: 0; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + position: sticky; + bottom: 0; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); } .post { - position: relative; - display: block; - width: 100%; - height: 40px; - color: var(--fgOnAccent); - font-weight: bold; - text-align: left; + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; - &:before { - content: ""; - display: block; - width: calc(100% - 38px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - } + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + + &:hover, &.active { + &:before { + background: var(--accentLighten); + } + } + + &.gamingLight:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + + &.gamingLight:hover, &.gamingLight.active { + &.gamingLight:before { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + &.gamingDark:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingDark:hover, &.gamingDark.active { + &.gamingDark:before { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } - &:hover, &.active { - &:before { - background: var(--accentLighten); - } - } } .postIcon { - position: relative; - margin-left: 30px; - margin-right: 8px; - width: 32px; + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; } .account { - position: relative; - display: flex; - align-items: center; - padding-left: 30px; - width: 100%; - text-align: left; - box-sizing: border-box; - margin-top: 16px; + position: relative; + display: flex; + align-items: center; + padding-left: 30px; + width: 100%; + text-align: left; + box-sizing: border-box; + margin-top: 16px; } .avatar { - display: block; - flex-shrink: 0; - position: relative; - width: 32px; - aspect-ratio: 1; - margin-right: 8px; + display: block; + flex-shrink: 0; + position: relative; + width: 32px; + aspect-ratio: 1; + margin-right: 8px; } .acct { - display: block; - flex-shrink: 1; - padding-right: 8px; + display: block; + flex-shrink: 1; + padding-right: 8px; } .middle { - flex: 1; + flex: 1; } .divider { - margin: 16px 16px; - border-top: solid 0.5px var(--divider); + margin: 16px 16px; + border-top: solid 0.5px var(--divider); } .item { - position: relative; - display: block; - padding-left: 24px; - line-height: 2.85rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - color: var(--navFg); + position: relative; + display: block; + padding-left: 24px; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + color: var(--navFg); - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } - &.active { - color: var(--navActive); - } + &.active { + color: var(--navActive); + } - &:hover, &.active { - &:before { - content: ""; - display: block; - width: calc(100% - 24px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--accentedBg); - } - } + &:hover, &.active { + &:before { + content: ""; + display: block; + width: calc(100% - 24px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accentedBg); + } + } + + &.gamingDark:hover, &.gamingDark.active { + text-decoration: none; + color: black; + + &.gamingDark:before { + content: ""; + display: block; + height: 100%; + aspect-ratio: 1; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + &.gamingLight:hover, &.gamingLight.active { + text-decoration: none; + color: white; + + &.gamingLight:before { + content: ""; + display: block; + height: 100%; + aspect-ratio: 1; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } } .itemIcon { - position: relative; - width: 32px; - margin-right: 8px; + position: relative; + width: 32px; + margin-right: 8px; } .itemIndicator { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + + &.gamingDark { + color: white; + } + + &.gamingLight { + color: black; + } position: absolute; top: 0; left: 20px; @@ -263,7 +495,79 @@ function more() { } .itemText { - position: relative; - font-size: 0.9em; + position: relative; + font-size: 0.9em; +} + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 2e7ae9da9b..ae59ca7c54 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -3,84 +3,169 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License --> <template> -<div :class="[$style.root, { [$style.iconOnly]: iconOnly }]"> - <div :class="$style.body"> - <div :class="$style.top"> - <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> - <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> - </button> - </div> - <div :class="$style.middle"> - <MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact> - <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> - </MkA> - <template v-for="item in menu"> - <div v-if="item === '-'" :class="$style.divider"></div> - <component - :is="navbarItemDef[item].to ? 'MkA' : 'button'" - v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" - v-tooltip.noDelay.right="navbarItemDef[item].title" - class="_button" - :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" - :activeClass="$style.active" - :to="navbarItemDef[item].to" - v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}" - > - <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"> - <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> - <i v-else class="_indicatorCircle"></i> - </span> - </component> - </template> - <div :class="$style.divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> - <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> - </MkA> - <button class="_button" :class="$style.item" @click="more"> - <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> - </button> - <MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings"> - <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> - </MkA> - </div> - <div :class="$style.bottom"> - <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post"> - <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> - </button> - <button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> - <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> - </button> - </div> - </div> -</div> + <div :class="[$style.root, { [$style.iconOnly]: iconOnly }]"> + <div :class="$style.body"> + <div :class="$style.top"> + <div :class="$style.banner" :style="{ backgroundImage: `url(${ bannerUrl })` }"></div> + <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" + @click="openInstanceMenu"> + <img :src="iconUrl" alt="" :class="$style.instanceIcon"/> + </button> + </div> + <div :class="$style.middle"> + <MkA v-tooltip.noDelay.right="i18n.ts.timeline" + :class="[$style.item, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" + :activeClass="$style.active" to="/" exact> + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ + i18n.ts.timeline + }}</span> + </MkA> + <template v-for="item in menu"> + <div v-if="item === '-'" :class="$style.divider"></div> + <component + :is="navbarItemDef[item].to ? 'MkA' : 'button'" + v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" + v-tooltip.noDelay.right="navbarItemDef[item].title" + class="_button" + :class="[$style.item, { [$style.active]: gamingType === '' && navbarItemDef[item].active, [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" + :activeClass="$style.active" + :to="navbarItemDef[item].to" + v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}" + > + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span + :class="$style.itemText">{{ navbarItemDef[item].title }}</span> + <span v-if="navbarItemDef[item].indicated" + :class="[$style.itemIndicator ,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]"> + <span v-if="navbarItemDef[item].indicateValue && indicatorCounterToggle" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span><i + v-else class="_indicatorCircle"></i></span> + </component> + </template> + <div :class="$style.divider"></div> + <MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" + :class="[$style.item, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" + :activeClass="$style.active" to="/admin"> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span + :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> + </MkA> + <button class="_button" + :class="[$style.item, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" + @click="more"> + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ + i18n.ts.more + }}</span> + <span v-if="otherMenuItemIndicated" + :class="[$style.itemIndicator,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]"><i + class="_indicatorCircle"></i></span> + </button> + <MkA v-tooltip.noDelay.right="i18n.ts.settings" + :class="[$style.item, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]" + :activeClass="$style.active" + to="/settings"> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span + :class="$style.itemText">{{ i18n.ts.settings }}</span> + </MkA> + </div> + <div :class="$style.bottom"> + <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" + :class="[$style.post ,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light',}]" + data-cy-open-post-form + @click="os.post"> + <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span + :class="$style.postText,{[$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light',}">{{ + i18n.ts.note + }}</span> + </button> + <button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" + :class="[$style.account]" @click="openAccountMenu"> + <MkAvatar :user="$i" :class="$style.avatar"/> + <MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> + </button> + </div> + </div> + </div> </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref, watch } from 'vue'; -import { openInstanceMenu } from './common.js'; -import * as os from '@/os.js'; -import { navbarItemDef } from '@/navbar.js'; -import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { defaultStore } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; +import {computed, defineAsyncComponent, ref, watch} from 'vue'; +import {openInstanceMenu} from './common.js'; +import * as os from '@/os'; +import {navbarItemDef} from '@/navbar.js'; +import {$i, openAccountMenu as openAccountMenu_} from '@/account'; +import {bannerDark, bannerLight, defaultStore, iconDark, iconLight} from '@/store'; +import {i18n} from '@/i18n'; +import {instance} from '@/instance'; +const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle')); +function hexToRgb(hex) { + hex = hex.replace(/^#/, ''); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return `${r},${g},${b}`; +} + +document.documentElement.style.setProperty('--homeColor', hexToRgb(defaultStore.state.homeColor)); +document.documentElement.style.setProperty("--followerColor",hexToRgb(defaultStore.state.followerColor)); +document.documentElement.style.setProperty("--specifiedColor",hexToRgb(defaultStore.state.specifiedColor)) +document.documentElement.style.setProperty("--localOnlyColor",hexToRgb(defaultStore.state.localOnlyColor)) +document.documentElement.style.setProperty('--gamingspeed', defaultStore.state.numberOfGamingSpeed+'s'); const iconOnly = ref(false); +let bannerUrl = computed(defaultStore.makeGetterSetter('bannerUrl')); +let iconUrl = ref(); +let gamingType = computed(defaultStore.makeGetterSetter('gamingType')); + +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +const enablehanntenn = computed(defaultStore.makeGetterSetter('enablehanntenn')); + +if (darkMode.value) { + bannerUrl.value = enablehanntenn.value ? bannerLight : bannerDark; + iconUrl.value = enablehanntenn.value ? iconLight : iconDark; +} else { + bannerUrl.value = enablehanntenn.value ? bannerDark : bannerLight; + iconUrl.value = enablehanntenn.value ? iconDark : iconLight; +} + +if (darkMode.value && gamingMode.value) { + gamingType.value = 'dark'; +} else if (!darkMode.value && gamingMode.value) { + gamingType.value = 'light'; +} else { + gamingType.value = ''; +} + +watch([darkMode,gamingMode], () => { + + if (darkMode.value) { + bannerUrl.value = enablehanntenn.value ? bannerLight : bannerDark; + iconUrl.value = enablehanntenn.value ? iconLight : iconDark; + } else { + bannerUrl.value = enablehanntenn.value ? bannerDark : bannerLight; + iconUrl.value = enablehanntenn.value ? iconDark : iconLight; + } + + if (darkMode.value && gamingMode.value) { + gamingType.value = 'dark'; + } else if (!darkMode.value && gamingMode.value) { + gamingType.value = 'light'; + } else { + gamingType.value = ''; + } + +}) const menu = computed(() => defaultStore.state.menu); const otherMenuItemIndicated = computed(() => { - for (const def in navbarItemDef) { - if (menu.value.includes(def)) continue; - if (navbarItemDef[def].indicated) return true; - } - return false; + for (const def in navbarItemDef) { + if (menu.value.includes(def)) continue; + if (navbarItemDef[def].indicated) return true; + } + return false; }); const calcViewState = () => { - iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon'); + iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon'); }; calcViewState(); @@ -88,20 +173,19 @@ calcViewState(); window.addEventListener('resize', calcViewState); watch(defaultStore.reactiveState.menuDisplay, () => { - calcViewState(); + calcViewState(); }); function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ - withExtraOperation: true, - }, ev); + openAccountMenu_({ + withExtraOperation: true, + }, ev); } function more(ev: MouseEvent) { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { - src: ev.currentTarget ?? ev.target, - }, { - }, 'closed'); + os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { + src: ev.currentTarget ?? ev.target, + }, {}, 'closed'); } </script> @@ -110,118 +194,192 @@ function more(ev: MouseEvent) { --nav-width: 250px; --nav-icon-only-width: 80px; - flex: 0 0 var(--nav-width); - width: var(--nav-width); - box-sizing: border-box; + flex: 0 0 var(--nav-width); + width: var(--nav-width); + box-sizing: border-box; } .body { - position: fixed; - top: 0; - left: 0; - z-index: 1001; - width: var(--nav-icon-only-width); - height: 100dvh; - box-sizing: border-box; - overflow: auto; - overflow-x: clip; - overscroll-behavior: contain; - background: var(--navBg); - contain: strict; - display: flex; - flex-direction: column; + position: fixed; + top: 0; + left: 0; + z-index: 1001; + width: var(--nav-icon-only-width); + height: 100dvh; + box-sizing: border-box; + overflow: auto; + overflow-x: clip; + overscroll-behavior: contain; + background: var(--navBg); + contain: strict; + display: flex; + flex-direction: column; } .root:not(.iconOnly) { - .body { - width: var(--nav-width); - } + .body { + width: var(--nav-width); + } - .top { - position: sticky; - top: 0; - z-index: 1; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - } + .top { + position: sticky; + top: 0; + z-index: 1; + padding: 20px 0; + //background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } - .banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center center; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - } + .banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + -webkit-mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.75) 20%); + mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.75) 20%); + } - .instance { - position: relative; - display: block; - text-align: center; - width: 100%; - } + .instance { + position: relative; + display: block; + text-align: center; + width: 100%; + } - .instanceIcon { - display: inline-block; - width: 38px; - aspect-ratio: 1; - } + .instanceIcon { + display: inline-block; + width: 38px; + aspect-ratio: 1; + } - .bottom { - position: sticky; - bottom: 0; - padding-top: 20px; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - } + .bottom { + position: sticky; + bottom: 0; + padding-top: 20px; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } - .post { - position: relative; - display: block; - width: 100%; - height: 40px; - color: var(--fgOnAccent); - font-weight: bold; - text-align: left; + .post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; - &:before { - content: ""; - display: block; - width: calc(100% - 38px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - } + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } - &:hover, &.active { - &:before { - background: var(--accentLighten); - } - } - } + &:hover, &.active { + &:before { + background: var(--accentLighten); + } + } - .postIcon { - position: relative; - margin-left: 30px; - margin-right: 8px; - width: 32px; - } + &.gamingLight { + color: white !important; + } - .postText { - position: relative; - } + &.gamingDark { + color: black !important; + } + + &.gamingLight:before { + color: white; + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + + &.gamingLight:hover, &.gamingLight.active { + + color: white; + + &.gamingLight:before { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + &.gamingDark:before { + color: black !important; + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingDark:hover, &.gamingDark.active { + color: black !important; + + &.gamingDark:before { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + } + + .postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; + } + + .postText { + position: relative; + } .account { position: relative; @@ -234,77 +392,166 @@ function more(ev: MouseEvent) { overflow: clip; } - .avatar { - display: block; - flex-shrink: 0; - position: relative; - width: 32px; - aspect-ratio: 1; - margin-right: 8px; - } + .avatar { + display: block; + flex-shrink: 0; + position: relative; + width: 32px; + aspect-ratio: 1; + margin-right: 8px; + } - .acct { - display: block; - flex-shrink: 1; - padding-right: 8px; - } + .acct { + display: block; + flex-shrink: 1; + padding-right: 8px; + } - .middle { - flex: 1; - } + .middle { + flex: 1; + } - .divider { - margin: 16px 16px; - border-top: solid 0.5px var(--divider); - } + .divider { + margin: 16px 16px; + border-top: solid 0.5px var(--divider); + } - .item { - position: relative; - display: block; - padding-left: 30px; - line-height: 2.85rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - color: var(--navFg); + .item { + position: relative; + display: block; + padding-left: 30px; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + color: var(--navFg); - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } + &.gamingDark { + color: var(--navFg); + } - &.active { - color: var(--navActive); - } + &.gamingLight { + color: var(--navFg); + } - &:hover, &.active { - color: var(--accent); + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } - &:before { - content: ""; - display: block; - width: calc(100% - 34px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--accentedBg); - } - } - } + &.active { + color: var(--navActive); + } - .itemIcon { - position: relative; - width: 32px; - margin-right: 8px; - } + &:hover, &.active { + color: var(--accent); + + &:before { + content: ""; + display: block; + width: calc(100% - 34px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accentedBg); + } + } + + &.gamingDark:hover { + color: black; + + background-size: 1800% 1800%; + text-decoration: none; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingDark.active { + color: black; + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingDark:hover, &.gamingDark.active { + color: black; + + &.gamingDark:before { + content: ""; + display: block; + width: calc(100% - 34px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + &.gamingLight:hover { + color: white; + background-size: 1800% 1800% !important; + text-decoration: none; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + + &.gamingLight:active { + color: white; + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + + &.gamingLight:hover, &.gamingLight.active { + color: white; + + + &.gamingLight:before { + content: ""; + display: block; + width: calc(100% - 34px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + } + + .itemIcon { + position: relative; + width: 32px; + margin-right: 8px; + } .itemIndicator { position: absolute; @@ -321,89 +568,181 @@ function more(ev: MouseEvent) { font-size: 10px; } } + .itemIndicator { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; - .itemText { - position: relative; - font-size: 0.9em; - } + &.gamingDark { + color: white; + } + + &.gamingDark.active { + color: black; + } + + &.gamingLight { + color: black; + } + + &.gamingLight.active { + color: white; + } + } + + .itemText { + position: relative; + font-size: 0.9em; + } } .root.iconOnly { - flex: 0 0 var(--nav-icon-only-width); - width: var(--nav-icon-only-width); + flex: 0 0 var(--nav-icon-only-width); + width: var(--nav-icon-only-width); - .body { - width: var(--nav-icon-only-width); - } + .body { + width: var(--nav-icon-only-width); + } - .top { - position: sticky; - top: 0; - z-index: 1; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - } + .top { + position: sticky; + top: 0; + z-index: 1; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } - .instance { - display: block; - text-align: center; - width: 100%; - } + .instance { + display: block; + text-align: center; + width: 100%; + } - .instanceIcon { - display: inline-block; - width: 30px; - aspect-ratio: 1; - } + .instanceIcon { + display: inline-block; + width: 30px; + aspect-ratio: 1; + } - .bottom { - position: sticky; - bottom: 0; - padding-top: 20px; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - } + .bottom { + position: sticky; + bottom: 0; + padding-top: 20px; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } - .post { - display: block; - position: relative; - width: 100%; - height: 52px; - text-align: center; + .post { + display: block; + position: relative; + width: 100%; + color: black; + height: 52px; - &:before { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - width: 52px; - aspect-ratio: 1/1; - border-radius: 100%; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - } + text-align: center; - &:hover, &.active { - &:before { - background: var(--accentLighten); - } - } - } + &:before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 52px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } - .postIcon { - position: relative; - color: var(--fgOnAccent); - } + &:hover, &.active { + &:before { + background: var(--accentLighten); + } + } - .postText { - display: none; - } + &.gamingLight:before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 52px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + color: white !important; + } + + &.gamingLight:hover, &.gamingLight.active { + + &.gamingLight:before { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + color: white !important; + } + } + + &.gamingDark:before { + color: black !important; + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 52px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite; + } + + &.gamingDark:hover, &.gamingDark.active { + color: black !important; + + &.gamingDark:before { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + + } + + .postIcon { + position: relative; + color: var(--fgOnAccent); + } + + .postText { + display: none; + } .account { display: block; @@ -413,68 +752,129 @@ function more(ev: MouseEvent) { overflow: clip; } - .avatar { - display: inline-block; - width: 38px; - aspect-ratio: 1; - } + .avatar { + display: inline-block; + width: 38px; + aspect-ratio: 1; + } - .acct { - display: none; - } + .acct { + display: none; + } - .middle { - flex: 1; - } + .middle { + flex: 1; + } - .divider { - margin: 8px auto; - width: calc(100% - 32px); - border-top: solid 0.5px var(--divider); - } + .divider { + margin: 8px auto; + width: calc(100% - 32px); + border-top: solid 0.5px var(--divider); + } - .item { - display: block; - position: relative; - padding: 18px 0; - width: 100%; - text-align: center; + .item { + display: block; + position: relative; + padding: 18px 0; + width: 100%; + text-align: center; - &:hover, &.active { - text-decoration: none; - color: var(--accent); + &.gamingLight { + color: var(--fg); + } - &:before { - content: ""; - display: block; - height: 100%; - aspect-ratio: 1; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--accentedBg); - } + &:hover, &.active { + text-decoration: none; + color: var(--accent); - > .icon, - > .text { - opacity: 1; - } - } - } + &.gamingDark { + color: black; + } - .itemIcon { - display: block; - margin: 0 auto; - opacity: 0.7; - } + &.gamingLight { + color: var(--fg); + } - .itemText { - display: none; - } + &:before { + content: ""; + display: block; + height: 100%; + aspect-ratio: 1; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accentedBg); + } + + > .icon, + > .text { + opacity: 1; + } + } + + &.gamingDark:hover, &.gamingDark.active { + text-decoration: none; + color: black; + + &.gamingDark:before { + color: black; + content: ""; + display: block; + height: 100%; + aspect-ratio: 1; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + + &.gamingLight:hover, &.gamingLight.active { + text-decoration: none; + color: white !important; + + &.gamingLight:before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 52px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + -moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important; + } + } + } + + .itemIcon { + display: block; + margin: 0 auto; + opacity: 0.7; + } + + .itemText { + display: none; + } .itemIndicator { position: absolute; @@ -492,5 +892,103 @@ function more(ev: MouseEvent) { font-size: 10px; } } + .itemIndicator { + position: absolute; + top: 6px; + left: 24px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + + &.gamingDark { + color: white; + } + + &.gamingDark.active { + color: black; + } + + &.gamingLight { + color: black; + } + + &.gamingLight.active { + color: white; + } + } + + +} + +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} + +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 80f9572689..06be19e89d 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -23,12 +23,17 @@ import { defaultStore } from '@/store.js'; const zIndex = os.claimZIndex('high'); const hasDisconnected = ref(false); +let timeoutId = ref<number>(); function onDisconnected() { - hasDisconnected.value = true; + window.clearTimeout(timeoutId.value); + timeoutId.value = window.setTimeout(() => { + hasDisconnected.value = true; + }, 1000 * 10); } function resetDisconnected() { + window.clearTimeout(timeoutId.value); hasDisconnected.value = false; } @@ -36,9 +41,12 @@ function reload() { location.reload(); } +useStream().on('_connected_', resetDisconnected); useStream().on('_disconnected_', onDisconnected); onUnmounted(() => { + window.clearTimeout(timeoutId); + useStream().off('_connected_', resetDisconnected); useStream().off('_disconnected_', onDisconnected); }); </script> diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index c894d28de4..3c1140e3a6 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -21,7 +21,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> <i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> <span v-if="navbarItemDef[item].indicated" class="indicator"> - <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> + <span v-if="navbarItemDef[item].indicateValue && indicatorCounterToggle" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> <i v-else class="_indicatorCircle"></i> </span> </component> @@ -63,6 +63,7 @@ import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; const WINDOW_THRESHOLD = 1400; +const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle')); const menu = ref(defaultStore.state.menu); const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 7ac0e9fe7b..424ddb3e9c 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -54,7 +54,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> <i :class="$style.navButtonIcon" class="ti ti-bell"></i> <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> - <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> + <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 && indicatorCounterToggle ? '99+' : $i.unreadNotificationsCount }}</span> </span> </button> <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> @@ -119,6 +119,7 @@ import { mainRouter } from '@/router/main.js'; import { MenuItem } from '@/types/menu.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); +const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle')); const columnComponents = { main: XMainColumn, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index dac6ae7db5..425eac9207 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -30,7 +30,7 @@ export type Column = { channelId?: string; roleId?: string; excludeTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global'; + tl?: 'home' | 'local' |'media' | 'social' | 'global'; withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index f2c8b85c3a..448d0da6e3 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -8,6 +8,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <i v-if="column.tl === 'home'" class="ti ti-home"></i> <i v-else-if="column.tl === 'local'" class="ti ti-planet"></i> <i v-else-if="column.tl === 'social'" class="ti ti-universe"></i> + <i v-else-if="column.tl === 'media'" class="ti ti-photo"></i> <i v-else-if="column.tl === 'global'" class="ti ti-whirl"></i> <span style="margin-left: 8px;">{{ column.name }}</span> </template> @@ -100,6 +101,8 @@ async function setType() { value: 'home' as const, text: i18n.ts._timelines.home, }, { value: 'local' as const, text: i18n.ts._timelines.local, + }, { + value: 'media' as const, text: i18n.ts._timelines.media, }, { value: 'social' as const, text: i18n.ts._timelines.social, }, { diff --git a/packages/frontend/src/ui/twilike.sidebar.vue b/packages/frontend/src/ui/twilike.sidebar.vue new file mode 100644 index 0000000000..3c75b5f52b --- /dev/null +++ b/packages/frontend/src/ui/twilike.sidebar.vue @@ -0,0 +1,260 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="npcljfve" :class="{ iconOnly }"> + <div class="about"> + <button v-click-anime class="item _button" @click="openInstanceMenu"> + <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> + </button> + </div> + <MkA v-click-anime class="item index" activeClass="active" to="/" exact> + <i class="ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> + </MkA> + <template v-for="item in menu"> + <div v-if="item === '-'" class="divider"></div> + <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> + <i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> + <span v-if="navbarItemDef[item].indicated" class="indicator"> + <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> + <i v-else class="_indicatorCircle"></i> + </span> + </component> + </template> + <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> + <i class="ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> + </MkA> + <button v-click-anime class="item _button" @click="more"> + <i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> + <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> + </button> + <MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> + <i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> + </MkA> + + <div class="post" data-cy-open-post-form @click="os.post"> + <MkButton class="button" gradate full rounded> + <div class="content"> + <i class="ti ti-pencil ti-fw"></i> + <span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span> + </div> + </MkButton> + </div> + + <button v-click-anime class="item _button account" @click="openAccountMenu"> + <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/> + </button> + + <!--<MisskeyLogo class="misskey"/>--> +</div> +</template> + +<script lang="ts" setup> +import { defineAsyncComponent, computed, watch, ref, shallowRef, onUnmounted, onMounted } from 'vue'; +import { openInstanceMenu } from './_common_/common.js'; +// import { host } from '@/config.js'; +import * as os from '@/os.js'; +import { navbarItemDef } from '@/navbar.js'; +import { openAccountMenu as openAccountMenu_, $i } from '@/account.js'; +import MkButton from '@/components/MkButton.vue'; +// import { StickySidebar } from '@/scripts/sticky-sidebar.js'; +// import { mainRouter } from '@/router.js'; +//import MisskeyLogo from '@assets/client/misskey.svg'; +import { defaultStore } from '@/store.js'; +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import { mainRouter } from '@/router/main.js'; + +const WINDOW_THRESHOLD = 1400; + +const menu = ref(defaultStore.state.menu); +const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); +const otherNavItemIndicated = computed<boolean>(() => { + for (const def in navbarItemDef) { + if (menu.value.includes(def)) continue; + if (navbarItemDef[def].indicated) return true; + } + return false; +}); +const el = shallowRef<HTMLElement>(); +// let accounts = $ref([]); +// let connection = $ref(null); +const iconOnly = ref(false); +const settingsWindowed = ref(false); + +function calcViewState() { + iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon'); + settingsWindowed.value = (window.innerWidth > WINDOW_THRESHOLD); +} + +function more(ev: MouseEvent) { + os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { + src: ev.currentTarget ?? ev.target, + }, {}, 'closed'); +} + +function openAccountMenu(ev: MouseEvent) { + openAccountMenu_({ + withExtraOperation: true, + }, ev); +} + +let updateIconOnly: () => void; +watch(defaultStore.reactiveState.menuDisplay, () => { + calcViewState(); +}); +onMounted(() => { + updateIconOnly = () => { + iconOnly.value = window.innerWidth < 1300; + }; + + window.addEventListener('resize', updateIconOnly); + + updateIconOnly(); // 初期値を設定 +}); + +onUnmounted(() => { + window.removeEventListener('resize', updateIconOnly); +}); +</script> + +<style lang="scss" scoped> + + .npcljfve { + $ui-font-size: 1em; // TODO: どこかに集約したい + $nav-icon-only-width: 78px; // TODO: どこかに集約したい + $avatar-size: 32px; + $avatar-margin: 8px; + overflow-y: auto; + padding: 0 16px; + box-sizing: border-box; + width: 260px; + height: 100%; + &.iconOnly { + flex: 0 0 $nav-icon-only-width; + width: $nav-icon-only-width !important; + + > .divider { + margin: 8px auto; + width: calc(100% - 32px); + } + + > .post { + > .button { + width: 46px; + height: 46px; + padding: 0; + } + } + + > .item { + padding-left: 0; + width: 100%; + text-align: center; + font-size: $ui-font-size * 1.5; + line-height: 3.7rem; + + > i, + > .avatar { + margin-right: 0; + } + + > i { + left: 10px; + } + + > .text { + display: none; + } + } + } + + > .post { + position: sticky; + top: 0; + z-index: 1; + padding: 16px 0; + background: var(--bg); + + > .button { + min-width: 0; + min-height: 52px; + } + .content { + font-size: larger; + } + } + + > .about { + fill: currentColor; + padding: 8px 0 16px 0; + text-align: center; + + > .item { + display: block; + width: 32px; + margin: 0 auto; + + img { + display: block; + width: 100%; + } + } + } + + > .item { + position: relative; + display: block; + font-size: $ui-font-size * 1.2; + line-height: 3rem; + text-overflow: ellipsis; + overflow: hidden; + height: 58px; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + padding: 8px 4px 4px; + > i { + width: 32px; + } + + > i, + > .avatar { + margin-right: $avatar-margin; + } + + > .avatar { + width: $avatar-size; + height: $avatar-size; + vertical-align: middle; + } + + > .indicator { + position: absolute; + top: 0; + left: 0; + color: var(--navIndicator); + font-size: 8px; + animation: global-blink 1s infinite; + + &:has(.itemIndicateValueIcon) { + animation: none; + left: auto; + right: 20px; + } + } + + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } + + &.active { + color: var(--navActive); + } + } + } +</style> diff --git a/packages/frontend/src/ui/twilike.vue b/packages/frontend/src/ui/twilike.vue new file mode 100644 index 0000000000..cd1609acba --- /dev/null +++ b/packages/frontend/src/ui/twilike.vue @@ -0,0 +1,463 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.root,{ wallpaper }]" :style="`--globalHeaderHeight:${globalHeaderHeight}px`"> + <XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/> + + <div :class="[$style.columns,{ [$style.fullView]:fullView, [$style.withGlobalHeader]: showMenuOnTop }]"> + <div v-if="!showMenuOnTop && isDesktop" :class="$style.sidebar"> + <XSidebar/> + </div> + <div v-else-if="!pageMetadata?.needWideArea && isDesktop" ref="widgetsLeft" :class="[$style.widgets,$style.left]"> + <XWidgets place="left" :marginTop="'var(--margin)'"/> + </div> + + <main :class="[$style.main, {[$style.wide]: pageMetadata?.needWideArea} ]" @contextmenu.stop="onContextmenu"> + <RouterView/> + </main> + + <div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" :class="$style.widgets"> + <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'"/> + </div> + </div> + + <Transition :name="defaultStore.state.animation ? 'tray-back' : ''"> + <div + v-if="widgetsShowing" + class="tray-back _modalBg" + @click="widgetsShowing = false" + @touchstart.passive="widgetsShowing = false" + ></div> + </Transition> + + <XCommon/> + <div v-if="!isDesktop" ref="navFooter" :class="$style.nav"> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> + <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> + <i :class="$style.navButtonIcon" class="ti ti-bell"></i> + <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> + <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> + </span> + </button> + <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button> + <button :class="$style.navButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> + </div> + <Transition + :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + > + <div + v-if="drawerMenuShowing || widgetsShowing" + :class="$style.menuDrawerBg" + class="_modalBg" + @click="drawerMenuShowing = false; widgetsShowing = false " + @touchstart.passive="drawerMenuShowing = false; widgetsShowing = false" + ></div> + </Transition> + + <Transition + :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + > + <div v-if="drawerMenuShowing" :class="$style.menuDrawer"> + <XDrawerMenu/> + </div> + </Transition> + <Transition + :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" + > + <div v-if="widgetsShowing" :class="$style.widgetsDrawer"> + <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button> + <XWidgets/> + </div> + </Transition> +</div> +</template> + +<script lang="ts" setup> +import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef, watch } from 'vue'; +import XSidebar from './twilike.sidebar.vue'; +import XCommon from './_common_/common.vue'; +import { instanceName, ui } from '@/config.js'; +import * as os from '@/os.js'; +import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; +import { defaultStore } from '@/store.js'; +import { i18n } from '@/i18n.js'; +import { miLocalStorage } from '@/local-storage.js'; +import { mainRouter } from '@/router/main.js'; +import { $i } from '@/account.js'; +import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; +const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); +const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); + +const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); + +const DESKTOP_THRESHOLD = 700; + +const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); +const drawerMenuShowing = ref(false); +const pageMetadata = ref<null | PageMetadata>(null); +const widgetsShowing = ref(false); +const fullView = ref(false); +const globalHeaderHeight = ref(0); +const wallpaper = miLocalStorage.getItem('wallpaper') != null; +const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top'); +const widgetsLeft = ref<HTMLElement>(); +const widgetsRight = ref<HTMLElement>(); + +provide('router', mainRouter); +provideMetadataReceiver((metadataGetter) => { + const info = metadataGetter(); + pageMetadata.value = info; + if (mainRouter.currentRoute.value.path.split('/').slice(1)[0] === 'settings') { + pageMetadata.value.needWideArea = true; + } + if (pageMetadata.value) { + if (isRoot.value && pageMetadata.value.title === instanceName) { + document.title = pageMetadata.value.title; + } else { + document.title = `${pageMetadata.value.title} | ${instanceName}`; + } + } +}); +provideReactiveMetadata(pageMetadata); +provide('shouldHeaderThin', showMenuOnTop.value); +provide('forceSpacerMin', true); + +function onContextmenu(ev: MouseEvent) { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + }; + if (isLink(ev.target)) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; + if (window.getSelection().toString() !== '') return; + const path = mainRouter.getCurrentPath(); + os.contextMenu([{ + type: 'label', + text: path, + }, { + icon: fullView.value ? 'ti ti-minimize' : 'ti ti-maximize', + text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView, + action: () => { + fullView.value = !fullView.value; + }, + }, { + icon: 'ti ti-window-maximize', + text: i18n.ts.openInWindow, + action: () => { + os.pageWindow(path); + }, + }], ev); +} + +document.documentElement.style.overflowY = 'scroll'; + +defaultStore.loaded.then(() => { + if (defaultStore.state.widgets.length === 0) { + defaultStore.set('widgets', [{ + name: 'calendar', + id: 'a', place: null, data: {}, + }, { + name: 'notifications', + id: 'b', place: null, data: {}, + }, { + name: 'trends', + id: 'c', place: null, data: {}, + }]); + } +}); + +onMounted(() => { + window.addEventListener('resize', () => { + isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD); + }, { passive: true }); +}); +</script> + +<style lang="scss" module> +$ui-font-size: 1em; +$widgets-hide-threshold: 1200px; +.transition_widgetsDrawer_enterActive, +.transition_widgetsDrawer_leaveActive { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.transition_widgetsDrawer_enterFrom, +.transition_widgetsDrawer_leaveTo { + opacity: 0; + transform: translateX(240px); +} +.tray-enter-active, +.tray-leave-active { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.tray-enter-from, +.tray-leave-active { + opacity: 0; + transform: translateX(240px); +} + +.tray-back-enter-active, +.tray-back-leave-active { + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.tray-back-enter-from, +.tray-back-leave-active { + opacity: 0; +} + +.transition_menuDrawerBg_enterActive, +.transition_menuDrawerBg_leaveActive { + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.transition_menuDrawerBg_enterFrom, +.transition_menuDrawerBg_leaveTo { + opacity: 0; +} + +.transition_menuDrawer_enterActive, +.transition_menuDrawer_leaveActive { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.transition_menuDrawer_enterFrom, +.transition_menuDrawer_leaveTo { + opacity: 0; + transform: translateX(-240px); +} +@media (min-width: 700px) { + .root{ + padding-right:48px; + padding-left:24px; + } +} +.wide{ + &.main { + margin: 0; + border-radius: 0; + box-shadow: none; + width: 100% !important; + max-width: 900px !important; + } +} + +.root { + + min-height: 100dvh; + box-sizing: border-box; + + &.wallpaper { + background: var(--wallpaperOverlay); + } + + > .tray-back { + z-index: 1001; + } + + > .tray { + position: fixed; + top: 0; + right: 0; + z-index: 1001; + height: 100dvh; + margin-top: var(--stickyTop); + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + box-sizing: border-box; + overflow: auto; + background: var(--bg); + } + + > .ivnzpscs { + position: fixed; + bottom: 0; + right: 0; + width: 300px; + height: 600px; + border: none; + pointer-events: none; + } +} + +.columns { + display: flex; + justify-content: center; + max-width: 100%; + + &.fullView { + margin: 0; + + > .sidebar { + display: none; + } + + > .widgets { + display: none; + } + + > .main { + margin: 0; + border-radius: 0; + box-shadow: none; + width: 100%; + } + } + + > .main { + flex: 1; + min-width: 0; + width: 750px; + margin: 0 16px 0 0; + border-left: solid 1px var(--divider); + border-right: solid 1px var(--divider); + border-radius: 0; + overflow: clip; + --margin: 12px; + max-width: 600px; + } + + > .widgets { + //--panelBorder: none; + width: 300px; + height: 100vh; + padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + position: sticky; + overflow-y: auto; + top: 0; + padding-top: 16px; + + @media (max-width: $widgets-hide-threshold) { + display: none; + } + + &.left { + margin-right: 16px; + } + } + + > .sidebar { + width: 275px; + height: 100vh; + position: sticky; + top: 0; + padding: 20px; + } + + &.withGlobalHeader { + > .main { + margin-top: 0; + border: solid 1px var(--divider); + border-radius: var(--radius); + --stickyTop: var(--globalHeaderHeight); + } + + > .widgets { + --stickyTop: var(--globalHeaderHeight); + margin-top: 0; + } + } + @media (max-width: 1300px) { + .sidebar { + width: 80px; + } + .main { + width: 100%; + max-width: 100%; + } + } + @media (max-width: 850px) { + margin: 0; + + > .sidebar { + border-right: solid 0.5px var(--divider); + } + + > .main { + margin: 0; + border-radius: 0; + box-shadow: none; + width: 100%; + } + } +} +.nav { + position: fixed; + z-index: 1000; + bottom: 0; + left: 0; + padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + grid-gap: 8px; + width: 100%; + box-sizing: border-box; + -webkit-backdrop-filter: var(--blur, blur(24px)); + backdrop-filter: var(--blur, blur(24px)); + background-color: var(--bg); + border-top: solid 0.5px var(--divider); + height: 52px; +} + +.menuDrawerBg { + z-index: 1001; +} + +.menuDrawer { + position: fixed; + top: 0; + left: 0; + z-index: 1001; + height: 100dvh; + width: 240px; + box-sizing: border-box; + contain: strict; + overflow: auto; + overscroll-behavior: contain; + background: var(--navBg); +} + +.navButton{ + font-size: 1.5em; +} +.widgetsDrawerBg { + z-index: 1001; +} + +.widgetsDrawer { + position: fixed; + top: 0; + right: 0; + z-index: 1001; + width: 310px; + height: 100dvh; + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; + box-sizing: border-box; + overflow: auto; + overscroll-behavior: contain; + background: var(--bg); +} +.widgetsCloseButton { + padding: 8px; + display: block; + margin: 0 auto; +} +</style> diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index b249c379f1..aae6857df6 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -1,12 +1,16 @@ <!-- -SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License-Identifier: AGPL-3.0-only +SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project +SPDX-License-Identifier: AGPL-3.0-only --> <template> <div :class="$style.root"> <XSidebar v-if="!isMobile" :class="$style.sidebar"/> - <MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu"> + <MkStickyContainer + ref="contents" :class="$style.contents" style="container-type: inline-size;" + @contextmenu.stop="onContextmenu" + > <template #header> <div> <XAnnouncements v-if="$i"/> @@ -21,19 +25,54 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <XWidgets/> </div> - <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> + <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"> + <i class="ti ti-apps"></i> + </button> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> - <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"> + <i + :class="$style.navButtonIcon" class="ti ti-menu-2" + ></i><span + v-if="menuIndicated" + :class="[$style.navButtonIndicator,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'}]" + ><i + class="_indicatorCircle" + ></i></span> + </button> + <button + :class="$style.navButton" class="_button" + @click="isRoot ? top() : mainRouter.push('/')" + > + <i + :class="$style.navButtonIcon" class="ti ti-home" + ></i> + </button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> - <i :class="$style.navButtonIcon" class="ti ti-bell"></i> - <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> - <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> + <i + :class="$style.navButtonIcon" class="ti ti-bell" + ></i><span + v-if="$i?.hasUnreadNotification" + :class="[$style.navButtonIndicator,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'}]" + > + <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 && indicatorCounterToggle ? '99+' : $i.unreadNotificationsCount }}</span> </span> </button> - <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button> - <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> + <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"> + <i + :class="$style.navButtonIcon" + class="ti ti-apps" + ></i> + </button> + <button + :class="[{[$style.postButton_gamingDark]: gaming === 'dark',[$style.postButton_gamingLight]: gaming === 'light',[$style.postButton]: gaming === ''}]" + class="_button" @click="os.post()" + > + <i + :class="$style.navButtonIcon" + class="ti ti-pencil" + ></i> + </button> </div> <Transition @@ -84,7 +123,11 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" > <div v-if="widgetsShowing" :class="$style.widgetsDrawer"> - <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button> + <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"> + <i + class="ti ti-x" + ></i> + </button> <XWidgets/> </div> </Transition> @@ -97,20 +140,52 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue'; import XCommon from './_common_/common.vue'; import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@/config'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; -import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; -import { navbarItemDef } from '@/navbar.js'; -import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { deviceKind } from '@/scripts/device-kind.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@/const.js'; -import { useScrollPositionManager } from '@/nirax.js'; +import * as os from '@/os'; +import { defaultStore } from '@/store'; +import { navbarItemDef } from '@/navbar'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; import { mainRouter } from '@/router/main.js'; +import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata'; +import { deviceKind } from '@/scripts/device-kind'; +import { miLocalStorage } from '@/local-storage'; +import { CURRENT_STICKY_BOTTOM } from '@/const'; +import { useScrollPositionManager } from '@/nirax'; +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle')); +let gaming = ref(); +// gaming.valueに新しい値を代入する +if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; +} else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; +} else { + gaming.value = ''; +} + +watch(darkMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +watch(gamingMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); @@ -121,6 +196,32 @@ const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); const DESKTOP_THRESHOLD = 1100; const MOBILE_THRESHOLD = 500; +onMounted(() => { + if ( + window.navigator.connection.type === 'cellular' && + !defaultStore.state.enableUltimateDataSaverMode && + defaultStore.state.enableCellularWithUltimateDataSaver + ) { + defaultStore.state.enableDataSaverMode = true; + defaultStore.state.enableUltimateDataSaverMode = true; + } else if (window.navigator.connection.type !== 'cellular' && window.navigator.connection.type !== 'undefined' && defaultStore.state.enableDataSaverMode && defaultStore.state.enableCellularWithDataSaver) { + defaultStore.state.enableDataSaverMode = false; + defaultStore.state.enableUltimateDataSaverMode = true; + } + + if ( + window.navigator.connection.type === 'cellular' && + !defaultStore.state.enableDataSaverMode && + defaultStore.state.enableCellularWithDataSaver + ) { + defaultStore.state.enableDataSaverMode = true; + } else if (window.navigator.connection.type !== 'cellular' && window.navigator.connection.type !== 'undefined' && defaultStore.state.enableDataSaverMode && defaultStore.state.enableCellularWithDataSaver) { + defaultStore.state.enableDataSaverMode = false; + } + if (defaultStore.state.enableUltimateDataSaverMode) { + defaultStore.state.enableDataSaverMode = true; + } +}); // デスクトップでウィンドウを狭くしたときモバイルUIが表示されて欲しいことはあるので deviceKind === 'desktop' の判定は行わない const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD); @@ -246,22 +347,22 @@ useScrollPositionManager(() => contents.value.rootEl, mainRouter); <style> html, body { - width: 100%; - height: 100%; - overflow: clip; - position: fixed; - top: 0; - left: 0; - overscroll-behavior: none; + width: 100%; + height: 100%; + overflow: clip; + position: fixed; + top: 0; + left: 0; + overscroll-behavior: none; } #type4ny { - width: 100%; - height: 100%; - overflow: clip; - position: absolute; - top: 0; - left: 0; + width: 100%; + height: 100%; + overflow: clip; + position: absolute; + top: 0; + left: 0; } </style> @@ -271,182 +372,238 @@ $widgets-hide-threshold: 1090px; .transition_menuDrawerBg_enterActive, .transition_menuDrawerBg_leaveActive { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); } + .transition_menuDrawerBg_enterFrom, .transition_menuDrawerBg_leaveTo { - opacity: 0; + opacity: 0; } .transition_menuDrawer_enterActive, .transition_menuDrawer_leaveActive { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); } + .transition_menuDrawer_enterFrom, .transition_menuDrawer_leaveTo { - opacity: 0; - transform: translateX(-240px); + opacity: 0; + transform: translateX(-240px); } .transition_widgetsDrawerBg_enterActive, .transition_widgetsDrawerBg_leaveActive { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); } + .transition_widgetsDrawerBg_enterFrom, .transition_widgetsDrawerBg_leaveTo { - opacity: 0; + opacity: 0; } .transition_widgetsDrawer_enterActive, .transition_widgetsDrawer_leaveActive { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); } + .transition_widgetsDrawer_enterFrom, .transition_widgetsDrawer_leaveTo { - opacity: 0; - transform: translateX(240px); + opacity: 0; + transform: translateX(240px); } .root { - height: 100dvh; - overflow: clip; - contain: strict; - box-sizing: border-box; - display: flex; + height: 100dvh; + overflow: clip; + contain: strict; + box-sizing: border-box; + display: flex; } .sidebar { - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--divider); } .contents { - flex: 1; - height: 100%; - min-width: 0; - overflow: auto; - overflow-y: scroll; - overscroll-behavior: contain; - background: var(--bg); + flex: 1; + height: 100%; + min-width: 0; + overflow: auto; + overflow-y: scroll; + overscroll-behavior: contain; + background: var(--bg); } .widgets { - width: 350px; - height: 100%; - box-sizing: border-box; - overflow: auto; - padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); - border-left: solid 0.5px var(--divider); - background: var(--bg); + width: 350px; + height: 100%; + box-sizing: border-box; + overflow: auto; + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + border-left: solid 0.5px var(--divider); + background: var(--bg); - @media (max-width: $widgets-hide-threshold) { - display: none; - } + @media (max-width: $widgets-hide-threshold) { + display: none; + } } .widgetButton { - display: block; - position: fixed; - z-index: 1000; - bottom: 32px; - right: 32px; - width: 64px; - height: 64px; - border-radius: 100%; - box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); - font-size: 22px; - background: var(--panel); + display: block; + position: fixed; + z-index: 1000; + bottom: 32px; + right: 32px; + width: 64px; + height: 64px; + border-radius: 100%; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); + font-size: 22px; + background: var(--panel); } .widgetsDrawerBg { - z-index: 1001; + z-index: 1001; } .widgetsDrawer { - position: fixed; - top: 0; - right: 0; - z-index: 1001; - width: 310px; - height: 100dvh; - padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; - box-sizing: border-box; - overflow: auto; - overscroll-behavior: contain; - background: var(--bg); + position: fixed; + top: 0; + right: 0; + z-index: 1001; + width: 310px; + height: 100dvh; + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; + box-sizing: border-box; + overflow: auto; + overscroll-behavior: contain; + background: var(--bg); } .widgetsCloseButton { - padding: 8px; - display: block; - margin: 0 auto; + padding: 8px; + display: block; + margin: 0 auto; } @media (min-width: 370px) { - .widgetsCloseButton { - display: none; - } + .widgetsCloseButton { + display: none; + } } .nav { - position: fixed; - z-index: 1000; - bottom: 0; - left: 0; - padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px; - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr; - grid-gap: 8px; - width: 100%; - box-sizing: border-box; - -webkit-backdrop-filter: var(--blur, blur(24px)); - backdrop-filter: var(--blur, blur(24px)); - background-color: var(--header); - border-top: solid 0.5px var(--divider); + position: fixed; + z-index: 1000; + bottom: 0; + left: 0; + padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + grid-gap: 8px; + width: 100%; + box-sizing: border-box; + -webkit-backdrop-filter: var(--blur, blur(24px)); + backdrop-filter: var(--blur, blur(24px)); + background-color: var(--header); + border-top: solid 0.5px var(--divider); } .navButton { - position: relative; - padding: 0; - aspect-ratio: 1; - width: 100%; - max-width: 60px; - margin: auto; - border-radius: 100%; - background: var(--panel); - color: var(--fg); + position: relative; + padding: 0; + aspect-ratio: 1; + width: 100%; + max-width: 60px; + margin: auto; + border-radius: 100%; + background: var(--panel); + color: var(--fg); - &:hover { - background: var(--panelHighlight); - } + &:hover { + background: var(--panelHighlight); + } - &:active { - background: var(--X2); - } + &:active { + background: var(--X2); + } +} + +.postButton_gamingDark { + composes: navButton; + color: var(--fgOnAccent); + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800%; + -webkit-animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + -moz-animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + animation: AnimationDark 44s cubic-bezier(0, 0.25, 0.25, 1) infinite; + + &.gamingDark:hover { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + } + + &.gamingDark:active { + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + } +} + +.postButton_gamingLight { + composes: navButton; + color: var(--fgOnAccent); + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + + &.gamingLight:hover { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + } + &.gamingLight:active { + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + } } .postButton { - composes: navButton; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - color: var(--fgOnAccent); + composes: navButton; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + color: var(--fgOnAccent); - &:hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } + &:hover { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } + + &:active { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } - &:active { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } } .navButtonIcon { - font-size: 18px; - vertical-align: middle; + font-size: 18px; + vertical-align: middle; } .navButtonIndicator { @@ -461,33 +618,106 @@ $widgets-hide-threshold: 1090px; animation: none; font-size: 12px; } + &.gamingDark { + color: white; + } + + &.gamingLight { + color: black; + } } .menuDrawerBg { - z-index: 1001; + z-index: 1001; } .menuDrawer { - position: fixed; - top: 0; - left: 0; - z-index: 1001; - height: 100dvh; - width: 240px; - box-sizing: border-box; - contain: strict; - overflow: auto; - overscroll-behavior: contain; - background: var(--navBg); + position: fixed; + top: 0; + left: 0; + z-index: 1001; + height: 100dvh; + width: 240px; + box-sizing: border-box; + contain: strict; + overflow: auto; + overscroll-behavior: contain; + background: var(--navBg); } .statusbars { - position: sticky; - top: 0; - left: 0; + position: sticky; + top: 0; + left: 0; } .spacer { - height: calc(var(--minBottomSpacing)); + height: calc(var(--minBottomSpacing)); +} +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index 3f88f363b7..937d174157 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -6,13 +6,13 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <div> <XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/> - <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> - <button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> + <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" :class="{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> + <button v-else class="_textButton" data-cy-widget-edit :class="$style.edit, {[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> </div> </template> <script lang="ts"> -import { computed, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; const editMode = ref(false); </script> <script lang="ts" setup> @@ -20,6 +20,37 @@ import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; +let gaming = ref(''); + +const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; +} else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; +} else { + gaming.value = ''; +} + +watch(darkMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); + +watch(gamingMode, () => { + if (darkMode.value && gamingMode.value == true) { + gaming.value = 'dark'; + } else if (!darkMode.value && gamingMode.value == true) { + gaming.value = 'light'; + } else { + gaming.value = ''; + } +}); const props = withDefaults(defineProps<{ // null = 全てのウィジェットを表示 // left = place: leftだけを表示 @@ -76,5 +107,103 @@ function updateWidgets(thisWidgets) { <style lang="scss" module> .edit { width: 100%; + &.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + &.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); + background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } +} +.gamingDark{ + background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd); background-size: 1800% 1800% !important; + -webkit-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationDark 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} +.gamingLight{ + background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4); background-size: 1800% 1800% !important; + -webkit-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -moz-animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + animation: AnimationLight 45s cubic-bezier(0, 0.25, 0.25, 1) infinite !important; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} +@-webkit-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} @keyframes AnimationLight { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } + } +@-webkit-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@-moz-keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } +} +@keyframes AnimationDark { + 0% { + background-position: 0% 50% + } + 50% { + background-position: 100% 50% + } + 100% { + background-position: 0% 50% + } } </style> diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index a693030a32..2c8fb3b975 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -4,18 +4,25 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <MkContainer :showHeader="widgetProps.showHeader" class="mkw-clicker"> - <template #icon><i class="ti ti-cookie"></i></template> + <template #icon> + <img + :class="[$style.icon,{[$style.dark]:darkMode}]" alt="kabocha kawadiri" + src="https://files.prismisskey.space/misskey/630c737c-e96f-4c10-94a4-73e138278576.webp" + /> + </template> <template #header>Clicker</template> <MkClickerGame/> </MkContainer> </template> <script lang="ts" setup> +import { computed } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkClickerGame from '@/components/MkClickerGame.vue'; - +import { defaultStore } from '@/store.js'; +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); const name = 'clicker'; const widgetPropsDef = { @@ -42,3 +49,14 @@ defineExpose<WidgetComponentExpose>({ id: props.widget ? props.widget.id : null, }); </script> + +<style lang="scss" module> +.icon { + width: 1.3em; + vertical-align: -24%; +} + +.dark { + filter: invert(1); +} +</style> diff --git a/packages/frontend/src/widgets/WidgetGamingMode.vue b/packages/frontend/src/widgets/WidgetGamingMode.vue new file mode 100644 index 0000000000..749527c546 --- /dev/null +++ b/packages/frontend/src/widgets/WidgetGamingMode.vue @@ -0,0 +1,41 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <MkSwitch v-model="enableGamingMode">{{ i18n.ts.gamingMode }} <template #caption>{{ i18n.ts.gamingModeInfo }} </template></MkSwitch> +</div> +</template> + +<script lang="ts" setup> +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form.js'; +import {i18n} from "@/i18n.js"; +import MkSwitch from "@/components/MkSwitch.vue"; +import {computed} from "vue"; +import {defaultStore} from "@/store.js"; +const enableGamingMode = computed(defaultStore.makeGetterSetter('gamingMode')); +const name = 'gamingMode'; + +const widgetPropsDef = { +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; + +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); + +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, +}); +</script> diff --git a/packages/frontend/src/widgets/WidgetGyakubariMode.vue b/packages/frontend/src/widgets/WidgetGyakubariMode.vue new file mode 100644 index 0000000000..13592a5c6a --- /dev/null +++ b/packages/frontend/src/widgets/WidgetGyakubariMode.vue @@ -0,0 +1,41 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <MkSwitch v-model="enablehanntenn">{{ i18n.ts.hanntenn }} <template #caption>{{ i18n.ts.hanntennInfo }} </template></MkSwitch> +</div> +</template> + +<script lang="ts" setup> +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form.js'; +import {i18n} from "@/i18n.js"; +import MkSwitch from "@/components/MkSwitch.vue"; +import {computed} from "vue"; +import {defaultStore} from "@/store.js"; +const enablehanntenn = computed(defaultStore.makeGetterSetter('enablehanntenn')); +const name = 'gyakubariMode'; + +const widgetPropsDef = { +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; + +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); + +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, +}); +</script> diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index e7b8d77ad8..0d8d3d5550 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -4,9 +4,9 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <template> <div class="_panel"> - <div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : null }"> + <div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ bannerUrl })` : null }"> <div :class="$style.iconContainer"> - <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/> + <img :src="iconUrl" alt="" :class="$style.icon"/> </div> <div :class="$style.bodyContainer"> <div :class="$style.body"> @@ -20,11 +20,32 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License <script lang="ts" setup> import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import { GetFormResultType } from '@/scripts/form.js'; -import { host } from '@/config.js'; -import { instance } from '@/instance.js'; +import { GetFormResultType } from '@/scripts/form'; +import { host } from '@/config'; +import { instance } from '@/instance'; +import {bannerDark, bannerLight, defaultStore, iconDark, iconLight} from "@/store"; +import {computed, ref, watch} from "vue"; const name = 'instanceInfo'; +let bannerUrl = ref(defaultStore.state.bannerUrl); +let iconUrl = ref(defaultStore.state.iconUrl); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +if (darkMode.value){ + bannerUrl.value = bannerDark; + iconUrl.value = iconDark; +}else{ + bannerUrl.value = bannerLight; + iconUrl.value = iconLight; +} +watch(darkMode, () => { + if (darkMode.value){ + bannerUrl.value = bannerDark; + iconUrl.value = iconDark; + }else{ + bannerUrl.value = bannerLight; + iconUrl.value = iconLight; + } +}) const widgetPropsDef = { }; diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index c633581b16..1094ee17cf 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -122,6 +122,10 @@ const choose = async (ev) => { text: i18n.ts._timelines.local, icon: 'ti ti-planet', action: () => { setSrc('local'); }, + }, { + text: i18n.ts._timelines.media, + icon: 'ti ti-photo', + action: () => { setSrc('media'); }, }, { text: i18n.ts._timelines.social, icon: 'ti ti-universe', diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index f5c6688694..f2250ed969 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -10,6 +10,8 @@ export default function(app: App) { app.component('WidgetInstanceInfo', defineAsyncComponent(() => import('./WidgetInstanceInfo.vue'))); app.component('WidgetMemo', defineAsyncComponent(() => import('./WidgetMemo.vue'))); app.component('WidgetNotifications', defineAsyncComponent(() => import('./WidgetNotifications.vue'))); + app.component('WidgetGamingMode', defineAsyncComponent(() => import('./WidgetGamingMode.vue'))); + app.component('WidgetGyakubariMode', defineAsyncComponent(() => import('./WidgetGyakubariMode.vue'))); app.component('WidgetTimeline', defineAsyncComponent(() => import('./WidgetTimeline.vue'))); app.component('WidgetCalendar', defineAsyncComponent(() => import('./WidgetCalendar.vue'))); app.component('WidgetRss', defineAsyncComponent(() => import('./WidgetRss.vue'))); @@ -41,6 +43,8 @@ export const widgets = [ 'instanceInfo', 'memo', 'notifications', + 'gamingMode', + 'gyakubariMode', 'timeline', 'calendar', 'rss', diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 6decbc0ef7..8daba1ec25 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -1,7 +1,7 @@ import path from 'path'; import pluginReplace from '@rollup/plugin-replace'; import pluginVue from '@vitejs/plugin-vue'; -import { type UserConfig, defineConfig } from 'vite'; +import {type UserConfig, defineConfig} from 'vite'; import locales from '../../locales/index.js'; import meta from '../../package.json'; diff --git a/packages/misskey-bubble-game/src/game.ts b/packages/misskey-bubble-game/src/game.ts index c07df62711..34b432d9d4 100644 --- a/packages/misskey-bubble-game/src/game.ts +++ b/packages/misskey-bubble-game/src/game.ts @@ -6,7 +6,7 @@ import { EventEmitter } from 'eventemitter3'; import * as Matter from 'matter-js'; import seedrandom from 'seedrandom'; -import { NORAML_MONOS, SQUARE_MONOS, SWEETS_MONOS, YEN_MONOS } from './monos.js'; +import { NORAML_MONOS, PRISMISSKEY_MONOS, SQUARE_MONOS, SWEETS_MONOS, YEN_MONOS } from './monos.js'; export type Mono = { id: string; @@ -58,7 +58,7 @@ export class DropAndFusionGame extends EventEmitter<{ private tickCallbackQueue: { frame: number; callback: () => void; }[] = []; private overflowCollider: Matter.Body; private isGameOver = false; - private gameMode: 'normal' | 'yen' | 'square' | 'sweets' | 'space'; + private gameMode: 'normal' | 'yen' | 'square' | 'sweets' | 'space' | 'prismisskey'; private rng: () => number; private logs: Log[] = []; @@ -87,6 +87,7 @@ export class DropAndFusionGame extends EventEmitter<{ case 'square': return SQUARE_MONOS; case 'sweets': return SWEETS_MONOS; case 'space': return NORAML_MONOS; + case 'prismisskey' : return PRISMISSKEY_MONOS; } } diff --git a/packages/misskey-bubble-game/src/monos.ts b/packages/misskey-bubble-game/src/monos.ts index 2db6d7a82a..6d4c994ebb 100644 --- a/packages/misskey-bubble-game/src/monos.ts +++ b/packages/misskey-bubble-game/src/monos.ts @@ -950,3 +950,299 @@ export const SWEETS_MONOS: Mono[] = [{ score: 30, dropCandidate: true, }]; + +const PRISMISSKEY_BASE_SIZE = 28; + +export const PRISMISSKEY_MONOS: Mono[] = [{ + id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525', + level: 10, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'custom', + vertices: [[ + { 'x': 1680, 'y': 270 }, + { 'x': 2150, 'y': 560 }, + { 'x': 2600, 'y': 910 }, + { 'x': 3279, 'y': 650 }, + { 'x': 3130, 'y': 1100 }, + { 'x': 2750, 'y': 1250 }, + { 'x': 2800, 'y': 1860 }, + { 'x': 1760, 'y': 1980 }, + { 'x': 1500, 'y': 1840 }, + { 'x': 989, 'y': 1800 }, + { 'x': 888, 'y': 1900 }, + { 'x': 688, 'y': 1540 }, + { 'x': 783, 'y': 1250 }, + { 'x': 660, 'y': 1190 }, + { 'x': 600, 'y': 800 }, + { 'x': 722, 'y': 382 }, + { 'x': 1100, 'y': 835 }, + { 'x': 1400, 'y': 670 }, + ]], + verticesSize: 1800, + score: 10000, + dropCandidate: false, +}, { + id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1', + level: 9, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'custom', + vertices: [ + [ + { 'x': 1023, 'y': 266 }, + { 'x': 1476, 'y': 474 }, + { 'x': 1595, 'y': 362 }, + { 'x': 1747, 'y': 359 }, + { 'x': 1939, 'y': 481 }, + { 'x': 1769, 'y': 700 }, + { 'x': 1666, 'y': 690 }, + { 'x': 1711, 'y': 982 }, + { 'x': 1638, 'y': 1227 }, + { 'x': 1494, 'y': 1300 }, + { 'x': 1037, 'y': 1300 }, + { 'x': 914, 'y': 1183 }, + { 'x': 447, 'y': 1200 }, + { 'x': 290, 'y': 1300 }, + { 'x': 204, 'y': 1280 }, + { 'x': 170, 'y': 835 }, + { 'x': 321, 'y': 523 }, + { 'x': 647, 'y': 323 }, + { 'x': 857, 'y': 277 }, + + ], + ], + verticesSize: 1300, + score: 1000, + dropCandidate: false, +}, { + id: '41607ef3-b6d6-4829-95b6-3737bf8bb956', + level: 8, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'custom', + vertices: [ + [ + { 'x': 1177, 'y': 85 }, + { 'x': 1498, 'y': 404 }, + { 'x': 1370, 'y': 374 }, + { 'x': 1737, 'y': 619 }, + { 'x': 1946, 'y': 721 }, + { 'x': 1764, 'y': 975 }, + { 'x': 1674, 'y': 949 }, + { 'x': 1715, 'y': 1183 }, + { 'x': 1641, 'y': 1498 }, + { 'x': 1479, 'y': 1555 }, + { 'x': 1008, 'y': 1566 }, + { 'x': 912, 'y': 1447 }, + { 'x': 456, 'y': 1453 }, + { 'x': 298, 'y': 1561 }, + { 'x': 240, 'y': 1551 }, + { 'x': 187, 'y': 1017 }, + { 'x': 283, 'y': 825 }, + { 'x': 517, 'y': 609 }, + { 'x': 187, 'y': 1017 }, + { 'x': 352, 'y': 521 }, + { 'x': 821, 'y': 333 }, + ], + ], + verticesSize: 1300, + score: 800, + dropCandidate: false, +}, { + id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416', + level: 7, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 64, + dropCandidate: false, +}, { + id: '1092e069-fe1a-450b-be97-b5d477ec398c', + level: 6, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 500, + dropCandidate: false, +}, { + id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0', + level: 5, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + + shape: 'custom', + vertices: [ + [ + { + 'x': 410, + 'y': 14, + }, + { + 'x': 741, + 'y': 160, + }, + { + 'x': 834, + 'y': 346, + }, + { + 'x': 853, + 'y': 637, + }, + { + 'x': 768, + 'y': 760, + }, + { + 'x': 422, + 'y': 813, + }, { + 'x': 66, + 'y': 748, + }, + { + 'x': 27, + 'y': 702, + }, + { + 'x': 7, + 'y': 543, + }, { + 'x': 63, + 'y': 263, + }, { + 'x': 170, + 'y': 108, + }, + { + 'x': 310, + 'y': 26, + }, + ], + ], + verticesSize: 1024, + score: 16, + dropCandidate: true, +}, { + id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a', + level: 4, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 200, + dropCandidate: true, +}, { + id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919', + level: 3, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25, + + shape: 'custom', + vertices: [ + [ + { + 'x': 87, + 'y': 6, + }, + { + 'x': 169, + 'y': 53, + }, + { + 'x': 169, + 'y': 147, + }, + { + 'x': 87, + 'y': 195, + }, + { + 'x': 6, + 'y': 147, + }, + { + 'x': 6, + 'y': 53, + }, + ], + ], + verticesSize: 128, + score: 100, + dropCandidate: true, +}, { + id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d', + level: 2, + sizeX: PRISMISSKEY_BASE_SIZE * 1.25, + sizeY: PRISMISSKEY_BASE_SIZE * 1.25, + + shape: 'custom', + vertices: [ + [ + { + 'x': 749, + 'y': 12, + }, + { + 'x': 1386, + 'y': 379, + }, + { + 'x': 1387, + 'y': 1116, + }, + { + 'x': 749, + 'y': 1484, + }, + { + 'x': 111, + 'y': 1116, + }, + { + 'x': 111, + 'y': 380, + }, + ], + ], + verticesSize: 1536, + score: 50, + dropCandidate: true, +}, { + id: '35e476ee-44bd-4711-ad42-87be245d3efd', + level: 1, + sizeX: PRISMISSKEY_BASE_SIZE, + sizeY: PRISMISSKEY_BASE_SIZE, + shape: 'custom', + vertices: [ + [ + { + 'x': 87, + 'y': 6, + }, + { + 'x': 169, + 'y': 53, + }, + { + 'x': 169, + 'y': 147, + }, + { + 'x': 87, + 'y': 195, + }, + { + 'x': 6, + 'y': 147, + }, + { + 'x': 6, + 'y': 53, + }, + ], + ], + verticesSize: 128, + score: 10, + dropCandidate: true, +}]; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 72aca4dee2..f78b611ada 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6351,6 +6351,7 @@ export type operations = { updatedAt: string | null; name: string; description: string; + category: string; url: string; roleIdsThatCanBeUsedThisDecoration: string[]; })[]; @@ -16368,6 +16369,7 @@ export type operations = { name: string; description: string; url: string; + category: string; roleIdsThatCanBeUsedThisDecoration: string[]; }[]; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09df15853b..e63a7c9b20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,7 +268,7 @@ importers: version: 4.1.0 jsdom: specifier: 24.0.0 - version: 24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + version: 24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3) json5: specifier: 2.2.3 version: 2.2.3 @@ -344,6 +344,9 @@ importers: promise-limit: specifier: 2.7.0 version: 2.7.0 + proxycheck-ts: + specifier: ^0.0.9 + version: 0.0.9(encoding@0.1.13) pug: specifier: 3.0.2 version: 3.0.2 @@ -422,6 +425,9 @@ importers: vary: specifier: 1.1.2 version: 1.1.2 + w3c-xmlserializer: + specifier: ^5.0.0 + version: 5.0.0 web-push: specifier: 3.6.7 version: 3.6.7 @@ -640,6 +646,9 @@ importers: '@types/vary': specifier: 1.1.3 version: 1.1.3 + '@types/w3c-xmlserializer': + specifier: ^2.0.4 + version: 2.0.4 '@types/web-push': specifier: 3.6.3 version: 3.6.3 @@ -697,6 +706,9 @@ importers: '@mcaptcha/vanilla-glue': specifier: 0.1.0-alpha-3 version: 0.1.0-alpha-3 + '@meersagor/wavesurfer-vue': + specifier: ^0.1.0 + version: 0.1.0(typescript@5.4.5) '@misskey-dev/browser-image-resizer': specifier: 2024.1.0 version: 2024.1.0 @@ -859,6 +871,9 @@ importers: vuedraggable: specifier: next version: 4.1.0(vue@3.4.26(typescript@5.5.2)) + wavesurfer.js: + specifier: ^7.7.14 + version: 7.7.14 devDependencies: '@misskey-dev/eslint-plugin': specifier: 1.0.0 @@ -874,7 +889,7 @@ importers: version: 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-interactions': specifier: 8.0.9 - version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) '@storybook/addon-links': specifier: 8.0.9 version: 8.0.9(react@18.3.1) @@ -907,7 +922,7 @@ importers: version: 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.5.2)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)) '@storybook/test': specifier: 8.0.9 - version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) '@storybook/theming': specifier: 8.0.9 version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -967,7 +982,7 @@ importers: version: 7.7.1(eslint@8.57.0)(typescript@5.5.2) '@vitest/coverage-v8': specifier: 0.34.6 - version: 0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + version: 0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) '@vue/runtime-core': specifier: 3.4.26 version: 3.4.26 @@ -1036,10 +1051,10 @@ importers: version: 1.0.3 vitest: specifier: 0.34.6 - version: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) + version: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) vitest-fetch-mock: specifier: 0.2.2 - version: 0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + version: 0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) vue-component-type-helpers: specifier: 2.0.16 version: 2.0.16 @@ -3048,6 +3063,9 @@ packages: '@types/react': '>=16' react: '>=16' + '@meersagor/wavesurfer-vue@0.1.0': + resolution: {integrity: sha512-DjOkssn3IgUxNLFlT4X9jpgxdTo+ykSHV762vwfJjRfKwaIna0cEUmOGpKDNDESP4XEHZKuWgrFWp2iEzPkGww==} + '@microsoft/api-extractor-model@7.28.14': resolution: {integrity: sha512-Bery/c8A8SsKPSvA82cTTuy/+OcxZbLRmKhPkk91/AJOQzxZsShcrmHFAGeiEqSIrv1nPZ3tKq9kfMLdCHmsqg==} @@ -4772,6 +4790,9 @@ packages: '@types/vary@1.1.3': resolution: {integrity: sha512-XJT8/ZQCL7NUut9QDLf6l24JfAEl7bnNdgxfj50cHIpEPRJLHHDDFOAq6i+GsEmeFfH7NamhBE4c4Thtb2egWg==} + '@types/w3c-xmlserializer@2.0.4': + resolution: {integrity: sha512-A37kb4IAiBxTnsQ+guO2p4WMAJx5+n6kmvmosQQyVn8OwmyNwcxlP72PQVFP9yumQIfqgvLNH4vzp2vKLkGcJA==} + '@types/web-push@3.6.3': resolution: {integrity: sha512-v3oT4mMJsHeJ/rraliZ+7TbZtr5bQQuxcgD7C3/1q/zkAj29c8RE0F9lVZVu3hiQe5Z9fYcBreV7TLnfKR+4mg==} @@ -5669,6 +5690,10 @@ packages: canvas-confetti@1.9.3: resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} + canvas@2.11.2: + resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} + engines: {node: '>=6'} + caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -6184,6 +6209,10 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decompress-response@4.2.1: + resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} + engines: {node: '>=8'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -8401,6 +8430,10 @@ packages: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} + mimic-response@2.1.0: + resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} + engines: {node: '>=8'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -9496,6 +9529,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxycheck-ts@0.0.9: + resolution: {integrity: sha512-qxdMgLB01kPksBAeAHI2Zt5q/fW0glZKZK9d9eNm0/KyowKuKzVmvirF2ztsGcIzU7r6s6YvPwXJuPTeQ+LBTg==} + ps-list@8.1.1: resolution: {integrity: sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10054,6 +10090,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@3.1.1: + resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + simple-oauth2@5.0.0: resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==} @@ -11197,6 +11239,9 @@ packages: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} + wavesurfer.js@7.7.14: + resolution: {integrity: sha512-sbd48yHnOVDEbZwsnD3dWzBj4SF2q2rsPysmPE8spoR2XwKVkU/POtZg/L0wPi6ypqXb7brQLftSeOovJNToPQ==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -13727,6 +13772,13 @@ snapshots: '@types/react': 18.0.28 react: 18.3.1 + '@meersagor/wavesurfer-vue@0.1.0(typescript@5.4.5)': + dependencies: + vue: 3.4.26(typescript@5.4.5) + wavesurfer.js: 7.7.14 + transitivePeerDependencies: + - typescript + '@microsoft/api-extractor-model@7.28.14(@types/node@20.12.7)': dependencies: '@microsoft/tsdoc': 0.14.2 @@ -14906,11 +14958,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@storybook/addon-interactions@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.0.9 - '@storybook/test': 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + '@storybook/test': 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) '@storybook/types': 8.0.9 polished: 4.2.2 ts-dedent: 2.2.0 @@ -15414,14 +15466,14 @@ snapshots: - encoding - supports-color - '@storybook/test@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@storybook/test@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': dependencies: '@storybook/client-logger': 8.0.9 '@storybook/core-events': 8.0.9 '@storybook/instrumenter': 8.0.9 '@storybook/preview-api': 8.0.9 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + '@testing-library/jest-dom': 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.6.0 @@ -15723,7 +15775,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@testing-library/jest-dom@6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.23.4 @@ -15737,7 +15789,7 @@ snapshots: '@jest/globals': 29.7.0 '@types/jest': 29.5.12 jest: 29.7.0(@types/node@20.12.7) - vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) + vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) '@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4)': dependencies: @@ -16162,6 +16214,8 @@ snapshots: dependencies: '@types/node': 20.12.7 + '@types/w3c-xmlserializer@2.0.4': {} + '@types/web-push@3.6.3': dependencies: '@types/node': 20.12.7 @@ -16449,7 +16503,7 @@ snapshots: vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) vue: 3.4.26(typescript@5.5.2) - '@vitest/coverage-v8@0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@vitest/coverage-v8@0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 @@ -16462,7 +16516,7 @@ snapshots: std-env: 3.7.0 test-exclude: 6.0.0 v8-to-istanbul: 9.2.0 - vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) + vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) transitivePeerDependencies: - supports-color @@ -17323,6 +17377,16 @@ snapshots: canvas-confetti@1.9.3: {} + canvas@2.11.2(encoding@0.1.13): + dependencies: + '@mapbox/node-pre-gyp': 1.0.9(encoding@0.1.13) + nan: 2.18.0 + simple-get: 3.1.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + caseless@0.12.0: {} cbor@9.0.2: @@ -17950,6 +18014,11 @@ snapshots: dependencies: character-entities: 2.0.2 + decompress-response@4.2.1: + dependencies: + mimic-response: 2.1.0 + optional: true + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -20305,7 +20374,7 @@ snapshots: transitivePeerDependencies: - supports-color - jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): + jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3): dependencies: cssstyle: 4.0.1 data-urls: 5.0.0 @@ -20328,6 +20397,8 @@ snapshots: whatwg-url: 14.0.0 ws: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 2.11.2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - supports-color @@ -20998,6 +21069,9 @@ snapshots: mimic-response@1.0.1: {} + mimic-response@2.1.0: + optional: true + mimic-response@3.1.0: {} mimic-response@4.0.0: {} @@ -22092,6 +22166,12 @@ snapshots: proxy-from-env@1.1.0: {} + proxycheck-ts@0.0.9(encoding@0.1.13): + dependencies: + cross-fetch: 4.0.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + ps-list@8.1.1: {} ps-tree@1.2.0: @@ -22805,6 +22885,16 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: + optional: true + + simple-get@3.1.1: + dependencies: + decompress-response: 4.2.1 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + simple-oauth2@5.0.0: dependencies: '@hapi/hoek': 10.0.1 @@ -23755,14 +23845,14 @@ snapshots: sass: 1.76.0 terser: 5.30.3 - vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)): + vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)): dependencies: cross-fetch: 3.1.6(encoding@0.1.13) - vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) + vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) transitivePeerDependencies: - encoding - vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3): + vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3): dependencies: '@types/chai': 4.3.11 '@types/chai-subset': 1.3.5 @@ -23790,7 +23880,7 @@ snapshots: why-is-node-running: 2.2.2 optionalDependencies: happy-dom: 10.0.3 - jsdom: 24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + jsdom: 24.0.0(bufferutil@4.0.7)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.3) transitivePeerDependencies: - less - lightningcss @@ -23931,6 +24021,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + wavesurfer.js@7.7.14: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4