├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── assets.d.ts ├── assets ├── css │ └── timeline.css ├── emojilist.json ├── emojis.json ├── file_list.json ├── icons │ ├── clip.png │ ├── direct.png │ ├── follow.png │ ├── follow_accept.png │ ├── follow_request.png │ ├── home.png │ ├── local_direct.png │ ├── local_home.png │ ├── local_lock.png │ ├── local_public.png │ ├── lock.png │ ├── poll.png │ ├── public.png │ ├── quote.png │ ├── reaction.png │ ├── renote.png │ └── reply.png ├── no_image.png ├── placeholder.json ├── settings.json └── version_data.json ├── docs └── tabs.json.md ├── package-lock.json ├── package.json ├── readme.md ├── replace_blocks.sample.json ├── src ├── assets.js ├── blocker │ ├── domain.js │ ├── index.js │ ├── replace.js │ ├── user_id.js │ └── word.js ├── client.js ├── file.js ├── index.js ├── login.js ├── main_window.js ├── menubar │ ├── file_menu.js │ ├── index.js │ └── post_menu.js ├── message_box.js ├── models │ ├── note.js │ ├── notification.js │ └── user.js ├── post_action.js ├── timelines │ ├── flag_label.js │ ├── index.js │ ├── note_counter.js │ ├── note_skin │ │ ├── default.js │ │ ├── flag_widget.js │ │ └── sobacha.js │ ├── notification_item.js │ ├── skin.js │ ├── tab_loader.js │ └── timeline.js ├── tools │ ├── data_directory │ │ └── index.js │ ├── desktop_notification │ │ └── index.js │ ├── emoji_parser │ │ ├── cache.js │ │ └── index.js │ ├── image_viewer │ │ └── index.js │ ├── note_cache │ │ └── index.js │ ├── notification_cache │ │ └── index.js │ ├── notification_parser │ │ └── index.js │ ├── post_parser │ │ └── index.js │ ├── random_emoji │ │ └── index.js │ ├── settings │ │ └── index.js │ ├── user_cache │ │ └── index.js │ └── version_parser │ │ └── index.js └── widgets │ ├── custom_post_window │ ├── button_area.js │ ├── cw_text_area.js │ ├── file_item.js │ ├── image_area.js │ ├── index.js │ ├── poll_area.js │ └── post_text_area.js │ ├── delete_confirm_window │ └── index.js │ ├── emoji_picker │ ├── emoji_item.js │ ├── emoji_list_widget.js │ └── index.js │ ├── icon_label │ └── index.js │ ├── login_window │ ├── host_input_widget.js │ ├── index.js │ └── login_url_widget.js │ ├── operation_menu │ └── index.js │ ├── postbox │ └── index.js │ ├── postview │ ├── content_box.js │ ├── flag_widget.js │ └── index.js │ ├── reaction_emoji_input_window │ └── index.js │ ├── reaction_menu │ └── index.js │ ├── setting_window │ ├── index.js │ └── setting_widget.js │ └── timeline_menu │ └── index.js ├── tsconfig.json ├── user_id_blocks.sample.json ├── webpack.config.js └── word_blocks.sample.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2020": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 11 10 | }, 11 | "rules": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: TenCha CI 5 | 6 | on: 7 | push: 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | os: [windows-2016, ubuntu-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Install pakages(linux) 23 | if: ${{ runner.os == 'Linux' }} 24 | run: sudo apt update && sudo apt install -y mesa-common-dev libglu1-mesa-dev libxkbcommon-x11-0 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install deps 32 | run: npm ci 33 | 34 | - name: Build 35 | run: npm run build --if-present 36 | 37 | - name: packer init 38 | run: npx nodegui-packer --init TenCha 39 | 40 | - name: Pack TenCha 41 | run: npx nodegui-packer --pack ./dist 42 | 43 | - name: Clean(Linux) 44 | if: ${{ runner.os == 'Linux' }} 45 | run: rm ${{ github.workspace }}/deploy/linux/build/TenCha/Application-x86_64.AppImage 46 | 47 | - name: Upload Linux package 48 | uses: actions/upload-artifact@v2 49 | if: ${{ runner.os == 'Linux' }} 50 | with: 51 | name: LinuxPackage 52 | path: ${{ github.workspace }}/deploy/linux/build/TenCha/ 53 | 54 | - name: Upload Windows package 55 | uses: actions/upload-artifact@v2 56 | if: ${{ runner.os == 'Windows' }} 57 | with: 58 | name: WindowsPackage 59 | path: ${{ github.workspace }}/deploy/win32/build/TenCha/ 60 | 61 | setup-release: 62 | needs: build 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Create release 66 | id: create_release 67 | uses: actions/create-release@v1.0.0 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | tag_name: ${{ github.ref }} 72 | release_name: Release ${{ github.ref }} 73 | draft: false 74 | prerelease: false 75 | 76 | - name: Info create 77 | env: 78 | url: ${{ steps.create_release.outputs.upload_url }} 79 | run: mkdir artifact && echo $url > artifact/url.txt 80 | 81 | - name: Upload info 82 | uses: actions/upload-artifact@v1 83 | with: 84 | name: artifact 85 | path: artifact/url.txt 86 | 87 | release: 88 | needs: setup-release 89 | runs-on: ubuntu-latest 90 | 91 | steps: 92 | - name: Download info 93 | uses: actions/download-artifact@v1 94 | with: 95 | name: artifact 96 | 97 | - name: Download linux package 98 | uses: actions/download-artifact@v1 99 | with: 100 | name: LinuxPackage 101 | 102 | - name: Download windows package 103 | uses: actions/download-artifact@v1 104 | with: 105 | name: WindowsPackage 106 | 107 | - name: Read info 108 | id: get_url 109 | run: url=$(cat artifact/url.txt) && echo "##[set-output name=upload_url;]$url" 110 | 111 | - name: zip(linux) 112 | run: zip tencha_linux -r LinuxPackage 113 | 114 | - name: zip(win) 115 | run: zip tencha_windows -r WindowsPackage 116 | 117 | - name: Upload release asset(linux) 118 | uses: actions/upload-release-asset@v1.0.1 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | with: 122 | upload_url: ${{ steps.get_url.outputs.upload_url }} 123 | asset_path: tencha_linux.zip 124 | asset_name: tencha_linux.zip 125 | asset_content_type: application/zip 126 | 127 | - name: Upload release asset(win) 128 | uses: actions/upload-release-asset@v1.0.1 129 | env: 130 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | with: 132 | upload_url: ${{ steps.get_url.outputs.upload_url }} 133 | asset_path: tencha_windows.zip 134 | asset_name: tencha_windows.zip 135 | asset_content_type: application/zip 136 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | deploy/ 4 | tmp/ 5 | data/ 6 | user_contents/ 7 | /config.json 8 | /tabs.json 9 | /settings.json 10 | /config.json1 11 | /domain_blocks.json 12 | /word_blocks.json 13 | /user_id_blocks.json 14 | /replace_blocks.json 15 | /content_settings.json 16 | /notify.mp3 17 | /post.mp3 18 | -------------------------------------------------------------------------------- /assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg"; 2 | declare module "*.png"; 3 | declare module "*.jpg"; 4 | declare module "*.jpeg"; 5 | declare module "*.gif"; 6 | declare module "*.bmp"; 7 | declare module "*.css"; 8 | -------------------------------------------------------------------------------- /assets/css/timeline.css: -------------------------------------------------------------------------------- 1 | flex-direction: column-reverse; 2 | align-items: stretch; 3 | justify-content: flex-end; 4 | background-color: #fff; 5 | -------------------------------------------------------------------------------- /assets/emojis.json: -------------------------------------------------------------------------------- 1 | { 2 | "emojis": [ 3 | "(=^・・^=)", 4 | "v('ω')v", 5 | "🐡( '-' 🐡 )フグパンチ!!!!", 6 | "✌️(´・_・`)✌️", 7 | "(。>﹏<。)", 8 | "(Δ・x・Δ)", 9 | "(コ`・ヘ・´ケ)", 10 | "ԅ( ˘ω˘ ԅ)モミモミ", 11 | "( ‘ω’ و(و “" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /assets/file_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "files":[ 3 | { 4 | "id": "tmp", 5 | "path": "/tmp", 6 | "type": "dir" 7 | }, 8 | { 9 | "id": "user_contents", 10 | "path": "/user_contents", 11 | "type": "dir" 12 | }, 13 | { 14 | "id": "settings", 15 | "path": "/settings", 16 | "type": "dir" 17 | }, 18 | { 19 | "id": "ng_settings", 20 | "path": "/ng_settings", 21 | "type": "dir" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /assets/icons/clip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/clip.png -------------------------------------------------------------------------------- /assets/icons/direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/direct.png -------------------------------------------------------------------------------- /assets/icons/follow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/follow.png -------------------------------------------------------------------------------- /assets/icons/follow_accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/follow_accept.png -------------------------------------------------------------------------------- /assets/icons/follow_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/follow_request.png -------------------------------------------------------------------------------- /assets/icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/home.png -------------------------------------------------------------------------------- /assets/icons/local_direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/local_direct.png -------------------------------------------------------------------------------- /assets/icons/local_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/local_home.png -------------------------------------------------------------------------------- /assets/icons/local_lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/local_lock.png -------------------------------------------------------------------------------- /assets/icons/local_public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/local_public.png -------------------------------------------------------------------------------- /assets/icons/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/lock.png -------------------------------------------------------------------------------- /assets/icons/poll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/poll.png -------------------------------------------------------------------------------- /assets/icons/public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/public.png -------------------------------------------------------------------------------- /assets/icons/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/quote.png -------------------------------------------------------------------------------- /assets/icons/reaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/reaction.png -------------------------------------------------------------------------------- /assets/icons/renote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/renote.png -------------------------------------------------------------------------------- /assets/icons/reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/icons/reply.png -------------------------------------------------------------------------------- /assets/no_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coke12103/TenCha/0e63c30a28ced55dc10afb6173d2258809d515d1/assets/no_image.png -------------------------------------------------------------------------------- /assets/placeholder.json: -------------------------------------------------------------------------------- 1 | { 2 | "placeholder": [ 3 | "いまどうしてる?", 4 | "何かありましたか?", 5 | "何をお考えですか?", 6 | "言いたいことは?", 7 | "ここに書いてください", 8 | "あなたが書くのを待っています...", 9 | "お気持ちを表明してください", 10 | "#わーーーーーーーーーーーーーーーーー", 11 | "投稿内容を入力...", 12 | "What’s happening?", 13 | "いまもふもふしてる?", 14 | "メッセージを送信", 15 | "藍かわいいよ藍", 16 | "おなかすいた?", 17 | "言い残したいことは?", 18 | "ハイクを詠め", 19 | "Write here", 20 | "ノートが土から生えてくるんだ" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /assets/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": [ 3 | { 4 | "id": "use_emojis", 5 | "name": "絵文字を表示", 6 | "description": "絵文字実装を利用して絵文字を表示するか", 7 | "default_value": true, 8 | "type": "Bool" 9 | }, 10 | { 11 | "id": "use_desktop_notification", 12 | "name": "デスクトップ通知を使用", 13 | "description": "デスクトップ通知を使用するか", 14 | "default_value": true, 15 | "type": "Bool" 16 | }, 17 | { 18 | "id": "post_cache_limit", 19 | "name": "投稿キャッシュの上限", 20 | "description": "上限に到達すると開放処理を1回分実行し、開放できる範囲で投稿キャッシュを削除します。", 21 | "default_value": 5000, 22 | "type": "Num" 23 | }, 24 | { 25 | "id": "post_cache_clear_count", 26 | "name": "投稿キャッシュ開放処理の1回の処理数", 27 | "description": "開放処理1回につき何件処理するか。", 28 | "default_value": 1000, 29 | "type": "Num" 30 | }, 31 | { 32 | "id": "start_load_limit", 33 | "name": "起動直後の読み込み数上限", 34 | "description": "1~100の間で指定", 35 | "default_value": 40, 36 | "type": "Num", 37 | "min": 1, 38 | "max": 100 39 | }, 40 | { 41 | "id": "font", 42 | "name": "フォント", 43 | "description": "フォント名を指定します。サイズは固定です。(再起動必須)", 44 | "default_value": "sans", 45 | "type": "String" 46 | }, 47 | { 48 | "id": "notification_sound", 49 | "name": "通知音", 50 | "description": "ファイルを指定します。", 51 | "default_value": "./notify.mp3", 52 | "type": "Path" 53 | }, 54 | { 55 | "id": "post_sound", 56 | "name": "投稿が来た時の音", 57 | "description": "ファイルを指定します。", 58 | "default_value": "./post.mp3", 59 | "type": "Path" 60 | }, 61 | { 62 | "id": "start_visibility", 63 | "name": "初期公開範囲", 64 | "description": "初期の公開範囲を指定します", 65 | "default_value": "public", 66 | "type": "Select", 67 | "select_values": [ 68 | { 69 | "id": "public", 70 | "text": "公開" 71 | }, 72 | { 73 | "id": "home", 74 | "text": "ホーム" 75 | }, 76 | { 77 | "id": "followers", 78 | "text": "フォロワー" 79 | }, 80 | { 81 | "id": "specified", 82 | "text": "ダイレクト" 83 | }, 84 | { 85 | "id": "random", 86 | "text": "ランダム" 87 | } 88 | ] 89 | }, 90 | { 91 | "id": "memory_visibility", 92 | "name": "公開範囲を記憶", 93 | "description": "有効にすると投稿後に公開範囲をリセットしません", 94 | "default_value": true, 95 | "type": "Bool" 96 | }, 97 | { 98 | "id": "search_engine", 99 | "name": "検索エンジン", 100 | "description": "なになに 検索の形式のリンクの検索エンジン(`https://`や%sはなしで指定)", 101 | "default_value": "duckduckgo.com/?q=", 102 | "type": "String" 103 | }, 104 | { 105 | "id": "default_timeline_limit", 106 | "name": "デフォルトのタイムラインの表示上限", 107 | "description": "タイムラインに個別に上限指定がない場合何件まで表示するか(再起動必須)", 108 | "default_value": 200, 109 | "type": "Num" 110 | }, 111 | { 112 | "id": "fake_useragent", 113 | "name": "画像リクエスト時のUserAgentの偽装", 114 | "description": "アイコンや絵文字、添付ファイルなどの取得時にUserAgentをブラウザに偽装します。サードパーティのクライアントを弾いているインスタンスなどに有効です。", 115 | "default_value": false, 116 | "type": "Bool" 117 | }, 118 | { 119 | "id": "full_timeline_flag_view", 120 | "name": "投稿のフラグ表示を3行にする", 121 | "description": "OFFにすると表示し切れない情報が出た場合投稿の種別のフラグが切り捨てられます。", 122 | "default_value": false, 123 | "type": "Bool" 124 | }, 125 | { 126 | "id": "self_post_color", 127 | "name": "自分の投稿に色", 128 | "description": "タイムライン上の自分の投稿に色を付けます。", 129 | "default_value": false, 130 | "type": "Bool" 131 | }, 132 | { 133 | "id": "reaction_picker_emojis", 134 | "name": "リアクションの絵文字", 135 | "description": "Misskey(v10-m544)のピッカーの設定をそのまま貼り付け可能", 136 | "default_value": "👍❤😆🤔😮🎉💢😥😇🍮", 137 | "type": "String" 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /assets/version_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "version_data": { 3 | "v10-m544":{ 4 | "id": "v10-m544", 5 | "match_data": "^10\\..*m544$", 6 | "diffs": [ 7 | 8 | ] 9 | }, 10 | "v11":{ 11 | "id": "v11", 12 | "match_data": "^11\\..*$", 13 | "diffs": [ 14 | { 15 | "path": "notes/reactions/create", 16 | "type": "body", 17 | "target": "reaction", 18 | "replace": [ 19 | { 20 | "from": "👍", 21 | "to": "like" 22 | }, 23 | { 24 | "from": "❤", 25 | "to": "love" 26 | }, 27 | { 28 | "from": "😆", 29 | "to": "laugh" 30 | }, 31 | { 32 | "from": "🤔", 33 | "to": "hmm" 34 | }, 35 | { 36 | "from": "😮", 37 | "to": "surprise" 38 | }, 39 | { 40 | "from": "🎉", 41 | "to": "congrats" 42 | }, 43 | { 44 | "from": "💢", 45 | "to": "angry" 46 | }, 47 | { 48 | "from": "😥", 49 | "to": "confused" 50 | }, 51 | { 52 | "from": "😇", 53 | "to": "rip" 54 | }, 55 | { 56 | "from": "🍮", 57 | "to": "pudding" 58 | } 59 | ] 60 | } 61 | ] 62 | }, 63 | "v12":{ 64 | "id": "v12", 65 | "match_data": "^12\\..*$", 66 | "diffs": [ 67 | 68 | ] 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/tabs.json.md: -------------------------------------------------------------------------------- 1 | # カスタムタブの利用方法 2 | ## 説明 3 | TenChaはタイムラインの重複防止を搭載し、タイムラインのタブをJSONファイルで管理しているので、複数のタイムラインを統合して表示することが可能です。また定義次第では特定の内容を抽出したタブを作ることも可能です。 4 | ## 前提知識 5 | - JSONファイルの構造 6 | 7 | ## 構造 8 | TenChaのタイムラインのファイルは`tabs.json`です。 9 | 中には`tabs`の配列があり、その中身は以下の要素で構成されています。 10 | 11 | - `id` 12 | - `name` 13 | - `disable_global_filter` 14 | - `limit` 15 | - `skin_name` 16 | - `source` 17 | - `from` 18 | - `filter` 19 | - `type` 20 | - `match` 21 | - `match_type` 22 | - `ignore_case` 23 | - `negative` 24 | 25 | これらの個別の説明は以下の通りです。 26 | 27 | | 項目 | 説明 | 28 | | ----|---- | 29 | | `id` | 必須。文字列。
タブの管理用のIDです。重複してはいけません。
基本的には半角英数が推奨されます。 | 30 | | `name` | 必須。文字列。
表示で利用される名前です。重複可、文字種制限なしです。| 31 | | `disable_global_filter` | 任意。真偽値。
全体に適用されるNG機能をこのタブに適用するか。 | 32 | | `limit` | 任意。数値。
このタイムラインの投稿蓄積上限。指定されなれば200件。 | 33 | | `skin_name` | 任意。文字列。
このタイムラインの見た目。下記の組み込みのテーマの名前を指定します。| 34 | | `source` | 必須。Object。
このタイムラインに流れる投稿に関する設定。| 35 | | `from` | 必須。文字列の配列。
この中には下記のタイムラインソースを文字列として入れます。| 36 | | `filter` | 任意。Objectの配列。
表示する内容を細かく調整できます。中の要素は複数定義できます。
基本的には表示しないという定義の方が優先されます。| 37 | | `type` | 必須。文字列。
何に対して定義を適用するか。下記のフィルターの種類のうちどれが1つを指定します。| 38 | | `match` | 必須。文字列。
フィルター対象と評価される値。`match_type`の指定によってどう比較されるかが変わります。| 39 | | `match_type` | 必須。文字列。
フィルター対象と`match`をどう比較するかの指定。
下記のフィルターの比較の種類のうちどれか1つを指定します。| 40 | | `ignore_case` | 任意。真偽値。
大文字小文字を区別するか。| 41 | | `negative` | 任意。真偽値。
指定された定義を否定にするか。 42 | 43 | ## タイムラインソース 44 | | 項目 | 説明 | 45 | | ----|---- | 46 | | `home` | ホーム | 47 | | `local` | ローカル | 48 | | `social` | ソーシャル | 49 | | `global` | グローバル | 50 | | `notification` | 通知 | 51 | | `all` | 通知を含む受信したすべての投稿 | 52 | | `post` | 通知を除く受信したすべての投稿 | 53 | 54 | ## フィルターの種類 55 | | 項目 | 説明 | 56 | | ----|---- | 57 | | `username` | `coke12103`のような形式のユーザーのID | 58 | | `acct` | `c0_ke@misskey.dev`のような形式のドメインが入ったユーザーのID | 59 | | `name` | ユーザーの表示名 | 60 | | `text` | 投稿の本文とCW | 61 | | `host` | インスタンスのドメイン | 62 | | `visibility` | 公開範囲 | 63 | | `file_count` | 添付されたファイルの数。比較の種類の指定が他のものと違うため注意。 | 64 | 65 | ## フィルターの比較の種類 66 | | 項目 | 説明 | 67 | | ----|---- | 68 | | `full`| 完全一致| 69 | | `part`| 部分一致| 70 | | `regexp`| 正規表現マッチ| 71 | 72 | ### `file_count`のみで利用される比較 73 | | 項目 | 説明 | 74 | | ----|---- | 75 | | `match`| 枚数完全一致| 76 | | `more`| 指定された枚数以上| 77 | | `under`| 指定された枚数以下| 78 | 79 | ## 組み込みのテーマ 80 | | テーマ名 | 説明 | 81 | | ---- | ---- | 82 | | `default`| デフォルトのテーマ。 | 83 | | `sobacha`| TwitterクライアントのSobaChaの1行表示風のテーマ。| 84 | 85 | 86 | ## サンプル 87 | ``` 88 | { 89 | "tabs": [ 90 | { 91 | "id": "home", 92 | "name": "ホーム", 93 | "source": { 94 | "from": [ 95 | "home" 96 | ] 97 | } 98 | }, 99 | { 100 | "id": "notification", 101 | "name": "通知", 102 | "source": { 103 | "from": [ 104 | "notification" 105 | ] 106 | } 107 | }, 108 | { 109 | "id": "tencha", 110 | "name": "TenCha", 111 | "source": { 112 | "from": ["social", "global"], 113 | "filter": [ 114 | { 115 | "negative": false, 116 | "type": "text", 117 | "match": "TenCha", 118 | "match_type": "part", 119 | "ignore_case": true 120 | } 121 | ] 122 | } 123 | }, 124 | { 125 | "id": "coke", 126 | "name": "自分だけ", 127 | "disable_global_filter": true, 128 | "source": { 129 | "from": ["social", "global", "home"], 130 | "filter": [ 131 | { 132 | "negative": false, 133 | "type": "acct", 134 | "match": "c0_ke@missley.dev", 135 | "match_type": "full", 136 | "ignore_case": true 137 | } 138 | ] 139 | } 140 | }, 141 | { 142 | "id": "tenchamemo", 143 | "name": "メモ", 144 | "source": { 145 | "from": ["social", "global", "home"], 146 | "filter": [ 147 | { 148 | "negative": false, 149 | "type": "acct", 150 | "match": "c0_ke@misskey.dev", 151 | "match_type": "full", 152 | "ignore_case": true 153 | }, 154 | { 155 | "type": "text", 156 | "match": "TenChaMemo", 157 | "match_type": "part", 158 | "ignore_case": true 159 | } 160 | ] 161 | } 162 | }, 163 | { 164 | "id": "locked", 165 | "name": "鍵TL", 166 | "source": { 167 | "from": ["social", "global", "home"], 168 | "filter": [ 169 | { 170 | "negative": false, 171 | "type": "visibility", 172 | "match": "followers", 173 | "match_type": "full", 174 | "ignore_case": true 175 | } 176 | ] 177 | } 178 | }, 179 | { 180 | "id": "pawoo", 181 | "name": "Pawoo", 182 | "source": { 183 | "from": ["social", "global", "home"], 184 | "filter": [ 185 | { 186 | "negative": false, 187 | "type": "host", 188 | "match": "pawoo.net", 189 | "match_type": "full", 190 | "ignore_case": true 191 | } 192 | ] 193 | } 194 | }, 195 | { 196 | "id": "pawoo_image", 197 | "name": "Pawooメディア", 198 | "source": { 199 | "from": ["social", "global", "home"], 200 | "filter": [ 201 | { 202 | "type": "host", 203 | "match": "pawoo.net", 204 | "match_type": "full", 205 | "ignore_case": true 206 | }, 207 | { 208 | "type": "file_count", 209 | "match": "0", 210 | "match_type": "more" 211 | } 212 | ] 213 | } 214 | } 215 | ] 216 | } 217 | ``` 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TenCha", 3 | "version": "0.5.6", 4 | "description": "2009年のMisskeyクライアント", 5 | "main": "index.js", 6 | "author": "coke12103", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "build": "webpack -p", 11 | "start": "webpack && qode ./dist/index.js", 12 | "debug": "webpack && qode --inspect ./dist/index.js", 13 | "lint": "eslint 'src/**/*.js'" 14 | }, 15 | "dependencies": { 16 | "@nodegui/nodegui": "0.40.1", 17 | "dateformat": "4.5.1", 18 | "mime-types": "^2.1.33", 19 | "node-notifier": "^10.0.0", 20 | "play-sound": "^1.1.3", 21 | "request": "^2.88.2", 22 | "request-promise": "^4.2.6", 23 | "twemoji-parser": "^13.1.0", 24 | "ws": "^8.2.3" 25 | }, 26 | "devDependencies": { 27 | "@nodegui/packer": "^1.5.0", 28 | "@types/node": "^16.11.6", 29 | "clean-webpack-plugin": "^4.0.0", 30 | "copy-webpack-plugin": "^9.0.1", 31 | "eslint": "^8.1.0", 32 | "file-loader": "^6.2.0", 33 | "native-addon-loader": "^2.0.1", 34 | "raw-loader": "^4.0.2", 35 | "ts-loader": "^9.2.6", 36 | "typescript": "^4.4.4", 37 | "webpack": "5.61.0", 38 | "webpack-cli": "^4.9.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # TenCha 2 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcoke12103%2FTenCha.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcoke12103%2FTenCha?ref=badge_shield) 3 | ![Build](https://github.com/coke12103/TenCha/workflows/TenCha%20CI/badge.svg) 4 | 5 | 2009年のTwitterへの夢と軽量クライアントの夢でできたMisskeyクライアント 6 | 7 | ## 特徴 8 | ### いにしえより続くGUI 9 | より多くの情報が表示でき、コンパクトで、それでいて軽い。 10 | ### 最高に謎な謎機能 11 | ランダム公開範囲や投稿する度に変わるプレースホルダーなど様々な謎機能を搭載。Misskey v12から削除されてしまったランダム絵文字(通称フグパンチ)も装備。 12 | ### 強力なNG機能 13 | 特定のドメインからの投稿を表示しなくなるNGドメイン、単語や正規表現で指定したワードを含む投稿を表示しなくなるNGワード、ユーザーをIDの完全一致また正規表現で指定し表示しなくなるNGユーザー、指定した単語を特定の単語に置換する置換ミュートを搭載。 14 | ### タブを自由に定義 15 | カスタムタブを利用すれば、特定の内容のみを抽出したタイムラインをいくつでも定義することができます。 16 | ### 様々な環境に対応 17 | Misskey v11、Misskey v12だけではなくMisskey v10(m544)に対応。 18 | 19 | ## 要件 20 | これらはソースコードを動かす場合のみに必要でバイナリ版では不要です。 21 | ### Node.js 22 | `v14.2.0`で開発しているのでそれ以降推奨。 23 | ### Npm 24 | ### CMake 25 | 3.1以上 26 | ### Make, GCC v7 27 | 28 | ## 動かし方 29 | 1. `npm i` 30 | 2. `npm start` 31 | 32 | ## ビルド方法 33 | 1. `npm i` 34 | 2. `npm run build` 35 | 3. `npx nodegui-packer --init TenCha` 36 | 4. `npx nodegui-packer --pack ./dist` 37 | 38 | ## 初期化 39 | `data`ディレクトリを削除してください。 40 | 41 | ## 設定 42 | See assets/settings.json 43 | 44 | ## 連絡先 45 | - Twitter: [coke12103](https://twitter.com/@coke12103) 46 | - Misskey: [c0_ke@mk.iaia.moe](https://mk.iaia.moe/@c0_ke) 47 | 48 | 49 | ## License 50 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcoke12103%2FTenCha.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcoke12103%2FTenCha?ref=badge_large) 51 | -------------------------------------------------------------------------------- /replace_blocks.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace_words":[ 3 | { 4 | "from": "Nvidia", 5 | "to": "謎のAI半導体メーカー", 6 | "regexp": false, 7 | "ignore_case": false 8 | }, 9 | { 10 | "from": "GTX([0-9]+)", 11 | "to": "RTX$1", 12 | "regexp": true, 13 | "ignore_case": false 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/assets.js: -------------------------------------------------------------------------------- 1 | class Asset{ 2 | constructor(asset_type){ 3 | switch(asset_type){ 4 | case "PostView": 5 | this.no_image = require('../assets/no_image.png').default; 6 | break; 7 | case "Postbox": 8 | this.placeholder = require('../assets/placeholder.json').placeholder; 9 | break; 10 | case "TimelineWidget": 11 | this.css = require('!!raw-loader!../assets/css/timeline.css').default; 12 | break; 13 | case "RandomEmoji": 14 | this.emojis = require('../assets/emojis.json').emojis; 15 | break; 16 | case "SettingsLoader": 17 | this.settings_template = require('../assets/settings.json').settings; 18 | break; 19 | case "CustomPostWindow": 20 | this.placeholder = require('../assets/placeholder.json').placeholder; 21 | break; 22 | case "UserAgent": 23 | this.fake = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"; 24 | break; 25 | case "Icons": 26 | this.clip = require('../assets/icons/clip.png').default; 27 | this.direct = require('../assets/icons/direct.png').default; 28 | this.home = require('../assets/icons/home.png').default; 29 | this.lock = require('../assets/icons/lock.png').default; 30 | this._public = require('../assets/icons/public.png').default; 31 | this.renote = require('../assets/icons/renote.png').default; 32 | this.reply = require('../assets/icons/reply.png').default; 33 | this.quote = require('../assets/icons/quote.png').default; 34 | this.reaction = require('../assets/icons/reaction.png').default; 35 | this.local_public = require('../assets/icons/local_public.png').default; 36 | this.local_home = require('../assets/icons/local_home.png').default; 37 | this.local_lock = require('../assets/icons/local_lock.png').default; 38 | this.local_direct = require('../assets/icons/local_direct.png').default; 39 | this.follow = require('../assets/icons/follow.png').default; 40 | this.follow_request = require('../assets/icons/follow_request.png').default; 41 | this.follow_accept = require('../assets/icons/follow_accept.png').default; 42 | this.poll = require('../assets/icons/poll.png').default; 43 | break; 44 | case "EmojiList": 45 | this.emoji_list = require('../assets/emojilist.json'); 46 | break; 47 | case "FileList": 48 | this.files = require('../assets/file_list.json').files; 49 | break; 50 | case "VersionData": 51 | this.version_data = require('../assets/version_data.json').version_data; 52 | break; 53 | } 54 | 55 | return; 56 | } 57 | } 58 | 59 | module.exports = Asset; 60 | -------------------------------------------------------------------------------- /src/blocker/domain.js: -------------------------------------------------------------------------------- 1 | const file = require('../file.js'); 2 | const App = require('../index.js'); 3 | 4 | class DomainBlocker{ 5 | constructor(){ 6 | if(!file.exist_check(`${App.data_directory.get('ng_settings')}domain_blocks.json`)){ 7 | this.create_default_file(); 8 | } 9 | 10 | try{ 11 | var f = file.load(`${App.data_directory.get('ng_settings')}domain_blocks.json`); 12 | f = JSON.parse(f); 13 | this.block_domains = f.block_domains; 14 | }catch(err){ 15 | console.log(err); 16 | this.block_domains = []; 17 | } 18 | } 19 | 20 | create_default_file(){ 21 | var default_file = { 22 | block_domains: [] 23 | }; 24 | 25 | file.json_write_sync(`${App.data_directory.get('ng_settings')}domain_blocks.json`, default_file); 26 | } 27 | 28 | is_block(note){ 29 | var result = false; 30 | for(var domain of this.block_domains){ 31 | if(note.user.host == domain) result = true; 32 | if(note.renote && note.renote.user.host == domain) result = true; 33 | } 34 | 35 | return result; 36 | } 37 | } 38 | 39 | module.exports = DomainBlocker; 40 | -------------------------------------------------------------------------------- /src/blocker/index.js: -------------------------------------------------------------------------------- 1 | const DomainBlocker = require('./domain.js'); 2 | const WordBlocker = require('./word.js'); 3 | const UserIdBlocker = require('./user_id.js'); 4 | const ReplaceBlocker = require('./replace.js'); 5 | 6 | class Blocker{ 7 | constructor(){ 8 | this.blocker = []; 9 | } 10 | 11 | init(){ 12 | const domain_blocker = new DomainBlocker(); 13 | const word_blocker = new WordBlocker(); 14 | const user_id_blocker = new UserIdBlocker(); 15 | const replace_blocker = new ReplaceBlocker(); 16 | 17 | this.blocker.push(domain_blocker); 18 | this.blocker.push(word_blocker); 19 | this.blocker.push(user_id_blocker); 20 | this.blocker.push(replace_blocker); 21 | } 22 | 23 | is_block(note){ 24 | var result = false; 25 | 26 | for(var block of this.blocker){ 27 | var b = block.is_block(note); 28 | if(b) result = true; 29 | } 30 | 31 | return !result; 32 | } 33 | } 34 | 35 | module.exports = Blocker; 36 | -------------------------------------------------------------------------------- /src/blocker/replace.js: -------------------------------------------------------------------------------- 1 | const file = require('../file.js'); 2 | const App = require('../index.js'); 3 | 4 | class ReplaceBlocker{ 5 | constructor(){ 6 | if(!file.exist_check(`${App.data_directory.get('ng_settings')}replace_blocks.json`)){ 7 | this.create_default_file(); 8 | } 9 | 10 | try{ 11 | var f = file.load(`${App.data_directory.get('ng_settings')}replace_blocks.json`); 12 | f = JSON.parse(f); 13 | this.replace_words = f.replace_words; 14 | }catch(err){ 15 | console.log(err); 16 | this.replace_words = []; 17 | } 18 | } 19 | 20 | create_default_file(){ 21 | var default_file = { 22 | replace_words: [] 23 | }; 24 | 25 | file.json_write_sync(`${App.data_directory.get('ng_settings')}replace_blocks.json`, default_file); 26 | } 27 | 28 | is_block(note){ 29 | for(var word of this.replace_words){ 30 | var replace_word = word.from; 31 | var replace_regexp; 32 | if(word.regexp){ 33 | if(word.ignore_case){ 34 | replace_regexp = new RegExp(replace_word, 'gi'); 35 | }else{ 36 | replace_regexp = new RegExp(replace_word, 'g'); 37 | } 38 | }else{ 39 | replace_regexp = new RegExp(this.escape_regexp(replace_word), 'g'); 40 | } 41 | 42 | if(word.screen_name && note.user.screen_name) note.user.name = note.user.screen_name.replace(replace_regexp, word.to); 43 | if(note.cw) note.cw = note.cw.replace(replace_regexp, word.to); 44 | if(note.text) note.text = note.text.replace(replace_regexp, word.to); 45 | if(note.renote && note.renote.cw) note.renote.cw = note.renote.cw.replace(replace_regexp, word.to); 46 | if(note.renote && note.renote.text) note.renote.text = note.renote.text.replace(replace_regexp, word.to); 47 | if(note.reply && note.reply.cw) note.reply.cw = note.reply.cw.replace(replace_regexp, word.to); 48 | if(note.reply && note.reply.text) note.reply.text = note.reply.text.replace(replace_regexp, word.to); 49 | } 50 | 51 | return false; 52 | } 53 | 54 | escape_regexp(str){ 55 | return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); 56 | } 57 | } 58 | 59 | module.exports = ReplaceBlocker; 60 | -------------------------------------------------------------------------------- /src/blocker/user_id.js: -------------------------------------------------------------------------------- 1 | const file = require('../file.js'); 2 | const App = require('../index.js'); 3 | 4 | class UserIdBlocker{ 5 | constructor(){ 6 | if(!file.exist_check(`${App.data_directory.get('ng_settings')}user_id_blocks.json`)){ 7 | this.create_default_file(); 8 | } 9 | 10 | try{ 11 | var f = file.load(`${App.data_directory.get('ng_settings')}user_id_blocks.json`); 12 | f = JSON.parse(f); 13 | this.block_user_ids = f.block_user_ids; 14 | }catch(err){ 15 | console.log(err); 16 | this.block_user_ids = []; 17 | } 18 | } 19 | 20 | create_default_file(){ 21 | var default_file = { 22 | block_user_ids: [] 23 | }; 24 | 25 | file.json_write_sync(`${App.data_directory.get('ng_settings')}user_id_blocks.json`, default_file); 26 | } 27 | 28 | is_block(note){ 29 | var u_id = note.user.acct; 30 | var r_u_id; 31 | if(note.renote) r_u_id = note.renote.user.acct; 32 | 33 | var result = false; 34 | for(var id of this.block_user_ids){ 35 | var block_id = id.id; 36 | var id_regexp; 37 | if(id.regexp){ 38 | if(id.ignore_case){ 39 | id_regexp = new RegExp(block_id, 'gi'); 40 | }else{ 41 | id_regexp = new RegExp(block_id, 'g'); 42 | } 43 | }else{ 44 | id_regexp = new RegExp(`^${this.escape_regexp(block_id)}$`, 'g'); 45 | } 46 | 47 | if(id_regexp.test(u_id)) result = true; 48 | if(r_u_id && id_regexp.test(r_u_id)) result = true; 49 | } 50 | 51 | return result; 52 | } 53 | 54 | escape_regexp(str){ 55 | return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); 56 | } 57 | } 58 | 59 | module.exports = UserIdBlocker; 60 | -------------------------------------------------------------------------------- /src/blocker/word.js: -------------------------------------------------------------------------------- 1 | const file = require('../file.js'); 2 | const App = require('../index.js'); 3 | 4 | class WordBlocker{ 5 | constructor(){ 6 | if(!file.exist_check(`${App.data_directory.get('ng_settings')}word_blocks.json`)){ 7 | this.create_default_file(); 8 | } 9 | 10 | try{ 11 | var f = file.load(`${App.data_directory.get('ng_settings')}word_blocks.json`); 12 | f = JSON.parse(f); 13 | this.block_words = f.block_words; 14 | }catch(err){ 15 | console.log(err); 16 | this.block_words = []; 17 | } 18 | } 19 | 20 | create_default_file(){ 21 | var default_file = { 22 | block_words: [] 23 | }; 24 | 25 | file.json_write_sync(`${App.data_directory.get('ng_settings')}word_blocks.json`, default_file); 26 | } 27 | 28 | is_block(note){ 29 | var cw = note.no_emoji_cw; 30 | var text = note.no_emoji_text; 31 | var renote = note.renote; 32 | var reply = note.reply; 33 | 34 | var result = false; 35 | for(var word of this.block_words){ 36 | var block_word = word.word; 37 | var word_regexp; 38 | if(word.regexp){ 39 | if(word.ignore_case){ 40 | word_regexp = new RegExp(block_word, 'gi'); 41 | }else{ 42 | word_regexp = new RegExp(block_word, 'g'); 43 | } 44 | }else{ 45 | word_regexp = new RegExp(this.escape_regexp(block_word), 'g'); 46 | } 47 | 48 | if(cw && word_regexp.test(cw)) result = true; 49 | if(text && word_regexp.test(text)) result = true; 50 | if(renote && renote.no_emoji_cw && word_regexp.test(renote.no_emoji_cw)) result = true; 51 | if(renote && renote.no_emoji_text && word_regexp.test(renote.no_emoji_text)) result = true; 52 | if(reply && reply.no_emoji_cw && word_regexp.test(reply.no_emoji_cw)) result = true; 53 | if(reply && reply.no_emoji_text && word_regexp.test(reply.no_emoji_text)) result = true; 54 | } 55 | 56 | return result; 57 | } 58 | 59 | escape_regexp(str){ 60 | return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); 61 | } 62 | } 63 | 64 | module.exports = WordBlocker; 65 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | const Login = require('./login.js'); 2 | const request = require("request-promise"); 3 | const WebSocket = require("ws"); 4 | 5 | const App = require('./index.js'); 6 | const MessageBox = require('./message_box.js'); 7 | 8 | class Client{ 9 | constructor(){ 10 | this.ws; 11 | } 12 | async login(){ 13 | var login = new Login(); 14 | try{ 15 | await login.start(); 16 | }catch(err){ 17 | console.log(err); 18 | var mes = new MessageBox('ログインエラー', 'わかった'); 19 | mes.exec(); 20 | process.exit(1); 21 | } 22 | 23 | this.host = login.host; 24 | this.token = login.token; 25 | this.secret = login.secret; 26 | this.api_token = login.api_token; 27 | 28 | this.username = login.username; 29 | this.version = login.version; 30 | this.emojis = login.emojis; 31 | } 32 | 33 | async call(path, data, is_ano = false, override_host = null){ 34 | var host = this.host; 35 | if(override_host) host = override_host; 36 | 37 | var req = { 38 | url: "https://" + host + "/api/" + path, 39 | method: "POST", 40 | json: data 41 | } 42 | if(!is_ano) data.i = this.api_token; 43 | 44 | try{ 45 | var body = await request(req); 46 | return body; 47 | }catch(err){ 48 | throw err; 49 | } 50 | } 51 | 52 | async call_multipart(path, data){ 53 | var req = { 54 | url: "https://" + this.host + "/api/" + path, 55 | method: "POST", 56 | formData: data 57 | } 58 | data.i = this.api_token; 59 | 60 | try{ 61 | var body = await request(req); 62 | return body; 63 | }catch(err){ 64 | throw err; 65 | } 66 | } 67 | 68 | connect_ws(timeline){ 69 | return new Promise((resolve, reject) => { 70 | this._connect(0, timeline); 71 | resolve(0); 72 | }) 73 | } 74 | 75 | _connect(count, timeline){ 76 | var url = 'wss://' + this.host + '/streaming?i=' + this.api_token; 77 | 78 | if(this.ws_connected) return; 79 | 80 | console.log('start connect'); 81 | const connection = new WebSocket(url); 82 | 83 | connection.on('open', () => { 84 | console.log('connected'); 85 | 86 | App.status_label.setText('サーバーに接続しました'); 87 | 88 | this.ws = connection; 89 | this.ws_connected = true; 90 | this._ws_heartbeat(); 91 | 92 | setTimeout(() => { 93 | this._create_channel_connect(connection, "notification", "main"); 94 | this._create_channel_connect(connection, "home", "homeTimeline"); 95 | this._create_channel_connect(connection, "local", "localTimeline"); 96 | this._create_channel_connect(connection, "social", "hybridTimeline"); 97 | this._create_channel_connect(connection, "global", "globalTimeline"); 98 | }, 100); 99 | // var data = { 100 | // type: "sn", 101 | // body: { 102 | // id: "id" 103 | // } 104 | // } 105 | // 106 | // connection.send(JSON.stringify(data)); 107 | }); 108 | 109 | connection.on('ping', this._ws_heartbeat); 110 | connection.on('pong', this._ws_heartbeat); 111 | 112 | connection.on('error', (err) => { 113 | console.log('error!: ' + err); 114 | App.status_label.setText('WebSocketエラー'); 115 | }); 116 | 117 | connection.on('close', (cl) => { 118 | console.log('closed!', cl); 119 | App.status_label.setText('サーバーから切断されました。0.5秒後に再接続を試みます。'); 120 | clearTimeout(this.pingTimeout) 121 | setTimeout(() => { 122 | this.ws_connected = false; 123 | this.ws = null; 124 | this._connect(count + 1, timeline); 125 | }, 500); 126 | }); 127 | 128 | connection.on('message', (data) => { 129 | this._ws_heartbeat(); 130 | timeline.onMess(data); 131 | }); 132 | } 133 | 134 | _create_channel_connect(con, id, channel){ 135 | var data = { 136 | type: "connect", 137 | body: { 138 | id: id, 139 | channel: channel 140 | } 141 | } 142 | 143 | con.send(JSON.stringify(data)); 144 | } 145 | 146 | _ws_heartbeat(){ 147 | if(!this.ws_connected) return; 148 | if(this.pingTimeout) clearTimeout(this.pingTimeout); 149 | console.log('heartbeat!'); 150 | this.pingTimeout = setTimeout(() => { 151 | this.ws.terminate(); 152 | }, 120000 + 1000); 153 | } 154 | } 155 | 156 | module.exports = Client; 157 | -------------------------------------------------------------------------------- /src/file.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | var _file = {}; 4 | 5 | _file.exist_check = function(path){ 6 | var exist = false; 7 | try{ 8 | fs.statSync(path); 9 | exist = true; 10 | }catch(err){ 11 | exist = false; 12 | } 13 | 14 | return exist; 15 | } 16 | 17 | _file.load = function(path){ 18 | var file; 19 | if(this.exist_check(path)){ 20 | file = fs.readFileSync(path, 'utf8'); 21 | }else{ 22 | console.log("File Not Found.: " + path); 23 | file = null; 24 | } 25 | 26 | return file; 27 | } 28 | 29 | _file.load_bin = function(path){ 30 | var file; 31 | if(this.exist_check(path)){ 32 | file = fs.readFileSync(path); 33 | }else{ 34 | console.log("File Not Found.: " + path); 35 | file = null; 36 | } 37 | 38 | return file; 39 | } 40 | 41 | _file.mkdir = function(dir){ 42 | try{ 43 | fs.mkdirSync(dir); 44 | return; 45 | }catch(err){ 46 | throw err; 47 | } 48 | } 49 | 50 | _file.json_write = function(path, data){ 51 | return new Promise(async (resolve, reject) => { 52 | try{ 53 | fs.writeFileSync(path, JSON.stringify(data, null, " "), (err) => { 54 | if(err){ 55 | console.log(err); 56 | throw err; 57 | } 58 | }); 59 | }catch(err){ 60 | reject(err); 61 | } 62 | 63 | resolve(true); 64 | }); 65 | } 66 | 67 | // なんか影響あると怖いのでSyncとして用意しておく 68 | _file.json_write_sync = function(path, data){ 69 | try{ 70 | fs.writeFileSync(path, JSON.stringify(data, null, " ")); 71 | }catch(err){ 72 | throw err; 73 | } 74 | } 75 | 76 | _file.bin_write_sync = function(path, data){ 77 | try{ 78 | fs.writeFileSync(path, data); 79 | }catch(err){ 80 | throw err; 81 | } 82 | } 83 | 84 | _file.bin_write = function(path, data){ 85 | return new Promise(async (resolve, reject) => { 86 | try{ 87 | fs.writeFile(path, data, (err) => { 88 | if(err){ 89 | console.log(err); 90 | throw err; 91 | } 92 | }); 93 | }catch(err){ 94 | reject(err); 95 | } 96 | 97 | resolve(true); 98 | }) 99 | } 100 | 101 | module.exports = _file; 102 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const MainWindow = require('./main_window.js'); 2 | 3 | const RandomEmoji = require('./tools/random_emoji/index.js'); 4 | const EmojiParser = require('./tools/emoji_parser/index.js'); 5 | const PostAction = require('./post_action.js'); 6 | const Settings = require('./tools/settings/index.js'); 7 | 8 | const ImageViewer = require('./tools/image_viewer/index.js'); 9 | const CustomPostWindow = require('./widgets/custom_post_window/index.js'); 10 | const Blocker = require('./blocker/index.js'); 11 | const MenuBar = require('./menubar/index.js'); 12 | 13 | const Client = require('./client.js'); 14 | const client = new Client(); 15 | exports.client = client; 16 | 17 | const UserCache = require('./tools/user_cache/index.js'); 18 | const NoteCache = require('./tools/note_cache/index.js'); 19 | const NotificationCache = require('./tools/notification_cache/index.js'); 20 | const SettingWindow = require('./widgets/setting_window/index.js'); 21 | const EmojiPicker = require('./widgets/emoji_picker/index.js'); 22 | const DataDirectory = require('./tools/data_directory/index.js'); 23 | const VersionParser = require('./tools/version_parser/index.js'); 24 | 25 | // ディレクトリは最初に読み込みする 26 | // もしオプションでディレクトリが指定されているならそれを利用する 27 | var data_directory; 28 | if(process.argv[2] && process.argv[3] && process.argv[2] == "data-dir"){ 29 | data_directory = new DataDirectory(process.argv[3]); 30 | }else{ 31 | data_directory = new DataDirectory(); 32 | } 33 | 34 | exports.data_directory = data_directory; 35 | 36 | var main_window = new MainWindow(); 37 | var statusLabel = main_window.status_label; 38 | exports.status_label = statusLabel; 39 | exports.timeline = main_window.timeline; 40 | 41 | var random_emoji = new RandomEmoji(); 42 | exports.random_emoji = random_emoji; 43 | 44 | var menu_bar = new MenuBar(); 45 | 46 | var emoji_parser = new EmojiParser(); 47 | exports.emoji_parser = emoji_parser; 48 | 49 | var settings = new Settings(); 50 | exports.settings = settings; 51 | 52 | var image_viewer = new ImageViewer(); 53 | 54 | var custom_post_window = new CustomPostWindow(); 55 | exports.custom_post_window = custom_post_window; 56 | 57 | var post_action = new PostAction(); 58 | exports.post_action = post_action; 59 | 60 | var blocker = new Blocker(); 61 | 62 | var user_cache = new UserCache(); 63 | var note_cache = new NoteCache(); 64 | var notification_cache = new NotificationCache(); 65 | exports.user_cache = user_cache; 66 | exports.note_cache = note_cache; 67 | exports.notification_cache = notification_cache; 68 | 69 | var version_parser = new VersionParser(); 70 | exports.version_parser = version_parser; 71 | 72 | async function init_cha(){ 73 | // 設定読み込み後にやるやつ 74 | settings.init(); 75 | blocker.init(); 76 | image_viewer.init(); 77 | 78 | menu_bar.post_menu.set_postbox(main_window.post_box); 79 | 80 | main_window.setMenuBar(menu_bar); 81 | 82 | main_window.setFont(settings.get('font')); 83 | menu_bar.setFont(settings.get('font')); 84 | 85 | var setting_window = new SettingWindow(); 86 | exports.setting_window = setting_window; 87 | 88 | statusLabel.setText('絵文字ピッカーの設定中...'); 89 | var emoji_picker = new EmojiPicker(); 90 | await emoji_picker.init(); 91 | exports.emoji_picker = emoji_picker; 92 | statusLabel.setText('絵文字ピッカーの設定完了'); 93 | 94 | custom_post_window.setup(); 95 | 96 | main_window.timeline.add_timeline_filter(blocker.is_block.bind(blocker)); 97 | 98 | main_window.show(); 99 | 100 | client.login().then(async () => { 101 | await version_parser.init(); 102 | menu_bar.init(); 103 | await main_window.timeline.init(); 104 | post_action.init(main_window.timeline, image_viewer); 105 | statusLabel.setText('ログイン成功!'); 106 | }); 107 | 108 | global.win = main_window; 109 | } 110 | 111 | init_cha(); 112 | -------------------------------------------------------------------------------- /src/login.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const Window = require('./widgets/login_window/index.js'); 4 | const file = require('./file.js'); 5 | const App = require('./index.js'); 6 | 7 | class Login{ 8 | constructor(){ 9 | this.is_login_done = false; 10 | } 11 | 12 | load_info(){ 13 | var result = false; 14 | 15 | try{ 16 | this.config_file = file.load(`${App.data_directory.get('settings')}config.json`); 17 | var config = JSON.parse(this.config_file); 18 | 19 | if(!config || !(config.host && config.token && config.secret)) throw "要素が満されていません!"; 20 | 21 | this.host = config.host; 22 | this.token = config.token; 23 | this.secret = config.secret 24 | var sha256 = crypto.createHash('sha256'); 25 | sha256.update(`${this.token}${this.secret}`); 26 | this.api_token = sha256.digest('hex'); 27 | 28 | result = true; 29 | }catch(err){ 30 | console.log(err); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | async _load_meta_info(){ 37 | try{ 38 | var meta = await App.client.call('meta', {}, true, this.host); 39 | this.version = meta.version; 40 | this.emojis = meta.emojis; 41 | 42 | var s = await App.client.call('i', { i: this.api_token }, true, this.host); 43 | this.username = s.username; 44 | }catch(err){ 45 | throw err; 46 | } 47 | } 48 | 49 | start(){ 50 | return new Promise(async (resolve, reject) =>{ 51 | // とりあえず読み込もうとする 52 | this.is_login_done = this.load_info(); 53 | 54 | // 読み込めたなら疎通テストしてエラーだったら投げる 55 | if(this.is_login_done){ 56 | try{ 57 | await this._load_meta_info(); 58 | }catch(err){ 59 | reject(err); 60 | } 61 | 62 | resolve(0); 63 | 64 | // 読み込めないならとりあえず新規ログインとして扱う 65 | }else{ 66 | const login_window = new Window(); 67 | 68 | var done_func = async function(){ 69 | var result = login_window.getResult(); 70 | if(result) reject(1); 71 | 72 | this.is_login_done = this.load_info(); 73 | await this._load_meta_info(); 74 | 75 | resolve(0); 76 | }; 77 | 78 | login_window.addEventListener('Close', done_func.bind(this)); 79 | login_window.show(); 80 | } 81 | }) 82 | } 83 | } 84 | 85 | module.exports = Login; 86 | -------------------------------------------------------------------------------- /src/main_window.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMainWindow, 3 | QLabel, 4 | QBoxLayout, 5 | Direction, 6 | QWidget, 7 | QFont 8 | } = require('@nodegui/nodegui'); 9 | 10 | const PostView = require('./widgets/postview/index.js'); 11 | const PostBox = require('./widgets/postbox/index.js'); 12 | const Timeline = require('./timelines/index.js'); 13 | 14 | class MainWindow extends QMainWindow{ 15 | constructor(){ 16 | super(); 17 | 18 | this.root = new QWidget(); 19 | this.root_layout = new QBoxLayout(Direction.TopToBottom); 20 | 21 | this.post_view = new PostView(); 22 | this.timeline = new Timeline(); 23 | this.post_box = new PostBox(); 24 | 25 | this.status_label = new QLabel(); 26 | 27 | this.setWindowTitle('TenCha'); 28 | // TODO: サイズ記憶 29 | this.resize(460, 700); 30 | 31 | this.root.setObjectName('rootView'); 32 | this.root.setLayout(this.root_layout); 33 | 34 | this.root_layout.setContentsMargins(5,5,5,5); 35 | this.root_layout.setSpacing(0); 36 | 37 | this.timeline.set_post_view(this.post_view); 38 | 39 | this.status_label.setWordWrap(true); 40 | this.status_label.setText('ログインチェック中...'); 41 | this.status_label.setObjectName('statusLabel'); 42 | this.status_label.setMinimumSize(120, 14); 43 | this.status_label.setMaximumSize(65535, 14); 44 | 45 | this.setCentralWidget(this.root); 46 | 47 | this.root_layout.addWidget(this.post_view); 48 | this.root_layout.addWidget(this.timeline, 1); 49 | this.root_layout.addWidget(this.post_box); 50 | this.root_layout.addWidget(this.status_label); 51 | } 52 | 53 | addWidget(widget, stretch = null){ 54 | var index = (this.root_layout.count > 0)? this.root_layout.count - 1: 0; 55 | 56 | if(stretch) this.root_layout.insertWidget(index, widget, stretch); 57 | else this.root_layout.insertWidget(index, widget); 58 | } 59 | 60 | setFont(fontname){ 61 | this.font = new QFont(fontname, 9); 62 | 63 | this.post_view.setFont(fontname); 64 | this.post_box.setup(this.font); 65 | this.status_label.setFont(this.font); 66 | } 67 | } 68 | 69 | module.exports = MainWindow; 70 | -------------------------------------------------------------------------------- /src/menubar/file_menu.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMenu, 3 | QAction 4 | } = require('@nodegui/nodegui'); 5 | 6 | const App = require('../index.js'); 7 | 8 | class FileMenu extends QMenu{ 9 | constructor(){ 10 | super(); 11 | 12 | this.setting_action = new QAction(); 13 | this.exit_action = new QAction(); 14 | 15 | this.setTitle('ファイル'); 16 | 17 | this.setting_action.setText('設定'); 18 | this.setting_action.addEventListener('triggered', () => { 19 | App.setting_window.exec(); 20 | }); 21 | 22 | this.exit_action.setText('終了'); 23 | this.exit_action.addEventListener('triggered', () => process.exit(0)); 24 | 25 | this.addAction(this.setting_action); 26 | this.addSeparator(this.exit_action); 27 | this.addAction(this.exit_action); 28 | } 29 | } 30 | 31 | module.exports = FileMenu; 32 | -------------------------------------------------------------------------------- /src/menubar/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMenuBar, 3 | QFont 4 | } = require('@nodegui/nodegui'); 5 | 6 | const FileMenu = require('./file_menu.js'); 7 | const PostMenu = require('./post_menu.js'); 8 | const TimelineMenu = require('../widgets/timeline_menu/index.js'); 9 | const OperationMenu = require('../widgets/operation_menu/index.js'); 10 | 11 | class MenuBar extends QMenuBar{ 12 | constructor(){ 13 | super(); 14 | 15 | this.file_menu = new FileMenu(); 16 | this.post_menu = new PostMenu(); 17 | this.timeline_menu = new TimelineMenu(); 18 | this.operation_menu = new OperationMenu(); 19 | 20 | this.addMenu(this.file_menu); 21 | this.addMenu(this.post_menu); 22 | this.addMenu(this.timeline_menu); 23 | this.addMenu(this.operation_menu); 24 | } 25 | 26 | init(){ 27 | this.operation_menu.init(); 28 | } 29 | 30 | setFont(fontname){ 31 | this.font = new QFont(fontname, 9); 32 | 33 | super.setFont(this.font); 34 | 35 | this.file_menu.setFont(this.font); 36 | this.post_menu.setFont(this.font); 37 | this.timeline_menu.setFont(this.font); 38 | this.operation_menu.setFont(this.font); 39 | } 40 | } 41 | 42 | module.exports = MenuBar; 43 | -------------------------------------------------------------------------------- /src/menubar/post_menu.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMenu, 3 | QAction 4 | } = require('@nodegui/nodegui'); 5 | 6 | const App = require('../index.js'); 7 | 8 | class PostMenu extends QMenu{ 9 | constructor(){ 10 | super(); 11 | 12 | this.random_emoji = new QAction(); 13 | this.emoji_picker = new QAction(); 14 | this.custom_post = new QAction(); 15 | 16 | this.setTitle('投稿'); 17 | 18 | this.random_emoji.setText('ランダム絵文字'); 19 | this.emoji_picker.setText('絵文字ピッカー'); 20 | this.custom_post.setText('カスタム投稿'); 21 | 22 | this.addAction(this.random_emoji); 23 | this.addAction(this.emoji_picker); 24 | this.addAction(this.custom_post); 25 | 26 | this.emoji_picker.addEventListener('triggered', () => { 27 | App.emoji_picker.exec(); 28 | App.emoji_picker.setCloseEvent(function(){ 29 | var result = App.emoji_picker.get_result(); 30 | this.postbox.filter((input) => input.insertPlainText(result)); 31 | }.bind(this)); 32 | } 33 | ); 34 | 35 | this.custom_post.addEventListener('triggered', () => { 36 | App.custom_post_window.exec({}); 37 | }) 38 | } 39 | 40 | set_postbox(postbox){ 41 | this.postbox = postbox; 42 | this.random_emoji.addEventListener('triggered', () => { 43 | postbox.random_emoji(); 44 | }); 45 | } 46 | } 47 | 48 | module.exports = PostMenu; 49 | -------------------------------------------------------------------------------- /src/message_box.js: -------------------------------------------------------------------------------- 1 | const { QMessageBox, QPushButton, ButtonRole } = require('@nodegui/nodegui'); 2 | 3 | class MessageBox extends QMessageBox{ 4 | constructor(display_message, button_message){ 5 | super(); 6 | 7 | this.accept = new QPushButton(); 8 | 9 | this.setText(display_message); 10 | this.setModal(true); 11 | 12 | this.accept.setText(button_message); 13 | 14 | this.addButton(this.accept, ButtonRole.NoRole); 15 | } 16 | 17 | onPush(callback){ 18 | this.accept.addEventListener('clicked', function(){ 19 | callback(); 20 | this.close(); 21 | }.bind(this)); 22 | } 23 | 24 | close(){ 25 | super.close(); 26 | } 27 | } 28 | 29 | module.exports = MessageBox; 30 | -------------------------------------------------------------------------------- /src/models/note.js: -------------------------------------------------------------------------------- 1 | const post_parser = require('../tools/post_parser/index.js'); 2 | const App = require('../index.js'); 3 | 4 | class Note{ 5 | constructor(json){ 6 | return (async () => { 7 | this.el_type = 'Note'; 8 | this.use_tl = {}; 9 | 10 | // 変わらない内容 11 | this.id = json.id; 12 | this.createdAt = Date.parse(json.createdAt); 13 | this.app = json.app; 14 | this.text = json.text; 15 | this.cw = json.cw; 16 | this.no_emoji_text = json.text; 17 | this.no_emoji_cw = json.cw; 18 | // this.display_text = json.text; 19 | // this.display_cw = json.cw; 20 | this.userId = json.userId; 21 | this.replyId = json.replyId; 22 | this.renoteId = json.renoteId; 23 | this.viaMobile = json.viaMobile; 24 | this.localOnly = json.localOnly; 25 | this.isHidden = json.isHidden; 26 | this.visibility = json.visibility; 27 | // this.mentions = if あとで 28 | this.visibleUserIds = json.visibleUserIds; 29 | this.fileIds = json.fileIds; 30 | this.files = json.files; 31 | this.tags = json.tags; 32 | this.poll = json.poll; 33 | this.geo = json.geo; 34 | this.uri = json.uri; 35 | 36 | // 変わる内容 37 | this.renoteCount = json.renoteCount; 38 | this.repliesCount = json.repliesCount; 39 | this.reactions = json.reactions; 40 | this.emojis = json.emojis; 41 | 42 | if(json.renote){ 43 | this.renote = await App.note_cache.get(json.renote); 44 | App.note_cache.use(this.renote.id, this.id); 45 | }else{ 46 | this.renote = null; 47 | } 48 | 49 | if(json.reply){ 50 | this.reply = await App.note_cache.get(json.reply); 51 | App.note_cache.use(this.reply.id, this.id); 52 | }else{ 53 | this.reply = null; 54 | } 55 | 56 | if(this.text) this.text = post_parser.escape_html(this.text); 57 | if(this.cw) this.cw = post_parser.escape_html(this.cw); 58 | 59 | // if(this.text){ 60 | // this.display_text = post_parser.escape_html(this.display_text); 61 | // this.display_text = await App.emoji_parser.parse(this.display_text, this.emojis); 62 | // } 63 | // if(this.cw){ 64 | // this.display_cw = post_parser.escape_html(this.cw); 65 | // this.display_cw = await App.emoji_parser.parse(this.display_text, this.emojis); 66 | // } 67 | 68 | this.user = await App.user_cache.use(json.user); 69 | 70 | this.is_renote = ( 71 | this.renote 72 | && !this.text 73 | && !this.cw 74 | && (!this.poll || (this.poll && !this.poll.choices)) 75 | && !this.files[0] 76 | ); 77 | 78 | this.is_quote = (!this.is_renote && this.renote); 79 | 80 | // 将来切りたい 81 | await App.emoji_parser.parse_note(this); 82 | 83 | return this; 84 | })(); 85 | } 86 | 87 | update(){ 88 | 89 | } 90 | 91 | purge(){ 92 | if(this.reply) App.note_cache.free(this.reply.id, this.id); 93 | if(this.renote) App.note_cache.free(this.renote.id, this.id); 94 | } 95 | } 96 | 97 | module.exports = Note; 98 | -------------------------------------------------------------------------------- /src/models/notification.js: -------------------------------------------------------------------------------- 1 | const App = require('../index.js'); 2 | 3 | class Notification{ 4 | constructor(notify){ 5 | return (async () => { 6 | this.el_type = 'Notification'; 7 | this.id = notify.id; 8 | this.createdAt = Date.parse(notify.createdAt); 9 | this.type = notify.type; 10 | this.userId = notify.userId; 11 | this.reaction = notify.reaction; 12 | this.display_reaction = notify.reaction; 13 | this.note; 14 | this.emojis = []; 15 | 16 | if(notify.note){ 17 | this.note = await App.note_cache.get(notify.note); 18 | App.note_cache.use(this.note.id, 'notification'); 19 | } 20 | 21 | if(this.reaction){ 22 | if(this.note){ 23 | this.emojis = this.note.emojis; 24 | } 25 | 26 | this.display_reaction = await App.emoji_parser.parse(this.reaction, this.emojis); 27 | } 28 | 29 | if(notify.user) this.user = await App.user_cache.use(notify.user); 30 | 31 | return this; 32 | })(); 33 | } 34 | } 35 | 36 | module.exports = Notification; 37 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | const request = require("request-promise"); 2 | const { QPixmap } = require('@nodegui/nodegui'); 3 | 4 | const no_image_image = require('../../assets/no_image.png'); 5 | const post_parser = require('../tools/post_parser/index.js'); 6 | const App = require('../index.js'); 7 | const Assets = require('../assets.js'); 8 | const UserAgent = new Assets('UserAgent'); 9 | 10 | class User{ 11 | constructor(json){ 12 | return (async () => { 13 | // 変わらないデータ 14 | this.id = json.id; 15 | this.username = json.username; 16 | this.host = json.host; 17 | if(this.host){ 18 | this.acct = this.username + "@" + this.host; 19 | }else{ 20 | this.acct = this.username; 21 | } 22 | this.url = json.url; 23 | this.createdAt = json.createdAt; 24 | 25 | // 変わるデータ 26 | this.name = json.name; 27 | this.no_emoji_name = json.name; 28 | // this.display_name = json.name; 29 | this.description = json.description; 30 | this.avatarUrl = json.avatarUrl; 31 | this.isCat = json.isCat; 32 | this.isLady = json.isLady; 33 | this.isAdmin = json.isAdmin; 34 | this.isBot = json.isBot; 35 | this.updatedAt = json.updatedAt; 36 | this.bannerUrl = json.bannerUrl; 37 | this.isLocked = json.isLocked; 38 | this.isModerator = json.isModerator; 39 | this.isSilenced = json.isSilenced; 40 | this.isSuspeded = json.isSuspeded; 41 | this.userDescription = json.userDescription; 42 | this.location = json.location; 43 | this.birthday = json.birthday; 44 | //this.fields = json.fields; 45 | this.followersCount = json.followersCount; 46 | this.followingCount = json.followingCount; 47 | this.notesCount = json.notesCount; 48 | //this.pinnedNoteIds = json.pinnedNoteIds; 49 | //this.pinnedNotes = json.pinnedNoteIds; 50 | //this.pinnedPageId = json.pinnedPageId; 51 | //this.pinnedPage = json.pinnedPage; 52 | //this.twoFactorEnabled = json.twoFactorEnabled; 53 | //this.usePasswordLessLogin = json.usePasswordLessLogin; 54 | //this.securityKeys = json.securityKeys; 55 | this.twitter = json.twitter; 56 | this.github = json.github; 57 | this.discord = json.discord; 58 | //this.hasUnreadSpecifiedNotes = json.hasUnreadSpecifiedNotes; 59 | //this.hasUnreadMentions = json.hasUnreadMentions; 60 | this.avatarId = json.avatarId; 61 | this.bannerId = json.bannerId; 62 | this.emojis = json.emojis; 63 | 64 | // if(this.name){ 65 | // this.display_name = post_parser.escape_html(this.name); 66 | // this.display_name = App.emoji_parser.parse(this.name, this.emojis); 67 | // } 68 | 69 | await this.load_avater().then((avater) => { 70 | this.avater = avater; 71 | }).catch((err) => { 72 | console.log('add avater failed') 73 | console.log(err); 74 | this.avater = null; 75 | }) 76 | 77 | if(this.name) this.name = post_parser.escape_html(this.name); 78 | await App.emoji_parser.parse_user(this); 79 | 80 | return this; 81 | })(); 82 | } 83 | 84 | async update(json){ 85 | if(this.name && (this.name != json.name)) this.name = json.name; 86 | if(this.emojis && (this.emojis != json.emojis)) this.emojis = json.emojis; 87 | if(this.avatarUrl && (this.avatarUrl != json.avatarUrl)){ 88 | this.avatarUrl = json.avatarUrl; 89 | await this.load_avater().then((avater) => { 90 | this.avater = avater; 91 | }).catch((err) => { 92 | console.log('add avater failed') 93 | console.log(err); 94 | }) 95 | } 96 | // if(this.name){ 97 | // this.display_name = post_parser.escape_html(this.name); 98 | // this.display_name = App.emoji_parser.parse(this.name, this.emojis); 99 | // } 100 | if(this.name) this.name = post_parser.escape_html(this.name); 101 | await App.emoji_parser.parse_user(this); 102 | } 103 | 104 | load_avater(){ 105 | return new Promise((resolve, reject) => { 106 | var headers = {}; 107 | if(App.settings.get('fake_useragent')) headers['User-Agent'] = UserAgent.fake; 108 | 109 | var opt = { 110 | url: this.avatarUrl, 111 | encoding: null, 112 | method: 'GET', 113 | headers: headers, 114 | resolveWithFullResponse: true 115 | }; 116 | 117 | var pix = new QPixmap(); 118 | 119 | request(opt).then((res) => { 120 | var ext = res.headers['content-type'].match(/\/([a-z]+)$/)[1].toUpperCase(); 121 | 122 | try{ 123 | pix.loadFromData(res.body, ext); 124 | console.log('set done'); 125 | }catch{ 126 | try{ 127 | pix.load(no_image_image.default); 128 | }catch(err){ 129 | throw err; 130 | } 131 | } 132 | resolve(pix); 133 | }).catch(() => { 134 | try{ 135 | pix.load(no_image_image.default); 136 | resolve(pix); 137 | }catch(err){ 138 | console.log(err); 139 | reject(1); 140 | } 141 | reject(1); 142 | }) 143 | }) 144 | } 145 | } 146 | 147 | module.exports = User; 148 | -------------------------------------------------------------------------------- /src/post_action.js: -------------------------------------------------------------------------------- 1 | const App = require('./index.js'); 2 | const DeleteConfirmWindow = require('./widgets/delete_confirm_window/index.js'); 3 | 4 | const { 5 | QApplication, 6 | QClipboardMode 7 | } = require('@nodegui/nodegui'); 8 | 9 | class PostAction{ 10 | constructor(){ 11 | this.clipboard = QApplication.clipboard(); 12 | } 13 | 14 | init(timelines, image_viewer){ 15 | this.timelines = timelines; 16 | this.image_viewer = image_viewer; 17 | } 18 | 19 | renote(){ 20 | this.timelines.filter(async (item) => { 21 | if((!item) || (item.el_type == 'Notification')) return; 22 | 23 | var _item = item; 24 | 25 | if(item.is_renote) _item = item.renote; 26 | if(!(_item.visibility === 'public' || _item.visibility === 'home')) return; 27 | 28 | var data = { renoteId: _item.id }; 29 | await App.client.call('notes/create',data); 30 | App.status_label.setText("Renoteしました!"); 31 | }) 32 | } 33 | 34 | quote(){ 35 | this.timelines.filter((item) => { 36 | if((!item) || (item.el_type == 'Notification')) return; 37 | 38 | var _item = item; 39 | 40 | if(item.is_renote) _item = item.renote; 41 | if(!(_item.visibility === 'public' || _item.visibility === 'home')) return; 42 | 43 | App.custom_post_window.exec({ renoteId: _item.id }); 44 | }) 45 | } 46 | 47 | reply(){ 48 | this.timelines.filter((item) => { 49 | if((!item) || (item.el_type == 'Notification')) return; 50 | 51 | var _item = item; 52 | 53 | if(item.is_renote) _item = item.renote; 54 | 55 | var opt = { 56 | replyId: _item.id, 57 | visibility: _item.visibility 58 | } 59 | 60 | if(_item.visibility === 'specified') opt.visibleUserIds = _item.visibleUserIds; 61 | 62 | App.custom_post_window.exec(opt); 63 | }) 64 | } 65 | 66 | uni_renote(){ 67 | this.timelines.filter((item) => { 68 | if((!item) || (item.el_type == 'Notification')) return; 69 | 70 | var _item = item; 71 | 72 | if(item.is_renote) _item = item.renote; 73 | if(!(_item.visibility === 'public' || _item.visibility === 'home')) return; 74 | 75 | var data = { renoteId: _item.id }; 76 | App.client.call('notes/create',data); 77 | App.custom_post_window.exec({ renoteId: _item.id }); 78 | }) 79 | } 80 | 81 | is_image_view_ready(){ 82 | var result = true; 83 | 84 | this.timelines.filter((item) => { 85 | if((!item) || (item.el_type == 'Notification')) result = false 86 | 87 | var _item = item; 88 | if(item.is_renote) _item = item.renote; 89 | 90 | if(!Object.keys(_item.files).length) result = false; 91 | }) 92 | 93 | return result; 94 | } 95 | 96 | is_renote_ready(){ 97 | var result = true; 98 | 99 | this.timelines.filter((item) => { 100 | if((!item) || (item.el_type == 'Notification')) result = false 101 | 102 | var _item = item; 103 | if(item.is_renote) _item = item.renote; 104 | 105 | if(!(_item.visibility === 'public' || _item.visibility === 'home')) result = false; 106 | }) 107 | 108 | return result; 109 | } 110 | 111 | image_view(){ 112 | this.timelines.filter((item) => { 113 | if((!item) || (item.el_type == 'Notification')) return; 114 | 115 | var _item = item; 116 | 117 | if(item.is_renote) _item = item.renote; 118 | 119 | if(!Object.keys(_item.files).length) return; 120 | 121 | this.image_viewer.show(_item); 122 | }) 123 | } 124 | 125 | is_remove_ready(){ 126 | var result = true; 127 | 128 | this.timelines.filter((item) => { 129 | if((!item) || (item.el_type == 'Notification')) result = false 130 | 131 | var _item = item; 132 | 133 | if( 134 | item.is_renote && 135 | !(item.user.username == App.client.username && !item.user.host) 136 | ) _item = item.renote; 137 | 138 | if(!(_item.user.username == App.client.username && !_item.user.host)){ 139 | result = false; 140 | } 141 | }) 142 | 143 | return result; 144 | } 145 | 146 | note_remove(){ 147 | this.timelines.filter((item) => { 148 | const delete_confirm_window = new DeleteConfirmWindow(item); 149 | delete_confirm_window.show(); 150 | }) 151 | } 152 | 153 | repost(){ 154 | this.timelines.filter((item) => { 155 | const delete_confirm_window = new DeleteConfirmWindow(item, true); 156 | delete_confirm_window.show(); 157 | }) 158 | } 159 | 160 | async _note_remove(item){ 161 | if((!item) || (item.el_type == 'Notification')) return; 162 | 163 | var _item = item; 164 | 165 | // 自分の投稿であるかの分岐、自分のでなかった場合Renote先の処理に移る(RenoteかQuoteかは関係ない) 166 | if( 167 | item.renote && 168 | !(item.user.username == App.client.username && !item.user.host) 169 | ) _item = item.renote; 170 | 171 | if(!(_item.user.username == App.client.username && !_item.user.host)) return; 172 | 173 | var data = { noteId: _item.id }; 174 | 175 | try{ 176 | await App.client.call('notes/delete',data); 177 | App.status_label.setText("削除しました!"); 178 | }catch(err){ 179 | console.log(err); 180 | App.status_label.setText("消せなかったかも..."); 181 | } 182 | } 183 | 184 | reaction(emoji){ 185 | this.timelines.filter(async function(item){ 186 | if((!item) || (item.el_type == 'Notification')) return; 187 | 188 | var _item = item; 189 | 190 | if(item.is_renote) _item = item.renote; 191 | 192 | var path = 'notes/reactions/create'; 193 | var data = { 194 | noteId: _item.id, 195 | reaction: emoji 196 | }; 197 | 198 | try{ 199 | await this._unreaction(_item.id); 200 | var parse_data = App.version_parser.parse(path, data); 201 | path = parse_data.path; 202 | data = parse_data.data; 203 | 204 | await App.client.call(path, data); 205 | App.status_label.setText("リアクションしました!"); 206 | }catch(err){ 207 | console.log(err); 208 | App.status_label.setText("できなかったかも..."); 209 | } 210 | }.bind(this)) 211 | } 212 | 213 | unreaction(){ 214 | this.timelines.filter(async function(item){ 215 | if((!item) || (item.el_type == 'Notification')) return; 216 | 217 | var _item = item; 218 | 219 | if(item.is_renote) _item = item.renote; 220 | 221 | await this._unreaction(_item.id); 222 | }.bind(this)) 223 | } 224 | 225 | async _unreaction(id){ 226 | var path = 'notes/reactions/delete'; 227 | var data = { 228 | noteId: id 229 | }; 230 | 231 | try{ 232 | await App.client.call(path, data); 233 | App.status_label.setText("リアクション外しました!"); 234 | }catch(err){ 235 | console.log(err); 236 | App.status_label.setText("できなかったかも..."); 237 | } 238 | } 239 | 240 | favorite(){ 241 | this.timelines.filter(async (item) => { 242 | if((!item) || (item.el_type == 'Notification')) return; 243 | 244 | var _item = item; 245 | 246 | if(item.is_renote) _item = item.renote; 247 | 248 | var data = { noteId: _item.id } 249 | 250 | try{ 251 | await App.client.call('notes/favorites/create', data); 252 | App.status_label.setText("お気に入りました!"); 253 | }catch(err){ 254 | console.log(err); 255 | App.status_label.setText("お気に入れなかった..."); 256 | } 257 | }) 258 | } 259 | 260 | copy_link(){ 261 | this.timelines.filter(async (item) => { 262 | if((!item) || (item.el_type == 'Notification')) return; 263 | 264 | var _item = item; 265 | 266 | if(item.is_renote) _item = item.renote; 267 | 268 | var url = `https://${App.client.host}/notes/${_item.id}`; 269 | 270 | try{ 271 | this.clipboard.setText(url, QClipboardMode.Clipboard); 272 | App.status_label.setText("Done!"); 273 | }catch(err){ 274 | console.log(err); 275 | App.status_label.setText("Error..."); 276 | } 277 | }) 278 | } 279 | } 280 | 281 | module.exports = PostAction; 282 | -------------------------------------------------------------------------------- /src/timelines/flag_label.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLabel, 3 | QPixmap, 4 | TransformationMode, 5 | AspectRatioMode 6 | } = require('@nodegui/nodegui'); 7 | 8 | const Assets = require("../assets.js"); 9 | const Icons = new Assets('Icons'); 10 | 11 | const IconsSize = 14; 12 | 13 | class FlagLabel extends QLabel{ 14 | constructor(){ 15 | super(); 16 | 17 | this.setMaximumSize(IconsSize, IconsSize); 18 | this.setMinimumSize(IconsSize, IconsSize); 19 | } 20 | 21 | setFlag(type){ 22 | var pixmap = new QPixmap(); 23 | 24 | switch(type){ 25 | case 'follow': 26 | pixmap.load(Icons.follow); 27 | break; 28 | case 'mention': 29 | case 'reply': 30 | pixmap.load(Icons.reply); 31 | break; 32 | case 'renote': 33 | pixmap.load(Icons.renote); 34 | break; 35 | case 'quote': 36 | pixmap.load(Icons.quote); 37 | break; 38 | case 'reaction': 39 | pixmap.load(Icons.reaction); 40 | break; 41 | case 'pollVote': 42 | pixmap.load(Icons.poll); 43 | break; 44 | case 'receiveFollowRequest': 45 | pixmap.load(Icons.follow_request); 46 | break; 47 | case 'followRequestAccepted': 48 | pixmap.load(Icons.follow_accept); 49 | break; 50 | } 51 | 52 | pixmap = pixmap.scaled( 53 | IconsSize, IconsSize, 54 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 55 | ); 56 | 57 | this.setPixmap(pixmap); 58 | } 59 | } 60 | 61 | module.exports = FlagLabel; 62 | 63 | -------------------------------------------------------------------------------- /src/timelines/note_counter.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLabel 3 | } = require('@nodegui/nodegui'); 4 | 5 | class NoteCounter extends QLabel{ 6 | constructor(){ 7 | super(); 8 | 9 | this.all = 0; 10 | this.current = 0; 11 | 12 | this.update(); 13 | } 14 | 15 | setAllCount(count){ 16 | this.all = count; 17 | this.update(); 18 | } 19 | 20 | setCurrentCount(count){ 21 | this.current = count; 22 | this.update(); 23 | } 24 | 25 | update(){ 26 | this.setText(`${this.current}/${this.all}`); 27 | } 28 | } 29 | 30 | module.exports = NoteCounter; 31 | -------------------------------------------------------------------------------- /src/timelines/note_skin/default.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLabel, 3 | QListWidgetItem, 4 | QSize, 5 | QWidget, 6 | QBoxLayout, 7 | Direction, 8 | QFont, 9 | ContextMenuPolicy 10 | } = require('@nodegui/nodegui'); 11 | 12 | const App = require('../../index.js'); 13 | const FlagWidget = require('./flag_widget.js'); 14 | const IconLabel = require('../../widgets/icon_label/index.js'); 15 | 16 | class NoteItem extends QWidget{ 17 | constructor(note, a, exe){ 18 | super(); 19 | 20 | this.item_height = 14; 21 | this.widget = this; 22 | 23 | this.list_item = new QListWidgetItem(); 24 | this.font = new QFont(App.settings.get("font"), 9); 25 | 26 | this.layout = new QBoxLayout(Direction.LeftToRight); 27 | 28 | this.flag = new FlagWidget(); 29 | this.icon = new IconLabel(this.item_height); 30 | this.name = new QLabel(); 31 | this.text = new QLabel(); 32 | 33 | this.setLayout(this.layout); 34 | this.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu); 35 | this.addEventListener('customContextMenuRequested', exe); 36 | 37 | this.layout.setContentsMargins(2,0,0,1); 38 | this.layout.setSpacing(5); 39 | 40 | this.list_item.setSizeHint(new QSize(1200, this.item_height + 1)); 41 | 42 | this.name.setFixedSize(120, this.item_height); 43 | this.name.setFont(this.font); 44 | 45 | this.text.setFont(this.font); 46 | 47 | this.layout.addWidget(this.flag); 48 | this.layout.addWidget(this.icon); 49 | this.layout.addWidget(this.name); 50 | this.layout.addWidget(this.text, 1); 51 | 52 | this.setNote(note); 53 | } 54 | 55 | setNote(note){ 56 | // flags 57 | this.flag.setNoteFlag(note); 58 | 59 | // name 60 | this.name.setText(note.user.acct); 61 | 62 | // avater 63 | this.icon.setPixmap(note.user.avater); 64 | 65 | // text 66 | this._parse_content_text(note); 67 | 68 | } 69 | 70 | _parse_note_text(note){ 71 | var result; 72 | if(note.cw){ 73 | result = note.cw.replace(/(\r\n|\n|\r)/gm," "); 74 | }else{ 75 | if(!note.text) result = ''; 76 | else result = note.text.replace(/(\r\n|\n|\r)/gm," "); 77 | } 78 | 79 | return result; 80 | } 81 | 82 | _parse_renote(note){ 83 | var result = this._parse_note_text(note); 84 | 85 | var renote = note.renote; 86 | 87 | var r_text = `RN @${renote.user.acct} `; 88 | if(result) result += " "; 89 | 90 | if(renote.reply){ 91 | r_text += this._parse_reply(renote); 92 | }else if(renote.renote){ 93 | r_text += this._parse_renote(renote); 94 | }else{ 95 | r_text += this._parse_note_text(renote); 96 | } 97 | 98 | result += r_text; 99 | 100 | return result; 101 | } 102 | 103 | _parse_reply(note){ 104 | var result = this._parse_note_text(note); 105 | var reply = note.reply; 106 | if(!reply) return result; 107 | 108 | var re_text; 109 | if(reply.renote){ 110 | re_text = this._parse_renote(reply); 111 | }else if(reply.reply){ 112 | re_text = this._parse_reply(reply); 113 | }else{ 114 | re_text = this._parse_note_text(reply); 115 | } 116 | 117 | result += ` RE: ${re_text}`; 118 | 119 | return result; 120 | } 121 | 122 | _parse_content_text(note){ 123 | var note_color = '#000'; 124 | var note_back = 'transparent'; 125 | 126 | var text = ''; 127 | 128 | if(note.renote){ 129 | note_back = 'rgba(119, 221, 117, 0.3)'; 130 | text = this._parse_renote(note); 131 | }else if(note.reply){ 132 | note_back = 'rgba(112, 116, 255, 0.3)'; 133 | text = this._parse_reply(note); 134 | }else{ 135 | text = this._parse_note_text(note); 136 | } 137 | 138 | if(note.cw){ 139 | note_color = '#555753'; 140 | } 141 | 142 | if( 143 | App.settings.get('self_post_color') 144 | && (note.user.username == App.client.username && !note.user.host) 145 | ) note_back = 'rgba(237, 146, 18, 0.3)'; 146 | 147 | this.setInlineStyle(`background-color: ${note_back};`); 148 | this.text.setInlineStyle(`color: ${note_color};`); 149 | 150 | this.text.setText(text); 151 | } 152 | 153 | destroy(){ 154 | this.layout.removeWidget(this.text); 155 | this.layout.removeWidget(this.name); 156 | this.layout.removeWidget(this.icon); 157 | this.layout.removeWidget(this.flag); 158 | 159 | this.text.close(); 160 | this.name.close(); 161 | this.icon.close(); 162 | this.flag.close(); 163 | 164 | this.close(); 165 | 166 | this.list_item = undefined; 167 | this.widget = undefined; 168 | this.font = undefined; 169 | this.item_height = undefined; 170 | this.layout = undefined; 171 | this.flag = undefined; 172 | this.icon = undefined; 173 | this.name = undefined; 174 | this.text = undefined; 175 | } 176 | } 177 | 178 | module.exports = NoteItem; 179 | -------------------------------------------------------------------------------- /src/timelines/note_skin/flag_widget.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | QLabel, 5 | Direction, 6 | QPixmap, 7 | TransformationMode, 8 | AspectRatioMode 9 | } = require('@nodegui/nodegui'); 10 | 11 | const Assets = require("../../assets.js"); 12 | const Icons = new Assets('Icons'); 13 | const App = require("../../index.js"); 14 | 15 | const IconsSize = 14; 16 | 17 | class FlagWidget extends QWidget{ 18 | constructor(){ 19 | super(); 20 | 21 | this.layout = new QBoxLayout(Direction.LeftToRight); 22 | 23 | // Left: PostType 24 | this.renote = new QLabel(); 25 | this.reply = new QLabel(); 26 | 27 | // Center: Files 28 | this.clip = new QLabel(); 29 | 30 | // Right: Visibility 31 | this.direct = new QLabel(); 32 | this.home = new QLabel(); 33 | this.lock = new QLabel(); 34 | 35 | this.local_public = new QLabel(); 36 | this.local_home = new QLabel(); 37 | this.local_lock = new QLabel(); 38 | this.local_direct = new QLabel(); 39 | 40 | this.setLayout(this.layout); 41 | // デフォルト 42 | this.setMaximumSize(30, IconsSize); 43 | this.setMinimumSize(30, IconsSize); 44 | 45 | this.layout.setContentsMargins(0,0,0,0); 46 | this.layout.setSpacing(2); 47 | 48 | var renote_pix = new QPixmap(Icons.renote); 49 | var reply_pix = new QPixmap(Icons.reply); 50 | 51 | var clip_pix = new QPixmap(Icons.clip); 52 | 53 | var direct_pix = new QPixmap(Icons.direct); 54 | var home_pix = new QPixmap(Icons.home); 55 | var lock_pix = new QPixmap(Icons.lock); 56 | 57 | var local_public_pix = new QPixmap(Icons.local_public); 58 | var local_home_pix = new QPixmap(Icons.local_home); 59 | var local_lock_pix = new QPixmap(Icons.local_lock); 60 | var local_direct_pix = new QPixmap(Icons.local_direct); 61 | 62 | this.local_public.setPixmap(local_public_pix.scaled( 63 | IconsSize, IconsSize, 64 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 65 | ) 66 | ); 67 | this.local_home.setPixmap(local_home_pix.scaled( 68 | IconsSize, IconsSize, 69 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 70 | ) 71 | ); 72 | this.local_lock.setPixmap(local_lock_pix.scaled( 73 | IconsSize, IconsSize, 74 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 75 | ) 76 | ); 77 | this.local_direct.setPixmap(local_direct_pix.scaled( 78 | IconsSize, IconsSize, 79 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 80 | ) 81 | ); 82 | 83 | this.home.setPixmap(home_pix.scaled( 84 | IconsSize, IconsSize, 85 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 86 | ) 87 | ); 88 | this.lock.setPixmap(lock_pix.scaled( 89 | IconsSize, IconsSize, 90 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 91 | ) 92 | ); 93 | this.direct.setPixmap(direct_pix.scaled( 94 | IconsSize, IconsSize, 95 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 96 | ) 97 | ); 98 | 99 | this.renote.setPixmap(renote_pix.scaled( 100 | IconsSize, IconsSize, 101 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 102 | ) 103 | ); 104 | this.reply.setPixmap(reply_pix.scaled( 105 | IconsSize, IconsSize, 106 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 107 | ) 108 | ); 109 | 110 | this.clip.setPixmap(clip_pix.scaled( 111 | IconsSize, IconsSize, 112 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 113 | ) 114 | ); 115 | } 116 | 117 | setNoteFlag(note){ 118 | // 3行時の処理 119 | if(App.settings.get("full_timeline_flag_view")){ 120 | // 上限をリセット 121 | this.setMaximumSize(46, IconsSize); 122 | this.setMinimumSize(46, IconsSize); 123 | 124 | if(note.renote) this.layout.addWidget(this.renote, 1); 125 | else if(note.reply) this.layout.addWidget(this.reply, 1); 126 | else this.layout.addStretch(1); 127 | 128 | if(note.files[0] || (note.renote && note.renote.files[0])) this.layout.addWidget(this.clip, 1); 129 | else this.layout.addStretch(1); 130 | }else{ 131 | // 2行時の処理 132 | var is_exist_file = (note.files[0] || (note.renote && note.renote.files[0])); 133 | 134 | if(is_exist_file){ 135 | this.layout.addWidget(this.clip, 1); 136 | }else{ 137 | if(note.renote) this.layout.addWidget(this.renote, 1); 138 | else if(note.reply) this.layout.addWidget(this.reply, 1); 139 | else this.layout.addStretch(1); 140 | } 141 | } 142 | 143 | switch(note.visibility){ 144 | case 'public': 145 | if(note.localOnly) this.layout.addWidget(this.local_public, 1); 146 | else this.layout.addStretch(1); 147 | break; 148 | case 'home': 149 | if(note.localOnly) this.layout.addWidget(this.local_home, 1) 150 | else this.layout.addWidget(this.home,1); 151 | break; 152 | case 'followers': 153 | if(note.localOnly) this.layout.addWidget(this.local_lock, 1) 154 | else this.layout.addWidget(this.lock,1); 155 | break; 156 | case 'specified': 157 | if(note.localOnly) this.layout.addWidget(this.local_direct, 1) 158 | else this.layout.addWidget(this.direct,1); 159 | break; 160 | default: 161 | break; 162 | } 163 | } 164 | } 165 | 166 | module.exports = FlagWidget; 167 | -------------------------------------------------------------------------------- /src/timelines/note_skin/sobacha.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLabel, 3 | QListWidgetItem, 4 | QSize, 5 | QWidget, 6 | QBoxLayout, 7 | Direction, 8 | QFont, 9 | AlignmentFlag, 10 | ContextMenuPolicy 11 | } = require('@nodegui/nodegui'); 12 | 13 | const App = require('../../index.js'); 14 | const IconLabel = require('../../widgets/icon_label/index.js'); 15 | 16 | class SobachaSkin extends QWidget{ 17 | constructor(note, a, exe){ 18 | super(); 19 | 20 | this.item_height = 14; 21 | this.widget = this; 22 | 23 | this.list_item = new QListWidgetItem(); 24 | this.font = new QFont(App.settings.get("font"), 9); 25 | 26 | this.layout = new QBoxLayout(Direction.TopToBottom); 27 | 28 | this.line_one = new QWidget(); 29 | this.line_two = new QWidget(); 30 | 31 | this.line_one_layout = new QBoxLayout(Direction.LeftToRight); 32 | this.line_two_layout = new QBoxLayout(Direction.LeftToRight); 33 | 34 | this.icon = new IconLabel(14); 35 | this.text = new QLabel(); 36 | this.sub_icon = new IconLabel(14); 37 | this.sub_text = new QLabel(); 38 | 39 | this.setLayout(this.layout); 40 | this.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu); 41 | this.addEventListener('customContextMenuRequested', exe); 42 | 43 | this.layout.setContentsMargins(0,0,0,1); 44 | this.layout.setSpacing(0); 45 | 46 | this.list_item.setSizeHint(new QSize(1200, 16)); 47 | 48 | this.line_one.setLayout(this.line_one_layout); 49 | 50 | this.line_two.setLayout(this.line_two_layout); 51 | 52 | this.line_one_layout.setContentsMargins(0,0,0,0); 53 | this.line_one_layout.setSpacing(6); 54 | 55 | this.line_two_layout.setContentsMargins(20,0,0,0); 56 | this.line_two_layout.setSpacing(6); 57 | 58 | this.text.setAlignment(AlignmentFlag.AlignLeft); 59 | this.text.setFont(this.font); 60 | 61 | this.sub_text.setAlignment(AlignmentFlag.AlignLeft); 62 | this.sub_text.setFont(this.font); 63 | 64 | this.line_one_layout.addWidget(this.icon); 65 | this.line_one_layout.addWidget(this.text, 1); 66 | 67 | this.line_two_layout.addWidget(this.sub_icon); 68 | this.line_two_layout.addWidget(this.sub_text, 1); 69 | 70 | this.layout.addWidget(this.line_one); 71 | this.layout.addWidget(this.line_two); 72 | 73 | this.setNote(note); 74 | } 75 | 76 | setNote(note){ 77 | var note_color = '#000'; 78 | 79 | var text = ''; 80 | 81 | if(note.renote){ 82 | var sub_text = ''; 83 | 84 | this.layout.addWidget(this.line_two); 85 | this.list_item.setSizeHint(new QSize(1200, 30)); 86 | 87 | note_color = '#167018'; 88 | sub_text = this._parse_renote(note); 89 | text = this._parse_note_text(note.renote); 90 | 91 | this.sub_icon.setPixmap(note.user.avater); 92 | this.sub_text.setText(sub_text); 93 | 94 | this.icon.setPixmap(note.renote.user.avater); 95 | }else{ 96 | this.line_two.hide(); 97 | if(note.reply) note_color = '#0b0ba9'; 98 | text = this._parse_note_text(note); 99 | 100 | this.icon.setPixmap(note.user.avater); 101 | } 102 | 103 | this.text.setText(text); 104 | 105 | this.setInlineStyle("border-bottom: 1px solid #d6d6d6;"); 106 | this.text.setInlineStyle(`color: ${note_color};`); 107 | } 108 | 109 | _parse_renote(note){ 110 | var r_text; 111 | if(note.text || note.cw){ 112 | r_text = `QN by @${note.user.acct} `; 113 | }else{ 114 | r_text = `RN by @${note.user.acct} `; 115 | } 116 | 117 | return r_text; 118 | } 119 | 120 | _parse_note_text(note){ 121 | var result; 122 | if(note.cw){ 123 | result = "CW: " + note.cw.replace(/(\r\n|\n|\r)/gm," "); 124 | }else{ 125 | if(!note.text) note.text = ''; 126 | result = note.text.replace(/(\r\n|\n|\r)/gm," "); 127 | } 128 | 129 | return result; 130 | } 131 | 132 | destroy(){ 133 | this.line_one_layout.removeWidget(this.text); 134 | this.line_one_layout.removeWidget(this.icon); 135 | 136 | this.line_two_layout.removeWidget(this.sub_icon); 137 | this.line_two_layout.removeWidget(this.sub_text); 138 | 139 | this.layout.removeWidget(this.line_one); 140 | this.layout.removeWidget(this.line_two); 141 | 142 | this.text.close(); 143 | this.icon.close(); 144 | 145 | this.sub_icon.close(); 146 | this.sub_text.close(); 147 | 148 | this.line_one.close(); 149 | this.line_two.close(); 150 | 151 | this.item_height = undefined; 152 | this.widget = undefined; 153 | 154 | this.list_item = undefined; 155 | this.font = undefined; 156 | 157 | this.layout = undefined; 158 | 159 | this.line_one = undefined; 160 | this.line_one = undefined; 161 | 162 | this.line_one_layout = undefined; 163 | this.line_two_layout = undefined; 164 | 165 | this.icon = undefined; 166 | this.text = undefined; 167 | this.sub_icon = undefined; 168 | this.sub_text = undefined; 169 | 170 | this.close(); 171 | } 172 | } 173 | 174 | module.exports = SobachaSkin; 175 | -------------------------------------------------------------------------------- /src/timelines/notification_item.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLabel, 3 | QListWidgetItem, 4 | QSize, 5 | QWidget, 6 | QBoxLayout, 7 | Direction, 8 | QFont 9 | } = require('@nodegui/nodegui'); 10 | 11 | const NotificationParser = require('../tools/notification_parser/index.js'); 12 | const FlagLabel = require('./flag_label.js'); 13 | const IconLabel = require('../widgets/icon_label/index.js'); 14 | const App = require('../index.js'); 15 | 16 | class NotificationItem extends QWidget{ 17 | constructor(notification, a){ 18 | super(); 19 | 20 | this.item_height = 14; 21 | this.widget = this; 22 | 23 | this.list_item = new QListWidgetItem(); 24 | this.font = new QFont(App.settings.get('font'), 9); 25 | 26 | this.layout = new QBoxLayout(Direction.LeftToRight); 27 | 28 | this.flag = new FlagLabel(); 29 | this.icon = new IconLabel(this.item_height); 30 | this.name = new QLabel(); 31 | this.desc = new QLabel(); 32 | 33 | this.setLayout(this.layout); 34 | 35 | this.layout.setContentsMargins(2,0,0,1); 36 | this.layout.setSpacing(6); 37 | 38 | this.list_item.setSizeHint(new QSize(1200, 15)); 39 | 40 | this.name.setFixedSize(120, this.item_height); 41 | 42 | this.name.setFont(this.font); 43 | this.desc.setFont(this.font); 44 | 45 | this.layout.addWidget(this.flag); 46 | this.layout.addWidget(this.icon); 47 | this.layout.addWidget(this.name); 48 | this.layout.addWidget(this.desc, 1); 49 | 50 | this.setNotification(notification); 51 | } 52 | 53 | setNotification(notification){ 54 | var text = NotificationParser.gen_desc_text(notification, 'notification_item'); 55 | 56 | this.flag.setFlag(notification.type); 57 | this.icon.setPixmap(notification.user.avater); 58 | this.name.setText(notification.user.acct); 59 | this.desc.setText(text); 60 | } 61 | 62 | destroy(){ 63 | this.layout.removeWidget(this.flag); 64 | this.layout.removeWidget(this.name); 65 | this.layout.removeWidget(this.icon); 66 | this.layout.removeWidget(this.desc); 67 | 68 | this.flag.close(); 69 | this.name.close(); 70 | this.icon.close(); 71 | this.desc.close(); 72 | 73 | this.close(); 74 | 75 | this.list_item = undefined; 76 | this.widget = undefined; 77 | this.font = undefined; 78 | this.item_height = undefined; 79 | this.layout = undefined; 80 | this.flag = undefined; 81 | this.icon = undefined; 82 | this.name = undefined; 83 | this.desc = undefined; 84 | } 85 | } 86 | 87 | module.exports = NotificationItem; 88 | -------------------------------------------------------------------------------- /src/timelines/skin.js: -------------------------------------------------------------------------------- 1 | class Skin{ 2 | constructor(){ 3 | this.skins = {}; 4 | 5 | this.skins.sobacha = require('./note_skin/sobacha.js'); 6 | this.skins.default_skin = require('./note_skin/default.js'); 7 | } 8 | 9 | get(name){ 10 | return this.skins[name]; 11 | } 12 | } 13 | 14 | module.exports = Skin; 15 | -------------------------------------------------------------------------------- /src/timelines/tab_loader.js: -------------------------------------------------------------------------------- 1 | const file = require('../file.js'); 2 | const App = require('../index.js'); 3 | 4 | class TabLoader{ 5 | constructor(){ 6 | if(!file.exist_check(`${App.data_directory.get('settings')}tabs.json`)) this.create_default_tab(); 7 | 8 | try{ 9 | var f = file.load(`${App.data_directory.get('settings')}tabs.json`); 10 | f = JSON.parse(f); 11 | }catch{ 12 | throw 'LoadErr'; 13 | } 14 | 15 | return f; 16 | } 17 | 18 | // TODO: limit 19 | create_default_tab(){ 20 | var default_tabs = { 21 | tabs: [ 22 | { 23 | "id": "home", 24 | "name": "ホーム", 25 | "source": { 26 | "from": ["home"] 27 | } 28 | }, 29 | { 30 | "id": "local", 31 | "name": "ローカル", 32 | "source": { 33 | "from": ["local"] 34 | } 35 | }, 36 | { 37 | "id": "social", 38 | "name": "ソーシャル", 39 | "source": { 40 | "from": ["social"] 41 | } 42 | }, 43 | { 44 | "id": "global", 45 | "name": "グローバル", 46 | "source": { 47 | "from": ["global"] 48 | } 49 | }, 50 | { 51 | "id": "notification", 52 | "name": "通知", 53 | "source": { 54 | "from": ["notification"] 55 | } 56 | } 57 | ] 58 | } 59 | 60 | file.json_write_sync(`${App.data_directory.get('settings')}tabs.json`, default_tabs); 61 | } 62 | } 63 | 64 | module.exports = TabLoader; 65 | -------------------------------------------------------------------------------- /src/timelines/timeline.js: -------------------------------------------------------------------------------- 1 | const NotificationItem = require('./notification_item.js'); 2 | const Assets = require("../assets.js"); 3 | const Skin = require("./skin.js"); 4 | const App = require("../index.js"); 5 | // const sleep = time => new Promise(resolve => setTimeout(resolve, time)); 6 | 7 | const { 8 | QListWidget 9 | } = require('@nodegui/nodegui'); 10 | 11 | class Timeline extends QListWidget{ 12 | constructor(id,limit, skin_name, exe){ 13 | super(); 14 | 15 | this.assets = new Assets("TimelineWidget"); 16 | this.skin = new Skin(); 17 | 18 | this.post_view; 19 | 20 | this.id = id; 21 | this.skin_name = skin_name; 22 | this.tl = []; 23 | this.item_queue = []; 24 | this.isAdd = false; 25 | this.limit = limit; 26 | this.exe = exe; 27 | } 28 | 29 | addNote(note){ 30 | var skin = this.skin.get(this.skin_name); 31 | var item = new skin(note, App.settings.get("font"), this.exe); 32 | var data = { 33 | item: item, 34 | id: note.id, 35 | sort_base: note.createdAt 36 | }; 37 | 38 | this.item_queue.push(data); 39 | this._add_item(); 40 | } 41 | 42 | addNotification(notification){ 43 | var item = new NotificationItem(notification, App.settings.get("font")); 44 | var data = { 45 | item: item, 46 | id: notification.id, 47 | sort_base: notification.createdAt 48 | }; 49 | 50 | this.item_queue.push(data); 51 | this._add_item(); 52 | } 53 | 54 | async _add_item(){ 55 | if(this.isAdd) return; 56 | if(this.item_queue.length == 0) return; 57 | 58 | this.isAdd = true; 59 | 60 | var data = this.item_queue.shift(); 61 | 62 | // 挿入位置の検索 63 | var pos = 1; 64 | 65 | for(var note of this.tl){ 66 | if(note.sort_base > data.sort_base) break; 67 | pos++; 68 | } 69 | 70 | this.tl.splice(pos -1, 0, data); 71 | 72 | this.insertItem((this.tl.length - pos), data.item.list_item); 73 | this.setItemWidget(data.item.list_item, data.item.widget); 74 | 75 | // nodegui v0.20.0では動作しない 76 | //await sleep(10); 77 | 78 | this.isAdd = false; 79 | this.fix_items(); 80 | this._add_item(); 81 | } 82 | 83 | check_exist_item(id){ 84 | var exist = this.tl.some((el) => { 85 | return (el.id == id); 86 | }); 87 | 88 | return exist; 89 | } 90 | 91 | fix_items(){ 92 | var limit = this.limit; 93 | while(this.tl.length > limit){ 94 | for(var i = 0; i < 10; i++){ 95 | if(this.tl[i].item.list_item.isSelected()) return; 96 | } 97 | this.remove_item(this.tl[0]); 98 | this.tl.shift(); 99 | } 100 | } 101 | 102 | remove_item(item){ 103 | this.takeItem(this.row(item.item.list_item)); 104 | item.item.destroy(); 105 | App.note_cache.free(item.id, this.id); 106 | 107 | return; 108 | } 109 | 110 | get_selected_item(){ 111 | var items = this.selectedItems(); 112 | var index = this.row(items[0]); 113 | index+=1 114 | return this.tl[this.tl.length - index]; 115 | } 116 | 117 | select_top_item(){ 118 | this.setCurrentRow(0); 119 | } 120 | 121 | count(){ 122 | return this.tl.length; 123 | } 124 | } 125 | 126 | module.exports = Timeline; 127 | -------------------------------------------------------------------------------- /src/tools/data_directory/index.js: -------------------------------------------------------------------------------- 1 | const file = require('../../file.js'); 2 | const Assets = require('../../assets.js'); 3 | const Files = new Assets('FileList').files; 4 | 5 | class DataDirectory{ 6 | constructor(root = './data'){ 7 | this.root = root; 8 | this.files = {}; 9 | if(!file.exist_check(this.root)) file.mkdir(this.root); 10 | 11 | for(var f of Files){ 12 | switch(f.type){ 13 | case "dir": 14 | if(!file.exist_check(`${this.root}/${f.path}`)) file.mkdir(`${this.root}/${f.path}`); 15 | this.files[f.id] = { path: `${this.root}/${f.path}/`, type: f.type }; 16 | break; 17 | } 18 | } 19 | } 20 | 21 | get(id){ 22 | var result = this.files[id]; 23 | if(!result) throw "error"; 24 | 25 | return result.path; 26 | } 27 | } 28 | 29 | module.exports = DataDirectory; 30 | -------------------------------------------------------------------------------- /src/tools/desktop_notification/index.js: -------------------------------------------------------------------------------- 1 | const notifier = require('node-notifier'); 2 | const path = require('path'); 3 | const winToast = new notifier.WindowsToaster({ 4 | withFallback: false, 5 | customPath: path.resolve('./dist/snoreToast/snoretoast-x86.exe') 6 | }); 7 | const App = require('../../index.js'); 8 | 9 | class DesktopNotification{ 10 | constructor(){ 11 | } 12 | 13 | show(title, message){ 14 | if(!title) return; 15 | if(!App.settings.get("use_desktop_notification")) return; 16 | // 音とか追加するためにクラス作っとく 17 | if(process.platform == 'win32') { 18 | winToast.notify({ 19 | appId: 'com.tencha', 20 | message: message, 21 | title: title, 22 | sound: false, 23 | wait: true 24 | },function(err, response) { 25 | console.log(err, response) 26 | }); 27 | } else { 28 | notifier.notify({ 29 | title: title, 30 | message: message 31 | }); 32 | } 33 | } 34 | } 35 | 36 | module.exports = DesktopNotification; 37 | -------------------------------------------------------------------------------- /src/tools/emoji_parser/cache.js: -------------------------------------------------------------------------------- 1 | const file = require('../../file.js'); 2 | const Assets = require('../../assets.js'); 3 | const App = require('../../index.js'); 4 | 5 | const request = require('request-promise'); 6 | const uuid = require('uuid'); 7 | const sleep = time => new Promise(resolve => setTimeout(resolve, time)); 8 | 9 | class EmojiCache{ 10 | constructor(){ 11 | this.assets = new Assets('UserAgent'); 12 | // 必須のファイル郡の作成 13 | try{ 14 | if(!file.exist_check(`${App.data_directory.get('tmp')}tmplist.json`)) file.json_write_sync(`${App.data_directory.get('tmp')}tmplist.json`, {tmp: []}); 15 | this.tmplist = JSON.parse(file.load(`${App.data_directory.get('tmp')}tmplist.json`)); 16 | this.worked = []; 17 | this.lock = false; 18 | console.log(this.tmplist.tmp.length + ' Emojis loaded!'); 19 | }catch(err){ 20 | throw err; 21 | } 22 | } 23 | 24 | // tmplist.json 25 | // - String match_str 26 | // - String type 27 | // - String filename 28 | // - String url 29 | async get(req){ 30 | var data = { 31 | match_str: "", 32 | type: "", 33 | url: "" 34 | } 35 | 36 | // name がある場合はMisskey Emoji 37 | if(req.name){ 38 | data.match_str = req.name; 39 | data.type = 'MisskeyCustomEmoji'; 40 | data.url = req.url; 41 | // そうでなければTwemoji 42 | }else{ 43 | data.match_str = req.text; 44 | data.type = 'Twemoji'; 45 | // svgではなくpngで 46 | data.url = req.url.replace('/v/latest/svg/', '/2/72x72/').replace('svg', 'png'); 47 | } 48 | 49 | // 自分のキャッシュから参照してあったらそれを返す 50 | var tmp_emoji = this.tmplist.tmp.find((v) => v.url == data.url); 51 | if(tmp_emoji) return tmp_emoji; 52 | 53 | // もし自身のキャッシュになければ 54 | if(!data.url) throw 'url undefined'; 55 | 56 | try{ 57 | for(;;){ 58 | var result = await this._cache_emoji(data); 59 | if(!result) await sleep(50); 60 | 61 | var tmp_emoji = this.tmplist.tmp.find((v) => v.url == data.url); 62 | if(tmp_emoji) return tmp_emoji; 63 | 64 | console.log('emoji loop: ', data.match_str); 65 | } 66 | }catch(err){ 67 | throw err; 68 | } 69 | } 70 | 71 | async _cache_emoji(data){ 72 | var isWork = this.worked.some((v) => v.url == data.url); 73 | if(isWork) return false; 74 | 75 | this.lock = true; 76 | this.worked.push(data); 77 | 78 | data.filename = ''; 79 | 80 | var headers = {}; 81 | if(App.settings.get('fake_useragent')) headers['User-Agent'] = this.assets.fake; 82 | 83 | var opt = { 84 | url: data.url, 85 | encoding: null, 86 | method: 'GET', 87 | headers: headers, 88 | resolveWithFullResponse: true 89 | }; 90 | 91 | try{ 92 | var res = await request(opt); 93 | 94 | data.filename = `${App.data_directory.get('tmp')}${uuid.v4().split('-').join('')}`; 95 | file.bin_write_sync(data.filename, res.body); 96 | 97 | this.tmplist.tmp.push(data); 98 | 99 | file.json_write_sync(`${App.data_directory.get('tmp')}tmplist.json`, this.tmplist); 100 | 101 | for(var i = 0; i < this.worked.length; i++){ 102 | if(this.worked[i].url == data.url){ 103 | this.worked.splice(i, 1); 104 | break; 105 | } 106 | } 107 | return true; 108 | }catch(err){ 109 | console.log(err); 110 | for(var i = 0; i < this.worked.length; i++){ 111 | if(this.worked[i].url == data.url){ 112 | this.worked.splice(i, 1); 113 | break; 114 | } 115 | } 116 | throw err; 117 | } 118 | } 119 | } 120 | 121 | module.exports = EmojiCache; 122 | -------------------------------------------------------------------------------- /src/tools/emoji_parser/index.js: -------------------------------------------------------------------------------- 1 | const { parse } = require('twemoji-parser'); 2 | const { QPixmap } = require('@nodegui/nodegui'); 3 | 4 | const Cache = require('./cache.js'); 5 | const App = require('../../index.js') 6 | 7 | class EmojiParser{ 8 | constructor(){ 9 | this.cache = new Cache(); 10 | } 11 | 12 | async parse(text, mk_emojis){ 13 | if(!App.settings.get("use_emojis")) return text; 14 | 15 | var emojis = []; 16 | 17 | var twemojis = parse(text); 18 | 19 | for(var emoji of twemojis) emojis.push(this._get_emoji_regexp(emoji, 'Twemoji')); 20 | for(var emoji of mk_emojis) emojis.push(this._get_emoji_regexp(emoji, 'MisskeyCustomEmoji')); 21 | 22 | var _emojis = await Promise.all(emojis); 23 | 24 | for(var emoji of _emojis){ 25 | if(!emoji.regexp) continue; 26 | 27 | text = text.replace(emoji.regexp, emoji.img); 28 | } 29 | 30 | return text; 31 | } 32 | 33 | // 廃止したい 34 | async parse_note(note){ 35 | if(!App.settings.get("use_emojis")) return note; 36 | 37 | var e_user = this.parse_user(note.user); 38 | if(note.text) var e_text = this.parse(note.text, note.emojis); 39 | if(note.cw) var e_cw = this.parse(note.cw, note.emojis); 40 | 41 | if(note.text) note.text = await e_text; 42 | if(note.cw) note.cw = await e_cw; 43 | 44 | await e_user; 45 | } 46 | 47 | async parse_user(user){ 48 | if(!App.settings.get("use_emojis")) return user; 49 | 50 | if(user.name) user.name = await this.parse(user.name, user.emojis); 51 | } 52 | 53 | async _get_emoji_regexp(emoji_data, type){ 54 | var result = { 55 | regexp: null, 56 | img: null 57 | } 58 | 59 | try{ 60 | var emoji = await this.cache.get(emoji_data); 61 | var pix = new QPixmap(emoji.filename); 62 | 63 | var width = pix.width(); 64 | var height = pix.height(); 65 | var ratio; 66 | 67 | // 高さのみ制限 68 | ratio = height / 14; 69 | 70 | width = width / ratio; 71 | height = 14; 72 | 73 | width = Math.ceil(width); 74 | height = Math.ceil(height); 75 | 76 | result.img = ``; 77 | 78 | if(type == 'Twemoji'){ 79 | result.regexp = new RegExp(this.escape_regexp(emoji.match_str), 'g'); 80 | }else if(type == 'MisskeyCustomEmoji'){ 81 | result.regexp = new RegExp(this.escape_regexp(`:${emoji.match_str}:`), 'g'); 82 | } 83 | }catch(err){ 84 | console.log(err); 85 | } 86 | 87 | return result; 88 | } 89 | 90 | escape_regexp(str){ 91 | return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); 92 | } 93 | } 94 | 95 | module.exports = EmojiParser; 96 | -------------------------------------------------------------------------------- /src/tools/image_viewer/index.js: -------------------------------------------------------------------------------- 1 | const file = require('../../file.js'); 2 | const Assets = require('../../assets.js'); 3 | const App = require('../../index.js'); 4 | 5 | const mime = require('mime-types'); 6 | const request = require('request-promise'); 7 | const child_process = require('child_process'); 8 | 9 | class ImageViewer{ 10 | constructor(){ 11 | this.assets = new Assets('UserAgent'); 12 | } 13 | 14 | init(){ 15 | if(!file.exist_check(`${App.data_directory.get('settings')}content_settings.json`)){ 16 | this.create_default_file(); 17 | } 18 | 19 | try{ 20 | var f = file.load(`${App.data_directory.get('settings')}content_settings.json`); 21 | f = JSON.parse(f); 22 | this.file_type_match = f.file_type_match; 23 | }catch(err){ 24 | console.log(err); 25 | this.file_type_match = []; 26 | } 27 | } 28 | 29 | create_default_file(){ 30 | // TODO: OS別の初期設定 31 | var default_file = { 32 | file_type_match: [ 33 | { 34 | type: "text", 35 | exec: "" 36 | }, 37 | { 38 | type: "image", 39 | exec: "" 40 | }, 41 | { 42 | type: "audio", 43 | exec: "" 44 | }, 45 | { 46 | type: "video", 47 | exec: "" 48 | } 49 | ] 50 | } 51 | 52 | file.json_write_sync(`${App.data_directory.get('settings')}content_settings.json`, default_file); 53 | } 54 | 55 | async show(note){ 56 | var filename_base = `cha_${note.user.username}_${note.id}_image`; 57 | var files = []; 58 | 59 | var i = 0; 60 | for(var f of note.files){ 61 | for(var type of this.file_type_match){ 62 | if(f.type.match(new RegExp(type.type, 'gi')) && type.exec){ 63 | var ext = mime.extension(f.type); 64 | var _f = { 65 | filename: `${filename_base}${i}.${ext}`, 66 | url: f.url, 67 | exec: type.exec 68 | }; 69 | files.push(_f); 70 | i++; 71 | } 72 | } 73 | } 74 | 75 | if(!files.length) return; 76 | 77 | var q = []; 78 | for(var f of files) q.push(this._download(f)); 79 | 80 | await Promise.all(q); 81 | 82 | child_process.exec(`${files[0].exec} ${App.data_directory.get('user_contents')}${files[0].filename}`); 83 | } 84 | 85 | async _download(f){ 86 | var headers = {}; 87 | if(App.settings.get('fake_useragent')) headers['User-Agent'] = this.assets.fake; 88 | 89 | var opt = { 90 | url: f.url, 91 | encoding: null, 92 | method: 'GET', 93 | headers: headers 94 | }; 95 | 96 | try{ 97 | var res = await request(opt); 98 | await file.bin_write(`${App.data_directory.get('user_contents')}${f.filename}`, res); 99 | }catch(err){ 100 | console.log(err); 101 | } 102 | 103 | return; 104 | } 105 | } 106 | 107 | module.exports = ImageViewer; 108 | -------------------------------------------------------------------------------- /src/tools/note_cache/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('../../models/note.js'); 2 | const App = require('../../index.js'); 3 | 4 | class NoteCache{ 5 | constructor(){ 6 | this.notes = {}; 7 | this.isRemoveRun = false; 8 | } 9 | 10 | async get(note){ 11 | // noteが自分のキャッシュにあるなら整形済みのそれを返す 12 | if(this.notes[note.id]){ 13 | // TODO: Update 14 | this.notes[note.id].update(note); 15 | }else{ 16 | // なければ作る 17 | try{ 18 | this.notes[note.id] = await new Note(note); 19 | }catch(err){ 20 | throw err; 21 | } 22 | 23 | this.remove_cache(); 24 | } 25 | 26 | console.log(Object.keys(this.notes).length); 27 | return this.notes[note.id]; 28 | } 29 | 30 | find_id(id){ 31 | var result = null; 32 | 33 | if(this.notes[id]) result = this.notes[id]; 34 | 35 | return result; 36 | } 37 | 38 | use(id, tl){ 39 | if(this.notes[id]) { 40 | this.notes[id].use_tl[tl] = true; 41 | }else{ 42 | console.log("ない") 43 | } 44 | } 45 | 46 | free(id, tl){ 47 | if(this.notes[id]) delete this.notes[id].use_tl[tl]; 48 | } 49 | 50 | count(){ 51 | return Object.keys(this.notes).length; 52 | } 53 | 54 | remove_cache(){ 55 | if(this.isRemoveRun) return; 56 | 57 | var limit = App.settings.get("post_cache_limit"); 58 | if(Object.keys(this.notes).length < limit) return; 59 | 60 | this.isRemoveRun = true; 61 | 62 | var clear_count = App.settings.get("post_cache_clear_count"); 63 | 64 | for(var i = 0; i < clear_count; i++){ 65 | var noteid = Object.keys(this.notes)[i]; 66 | if(!Object.keys(this.notes[noteid].use_tl).length < 1) continue; 67 | 68 | this._remove_note(noteid); 69 | } 70 | 71 | console.log('clear'); 72 | 73 | this.isRemoveRun = false; 74 | } 75 | 76 | _remove_note(noteid){ 77 | this.notes[noteid].purge(); 78 | console.log('removeing: ' + this.notes[noteid].id); 79 | delete this.notes[noteid]; 80 | } 81 | } 82 | 83 | module.exports = NoteCache; 84 | -------------------------------------------------------------------------------- /src/tools/notification_cache/index.js: -------------------------------------------------------------------------------- 1 | const Notification = require('../../models/notification.js'); 2 | 3 | class NotificationCache{ 4 | constructor(){ 5 | this.notifications = {}; 6 | } 7 | 8 | find_id(id){ 9 | var result = null; 10 | 11 | if(this.notifications[id]) result = this.notifications[id]; 12 | 13 | return result; 14 | } 15 | 16 | async get(notification){ 17 | if(!notification) return null; 18 | if(notification.type == 'readAllUnreadMentions') return null; 19 | if(notification.type == 'readAllUnreadSpecifiedNotes') return null; 20 | 21 | // notificationが自分のキャッシュにあるなら整形済みのそれを返す 22 | if(this.notifications[notification.id]){ 23 | //this.notifications[notification.id].update(notification); 24 | }else{ 25 | // なければ作る 26 | try{ 27 | this.notifications[notification.id] = await new Notification(notification); 28 | }catch(err){ 29 | throw err; 30 | } 31 | } 32 | 33 | return this.notifications[notification.id]; 34 | } 35 | } 36 | 37 | module.exports = NotificationCache; 38 | -------------------------------------------------------------------------------- /src/tools/notification_parser/index.js: -------------------------------------------------------------------------------- 1 | class NotificationParser{ 2 | constructor(){} 3 | 4 | static gen_desc_text(notification, type){ 5 | var text; 6 | switch(type){ 7 | case 'postview': 8 | text = this._gen_postview_desc_text(notification); 9 | break; 10 | case 'notification_item': 11 | text = this._gen_notification_item_desc_text(notification); 12 | break; 13 | case 'desktop_notification': 14 | text = this._gen_desktop_notification_desc_text(notification); 15 | break 16 | } 17 | 18 | return text; 19 | } 20 | 21 | static _gen_postview_desc_text(notification){ 22 | var desc_text; 23 | 24 | var text = this._parse_note_text(notification); 25 | 26 | switch(notification.type){ 27 | case 'follow': 28 | desc_text = `フォローされています!`; 29 | break; 30 | case 'mention': 31 | case 'reply': 32 | desc_text = `言及:\n${text}`; 33 | break; 34 | case 'renote': 35 | var text = ''; 36 | if(notification.note.renote.cw){ 37 | text = text + notification.note.renote.cw; 38 | text = text + '\n------------CW------------\n'; 39 | text = text + notification.note.renote.text; 40 | }else{ 41 | text = text + notification.note.renote.text; 42 | } 43 | desc_text = `Renoteされました!:\n${text}`; 44 | break; 45 | case 'quote': 46 | desc_text = `引用Renoteされました!:\n${text}`; 47 | break; 48 | case 'reaction': 49 | desc_text = `${notification.display_reaction}でリアクションされました!:\n${text}`; 50 | break; 51 | case 'pollVote': 52 | desc_text = `投票しました!:\n${text}`; 53 | break; 54 | case 'receiveFollowRequest': 55 | desc_text = `フォローリクエストされています!`; 56 | break; 57 | case 'followRequestAccepted': 58 | desc_text = `フォローリクエストが許可されました!`; 59 | break; 60 | } 61 | 62 | return desc_text; 63 | } 64 | 65 | static _gen_notification_item_desc_text(notification){ 66 | var result = ''; 67 | 68 | var text = ''; 69 | 70 | if(notification.note && notification.note.cw){ 71 | text = notification.note.cw.replace(/(\r\n|\n|\r)/gm," "); 72 | }else if(notification.note && notification.note.text){ 73 | text = notification.note.text.replace(/(\r\n|\n|\r)/gm," "); 74 | } 75 | 76 | switch(notification.type){ 77 | case 'follow': 78 | result = `フォローされています!`; 79 | break; 80 | case 'mention': 81 | case 'reply': 82 | result = `言及: ${text}`; 83 | break; 84 | case 'renote': 85 | var text = ''; 86 | if(notification.note.renote.cw){ 87 | text = notification.note.renote.cw; 88 | }else{ 89 | text = notification.note.renote.text; 90 | } 91 | result = `Renoteされました!: ${text}`; 92 | break; 93 | case 'quote': 94 | result = `引用Renoteされました!: ${text}`; 95 | break; 96 | case 'reaction': 97 | result = `${notification.display_reaction}でリアクションされました!: ${text}`; 98 | break; 99 | case 'pollVote': 100 | result = `投票しました!: ${text}`; 101 | break; 102 | case 'receiveFollowRequest': 103 | result = `フォローリクエストされています!`; 104 | break; 105 | case 'followRequestAccepted': 106 | result = `フォローリクエストが許可されました!`; 107 | break; 108 | } 109 | 110 | return result; 111 | } 112 | 113 | static _gen_desktop_notification_desc_text(notification){ 114 | var title, message; 115 | 116 | var text; 117 | if(notification.note && notification.note.no_emoji_cw){ 118 | text = notification.note.no_emoji_cw.replace(/(\r\n|\n|\r)/gm," "); 119 | }else if(notification.note && notification.note.no_emoji_text){ 120 | text = notification.note.no_emoji_text.replace(/(\r\n|\n|\r)/gm," "); 121 | } 122 | 123 | var description; 124 | if(notification.user && notification.user.description){ 125 | description = notification.user.description.replace(/(\r\n|\n|\r)/gm," "); 126 | }else{ 127 | description = '(BIOがありません)'; 128 | } 129 | 130 | switch(notification.type){ 131 | case 'follow': 132 | title = `${notification.user.acct}にフォローされています!`; 133 | message = `${description}`; 134 | break; 135 | case 'mention': 136 | case 'reply': 137 | title = `${notification.user.acct}からの言及`; 138 | message = `${text}`; 139 | break; 140 | case 'renote': 141 | var text = ''; 142 | if(notification.note.renote.cw){ 143 | text = notification.note.renote.cw; 144 | }else{ 145 | text = notification.note.renote.text; 146 | } 147 | title = `${notification.user.acct}にRenoteされました!`; 148 | message = `${text}`; 149 | break; 150 | case 'quote': 151 | title = `${notification.user.acct}に引用Renoteされました!`; 152 | message = `${text}`; 153 | break; 154 | case 'reaction': 155 | title = `${notification.user.acct}に${notification.reaction}でリアクションされました!`; 156 | message = `${text}`; 157 | break; 158 | case 'pollVote': 159 | title = `${notification.user.acct}に投票されました!`; 160 | message = `${text}`; 161 | break; 162 | case 'receiveFollowRequest': 163 | title = `${notification.user.acct}にフォローリクエストされました!`; 164 | message = `${description}`; 165 | break; 166 | case 'followRequestAccepted': 167 | title = `${notification.user.acct}へのフォローリクエストが許可されました!`; 168 | message = `${description}`; 169 | break; 170 | } 171 | 172 | return { title: title, message: message }; 173 | } 174 | 175 | static _parse_note_text(notification){ 176 | var text = ''; 177 | if(notification.note && notification.note.renote){ 178 | var r_text = "RN @" + notification.note.renote.user.acct + ' '; 179 | if(notification.note.cw){ 180 | text = notification.note.cw + '\n------------CW------------\n'; 181 | text = text + notification.note.text; 182 | }else if(notification.note.text){ 183 | text = notification.note.text + ' '; 184 | } 185 | 186 | if(notification.note.renote.cw){ 187 | r_text = r_text + notification.note.renote.cw; 188 | r_text = r_text + '\n------------CW------------\n'; 189 | r_text = r_text + notification.note.renote.text; 190 | }else{ 191 | r_text = r_text + notification.note.renote.text; 192 | } 193 | 194 | text = text + r_text; 195 | }else if(notification.note){ 196 | if(notification.note.cw){ 197 | text = notification.note.cw + '\n------------CW------------\n'; 198 | text = text + notification.note.text; 199 | }else{ 200 | text = notification.note.text; 201 | } 202 | } 203 | 204 | return text; 205 | } 206 | } 207 | 208 | module.exports = NotificationParser; 209 | -------------------------------------------------------------------------------- /src/tools/post_parser/index.js: -------------------------------------------------------------------------------- 1 | class PostParser{ 2 | constructor(){ 3 | } 4 | parse(text){ 5 | var break_regexp = new RegExp('\n', 'g'); 6 | var url_regexp = /(?$1'); 13 | 14 | _r += _t + "\n"; 15 | } 16 | text = _r; 17 | } 18 | 19 | if(search_regexp.test(text)){ 20 | var _r = ''; 21 | for(var _t of text.split('\n')){ 22 | _t = _t.replace(search_regexp, '$2'); 23 | _t = _t.replace("search://", 'https://'); 24 | _t = _t.replace("<%3A>", ":"); 25 | 26 | _r += _t + "\n"; 27 | } 28 | text = _r; 29 | } 30 | 31 | text = text.replace(/\n+$/,"").replace(break_regexp, '
'); 32 | 33 | text = '' + text + ''; 34 | 35 | return text; 36 | } 37 | static escape_html(text){ 38 | // text = text.replace(/&/g, '&'); 39 | text = text.replace(/>/g, '>'); 40 | text = text.replace(/ v.path == path); 36 | 37 | if(!is_diff) return result; 38 | 39 | switch(this.version_info.diffs.type){ 40 | case "body": 41 | data[this.version_info.diffs.target] = this._parse_body(data[this.version_info.diffs.target]); 42 | break; 43 | } 44 | 45 | return result; 46 | } 47 | 48 | _parse_body(data){ 49 | for(var diff of this.version_info.diffs.replace){ 50 | data = data.replace(new RegExp(this.escape_regexp(diff.from)), diff.to); 51 | } 52 | 53 | return data; 54 | } 55 | 56 | escape_regexp(str){ 57 | return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); 58 | } 59 | } 60 | 61 | module.exports = VersionParser; 62 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/button_area.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | QPushButton, 6 | QCheckBox 7 | } = require('@nodegui/nodegui'); 8 | 9 | class ButtonArea extends QWidget{ 10 | constructor(){ 11 | super(); 12 | 13 | this.layout = new QBoxLayout(Direction.LeftToRight); 14 | 15 | this.post_button = new QPushButton(); 16 | this.clear_button = new QPushButton(); 17 | this.close_button = new QPushButton(); 18 | this.is_after_close_check = new QCheckBox(); 19 | 20 | this.setObjectName('buttonsArea'); 21 | this.setLayout(this.layout); 22 | 23 | this.layout.setContentsMargins(0,0,0,0); 24 | this.layout.setSpacing(5); 25 | 26 | this.post_button.setObjectName('postButton'); 27 | this.post_button.setText('投稿'); 28 | 29 | this.clear_button.setObjectName('clearButton'); 30 | this.clear_button.setText('クリア'); 31 | 32 | this.close_button.setObjectName('closeButton'); 33 | this.close_button.setText('閉じる'); 34 | 35 | this.is_after_close_check.setObjectName('isAfterCloseCheck'); 36 | this.is_after_close_check.setText('投稿後に閉じない'); 37 | 38 | this.layout.addWidget(this.post_button); 39 | this.layout.addWidget(this.clear_button); 40 | this.layout.addWidget(this.close_button); 41 | this.layout.addWidget(this.is_after_close_check); 42 | } 43 | 44 | setFont(font){ 45 | this.post_button.setFont(font); 46 | this.clear_button.setFont(font); 47 | this.close_button.setFont(font); 48 | } 49 | } 50 | 51 | module.exports = ButtonArea; 52 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/cw_text_area.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | QPlainTextEdit 6 | } = require('@nodegui/nodegui'); 7 | 8 | class CwTextArea extends QWidget{ 9 | constructor(){ 10 | super(); 11 | 12 | this.layout = new QBoxLayout(Direction.LeftToRight); 13 | 14 | this.text_input = new QPlainTextEdit(); 15 | this.visible_user_ids_input = new QPlainTextEdit(); 16 | 17 | this.setObjectName('cwTextArea'); 18 | this.setLayout(this.layout); 19 | 20 | this.layout.setContentsMargins(0,0,0,0); 21 | this.layout.setSpacing(5); 22 | 23 | this.text_input.setObjectName('cwTextInput'); 24 | this.text_input.setReadOnly(false); 25 | this.text_input.setWordWrapMode(3); 26 | this.text_input.setPlaceholderText('ここに警告文を入力します'); 27 | 28 | this.visible_user_ids_input.setObjectName('visibleUserIdsInput'); 29 | this.visible_user_ids_input.setReadOnly(false); 30 | this.visible_user_ids_input.setWordWrapMode(3); 31 | this.visible_user_ids_input.setPlaceholderText('閲覧を許可するユーザーのID'); 32 | 33 | this.visible_user_ids_input.setFixedSize(99,76); 34 | 35 | this.layout.addWidget(this.text_input, 1); 36 | this.layout.addWidget(this.visible_user_ids_input); 37 | } 38 | 39 | setFont(font){ 40 | this.text_input.setFont(font); 41 | this.visible_user_ids_input.setFont(font); 42 | } 43 | 44 | setVisbleUserIds(visible_user_ids){ 45 | this.visible_user_ids_input.setPlainText(visible_user_ids.join('\n')); 46 | } 47 | 48 | setText(text){ 49 | this.text_input.setPlainText(text); 50 | } 51 | 52 | getInfo(){ 53 | var visible_user_ids = this.visible_user_ids_input.toPlainText().split('\n').filter((val, i, self) => { 54 | return !(!(val) || !(self.indexOf(val) === i)); 55 | }); 56 | 57 | return { 58 | cw: this.text_input.toPlainText(), 59 | visibleUserIds: visible_user_ids 60 | } 61 | } 62 | 63 | clear(){ 64 | this.text_input.setPlainText(''); 65 | this.visible_user_ids_input.setPlainText(''); 66 | } 67 | } 68 | 69 | module.exports = CwTextArea; 70 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/file_item.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | QPushButton, 5 | QLineEdit, 6 | Direction 7 | } = require('@nodegui/nodegui'); 8 | 9 | class FileItem extends QWidget{ 10 | constructor(){ 11 | super(); 12 | 13 | this.layout = new QBoxLayout(Direction.LeftToRight); 14 | 15 | this.filename = new QLineEdit(); 16 | this.remove_button = new QPushButton; 17 | this.nsfw_button = new QPushButton; 18 | 19 | this.setLayout(this.layout); 20 | 21 | this.layout.setContentsMargins(0,0,0,0); 22 | this.layout.setSpacing(0); 23 | 24 | this.filename.setReadOnly(true); 25 | 26 | this.remove_button.setText('×'); 27 | 28 | this.nsfw_button.setText('NSFW'); 29 | 30 | this.filename.setMaximumSize(65535, 20); 31 | this.filename.setMinimumSize(10, 20); 32 | 33 | this.remove_button.setFixedSize(20,20); 34 | this.nsfw_button.setFixedSize(40,20); 35 | 36 | this.layout.addWidget(this.filename, 1); 37 | this.layout.addWidget(this.nsfw_button); 38 | this.layout.addWidget(this.remove_button); 39 | } 40 | 41 | setFont(font){ 42 | this.filename.setFont(font); 43 | this.remove_button.setFont(font); 44 | this.nsfw_button.setFont(font); 45 | } 46 | 47 | setFilename(filename){ 48 | this.filename.setText(filename); 49 | } 50 | } 51 | 52 | module.exports = FileItem; 53 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/image_area.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QFileDialog, 4 | QPushButton, 5 | QBoxLayout, 6 | QClipboardMode, 7 | QApplication, 8 | FileMode, 9 | Direction 10 | } = require('@nodegui/nodegui'); 11 | const fs = require('fs'); 12 | const dateformat = require('dateformat'); 13 | 14 | const FileItem = require('./file_item.js'); 15 | const App = require('../../index.js'); 16 | 17 | class ImageArea extends QWidget{ 18 | constructor(){ 19 | super(); 20 | 21 | this.clipboard = QApplication.clipboard(); 22 | this.file_max = 4; 23 | this.files = []; 24 | 25 | this.layout = new QBoxLayout(Direction.LeftToRight); 26 | 27 | this.left = new QWidget(); 28 | this.right = new QWidget(); 29 | 30 | this.left_layout = new QBoxLayout(Direction.TopToBottom); 31 | this.right_layout = new QBoxLayout(Direction.TopToBottom); 32 | 33 | this.from_file_button = new QPushButton(); 34 | this.from_clipboard_button = new QPushButton(); 35 | //this.from_drive_button = new QPushButton(); 36 | 37 | this.setObjectName('imageArea'); 38 | this.setLayout(this.layout); 39 | 40 | this.layout.setContentsMargins(0,5,0,5); 41 | this.layout.setSpacing(5); 42 | 43 | this.left.setLayout(this.left_layout); 44 | this.left.setObjectName('imageStatusArea'); 45 | 46 | this.left_layout.setContentsMargins(0,0,0,0); 47 | this.left_layout.setSpacing(0); 48 | 49 | this.right.setLayout(this.right_layout); 50 | this.right.setObjectName('imageAddArea'); 51 | 52 | this.right_layout.setContentsMargins(0,0,0,0); 53 | this.right_layout.setSpacing(0); 54 | 55 | this.from_file_button.setObjectName('imageSelectDialogButton'); 56 | this.from_file_button.setText('ファイルを選択'); 57 | 58 | this.from_clipboard_button.setObjectName('imageSelectClipboardButton'); 59 | this.from_clipboard_button.setText('クリップボードから取得'); 60 | 61 | //this.from_drive_button.setObjectName('imageSelectDriveButton'); 62 | //this.from_drive_button.setText('ドライブから選択'); 63 | 64 | this.right_layout.addWidget(this.from_file_button); 65 | this.right_layout.addWidget(this.from_clipboard_button); 66 | //this.right_layout.addWidget(this.from_drive_button); 67 | 68 | this.layout.addWidget(this.left, 1); 69 | this.layout.addWidget(this.right); 70 | 71 | this.from_file_button.addEventListener('clicked', this._add_file.bind(this, 'file')); 72 | this.from_clipboard_button.addEventListener('clicked', this._add_file.bind(this, 'clipboard')); 73 | //this.from_drive_button.addEventListener('clicked', this._add_file.bind(this, 'drive')); 74 | 75 | this._setup(); 76 | } 77 | 78 | setFont(font){ 79 | this.from_file_button.setFont(font); 80 | this.from_clipboard_button.setFont(font); 81 | //this.from_drive_button.setFont(font); 82 | 83 | for(var f of this.files) f.item.setFont(font); 84 | } 85 | 86 | _setup(){ 87 | for(var i = 0; i < this.file_max; i++){ 88 | var data = { 89 | item: new FileItem(this.font), 90 | isUse: false, 91 | data: {}, 92 | index: i 93 | } 94 | 95 | this.files.push(data); 96 | this.left_layout.addWidget(data.item); 97 | 98 | data.item.hide(); 99 | } 100 | } 101 | 102 | get_files(){ 103 | var result = []; 104 | 105 | for(var file of this.files){ 106 | if(file.isUse) result.push(file.data.id); 107 | } 108 | 109 | return result; 110 | } 111 | 112 | clear(){ 113 | for(var file of this.files){ 114 | if(file.isUse) file.item.remove_button.click(); 115 | } 116 | } 117 | 118 | async _add_file(type){ 119 | if(!this._check_insert_ready()) return; 120 | 121 | switch(type){ 122 | case 'clipboard': 123 | var image = this.clipboard.pixmap(QClipboardMode.Clipboard); 124 | 125 | if((0 >= image.width()) || (0 >= image.height())) return; 126 | 127 | var name = dateformat(new Date(), 'yyyy-mm-dd-HH-MM-ss.png'); 128 | 129 | image.save(`${App.data_directory.get('tmp')}${name}`, 'PNG'); 130 | 131 | var file = fs.createReadStream(`${App.data_directory.get('tmp')}${name}`); 132 | 133 | if(!file) return; 134 | 135 | await this._insert_file(name, file); 136 | 137 | fs.unlinkSync(`${App.data_directory.get('tmp')}${name}`); 138 | 139 | return; 140 | case 'file': { 141 | const file_dialog = new QFileDialog(); 142 | file_dialog.setFileMode(FileMode.AnyFile); 143 | file_dialog.exec(); 144 | 145 | if(file_dialog.result() != 1) return; 146 | 147 | var file = fs.createReadStream(file_dialog.selectedFiles()[0]); 148 | this._insert_file("", file); 149 | return; 150 | } 151 | 152 | case 'drive': 153 | return; 154 | } 155 | } 156 | 157 | _check_insert_ready(){ 158 | var result = false; 159 | 160 | for(var f of this.files){ 161 | if(!f.isUse) result = true; 162 | } 163 | 164 | return result; 165 | } 166 | 167 | async _insert_file(name, file){ 168 | var data = { 169 | file: file, 170 | name: name 171 | }; 172 | 173 | try{ 174 | data = await App.client.call_multipart("drive/files/create", data); 175 | data = JSON.parse(data); 176 | }catch(err){ 177 | console.log(err); 178 | return; 179 | } 180 | 181 | this.insertFile(data); 182 | } 183 | 184 | insertFile(data){ 185 | for(var i = 0; i < this.file_max; i++){ 186 | var file = this.files[i]; 187 | if(file.isUse) continue; 188 | 189 | var remove_func = function(i, remove_func){ 190 | var file = this.files[i]; 191 | 192 | file.item.hide(); 193 | file.item.filename.clear(); 194 | file.data = {}; 195 | file.isUse = false; 196 | file.item.remove_button.removeEventListener('clicked', remove_func); 197 | }.bind(this, i, remove_func); 198 | 199 | var toggle_nsfw = async function(i){ 200 | var file = this.files[i]; 201 | var is_sensitive = file.data.isSensitive; 202 | 203 | var data = { 204 | fileId: file.data.id, 205 | isSensitive: !is_sensitive 206 | } 207 | 208 | file.data = await App.client.call("drive/files/update", data); 209 | 210 | if(is_sensitive) file.item.nsfw_button.setText("NSFW"); 211 | else file.item.nsfw_button.setText("普通"); 212 | }.bind(this, i); 213 | 214 | if(data.isSensitive) file.item.nsfw_button.setText("普通"); 215 | else file.item.nsfw_button.setText("NSFW"); 216 | 217 | file.item.remove_button.addEventListener('clicked', remove_func); 218 | file.item.nsfw_button.addEventListener('clicked', toggle_nsfw); 219 | 220 | file.item.setFilename(data.name); 221 | file.data = data; 222 | file.isUse = true; 223 | file.item.show(); 224 | break; 225 | } 226 | } 227 | } 228 | 229 | module.exports = ImageArea; 230 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLineEdit, 3 | QWidget, 4 | WindowType, 5 | QBoxLayout, 6 | QFont, 7 | Direction 8 | } = require('@nodegui/nodegui'); 9 | 10 | const ImageArea = require("./image_area.js"); 11 | const PollArea = require("./poll_area.js"); 12 | const PostTextArea = require("./post_text_area.js"); 13 | const CwTextArea = require("./cw_text_area.js"); 14 | const ButtonArea = require("./button_area.js"); 15 | const App = require('../../index.js'); 16 | 17 | class CustomPostWindow extends QWidget{ 18 | constructor(){ 19 | super(); 20 | 21 | this.random_emoji = App.random_emoji; 22 | 23 | this.layout = new QBoxLayout(Direction.TopToBottom); 24 | 25 | this.target_area = new QWidget(); 26 | 27 | this.target_area_layout = new QBoxLayout(Direction.LeftToRight); 28 | 29 | this.reply_id_input = new QLineEdit(); 30 | this.renote_id_input = new QLineEdit(); 31 | 32 | this.post_text_area = new PostTextArea(); 33 | this.cw_text_area = new CwTextArea(); 34 | this.image_area = new ImageArea(); 35 | this.poll_area = new PollArea(); 36 | this.buttons_area = new ButtonArea(); 37 | 38 | this.setLayout(this.layout); 39 | this.setWindowFlag(WindowType.Window, true); 40 | //this.setWindowFlag(WindowType.WindowCloseButtonHint, false); 41 | this.setWindowTitle('拡張投稿 - TenCha'); 42 | this.resize(368,430); 43 | this.setObjectName('rootView'); 44 | //this.setStyleSheet(assets.css); 45 | 46 | this.layout.setContentsMargins(5,5,5,5); 47 | this.layout.setSpacing(5); 48 | 49 | this.target_area.setObjectName('replyRenoteArea'); 50 | this.target_area.setLayout(this.target_area_layout); 51 | 52 | this.target_area_layout.setContentsMargins(0,0,0,0); 53 | this.target_area_layout.setSpacing(5); 54 | 55 | this.reply_id_input.setPlaceholderText('リプライ先'); 56 | this.reply_id_input.setObjectName('replyIdInput'); 57 | 58 | this.renote_id_input.setPlaceholderText('引用先'); 59 | this.renote_id_input.setObjectName('renoteIdInput'); 60 | 61 | this.target_area_layout.addWidget(this.reply_id_input); 62 | this.target_area_layout.addWidget(this.renote_id_input); 63 | 64 | this.layout.addWidget(this.post_text_area); 65 | this.layout.addWidget(this.cw_text_area); 66 | this.layout.addWidget(this.target_area); 67 | this.layout.addWidget(this.image_area); 68 | this.layout.addWidget(this.poll_area); 69 | this.layout.addWidget(this.buttons_area); 70 | 71 | this.buttons_area.post_button.addEventListener('clicked', this.post.bind(this)); 72 | this.buttons_area.clear_button.addEventListener('clicked', this.clear.bind(this)); 73 | this.buttons_area.close_button.addEventListener('clicked', this.close.bind(this)); 74 | 75 | this.poll_area.reset_exp_date_time(); 76 | this.post_text_area.updatePlaceholder(); 77 | } 78 | 79 | setup(){ 80 | this.font = new QFont(App.settings.get("font"), 9); 81 | 82 | this.post_text_area.setFont(this.font); 83 | this.cw_text_area.setFont(this.font); 84 | this.image_area.setFont(this.font); 85 | this.poll_area.setFont(this.font); 86 | this.reply_id_input.setFont(this.font); 87 | this.renote_id_input.setFont(this.font); 88 | this.buttons_area.setFont(this.font); 89 | } 90 | 91 | exec(data){ 92 | this.clear(); 93 | 94 | if(!this.isVisible()) this.show(); 95 | 96 | if(data.text) this.post_text_area.setText(data.text); 97 | if(data.cw) this.cw_text_area.setText(data.cw); 98 | if(data.viaMobile) this.post_text_area.setViaMobile(data.viaMobile); 99 | if(data.files) for(var f of data.files) this.image_area.insertFile(f); 100 | if(data.poll) this.poll_area.setPoll(data.poll); 101 | 102 | if(data.replyId) this.reply_id_input.setText(data.replyId); 103 | if(data.renoteId) this.renote_id_input.setText(data.renoteId); 104 | 105 | if(data.visibility){ 106 | this.post_text_area.setVisibility(data.visibility); 107 | if( 108 | data.visibility == "specified" 109 | && data.visible_user_ids 110 | ) this.cw_text_area.setVisbleUserIds(data.visible_user_ids); 111 | } 112 | } 113 | 114 | async post(){ 115 | var post_area_info = this.post_text_area.getInfo(); 116 | var cw_area_info = this.cw_text_area.getInfo(); 117 | var reply_id = this.reply_id_input.text(); 118 | var renote_id = this.renote_id_input.text(); 119 | var poll = this.poll_area.get_poll(); 120 | var files = this.image_area.get_files(); 121 | 122 | // 投稿していいか 123 | if(!post_area_info.text && !files[0] && !poll.choices) return; 124 | 125 | var data = post_area_info; 126 | 127 | if(files[0]) data.fileIds = files; 128 | if(cw_area_info.cw) data.cw = cw_area_info.cw; 129 | if(post_area_info.visibility == 'specified' && cw_area_info.visibleUserIds) data.visibleUserIds = cw_area_info.visibleUserIds; 130 | if(!renote_id && reply_id) data.replyId = reply_id; 131 | if(!reply_id && renote_id) data.renoteId = renote_id; 132 | if(poll.choices) data.poll = poll; 133 | 134 | try{ 135 | App.status_label.setText('投稿中...'); 136 | await App.client.call('notes/create', data); 137 | this.clear(); 138 | if(!this.buttons_area.is_after_close_check.isChecked()) this.hide(); 139 | App.status_label.setText('投稿成功!'); 140 | }catch(err){ 141 | console.log(err); 142 | App.status_label.setText(`投稿失敗...(${err.error.error.message})`); 143 | } 144 | } 145 | 146 | clear(){ 147 | this.post_text_area.clear(); 148 | this.cw_text_area.clear(); 149 | this.reply_id_input.clear(); 150 | this.renote_id_input.clear(); 151 | this.image_area.clear(); 152 | this.poll_area.clear(); 153 | this.post_text_area.updatePlaceholder(); 154 | } 155 | } 156 | 157 | module.exports = CustomPostWindow; 158 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/poll_area.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | QPlainTextEdit, 6 | QCheckBox, 7 | QDateTimeEdit, 8 | QSpinBox, 9 | QRadioButton, 10 | DateFormat, 11 | QDate, 12 | QTime 13 | } = require('@nodegui/nodegui'); 14 | 15 | class PollArea extends QWidget{ 16 | constructor(){ 17 | super(); 18 | 19 | this.layout = new QBoxLayout(Direction.LeftToRight); 20 | 21 | this.right = new QWidget(); 22 | 23 | this.right_layout = new QBoxLayout(Direction.TopToBottom); 24 | 25 | this.choices_input = new QPlainTextEdit(); 26 | this.is_multiple_check = new QCheckBox(); 27 | this.exp_radio_unlimited = new QRadioButton(); 28 | this.exp_radio_at = new QRadioButton(); 29 | this.exp_radio_after = new QRadioButton(); 30 | this.expires_at_input = new QDateTimeEdit(); 31 | this.expired_after_input = new QSpinBox(); 32 | 33 | this.setObjectName('pollArea'); 34 | this.setLayout(this.layout); 35 | 36 | this.layout.setContentsMargins(0,0,0,0); 37 | this.layout.setSpacing(5); 38 | 39 | this.right.setObjectName('pollAreaRight'); 40 | this.right.setLayout(this.right_layout); 41 | 42 | this.right_layout.setContentsMargins(0,0,0,0); 43 | this.right_layout.setSpacing(5); 44 | 45 | this.choices_input.setObjectName('choicesInput'); 46 | this.choices_input.setReadOnly(false); 47 | this.choices_input.setWordWrapMode(3); 48 | this.choices_input.setPlaceholderText('選択肢'); 49 | 50 | this.is_multiple_check.setObjectName('isMultipleCheck'); 51 | this.is_multiple_check.setText('複数選択可'); 52 | 53 | this.exp_radio_unlimited.setObjectName('expRadioUnlimited'); 54 | this.exp_radio_unlimited.setText('無制限'); 55 | 56 | this.exp_radio_at.setObjectName('expRadioAt'); 57 | this.exp_radio_at.setText('日付指定'); 58 | 59 | this.exp_radio_after.setObjectName('expRadioAfter'); 60 | this.exp_radio_after.setText('経過指定(ms)'); 61 | 62 | this.expires_at_input.setObjectName('expiresAtInput'); 63 | 64 | this.expired_after_input.setObjectName('expiredAfterInput') 65 | this.expired_after_input.setMinimum(1); 66 | this.expired_after_input.setMaximum(99999999999); 67 | 68 | this.right_layout.addWidget(this.is_multiple_check); 69 | this.right_layout.addWidget(this.exp_radio_unlimited); 70 | this.right_layout.addWidget(this.exp_radio_at); 71 | this.right_layout.addWidget(this.exp_radio_after); 72 | this.right_layout.addWidget(this.expires_at_input); 73 | this.right_layout.addWidget(this.expired_after_input); 74 | 75 | this.layout.addWidget(this.choices_input, 1); 76 | this.layout.addWidget(this.right); 77 | } 78 | 79 | setFont(font){ 80 | this.choices_input.setFont(font); 81 | this.is_multiple_check.setFont(font); 82 | this.exp_radio_unlimited.setFont(font); 83 | this.exp_radio_at.setFont(font); 84 | this.exp_radio_after.setFont(font); 85 | this.expires_at_input.setFont(font); 86 | this.expired_after_input.setFont(font); 87 | } 88 | 89 | setPoll(poll){ 90 | var choices = []; 91 | for(var c of poll.choices) choices.push(c.text); 92 | choices = choices.join('\n'); 93 | var is_multiple = poll.multiple; 94 | 95 | this.choices_input.setPlainText(choices); 96 | this.is_multiple_check.setChecked(is_multiple); 97 | this.exp_radio_unlimited.setChecked(true); 98 | } 99 | 100 | get_poll(){ 101 | var data = {}; 102 | var choices = this.choices_input.toPlainText().split('\n').filter((val, i, self) => { 103 | return !(!(val) || !(self.indexOf(val) === i)); 104 | }); 105 | 106 | if(choices.length >= 2){ 107 | data.choices = choices; 108 | data.multiple = this.is_multiple_check.isChecked(); 109 | 110 | if(this.exp_radio_at.isChecked()){ 111 | var t = this.expires_at_input.dateTime().toString(DateFormat.ISODate); 112 | data.expiresAt = Date.parse(t); 113 | } 114 | 115 | if(this.exp_radio_after.isChecked()){ 116 | var t = this.expired_after_input.value(); 117 | data.expiredAfter = t; 118 | } 119 | }else{ 120 | return {}; 121 | } 122 | 123 | return data; 124 | } 125 | 126 | clear(){ 127 | this.choices_input.setPlainText(''); 128 | this.is_multiple_check.setChecked(false); 129 | this.reset_exp_date_time(); 130 | } 131 | 132 | reset_exp_date_time(){ 133 | var _next_day = new Date(); 134 | _next_day.setDate(_next_day.getDate() +1); 135 | 136 | var _d = new QDate(); 137 | var _t = new QTime(); 138 | _d.setDate(_next_day.getFullYear(), _next_day.getMonth(), _next_day.getDay()); 139 | _t.setHMS(_next_day.getHours(), _next_day.getMinutes(), _next_day.getSeconds()); 140 | this.expires_at_input.setTime(_d); 141 | this.expires_at_input.setDate(_t); 142 | } 143 | } 144 | 145 | module.exports = PollArea; 146 | -------------------------------------------------------------------------------- /src/widgets/custom_post_window/post_text_area.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | QPlainTextEdit, 6 | QComboBox, 7 | QPushButton, 8 | QCheckBox 9 | } = require('@nodegui/nodegui'); 10 | 11 | const Assets = require("../../assets.js"); 12 | const App = require("../../index.js"); 13 | 14 | class PostTextArea extends QWidget{ 15 | constructor(){ 16 | super(); 17 | 18 | this.assets = new Assets("CustomPostWindow"); 19 | 20 | this.visibilitys = [ 21 | { name: "public", text: "公開" }, 22 | { name: "home", text: "ホーム" }, 23 | { name: "followers", text: "フォロワー" }, 24 | { name: "specified", text: "ダイレクト" }, 25 | { name: "random", text: "ランダム" } 26 | ]; 27 | 28 | this.layout = new QBoxLayout(Direction.LeftToRight); 29 | 30 | this.right = new QWidget(); 31 | 32 | this.right_layout = new QBoxLayout(Direction.TopToBottom); 33 | 34 | this.text_input = new QPlainTextEdit(); 35 | this.emoji_pick_button = new QPushButton(); 36 | this.random_emoji_button = new QPushButton(); 37 | this.visibility_select = new QComboBox(); 38 | this.is_local_check = new QCheckBox(); 39 | this.is_mobile_check = new QCheckBox(); 40 | 41 | this.setLayout(this.layout); 42 | this.setObjectName('postTextArea'); 43 | 44 | this.layout.setContentsMargins(0,0,0,0); 45 | this.layout.setSpacing(5); 46 | 47 | this.right.setObjectName('postTextAreaSettingArea'); 48 | this.right.setLayout(this.right_layout); 49 | 50 | this.right_layout.setContentsMargins(0,0,0,0); 51 | this.right_layout.setSpacing(3); 52 | 53 | this.text_input.setObjectName('postTextInput'); 54 | this.text_input.setReadOnly(false); 55 | this.text_input.setWordWrapMode(3); 56 | 57 | this.emoji_pick_button.setText('絵文字'); 58 | this.emoji_pick_button.setObjectName('emojiPickerButton'); 59 | this.emoji_pick_button.addEventListener('clicked', this.exec_emoji_picker.bind(this)); 60 | 61 | this.random_emoji_button.setText('ランダム絵文字'); 62 | this.random_emoji_button.setObjectName('randomEmojiButton'); 63 | this.random_emoji_button.addEventListener('clicked', this.random_emoji.bind(this)); 64 | 65 | this.visibility_select.setObjectName('visibility_select'); 66 | for(var vis of this.visibilitys){ 67 | this.visibility_select.addItem(undefined, vis.text); 68 | } 69 | 70 | this.is_local_check.setObjectName('isLocalCheck'); 71 | this.is_local_check.setText('ローカルのみ'); 72 | 73 | this.is_mobile_check.setObjectName('isMobileCheck'); 74 | this.is_mobile_check.setText('モバイルから'); 75 | 76 | this.right_layout.addWidget(this.visibility_select); 77 | this.right_layout.addWidget(this.is_local_check); 78 | this.right_layout.addWidget(this.is_mobile_check); 79 | this.right_layout.addWidget(this.emoji_pick_button); 80 | this.right_layout.addWidget(this.random_emoji_button); 81 | 82 | this.layout.addWidget(this.text_input, 1); 83 | this.layout.addWidget(this.right); 84 | } 85 | 86 | setFont(font){ 87 | this.text_input.setFont(font); 88 | this.visibility_select.setFont(font); 89 | this.is_local_check.setFont(font); 90 | this.is_mobile_check.setFont(font); 91 | this.emoji_pick_button.setFont(font); 92 | this.random_emoji_button.setFont(font); 93 | 94 | this.setVisibility(App.settings.get("start_visibility")); 95 | } 96 | 97 | setVisibility(vis){ 98 | for(var v of this.visibilitys){ 99 | if(v.name == vis){ 100 | this.visibility_select.setCurrentText(v.text); 101 | } 102 | } 103 | } 104 | 105 | setText(text){ 106 | this.text_input.setPlainText(text); 107 | } 108 | 109 | setViaMobile(is_mobile){ 110 | this.is_mobile_check.setChecked(is_mobile); 111 | } 112 | 113 | updatePlaceholder(){ 114 | var _placeholder = this.assets.placeholder; 115 | var placeholder = _placeholder[Math.floor(Math.random() * _placeholder.length)]; 116 | this.text_input.setPlaceholderText(placeholder); 117 | } 118 | 119 | getInfo(){ 120 | return { 121 | text: this.text_input.toPlainText(), 122 | visibility: this._parse_visibility(), 123 | localOnly: this.is_local_check.isChecked(), 124 | viaMobile: this.is_mobile_check.isChecked() 125 | } 126 | } 127 | 128 | _parse_visibility(){ 129 | var result = 'public'; 130 | 131 | for(var v of this.visibilitys){ 132 | if(v.text == this.visibility_select.currentText()){ 133 | result = v.name; 134 | } 135 | } 136 | 137 | return result; 138 | } 139 | 140 | random_emoji(){ 141 | var emoji = App.random_emoji.exec(); 142 | this.text_input.insertPlainText(emoji); 143 | } 144 | 145 | exec_emoji_picker(){ 146 | App.emoji_picker.exec(); 147 | App.emoji_picker.setCloseEvent(function(){ 148 | var result = App.emoji_picker.get_result(); 149 | this.text_input.insertPlainText(result); 150 | }.bind(this)); 151 | } 152 | 153 | clear(){ 154 | this.text_input.setPlainText(''); 155 | if(!App.settings.get("memory_visibility")) this.setVisibility(App.settings.get("start_visibility")); 156 | this.is_local_check.setChecked(false); 157 | this.is_mobile_check.setChecked(false); 158 | } 159 | } 160 | 161 | module.exports = PostTextArea; 162 | -------------------------------------------------------------------------------- /src/widgets/delete_confirm_window/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMessageBox, 3 | QPushButton, 4 | ButtonRole, 5 | QFont 6 | } = require("@nodegui/nodegui"); 7 | 8 | const App = require('../../index.js'); 9 | 10 | class DeleteConfirmWindow extends QMessageBox{ 11 | constructor(note, is_repost = false){ 12 | super(); 13 | 14 | this.accept = new QPushButton(); 15 | this.reject = new QPushButton(); 16 | 17 | this.setText(`失った信頼はもう戻ってきませんが、このノートを削除しますか?\n\n> ${note.text}`); 18 | 19 | this.accept.setText("おk"); 20 | this.reject.setText("やめとく"); 21 | 22 | this.addButton(this.reject, ButtonRole.RejectRole); 23 | this.addButton(this.accept, ButtonRole.AcceptRole); 24 | 25 | this.setFont(new QFont(App.settings.get('font'), 9)); 26 | this.accept.setFont(new QFont(App.settings.get('font'), 9)); 27 | this.reject.setFont(new QFont(App.settings.get('font'), 9)); 28 | 29 | this.accept.addEventListener('clicked', () => { 30 | App.post_action._note_remove(note); 31 | App.status_label.setText("削除しました!"); 32 | 33 | if(is_repost){ 34 | var data = {}; 35 | 36 | // TODO: isLocal 37 | if(note.no_emoji_text) data.text = note.no_emoji_text; 38 | if(note.no_emoji_cw) data.cw = note.no_emoji_cw; 39 | if(note.viaMobile) data.viaMobile = note.viaMobile; 40 | if(note.files) data.files = note.files; 41 | if(note.poll) data.poll = note.poll; 42 | if(note.replyId) data.replyId = note.replyId; 43 | if(note.renoteId) data.renoteId = note.renoteId; 44 | data.visibility = note.visibility; 45 | if(note.visibleUserIds) data.visible_user_ids = note.visibleUserIds; 46 | 47 | App.custom_post_window.exec(data); 48 | } 49 | this.close(); 50 | }); 51 | this.reject.addEventListener('clicked', () => { 52 | this.close(); 53 | }); 54 | } 55 | 56 | close(){ 57 | this.accept.close(); 58 | this.reject.close(); 59 | 60 | this.accept = undefined; 61 | this.reject = undefined; 62 | 63 | super.close(); 64 | } 65 | } 66 | 67 | module.exports = DeleteConfirmWindow; 68 | -------------------------------------------------------------------------------- /src/widgets/emoji_picker/emoji_item.js: -------------------------------------------------------------------------------- 1 | const { 2 | QPushButton, 3 | QIcon, 4 | QSize 5 | } = require('@nodegui/nodegui'); 6 | 7 | class EmojiItem extends QPushButton{ 8 | constructor(emoji){ 9 | super(); 10 | 11 | this.icon = new QIcon(emoji.filename); 12 | 13 | this.setIcon(this.icon); 14 | this.setIconSize(new QSize(16,16)); 15 | } 16 | } 17 | 18 | module.exports = EmojiItem; 19 | -------------------------------------------------------------------------------- /src/widgets/emoji_picker/emoji_list_widget.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QGridLayout, 4 | QScrollArea 5 | } = require('@nodegui/nodegui'); 6 | 7 | class EmojiListWidget extends QScrollArea{ 8 | constructor(category){ 9 | super(); 10 | 11 | this.count = 0; 12 | this.max_col = 8; 13 | this.category = category; 14 | 15 | this.widget = new QWidget(); 16 | this.layout = new QGridLayout(); 17 | 18 | this.widget.setLayout(this.layout); 19 | this.setWidget(this.widget); 20 | } 21 | 22 | addWidget(widget){ 23 | var row = this.count / this.max_col; 24 | var col = this.count % this.max_col; 25 | 26 | this.layout.addWidget(widget, row, col); 27 | this.count++; 28 | } 29 | } 30 | 31 | module.exports = EmojiListWidget; 32 | -------------------------------------------------------------------------------- /src/widgets/emoji_picker/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | QTabWidget, 6 | WindowType, 7 | QFont, 8 | QIcon 9 | } = require('@nodegui/nodegui'); 10 | const { parse } = require('twemoji-parser'); 11 | 12 | const App = require('../../index.js'); 13 | const Assets = require('../../assets.js'); 14 | const EmojiListWidget = require('./emoji_list_widget.js'); 15 | const EmojiItem = require('./emoji_item.js'); 16 | 17 | class EmojiPicker extends QWidget{ 18 | constructor(){ 19 | super(); 20 | 21 | this.result = ''; 22 | this.close_event = null; 23 | 24 | this.assets = new Assets('EmojiList'); 25 | 26 | this.layout = new QBoxLayout(Direction.TopToBottom); 27 | 28 | this.tab_widget = new QTabWidget(); 29 | 30 | this.setLayout(this.layout); 31 | this.setWindowFlag(WindowType.Window, true); 32 | this.setWindowTitle('絵文字ピッカー - TenCha'); 33 | this.resize(360, 430); 34 | this.setMaximumSize(360, 430); 35 | this.setMinimumSize(360, 430); 36 | 37 | this.layout.setContentsMargins(5,5,5,5); 38 | this.layout.setSpacing(5); 39 | 40 | this.layout.addWidget(this.tab_widget, 1); 41 | } 42 | 43 | async init(){ 44 | this.tab_widget.setFont(new QFont(App.settings.get("font"), 9)); 45 | 46 | for(var category of this.assets.emoji_list.categorys){ 47 | var widget = new EmojiListWidget(category.name); 48 | var emojis = []; 49 | 50 | for(var _emoji of this.assets.emoji_list.emojis){ 51 | if(_emoji.category != category.name) continue; 52 | emojis.push(this._set_emoji(_emoji, widget)); 53 | } 54 | 55 | var result = await Promise.all(emojis); 56 | 57 | for(var emoji of result){ 58 | if(!emoji) continue; 59 | emoji.widget.addWidget(emoji.item); 60 | } 61 | 62 | this.tab_widget.addTab(widget, new QIcon(), category.text); 63 | } 64 | } 65 | 66 | async _set_emoji(_emoji, widget){ 67 | try{ 68 | var twemojis = parse(_emoji.text); 69 | var emoji = await App.emoji_parser.cache.get(twemojis[0]); 70 | 71 | var item = new EmojiItem(emoji); 72 | item.addEventListener('clicked', () => { 73 | this.result = _emoji.text; 74 | this.close(); 75 | }); 76 | return { widget: widget, item: item }; 77 | }catch(err){ 78 | console.log(err); 79 | return; 80 | } 81 | } 82 | 83 | exec(){ 84 | super.show(); 85 | } 86 | 87 | close(){ 88 | super.close(); 89 | } 90 | 91 | get_result(){ 92 | var result = this.result; 93 | this.result = ''; 94 | // 消す 95 | this.removeEventListener('Close', this.close_event); 96 | // 使い終わったら忘れる 97 | this.close_event = null; 98 | 99 | return result; 100 | } 101 | 102 | setCloseEvent(callback){ 103 | // もし既に設定されているなら拒否 104 | if(this.close_event) return; 105 | 106 | // 設定する 107 | this.close_event = callback; 108 | this.addEventListener('Close', callback); 109 | } 110 | } 111 | 112 | module.exports = EmojiPicker; 113 | -------------------------------------------------------------------------------- /src/widgets/icon_label/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QLabel, 3 | AspectRatioMode, 4 | TransformationMode 5 | } = require('@nodegui/nodegui'); 6 | 7 | class IconLabel extends QLabel{ 8 | constructor(size){ 9 | super(); 10 | this.maxSize = size; 11 | 12 | this.setMinimumSize(this.maxSize, this.maxSize); 13 | this.setFixedSize(this.maxSize, this.maxSize); 14 | } 15 | 16 | setPixmap(pix){ 17 | var width = pix.width(); 18 | var height = pix.height(); 19 | 20 | var ratio; 21 | 22 | if(width > height){ 23 | ratio = width / this.maxSize; 24 | 25 | width = this.maxSize; 26 | height = height / ratio; 27 | }else{ 28 | ratio = height / this.maxSize; 29 | 30 | width = width / ratio; 31 | height = this.maxSize; 32 | } 33 | 34 | width = Math.ceil(width); 35 | height = Math.ceil(height); 36 | 37 | var icon = pix.scaled(width, height, AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation); 38 | 39 | super.setPixmap(icon); 40 | } 41 | } 42 | 43 | module.exports = IconLabel; 44 | -------------------------------------------------------------------------------- /src/widgets/login_window/host_input_widget.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QLabel, 4 | QBoxLayout, 5 | Direction, 6 | QPushButton, 7 | QLineEdit 8 | } = require('@nodegui/nodegui'); 9 | 10 | class HostInputWidget extends QWidget{ 11 | constructor(){ 12 | super(); 13 | 14 | this.layout = new QBoxLayout(Direction.TopToBottom); 15 | 16 | this.message_label = new QLabel(); 17 | this.status_label = new QLabel(); 18 | this.host_input = new QLineEdit(); 19 | this.via_input = new QLineEdit(); 20 | this.confirm_button = new QPushButton(); 21 | 22 | this.setLayout(this.layout); 23 | 24 | this.layout.setContentsMargins(5,5,5,5); 25 | this.layout.setSpacing(5); 26 | 27 | this.message_label.setWordWrap(true); 28 | this.message_label.setText('ログインしたいインスタンスのドメインを入れてSendを押してね!\nVia芸をしたいならViaも入れてね!'); 29 | this.message_label.setObjectName('loginLabel'); 30 | 31 | this.status_label.setText(''); 32 | this.status_label.setWordWrap(true); 33 | this.status_label.setObjectName('statusLabel'); 34 | 35 | this.host_input.setPlaceholderText('misskey.dev'); 36 | this.host_input.setObjectName('hostInput') 37 | 38 | this.via_input.setPlaceholderText('TenCha'); 39 | this.via_input.setObjectName('viaInput'); 40 | 41 | this.confirm_button.setText('Send!'); 42 | this.confirm_button.setObjectName('postButton'); 43 | 44 | this.layout.addWidget(this.message_label); 45 | this.layout.addWidget(this.status_label); 46 | this.layout.addWidget(this.host_input); 47 | this.layout.addWidget(this.via_input); 48 | this.layout.addStretch(1); 49 | this.layout.addWidget(this.confirm_button); 50 | } 51 | 52 | getInfo(){ 53 | var data = { 54 | host: this.host_input.text(), 55 | via: this.via_input.text() 56 | } 57 | 58 | return data; 59 | } 60 | } 61 | 62 | module.exports = HostInputWidget; 63 | -------------------------------------------------------------------------------- /src/widgets/login_window/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | WindowType, 6 | QStackedWidget 7 | } = require('@nodegui/nodegui'); 8 | 9 | const HostInputWidget = require('./host_input_widget.js'); 10 | const LoginUrlWidget = require('./login_url_widget.js'); 11 | const App = require('../../index.js'); 12 | const file = require('../../file.js'); 13 | 14 | class LoginWindow extends QWidget{ 15 | constructor(){ 16 | super(); 17 | 18 | this.data = {}; 19 | 20 | this.layout = new QBoxLayout(Direction.TopToBottom); 21 | 22 | this.root = new QStackedWidget(); 23 | 24 | this.host_input_widget = new HostInputWidget(); 25 | this.login_url_widget = new LoginUrlWidget(); 26 | 27 | this.setLayout(this.layout); 28 | this.setWindowFlag(WindowType.Window, true); 29 | this.setWindowTitle('新規ログイン - TenCha'); 30 | this.resize(300, 150); 31 | this.setObjectName('rootView'); 32 | 33 | this.layout.setContentsMargins(0,0,0,0); 34 | this.layout.setSpacing(0); 35 | 36 | this.root.addWidget(this.host_input_widget); 37 | this.root.addWidget(this.login_url_widget); 38 | 39 | this.layout.addWidget(this.root); 40 | 41 | this.host_input_widget.confirm_button.addEventListener('clicked', this._get_app_and_session.bind(this)); 42 | this.login_url_widget.confirm_button.addEventListener('clicked', this._get_userkey.bind(this)); 43 | 44 | this.root.setCurrentWidget(this.host_input_widget); 45 | } 46 | 47 | async _get_app_and_session(){ 48 | var info = this.host_input_widget.getInfo(); 49 | 50 | if(!info.host){ 51 | this.host_input_widget.status_label.setText('ホストを入力してね'); 52 | return; 53 | } 54 | 55 | if(!info.via) info.via = 'TenCha'; 56 | 57 | var data = { 58 | name: info.via, 59 | description: 'ネイティブなウインドウでMisskeyをしたいんだ...', 60 | permission: ['read:account', 'write:account', 'read:blocks', 'write:blocks', 'read:drive', 'write:drive', 'read:favorites', 'write:favorites', 'read:following', 'write:following', 'read:messaging', 'write:messaging', 'read:mutes', 'write:mutes', 'write:notes', 'read:notifications', 'write:notifications', 'read:reactions', 'write:reactions', 'write:votes', 'read:pages', 'write:pages', 'write:page-likes', 'read:page-likes', 'read:user-groups', 'write:user-groups'] 61 | }; 62 | 63 | this.data.host = info.host; 64 | this.data.via = info.via; 65 | 66 | this.host_input_widget.status_label.setText('アプリの作成中...'); 67 | 68 | try{ 69 | var app = await App.client.call('app/create', data, true, info.host); 70 | this.data.app = app; 71 | console.log(app); 72 | }catch(err){ 73 | console.log(err); 74 | this.host_input_widget.status_label.setText('アプリ作成に失敗しました!'); 75 | return; 76 | } 77 | 78 | this.host_input_widget.status_label.setText('セッションの作成中...'); 79 | var session_data = { appSecret: this.data.app.secret }; 80 | 81 | try{ 82 | var session = await App.client.call('auth/session/generate', session_data, true, info.host); 83 | this.data.session = session; 84 | console.log(session); 85 | }catch(err){ 86 | console.log(err); 87 | this.host_input_widget.status_label.setText('セッション作成に失敗しました!'); 88 | return; 89 | } 90 | 91 | this.login_url_widget.link_label.setText(`${this.data.session.url}`); 92 | this.root.setCurrentWidget(this.login_url_widget); 93 | } 94 | 95 | async _get_userkey(){ 96 | this.login_url_widget.status_label.setText('ユーザーキーの取得中...'); 97 | 98 | var data = { 99 | appSecret: this.data.app.secret, 100 | token: this.data.session.token 101 | } 102 | 103 | try{ 104 | var userkey = await App.client.call('auth/session/userkey', data, true, this.data.host); 105 | this.data.userkey = userkey; 106 | console.log(userkey); 107 | }catch(err){ 108 | console.log(err); 109 | this.login_url_widget.status_label.setText('ユーザーキーの取得に失敗しました!'); 110 | return; 111 | } 112 | 113 | this.login_url_widget.status_label.setText('認証情報の書き出し中...'); 114 | var auth_info = { 115 | host: this.data.host, 116 | secret: this.data.app.secret, 117 | token: this.data.userkey.accessToken 118 | } 119 | 120 | try{ 121 | await file.json_write(`${App.data_directory.get('settings')}config.json`, auth_info); 122 | }catch(err){ 123 | console.log(err); 124 | this.login_url_widget.status_label.setText('認証情報の書き出しに失敗しました!'); 125 | return; 126 | } 127 | 128 | this.data.done = true; 129 | 130 | this.close(); 131 | } 132 | 133 | getResult(){ 134 | var result = false; 135 | 136 | if(!this.data.done) result = true; 137 | 138 | return result; 139 | } 140 | } 141 | 142 | module.exports = LoginWindow; 143 | -------------------------------------------------------------------------------- /src/widgets/login_window/login_url_widget.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QLabel, 4 | QBoxLayout, 5 | Direction, 6 | QPushButton, 7 | TextInteractionFlag 8 | } = require('@nodegui/nodegui'); 9 | 10 | class LoginUrlWidget extends QWidget{ 11 | constructor(){ 12 | super(); 13 | 14 | this.layout = new QBoxLayout(Direction.TopToBottom); 15 | 16 | this.message_label = new QLabel(); 17 | this.status_label = new QLabel(); 18 | this.link_label = new QLabel(); 19 | this.confirm_button = new QPushButton(); 20 | 21 | this.setLayout(this.layout); 22 | 23 | this.layout.setContentsMargins(5,5,5,5); 24 | this.layout.setSpacing(5); 25 | 26 | this.message_label.setWordWrap(true); 27 | this.message_label.setText('アプリケーションの作成に成功しました!\nURLにアクセスしてアクセス許可をして「やっていく」ボタンを押してください!'); 28 | this.message_label.setObjectName('loginLabel'); 29 | 30 | this.status_label.setText(''); 31 | this.status_label.setWordWrap(true); 32 | this.status_label.setObjectName('statusLabel'); 33 | 34 | this.link_label.setWordWrap(true); 35 | this.link_label.setTextInteractionFlags(TextInteractionFlag.LinksAccessibleByMouse); 36 | this.link_label.setOpenExternalLinks(true); 37 | this.link_label.setObjectName('link'); 38 | 39 | this.confirm_button.setText('やっていく'); 40 | this.confirm_button.setObjectName('postButton'); 41 | 42 | this.layout.addWidget(this.message_label); 43 | this.layout.addWidget(this.status_label); 44 | this.layout.addWidget(this.link_label); 45 | this.layout.addStretch(1); 46 | this.layout.addWidget(this.confirm_button); 47 | } 48 | } 49 | 50 | module.exports = LoginUrlWidget; 51 | -------------------------------------------------------------------------------- /src/widgets/operation_menu/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMenu, 3 | QAction 4 | } = require('@nodegui/nodegui'); 5 | 6 | const App = require('../../index.js'); 7 | const ReactionMenu = require('../../widgets/reaction_menu/index.js'); 8 | 9 | class OperationMenu extends QMenu{ 10 | constructor(){ 11 | super(); 12 | 13 | this.reaction_menu = new ReactionMenu(); 14 | 15 | this.reaction_action = new QAction(); 16 | this.renote_action = new QAction(); 17 | this.quote_action = new QAction(); 18 | this.reply_action = new QAction(); 19 | this.copy_link_action = new QAction(); 20 | this.image_view_action = new QAction(); 21 | this.favorite_action = new QAction(); 22 | this.note_remove_action = new QAction(); 23 | this.repost_action = new QAction(); 24 | 25 | this.setTitle('操作'); 26 | 27 | this.reaction_action.setText('リアクション'); 28 | this.reaction_action.setMenu(this.reaction_menu); 29 | 30 | this.renote_action.setText('Renote'); 31 | this.quote_action.setText('引用Renote'); 32 | this.reply_action.setText('リプライ'); 33 | this.copy_link_action.setText('リンクをコピー'); 34 | this.image_view_action.setText('画像を表示'); 35 | this.favorite_action.setText('お気に入り'); 36 | this.note_remove_action.setText('削除'); 37 | this.repost_action.setText('削除して再投稿'); 38 | 39 | this.addAction(this.reaction_action); 40 | this.addSeparator(this.reply_action); 41 | 42 | this.addAction(this.reply_action); 43 | this.addSeparator(this.renote_action); 44 | 45 | this.addAction(this.renote_action); 46 | this.addAction(this.quote_action); 47 | this.addSeparator(this.favorite_action); 48 | 49 | this.addAction(this.favorite_action); 50 | this.addSeparator(this.copy_link_action); 51 | 52 | this.addAction(this.copy_link_action); 53 | this.addAction(this.image_view_action); 54 | this.addSeparator(this.note_remove_action); 55 | 56 | this.addAction(this.note_remove_action); 57 | 58 | this.addAction(this.repost_action); 59 | 60 | this.renote_action.addEventListener('triggered', () => { 61 | App.post_action.renote(); 62 | }); 63 | this.image_view_action.addEventListener('triggered', () => { 64 | App.post_action.image_view(); 65 | }); 66 | this.quote_action.addEventListener('triggered', () => { 67 | App.post_action.quote(); 68 | }); 69 | this.reply_action.addEventListener('triggered', () => { 70 | App.post_action.reply(); 71 | }); 72 | this.note_remove_action.addEventListener('triggered', () => { 73 | App.post_action.note_remove(); 74 | }); 75 | this.favorite_action.addEventListener('triggered', () => App.post_action.favorite()); 76 | this.repost_action.addEventListener('triggered', () => App.post_action.repost()); 77 | this.copy_link_action.addEventListener('triggered', () => App.post_action.copy_link()); 78 | 79 | this.addEventListener('Show', function(){ 80 | var is_renote_ready = App.post_action.is_renote_ready(); 81 | var is_remove_ready = App.post_action.is_remove_ready(); 82 | 83 | this.renote_action.setEnabled(is_renote_ready); 84 | this.quote_action.setEnabled(is_renote_ready); 85 | 86 | this.image_view_action.setEnabled(App.post_action.is_image_view_ready()); 87 | 88 | this.note_remove_action.setEnabled(is_remove_ready); 89 | this.repost_action.setEnabled(is_remove_ready); 90 | }.bind(this)); 91 | } 92 | 93 | init(){ 94 | this.reaction_menu.init(); 95 | } 96 | 97 | setFont(font){ 98 | super.setFont(font); 99 | this.reaction_menu.setFont(font); 100 | } 101 | } 102 | 103 | module.exports = OperationMenu; 104 | -------------------------------------------------------------------------------- /src/widgets/postbox/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | QPlainTextEdit, 5 | QPushButton, 6 | QKeyEvent, 7 | KeyboardModifier, 8 | Key, 9 | QComboBox, 10 | QCheckBox, 11 | Direction 12 | } = require('@nodegui/nodegui'); 13 | 14 | const Assets = require('../../assets.js'); 15 | const App = require('../../index.js'); 16 | 17 | class PostBox extends QWidget{ 18 | constructor(){ 19 | super(); 20 | 21 | this.assets = new Assets("Postbox"); 22 | this.filters = new Array(); 23 | this.visibilitys = [ 24 | { name: "public", text: "公開" }, 25 | { name: "home", text: "ホーム" }, 26 | { name: "followers", text: "フォロワー" }, 27 | { name: "specified", text: "ダイレクト" }, 28 | { name: "random", text: "ランダム" } 29 | ]; 30 | 31 | this.layout = new QBoxLayout(Direction.LeftToRight); 32 | 33 | this.right = new QWidget(); 34 | 35 | this.right_layout = new QBoxLayout(Direction.TopToBottom); 36 | 37 | this.post_button = new QPushButton(); 38 | this.text_input = new QPlainTextEdit(); 39 | this.visibility_select = new QComboBox(); 40 | this.is_local_check = new QCheckBox(); 41 | 42 | this.setObjectName('postArea'); 43 | this.setLayout(this.layout); 44 | this.setMinimumSize(120, 86); 45 | this.setMaximumSize(65535, 86); 46 | 47 | this.layout.setContentsMargins(0,5,0,5); 48 | this.layout.setSpacing(5); 49 | 50 | this.right.setObjectName('postSubArea'); 51 | this.right.setLayout(this.right_layout); 52 | 53 | this.right_layout.setContentsMargins(0,0,0,0); 54 | this.right_layout.setSpacing(2); 55 | 56 | this.text_input.setObjectName('postTextInput'); 57 | this.text_input.setReadOnly(false); 58 | this.text_input.setWordWrapMode(3); 59 | 60 | this.post_button.setObjectName('postButton'); 61 | this.post_button.setText('Post!'); 62 | 63 | this.visibility_select.setObjectName('postVisibilitySelect'); 64 | this.visibility_select.addItem(undefined, '公開'); 65 | this.visibility_select.addItem(undefined, 'ホーム'); 66 | this.visibility_select.addItem(undefined, 'フォロワー'); 67 | this.visibility_select.addItem(undefined, 'ダイレクト'); 68 | this.visibility_select.addItem(undefined, 'ランダム'); 69 | 70 | this.is_local_check.setObjectName('postIsLocalCheck'); 71 | this.is_local_check.setText('ローカルのみ'); 72 | 73 | this.post_button.setFixedSize(96,24); 74 | this.visibility_select.setFixedSize(96,24); 75 | this.is_local_check.setFixedSize(96,24); 76 | 77 | this.right_layout.addWidget(this.post_button); 78 | this.right_layout.addWidget(this.visibility_select); 79 | this.right_layout.addWidget(this.is_local_check); 80 | 81 | this.layout.addWidget(this.text_input, 1); 82 | this.layout.addWidget(this.right); 83 | 84 | this.update_placeholder(); 85 | 86 | this.post_button.addEventListener('clicked', this._post_note.bind(this)); 87 | this.text_input.addEventListener('KeyPress', this._key_parse.bind(this)); 88 | } 89 | 90 | async _post_note(){ 91 | for(var filter of this.filters){ 92 | filter(this.text_input); 93 | } 94 | 95 | var data = this._get_data(); 96 | 97 | if(!data.text){ 98 | App.status_label.setText('本文を入れてね'); 99 | return; 100 | } 101 | 102 | App.status_label.setText('投稿中...'); 103 | 104 | try{ 105 | await App.client.call('notes/create', data); 106 | this.clear(); 107 | App.status_label.setText("投稿成功!"); 108 | }catch(err){ 109 | console.log(err); 110 | App.statusLabel.setText(err.error.error.message); 111 | } 112 | 113 | this.update_placeholder(); 114 | } 115 | 116 | _key_parse(key){ 117 | var _key = new QKeyEvent(key); 118 | if(_key.modifiers() != KeyboardModifier.ControlModifier) return; 119 | if(!( 120 | _key.key() == Key.Key_Enter || 121 | _key.key() == Key.Key_Return 122 | )) return; 123 | 124 | this._post_note(); 125 | } 126 | 127 | _get_data(){ 128 | var visibility = 'public'; 129 | 130 | switch(this.visibility_select.currentText()){ 131 | case "公開": 132 | visibility = 'public'; 133 | break; 134 | case 'ホーム': 135 | visibility = 'home'; 136 | break; 137 | case 'フォロワー': 138 | visibility = 'followers'; 139 | break; 140 | case 'ダイレクト': 141 | visibility = 'specified'; 142 | break; 143 | case 'ランダム': 144 | var _visibility = ['public', 'home', 'followers', 'specified']; 145 | visibility = _visibility[Math.floor(Math.random() * _visibility.length)]; 146 | } 147 | 148 | var data = { 149 | text: this.text_input.toPlainText(), 150 | visibility: visibility, 151 | localOnly: this.is_local_check.isChecked() 152 | } 153 | 154 | return data; 155 | } 156 | 157 | clear(){ 158 | this.text_input.setPlainText(''); 159 | if(!App.settings.get("memory_visibility")) this.setVisibility(App.settings.get("start_visibility")); 160 | } 161 | 162 | update_placeholder(){ 163 | var _placeholder = this.assets.placeholder; 164 | var placeholder = _placeholder[Math.floor(Math.random() * _placeholder.length)]; 165 | this.text_input.setPlaceholderText(placeholder); 166 | } 167 | 168 | setup(font){ 169 | this.text_input.setFont(font); 170 | this.post_button.setFont(font); 171 | this.visibility_select.setFont(font); 172 | this.is_local_check.setFont(font); 173 | 174 | this.setVisibility(App.settings.get("start_visibility")); 175 | } 176 | 177 | add_post_filter(callback){ 178 | this.filters.push(callback); 179 | } 180 | 181 | filter(callback){ 182 | callback(this.text_input); 183 | } 184 | 185 | random_emoji(){ 186 | var emoji = App.random_emoji.exec(); 187 | this.text_input.insertPlainText(emoji); 188 | } 189 | 190 | setVisibility(vis){ 191 | for(var v of this.visibilitys){ 192 | if(v.name == vis){ 193 | this.visibility_select.setCurrentText(v.text); 194 | } 195 | } 196 | } 197 | } 198 | 199 | module.exports = PostBox; 200 | -------------------------------------------------------------------------------- /src/widgets/postview/content_box.js: -------------------------------------------------------------------------------- 1 | const { 2 | QScrollArea, 3 | QLabel, 4 | ScrollBarPolicy, 5 | AlignmentFlag, 6 | Shape, 7 | TextInteractionFlag 8 | } = require("@nodegui/nodegui"); 9 | 10 | class ContentBox extends QScrollArea{ 11 | constructor(){ 12 | super(); 13 | 14 | this.setObjectName("postViewContentArea"); 15 | this.setAlignment(AlignmentFlag.AlignTop|AlignmentFlag.AlignLeft); 16 | this.setHorizontalScrollBarPolicy(ScrollBarPolicy.ScrollBarAsNeeded); 17 | this.setVerticalScrollBarPolicy(ScrollBarPolicy.ScrollBarAsNeeded); 18 | this.setFrameShape(Shape.NoFrame); 19 | this.setWidgetResizable(true); 20 | 21 | this.content = new QLabel(); 22 | this.content.setObjectName('postViewBodyLabel'); 23 | this.content.setAlignment(AlignmentFlag.AlignTop|AlignmentFlag.AlignLeft); 24 | this.content.setWordWrap(true); 25 | this.content.setTextInteractionFlags( 26 | TextInteractionFlag.LinksAccessibleByMouse 27 | | TextInteractionFlag.TextSelectableByMouse 28 | ); 29 | this.content.setOpenExternalLinks(true); 30 | 31 | this.setWidget(this.content); 32 | } 33 | 34 | setText(text){ 35 | this.content.setText(text); 36 | } 37 | 38 | setFont(font){ 39 | this.content.setFont(font); 40 | } 41 | } 42 | 43 | module.exports = ContentBox; 44 | -------------------------------------------------------------------------------- /src/widgets/postview/flag_widget.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | QLabel, 5 | Direction, 6 | QPixmap, 7 | TransformationMode, 8 | AspectRatioMode 9 | } = require('@nodegui/nodegui'); 10 | 11 | const Assets = require("../../assets.js"); 12 | const Icons = new Assets('Icons'); 13 | 14 | const IconsSize = 14; 15 | 16 | class FlagWidget extends QWidget{ 17 | constructor(){ 18 | super(); 19 | 20 | this.layout = new QBoxLayout(Direction.LeftToRight); 21 | 22 | this.clip = new QLabel(); 23 | 24 | this.renote = new QLabel(); 25 | this.reply = new QLabel(); 26 | 27 | this._public = new QLabel() 28 | this.home = new QLabel(); 29 | this.lock = new QLabel(); 30 | this.direct = new QLabel(); 31 | 32 | this.local_home = new QLabel(); 33 | this.local_lock = new QLabel(); 34 | this.local_public = new QLabel() 35 | this.local_direct = new QLabel(); 36 | 37 | this.setLayout(this.layout); 38 | 39 | this.layout.setContentsMargins(0,0,0,0); 40 | this.layout.setSpacing(4); 41 | 42 | var clip_pix = new QPixmap(Icons.clip); 43 | 44 | var renote_pix = new QPixmap(Icons.renote); 45 | var reply_pix = new QPixmap(Icons.reply); 46 | 47 | var public_pix = new QPixmap(Icons._public); 48 | var home_pix = new QPixmap(Icons.home); 49 | var lock_pix = new QPixmap(Icons.lock); 50 | var direct_pix = new QPixmap(Icons.direct); 51 | 52 | var local_public_pix = new QPixmap(Icons.local_public); 53 | var local_home_pix = new QPixmap(Icons.local_home); 54 | var local_lock_pix = new QPixmap(Icons.local_lock); 55 | var local_direct_pix = new QPixmap(Icons.local_direct); 56 | 57 | this._public.setPixmap(public_pix.scaled( 58 | IconsSize, IconsSize, 59 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 60 | ) 61 | ); 62 | this.home.setPixmap(home_pix.scaled( 63 | IconsSize, IconsSize, 64 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 65 | ) 66 | ); 67 | this.lock.setPixmap(lock_pix.scaled( 68 | IconsSize, IconsSize, 69 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 70 | ) 71 | ); 72 | this.direct.setPixmap(direct_pix.scaled( 73 | IconsSize, IconsSize, 74 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 75 | ) 76 | ); 77 | 78 | this.local_public.setPixmap(local_public_pix.scaled( 79 | IconsSize, IconsSize, 80 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 81 | ) 82 | ); 83 | this.local_home.setPixmap(local_home_pix.scaled( 84 | IconsSize, IconsSize, 85 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 86 | ) 87 | ); 88 | this.local_lock.setPixmap(local_lock_pix.scaled( 89 | IconsSize, IconsSize, 90 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 91 | ) 92 | ); 93 | this.local_direct.setPixmap(local_direct_pix.scaled( 94 | IconsSize, IconsSize, 95 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 96 | ) 97 | ); 98 | 99 | this.renote.setPixmap(renote_pix.scaled( 100 | IconsSize, IconsSize, 101 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 102 | ) 103 | ); 104 | this.reply.setPixmap(reply_pix.scaled( 105 | IconsSize, IconsSize, 106 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 107 | ) 108 | ); 109 | 110 | this.clip.setPixmap(clip_pix.scaled( 111 | IconsSize, IconsSize, 112 | AspectRatioMode.KeepAspectRatio, TransformationMode.SmoothTransformation 113 | ) 114 | ); 115 | 116 | this.layout.addStretch(1); 117 | 118 | this.layout.addWidget(this.clip); 119 | 120 | this.layout.addWidget(this.renote); 121 | this.layout.addWidget(this.reply); 122 | 123 | this.layout.addWidget(this._public); 124 | this.layout.addWidget(this.home); 125 | this.layout.addWidget(this.lock); 126 | this.layout.addWidget(this.direct); 127 | 128 | this.layout.addWidget(this.local_public); 129 | this.layout.addWidget(this.local_home); 130 | this.layout.addWidget(this.local_lock); 131 | this.layout.addWidget(this.local_direct); 132 | 133 | this.layout.addStretch(1); 134 | } 135 | 136 | clear(){ 137 | this.clip.hide(); 138 | 139 | this.renote.hide(); 140 | this.reply.hide(); 141 | 142 | this._public.hide(); 143 | this.home.hide(); 144 | this.lock.hide(); 145 | this.direct.hide(); 146 | 147 | this.local_public.hide(); 148 | this.local_home.hide(); 149 | this.local_lock.hide(); 150 | this.local_direct.hide(); 151 | } 152 | 153 | setNoteFlag(note){ 154 | this.clear(); 155 | 156 | if(note.reply) this.reply.show(); 157 | if(note.renote) this.renote.show(); 158 | if(note.files[0] || (note.renote && note.renote.files[0])) this.clip.show(); 159 | 160 | switch(note.visibility){ 161 | case 'public': 162 | if(note.localOnly) this.local_public.show(); 163 | else this._public.show(); 164 | break; 165 | case 'home': 166 | if(note.localOnly) this.local_home.show(); 167 | else this.home.show(); 168 | break; 169 | case 'followers': 170 | if(note.localOnly) this.local_lock.show(); 171 | else this.lock.show(); 172 | break; 173 | case 'specified': 174 | if(note.localOnly) this.local_direct.show(); 175 | else this.direct.show(); 176 | break; 177 | default: 178 | break 179 | } 180 | } 181 | } 182 | 183 | module.exports = FlagWidget; 184 | -------------------------------------------------------------------------------- /src/widgets/postview/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | QLabel, 5 | QFont, 6 | QFontWeight, 7 | AlignmentFlag, 8 | TextInteractionFlag, 9 | Direction 10 | } = require('@nodegui/nodegui'); 11 | const dateFormat = require('dateformat'); 12 | 13 | const PostParser = require('../../tools/post_parser/index.js'); 14 | const NotificationParser = require('../../tools/notification_parser/index.js'); 15 | const IconLabel = require('../icon_label/index.js'); 16 | const ContentBox = require('./content_box.js'); 17 | const App = require("../../index.js"); 18 | const FlagWidget = require('./flag_widget.js'); 19 | 20 | class PostView extends QWidget{ 21 | constructor(){ 22 | super(); 23 | 24 | this.layout = new QBoxLayout(Direction.LeftToRight); 25 | 26 | this.left = new QWidget(); 27 | this.right = new QWidget(); 28 | this.right_info = new QWidget(); 29 | //this.reactions = new QWidget(); 30 | 31 | this.left_layout = new QBoxLayout(Direction.TopToBottom); 32 | this.right_layout = new QBoxLayout(Direction.TopToBottom); 33 | this.right_info_layout = new QBoxLayout(Direction.LeftToRight); 34 | //this.reactions_layout = new QBoxLayout(Direction.LeftToRight); 35 | 36 | this.avater = new IconLabel(52); 37 | this.flag = new QLabel(); 38 | 39 | this.name = new QLabel(); 40 | this.date = new QLabel(); 41 | this.content = new ContentBox(); 42 | this.post_flag = new FlagWidget(); 43 | 44 | this.post_parser = new PostParser(); 45 | 46 | this.setObjectName("postViewArea"); 47 | this.setLayout(this.layout); 48 | this.setMinimumSize(120, 120); 49 | this.setMaximumSize(65535, 120); 50 | 51 | this.layout.setContentsMargins(0,0,0,0); 52 | this.layout.setSpacing(5); 53 | 54 | this.left.setObjectName("postViewLeft"); 55 | this.left.setLayout(this.left_layout); 56 | 57 | this.left_layout.setContentsMargins(0,0,0,0); 58 | this.left_layout.setSpacing(5); 59 | 60 | this.right.setObjectName("postViewRight"); 61 | this.right.setLayout(this.right_layout); 62 | 63 | this.right_layout.setContentsMargins(0,0,0,0); 64 | this.right_layout.setSpacing(0); 65 | 66 | this.right_info.setObjectName('postViewRightTop'); 67 | this.right_info.setLayout(this.right_info_layout); 68 | 69 | this.right_info_layout.setContentsMargins(0,0,0,0); 70 | this.right_info_layout.setSpacing(5); 71 | 72 | //this.reactions.setObjectName('postViewReactionsArea'); 73 | //this.reactions.setLayout(this.reactions_layout); 74 | 75 | this.avater.setObjectName('postViewIconLabel'); 76 | 77 | this.flag.setObjectName('postViewUserFlagLabel'); 78 | this.flag.setAlignment(AlignmentFlag.AlignCenter); 79 | 80 | this.post_flag.setObjectName('postViewPostFlagLabel'); 81 | 82 | this.name.setObjectName('postViewUserNameLabel'); 83 | //this.name.setWordWrap(true); 84 | this.name.setMinimumSize(0, 16); 85 | this.name.setAlignment(AlignmentFlag.AlignVCenter|AlignmentFlag.AlignLeft); 86 | 87 | this.date.setObjectName('postViewDateLabel'); 88 | this.date.setWordWrap(false); 89 | this.date.setAlignment(AlignmentFlag.AlignVCenter|AlignmentFlag.AlignRight); 90 | this.date.setTextInteractionFlags(TextInteractionFlag.LinksAccessibleByMouse); 91 | this.date.setOpenExternalLinks(true); 92 | 93 | this.flag.setFixedSize(52, 12); 94 | this.date.setFixedSize(126, 16); 95 | 96 | this.left_layout.addWidget(this.avater); 97 | this.left_layout.addWidget(this.flag); 98 | this.left_layout.addWidget(this.post_flag); 99 | this.left_layout.addStretch(1); 100 | 101 | this.right_info_layout.addWidget(this.name, 1); 102 | this.right_info_layout.addWidget(this.date); 103 | 104 | this.right_layout.addWidget(this.right_info); 105 | this.right_layout.addWidget(this.content, 1); 106 | //this.right_layout.addWidget(this.reactions); 107 | 108 | this.layout.addWidget(this.left); 109 | this.layout.addWidget(this.right, 1); 110 | } 111 | 112 | _parse_user_flag(user){ 113 | var flag = ''; 114 | if(user.isLocked) flag += '鍵'; 115 | if(user.isBot) flag += '機'; 116 | if(user.isCat) flag += '猫'; 117 | if(user.isLady) flag += '嬢'; 118 | 119 | if(!flag || flag == "鍵") flag += '人'; 120 | 121 | return flag; 122 | } 123 | 124 | _parse_renote(note){ 125 | var result = this._parse_note_text(note); 126 | 127 | var renote = note.renote; 128 | if(!renote) return result; 129 | 130 | var r_text = `RN @${renote.user.acct} `; 131 | if(result) result += " "; 132 | 133 | if(renote.reply){ 134 | r_text += this._parse_reply(renote); 135 | }else if(renote.renote){ 136 | r_text += this._parse_renote(renote); 137 | }else{ 138 | r_text += this._parse_note_text(renote); 139 | } 140 | 141 | result += r_text; 142 | 143 | return result; 144 | } 145 | 146 | _parse_reply(note){ 147 | var result = this._parse_note_text(note); 148 | var reply = note.reply; 149 | if(!reply) return result; 150 | 151 | var re_text; 152 | 153 | if(reply.renote){ 154 | re_text = this._parse_renote(reply); 155 | }else if(reply.reply){ 156 | re_text = this._parse_reply(reply); 157 | }else{ 158 | re_text = this._parse_note_text(reply); 159 | } 160 | 161 | result += `\nRE: ${re_text}`; 162 | 163 | return result; 164 | } 165 | 166 | _parse_note_text(note){ 167 | var result = ''; 168 | if(note.cw){ 169 | result = this._parse_search(note, true) + '\n------------CW------------\n'; 170 | } 171 | result += this._parse_search(note, false); 172 | 173 | return result; 174 | } 175 | 176 | _parse_search(note, is_cw){ 177 | var result = ''; 178 | var from = is_cw ? note.no_emoji_cw : note.no_emoji_text; 179 | var from_em = is_cw ? note.cw : note.text; 180 | 181 | if(!from) return ""; 182 | 183 | var from_arr = from.split('\n'); 184 | var from_em_arr = from_em.split('\n'); 185 | 186 | for(var i = 0; from_arr.length > i; i++){ 187 | var text = from_arr[i]; 188 | var _t = text; 189 | if(text.search(/^(^.+) (\[search\]|検索)$/i) != -1){ 190 | _t = text.match(/^(.+) (\[search\]|検索)$/i)[1]; 191 | _t = encodeURIComponent(_t); 192 | text = text.replace(/ (\[search\]|検索)$/i, ''); 193 | text = text.replace(/:/gi, "<%3A>"); 194 | _t = `search://${App.settings.get("search_engine")}${_t} [${text} 検索]`; 195 | }else{ 196 | _t = from_em_arr[i]; 197 | } 198 | result += _t + "\n"; 199 | } 200 | 201 | return result; 202 | } 203 | _parse_note_content(note){ 204 | var text; 205 | 206 | if(note.renote){ 207 | text = this._parse_renote(note); 208 | }else if(note.reply){ 209 | text = this._parse_reply(note); 210 | }else{ 211 | text = this._parse_note_text(note); 212 | } 213 | text = this.post_parser.parse(text); 214 | 215 | return text; 216 | } 217 | 218 | setNote(note){ 219 | // avater 220 | if(note.user.avater) this.avater.setPixmap(note.user.avater); 221 | 222 | // flags 223 | this.flag.setText(this._parse_user_flag(note.user)); 224 | this.post_flag.setNoteFlag(note); 225 | 226 | // name 227 | var name; 228 | if(note.user.name){ 229 | name = `${note.user.name}/${note.user.acct}`; 230 | }else{ 231 | name = note.user.acct; 232 | } 233 | this.name.setText(name); 234 | 235 | // date 236 | var date = dateFormat(note.createdAt, 'yyyy/mm/dd HH:MM:ss'); 237 | var date_url = `https://${App.client.host}/notes/${note.id}`; 238 | 239 | this.date.setText(`${date}`); 240 | 241 | // content 242 | var text = this._parse_note_content(note); 243 | this.content.setText(text); 244 | } 245 | 246 | setNotification(notification){ 247 | // avater 248 | if(notification.user.avater) this.avater.setPixmap(notification.user.avater); 249 | 250 | // flags 251 | this.flag.setText(this._parse_user_flag(notification.user)); 252 | this.post_flag.clear(); 253 | 254 | // name 255 | var name; 256 | if(notification.user.name){ 257 | name = `${notification.user.name}/${notification.user.acct}`; 258 | }else{ 259 | name = notification.user.acct; 260 | } 261 | this.name.setText(name); 262 | 263 | // date 264 | this.date.setText(dateFormat(notification.createdAt, 'yyyy/mm/dd HH:MM:ss')); 265 | 266 | // content 267 | var text = NotificationParser.gen_desc_text(notification, 'postview'); 268 | text = this.post_parser.parse(text); 269 | this.content.setText(text); 270 | } 271 | 272 | setFont(fontname){ 273 | const font = new QFont(fontname, 9); 274 | const NameFont = new QFont(fontname, 9, QFontWeight.Bold); 275 | 276 | this.flag.setFont(font); 277 | this.name.setFont(NameFont); 278 | this.date.setFont(font); 279 | this.content.setFont(font); 280 | } 281 | } 282 | 283 | module.exports = PostView; 284 | -------------------------------------------------------------------------------- /src/widgets/reaction_emoji_input_window/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QDialog, 3 | QWidget, 4 | QBoxLayout, 5 | Direction, 6 | QPushButton, 7 | QLabel, 8 | QLineEdit 9 | } = require('@nodegui/nodegui'); 10 | 11 | class ReactionEmojiInputWindow extends QDialog{ 12 | constructor(){ 13 | super(); 14 | 15 | this.layout = new QBoxLayout(Direction.TopToBottom); 16 | 17 | this.input_area = new QWidget(); 18 | this.button_area = new QWidget(); 19 | 20 | this.input_area_layout = new QBoxLayout(Direction.TopToBottom); 21 | this.button_area_layout = new QBoxLayout(Direction.LeftToRight); 22 | 23 | this.message_label = new QLabel(); 24 | this.input = new QLineEdit(); 25 | 26 | this.ok_button = new QPushButton(); 27 | this.cancel_button = new QPushButton(); 28 | 29 | this.setLayout(this.layout); 30 | this.setWindowTitle('絵文字入力 - TenCha'); 31 | this.resize(300, 100); 32 | 33 | this.layout.setContentsMargins(5,5,5,5); 34 | this.layout.setSpacing(5); 35 | 36 | this.input_area.setLayout(this.input_area_layout); 37 | 38 | this.button_area.setLayout(this.button_area_layout); 39 | 40 | this.input_area_layout.setContentsMargins(0,0,0,0); 41 | this.input_area_layout.setSpacing(5); 42 | 43 | this.button_area_layout.setContentsMargins(0,0,0,0); 44 | this.button_area_layout.setSpacing(5); 45 | 46 | this.message_label.setText('絵文字を入力:'); 47 | 48 | this.input.setPlaceholderText(':misskey:'); 49 | 50 | this.ok_button.setText('おk'); 51 | this.ok_button.setDefault(true); 52 | 53 | this.cancel_button.setText('やめとく'); 54 | 55 | this.input_area_layout.addWidget(this.message_label); 56 | this.input_area_layout.addWidget(this.input); 57 | 58 | this.button_area_layout.addStretch(1); 59 | this.button_area_layout.addWidget(this.cancel_button); 60 | this.button_area_layout.addWidget(this.ok_button); 61 | 62 | this.layout.addWidget(this.input_area); 63 | this.layout.addWidget(this.button_area); 64 | 65 | this.cancel_button.addEventListener('clicked', function(){ 66 | this.setResult(0); 67 | this.close(); 68 | }.bind(this)); 69 | 70 | this.ok_button.addEventListener('clicked', function(){ 71 | this.setResult(1); 72 | this.close(); 73 | }.bind(this)); 74 | 75 | this.input.addEventListener('returnPressed', function(){ 76 | this.setResult(1); 77 | this.close(); 78 | }.bind(this)); 79 | } 80 | 81 | // QFont 82 | setFont(font){ 83 | this.message_label.setFont(font); 84 | this.input.setFont(font); 85 | this.ok_button.setFont(font); 86 | this.cancel_button.setFont(font); 87 | } 88 | 89 | getResult(){ 90 | var result = ''; 91 | if(this.result() == 1) result = this.input.text(); 92 | this.input.clear(); 93 | return result; 94 | } 95 | } 96 | 97 | module.exports = ReactionEmojiInputWindow; 98 | -------------------------------------------------------------------------------- /src/widgets/reaction_menu/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMenu, 3 | QAction, 4 | QIcon, 5 | QFont 6 | } = require('@nodegui/nodegui'); 7 | const { parse } = require('twemoji-parser'); 8 | 9 | const App = require('../../index.js'); 10 | const ReactionEmojiInputWindow = require('../reaction_emoji_input_window'); 11 | 12 | class ReactionMenu extends QMenu{ 13 | constructor(){ 14 | super(); 15 | 16 | this.reactions = []; 17 | 18 | this.emoji_input_window = new ReactionEmojiInputWindow(); 19 | 20 | this.code_input_action = new QAction(); 21 | this.unreaction_action = new QAction(); 22 | 23 | this.code_input_action.setText('絵文字を入力...'); 24 | this.code_input_action.addEventListener('triggered', function(){ 25 | this.emoji_input_window.exec(); 26 | }.bind(this) 27 | ); 28 | 29 | this.unreaction_action.setText('外す'); 30 | this.unreaction_action.addEventListener('triggered', function(){ 31 | App.post_action.unreaction(); 32 | }); 33 | 34 | this.emoji_input_window.addEventListener('Close', function(){ 35 | var result = this.emoji_input_window.getResult(); 36 | if(!result) return; 37 | App.post_action.reaction(result); 38 | }.bind(this)); 39 | } 40 | 41 | init(){ 42 | this.reload(); 43 | this.emoji_input_window.setFont(new QFont(App.settings.get('font'), 9)); 44 | } 45 | 46 | clear(){ 47 | for(var action of this.reactions) this.removeAction(action); 48 | this.removeAction(this.code_input_action); 49 | this.removeAction(this.unreaction_action); 50 | } 51 | 52 | _parse_mk_emojis(shortcode){ 53 | shortcode = shortcode.replace(/:/g, ''); 54 | return App.client.emojis.find((v) => v.name == shortcode); 55 | } 56 | 57 | async reload(){ 58 | this.clear(); 59 | 60 | var reactions = Array.from(App.settings.get('reaction_picker_emojis')); 61 | 62 | var emojis = []; 63 | var is_code = false; 64 | var shortcode = ""; 65 | 66 | for(var emoji of reactions){ 67 | if(emoji == ":"){ 68 | if(!is_code){ 69 | is_code = true; 70 | }else{ 71 | is_code = false; 72 | emojis.push(`:${shortcode}:`); 73 | shortcode = ''; 74 | } 75 | }else if(is_code){ 76 | shortcode += emoji; 77 | }else{ 78 | emojis.push(emoji); 79 | } 80 | } 81 | 82 | for(var emoji of emojis){ 83 | var twemojis = parse(`text ${emoji} text`); 84 | var mk_emojis = this._parse_mk_emojis(emoji); 85 | 86 | var reaction_func = function(emoji){ 87 | App.post_action.reaction(emoji); 88 | }.bind(this, emoji); 89 | 90 | var action = new QAction(); 91 | this.addAction(action); 92 | 93 | if(twemojis.length > 0){ 94 | var _emoji = await App.emoji_parser.cache.get(twemojis[0]); 95 | var icon = new QIcon(_emoji.filename); 96 | 97 | action.setIcon(icon); 98 | }else if(mk_emojis){ 99 | var _emoji = await App.emoji_parser.cache.get(mk_emojis); 100 | var icon = new QIcon(_emoji.filename); 101 | 102 | action.setIcon(icon); 103 | }else{ 104 | action.setText(emoji); 105 | } 106 | 107 | action.addEventListener('triggered', reaction_func); 108 | this.reactions.push(action); 109 | } 110 | 111 | this.addSeparator(this.code_input_action); 112 | this.addAction(this.code_input_action); 113 | 114 | this.addSeparator(this.unreaction_action); 115 | this.addAction(this.unreaction_action); 116 | } 117 | 118 | exec(pos){ 119 | super.exec(pos); 120 | } 121 | } 122 | 123 | module.exports = ReactionMenu; 124 | -------------------------------------------------------------------------------- /src/widgets/setting_window/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | WindowType, 6 | QPushButton 7 | } = require('@nodegui/nodegui'); 8 | 9 | const App = require('../../index.js'); 10 | const SettingWidget = require('./setting_widget.js'); 11 | 12 | class SettingWindow extends QWidget{ 13 | constructor(){ 14 | super(); 15 | 16 | this.layout = new QBoxLayout(Direction.TopToBottom); 17 | 18 | this.button_area = new QWidget(); 19 | 20 | this.button_area_layout = new QBoxLayout(Direction.LeftToRight); 21 | 22 | this.confirm_button = new QPushButton(); 23 | this.cancel_button = new QPushButton(); 24 | 25 | this.widgets = {}; 26 | for(var se of App.settings.get_settings_list()){ 27 | this.widgets[se.id] = new SettingWidget(se); 28 | } 29 | 30 | this.setLayout(this.layout); 31 | this.setWindowFlag(WindowType.Window, true); 32 | this.setWindowTitle('設定 - TenCha'); 33 | this.resize(360, 430); 34 | 35 | this.layout.setContentsMargins(5,5,5,5); 36 | this.layout.setSpacing(5); 37 | 38 | this.button_area.setLayout(this.button_area_layout); 39 | 40 | this.button_area_layout.setContentsMargins(0,0,0,0); 41 | this.button_area_layout.setSpacing(5); 42 | 43 | this.confirm_button.setText('OK'); 44 | this.confirm_button.addEventListener('clicked', this.write.bind(this)); 45 | 46 | this.cancel_button.setText('キャンセル'); 47 | this.cancel_button.addEventListener('clicked', super.close.bind(this)); 48 | 49 | this.button_area_layout.addStretch(1); 50 | this.button_area_layout.addWidget(this.confirm_button); 51 | this.button_area_layout.addWidget(this.cancel_button); 52 | 53 | for(var wi of Object.keys(this.widgets)) this.layout.addWidget(this.widgets[wi]); 54 | 55 | this.layout.addWidget(this.button_area); 56 | } 57 | 58 | exec(){ 59 | this.clear(); 60 | super.show(); 61 | } 62 | 63 | clear(){ 64 | for(var se of App.settings.get_settings_list()){ 65 | this.widgets[se.id].setValue(se); 66 | } 67 | } 68 | 69 | write(){ 70 | for(var val of Object.keys(this.widgets)){ 71 | var value = this.widgets[val].value(); 72 | App.settings.set(value.id, value.value); 73 | } 74 | super.close(); 75 | } 76 | } 77 | 78 | module.exports = SettingWindow; 79 | -------------------------------------------------------------------------------- /src/widgets/setting_window/setting_widget.js: -------------------------------------------------------------------------------- 1 | const { 2 | QWidget, 3 | QBoxLayout, 4 | Direction, 5 | QLabel, 6 | QComboBox, 7 | QCheckBox, 8 | QSpinBox, 9 | QLineEdit, 10 | QFileDialog, 11 | QPushButton, 12 | FileMode, 13 | QFont 14 | } = require('@nodegui/nodegui'); 15 | 16 | const App = require('../../index.js'); 17 | 18 | class SettingWidget extends QWidget{ 19 | constructor(setting){ 20 | super(); 21 | 22 | this.layout = new QBoxLayout(Direction.LeftToRight); 23 | 24 | this.font = new QFont(App.settings.get("font"), 9); 25 | 26 | this.setLayout(this.layout); 27 | 28 | this.layout.setContentsMargins(0,0,0,0); 29 | this.layout.setSpacing(5); 30 | 31 | this.id = setting.id; 32 | this.type = setting.type; 33 | 34 | switch(this.type){ 35 | case "Bool": 36 | this.checkbox = new QCheckBox(); 37 | 38 | this.checkbox.setText(setting.name); 39 | this.checkbox.setFont(this.font); 40 | 41 | this.layout.addWidget(this.checkbox, 1); 42 | 43 | break; 44 | case "Num": 45 | this.label = new QLabel(); 46 | this.spinbox = new QSpinBox(); 47 | 48 | this.label.setText(setting.name); 49 | this.label.setFont(this.font); 50 | 51 | if(setting.min) this.spinbox.setMinimum(setting.min); 52 | else this.spinbox.setMinimum(1); 53 | if(setting.max) this.spinbox.setMaximum(setting.max); 54 | else this.spinbox.setMaximum(99999999); 55 | this.spinbox.setFont(this.font); 56 | 57 | this.layout.addWidget(this.label, 1); 58 | this.layout.addWidget(this.spinbox); 59 | break; 60 | case "String": 61 | this.label = new QLabel(); 62 | this.lineedit = new QLineEdit(); 63 | 64 | this.label.setText(setting.name); 65 | this.label.setFont(this.font); 66 | 67 | this.lineedit.setFont(this.font); 68 | 69 | this.layout.addWidget(this.label, 1); 70 | this.layout.addWidget(this.lineedit); 71 | break; 72 | case "Path": 73 | this.label = new QLabel(); 74 | this.lineedit = new QLineEdit(); 75 | this.pushbutton = new QPushButton(); 76 | 77 | this.label.setText(setting.name); 78 | this.label.setFont(this.font); 79 | 80 | this.lineedit.setFont(this.font); 81 | 82 | this.pushbutton.setText("参照"); 83 | this.pushbutton.setFont(this.font); 84 | this.pushbutton.addEventListener('clicked', this.select_file.bind(this)); 85 | 86 | this.layout.addWidget(this.label, 1); 87 | this.layout.addWidget(this.lineedit); 88 | this.layout.addWidget(this.pushbutton); 89 | break; 90 | case "Select": 91 | this.label = new QLabel(); 92 | this.combobox = new QComboBox(); 93 | 94 | this.select_values = setting.select_values; 95 | 96 | this.label.setText(setting.name); 97 | this.label.setFont(this.font); 98 | 99 | this.combobox.setFont(this.font); 100 | for(var val of setting.select_values) this.combobox.addItem(undefined, val.text); 101 | 102 | this.layout.addWidget(this.label, 1); 103 | this.layout.addWidget(this.combobox); 104 | break; 105 | } 106 | 107 | this.setValue(setting); 108 | } 109 | 110 | select_file(){ 111 | const file_dialog = new QFileDialog(); 112 | file_dialog.setFileMode(FileMode.ExistingFile); 113 | file_dialog.exec(); 114 | 115 | if(file_dialog.result() != 1) return; 116 | 117 | this.lineedit.setText(file_dialog.selectedFiles()[0]); 118 | return; 119 | } 120 | 121 | value(){ 122 | var result = {}; 123 | result.id = this.id; 124 | 125 | switch(this.type){ 126 | case "Bool": 127 | result.value = this.checkbox.isChecked(); 128 | break; 129 | case "Num": 130 | result.value = this.spinbox.value(); 131 | break; 132 | case "String": 133 | case "Path": 134 | result.value = this.lineedit.text(); 135 | break; 136 | case "Select": 137 | for(var val of this.select_values){ 138 | if(this.combobox.currentText() == val.text) result.value = val.id; 139 | } 140 | break; 141 | } 142 | 143 | return result; 144 | } 145 | 146 | setValue(setting){ 147 | switch(this.type){ 148 | case "Bool": 149 | this.checkbox.setChecked(setting.value); 150 | break; 151 | case "Num": 152 | this.spinbox.setValue(setting.value); 153 | break; 154 | case "String": 155 | case "Path": 156 | this.lineedit.setText(setting.value); 157 | break; 158 | case "Select": 159 | for(var val of this.select_values){ 160 | if(setting.value == val.id) this.combobox.setCurrentText(val.text); 161 | } 162 | break; 163 | } 164 | 165 | } 166 | } 167 | 168 | module.exports = SettingWidget; 169 | -------------------------------------------------------------------------------- /src/widgets/timeline_menu/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | QMenu, 3 | QAction 4 | } = require('@nodegui/nodegui'); 5 | 6 | const App = require('../../index.js'); 7 | 8 | class TimelineMenu extends QMenu{ 9 | constructor(){ 10 | super(); 11 | 12 | this.reload_action = new QAction(); 13 | 14 | this.setTitle('タイムライン'); 15 | 16 | this.reload_action.setText('更新'); 17 | this.reload_action.addEventListener('triggered', () => App.timeline.start_load_tl()); 18 | 19 | 20 | this.addAction(this.reload_action); 21 | } 22 | } 23 | 24 | module.exports = TimelineMenu; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "allowJs": true, 7 | "checkJs": false, 8 | "outDir": "./dist", 9 | "sourceMap": true, 10 | "strict": true, 11 | "alwaysStrict": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true 14 | }, 15 | "include": ["**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /user_id_blocks.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "block_user_ids":[ 3 | { 4 | "id": "followbot", 5 | "regexp": true, 6 | "ignore_case": true 7 | } 8 | ] 9 | } 10 | 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: process.NODE_ENV || "development", 7 | entry: "./src", 8 | externals: [/node_modules/, 'bufferutil', 'utf-8-validate'], 9 | target: "node", 10 | output: { 11 | path: path.resolve(__dirname, "dist"), 12 | filename: "index.js" 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: "ts-loader", 19 | exclude: /node_modules/ 20 | }, 21 | { 22 | test: /\.(png|jpe?g|gif|svg|css)$/i, 23 | use: [ 24 | { 25 | loader: "file-loader", 26 | options: { publicPath: "dist" } 27 | } 28 | ] 29 | }, 30 | { 31 | test: /\.node$/, 32 | use: [ 33 | { 34 | loader: "native-addon-loader", 35 | options: { name: "[name]-[hash].[ext]" } 36 | } 37 | ] 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | extensions: [".tsx", ".ts", ".js", ".jsx"] 43 | }, 44 | plugins: [ 45 | new CleanWebpackPlugin(), 46 | new CopyPlugin({ 47 | patterns: [ 48 | { from: './node_modules/node-notifier/vendor/snoreToast/snoretoast-x86.exe', to: './snoreToast/snoretoast-x86.exe' }, 49 | { from: './node_modules/node-notifier/vendor/snoreToast/snoretoast-x64.exe', to: './snoreToast/snoretoast-x64.exe' }, 50 | ] 51 | }) 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /word_blocks.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "block_words":[ 3 | { 4 | "word": "ミュートしたいワード", 5 | "regexp": false, 6 | "ignore_case": false 7 | }, 8 | { 9 | "word": "^ミュートしたいワード$", 10 | "regexp": true, 11 | "ignore_case": false 12 | }, 13 | { 14 | "word": "[a-z0-9]+", 15 | "regexp": true, 16 | "ignore_case": true 17 | } 18 | ] 19 | } 20 | 21 | --------------------------------------------------------------------------------