├── .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 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fcoke12103%2FTenCha?ref=badge_shield)
3 | 
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 | [](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 |
--------------------------------------------------------------------------------