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/app | |
| 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/app')
58 files changed, 1164 insertions, 743 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> |