diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2018-02-05 03:59:29 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2018-02-05 03:59:29 +0900 |
| commit | dbd3cdb308d2edf600b20a8b632045c6163ae326 (patch) | |
| tree | de3630065fcddeb1916668ef3b0b43a219340e2e /src/web | |
| parent | Fix (diff) | |
| parent | Merge pull request #1097 from syuilo/refactor (diff) | |
| download | sharkey-dbd3cdb308d2edf600b20a8b632045c6163ae326.tar.gz sharkey-dbd3cdb308d2edf600b20a8b632045c6163ae326.tar.bz2 sharkey-dbd3cdb308d2edf600b20a8b632045c6163ae326.zip | |
Merge remote-tracking branch 'refs/remotes/origin/master' into vue-#972
Diffstat (limited to 'src/web')
91 files changed, 2670 insertions, 744 deletions
diff --git a/src/web/app/app.styl b/src/web/app/app.styl index de66df74d4..22043b8833 100644 --- a/src/web/app/app.styl +++ b/src/web/app/app.styl @@ -1,29 +1,4 @@ -json('../../const.json') - -@charset 'utf-8' - -$theme-color = themeColor -$theme-color-foreground = themeColorForeground - -/* - ::selection - background $theme-color - color #fff -*/ - -* - position relative - box-sizing border-box - background-clip padding-box !important - tap-highlight-color rgba($theme-color, 0.7) - -webkit-tap-highlight-color rgba($theme-color, 0.7) - -html, body - margin 0 - padding 0 - scroll-behavior smooth - text-size-adjust 100% - font-family sans-serif +@import "../style" html &.progress @@ -96,17 +71,6 @@ body 100% transform rotate(360deg) -a - text-decoration none - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - * - cursor pointer - code font-family Consolas, 'Courier New', Courier, Monaco, monospace diff --git a/src/web/app/base.pug b/src/web/app/base.pug index 140286a768..d7c7f0aed4 100644 --- a/src/web/app/base.pug +++ b/src/web/app/base.pug @@ -24,6 +24,9 @@ html //- FontAwesome style style #{facss} + //- highlight.js style + style #{hljscss} + body noscript: p | JavaScriptを有効にしてください diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts index 2008e6f5ac..bba838f56b 100644 --- a/src/web/app/common/scripts/api.ts +++ b/src/web/app/common/scripts/api.ts @@ -40,7 +40,7 @@ export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => { } else { res.json().then(err => { reject(err.error); - }); + }, reject); } }).catch(reject); }); diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts new file mode 100644 index 0000000000..512791ecb0 --- /dev/null +++ b/src/web/app/common/scripts/parse-search-query.ts @@ -0,0 +1,53 @@ +export default function(qs: string) { + const q = { + text: '' + }; + + qs.split(' ').forEach(x => { + if (/^([a-z_]+?):(.+?)$/.test(x)) { + const [key, value] = x.split(':'); + switch (key) { + case 'user': + q['include_user_usernames'] = value.split(','); + break; + case 'exclude_user': + q['exclude_user_usernames'] = value.split(','); + break; + case 'follow': + q['following'] = value == 'null' ? null : value == 'true'; + break; + case 'reply': + q['reply'] = value == 'null' ? null : value == 'true'; + break; + case 'repost': + q['repost'] = value == 'null' ? null : value == 'true'; + break; + case 'media': + q['media'] = value == 'null' ? null : value == 'true'; + break; + case 'poll': + q['poll'] = value == 'null' ? null : value == 'true'; + break; + case 'until': + case 'since': + // YYYY-MM-DD + if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { + const [yyyy, mm, dd] = value.split('-'); + q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); + } + break; + default: + q[key] = value; + break; + } + } else { + q.text += x + ' '; + } + }); + + if (q.text) { + q.text = q.text.trim(); + } + + return q; +} diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag index 0078a18636..0594032de6 100644 --- a/src/web/app/common/tags/authorized-apps.tag +++ b/src/web/app/common/tags/authorized-apps.tag @@ -1,5 +1,7 @@ <mk-authorized-apps> - <p class="none" if={ !fetching && apps.length == 0 }>%i18n:common.tags.mk-authorized-apps.no-apps%</p> + <div class="none ui info" if={ !fetching && apps.length == 0 }> + <p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p> + </div> <div class="apps" if={ apps.length != 0 }> <div each={ app in apps }> <p><b>{ app.name }</b></p> diff --git a/src/web/app/common/tags/copyright.tag b/src/web/app/common/tags/copyright.tag deleted file mode 100644 index 9c3f1f648b..0000000000 --- a/src/web/app/common/tags/copyright.tag +++ /dev/null @@ -1,7 +0,0 @@ -<mk-copyright> - <span>(c) syuilo 2014-2017</span> - <style> - :scope - display block - </style> -</mk-copyright> diff --git a/src/web/app/common/tags/index.ts b/src/web/app/common/tags/index.ts index 2f4e1181d4..df99d93cc5 100644 --- a/src/web/app/common/tags/index.ts +++ b/src/web/app/common/tags/index.ts @@ -12,7 +12,6 @@ require('./signin.tag'); require('./signup.tag'); require('./forkit.tag'); require('./introduction.tag'); -require('./copyright.tag'); require('./signin-history.tag'); require('./twitter-setting.tag'); require('./authorized-apps.tag'); diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag index 3256688d10..28afc6fa46 100644 --- a/src/web/app/common/tags/introduction.tag +++ b/src/web/app/common/tags/introduction.tag @@ -3,7 +3,7 @@ <h1>Misskeyとは?</h1> <p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo/misskey" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p> <p>無料で誰でも利用でき、広告も掲載していません。</p> - <p><a href={ _ABOUT_URL_ } target="_blank">もっと知りたい方はこちら</a></p> + <p><a href={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p> </article> <style> :scope diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag index a149e1de22..7b4d1be569 100644 --- a/src/web/app/common/tags/messaging/room.tag +++ b/src/web/app/common/tags/messaging/room.tag @@ -254,7 +254,7 @@ this.api('messaging/messages', { user_id: this.user.id, limit: max + 1, - max_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined + until_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined }).then(messages => { if (messages.length == max + 1) { this.moreMessagesIsInStock = true; diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag index 71f0453db0..ea122575aa 100644 --- a/src/web/app/common/tags/nav-links.tag +++ b/src/web/app/common/tags/nav-links.tag @@ -1,7 +1,10 @@ <mk-nav-links> - <a href={ _ABOUT_URL_ }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a> + <a href={ aboutUrl }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a> <style> :scope display inline </style> + <script> + this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`; + </script> </mk-nav-links> diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag index 03afd72326..cdd58c4c67 100644 --- a/src/web/app/common/tags/signin-history.tag +++ b/src/web/app/common/tags/signin-history.tag @@ -1,55 +1,11 @@ <mk-signin-history> <div class="records" if={ history.length != 0 }> - <div each={ history }> - <mk-time time={ created_at }/> - <header> - <virtual if={ success }>%fa:check%</virtual> - <virtual if={ !success }>%fa:times%</virtual> - <span class="ip">{ ip }</span> - </header> - <pre><code>{ JSON.stringify(headers, null, ' ') }</code></pre> - </div> + <mk-signin-record each={ rec in history } rec={ rec }/> </div> <style> :scope display block - > .records - > div - padding 16px 0 0 0 - border-bottom solid 1px #eee - - > header - - > [data-fa] - margin-right 8px - - &.check - color #0fda82 - - &.times - color #ff3100 - - > .ip - display inline-block - color #444 - background #f8f8f8 - - > mk-time - position absolute - top 16px - right 0 - color #777 - - > pre - overflow auto - max-height 100px - - > code - white-space pre-wrap - word-break break-all - color #4a535a - </style> <script> this.mixin('i'); @@ -84,3 +40,77 @@ }; </script> </mk-signin-history> + +<mk-signin-record> + <header onclick={ toggle }> + <virtual if={ rec.success }>%fa:check%</virtual> + <virtual if={ !rec.success }>%fa:times%</virtual> + <span class="ip">{ rec.ip }</span> + <mk-time time={ rec.created_at }/> + </header> + <pre ref="headers" class="json" show={ show }>{ JSON.stringify(rec.headers, null, 2) }</pre> + + <style> + :scope + display block + border-bottom solid 1px #eee + + > header + display flex + padding 8px 0 + line-height 32px + cursor pointer + + > [data-fa] + margin-right 8px + text-align left + + &.check + color #0fda82 + + &.times + color #ff3100 + + > .ip + display inline-block + text-align left + padding 8px + line-height 16px + font-family monospace + font-size 14px + color #444 + background #f8f8f8 + border-radius 4px + + > mk-time + margin-left auto + text-align right + color #777 + + > pre + overflow auto + margin 0 0 16px 0 + max-height 100px + white-space pre-wrap + word-break break-all + color #4a535a + + </style> + + <script> + import hljs from 'highlight.js'; + + this.rec = this.opts.rec; + this.show = false; + + this.on('mount', () => { + hljs.highlightBlock(this.refs.headers); + }); + + this.toggle = () => { + this.update({ + show: !this.show + }); + }; + </script> +</mk-signin-record> diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag index f25d99974b..f5a2be94ed 100644 --- a/src/web/app/common/tags/signin.tag +++ b/src/web/app/common/tags/signin.tag @@ -6,6 +6,9 @@ <label class="password"> <input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock% </label> + <label class="token" if={ user && user.two_factor_enabled }> + <input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock% + </label> <button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button> </form> <style> @@ -39,6 +42,7 @@ input[type=text] input[type=password] + input[type=number] user-select text display inline-block cursor auto @@ -123,6 +127,10 @@ this.refs.password.focus(); return false; } + if (this.user && this.user.two_factor_enabled && this.refs.token.value == '') { + this.refs.token.focus(); + return false; + } this.update({ signing: true @@ -130,7 +138,8 @@ this.api('signin', { username: this.refs.username.value, - password: this.refs.password.value + password: this.refs.password.value, + token: this.user && this.user.two_factor_enabled ? this.refs.token.value : undefined }).then(() => { location.reload(); }).catch(() => { diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag index 4816fe66db..b488efb927 100644 --- a/src/web/app/common/tags/signup.tag +++ b/src/web/app/common/tags/signup.tag @@ -34,7 +34,7 @@ </label> <label class="agree-tou"> <input name="agree-tou" type="checkbox" autocomplete="off" required="required"/> - <p><a href="https://github.com/syuilo/misskey/blob/master/src/docs/tou.md" target="_blank">利用規約</a>に同意する</p> + <p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p> </label> <button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button> </form> @@ -182,6 +182,8 @@ this.passwordRetypeState = null; this.recaptchaed = false; + this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`; + window.onRecaptchaed = () => { this.recaptchaed = true; this.update(); diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag index 3b70505ba2..4d57cfa55a 100644 --- a/src/web/app/common/tags/twitter-setting.tag +++ b/src/web/app/common/tags/twitter-setting.tag @@ -1,5 +1,5 @@ <mk-twitter-setting> - <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _ABOUT_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p> + <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p> <p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p> <p> <a href={ _API_URL_ + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a> diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts index 27b63ab2ef..ce68c4f2d1 100644 --- a/src/web/app/desktop/router.ts +++ b/src/web/app/desktop/router.ts @@ -16,7 +16,7 @@ export default (mios: MiOS) => { route('/i/messaging/:user', messaging); route('/i/mentions', mentions); route('/post::post', post); - route('/search::query', search); + route('/search', search); route('/:user', user.bind(null, 'home')); route('/:user/graphs', user.bind(null, 'graphs')); route('/:user/:post', post); @@ -47,7 +47,7 @@ export default (mios: MiOS) => { function search(ctx) { const el = document.createElement('mk-search-page'); - el.setAttribute('query', ctx.params.query); + el.setAttribute('query', ctx.querystring.substr(2)); mount(el); } diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl index d99e5df2b4..c893e2ed67 100644 --- a/src/web/app/desktop/style.styl +++ b/src/web/app/desktop/style.styl @@ -2,6 +2,8 @@ @import "../reset" @import "../../../../node_modules/cropperjs/dist/cropper.css" +@import "./ui" + *::input-placeholder color #D8CBC5 @@ -47,66 +49,3 @@ html #wait right auto left 15px - -button - font-family sans-serif - - * - pointer-events none - - &.style-normal - &.style-primary - display block - cursor pointer - padding 0 16px - margin 0 - min-width 100px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - &.style-normal - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - &.style-primary - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag index 901daabfd8..a60a46b790 100644 --- a/src/web/app/desktop/tags/drive/browser.tag +++ b/src/web/app/desktop/tags/drive/browser.tag @@ -18,14 +18,16 @@ <virtual each={ folder in folders }> <mk-drive-browser-folder class="folder" folder={ folder }/> </virtual> - <div class="padding" each={ folders }></div> + <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> + <div class="padding" each={ Array(10).fill(16) }></div> <button if={ moreFolders }>%i18n:desktop.tags.mk-drive-browser.load-more%</button> </div> <div class="files" ref="filesContainer" if={ files.length > 0 }> <virtual each={ file in files }> <mk-drive-browser-file class="file" file={ file }/> </virtual> - <div class="padding" each={ files }></div> + <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> + <div class="padding" each={ Array(10).fill(16) }></div> <button if={ moreFiles } onclick={ fetchMoreFiles }>%i18n:desktop.tags.mk-drive-browser.load-more%</button> </div> <div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }> diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag index 0f019d95bf..8b3d36b3f3 100644 --- a/src/web/app/desktop/tags/drive/file.tag +++ b/src/web/app/desktop/tags/drive/file.tag @@ -5,7 +5,9 @@ <div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/> <p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p> </div> - <div class="thumbnail"><img src={ file.url + '?thumbnail&size=128' } alt=""/></div> + <div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }"> + <img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/> + </div> <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p> <style> :scope @@ -139,6 +141,7 @@ </style> <script> + import anime from 'animejs'; import bytesToSize from '../../../common/scripts/bytes-to-size'; this.mixin('i'); @@ -199,5 +202,16 @@ this.isDragging = false; this.browser.isDragSource = false; }; + + this.onload = () => { + if (this.file.properties.average_color) { + anime({ + targets: this.refs.thumbnail, + backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`, + duration: 100, + easing: 'linear' + }); + } + }; </script> </mk-drive-browser-file> diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag index a48c7239a1..2687283079 100644 --- a/src/web/app/desktop/tags/home-widgets/mentions.tag +++ b/src/web/app/desktop/tags/home-widgets/mentions.tag @@ -101,7 +101,7 @@ }); this.api('posts/mentions', { following: this.mode == 'following', - max_id: this.refs.timeline.tail().id + until_id: this.refs.timeline.tail().id }).then(posts => { this.update({ moreLoading: false diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag index 4c58aa4aa8..9571b09f34 100644 --- a/src/web/app/desktop/tags/home-widgets/timeline.tag +++ b/src/web/app/desktop/tags/home-widgets/timeline.tag @@ -86,7 +86,7 @@ }); this.api('posts/timeline', { - max_date: this.date ? this.date.getTime() : undefined + until_date: this.date ? this.date.getTime() : undefined }).then(posts => { this.update({ isLoading: false, @@ -103,7 +103,7 @@ moreLoading: true }); this.api('posts/timeline', { - max_id: this.refs.timeline.tail().id + until_id: this.refs.timeline.tail().id }).then(posts => { this.update({ moreLoading: false diff --git a/src/web/app/desktop/tags/image-dialog.tag b/src/web/app/desktop/tags/image-dialog.tag deleted file mode 100644 index 39d16ca139..0000000000 --- a/src/web/app/desktop/tags/image-dialog.tag +++ /dev/null @@ -1,61 +0,0 @@ -<mk-image-dialog> - <div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/> - <style> - :scope - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - opacity 0 - - > .bg - display block - position fixed - z-index 1 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - - > img - position fixed - z-index 2 - top 0 - right 0 - bottom 0 - left 0 - max-width 100% - max-height 100% - margin auto - cursor zoom-out - - </style> - <script> - import anime from 'animejs'; - - this.image = this.opts.image; - - this.on('mount', () => { - anime({ - targets: this.root, - opacity: 1, - duration: 100, - easing: 'linear' - }); - }); - - this.close = () => { - anime({ - targets: this.root, - opacity: 0, - duration: 100, - easing: 'linear', - complete: () => this.unmount() - }); - }; - </script> -</mk-image-dialog> diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag deleted file mode 100644 index 44a61cb747..0000000000 --- a/src/web/app/desktop/tags/images-viewer.tag +++ /dev/null @@ -1,45 +0,0 @@ -<mk-images-viewer> - <div class="image" ref="view" onmousemove={ mousemove } style={ 'background-image: url(' + image.url + '?thumbnail' } onclick={ click }><img src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div> - <style> - :scope - display block - overflow hidden - border-radius 4px - - > .image - cursor zoom-in - - > img - display block - max-height 256px - max-width 100% - margin 0 auto - - &:hover - > img - visibility hidden - - &:not(:hover) - background-image none !important - - </style> - <script> - this.images = this.opts.images; - this.image = this.images[0]; - - this.mousemove = e => { - const rect = this.refs.view.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - const xp = mouseX / this.refs.view.offsetWidth * 100; - const yp = mouseY / this.refs.view.offsetHeight * 100; - this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%'; - }; - - this.click = () => { - riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), { - image: this.image - }); - }; - </script> -</mk-images-viewer> diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag new file mode 100644 index 0000000000..0cd408576f --- /dev/null +++ b/src/web/app/desktop/tags/images.tag @@ -0,0 +1,172 @@ +<mk-images> + <virtual each={ image in images }> + <mk-images-image image={ image }/> + </virtual> + <style> + :scope + display grid + grid-gap 4px + height 256px + </style> + <script> + this.images = this.opts.images; + + this.on('mount', () => { + if (this.images.length == 1) { + this.root.style.gridTemplateRows = '1fr'; + + this.tags['mk-images-image'].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'].root.style.gridRow = '1 / 2'; + } else if (this.images.length == 2) { + this.root.style.gridTemplateColumns = '1fr 1fr'; + this.root.style.gridTemplateRows = '1fr'; + + this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2'; + } else if (this.images.length == 3) { + this.root.style.gridTemplateColumns = '1fr 0.5fr'; + this.root.style.gridTemplateRows = '1fr 1fr'; + + this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3'; + this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3'; + } else if (this.images.length == 4) { + this.root.style.gridTemplateColumns = '1fr 1fr'; + this.root.style.gridTemplateRows = '1fr 1fr'; + + this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3'; + this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3'; + } + }); + </script> +</mk-images> + +<mk-images-image> + <a ref="view" + href={ image.url } + onmousemove={ mousemove } + onmouseleave={ mouseleave } + style={ styles } + onclick={ click } + title={ image.name }></a> + <style> + :scope + display block + overflow hidden + border-radius 4px + + > a + display block + cursor zoom-in + overflow hidden + width 100% + height 100% + background-position center + + &:not(:hover) + background-size cover + + </style> + <script> + this.image = this.opts.image; + this.styles = { + 'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent', + 'background-image': `url(${this.image.url}?thumbnail&size=512)` + }; + + this.mousemove = e => { + const rect = this.refs.view.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + const xp = mouseX / this.refs.view.offsetWidth * 100; + const yp = mouseY / this.refs.view.offsetHeight * 100; + this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%'; + this.refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")'; + }; + + this.mouseleave = () => { + this.refs.view.style.backgroundPosition = ''; + }; + + this.click = ev => { + ev.preventDefault(); + riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), { + image: this.image + }); + return false; + }; + </script> +</mk-images-image> + +<mk-image-dialog> + <div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/> + <style> + :scope + display block + position fixed + z-index 2048 + top 0 + left 0 + width 100% + height 100% + opacity 0 + + > .bg + display block + position fixed + z-index 1 + top 0 + left 0 + width 100% + height 100% + background rgba(0, 0, 0, 0.7) + + > img + position fixed + z-index 2 + top 0 + right 0 + bottom 0 + left 0 + max-width 100% + max-height 100% + margin auto + cursor zoom-out + + </style> + <script> + import anime from 'animejs'; + + this.image = this.opts.image; + + this.on('mount', () => { + anime({ + targets: this.root, + opacity: 1, + duration: 100, + easing: 'linear' + }); + }); + + this.close = () => { + anime({ + targets: this.root, + opacity: 0, + duration: 100, + easing: 'linear', + complete: () => this.unmount() + }); + }; + </script> +</mk-image-dialog> diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/tags/index.ts index 3ec1d108aa..4edda83534 100644 --- a/src/web/app/desktop/tags/index.ts +++ b/src/web/app/desktop/tags/index.ts @@ -76,8 +76,7 @@ require('./set-avatar-suggestion.tag'); require('./set-banner-suggestion.tag'); require('./repost-form.tag'); require('./sub-post-content.tag'); -require('./images-viewer.tag'); -require('./image-dialog.tag'); +require('./images.tag'); require('./donation.tag'); require('./users-list.tag'); require('./user-following.tag'); diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag index 3218c00f6a..39862487e9 100644 --- a/src/web/app/desktop/tags/notifications.tag +++ b/src/web/app/desktop/tags/notifications.tag @@ -283,7 +283,7 @@ this.api('i/notifications', { limit: max + 1, - max_id: this.notifications[this.notifications.length - 1].id + until_id: this.notifications[this.notifications.length - 1].id }).then(notifications => { if (notifications.length == max + 1) { this.moreNotifications = true; diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag index 44548e4183..974f49a4fe 100644 --- a/src/web/app/desktop/tags/pages/entrance.tag +++ b/src/web/app/desktop/tags/pages/entrance.tag @@ -18,7 +18,7 @@ <footer> <div> <mk-nav-links/> - <mk-copyright/> + <p class="c">{ _COPYRIGHT_ }</p> </div> </footer> <!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)--> @@ -101,7 +101,7 @@ text-align center border-top solid 1px #fff - > mk-copyright + > .c margin 0 line-height 64px font-size 10px @@ -150,7 +150,7 @@ </mk-entrance> <mk-entrance-signin> - <a class="help" href={ _ABOUT_URL_ + '/help' } title="お困りですか?">%fa:question%</a> + <a class="help" href={ _DOCS_URL_ + '/help' } title="お困りですか?">%fa:question%</a> <div class="form"> <h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/> <p>{ user ? user.name : 'アカウント' }</p> diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag index e22386df91..cccd85c474 100644 --- a/src/web/app/desktop/tags/post-detail-sub.tag +++ b/src/web/app/desktop/tags/post-detail-sub.tag @@ -9,7 +9,7 @@ <span class="username">@{ post.user.username }</span> </div> <div class="right"> - <a class="time" href={ '/' + this.post.user.username + '/' + this.post.id }> + <a class="time" href={ '/' + post.user.username + '/' + post.id }> <mk-time time={ post.created_at }/> </a> </div> @@ -17,9 +17,7 @@ <div class="body"> <div class="text" ref="text"></div> <div class="media" if={ post.media }> - <virtual each={ file in post.media }> - <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/> - </virtual> + <mk-images images={ post.media }/> </div> </div> </div> @@ -107,11 +105,6 @@ > mk-url-preview margin-top 8px - > .media - > img - display block - max-width 100% - </style> <script> import compile from '../../common/scripts/text-compiler'; diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag index 37f90a6ffb..47c71a6c12 100644 --- a/src/web/app/desktop/tags/post-detail.tag +++ b/src/web/app/desktop/tags/post-detail.tag @@ -37,7 +37,7 @@ <div class="body"> <div class="text" ref="text"></div> <div class="media" if={ p.media }> - <virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual> + <mk-images images={ p.media }/> </div> <mk-poll if={ p.poll } post={ p }/> </div> @@ -208,11 +208,6 @@ > mk-url-preview margin-top 8px - > .media - > img - display block - max-width 100% - > footer font-size 1.2em diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag index 8e5171c83e..0b4c07906a 100644 --- a/src/web/app/desktop/tags/post-form.tag +++ b/src/web/app/desktop/tags/post-form.tag @@ -1,13 +1,12 @@ <mk-post-form ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }> <div class="content"> <textarea class={ with: (files.length != 0 || poll) } ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea> - <div class="medias { with: poll }" if={ files.length != 0 }> - <ul> - <li each={ files }> + <div class="medias { with: poll }" show={ files.length != 0 }> + <ul ref="media"> + <li each={ files } data-id={ id }> <div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div> <img class="remove" onclick={ removeFile } src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/> </li> - <li class="add" if={ files.length < 4 } title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li> </ul> <p class="remain">{ 4 - files.length }/4</p> </div> @@ -118,8 +117,9 @@ > li display block float left - margin 4px + margin 0 padding 0 + border solid 4px transparent cursor move &:hover > .remove @@ -140,29 +140,6 @@ height 16px cursor pointer - > .add - display block - float left - margin 4px - padding 0 - border dashed 2px rgba($theme-color, 0.2) - cursor pointer - - &:hover - border-color rgba($theme-color, 0.3) - - > i - color rgba($theme-color, 0.4) - - > i - display block - width 60px - height 60px - line-height 60px - text-align center - font-size 1.2em - color rgba($theme-color, 0.2) - > mk-poll-editor background lighten($theme-color, 98%) border solid 1px rgba($theme-color, 0.1) @@ -306,6 +283,7 @@ </style> <script> + import Sortable from 'sortablejs'; import getKao from '../../common/scripts/get-kao'; import notify from '../scripts/notify'; import Autocomplete from '../scripts/autocomplete'; @@ -365,6 +343,10 @@ this.trigger('change-files', this.files); this.update(); } + + new Sortable(this.refs.media, { + animation: 150 + }); }); this.on('unmount', () => { @@ -413,14 +395,17 @@ const data = e.dataTransfer.getData('text'); if (data == null) return false; - // パース - // TODO: Validate JSON - const obj = JSON.parse(data); + try { + // パース + const obj = JSON.parse(data); + + // (ドライブの)ファイルだったら + if (obj.type == 'file') { + this.files.push(obj.file); + this.update(); + } + } catch (e) { - // (ドライブの)ファイルだったら - if (obj.type == 'file') { - this.files.push(obj.file); - this.update(); } }; @@ -483,13 +468,19 @@ this.post = e => { this.wait = true; - const files = this.files && this.files.length > 0 - ? this.files.map(f => f.id) - : undefined; + const files = []; + + if (this.files.length > 0) { + Array.from(this.refs.media.children).forEach(el => { + const id = el.getAttribute('data-id'); + const file = this.files.find(f => f.id == id); + files.push(file); + }); + } this.api('posts/create', { text: this.refs.text.value == '' ? undefined : this.refs.text.value, - media_ids: files, + media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined, reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined, repost_id: this.repost ? this.repost.id : undefined, poll: this.poll ? this.refs.poll.get() : undefined diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag index 52f765d1a1..f7ec85a4fe 100644 --- a/src/web/app/desktop/tags/search-posts.tag +++ b/src/web/app/desktop/tags/search-posts.tag @@ -33,21 +33,22 @@ </style> <script> + import parse from '../../common/scripts/parse-search-query'; + this.mixin('api'); this.query = this.opts.query; this.isLoading = true; this.isEmpty = false; this.moreLoading = false; - this.page = 0; + this.limit = 30; + this.offset = 0; this.on('mount', () => { document.addEventListener('keydown', this.onDocumentKeydown); window.addEventListener('scroll', this.onScroll); - this.api('posts/search', { - query: this.query - }).then(posts => { + this.api('posts/search', parse(this.query)).then(posts => { this.update({ isLoading: false, isEmpty: posts.length == 0 @@ -72,16 +73,16 @@ this.more = () => { if (this.moreLoading || this.isLoading || this.timeline.posts.length == 0) return; + this.offset += this.limit; this.update({ moreLoading: true }); - this.api('posts/search', { - query: this.query, - page: this.page + 1 - }).then(posts => { + return this.api('posts/search', Object.assign({}, parse(this.query), { + limit: this.limit, + offset: this.offset + })).then(posts => { this.update({ - moreLoading: false, - page: page + 1 + moreLoading: false }); this.refs.timeline.prependPosts(posts); }); diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag index 17cc607988..c660a2fe90 100644 --- a/src/web/app/desktop/tags/select-file-from-drive-window.tag +++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag @@ -33,7 +33,7 @@ height 72px background lighten($theme-color, 95%) - .upload + > .upload display inline-block position absolute top 8px @@ -72,8 +72,8 @@ border 2px solid rgba($theme-color, 0.3) border-radius 8px - .ok - .cancel + > .ok + > .cancel display block position absolute bottom 16px @@ -102,7 +102,7 @@ opacity 0.7 cursor default - .ok + > .ok right 16px color $theme-color-foreground background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) @@ -119,7 +119,7 @@ background $theme-color border-color $theme-color - .cancel + > .cancel right 148px color #888 background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag index 46cd405520..457b7e2276 100644 --- a/src/web/app/desktop/tags/settings.tag +++ b/src/web/app/desktop/tags/settings.tag @@ -1,47 +1,35 @@ <mk-settings> <div class="nav"> - <p class={ active: page == 'account' } onmousedown={ setPage.bind(null, 'account') }>%fa:user .fw%アカウント</p> + <p class={ active: page == 'profile' } onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p> <p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p> <p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p> - <p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%ドライブ</p> + <p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p> + <p class={ active: page == 'mute' } onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p> <p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p> <p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p> - <p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }>%fa:sign-in-alt .fw%ログイン履歴</p> - <p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.password%</p> + <p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p> <p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p> + <p class={ active: page == 'other' } onmousedown={ setPage.bind(null, 'other') }>%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p> </div> <div class="pages"> - <section class="account" show={ page == 'account' }> - <h1>アカウント</h1> - <label class="avatar"> - <p>アバター</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - <button class="style-normal" onclick={ avatar }>画像を選択</button> - </label> - <label> - <p>名前</p> - <input ref="accountName" type="text" value={ I.name }/> - </label> - <label> - <p>場所</p> - <input ref="accountLocation" type="text" value={ I.profile.location }/> - </label> - <label> - <p>自己紹介</p> - <textarea ref="accountDescription">{ I.description }</textarea> - </label> - <label> - <p>誕生日</p> - <input ref="accountBirthday" type="date" value={ I.profile.birthday }/> - </label> - <button class="style-primary" onclick={ updateAccount }>保存</button> + <section class="profile" show={ page == 'profile' }> + <h1>%i18n:desktop.tags.mk-settings.profile%</h1> + <mk-profile-setting/> </section> <section class="web" show={ page == 'web' }> <h1>デザイン</h1> - <a href="/i/customize-home">ホームをカスタマイズ</a> + <a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a> </section> - <section class="web" show={ page == 'web' }> + <section class="drive" show={ page == 'drive' }> + <h1>%i18n:desktop.tags.mk-settings.drive%</h1> + <mk-drive-setting/> + </section> + + <section class="mute" show={ page == 'mute' }> + <h1>%i18n:desktop.tags.mk-settings.mute%</h1> + <mk-mute-setting/> </section> <section class="apps" show={ page == 'apps' }> @@ -54,20 +42,30 @@ <mk-twitter-setting/> </section> - <section class="signin" show={ page == 'signin' }> - <h1>ログイン履歴</h1> - <mk-signin-history/> - </section> - - <section class="password" show={ page == 'password' }> + <section class="password" show={ page == 'security' }> <h1>%i18n:desktop.tags.mk-settings.password%</h1> <mk-password-setting/> </section> + <section class="2fa" show={ page == 'security' }> + <h1>%i18n:desktop.tags.mk-settings.2fa%</h1> + <mk-2fa-setting/> + </section> + + <section class="signin" show={ page == 'security' }> + <h1>サインイン履歴</h1> + <mk-signin-history/> + </section> + <section class="api" show={ page == 'api' }> <h1>API</h1> <mk-api-info/> </section> + + <section class="other" show={ page == 'other' }> + <h1>%i18n:desktop.tags.mk-settings.license%</h1> + %license% + </section> </div> <style> :scope @@ -75,25 +73,6 @@ width 100% height 100% - input:not([type]) - input[type='text'] - input[type='password'] - input[type='email'] - input[type='date'] - textarea - padding 8px - width 100% - font-size 16px - color #55595c - border solid 1px #dadada - border-radius 4px - - &:hover - border-color #aeaeae - - &:focus - border-color #aeaeae - > .nav flex 0 0 200px width 100% @@ -128,64 +107,63 @@ overflow auto > section - padding 32px + margin 32px + color #4a535a - // & + section - // margin-top 16px - - h1 + > h1 display block - margin 0 + margin 0 0 1em 0 padding 0 0 8px 0 font-size 1em color #555 border-bottom solid 1px #eee - label - display block - margin 16px 0 - - &:after - content "" - display block - clear both - - > p - margin 0 0 8px 0 - font-weight bold - color #373a3c - - &.checkbox - > input - position absolute - top 0 - left 0 - - &:checked + p - color $theme-color + </style> + <script> + this.page = 'profile'; - > p - width calc(100% - 32px) - margin 0 0 0 32px - font-weight bold + this.setPage = page => { + this.page = page; + }; + </script> +</mk-settings> - &:last-child - font-weight normal - color #999 +<mk-profile-setting> + <label class="avatar ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/> + <button class="ui" onclick={ avatar }>%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.name%</p> + <input ref="accountName" type="text" value={ I.name } class="ui"/> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.location%</p> + <input ref="accountLocation" type="text" value={ I.profile.location } class="ui"/> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.description%</p> + <textarea ref="accountDescription" class="ui">{ I.description }</textarea> + </label> + <label class="ui from group"> + <p>%i18n:desktop.tags.mk-profile-setting.birthday%</p> + <input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/> + </label> + <button class="ui primary" onclick={ updateAccount }>%i18n:desktop.tags.mk-profile-setting.save%</button> + <style> + :scope + display block - &.account - > .general - > .avatar - > img - display block - float left - width 64px - height 64px - border-radius 4px + > .avatar + > img + display inline-block + vertical-align top + width 64px + height 64px + border-radius 4px - > button - float left - margin-left 8px + > button + margin-left 8px </style> <script> @@ -195,12 +173,6 @@ this.mixin('i'); this.mixin('api'); - this.page = 'account'; - - this.setPage = page => { - this.page = page; - }; - this.avatar = () => { updateAvatar(this.I); }; @@ -216,21 +188,25 @@ }); }; </script> -</mk-settings> +</mk-profile-setting> <mk-api-info> - <p>Token:<code>{ I.token }</code></p> - <p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p> - <p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p> - <p>万が一このトークンが漏れたりその可能性がある場合は<a class="regenerate" onclick={ regenerateToken }>トークンを再生成</a>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)</p> + <p>Token: <code>{ I.token }</code></p> + <p>%i18n:desktop.tags.mk-api-info.intro%</p> + <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div> + <p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p> + <button class="ui" onclick={ regenerateToken }>%i18n:desktop.tags.mk-api-info.regenerate-token%</button> <style> :scope display block color #4a535a code - padding 4px + display inline-block + padding 4px 6px + color #555 background #eee + border-radius 2px </style> <script> import passwordDialog from '../scripts/password-dialog'; @@ -239,7 +215,7 @@ this.mixin('api'); this.regenerateToken = () => { - passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => { + passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => { this.api('i/regenerate_token', { password: password }); @@ -249,7 +225,7 @@ </mk-api-info> <mk-password-setting> - <button onclick={ reset }>%i18n:desktop.tags.mk-password-setting.reset%</button> + <button onclick={ reset } class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button> <style> :scope display block @@ -285,3 +261,166 @@ }; </script> </mk-password-setting> + +<mk-2fa-setting> + <p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p> + <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div> + <p if={ !data && !I.two_factor_enabled }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p> + <virtual if={ I.two_factor_enabled }> + <p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p> + <button onclick={ unregister } class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button> + </virtual> + <div if={ data }> + <ol> + <li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li> + <li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li> + <li>%i18n:desktop.tags.mk-2fa-setting.done%<br> + <input type="number" ref="token" class="ui"> + <button onclick={ submit } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button> + </li> + </ol> + <div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div> + </div> + <style> + :scope + display block + color #4a535a + + </style> + <script> + import passwordDialog from '../scripts/password-dialog'; + import notify from '../scripts/notify'; + + this.mixin('i'); + this.mixin('api'); + + this.register = () => { + passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => { + this.api('i/2fa/register', { + password: password + }).then(data => { + this.update({ + data: data + }); + }); + }); + }; + + this.unregister = () => { + passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => { + this.api('i/2fa/unregister', { + password: password + }).then(data => { + notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%'); + this.I.two_factor_enabled = false; + this.I.update(); + }); + }); + }; + + this.submit = () => { + this.api('i/2fa/done', { + token: this.refs.token.value + }).then(() => { + notify('%i18n:desktop.tags.mk-2fa-setting.success%'); + this.I.two_factor_enabled = true; + this.I.update(); + }).catch(() => { + notify('%i18n:desktop.tags.mk-2fa-setting.failed%'); + }); + }; + </script> +</mk-2fa-setting> + +<mk-drive-setting> + <svg viewBox="0 0 1 1" preserveAspectRatio="none"> + <circle + riot-r={ r } + cx="50%" cy="50%" + fill="none" + stroke-width="0.1" + stroke="rgba(0, 0, 0, 0.05)"/> + <circle + riot-r={ r } + cx="50%" cy="50%" + riot-stroke-dasharray={ Math.PI * (r * 2) } + riot-stroke-dashoffset={ strokeDashoffset } + fill="none" + stroke-width="0.1" + riot-stroke={ color }/> + <text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (usageP * 100).toFixed(0) }%</text> + </svg> + + <style> + :scope + display block + color #4a535a + + > svg + display block + height 128px + + > circle + transform-origin center + transform rotate(-90deg) + transition stroke-dashoffset 0.5s ease + + > text + font-size 0.15px + fill rgba(0, 0, 0, 0.6) + + </style> + <script> + this.mixin('api'); + + this.r = 0.4; + + this.on('mount', () => { + this.api('drive').then(info => { + const usageP = info.usage / info.capacity; + const color = `hsl(${180 - (usageP * 180)}, 80%, 70%)`; + const strokeDashoffset = (1 - usageP) * (Math.PI * (this.r * 2)); + + this.update({ + color, + strokeDashoffset, + usageP, + usage: info.usage, + capacity: info.capacity + }); + }); + }); + </script> +</mk-drive-setting> + +<mk-mute-setting> + <div class="none ui info" if={ !fetching && users.length == 0 }> + <p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p> + </div> + <div class="users" if={ users.length != 0 }> + <div each={ user in users }> + <p><b>{ user.name }</b> @{ user.username }</p> + </div> + </div> + + <style> + :scope + display block + + </style> + <script> + this.mixin('api'); + + this.apps = []; + this.fetching = true; + + this.on('mount', () => { + this.api('mute/list').then(x => { + this.update({ + fetching: false, + users: x.users + }); + }); + }); + </script> +</mk-mute-setting> diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag index 8989ff1c5b..1a81b545b6 100644 --- a/src/web/app/desktop/tags/sub-post-content.tag +++ b/src/web/app/desktop/tags/sub-post-content.tag @@ -8,7 +8,7 @@ </div> <details if={ post.media }> <summary>({ post.media.length }つのメディア)</summary> - <mk-images-viewer images={ post.media }/> + <mk-images images={ post.media }/> </details> <details if={ post.poll }> <summary>投票</summary> diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag index 08e658a3c6..ed77a9e608 100644 --- a/src/web/app/desktop/tags/timeline.tag +++ b/src/web/app/desktop/tags/timeline.tag @@ -120,7 +120,7 @@ <a class="quote" if={ p.repost != null }>RP:</a> </div> <div class="media" if={ p.media }> - <mk-images-viewer images={ p.media }/> + <mk-images images={ p.media }/> </div> <mk-poll if={ p.poll } post={ p } ref="pollViewer"/> <div class="repost" if={ p.repost }>%fa:quote-right -flip-h% @@ -357,11 +357,6 @@ background $theme-color border-radius 4px - > .media - > img - display block - max-width 100% - > mk-poll font-size 80% diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index 052568062a..3dfdeec01c 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -146,6 +146,9 @@ color #9eaba8 pointer-events none + > * + vertical-align middle + > input user-select text cursor auto @@ -162,7 +165,7 @@ transition color 0.5s ease, border 0.5s ease font-family FontAwesome, sans-serif - &:placeholder-shown + &::placeholder color #9eaba8 &:hover @@ -177,7 +180,7 @@ this.onsubmit = e => { e.preventDefault(); - this.page('/search:' + this.refs.q.value); + this.page('/search?q=' + encodeURIComponent(this.refs.q.value)); }; </script> </mk-ui-header-search> diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag index 2b05f6b5cf..134aeee28c 100644 --- a/src/web/app/desktop/tags/user-timeline.tag +++ b/src/web/app/desktop/tags/user-timeline.tag @@ -96,7 +96,7 @@ this.fetch = cb => { this.api('users/posts', { user_id: this.user.id, - max_date: this.date ? this.date.getTime() : undefined, + until_date: this.date ? this.date.getTime() : undefined, with_replies: this.mode == 'with-replies' }).then(posts => { this.update({ @@ -116,7 +116,7 @@ this.api('users/posts', { user_id: this.user.id, with_replies: this.mode == 'with-replies', - max_id: this.refs.timeline.tail().id + until_id: this.refs.timeline.tail().id }).then(posts => { this.update({ moreLoading: false diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag index b4db47f9dd..b29d1eaebc 100644 --- a/src/web/app/desktop/tags/user.tag +++ b/src/web/app/desktop/tags/user.tag @@ -226,7 +226,9 @@ <mk-user-profile> <div class="friend-form" if={ SIGNIN && I.id != user.id }> <mk-big-follow-button user={ user }/> - <p class="followed" if={ user.is_followed }>フォローされています</p> + <p class="followed" if={ user.is_followed }>%i18n:desktop.tags.mk-user.follows-you%</p> + <p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a onclick={ unmute }>%i18n:desktop.tags.mk-user.unmute%</a></p> + <p if={ !user.is_muted }><a onclick={ mute }>%i18n:desktop.tags.mk-user.mute%</a></p> </div> <div class="description" if={ user.description }>{ user.description }</div> <div class="birthday" if={ user.profile.birthday }> @@ -311,6 +313,7 @@ this.age = require('s-age'); this.mixin('i'); + this.mixin('api'); this.user = this.opts.user; @@ -325,6 +328,28 @@ user: this.user }); }; + + this.mute = () => { + this.api('mute/create', { + user_id: this.user.id + }).then(() => { + this.user.is_muted = true; + this.update(); + }, e => { + alert('error'); + }); + }; + + this.unmute = () => { + this.api('mute/delete', { + user_id: this.user.id + }).then(() => { + this.user.is_muted = false; + this.update(); + }, e => { + alert('error'); + }); + }; </script> </mk-user-profile> diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl new file mode 100644 index 0000000000..058271876b --- /dev/null +++ b/src/web/app/desktop/ui.styl @@ -0,0 +1,122 @@ +@import "../../const" + +button + font-family sans-serif + + * + pointer-events none + +button.ui +.button.ui + display inline-block + cursor pointer + padding 0 14px + margin 0 + min-width 100px + line-height 38px + font-size 14px + color #888 + text-decoration none + background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px #e2e2e2 + border-radius 4px + outline none + + &:focus + &:after + content "" + pointer-events none + position absolute + top -5px + right -5px + bottom -5px + left -5px + border 2px solid rgba($theme-color, 0.3) + border-radius 8px + + &:disabled + opacity 0.7 + cursor default + + &:hover + background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color #dcdcdc + + &:active + background #ececec + border-color #dcdcdc + + &.primary + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:not(:disabled) + font-weight bold + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + +input:not([type]).ui +input[type='text'].ui +input[type='password'].ui +input[type='email'].ui +input[type='date'].ui +input[type='number'].ui +textarea.ui + display block + padding 10px + width 100% + height 40px + font-family sans-serif + font-size 16px + color #55595c + border solid 1px #dadada + border-radius 4px + + &:hover + border-color #b0b0b0 + + &:focus + border-color $theme-color + +textarea.ui + min-width 100% + max-width 100% + min-height 64px + +.ui.info + display block + margin 1em 0 + padding 0 1em + font-size 90% + color rgba(#000, 0.87) + background #f8f8f9 + border solid 1px rgba(34, 36, 38, 0.22) + border-radius 4px + + > p + opacity 0.8 + + > [data-fa]:first-child + margin-right 0.25em + + &.warn + color #573a08 + background #FFFAF3 + border-color #C9BA9B + +.ui.from.group + display block + margin 16px 0 + + > p:first-child + margin 0 0 6px 0 + font-size 90% + font-weight bold + color rgba(#373a3c, 0.9) diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts index 0358d10e9e..afb9aa6201 100644 --- a/src/web/app/mobile/router.ts +++ b/src/web/app/mobile/router.ts @@ -19,12 +19,11 @@ export default (mios: MiOS) => { route('/i/settings', settings); route('/i/settings/profile', settingsProfile); route('/i/settings/signin-history', settingsSignin); - route('/i/settings/api', settingsApi); route('/i/settings/twitter', settingsTwitter); route('/i/settings/authorized-apps', settingsAuthorizedApps); route('/post/new', newPost); route('/post::post', post); - route('/search::query', search); + route('/search', search); route('/:user', user.bind(null, 'overview')); route('/:user/graphs', user.bind(null, 'graphs')); route('/:user/followers', userFollowers); @@ -74,10 +73,6 @@ export default (mios: MiOS) => { mount(document.createElement('mk-signin-history-page')); } - function settingsApi() { - mount(document.createElement('mk-api-info-page')); - } - function settingsTwitter() { mount(document.createElement('mk-twitter-setting-page')); } @@ -88,7 +83,7 @@ export default (mios: MiOS) => { function search(ctx) { const el = document.createElement('mk-search-page'); - el.setAttribute('query', ctx.params.query); + el.setAttribute('query', ctx.querystring.substr(2)); mount(el); } diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag index 8350ce07e1..2a3ff23bfa 100644 --- a/src/web/app/mobile/tags/drive.tag +++ b/src/web/app/mobile/tags/drive.tag @@ -1,9 +1,9 @@ <mk-drive> <nav ref="nav"> - <p onclick={ goRoot }>%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</p> + <a onclick={ goRoot } href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a> <virtual each={ folder in hierarchyFolders }> <span>%fa:angle-right%</span> - <p onclick={ move }>{ folder.name }</p> + <a onclick={ move } href="/i/drive/folder/{ folder.id }">{ folder.name }</a> </virtual> <virtual if={ folder != null }> <span>%fa:angle-right%</span> @@ -74,9 +74,12 @@ border-bottom solid 1px rgba(0, 0, 0, 0.13) > p + > a display inline margin 0 padding 0 + text-decoration none !important + color inherit &:last-child font-weight bold @@ -245,7 +248,9 @@ }; this.move = ev => { + ev.preventDefault(); this.cd(ev.item.folder); + return false; }; this.cd = (target, silent = false) => { @@ -329,7 +334,9 @@ this.prependFile = file => this.addFile(file, true); this.prependFolder = file => this.addFolder(file, true); - this.goRoot = () => { + this.goRoot = ev => { + ev.preventDefault(); + if (this.folder || this.file) { this.update({ file: null, @@ -339,6 +346,8 @@ this.trigger('move-root'); this.fetch(); } + + return false; }; this.fetch = () => { @@ -421,7 +430,7 @@ this.api('drive/files', { folder_id: this.folder ? this.folder.id : null, limit: max + 1, - max_id: this.files[this.files.length - 1].id + until_id: this.files[this.files.length - 1].id }).then(files => { if (files.length == max + 1) { this.moreFiles = true; diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag index da895359dc..259873d95c 100644 --- a/src/web/app/mobile/tags/drive/file-viewer.tag +++ b/src/web/app/mobile/tags/drive/file-viewer.tag @@ -1,6 +1,11 @@ <mk-drive-file-viewer> <div class="preview"> - <img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name }> + <img if={ kind == 'image' } ref="img" + src={ file.url } + alt={ file.name } + title={ file.name } + onload={ onImageLoaded } + style="background-color:rgb({ file.properties.average_color.join(',') })"> <virtual if={ kind != 'image' }>%fa:file%</virtual> <footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }> <span class="size"> @@ -39,6 +44,14 @@ </button> </div> </div> + <div class="exif" show={ exif }> + <div> + <p> + %fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif% + </p> + <pre ref="exif" class="json">{ exif ? JSON.stringify(exif, null, 2) : '' }</pre> + </div> + </div> <div class="hash"> <div> <p> @@ -178,12 +191,45 @@ white-space nowrap overflow auto font-size 0.8em + color #222 + border solid 1px #dfdfdf + border-radius 2px + background #f5f5f5 + + > .exif + padding 14px + border-top solid 1px #dfdfdf + + > div + max-width 500px + margin 0 auto + + > p + display block + margin 0 + padding 0 + color #555 + font-size 0.9em + + > [data-fa] + margin-right 4px + + > pre + display block + width 100% + margin 6px 0 0 0 + padding 8px + height 128px + overflow auto + font-size 0.9em border solid 1px #dfdfdf border-radius 2px background #f5f5f5 </style> <script> + import EXIF from 'exif-js'; + import hljs from 'highlight.js'; import bytesToSize from '../../../common/scripts/bytes-to-size'; import gcd from '../../../common/scripts/gcd'; @@ -195,6 +241,17 @@ this.file = this.opts.file; this.kind = this.file.type.split('/')[0]; + this.onImageLoaded = () => { + const self = this; + EXIF.getData(this.refs.img, function() { + const allMetaData = EXIF.getAllTags(this); + self.update({ + exif: allMetaData + }); + hljs.highlightBlock(self.refs.exif); + }); + }; + this.rename = () => { const name = window.prompt('名前を変更', this.file.name); if (name == null || name == '' || name == this.file.name) return; diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag index 93a8dba7e5..684df7dd08 100644 --- a/src/web/app/mobile/tags/drive/file.tag +++ b/src/web/app/mobile/tags/drive/file.tag @@ -1,119 +1,123 @@ -<mk-drive-file onclick={ onclick } data-is-selected={ isSelected }> - <div class="container"> - <div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div> - <div class="body"> - <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p> - <!-- - if file.tags.length > 0 - ul.tags - each tag in file.tags - li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name - --> - <footer> - <p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p> - <p class="separator"></p> - <p class="data-size">{ bytesToSize(file.datasize) }</p> - <p class="separator"></p> - <p class="created-at"> - %fa:R clock%<mk-time time={ file.created_at }/> - </p> - </footer> +<mk-drive-file data-is-selected={ isSelected }> + <a onclick={ onclick } href="/i/drive/file/{ file.id }"> + <div class="container"> + <div class="thumbnail" style={ thumbnail }></div> + <div class="body"> + <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p> + <!-- + if file.tags.length > 0 + ul.tags + each tag in file.tags + li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name + --> + <footer> + <p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p> + <p class="separator"></p> + <p class="data-size">{ bytesToSize(file.datasize) }</p> + <p class="separator"></p> + <p class="created-at"> + %fa:R clock%<mk-time time={ file.created_at }/> + </p> + </footer> + </div> </div> - </div> + </a> <style> :scope display block - &, * - user-select none + > a + display block + text-decoration none !important - * - pointer-events none + * + user-select none + pointer-events none - > .container - max-width 500px - margin 0 auto - padding 16px + > .container + max-width 500px + margin 0 auto + padding 16px - &:after - content "" - display block - clear both - - > .thumbnail - display block - float left - width 64px - height 64px - background-size cover - background-position center center - - > .body - display block - float left - width calc(100% - 74px) - margin-left 10px - - > .name + &:after + content "" display block - margin 0 - padding 0 - font-size 0.9em - font-weight bold - color #555 - text-overflow ellipsis - overflow-wrap break-word + clear both - > .ext - opacity 0.5 - - > .tags + > .thumbnail display block - margin 4px 0 0 0 - padding 0 - list-style none - font-size 0.5em - - > .tag - display inline-block - margin 0 5px 0 0 - padding 1px 5px - border-radius 2px + float left + width 64px + height 64px + background-size cover + background-position center center - > footer + > .body display block - margin 4px 0 0 0 - font-size 0.7em + float left + width calc(100% - 74px) + margin-left 10px - > .separator - display inline - margin 0 - padding 0 4px - color #CDCDCD - - > .type - display inline + > .name + display block margin 0 padding 0 - color #9D9D9D + font-size 0.9em + font-weight bold + color #555 + text-overflow ellipsis + overflow-wrap break-word - > mk-file-type-icon - margin-right 4px + > .ext + opacity 0.5 - > .data-size - display inline - margin 0 + > .tags + display block + margin 4px 0 0 0 padding 0 - color #9D9D9D + list-style none + font-size 0.5em - > .created-at - display inline - margin 0 - padding 0 - color #BDBDBD + > .tag + display inline-block + margin 0 5px 0 0 + padding 1px 5px + border-radius 2px + + > footer + display block + margin 4px 0 0 0 + font-size 0.7em + + > .separator + display inline + margin 0 + padding 0 4px + color #CDCDCD + + > .type + display inline + margin 0 + padding 0 + color #9D9D9D - > [data-fa] - margin-right 2px + > mk-file-type-icon + margin-right 4px + + > .data-size + display inline + margin 0 + padding 0 + color #9D9D9D + + > .created-at + display inline + margin 0 + padding 0 + color #BDBDBD + + > [data-fa] + margin-right 2px &[data-is-selected] background $theme-color @@ -128,14 +132,20 @@ this.browser = this.parent; this.file = this.opts.file; + this.thumbnail = { + 'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent', + 'background-image': `url(${this.file.url}?thumbnail&size=128)` + }; this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id); this.browser.on('change-selection', selections => { this.isSelected = selections.some(f => f.id == this.file.id); }); - this.onclick = () => { + this.onclick = ev => { + ev.preventDefault(); this.browser.chooseFile(this.file); + return false; }; </script> </mk-drive-file> diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag index 196e7e326b..6125e0b254 100644 --- a/src/web/app/mobile/tags/drive/folder.tag +++ b/src/web/app/mobile/tags/drive/folder.tag @@ -1,47 +1,53 @@ -<mk-drive-folder onclick={ onclick }> - <div class="container"> - <p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right% - </div> +<mk-drive-folder> + <a onclick={ onclick } href="/i/drive/folder/{ folder.id }"> + <div class="container"> + <p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right% + </div> + </a> <style> :scope display block - color #777 - &, * - user-select none + > a + display block + color #777 + text-decoration none !important - * - pointer-events none + * + user-select none + pointer-events none - > .container - max-width 500px - margin 0 auto - padding 16px + > .container + max-width 500px + margin 0 auto + padding 16px - > .name - display block - margin 0 - padding 0 + > .name + display block + margin 0 + padding 0 + + > [data-fa] + margin-right 6px > [data-fa] - margin-right 6px + position absolute + top 0 + bottom 0 + right 20px - > [data-fa] - position absolute - top 0 - bottom 0 - right 8px - margin auto 0 auto 0 - width 1em - height 1em + > * + height 100% </style> <script> this.browser = this.parent; this.folder = this.opts.folder; - this.onclick = () => { + this.onclick = ev => { + ev.preventDefault(); this.browser.cd(this.folder); + return false; }; </script> </mk-drive-folder> diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag index e96823fa10..397d2b3980 100644 --- a/src/web/app/mobile/tags/home-timeline.tag +++ b/src/web/app/mobile/tags/home-timeline.tag @@ -47,7 +47,7 @@ this.more = () => { return this.api('posts/timeline', { - max_id: this.refs.timeline.tail().id + until_id: this.refs.timeline.tail().id }); }; diff --git a/src/web/app/mobile/tags/images-viewer.tag b/src/web/app/mobile/tags/images-viewer.tag deleted file mode 100644 index 8ef4a50be0..0000000000 --- a/src/web/app/mobile/tags/images-viewer.tag +++ /dev/null @@ -1,26 +0,0 @@ -<mk-images-viewer> - <div class="image" ref="view" onclick={ click }><img ref="img" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div> - <style> - :scope - display block - overflow hidden - border-radius 4px - - > .image - - > img - display block - max-height 256px - max-width 100% - margin 0 auto - - </style> - <script> - this.images = this.opts.images; - this.image = this.images[0]; - - this.click = () => { - window.open(this.image.url); - }; - </script> -</mk-images-viewer> diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag new file mode 100644 index 0000000000..5899364aef --- /dev/null +++ b/src/web/app/mobile/tags/images.tag @@ -0,0 +1,82 @@ +<mk-images> + <virtual each={ image in images }> + <mk-images-image image={ image }/> + </virtual> + <style> + :scope + display grid + grid-gap 4px + height 256px + + @media (max-width 500px) + height 192px + </style> + <script> + this.images = this.opts.images; + + this.on('mount', () => { + if (this.images.length == 1) { + this.root.style.gridTemplateRows = '1fr'; + + this.tags['mk-images-image'].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'].root.style.gridRow = '1 / 2'; + } else if (this.images.length == 2) { + this.root.style.gridTemplateColumns = '1fr 1fr'; + this.root.style.gridTemplateRows = '1fr'; + + this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2'; + } else if (this.images.length == 3) { + this.root.style.gridTemplateColumns = '1fr 0.5fr'; + this.root.style.gridTemplateRows = '1fr 1fr'; + + this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3'; + this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3'; + } else if (this.images.length == 4) { + this.root.style.gridTemplateColumns = '1fr 1fr'; + this.root.style.gridTemplateRows = '1fr 1fr'; + + this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2'; + this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2'; + this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3'; + this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3'; + this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3'; + } + }); + </script> +</mk-images> + +<mk-images-image> + <a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a> + <style> + :scope + display block + overflow hidden + border-radius 4px + + > a + display block + overflow hidden + width 100% + height 100% + background-position center + background-size cover + + </style> + <script> + this.image = this.opts.image; + this.styles = { + 'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent', + 'background-image': `url(${this.image.url}?thumbnail&size=512)` + }; + </script> +</mk-images-image> diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts index 19952c20cd..20934cdd8d 100644 --- a/src/web/app/mobile/tags/index.ts +++ b/src/web/app/mobile/tags/index.ts @@ -14,7 +14,6 @@ require('./page/search.tag'); require('./page/settings.tag'); require('./page/settings/profile.tag'); require('./page/settings/signin.tag'); -require('./page/settings/api.tag'); require('./page/settings/authorized-apps.tag'); require('./page/settings/twitter.tag'); require('./page/messaging.tag'); @@ -25,7 +24,7 @@ require('./home-timeline.tag'); require('./timeline.tag'); require('./post-preview.tag'); require('./sub-post-content.tag'); -require('./images-viewer.tag'); +require('./images.tag'); require('./drive.tag'); require('./drive-selector.tag'); require('./drive-folder-selector.tag'); diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag index c3500d1b84..742cc45145 100644 --- a/src/web/app/mobile/tags/notifications.tag +++ b/src/web/app/mobile/tags/notifications.tag @@ -146,7 +146,7 @@ this.api('i/notifications', { limit: max + 1, - max_id: this.notifications[this.notifications.length - 1].id + until_id: this.notifications[this.notifications.length - 1].id }).then(notifications => { if (notifications.length == max + 1) { this.moreNotifications = true; diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag index 380fb780bc..191874caf9 100644 --- a/src/web/app/mobile/tags/page/entrance.tag +++ b/src/web/app/mobile/tags/page/entrance.tag @@ -8,7 +8,7 @@ </div> </main> <footer> - <mk-copyright/> + <p class="c">{ _COPYRIGHT_ }</p> </footer> <style> :scope @@ -34,7 +34,7 @@ margin 16px auto 0 auto > footer - > mk-copyright + > .c margin 0 text-align center line-height 64px diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag index 9789782144..9a73b0af3c 100644 --- a/src/web/app/mobile/tags/page/settings.tag +++ b/src/web/app/mobile/tags/page/settings.tag @@ -24,7 +24,6 @@ <li><a href="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</a></li> <li><a href="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</a></li> <li><a href="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</a></li> - <li><a href="./settings/api">%fa:key%%i18n:mobile.tags.mk-settings-page.api%%fa:angle-right%</a></li> </ul> <ul> <li><a onclick={ signout }>%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li> diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag deleted file mode 100644 index 8de0e96963..0000000000 --- a/src/web/app/mobile/tags/page/settings/api.tag +++ /dev/null @@ -1,36 +0,0 @@ -<mk-api-info-page> - <mk-ui ref="ui"> - <mk-api-info/> - </mk-ui> - <style> - :scope - display block - </style> - <script> - import ui from '../../../scripts/ui-event'; - - this.on('mount', () => { - document.title = 'Misskey | API'; - ui.trigger('title', '%fa:key%API'); - }); - </script> -</mk-api-info-page> - -<mk-api-info> - <p>Token:<code>{ I.token }</code></p> - <p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p> - <p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p> - <p>万が一このトークンが漏れたりその可能性がある場合はデスクトップ版Misskeyから再生成できます。</p> - <style> - :scope - display block - color #4a535a - - code - padding 4px - background #eee - </style> - <script> - this.mixin('i'); - </script> -</mk-api-info> diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag index 9f212a2496..1816d1bf93 100644 --- a/src/web/app/mobile/tags/post-detail.tag +++ b/src/web/app/mobile/tags/post-detail.tag @@ -34,7 +34,7 @@ <div class="body"> <div class="text" ref="text"></div> <div class="media" if={ p.media }> - <virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual> + <mk-images images={ p.media }/> </div> <mk-poll if={ p.poll } post={ p }/> </div> diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag index 3ac7296f73..05466a6ec2 100644 --- a/src/web/app/mobile/tags/post-form.tag +++ b/src/web/app/mobile/tags/post-form.tag @@ -9,12 +9,11 @@ <div class="form"> <mk-post-preview if={ opts.reply } post={ opts.reply }/> <textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea> - <div class="attaches" if={ files.length != 0 }> + <div class="attaches" show={ files.length != 0 }> <ul class="files" ref="attaches"> - <li class="file" each={ files }> - <div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div> + <li class="file" each={ files } data-id={ id }> + <div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" onclick={ removeFile }></div> </li> - <li class="add" if={ files.length < 4 } title="%i18n:mobile.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li> </ul> </div> <mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/> @@ -93,12 +92,9 @@ > .file display block float left - margin 4px + margin 0 padding 0 - cursor move - - &:hover > .remove - display block + border solid 4px transparent > .img width 64px @@ -106,38 +102,6 @@ background-size cover background-position center center - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - - > .add - display block - float left - margin 4px - padding 0 - border dashed 2px rgba($theme-color, 0.2) - cursor pointer - - &:hover - border-color rgba($theme-color, 0.3) - - > [data-fa] - color rgba($theme-color, 0.4) - - > [data-fa] - display block - width 60px - height 60px - line-height 60px - text-align center - font-size 1.2em - color rgba($theme-color, 0.2) - > mk-uploader margin 8px 0 0 0 padding 8px @@ -181,6 +145,7 @@ </style> <script> + import Sortable from 'sortablejs'; import getKao from '../../common/scripts/get-kao'; this.mixin('api'); @@ -200,6 +165,10 @@ }); this.refs.text.focus(); + + new Sortable(this.refs.attaches, { + animation: 150 + }); }); this.onkeydown = e => { @@ -247,6 +216,13 @@ this.update(); }; + this.removeFile = e => { + const file = e.item; + this.files = this.files.filter(x => x.id != file.id); + this.trigger('change-files', this.files); + this.update(); + }; + this.addPoll = () => { this.poll = true; }; @@ -258,15 +234,23 @@ }; this.post = () => { - this.wait = true; + this.update({ + wait: true + }); - const files = this.files && this.files.length > 0 - ? this.files.map(f => f.id) - : undefined; + const files = []; + + if (this.files.length > 0) { + Array.from(this.refs.attaches.children).forEach(el => { + const id = el.getAttribute('data-id'); + const file = this.files.find(f => f.id == id); + files.push(file); + }); + } this.api('posts/create', { text: this.refs.text.value == '' ? undefined : this.refs.text.value, - media_ids: files, + media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined, reply_id: opts.reply ? opts.reply.id : undefined, poll: this.poll ? this.refs.poll.get() : undefined }).then(data => { diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag index 967764bc2c..3e3c034f21 100644 --- a/src/web/app/mobile/tags/search-posts.tag +++ b/src/web/app/mobile/tags/search-posts.tag @@ -15,30 +15,28 @@ width calc(100% - 32px) </style> <script> + import parse from '../../common/scripts/parse-search-query'; + this.mixin('api'); - this.max = 30; + this.limit = 30; this.offset = 0; this.query = this.opts.query; - this.withMedia = this.opts.withMedia; this.init = new Promise((res, rej) => { - this.api('posts/search', { - query: this.query - }).then(posts => { + this.api('posts/search', parse(this.query)).then(posts => { res(posts); this.trigger('loaded'); }); }); this.more = () => { - this.offset += this.max; - return this.api('posts/search', { - query: this.query, - max: this.max, + this.offset += this.limit; + return this.api('posts/search', Object.assign({}, parse(this.query), { + limit: this.limit, offset: this.offset - }); + })); }; </script> </mk-search-posts> diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag index 9436b6c1d7..adeb84dea0 100644 --- a/src/web/app/mobile/tags/sub-post-content.tag +++ b/src/web/app/mobile/tags/sub-post-content.tag @@ -2,7 +2,7 @@ <div class="body"><a class="reply" if={ post.reply_id }>%fa:reply%</a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div> <details if={ post.media }> <summary>({ post.media.length }個のメディア)</summary> - <mk-images-viewer images={ post.media }/> + <mk-images images={ post.media }/> </details> <details if={ post.poll }> <summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary> diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag index 19f90a1c11..9e85f97da3 100644 --- a/src/web/app/mobile/tags/timeline.tag +++ b/src/web/app/mobile/tags/timeline.tag @@ -172,7 +172,7 @@ <a class="quote" if={ p.repost != null }>RP:</a> </div> <div class="media" if={ p.media }> - <mk-images-viewer images={ p.media }/> + <mk-images images={ p.media }/> </div> <mk-poll if={ p.poll } post={ p } ref="pollViewer"/> <span class="app" if={ p.app }>via <b>{ p.app.name }</b></span> diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag index 62e128489a..77ad14530d 100644 --- a/src/web/app/mobile/tags/ui.tag +++ b/src/web/app/mobile/tags/ui.tag @@ -248,7 +248,7 @@ <li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li> </ul> </div> - <a href={ _ABOUT_URL_ }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a> + <a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a> </div> <style> :scope @@ -359,6 +359,8 @@ this.connection = this.stream.getConnection(); this.connectionId = this.stream.use(); + this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`; + this.on('mount', () => { this.connection.on('read_all_notifications', this.onReadAllNotifications); this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); @@ -411,7 +413,7 @@ this.search = () => { const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%'); if (query == null || query == '') return; - this.page('/search:' + query); + this.page('/search?q=' + encodeURIComponent(query)); }; </script> </mk-ui-nav> diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag index 4dbe719f5a..86ead5971f 100644 --- a/src/web/app/mobile/tags/user-timeline.tag +++ b/src/web/app/mobile/tags/user-timeline.tag @@ -26,7 +26,7 @@ return this.api('users/posts', { user_id: this.user.id, with_media: this.withMedia, - max_id: this.refs.timeline.tail().id + until_id: this.refs.timeline.tail().id }); }; </script> diff --git a/src/web/assets/code-highlight.css b/src/web/assets/code-highlight.css new file mode 100644 index 0000000000..f0807dc9c3 --- /dev/null +++ b/src/web/assets/code-highlight.css @@ -0,0 +1,93 @@ +.hljs { + font-family: Consolas, 'Courier New', Courier, Monaco, monospace; +} + +.hljs, +.hljs-subst { + color: #444; +} + +.hljs-comment { + color: #888888; +} + +.hljs-keyword { + color: #2973b7; +} + +.hljs-number { + color: #ae81ff; +} + +.hljs-string { + color: #e96900; +} + +.hljs-regexp { + color: #e9003f; +} + +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold; +} + +.hljs-type, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060; +} + +/* Language color: hue: 90; */ + +.hljs-literal { + color: #78A960; +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/src/web/assets/recover.html b/src/web/assets/recover.html index 35afd2adf2..4922b68d35 100644 --- a/src/web/assets/recover.html +++ b/src/web/assets/recover.html @@ -6,7 +6,7 @@ <title>Misskeyのリカバリ</title> <script> - const yn = window.confirm('キャッシュをクリアしますか?\n\nDo you want to clear caches?'); + const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches?'); if (yn) { try { diff --git a/src/web/const.styl b/src/web/const.styl new file mode 100644 index 0000000000..b6560701d9 --- /dev/null +++ b/src/web/const.styl @@ -0,0 +1,4 @@ +json('../const.json') + +$theme-color = themeColor +$theme-color-foreground = themeColorForeground diff --git a/src/web/docs/about.en.pug b/src/web/docs/about.en.pug new file mode 100644 index 0000000000..893d9dd6a1 --- /dev/null +++ b/src/web/docs/about.en.pug @@ -0,0 +1,3 @@ +h1 About Misskey + +p Misskey is a mini blog SNS. diff --git a/src/web/docs/about.ja.pug b/src/web/docs/about.ja.pug new file mode 100644 index 0000000000..fec933b0c6 --- /dev/null +++ b/src/web/docs/about.ja.pug @@ -0,0 +1,3 @@ +h1 Misskeyについて + +p MisskeyはミニブログSNSです。 diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug new file mode 100644 index 0000000000..2bb08f7f32 --- /dev/null +++ b/src/web/docs/api.ja.pug @@ -0,0 +1,103 @@ +h1 Misskey API + +p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。 +p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。 + +section + h2 自分の所有するアカウントからAPIにアクセスする場合 + p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。 + p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。 + div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 + p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 + +section + h2 アプリケーションからAPIにアクセスする場合 + p + | 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、 + | アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、 + | そのトークンをリクエストのパラメータに含める必要があります。 + div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます + + p それでは、アクセストークンを取得するまでの流れを説明します。 + + section + h3 1.アプリケーションを登録する + p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 + p + a(href=common.config.dev_url, target="_blank") デベロッパーセンター + | にアクセスし、「アプリ > アプリ作成」に進みます。 + | フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです: + + table + thead + tr + th 名前 + th 説明 + tbody + tr + td アプリケーション名 + td あなたのアプリの名称。 + tr + td アプリの概要 + td あなたのアプリの簡単な説明や紹介。 + tr + td コールバックURL + td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 + tr + td 権限 + td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 + + p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 + div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。 + + section + h3 2.ユーザーに認証させる + p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。 + p + | 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。 + | リクエスト形式はJSONで、メソッドはPOSTです。 + | レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。 + + p + | あなたのアプリがコールバックURLを設定している場合、 + | ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 + + p + | あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 + + section + h3 3.ユーザーのアクセストークンを取得する + p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します: + table + thead + tr + th 名前 + th 型 + th 説明 + tbody + tr + td app_secret + td string + td あなたのアプリのシークレットキー + tr + td token + td string + td セッションのトークン + p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! + + p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。 + + p 「i」パラメータの生成方法を擬似コードで表すと次のようになります: + pre: code + | const i = sha256(accessToken + secretKey); + + p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 + +section + h2 Misskey APIの利用 + p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。 + p APIリファレンスもご確認ください。 + + section + h3 レートリミット + p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。 diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml new file mode 100644 index 0000000000..5e2307dab4 --- /dev/null +++ b/src/web/docs/api/endpoints/posts/create.yaml @@ -0,0 +1,53 @@ +endpoint: "posts/create" + +desc: + ja: "投稿します。" + en: "Compose new post." + +params: + - name: "text" + type: "string" + optional: true + desc: + ja: "投稿の本文" + en: "The text of your post" + - name: "media_ids" + type: "id(DriveFile)[]" + optional: true + desc: + ja: "添付するメディア(1~4つ)" + en: "Media you want to attach (1~4)" + - name: "reply_id" + type: "id(Post)" + optional: true + desc: + ja: "返信する投稿" + en: "The post you want to reply" + - name: "repost_id" + type: "id(Post)" + optional: true + desc: + ja: "引用する投稿" + en: "The post you want to quote" + - name: "poll" + type: "object" + optional: true + desc: + ja: "投票" + en: "The poll" + defName: "poll" + def: + - name: "choices" + type: "string[]" + optional: false + desc: + ja: "投票の選択肢" + en: "Choices of a poll" + +res: + - name: "created_post" + type: "entity(Post)" + optional: false + desc: + ja: "作成した投稿" + en: "A post that created" diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml new file mode 100644 index 0000000000..01976b0611 --- /dev/null +++ b/src/web/docs/api/endpoints/posts/timeline.yaml @@ -0,0 +1,32 @@ +endpoint: "posts/timeline" + +desc: + ja: "タイムラインを取得します。" + en: "Get your timeline." + +params: + - name: "limit" + type: "number" + optional: true + desc: + ja: "取得する最大の数" + - name: "since_id" + type: "id(Post)" + optional: true + desc: + ja: "指定すると、この投稿を基点としてより新しい投稿を取得します" + - name: "until_id" + type: "id(Post)" + optional: true + desc: + ja: "指定すると、この投稿を基点としてより古い投稿を取得します" + - name: "since_date" + type: "number" + optional: true + desc: + ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" + - name: "until_date" + type: "number" + optional: true + desc: + ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl new file mode 100644 index 0000000000..2af9fe9a77 --- /dev/null +++ b/src/web/docs/api/endpoints/style.styl @@ -0,0 +1,21 @@ +@import "../style" + +#url + padding 8px 12px 8px 8px + font-family Consolas, 'Courier New', Courier, Monaco, monospace + color #fff + background #222e40 + border-radius 4px + + > .method + display inline-block + margin 0 8px 0 0 + padding 0 6px + color #f4fcff + background #17afc7 + border-radius 4px + user-select none + pointer-events none + + > .host + opacity 0.7 diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug new file mode 100644 index 0000000000..d271a5517a --- /dev/null +++ b/src/web/docs/api/endpoints/view.pug @@ -0,0 +1,32 @@ +extends ../../layout.pug +include ../mixins + +block meta + link(rel="stylesheet" href="/assets/api/endpoints/style.css") + +block main + h1= endpoint + + p#url + span.method POST + span.host + = url.host + | / + span.path= url.path + + p#desc= desc[lang] || desc['ja'] + + section + h2 %i18n:docs.api.endpoints.params% + +propTable(params) + + if paramDefs + each paramDef in paramDefs + section(id= paramDef.name) + h3= paramDef.name + +propTable(paramDef.params) + + if res + section + h2 %i18n:docs.api.endpoints.res% + +propTable(res) diff --git a/src/web/docs/api/entities/drive-file.yaml b/src/web/docs/api/entities/drive-file.yaml new file mode 100644 index 0000000000..2ebbb089ab --- /dev/null +++ b/src/web/docs/api/entities/drive-file.yaml @@ -0,0 +1,73 @@ +name: "DriveFile" + +desc: + ja: "ドライブのファイル。" + en: "A file of Drive." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "ファイルID" + en: "The ID of this file" + - name: "created_at" + type: "date" + optional: false + desc: + ja: "アップロード日時" + en: "The upload date of this file" + - name: "user_id" + type: "id(User)" + optional: false + desc: + ja: "所有者ID" + en: "The ID of the owner of this file" + - name: "user" + type: "entity(User)" + optional: true + desc: + ja: "所有者" + en: "The owner of this file" + - name: "name" + type: "string" + optional: false + desc: + ja: "ファイル名" + en: "The name of this file" + - name: "md5" + type: "string" + optional: false + desc: + ja: "ファイルのMD5ハッシュ値" + en: "The md5 hash value of this file" + - name: "type" + type: "string" + optional: false + desc: + ja: "ファイルの種類" + en: "The type of this file" + - name: "datasize" + type: "number" + optional: false + desc: + ja: "ファイルサイズ(bytes)" + en: "The size of this file (bytes)" + - name: "url" + type: "string" + optional: false + desc: + ja: "ファイルのURL" + en: "The URL of this file" + - name: "folder_id" + type: "id(DriveFolder)" + optional: true + desc: + ja: "フォルダID" + en: "The ID of the folder of this file" + - name: "folder" + type: "entity(DriveFolder)" + optional: true + desc: + ja: "フォルダ" + en: "The folder of this file" diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml new file mode 100644 index 0000000000..551f3b7c3e --- /dev/null +++ b/src/web/docs/api/entities/post.yaml @@ -0,0 +1,124 @@ +name: "Post" + +desc: + ja: "投稿。" + en: "A post." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "投稿ID" + en: "The ID of this post" + - name: "created_at" + type: "date" + optional: false + desc: + ja: "投稿日時" + en: "The posted date of this post" + - name: "text" + type: "string" + optional: true + desc: + ja: "投稿の本文" + en: "The text of this post" + - name: "media_ids" + type: "id(DriveFile)[]" + optional: true + desc: + ja: "添付されているメディアのID" + en: "The IDs of the attached media" + - name: "media" + type: "entity(DriveFile)[]" + optional: true + desc: + ja: "添付されているメディア" + en: "The attached media" + - name: "user_id" + type: "id(User)" + optional: false + desc: + ja: "投稿者ID" + en: "The ID of author of this post" + - name: "user" + type: "entity(User)" + optional: true + desc: + ja: "投稿者" + en: "The author of this post" + - name: "my_reaction" + type: "string" + optional: true + desc: + ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" + en: "The your <a href='/docs/api/reactions'>reaction</a> of this post" + - name: "reaction_counts" + type: "object" + optional: false + desc: + ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" + - name: "reply_id" + type: "id(Post)" + optional: true + desc: + ja: "返信した投稿のID" + en: "The ID of the replyed post" + - name: "reply" + type: "entity(Post)" + optional: true + desc: + ja: "返信した投稿" + en: "The replyed post" + - name: "repost_id" + type: "id(Post)" + optional: true + desc: + ja: "引用した投稿のID" + en: "The ID of the quoted post" + - name: "repost" + type: "entity(Post)" + optional: true + desc: + ja: "引用した投稿" + en: "The quoted post" + - name: "poll" + type: "object" + optional: true + desc: + ja: "投票" + en: "The poll" + defName: "poll" + def: + - name: "choices" + type: "object[]" + optional: false + desc: + ja: "投票の選択肢" + en: "The choices of this poll" + defName: "choice" + def: + - name: "id" + type: "number" + optional: false + desc: + ja: "選択肢ID" + en: "The ID of this choice" + - name: "is_voted" + type: "boolean" + optional: true + desc: + ja: "自分がこの選択肢に投票したかどうか" + en: "Whether you voted to this choice" + - name: "text" + type: "string" + optional: false + desc: + ja: "選択肢本文" + en: "The text of this choice" + - name: "votes" + type: "number" + optional: false + desc: + ja: "この選択肢に投票された数" + en: "The number voted for this choice" diff --git a/src/web/docs/api/entities/style.styl b/src/web/docs/api/entities/style.styl new file mode 100644 index 0000000000..bddf0f53ab --- /dev/null +++ b/src/web/docs/api/entities/style.styl @@ -0,0 +1 @@ +@import "../style" diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml new file mode 100644 index 0000000000..e62ad84db8 --- /dev/null +++ b/src/web/docs/api/entities/user.yaml @@ -0,0 +1,153 @@ +name: "User" + +desc: + ja: "ユーザー。" + en: "A user." + +props: + - name: "id" + type: "id" + optional: false + desc: + ja: "ユーザーID" + en: "The ID of this user" + - name: "created_at" + type: "date" + optional: false + desc: + ja: "アカウント作成日時" + en: "The registered date of this user" + - name: "username" + type: "string" + optional: false + desc: + ja: "ユーザー名" + en: "The username of this user" + - name: "description" + type: "string" + optional: false + desc: + ja: "アカウントの説明(自己紹介)" + en: "The description of this user" + - name: "avatar_id" + type: "id(DriveFile)" + optional: true + desc: + ja: "アバターのID" + en: "The ID of the avatar of this user" + - name: "avatar_url" + type: "string" + optional: false + desc: + ja: "アバターのURL" + en: "The URL of the avatar of this user" + - name: "banner_id" + type: "id(DriveFile)" + optional: true + desc: + ja: "バナーのID" + en: "The ID of the banner of this user" + - name: "banner_url" + type: "string" + optional: false + desc: + ja: "バナーのURL" + en: "The URL of the banner of this user" + - name: "followers_count" + type: "number" + optional: false + desc: + ja: "フォロワーの数" + en: "The number of the followers for this user" + - name: "following_count" + type: "number" + optional: false + desc: + ja: "フォローしているユーザーの数" + en: "The number of the following users for this user" + - name: "is_following" + type: "boolean" + optional: true + desc: + ja: "自分がこのユーザーをフォローしているか" + - name: "is_followed" + type: "boolean" + optional: true + desc: + ja: "自分がこのユーザーにフォローされているか" + - name: "is_muted" + type: "boolean" + optional: true + desc: + ja: "自分がこのユーザーをミュートしているか" + en: "Whether you muted this user" + - name: "last_used_at" + type: "date" + optional: false + desc: + ja: "最終利用日時" + en: "The last used date of this user" + - name: "posts_count" + type: "number" + optional: false + desc: + ja: "投稿の数" + en: "The number of the posts of this user" + - name: "pinned_post" + type: "entity(Post)" + optional: true + desc: + ja: "ピン留めされた投稿" + en: "The pinned post of this user" + - name: "pinned_post_id" + type: "id(Post)" + optional: true + desc: + ja: "ピン留めされた投稿のID" + en: "The ID of the pinned post of this user" + - name: "drive_capacity" + type: "number" + optional: false + desc: + ja: "ドライブの容量(bytes)" + en: "The capacity of drive of this user (bytes)" + - name: "twitter" + type: "object" + optional: true + desc: + ja: "連携されているTwitterアカウント情報" + en: "The info of the connected twitter account of this user" + defName: "twitter" + def: + - name: "user_id" + type: "string" + optional: false + desc: + ja: "ユーザーID" + en: "The user ID" + - name: "screen_name" + type: "string" + optional: false + desc: + ja: "ユーザー名" + en: "The screen name of this user" + - name: "profile" + type: "object" + optional: false + desc: + ja: "プロフィール" + en: "The profile of this user" + defName: "profile" + def: + - name: "location" + type: "string" + optional: true + desc: + ja: "場所" + en: "The location of this user" + - name: "birthday" + type: "string" + optional: true + desc: + ja: "誕生日 (YYYY-MM-DD)" + en: "The birthday of this user (YYYY-MM-DD)" diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug new file mode 100644 index 0000000000..2156463dc7 --- /dev/null +++ b/src/web/docs/api/entities/view.pug @@ -0,0 +1,20 @@ +extends ../../layout.pug +include ../mixins + +block meta + link(rel="stylesheet" href="/assets/api/entities/style.css") + +block main + h1= name + + p#desc= desc[lang] || desc['ja'] + + section + h2 %i18n:docs.api.entities.properties% + +propTable(props) + + if propDefs + each propDef in propDefs + section(id= propDef.name) + h3= propDef.name + +propTable(propDef.params) diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts new file mode 100644 index 0000000000..cd1bf15307 --- /dev/null +++ b/src/web/docs/api/gulpfile.ts @@ -0,0 +1,188 @@ +/** + * Gulp tasks + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as glob from 'glob'; +import * as gulp from 'gulp'; +import * as pug from 'pug'; +import * as yaml from 'js-yaml'; +import * as mkdirp from 'mkdirp'; + +import locales from '../../../../locales'; +import I18nReplacer from '../../../common/build/i18n'; +import fa from '../../../common/build/fa'; +import config from './../../../conf'; + +import generateVars from '../vars'; + +const langs = Object.keys(locales); + +const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); + +const parseParam = param => { + const id = param.type.match(/^id\((.+?)\)|^id/); + const entity = param.type.match(/^entity\((.+?)\)/); + const isObject = /^object/.test(param.type); + const isDate = /^date/.test(param.type); + const isArray = /\[\]$/.test(param.type); + if (id) { + param.kind = 'id'; + param.type = 'string'; + param.entity = id[1]; + if (isArray) { + param.type += '[]'; + } + } + if (entity) { + param.kind = 'entity'; + param.type = 'object'; + param.entity = entity[1]; + if (isArray) { + param.type += '[]'; + } + } + if (isObject) { + param.kind = 'object'; + } + if (isDate) { + param.kind = 'date'; + param.type = 'string'; + if (isArray) { + param.type += '[]'; + } + } + + return param; +}; + +const sortParams = params => { + params.sort((a, b) => { + if (a.name < b.name) + return -1; + if (a.name > b.name) + return 1; + return 0; + }); + return params; +}; + +const extractDefs = params => { + let defs = []; + + params.forEach(param => { + if (param.def) { + defs.push({ + name: param.defName, + params: sortParams(param.def.map(p => parseParam(p))) + }); + + const childDefs = extractDefs(param.def); + + defs = defs.concat(childDefs); + } + }); + + return sortParams(defs); +}; + +gulp.task('doc:api', [ + 'doc:api:endpoints', + 'doc:api:entities' +]); + +gulp.task('doc:api:endpoints', async () => { + const commonVars = await generateVars(); + glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => { + if (globErr) { + console.error(globErr); + return; + } + //console.log(files); + files.forEach(file => { + const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); + const vars = { + endpoint: ep.endpoint, + url: { + host: config.api_url, + path: ep.endpoint + }, + desc: ep.desc, + params: sortParams(ep.params.map(p => parseParam(p))), + paramDefs: extractDefs(ep.params), + res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null, + resDefs: ep.res ? extractDefs(ep.res) : null, + }; + langs.forEach(lang => { + pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, { + lang, + title: ep.endpoint, + src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/endpoints/${ep.endpoint}.yaml`, + kebab, + common: commonVars + }), (renderErr, html) => { + if (renderErr) { + console.error(renderErr); + return; + } + const i18n = new I18nReplacer(lang); + html = html.replace(i18n.pattern, i18n.replacement); + html = fa(html); + const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`; + mkdirp(path.dirname(htmlPath), (mkdirErr) => { + if (mkdirErr) { + console.error(mkdirErr); + return; + } + fs.writeFileSync(htmlPath, html, 'utf-8'); + }); + }); + }); + }); + }); +}); + +gulp.task('doc:api:entities', async () => { + const commonVars = await generateVars(); + glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => { + if (globErr) { + console.error(globErr); + return; + } + files.forEach(file => { + const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); + const vars = { + name: entity.name, + desc: entity.desc, + props: sortParams(entity.props.map(p => parseParam(p))), + propDefs: extractDefs(entity.props), + }; + langs.forEach(lang => { + pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, { + lang, + title: entity.name, + src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/entities/${kebab(entity.name)}.yaml`, + kebab, + common: commonVars + }), (renderErr, html) => { + if (renderErr) { + console.error(renderErr); + return; + } + const i18n = new I18nReplacer(lang); + html = html.replace(i18n.pattern, i18n.replacement); + html = fa(html); + const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`; + mkdirp(path.dirname(htmlPath), (mkdirErr) => { + if (mkdirErr) { + console.error(mkdirErr); + return; + } + fs.writeFileSync(htmlPath, html, 'utf-8'); + }); + }); + }); + }); + }); +}); diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug new file mode 100644 index 0000000000..686bf6a2b6 --- /dev/null +++ b/src/web/docs/api/mixins.pug @@ -0,0 +1,37 @@ +mixin propTable(props) + table.props + thead: tr + th %i18n:docs.api.props.name% + th %i18n:docs.api.props.type% + th %i18n:docs.api.props.optional% + th %i18n:docs.api.props.description% + tbody + each prop in props + tr + td.name= prop.name + td.type + i= prop.type + if prop.kind == 'id' + if prop.entity + | ( + a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity + | ID) + else + | (ID) + else if prop.kind == 'entity' + | ( + a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity + | ) + else if prop.kind == 'object' + if prop.def + | ( + a(href=`#${prop.defName}`)= prop.defName + | ) + else if prop.kind == 'date' + | (Date) + td.optional + if prop.optional + | %i18n:docs.api.props.yes% + else + | %i18n:docs.api.props.no% + td.desc!= prop.desc[lang] || prop.desc['ja'] diff --git a/src/web/docs/api/style.styl b/src/web/docs/api/style.styl new file mode 100644 index 0000000000..3675a4da6f --- /dev/null +++ b/src/web/docs/api/style.styl @@ -0,0 +1,11 @@ +@import "../style" + +table.props + .name + font-weight bold + + .name + .type + .optional + font-family Consolas, 'Courier New', Courier, Monaco, monospace + diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts new file mode 100644 index 0000000000..d5ddda108d --- /dev/null +++ b/src/web/docs/gulpfile.ts @@ -0,0 +1,77 @@ +/** + * Gulp tasks + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as glob from 'glob'; +import * as gulp from 'gulp'; +import * as pug from 'pug'; +import * as mkdirp from 'mkdirp'; +import stylus = require('gulp-stylus'); +import cssnano = require('gulp-cssnano'); + +import I18nReplacer from '../../common/build/i18n'; +import fa from '../../common/build/fa'; +import generateVars from './vars'; + +require('./api/gulpfile.ts'); + +gulp.task('doc', [ + 'doc:docs', + 'doc:api', + 'doc:styles' +]); + +gulp.task('doc:docs', async () => { + const commonVars = await generateVars(); + + glob('./src/web/docs/**/*.*.pug', (globErr, files) => { + if (globErr) { + console.error(globErr); + return; + } + files.forEach(file => { + const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/); + const vars = { + common: commonVars, + lang: lang, + title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1], + src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/${name}.${lang}.pug`, + }; + pug.renderFile(file, vars, (renderErr, content) => { + if (renderErr) { + console.error(renderErr); + return; + } + + pug.renderFile('./src/web/docs/layout.pug', Object.assign({}, vars, { + content + }), (renderErr2, html) => { + if (renderErr2) { + console.error(renderErr2); + return; + } + const i18n = new I18nReplacer(lang); + html = html.replace(i18n.pattern, i18n.replacement); + html = fa(html); + const htmlPath = `./built/web/docs/${lang}/${name}.html`; + mkdirp(path.dirname(htmlPath), (mkdirErr) => { + if (mkdirErr) { + console.error(mkdirErr); + return; + } + fs.writeFileSync(htmlPath, html, 'utf-8'); + }); + }); + }); + }); + }); +}); + +gulp.task('doc:styles', () => + gulp.src('./src/web/docs/**/*.styl') + .pipe(stylus()) + .pipe((cssnano as any)()) + .pipe(gulp.dest('./built/web/docs/assets/')) +); diff --git a/src/web/docs/index.en.pug b/src/web/docs/index.en.pug new file mode 100644 index 0000000000..1fcc870d3d --- /dev/null +++ b/src/web/docs/index.en.pug @@ -0,0 +1,3 @@ +h1 Misskey Docs + +p Welcome to docs of Misskey. diff --git a/src/web/docs/index.ja.pug b/src/web/docs/index.ja.pug new file mode 100644 index 0000000000..4a0bf7fa1d --- /dev/null +++ b/src/web/docs/index.ja.pug @@ -0,0 +1,3 @@ +h1 Misskey ドキュメント + +p Misskeyのドキュメントへようこそ diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug new file mode 100644 index 0000000000..9dfd0ab7af --- /dev/null +++ b/src/web/docs/layout.pug @@ -0,0 +1,41 @@ +doctype html + +html(lang= lang) + head + meta(charset="UTF-8") + meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no") + title + | #{title} | Misskey Docs + link(rel="stylesheet" href="/assets/style.css") + block meta + + //- FontAwesome style + style #{common.facss} + + body + nav + ul + each doc in common.docs + li: a(href=`/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] + section + h2 API + ul + li Entities + ul + each entity in common.entities + li: a(href=`/${lang}/api/entities/${common.kebab(entity)}`)= entity + li Endpoints + ul + each endpoint in common.endpoints + li: a(href=`/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint + main + article + block main + if content + | !{content} + + footer + p + | %i18n:docs.edit-this-page-on-github% + a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link% + small= common.copyright diff --git a/src/web/docs/license.en.pug b/src/web/docs/license.en.pug new file mode 100644 index 0000000000..45d8b76473 --- /dev/null +++ b/src/web/docs/license.en.pug @@ -0,0 +1,17 @@ +h1 License + +div!= common.license + +details + summary Libraries + + section + h2 Libraries + + each dependency, name in common.dependencies + details + summary= name + + section + h3= name + pre= dependency.licenseText diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug new file mode 100644 index 0000000000..7bd9a62941 --- /dev/null +++ b/src/web/docs/license.ja.pug @@ -0,0 +1,17 @@ +h1 ライセンス + +div!= common.license + +details + summary ライブラリ + + section + h2 ライブラリ + + each dependency, name in common.dependencies + details + summary= name + + section + h3= name + pre= dependency.licenseText diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug new file mode 100644 index 0000000000..5e79af5f8c --- /dev/null +++ b/src/web/docs/mute.ja.pug @@ -0,0 +1,13 @@ +h1 ミュート + +p ユーザーページから、そのユーザーをミュートすることができます。 + +p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります: +ul + li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost) + li そのユーザーからの通知 + li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴 + +p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。 + +p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。 diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug new file mode 100644 index 0000000000..f33091ee6b --- /dev/null +++ b/src/web/docs/search.ja.pug @@ -0,0 +1,116 @@ +h1 検索 + +p 投稿を検索することができます。 +p + | キーワードを半角スペースで区切ると、and検索になります。 + | 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。 + +section + h2 キーワードの除外 + p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。 + p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります: + code git -コミット + +section + h2 完全一致 + p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。 + p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。 + +section + h2 オプション + p + | オプションを使用して、より高度な検索を行えます。 + | オプションを指定するには、「オプション名:値」という形式でクエリに含めます。 + p 利用可能なオプション一覧です: + + table + thead + tr + th 名前 + th 説明 + tbody + tr + td user + td + | 指定されたユーザー名のユーザーの投稿に限定します。 + | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 + br + | 例えば、 + code user:himawari,sakurako + | と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。 + | (つまりユーザーのホワイトリストです) + tr + td exclude_user + td + | 指定されたユーザー名のユーザーの投稿を除外します。 + | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 + br + | 例えば、 + code exclude_user:akari,chinatsu + | と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。 + | (つまりユーザーのブラックリストです) + tr + td follow + td + | true ... フォローしているユーザーに限定。 + br + | false ... フォローしていないユーザーに限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td mute + td + | mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト) + br + | mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する + br + | mute_direct ... ミュートしているユーザーの投稿だけ除外する + br + | disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める + br + | direct_only ... ミュートしているユーザーの投稿だけに限定 + br + | related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定 + br + | all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定 + tr + td reply + td + | true ... 返信に限定。 + br + | false ... 返信でない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td repost + td + | true ... Repostに限定。 + br + | false ... Repostでない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td media + td + | true ... メディアが添付されている投稿に限定。 + br + | false ... メディアが添付されていない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td poll + td + | true ... 投票が添付されている投稿に限定。 + br + | false ... 投票が添付されていない投稿に限定。 + br + | null ... 特に限定しない(デフォルト) + tr + td until + td 上限の日時。(YYYY-MM-DD) + tr + td since + td 下限の日時。(YYYY-MM-DD) + + p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります: + code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey diff --git a/src/web/docs/server.ts b/src/web/docs/server.ts new file mode 100644 index 0000000000..b2e50457e5 --- /dev/null +++ b/src/web/docs/server.ts @@ -0,0 +1,21 @@ +/** + * Docs Server + */ + +import * as express from 'express'; + +/** + * Init app + */ +const app = express(); +app.disable('x-powered-by'); + +app.use('/assets', express.static(`${__dirname}/assets`)); + +/** + * Routing + */ +app.get(/^\/([a-z_\-\/]+?)$/, (req, res) => + res.sendFile(`${__dirname}/${req.params[0]}.html`)); + +module.exports = app; diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl new file mode 100644 index 0000000000..bc165f8728 --- /dev/null +++ b/src/web/docs/style.styl @@ -0,0 +1,120 @@ +@import "../style" +@import "./ui" + +body + margin 0 + color #34495e + word-break break-word + +main + margin 0 0 0 256px + padding 64px + width 100% + max-width 768px + + section + margin 32px 0 + + h1 + margin 0 0 24px 0 + padding 16px 0 + font-size 1.5em + border-bottom solid 2px #eee + + h2 + margin 0 0 24px 0 + padding 0 0 16px 0 + font-size 1.4em + border-bottom solid 1px #eee + + h3 + margin 0 + padding 0 + font-size 1.25em + + h4 + margin 0 + + p + margin 1em 0 + line-height 1.6em + + footer + margin 32px 0 0 0 + border-top solid 2px #eee + + > small + margin 16px 0 0 0 + color #aaa + +nav + display block + position fixed + z-index 10000 + top 0 + left 0 + width 256px + height 100% + overflow auto + padding 32px + background #fff + border-right solid 2px #eee + +@media (max-width 1025px) + main + margin 0 + max-width 100% + + nav + position relative + width 100% + max-height 128px + background #f9f9f9 + border-right none + +@media (max-width 768px) + main + padding 32px + +@media (max-width 512px) + main + padding 16px + +table + display block + width 100% + max-width 100% + overflow auto + border-spacing 0 + border-collapse collapse + + thead + font-weight bold + border-bottom solid 2px #eee + + tr + th + text-align left + + tbody + tr + &:nth-child(odd) + background #fbfbfb + + th, td + padding 8px 16px + min-width 128px + +code + display inline-block + padding 8px 10px + font-family Consolas, 'Courier New', Courier, Monaco, monospace + color #295c92 + background #f2f2f2 + border-radius 4px + +pre + overflow auto + + > code + display block diff --git a/src/web/docs/tou.ja.pug b/src/web/docs/tou.ja.pug new file mode 100644 index 0000000000..7663258f82 --- /dev/null +++ b/src/web/docs/tou.ja.pug @@ -0,0 +1,3 @@ +h1 利用規約 + +p 公序良俗に反する行為はおやめください。 diff --git a/src/web/docs/ui.styl b/src/web/docs/ui.styl new file mode 100644 index 0000000000..8d5515712f --- /dev/null +++ b/src/web/docs/ui.styl @@ -0,0 +1,19 @@ +.ui.info + display block + margin 1em 0 + padding 0 1em + font-size 90% + color rgba(#000, 0.87) + background #f8f8f9 + border-radius 4px + overflow hidden + + > p + opacity 0.8 + + > [data-fa]:first-child + margin-right 0.25em + + &.warn + color #573a08 + background #FFFAF3 diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts new file mode 100644 index 0000000000..6f713f21d0 --- /dev/null +++ b/src/web/docs/vars.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as util from 'util'; +import * as glob from 'glob'; +import * as yaml from 'js-yaml'; +import * as licenseChecker from 'license-checker'; +import * as tmp from 'tmp'; + +import { fa } from '../../common/build/fa'; +import config from '../../conf'; +import { licenseHtml } from '../../common/build/license'; +const constants = require('../../const.json'); + +export default async function(): Promise<{ [key: string]: any }> { + const vars = {} as { [key: string]: any }; + + const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml'); + vars['endpoints'] = endpoints.map(ep => { + const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8')); + return _ep.endpoint; + }); + + const entities = glob.sync('./src/web/docs/api/entities/**/*.yaml'); + vars['entities'] = entities.map(x => { + const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8')); + return _x.name; + }); + + const docs = glob.sync('./src/web/docs/**/*.*.pug'); + vars['docs'] = {}; + docs.forEach(x => { + const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/); + if (vars['docs'][name] == null) { + vars['docs'][name] = { + name, + title: {} + }; + } + vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]; + }); + + vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); + + vars['config'] = config; + + vars['copyright'] = constants.copyright; + + vars['facss'] = fa.dom.css(); + + vars['license'] = licenseHtml; + + const tmpObj = tmp.fileSync(); + fs.writeFileSync(tmpObj.name, JSON.stringify({ + licenseText: '' + }), 'utf-8'); + const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({ + start: __dirname + '/../../../', + customPath: tmpObj.name + }); + tmpObj.removeCallback(); + + vars['dependencies'] = dependencies; + + return vars; +} diff --git a/src/web/server.ts b/src/web/server.ts index 1d3687f89e..062d1f197a 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -10,6 +10,9 @@ import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as favicon from 'serve-favicon'; import * as compression from 'compression'; +import vhost = require('vhost'); + +import config from '../conf'; /** * Init app @@ -17,6 +20,8 @@ import * as compression from 'compression'; const app = express(); app.disable('x-powered-by'); +app.use(vhost(`docs.${config.host}`, require('./docs/server'))); + app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ type: ['application/json', 'text/plain'] diff --git a/src/web/style.styl b/src/web/style.styl new file mode 100644 index 0000000000..c25fc8fb52 --- /dev/null +++ b/src/web/style.styl @@ -0,0 +1,35 @@ +@charset 'utf-8' + +@import "./const" + +/* + ::selection + background $theme-color + color #fff +*/ + +* + position relative + box-sizing border-box + background-clip padding-box !important + tap-highlight-color rgba($theme-color, 0.7) + -webkit-tap-highlight-color rgba($theme-color, 0.7) + +html, body + margin 0 + padding 0 + scroll-behavior smooth + text-size-adjust 100% + font-family sans-serif + +a + text-decoration none + color $theme-color + cursor pointer + + &:hover + text-decoration underline + + * + cursor pointer + |