├── .gitignore ├── INSTALL.md ├── LICENSE.md ├── README.md ├── lint_yml └── .jshintrc ├── sideci.yml ├── src ├── css │ └── tsumiqiita.css ├── index.html ├── index.js ├── js │ └── functions.js └── package.json └── ss.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | build/ 4 | token.txt 5 | src/css/style.css 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # インストール方法 2 | 3 | 1. gitをインストール () 4 | 1. npmをインストール () 5 | 1. npmを以下の手順で [npm v6.0.0](https://nodejs.org/ja/) 以上にアップデート 6 | ```sh 7 | ## for macOS and Linux 8 | sudo npm i -g npm 9 | 10 | ## for Windows 11 | npm i -g npm 12 | ``` 13 | 1. リポジトリのインストール 14 | ```sh 15 | git clone https://github.com/hidao80/TsumiQiita.git 16 | cd TsumiQiita/src 17 | npm i 18 | npm run build 19 | ``` 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 hidao80 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TsumiQiitaとは 2 | 3 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE.md) 4 | [![npm v6.0.0](https://img.shields.io/badge/node-6.0.0-blue.svg)](https://nodejs.org/ja/) 5 | ![hidao quality](https://img.shields.io/badge/hidao-quality-orange.svg) 6 | 7 | MarkdownファイルをQiitaに投稿するelectron製デスクトップアプリです。\ 8 | 下書きに入りきらない**積みQiita**をするためにご利用ください。 9 | 10 | 11 | 12 | node.jsでビルドします。**インストール方法は[INSTALL.md](INSTALL.md)**を参照してください。 13 | 14 | ビルド済みは[**こちら**](https://github.com/hidao80/TsumiQiita/releases) 15 | 16 | ## 主な機能 17 | 18 | - 選択したファイルをQiitaに状態を指定して投稿(限定公開投稿可) 19 | - 選択したMarkdownファイルをプレビュー 20 | - PCのストレージから任意のMarkdownファイルを開く 21 | - 新規Markdownファイルの作成 22 | - Markdownで編集 23 | - リアルタイムプレビュー 24 | - 最後に編集してから3秒後に自動保存 25 | - タグをつけて投稿 26 | - 同一フォルダ内の Markdown ファイルをリストにして常時表示 27 | - Windows / macOS / Linux でほぼ同一の UI を提供 28 | - twitter連携オプション可 29 | - Qiita team用オプション可 30 | 31 | 以上の機能により、**記事の草案をローカルに際限なく貯めることを可能にします**。 32 | 33 | ### ユースケース 34 | 35 | 1. クラウドストレージにMarkdownファイルを保存し、任意の端末から編集して置いて本アプリで投稿する。 36 | 1. リアルタイムプレビューを見ながら下書きをする。 37 | 38 | ## 注意 39 | 40 | 1. プレビューはQiitaやGitHubでの表示と異なります。あくまで参考程度とご理解ください。 41 | 1. 既存の記事とまったく同じ記事が投稿できます。上書きされません。 42 | 43 | ### Qiita記事のヘッダ書式 44 | 45 | --- 46 | title: 記事のタイトル 47 | tags: タグA:0.0.1 タグB 48 | tweet: false 49 | private: false 50 | coediting: false 51 | group_url_name: dev 52 | --- 53 | # 見出し1 54 | 本文... 55 | 56 | フィールド | 必須 | Type | 説明 57 | ---|---|---|--- 58 | title | 〇 | string | 記事のタイトル 59 | tags | 〇 | string | タグ。バージョン併記可。5つまで。 60 | tweet | | true/false | Twitterに投稿するかどうか (Twitter連携を有効化している場合のみ有効) 61 | private | | true/false | 限定共有状態かどうかを表すフラグ (Qiita Teamでは無効) 62 | coedition | | true/false | この記事が共同更新状態かどうか (Qiita Teamでのみ有効) 63 | group_url_name | | null/string | この投稿を公開するグループの url_name (null で全体に公開。Qiita Teamでのみ有効) 64 | 65 | ### TODO 66 | 67 | - [ ] 同期スクロールでプレビューが一番下まで表示されない 68 | - [x] ~~対象フォルダ選択~~ 69 | - [x] ~~Markdownファイル選択~~ 70 | - [x] ~~Markdownファイルプレビュー~~ 71 | - [x] ~~プレビュー中のファイルをQiitaへ限定公開として投稿する~~ 72 | - [x] ~~編集~~ 73 | - [x] ~~新ファイル作成~~ 74 | - [ ] Qiita CSS 適用 75 | - [x] ~~最後に編集してから3秒後に自動保存する~~ 76 | - [x] ~~タグを登録できる~~ 77 | - [x] ~~シンタックスハイライトに対応~~ 78 | - [x] ~~CommonMarkdown に作表と打ち消し線、チェックボックス表示機能を追加~~ 79 | - [x] ~~Qiita風のコードブロック(ファイル名表示機能付)~~ 80 | - [x] ~~投稿できない~~ -------------------------------------------------------------------------------- /lint_yml/.jshintrc: -------------------------------------------------------------------------------- 1 | 2 | // NOTE: I added the .js extension to this gist so it would have syntax highlighting. This file should have NO file extension 3 | 4 | { 5 | // Settings 6 | "passfail" : false, // Stop on first error. 7 | "maxerr" : 100, // Maximum error before stopping. 8 | "esversion" : 6, 9 | 10 | // Predefined globals whom JSHint will ignore. 11 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 12 | 13 | "node" : true, 14 | "rhino" : false, 15 | "couch" : false, 16 | "wsh" : true, // Windows Scripting Host. 17 | 18 | "jquery" : true, 19 | "ender" : true, 20 | "prototypejs" : false, 21 | "mootools" : false, 22 | "dojo" : false, 23 | 24 | "predef" : [ // Custom globals. 25 | //"exampleVar", 26 | //"anotherCoolGlobal", 27 | //"iLoveDouglas" 28 | ], 29 | 30 | 31 | // Development. 32 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 33 | "devel" : false, // Allow developments statements e.g. `console.log();`. 34 | 35 | 36 | // ECMAScript 5. 37 | "es5" : false, // Allow ECMAScript 5 syntax. 38 | "strict" : false, // Require `use strict` pragma in every file. 39 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 40 | 41 | 42 | // The Good Parts. 43 | "asi" : true, // Tolerate Automatic Semicolon Insertion (no semicolons). 44 | "laxbreak" : true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 45 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 46 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 47 | "curly" : true, // Require {} for every new block or scope. 48 | "eqeqeq" : true, // Require triple equals i.e. `===`. 49 | "eqnull" : false, // Tolerate use of `== null`. 50 | "evil" : false, // Tolerate use of `eval`. 51 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 52 | "forin" : false, // Tolerate `for in` loops without `hasOwnPrototype`. 53 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 54 | "latedef" : true, // Prohipit variable use before definition. 55 | "loopfunc" : false, // Allow functions to be defined within loops. 56 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 57 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 58 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 59 | "scripturl" : true, // Tolerate script-targeted URLs. 60 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 61 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 62 | "undef" : true, // Require all non-global variables be declared before they are used. 63 | 64 | 65 | // Personal styling preferences. 66 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 67 | "noempty" : true, // Prohibit use of empty blocks. 68 | "nonew" : true, // Prohibit use of constructors for side-effects. 69 | "nomen" : true, // Prohibit use of initial or trailing underbars in names. 70 | "onevar" : false, // Allow only one `var` statement per function. 71 | "plusplus" : false, // Prohibit use of `++` & `--`. 72 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 73 | "trailing" : true, // Prohibit trailing whitespaces. 74 | "white" : false, // Check against strict whitespace and indentation rules. 75 | "indent" : 2 // Specify indentation spacing 76 | } -------------------------------------------------------------------------------- /sideci.yml: -------------------------------------------------------------------------------- 1 | linter: 2 | jshint: 3 | options: 4 | config: lint_yml/.jshintrc 5 | -------------------------------------------------------------------------------- /src/css/tsumiqiita.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | width: 100%; 6 | overflow: hidden; 7 | } 8 | body { 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | width: 100%; 13 | overflow: hidden; 14 | display: flex; 15 | } 16 | #working-area { 17 | width: 100%; 18 | } 19 | #preview { 20 | overflow-x: hidden; 21 | overflow-y: hidden; 22 | height: 50%; 23 | margin: 0; 24 | width: calc(100% - 2px); 25 | } 26 | #tags-label { 27 | color: gray; 28 | font-size: 1rem; 29 | height: 1.1rem; 30 | } 31 | ::selection { 32 | background: #bbf; 33 | } 34 | #tsumiqiita-editor { 35 | overflow-x: scroll; 36 | overflow-y: scroll; 37 | border: 1px solid darkgray; 38 | height: calc(50% - 20px - 1.1rem); 39 | margin: 0; 40 | resize: none; 41 | width: calc(100% - 22px); 42 | } 43 | ::-webkit-scrollbar { 44 | width: 20px; 45 | height: 20px; 46 | } 47 | #controls { 48 | width: 30%; 49 | } 50 | #header { 51 | display: flex; 52 | width: 30%; 53 | height: 2rem; 54 | font-size: 2rem; 55 | margin: 1rem; 56 | } 57 | #files { 58 | overflow-x: hidden; 59 | overflow-y: scroll; 60 | height: calc(calc(100% - 2rem) * 0.9); 61 | width: calc(100% - 20px); 62 | } 63 | .files-item-radio { 64 | display: none; 65 | } 66 | .files-item-div { 67 | border-bottom: 1px solid #eee; 68 | margin:1rem; 69 | } 70 | input[type="radio"]:checked + .files-item-label { 71 | color: #fff; 72 | background-color: deepskyblue; 73 | } 74 | .files-item-label { 75 | display: block; 76 | font-size: 1.3rem; 77 | margin: 0; 78 | height: 100%; 79 | width: 100%; 80 | white-space: nowrap; 81 | } 82 | .menu-item-radio { 83 | display: none; 84 | } 85 | .menu-item-div { 86 | border-bottom: 1px solid #eee; 87 | margin:1rem; 88 | } 89 | input[type="radio"]:checked + .menu-item-label { 90 | color: #fff; 91 | background-color: deepskyblue; 92 | } 93 | .menu-item-label { 94 | display: block; 95 | font-size: 1.3rem; 96 | margin: 0; 97 | height: 100%; 98 | width: 100%; 99 | white-space: nowrap; 100 | } 101 | div.target-dir { 102 | border-bottom: 1px solid #eee; 103 | font-size: 1.5rem; 104 | margin: 0; 105 | height: 5%; 106 | white-space: nowrap; 107 | width: 100%; 108 | } 109 | pre { 110 | background-color: #272822; 111 | color: #F8F8F2; 112 | } 113 | dialog > span { 114 | font-size: 1rem; 115 | } 116 | .code-frame { 117 | margin: 1.2em 3px 3px 3px; 118 | position: relative; 119 | } 120 | .code-lang { 121 | background-color: #888; 122 | font-size: 1em; 123 | left: 0; 124 | margin: 0; 125 | padding: 0 0.5em 0 0; 126 | position: absolute; 127 | top: 0; 128 | transform: translateY(-1.7em) translateX(-10px); 129 | } 130 | 131 | table { 132 | margin: 1em 0; 133 | border-left: solid 1px #e6e6e6;; 134 | border-top: solid 1px #e6e6e6;; 135 | border-collapse: collapse; 136 | } 137 | 138 | td, 139 | th { 140 | border-bottom: solid 1px #e6e6e6; 141 | border-right: solid 1px #e6e6e6;; 142 | padding: 8px 10px; 143 | word-wrap: break-word; 144 | } 145 | 146 | thead td, 147 | th { 148 | font-weight: 700; 149 | background-color: #efefef; 150 | } 151 | 152 | tbody:nth-child(odd) tr { 153 | background-color: rgba(#000, .03); 154 | } 155 | 156 | 157 | /*チェックボックス等は非表示に*/ 158 | .nav-unshown { 159 | display:none; 160 | } 161 | /*アイコンのスペース*/ 162 | #nav-open { 163 | display: inline-block; 164 | width: 60px; 165 | height: 44px; 166 | vertical-align: middle; 167 | } 168 | /*ハンバーガーアイコンをCSSだけで表現*/ 169 | #nav-open span, #nav-open span:before, #nav-open span:after { 170 | position: absolute; 171 | height: 7px;/*線の太さ*/ 172 | width: 40px;/*長さ*/ 173 | border-radius: 3px; 174 | background: #555; 175 | display: block; 176 | content: ''; 177 | cursor: pointer; 178 | } 179 | #nav-open span { 180 | margin-left: 1rem; 181 | } 182 | #nav-open span:before { 183 | bottom: -14px; 184 | } 185 | #nav-open span:after { 186 | bottom: -28px; 187 | } 188 | /*中身*/ 189 | #nav-content { 190 | overflow: auto; 191 | position: fixed; 192 | top: 0; 193 | left: 0; 194 | z-index: 9999;/*最前面に*/ 195 | width: 90%;/*右側に隙間を作る(閉じるカバーを表示)*/ 196 | max-width: 330px;/*最大幅(調整してください)*/ 197 | height: 100%; 198 | background: #fff;/*背景色*/ 199 | transition: .3s ease-in-out;/*滑らかに表示*/ 200 | -webkit-transform: translateX(-105%); 201 | transform: translateX(-105%);/*左に隠しておく*/ 202 | } 203 | /*閉じる用の薄黒カバー*/ 204 | #nav-close { 205 | display: none;/*はじめは隠しておく*/ 206 | position: fixed; 207 | z-index: 99; 208 | top: 0;/*全体に広がるように*/ 209 | left: 0; 210 | width: 100%; 211 | height: 100%; 212 | background: black; 213 | opacity: 0; 214 | transition: .3s ease-in-out; 215 | } 216 | /*チェックが入ったらもろもろ表示*/ 217 | #nav-input:checked ~ #nav-close { 218 | display: block;/*カバーを表示*/ 219 | opacity: .5; 220 | } 221 | #nav-input:checked ~ #nav-content { 222 | -webkit-transform: translateX(0%); 223 | transform: translateX(0%);/*中身を表示(右へスライド)*/ 224 | box-shadow: 6px 0 25px rgba(0,0,0,.15); 225 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | TsumiQiita 9 | 10 | 11 | 12 |
13 | 47 |
48 |
49 |
50 |
51 | Qiitaの記事ヘッダを参照してください。titleとtagsは必須です。 52 | 53 |
54 | 58 | 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion:6*/ 2 | const electron = require('electron'); 3 | const {shell} = require('electron'); 4 | let app = electron.app; 5 | let BrowserWindow = electron.BrowserWindow; 6 | 7 | let mainWindow = null; 8 | app.on('ready', () => { 9 | // mainWindowを作成(windowの大きさや、Kioskモードにするかどうかなどもここで定義できる) 10 | mainWindow = new BrowserWindow({width: 1000, height: 800}); 11 | // Electronに表示するhtmlを絶対パスで指定(相対パスだと動かない) 12 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 13 | 14 | mainWindow.webContents.on('new-window', (event, url) => { 15 | event.preventDefault(); 16 | shell.openExternal(url); 17 | }); 18 | 19 | // ChromiumのDevツールを開く 20 | //mainWindow.webContents.openDevTools(); 21 | 22 | const { Menu } = require('electron'); 23 | const menuTemplate = []; 24 | 25 | if (process.platform === 'darwin') { 26 | menuTemplate.push({ 27 | label: 'Edit', 28 | submenu: [ 29 | {role: 'undo'}, 30 | {role: 'redo'}, 31 | {type: 'separator'}, 32 | {role: 'cut'}, 33 | {role: 'copy'}, 34 | {role: 'paste'}, 35 | {role: 'pasteandmatchstyle'}, 36 | {role: 'delete'}, 37 | {role: 'selectall'} 38 | ] 39 | }) 40 | 41 | const applicationMenu = Menu.buildFromTemplate(menuTemplate) 42 | Menu.setApplicationMenu(applicationMenu) 43 | } 44 | }); 45 | 46 | app.on("window-all-closed", function () { 47 | app.quit(); 48 | }); 49 | 50 | const shouldQuit = app.makeSingleInstance((argv, workingDirectory) => { 51 | }); 52 | if (shouldQuit) { 53 | app.quit(); 54 | } 55 | -------------------------------------------------------------------------------- /src/js/functions.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion:6*/ 2 | var timer; 3 | const WRITE_INTERVAL = 3000; 4 | const hljs = require('highlight.js'); 5 | const Config = require('electron-store'); 6 | const config = new Config(); 7 | 8 | function $(name) { 9 | return document.querySelector(name); 10 | } 11 | 12 | function fileNameEncode(name) { 13 | return name.replace(".", "_"); 14 | } 15 | 16 | function getBody(md) { 17 | return md.match(/(-{3,}[\s\S])([\s\S.]*?)(-{3,}[\s\S])([\s\S.]*)/)[4]; 18 | } 19 | 20 | function getHeader(md) { 21 | return md.match(/(-{3,}[\s\S])([\s\S.]*?)(-{3,}[\s\S])([\s\S.]*)/)[2]; 22 | } 23 | 24 | function writeMarkdownFile() { 25 | const currentFile = config.get('CURRENT_FILE'); 26 | const md = $("#tsumiqiita-editor").value; 27 | 28 | if (currentFile !== undefined) { 29 | if (currentFile.length > 0) { 30 | const fs = require('fs'); 31 | const filePath = currentFile.replace(/\\/g, "/"); 32 | fs.writeFileSync(filePath, md); 33 | clearInterval(timer); 34 | $("#title").style.fontStyle = "normal"; 35 | } 36 | } 37 | } 38 | 39 | function rendaring() { 40 | const it = require('markdown-it')('commonmark', { 41 | langPrefix: "hljs language-", 42 | highlight: (str, lang) => { 43 | var filename = ''; 44 | if (lang && lang.indexOf(":") >= 1) { 45 | var sp = lang.split(":"); 46 | lang = sp[0]; 47 | filename = sp[1]; 48 | } 49 | 50 | var header = ""; 51 | var style = "style='margin-top:3px;'"; 52 | if (filename) { 53 | header = "
" + filename + "
"; 54 | style = ""; 55 | } 56 | 57 | var codeBlockHTML; 58 | if (lang) { 59 | try { 60 | codeBlockHTML = '
' + hljs.highlight(lang, str).value + '
'; 61 | } 62 | catch (e) { 63 | codeBlockHTML = '
' + require("escape-html")(str) + '
'; 64 | } 65 | } else { 66 | codeBlockHTML = '
' + require("escape-html")(str) + '
'; 67 | } 68 | 69 | return "
" + 70 | header + 71 | "
" + codeBlockHTML + "
" + 72 | "
" 73 | ; 74 | }, 75 | }).enable(['table', 'strikethrough']).use(require('markdown-it-checkbox')); 76 | const md = $("#tsumiqiita-editor").value; 77 | $("#preview").innerHTML = it.render(getBody(md)); 78 | 79 | var myDivEl = document.getElementById("preview"); 80 | var anchorsInMyDiv = myDivEl.querySelectorAll("a"); 81 | anchorsInMyDiv.forEach(s => s.setAttribute("target", "_blank")); 82 | } 83 | 84 | function openMarkdownFile(path) { 85 | const fs = require('fs'); 86 | fs.readFile(path, (error, text) => { 87 | if (error !== null) { 88 | return; 89 | } 90 | $("#tsumiqiita-editor").value = text.toString(); 91 | 92 | rendaring(); 93 | 94 | config.set('CURRENT_FILE', path); 95 | }); 96 | } 97 | 98 | function autoSave() { 99 | try { 100 | $("#title").style.fontStyle = "italic"; 101 | clearTimeout(timer); 102 | timer = setInterval(writeMarkdownFile, WRITE_INTERVAL); 103 | } catch (err) { 104 | return false; 105 | } 106 | } 107 | 108 | function setScrollSync() { 109 | const p = $('#preview'); 110 | const e = $('#tsumiqiita-editor'); 111 | 112 | e.onscroll = () => { 113 | let rate = e.scrollTop / e.scrollHeight; 114 | p.scrollTop = p.scrollHeight * rate; 115 | }; 116 | } 117 | 118 | 119 | // ファイルリストを取得。mdファイルのみ。 120 | function updateFileListPain(dir, givinefileName) { 121 | const fs = require('fs'); 122 | const path = require('path'); 123 | 124 | if (dir.length === 0) { return; } 125 | fs.readdir(dir, (err, files) => { 126 | if (err) { throw err; } 127 | let fileList = []; 128 | files.filter((file) => { 129 | const target = dir + path.sep + file; 130 | return fs.statSync(target).isFile() && /.*\.md$/.test(target); //絞り込み 131 | }).forEach((file) => { 132 | fileList.push(file); 133 | }); 134 | 135 | // 画面に反映 136 | let fileListHtml = ""; 137 | for (let fileName of fileList) { 138 | let filePath = dir + path.sep + fileName; 139 | let attrChecked = ""; 140 | filePath = filePath.replace(/\\{1,}/g, "\\\\"); 141 | if (givinefileName === fileName) { 142 | attrChecked = " checked=true"; 143 | } 144 | fileListHtml += "
\n" + 145 | " \n" + 146 | "
\n"; 147 | } 148 | 149 | $('#files').innerHTML = fileListHtml; 150 | }); 151 | } 152 | 153 | function init_FileList(fileName) { 154 | let targetDir = config.get('TARGET_DIR'); 155 | if (targetDir === undefined) { targetDir = ""; } 156 | else if (targetDir === "undefined") { targetDir = ""; } 157 | else if (targetDir.length === 0) { targetDir = "フォルダを選択してください"; } 158 | else { updateFileListPain(targetDir, fileName); } 159 | 160 | $('#target-dir').innerHTML = targetDir; 161 | } 162 | 163 | function init_LastOpenFile() { 164 | const currentFile = config.get('CURRENT_FILE'); 165 | if (currentFile !== undefined) { 166 | if (currentFile.length > 0) { 167 | const filePath = currentFile.replace(/\\/g, "/"); 168 | openMarkdownFile(filePath); 169 | 170 | return require('path').basename(currentFile); 171 | } 172 | } 173 | return null; 174 | } 175 | 176 | function init() { 177 | init_FileList(init_LastOpenFile()); 178 | 179 | $('#input').value = config.get("TOKEN"); 180 | 181 | setScrollSync(); 182 | hljs.initHighlightingOnLoad(); 183 | } 184 | 185 | function selectTargetDir() { 186 | const Dialog = require('electron').remote.dialog; 187 | 188 | Dialog.showOpenDialog({ 189 | properties: ['openDirectory'], 190 | title: 'フォルダの選択', 191 | defaultPath: '.' 192 | }, (folderNames) => { 193 | config.set('TARGET_DIR', folderNames[0]); 194 | $('#target-dir').innerHTML = folderNames[0]; 195 | 196 | updateFileListPain(folderNames[0], ""); 197 | }); 198 | } 199 | 200 | function setToken() { 201 | config.set("TOKEN", $('#input').value); 202 | } 203 | 204 | function getTags() { 205 | const tags = $("#tsumiqiita-editor").value.split("\n")[0].trim().split(" "); 206 | let ret = []; 207 | 208 | for (let i = 0; i < tags.length && i < 5; i++) { 209 | let tmp = tags[i].split(":"); 210 | if (tmp.length < 2) { 211 | ret.push({ "name": tmp[0] }); 212 | } else { 213 | ret.push({ "name": tmp[0], "versions": [tmp[1]] }); 214 | } 215 | } 216 | return ret; 217 | } 218 | 219 | /** 220 | * @param {object[]} tags 221 | */ 222 | function createTagsObject(tags) { 223 | if (JSON.stringify(tags) == JSON.stringify([""])) return undefined; 224 | 225 | let array = []; 226 | let tag = ""; 227 | for (const obj of tags) { 228 | tag = obj.split(":"); 229 | if (tag.length >= 2) { 230 | array.push({ 231 | "name": tag[0], 232 | "versions": [ tag[1] ] 233 | }); 234 | } else { 235 | array.push({ 236 | "name": tag[0] 237 | }); 238 | } 239 | } 240 | return array; 241 | } 242 | 243 | function post() { 244 | const fs = require('fs'); 245 | const token = config.get("TOKEN"); 246 | const p = config.get("CURRENT_FILE"); 247 | const text = fs.readFileSync(p, 'utf-8'); 248 | const request = require('request'); 249 | 250 | if (token === undefined) { 251 | $('#message').innerText = "トークンがセットされていません" 252 | $('#msg-dialog').showModal(); 253 | return; 254 | } 255 | 256 | const options = { 257 | url: "https://qiita.com/api/v2/items", 258 | method: "POST", 259 | headers: { 260 | "Content-Type": "application/json", 261 | "Authorization": "Bearer " + token 262 | }, 263 | json: {} 264 | }; 265 | 266 | const textHeader = getHeader(text); 267 | options.json["body"] = getBody(text); 268 | 269 | let item = ""; 270 | let label = "" 271 | for (const line of textHeader.split('\n')) { 272 | if (line.trim().length > 0) { 273 | item = line.split(":"); 274 | label = item[0].trim(); 275 | switch (label) { 276 | case "coediting": 277 | case "private": 278 | case "tweet": 279 | options.json[label] = (item[1].trim() === 'true'); 280 | break; 281 | case "group_url_name": 282 | case "title": 283 | options.json[label] = item[1].trim(); 284 | break; 285 | case "tags": 286 | options.json[label] = createTagsObject(line.slice(line.indexOf(":")+1).trim().split(" ")); 287 | break; 288 | } 289 | } 290 | } 291 | 292 | if (!options.json["title"]) { 293 | $('#message').innerText = "タイトルが入力されていません" 294 | $('#msg-dialog').showModal(); 295 | return; 296 | } 297 | if (options.json["tags"] === undefined) { 298 | $('#message').innerText = "タグが入力されていません" 299 | $('#msg-dialog').showModal(); 300 | return; 301 | } 302 | 303 | request(options, (error, response) => { 304 | $('#message').innerText = "投稿しました!" 305 | if (response.statusCode != 201) { 306 | $('#message').innerText = "投稿に失敗しました…\n・タグが6つ以上あるかもしれません\n・正しいトークンがセットされていないかもしれません" 307 | } 308 | $('#msg-dialog').showModal(); 309 | }); 310 | } 311 | 312 | function createArticle() { 313 | const Dialog = require('electron').remote.dialog; 314 | 315 | Dialog.showSaveDialog({ 316 | title: '新規作成', 317 | defaultPath: '.', 318 | filters: [ 319 | { name: 'Markdownファイル', extensions: ['md'] }, 320 | ] 321 | }, (savedFile) => { 322 | try { 323 | if (savedFile !== undefined && savedFile !== "") { 324 | const fs = require('fs'); 325 | fs.writeFileSync(savedFile, ""); 326 | console.log(savedFile); 327 | config.set('CURRENT_FILE', savedFile); 328 | updateFileListPain(config.get('TARGET_DIR'), require('path').basename(savedFile)); 329 | openMarkdownFile(savedFile); 330 | } 331 | } catch (err) { 332 | return false; 333 | } 334 | }); 335 | } 336 | 337 | function OnTabKey( e, obj ){ 338 | // タブキーが押された時以外は即リターン 339 | if( e.keyCode!=9 ){ return; } 340 | 341 | // タブキーを押したときのデフォルトの挙動を止める 342 | e.preventDefault(); 343 | 344 | // 現在のカーソルの位置と、カーソルの左右の文字列を取得しておく 345 | var cursorPosition = obj.selectionStart; 346 | var cursorLeft = obj.value.substr( 0, cursorPosition ); 347 | var cursorRight = obj.value.substr( cursorPosition, obj.value.length ); 348 | 349 | // テキストエリアの中身を、 350 | // 「取得しておいたカーソルの左側」+「タブ」+「取得しておいたカーソルの右側」 351 | // という状態にする。 352 | obj.value = cursorLeft+"\t"+cursorRight; 353 | 354 | // カーソルの位置を入力したタブの後ろにする 355 | obj.selectionEnd = cursorPosition+1; 356 | } 357 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsumiqiita", 3 | "version": "1.8.2", 4 | "description": "MarkdownファイルをQiitaに投稿するelectron製デスクトップアプリです。", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node_modules/.bin/electron .", 8 | "build": "node_modules/.bin/build" 9 | }, 10 | "build": { 11 | "productName": "TsumiQiita", 12 | "appId": "tsumiqiita", 13 | "mac": { 14 | "target": "dmg" 15 | }, 16 | "directories": { 17 | "output": "../build" 18 | } 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/hidao80/TsumiQiita.git" 23 | }, 24 | "author": "hidao80", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/hidao80/TsumiQiita/issues" 28 | }, 29 | "homepage": "https://github.com/hidao80/TsumiQiita#readme", 30 | "devDependencies": { 31 | "electron": "^2.0.12", 32 | "electron-builder": "^20.29.0" 33 | }, 34 | "dependencies": { 35 | "electron-store": "^2.0.0", 36 | "escape-html": "^1.0.3", 37 | "github-markdown-css": "^2.10.0", 38 | "highlight.js": "^9.12.0", 39 | "markdown-it": "^8.4.1", 40 | "markdown-it-checkbox": "^1.1.0", 41 | "request": "^2.88.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidao80/TsumiQiita/c2ca3560dc4e53da3a53d594acafe6e36633163d/ss.png --------------------------------------------------------------------------------