Initial commit 🍀

This commit is contained in:
syuilo
2016-12-29 07:49:51 +09:00
commit b3f42e62af
405 changed files with 31017 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
riot = require \riot
module.exports = (me) ~>
riot.mixin \sortable do
Sortable: require \Sortable
if me?
(require './scripts/stream.ls') me
require './scripts/user-preview.ls'
require './scripts/open-window.ls'
riot.mixin \notify do
notify: require './scripts/notify.ls'
dialog = require './scripts/dialog.ls'
riot.mixin \dialog do
dialog: dialog
riot.mixin \NotImplementedException do
NotImplementedException: ~>
dialog do
'<i class="fa fa-exclamation-triangle"></i>Not implemented yet'
'要求された操作は実装されていません。<br>→<a href="https://github.com/syuilo/misskey" target="_blank">Misskeyの開発に参加する</a>'
[
text: \OK
]
riot.mixin \input-dialog do
input-dialog: require './scripts/input-dialog.ls'
riot.mixin \update-avatar do
update-avatar: require './scripts/update-avatar.ls'
riot.mixin \update-banner do
update-banner: require './scripts/update-banner.ls'
riot.mixin \update-wallpaper do
update-wallpaper: require './scripts/update-wallpaper.ls'
riot.mixin \autocomplete do
Autocomplete: require './scripts/autocomplete.ls'
riot.mixin \follow-scroll do
Follower: require './scripts/follow-scroll.ls'

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" width="1024px" height="512px" viewBox="0 256 1024 512" enable-background="new 0 256 1024 512" xml:space="preserve">
<polyline opacity="0.5" fill="none" stroke="#000000" stroke-width="34" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
896.5,608.5 800.5,416.5 704.5,608.5 608.5,416.5 512.5,608.5 416.5,416.5 320.5,608.5 224.5,416.5 128.5,608.5 "/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,77 @@
# Router
#================================
riot = require \riot
route = require \page
page = null
module.exports = (me) ~>
# Routing
#--------------------------------
route \/ index
route \/i>mentions mentions
route \/post::post post
route \/search::query search
route \/:user user.bind null \home
route \/:user/graphs user.bind null \graphs
route \/:user/:post post
route \* not-found
# Handlers
#--------------------------------
function index
if me? then home! else entrance!
function home
mount document.create-element \mk-home-page
function entrance
mount document.create-element \mk-entrance
document.document-element.set-attribute \data-page \entrance
function mentions
document.create-element \mk-home-page
..set-attribute \mode \mentions
.. |> mount
function search ctx
document.create-element \mk-search-page
..set-attribute \query ctx.params.query
.. |> mount
function user page, ctx
document.create-element \mk-user-page
..set-attribute \user ctx.params.user
..set-attribute \page page
.. |> mount
function post ctx
document.create-element \mk-post-page
..set-attribute \post ctx.params.post
.. |> mount
function not-found
mount document.create-element \mk-not-found
# Register mixin
#--------------------------------
riot.mixin \page do
page: route
# Exec
#--------------------------------
route!
# Mount
#================================
function mount content
document.document-element.remove-attribute \data-page
if page? then page.unmount!
body = document.get-element-by-id \app
page := riot.mount body.append-child content .0

View File

@@ -0,0 +1,42 @@
/**
* Desktop Client
*/
require('chart.js');
require('./tags.ls');
const riot = require('riot');
const boot = require('../boot.ls');
const mixins = require('./mixins.ls');
const route = require('./router.ls');
const fuckAdBlock = require('./scripts/fuck-ad-block.ls');
/**
* Boot
*/
boot(me => {
/**
* Fuck AD Block
*/
fuckAdBlock();
/**
* Init Notification
*/
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission == 'default') {
Notification.requestPermission();
}
}
// Register mixins
mixins(me);
// Debug
if (me != null && me.data.debug) {
riot.mount(document.body.appendChild(document.createElement('mk-log-window')));
}
// Start routing
route(me);
});

View File

@@ -0,0 +1,108 @@
# Autocomplete
#================================
get-caret-coordinates = require 'textarea-caret-position'
riot = require 'riot'
# オートコンプリートを管理するクラスです。
class Autocomplete
@textarea = null
@suggestion = null
# 対象のテキストエリアを与えてインスタンスを初期化します。
(textarea) ~>
@textarea = textarea
# このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
attach: ~>
@textarea.add-event-listener \input @on-input
# このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
detach: ~>
@textarea.remove-event-listener \input @on-input
@close!
# テキスト入力時
on-input: ~>
@close!
caret = @textarea.selection-start
text = @textarea.value.substr 0 caret
mention-index = text.last-index-of \@
if mention-index == -1
return
username = text.substr mention-index + 1
if not username.match /^[a-zA-Z0-9-]+$/
return
@open \user username
# サジェストを提示します。
open: (type, q) ~>
# 既に開いているサジェストは閉じる
@close!
# サジェスト要素作成
suggestion = document.create-element \mk-autocomplete-suggestion
# ~ サジェストを表示すべき位置を計算 ~
caret-position = get-caret-coordinates @textarea, @textarea.selection-start
rect = @textarea.get-bounding-client-rect!
x = rect.left + window.page-x-offset + caret-position.left
y = rect.top + window.page-y-offset + caret-position.top
suggestion.style.left = x + \px
suggestion.style.top = y + \px
# 要素追加
el = document.body.append-child suggestion
# マウント
mounted = riot.mount el, do
textarea: @textarea
complete: @complete
close: @close
type: type
q: q
@suggestion = mounted.0
# サジェストを閉じます。
close: ~>
if !@suggestion?
return
@suggestion.unmount!
@suggestion = null
@textarea.focus!
# オートコンプリートする
complete: (user) ~>
@close!
value = user.username
caret = @textarea.selection-start
source = @textarea.value
before = source.substr 0 caret
trimed-before = before.substring 0 before.last-index-of \@
after = source.substr caret
# 結果を挿入する
@textarea.value = trimed-before + \@ + value + ' ' + after
# キャレットを戻す
@textarea.focus!
pos = caret + value.length
@textarea.set-selection-range pos, pos
module.exports = Autocomplete

View File

@@ -0,0 +1,17 @@
# Dialog
#================================
riot = require 'riot'
module.exports = (title, text, buttons, can-through, on-through) ~>
dialog = document.body.append-child document.create-element \mk-dialog
controller = riot.observable!
riot.mount dialog, do
controller: controller
title: title
text: text
buttons: buttons
can-through: can-through
on-through: on-through
controller.trigger \open
return controller

View File

@@ -0,0 +1,56 @@
class Follower
(el) ->
@follower = el
@last-scroll-top = window.scroll-y
@initial-follower-top = @follower.get-bounding-client-rect!.top
@page-top = 48
follow: ->
window-height = window.inner-height
follower-height = @follower.offset-height
scroll-top = window.scroll-y
scroll-bottom = scroll-top + window-height
follower-top = @follower.get-bounding-client-rect!.top + scroll-top
follower-bottom = follower-top + follower-height
height-delta = Math.abs window-height - follower-height
scroll-delta = @last-scroll-top - scroll-top
is-scrolling-down = (scroll-top > @last-scroll-top)
is-window-larger = (window-height > follower-height)
console.log @initial-follower-top
if (is-window-larger && scroll-top > @initial-follower-top) || (!is-window-larger && scroll-top > @initial-follower-top + height-delta)
@follower.class-list.add \fixed
else if !is-scrolling-down && scroll-top + @page-top <= @initial-follower-top
@follower.class-list.remove \fixed
@follower.style.top = 0
return
drag-bottom-down = (follower-bottom <= scroll-bottom && is-scrolling-down)
drag-top-up = (follower-top >= scroll-top + @page-top && !is-scrolling-down)
if drag-bottom-down
console.log \down
@follower.style.top = if is-window-larger then 0 else -height-delta + \px
else if drag-top-up
console.log \up
@follower.style.top = @page-top + \px
else if @follower.class-list.contains \fixed
console.log \-
current-top = parse-int @follower.style.top, 10
min-top = -height-delta
scrolled-top = current-top + scroll-delta
is-page-at-bottom = (scroll-top + window-height >= document.body.offset-height)
new-top = if is-page-at-bottom then min-top else scrolled-top
@follower.style.top = new-top + \px
@last-scroll-top = scroll-top
module.exports = Follower

View File

@@ -0,0 +1,19 @@
# FUCK AD BLOCK
#================================
require 'fuck-adblock'
dialog = require './dialog.ls'
module.exports = ~>
if fuck-ad-block == undefined
ad-block-detected!
else
fuck-ad-block.on-detected ad-block-detected
function ad-block-detected
dialog do
'<i class="fa fa-exclamation-triangle"></i>広告ブロッカーを無効にしてください'
'<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。'
[
text: \OK
]

View File

@@ -0,0 +1,13 @@
# Input Dialog
#================================
riot = require 'riot'
module.exports = (title, placeholder, default-value, on-ok, on-cancel) ~>
dialog = document.body.append-child document.create-element \mk-input-dialog
riot.mount dialog, do
title: title
placeholder: placeholder
default: default-value
on-ok: on-ok
on-cancel: on-cancel

View File

@@ -0,0 +1,6 @@
riot = require \riot
module.exports = (message) ~>
notification = document.body.append-child document.create-element \mk-ui-notification
riot.mount notification, do
message: message

View File

@@ -0,0 +1,8 @@
riot = require \riot
function open(name, opts)
window = document.body.append-child document.create-element name
riot.mount window, opts
riot.mixin \open-window do
open-window: open

View File

@@ -0,0 +1,38 @@
# Stream
#================================
stream = require '../../common/scripts/stream.ls'
get-post-summary = require '../../common/scripts/get-post-summary.ls'
riot = require \riot
module.exports = (me) ~>
s = stream me
s.event.on \drive_file_created (file) ~>
n = new Notification 'ファイルがアップロードされました' do
body: file.name
icon: file.url + '?thumbnail&size=64'
set-timeout (n.close.bind n), 5000ms
s.event.on \mention (post) ~>
n = new Notification "#{post.user.name}さんから:" do
body: get-post-summary post
icon: post.user.avatar_url + '?thumbnail&size=64'
set-timeout (n.close.bind n), 6000ms
s.event.on \reply (post) ~>
n = new Notification "#{post.user.name}さんから返信:" do
body: get-post-summary post
icon: post.user.avatar_url + '?thumbnail&size=64'
set-timeout (n.close.bind n), 6000ms
s.event.on \quote (post) ~>
n = new Notification "#{post.user.name}さんが引用:" do
body: get-post-summary post
icon: post.user.avatar_url + '?thumbnail&size=64'
set-timeout (n.close.bind n), 6000ms
riot.mixin \stream do
stream: s.event
get-stream-state: s.get-state
stream-state-ev: s.state-ev

View File

@@ -0,0 +1,81 @@
# Update Avatar
#================================
riot = require 'riot'
dialog = require './dialog.ls'
api = require '../../common/scripts/api.ls'
module.exports = (I, cb, file = null) ~>
@file-selected = (file) ~>
cropper = document.body.append-child document.create-element \mk-crop-window
cropper = riot.mount cropper, do
file: file
title: 'アバターとして表示する部分を選択'
aspect-ratio: 1 / 1
.0
cropper.on \cropped (blob) ~>
data = new FormData!
data.append \i I.token
data.append \file blob, file.name + '.cropped.png'
api I, \drive/folders/find do
name: 'アイコン'
.then (icon-folder) ~>
if icon-folder.length == 0
api I, \drive/folders/create do
name: 'アイコン'
.then (icon-folder) ~>
@uplaod data, icon-folder
else
@uplaod data, icon-folder.0
cropper.on \skiped ~>
@set file
@uplaod = (data, folder) ~>
progress = document.body.append-child document.create-element \mk-progress-dialog
progress = riot.mount progress, do
title: '新しいアバターをアップロードしています'
.0
if folder?
data.append \folder_id folder.id
xhr = new XMLHttpRequest!
xhr.open \POST CONFIG.api.url + \/drive/files/create true
xhr.onload = (e) ~>
file = JSON.parse e.target.response
progress.close!
@set file
xhr.upload.onprogress = (e) ~>
if e.length-computable
progress.update-progress e.loaded, e.total
xhr.send data
@set = (file) ~>
api I, \i/update do
avatar_id: file.id
.then (i) ~>
dialog do
'<i class="fa fa-info-circle"></i>アバターを更新しました'
'新しいアバターが反映されるまで時間がかかる場合があります。'
[
text: \わかった
]
if cb? then cb i
.catch (err) ~>
console.error err
#@opts.ui.trigger \notification 'Error!'
if file?
@file-selected file
else
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
browser = riot.mount browser, do
multiple: false
title: '<i class="fa fa-picture-o"></i>アバターにする画像を選択'
.0
browser.one \selected (file) ~>
@file-selected file

View File

@@ -0,0 +1,81 @@
# Update Banner
#================================
riot = require 'riot'
dialog = require './dialog.ls'
api = require '../../common/scripts/api.ls'
module.exports = (I, cb, file = null) ~>
@file-selected = (file) ~>
cropper = document.body.append-child document.create-element \mk-crop-window
cropper = riot.mount cropper, do
file: file
title: 'バナーとして表示する部分を選択'
aspect-ratio: 16 / 9
.0
cropper.on \cropped (blob) ~>
data = new FormData!
data.append \i I.token
data.append \file blob, file.name + '.cropped.png'
api I, \drive/folders/find do
name: 'バナー'
.then (banner-folder) ~>
if banner-folder.length == 0
api I, \drive/folders/create do
name: 'バナー'
.then (banner-folder) ~>
@uplaod data, banner-folder
else
@uplaod data, banner-folder.0
cropper.on \skiped ~>
@set file
@uplaod = (data, folder) ~>
progress = document.body.append-child document.create-element \mk-progress-dialog
progress = riot.mount progress, do
title: '新しいバナーをアップロードしています'
.0
if folder?
data.append \folder_id folder.id
xhr = new XMLHttpRequest!
xhr.open \POST CONFIG.api.url + \/drive/files/create true
xhr.onload = (e) ~>
file = JSON.parse e.target.response
progress.close!
@set file
xhr.upload.onprogress = (e) ~>
if e.length-computable
progress.update-progress e.loaded, e.total
xhr.send data
@set = (file) ~>
api I, \i/update do
banner_id: file.id
.then (i) ~>
dialog do
'<i class="fa fa-info-circle"></i>バナーを更新しました'
'新しいバナーが反映されるまで時間がかかる場合があります。'
[
text: \わかりました。
]
if cb? then cb i
.catch (err) ~>
console.error err
#@opts.ui.trigger \notification 'Error!'
if file?
@file-selected file
else
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
browser = riot.mount browser, do
multiple: false
title: '<i class="fa fa-picture-o"></i>バナーにする画像を選択'
.0
browser.one \selected (file) ~>
@file-selected file

View File

@@ -0,0 +1,35 @@
# Update Wallpaper
#================================
riot = require 'riot'
dialog = require './dialog.ls'
api = require '../../common/scripts/api.ls'
module.exports = (I, cb, file = null) ~>
@set = (file) ~>
api I, \i/appdata/set do
data: JSON.stringify do
wallpaper: file.id
.then (i) ~>
dialog do
'<i class="fa fa-info-circle"></i>壁紙を更新しました'
'新しい壁紙が反映されるまで時間がかかる場合があります。'
[
text: \はい
]
if cb? then cb i
.catch (err) ~>
console.error err
#@opts.ui.trigger \notification 'Error!'
if file?
@set file
else
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
browser = riot.mount browser, do
multiple: false
title: '<i class="fa fa-picture-o"></i>壁紙にする画像を選択'
.0
browser.one \selected (file) ~>
@set file

View File

@@ -0,0 +1,74 @@
# User Preview
#================================
riot = require \riot
riot.mixin \user-preview do
init: ->
@on \mount ~>
scan.call @
@on \updated ~>
scan.call @
function scan
elems = @root.query-selector-all '[data-user-preview]:not([data-user-preview-attached])'
elems.for-each attach.bind @
function attach el
el.set-attribute \data-user-preview-attached true
user = el.get-attribute \data-user-preview
tag = null
show-timer = null
hide-timer = null
el.add-event-listener \mouseover ~>
clear-timeout show-timer
clear-timeout hide-timer
show-timer := set-timeout ~>
show!
, 500ms
el.add-event-listener \mouseleave ~>
clear-timeout show-timer
clear-timeout hide-timer
hide-timer := set-timeout ~>
close!
, 500ms
@on \unmount ~>
clear-timeout show-timer
clear-timeout hide-timer
close!
function show
if tag?
return
preview = document.create-element \mk-user-preview
rect = el.get-bounding-client-rect!
x = rect.left + el.offset-width + window.page-x-offset
y = rect.top + window.page-y-offset
preview.style.top = y + \px
preview.style.left = x + \px
preview.add-event-listener \mouseover ~>
clear-timeout hide-timer
preview.add-event-listener \mouseleave ~>
clear-timeout show-timer
hide-timer := set-timeout ~>
close!
, 500ms
tag := riot.mount (document.body.append-child preview), do
user: user
.0
function close
if tag?
tag.close!
tag := null

View File

@@ -0,0 +1,114 @@
@import "../base"
@import "../../../../node_modules/cropperjs/dist/cropper.css"
*::input-placeholder
color #D8CBC5
*
&:focus
outline none
&::scrollbar
width 5px
background transparent
&:horizontal
height 5px
&::scrollbar-button
width 0
height 0
background rgba(0, 0, 0, 0.2)
&::scrollbar-piece
background transparent
&:start
background transparent
&::scrollbar-thumb
background rgba(0, 0, 0, 0.2)
&:hover
background rgba(0, 0, 0, 0.4)
&:active
background $theme-color
&::scrollbar-corner
background rgba(0, 0, 0, 0.2)
html
background #fdfdfd
// workaround of https://github.com/riot/riot/issues/2134
&[data-page='entrance']
#wait
right auto
left 15px
html[theme='dark']
background #100f0f
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

103
src/web/app/desktop/tags.ls Normal file
View File

@@ -0,0 +1,103 @@
require './tags/contextmenu.tag'
require './tags/dialog.tag'
require './tags/window.tag'
require './tags/input-dialog.tag'
require './tags/follow-button.tag'
require './tags/drive/base-contextmenu.tag'
require './tags/drive/file-contextmenu.tag'
require './tags/drive/folder-contextmenu.tag'
require './tags/drive/file.tag'
require './tags/drive/folder.tag'
require './tags/drive/nav-folder.tag'
require './tags/drive/browser-window.tag'
require './tags/drive/browser.tag'
require './tags/select-file-from-drive-window.tag'
require './tags/crop-window.tag'
require './tags/settings.tag'
require './tags/settings-window.tag'
require './tags/analog-clock.tag'
require './tags/go-top.tag'
require './tags/ui-header.tag'
require './tags/ui-header-account.tag'
require './tags/ui-header-notifications.tag'
require './tags/ui-header-clock.tag'
require './tags/ui-header-nav.tag'
require './tags/ui-header-post-button.tag'
require './tags/ui-header-search.tag'
require './tags/notifications.tag'
require './tags/post-form-window.tag'
require './tags/post-form.tag'
require './tags/timeline-post.tag'
require './tags/post-preview.tag'
require './tags/repost-form-window.tag'
require './tags/home-widgets/user-recommendation.tag'
require './tags/home-widgets/timeline.tag'
require './tags/home-widgets/mentions.tag'
require './tags/home-widgets/calendar.tag'
require './tags/home-widgets/donation.tag'
require './tags/home-widgets/tips.tag'
require './tags/home-widgets/nav.tag'
require './tags/home-widgets/profile.tag'
require './tags/home-widgets/notifications.tag'
require './tags/home-widgets/rss-reader.tag'
require './tags/home-widgets/photo-stream.tag'
require './tags/home-widgets/broadcast.tag'
require './tags/stream-indicator.tag'
require './tags/timeline.tag'
require './tags/messaging/window.tag'
require './tags/messaging/room.tag'
require './tags/messaging/room-window.tag'
require './tags/messaging/message.tag'
require './tags/messaging/index.tag'
require './tags/messaging/form.tag'
require './tags/following-setuper.tag'
require './tags/ellipsis-icon.tag'
require './tags/ui.tag'
require './tags/home.tag'
require './tags/detect-slow-internet-connection-notice.tag'
require './tags/user-header.tag'
require './tags/user-profile.tag'
require './tags/user-timeline.tag'
require './tags/user.tag'
require './tags/user-home.tag'
require './tags/user-graphs.tag'
require './tags/user-photos.tag'
require './tags/big-follow-button.tag'
require './tags/pages/entrance.tag'
require './tags/pages/entrance/signin.tag'
require './tags/pages/entrance/signup.tag'
require './tags/pages/home.tag'
require './tags/pages/user.tag'
require './tags/pages/post.tag'
require './tags/pages/search.tag'
require './tags/pages/not-found.tag'
require './tags/autocomplete-suggestion.tag'
require './tags/progress-dialog.tag'
require './tags/user-preview.tag'
require './tags/post-detail.tag'
require './tags/post-detail-sub.tag'
require './tags/search.tag'
require './tags/search-posts.tag'
require './tags/set-avatar-suggestion.tag'
require './tags/set-banner-suggestion.tag'
require './tags/repost-form.tag'
require './tags/timeline-post-sub.tag'
require './tags/sub-post-content.tag'
require './tags/images-viewer.tag'
require './tags/image-dialog.tag'
require './tags/donation.tag'
require './tags/user-posts-graph.tag'
require './tags/user-friends-graph.tag'
require './tags/user-likes-graph.tag'
require './tags/post-status-graph.tag'
require './tags/debugger.tag'
require './tags/users-list.tag'
require './tags/user-following.tag'
require './tags/user-followers.tag'
require './tags/user-following-window.tag'
require './tags/user-followers-window.tag'
require './tags/list-user.tag'
require './tags/ui-notification.tag'
require './tags/signin-history.tag'
require './tags/log.tag'
require './tags/log-window.tag'

View File

@@ -0,0 +1,102 @@
mk-analog-clock
canvas@canvas(width='256', height='256')
style.
> canvas
display block
width 256px
height 256px
script.
@on \mount ~>
@draw!
@clock = set-interval @draw, 1000ms
@on \unmount ~>
clear-interval @clock
@draw = ~>
now = new Date!
s = now.get-seconds!
m = now.get-minutes!
h = now.get-hours!
vec2 = (x, y) ->
@x = x
@y = y
ctx = @refs.canvas.get-context \2d
canv-w = @refs.canvas.width
canv-h = @refs.canvas.height
ctx.clear-rect 0, 0, canv-w, canv-h
# 背景
center = (Math.min (canv-w / 2), (canv-h / 2))
line-start = center * 0.90
line-end-short = center * 0.87
line-end-long = center * 0.84
for i from 0 to 59 by 1
angle = Math.PI * i / 30
uv = new vec2 (Math.sin angle), (-Math.cos angle)
ctx.begin-path!
ctx.line-width = 1
ctx.move-to do
(canv-w / 2) + uv.x * line-start
(canv-h / 2) + uv.y * line-start
if i % 5 == 0
ctx.stroke-style = 'rgba(255, 255, 255, 0.2)'
ctx.line-to do
(canv-w / 2) + uv.x * line-end-long
(canv-h / 2) + uv.y * line-end-long
else
ctx.stroke-style = 'rgba(255, 255, 255, 0.1)'
ctx.line-to do
(canv-w / 2) + uv.x * line-end-short
(canv-h / 2) + uv.y * line-end-short
ctx.stroke!
# 分
angle = Math.PI * (m + s / 60) / 30
length = (Math.min canv-w, canv-h) / 2.6
uv = new vec2 (Math.sin angle), (-Math.cos angle)
ctx.begin-path!
ctx.stroke-style = \#ffffff
ctx.line-width = 2
ctx.move-to do
(canv-w / 2) - uv.x * length / 5
(canv-h / 2) - uv.y * length / 5
ctx.line-to do
(canv-w / 2) + uv.x * length
(canv-h / 2) + uv.y * length
ctx.stroke!
# 時
angle = Math.PI * (h % 12 + m / 60) / 6
length = (Math.min canv-w, canv-h) / 4
uv = new vec2 (Math.sin angle), (-Math.cos angle)
ctx.begin-path!
#ctx.stroke-style = \#ffffff
ctx.stroke-style = CONFIG.theme-color
ctx.line-width = 2
ctx.move-to do
(canv-w / 2) - uv.x * length / 5
(canv-h / 2) - uv.y * length / 5
ctx.line-to do
(canv-w / 2) + uv.x * length
(canv-h / 2) + uv.y * length
ctx.stroke!
# 秒
angle = Math.PI * s / 30
length = (Math.min canv-w, canv-h) / 2.6
uv = new vec2 (Math.sin angle), (-Math.cos angle)
ctx.begin-path!
ctx.stroke-style = 'rgba(255, 255, 255, 0.5)'
ctx.line-width = 1
ctx.move-to do
(canv-w / 2) - uv.x * length / 5
(canv-h / 2) - uv.y * length / 5
ctx.line-to do
(canv-w / 2) + uv.x * length
(canv-h / 2) + uv.y * length
ctx.stroke!

View File

@@ -0,0 +1,182 @@
mk-autocomplete-suggestion
ol.users@users(if={ users.length > 0 })
li(each={ users }, onclick={ parent.on-click }, onkeydown={ parent.on-keydown }, tabindex='-1')
img.avatar(src={ avatar_url + '?thumbnail&size=32' }, alt='')
span.name { name }
span.username @{ username }
style.
display block
position absolute
z-index 65535
margin-top calc(1em + 8px)
overflow hidden
background #fff
border solid 1px rgba(0, 0, 0, 0.1)
border-radius 4px
> .users
display block
margin 0
padding 4px 0
max-height 190px
max-width 500px
overflow auto
list-style none
> li
display block
padding 4px 12px
white-space nowrap
overflow hidden
font-size 0.9em
color rgba(0, 0, 0, 0.8)
cursor default
&, *
user-select none
&:hover
&[data-selected='true']
color #fff
background $theme-color
.name
color #fff
.username
color #fff
&:active
color #fff
background darken($theme-color, 10%)
.name
color #fff
.username
color #fff
.avatar
vertical-align middle
min-width 28px
min-height 28px
max-width 28px
max-height 28px
margin 0 8px 0 0
border-radius 100%
.name
margin 0 8px 0 0
/*font-weight bold*/
font-weight normal
color rgba(0, 0, 0, 0.8)
.username
font-weight normal
color rgba(0, 0, 0, 0.3)
script.
@mixin \api
@q = @opts.q
@textarea = @opts.textarea
@loading = true
@users = []
@select = -1
@on \mount ~>
@textarea.add-event-listener \keydown @on-keydown
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.add-event-listener \mousedown @mousedown
@api \users/search_by_username do
query: @q
limit: 30users
.then (users) ~>
@users = users
@loading = false
@update!
.catch (err) ~>
console.error err
@on \unmount ~>
@textarea.remove-event-listener \keydown @on-keydown
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.remove-event-listener \mousedown @mousedown
@mousedown = (e) ~>
if (!contains @root, e.target) and (@root != e.target)
@close!
@on-click = (e) ~>
@complete e.item
@on-keydown = (e) ~>
key = e.which
switch (key)
| 10, 13 => # Key[ENTER]
if @select != -1
e.prevent-default!
e.stop-propagation!
@complete @users[@select]
else
@close!
| 27 => # Key[ESC]
e.prevent-default!
e.stop-propagation!
@close!
| 38 => # Key[↑]
if @select != -1
e.prevent-default!
e.stop-propagation!
@select-prev!
else
@close!
| 9, 40 => # Key[TAB] or Key[↓]
e.prevent-default!
e.stop-propagation!
@select-next!
| _ =>
@close!
@select-next = ~>
@select++
if @select >= @users.length
@select = 0
@apply-select!
@select-prev = ~>
@select--
if @select < 0
@select = @users.length - 1
@apply-select!
@apply-select = ~>
@refs.users.children.for-each (el) ~>
el.remove-attribute \data-selected
@refs.users.children[@select].set-attribute \data-selected \true
@refs.users.children[@select].focus!
@complete = (user) ~>
@opts.complete user
@close = ~>
@opts.close!
function contains(parent, child)
node = child.parent-node
while node?
if node == parent
return true
node = node.parent-node
return false

View File

@@ -0,0 +1,134 @@
mk-big-follow-button
button(if={ !init }, class={ wait: wait, follow: !user.is_following, unfollow: user.is_following },
onclick={ onclick },
disabled={ wait },
title={ user.is_following ? 'フォロー解除' : 'フォローする' })
span(if={ !wait && user.is_following })
i.fa.fa-minus
| フォロー解除
span(if={ !wait && !user.is_following })
i.fa.fa-plus
| フォロー
i.fa.fa-spinner.fa-pulse.fa-fw(if={ wait })
div.init(if={ init }): i.fa.fa-spinner.fa-pulse.fa-fw
style.
display block
> button
> .init
display block
cursor pointer
padding 0
margin 0
width 100%
line-height 38px
font-size 1em
outline none
border-radius 4px
*
pointer-events none
i
margin-right 8px
&: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
&.follow
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
&.unfollow
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
&.wait
cursor wait !important
opacity 0.7
script.
@mixin \api
@mixin \is-promise
@mixin \stream
@user = null
@user-promise = if @is-promise @opts.user then @opts.user else Promise.resolve @opts.user
@init = true
@wait = false
@on \mount ~>
@user-promise.then (user) ~>
@user = user
@init = false
@update!
@stream.on \follow @on-stream-follow
@stream.on \unfollow @on-stream-unfollow
@on \unmount ~>
@stream.off \follow @on-stream-follow
@stream.off \unfollow @on-stream-unfollow
@on-stream-follow = (user) ~>
if user.id == @user.id
@user = user
@update!
@on-stream-unfollow = (user) ~>
if user.id == @user.id
@user = user
@update!
@onclick = ~>
@wait = true
if @user.is_following
@api \following/delete do
user_id: @user.id
.then ~>
@user.is_following = false
.catch (err) ->
console.error err
.then ~>
@wait = false
@update!
else
@api \following/create do
user_id: @user.id
.then ~>
@user.is_following = true
.catch (err) ->
console.error err
.then ~>
@wait = false
@update!

View File

@@ -0,0 +1,138 @@
mk-contextmenu
| <yield />
style.
$width = 240px
$item-height = 38px
$padding = 10px
display none
position fixed
top 0
left 0
z-index 4096
width $width
font-size 0.8em
background #fff
border-radius 0 4px 4px 4px
box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
ul
display block
margin 0
padding $padding 0
list-style none
li
display block
&.separator
margin-top $padding
padding-top $padding
border-top solid 1px #eee
&.has-child
> p
cursor default
> i:last-child
position absolute
top 0
right 8px
line-height $item-height
&:hover > ul
visibility visible
&:active
> p, a
background $theme-color
> p, a
display block
z-index 1
margin 0
padding 0 32px 0 38px
line-height $item-height
color #868C8C
text-decoration none
cursor pointer
&:hover
text-decoration none
*
pointer-events none
> i
width 28px
margin-left -28px
text-align center
&:hover
> p, a
text-decoration none
background $theme-color
color $theme-color-foreground
&:active
> p, a
text-decoration none
background darken($theme-color, 10%)
color $theme-color-foreground
li > ul
visibility hidden
position absolute
top 0
left $width
margin-top -($padding)
width $width
background #fff
border-radius 0 4px 4px 4px
box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
transition visibility 0s linear 0.2s
script.
@root.add-event-listener \contextmenu (e) ~>
e.prevent-default!
@mousedown = (e) ~>
e.prevent-default!
if (!contains @root, e.target) and (@root != e.target)
@close!
return false
@open = (pos) ~>
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.add-event-listener \mousedown @mousedown
@root.style.display = \block
@root.style.left = pos.x + \px
@root.style.top = pos.y + \px
Velocity @root, \finish true
Velocity @root, { opacity: 0 } 0ms
Velocity @root, {
opacity: 1
} {
queue: false
duration: 100ms
easing: \linear
}
@close = ~>
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.remove-event-listener \mousedown @mousedown
@trigger \closed
@unmount!
function contains(parent, child)
node = child.parent-node
while (node != null)
if (node == parent)
return true
node = node.parent-node
return false

View File

@@ -0,0 +1,189 @@
mk-crop-window
mk-window@window(is-modal={ true }, width={ '800px' })
<yield to="header">
i.fa.fa-crop
| { parent.title }
</yield>
<yield to="content">
div.body
img@img(src={ parent.image.url + '?thumbnail&quality=80' }, alt='')
div.action
button.skip(onclick={ parent.skip }) クロップをスキップ
button.cancel(onclick={ parent.cancel }) キャンセル
button.ok(onclick={ parent.ok }) 決定
</yield>
style.
display block
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
> .body
> img
width 100%
max-height 400px
.cropper-modal {
opacity: 0.8;
}
.cropper-view-box {
outline-color: $theme-color;
}
.cropper-line, .cropper-point {
background-color: $theme-color;
}
.cropper-bg {
animation: cropper-bg 0.5s linear infinite;
}
@-webkit-keyframes cropper-bg {
0% {
background-position: 0 0;
}
100% {
background-position: -8px -8px;
}
}
@-moz-keyframes cropper-bg {
0% {
background-position: 0 0;
}
100% {
background-position: -8px -8px;
}
}
@-ms-keyframes cropper-bg {
0% {
background-position: 0 0;
}
100% {
background-position: -8px -8px;
}
}
@keyframes cropper-bg {
0% {
background-position: 0 0;
}
100% {
background-position: -8px -8px;
}
}
> .action
height 72px
background lighten($theme-color, 95%)
.ok
.cancel
.skip
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
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
.ok
.cancel
width 120px
.ok
right 16px
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
.cancel
.skip
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
.cancel
right 148px
.skip
left 16px
width 150px
script.
@mixin \cropper
@image = @opts.file
@title = @opts.title
@aspect-ratio = @opts.aspect-ratio
@cropper = null
@on \mount ~>
@img = @refs.window.refs.img
@cropper = new @Cropper @img, do
aspect-ratio: @aspect-ratio
highlight: no
view-mode: 1
@ok = ~>
@cropper.get-cropped-canvas!.to-blob (blob) ~>
@trigger \cropped blob
@refs.window.close!
@skip = ~>
@trigger \skiped
@refs.window.close!
@cancel = ~>
@trigger \canceled
@refs.window.close!

View File

@@ -0,0 +1,87 @@
mk-debugger
mk-window@window(is-modal={ false }, width={ '700px' }, height={ '550px' })
<yield to="header">
i.fa.fa-wrench
| Debugger
</yield>
<yield to="content">
section.progress-dialog
h1 progress-dialog
button.style-normal(onclick={ parent.progress-dialog }): i.fa.fa-play
button.style-normal(onclick={ parent.progress-dialog-destroy }): i.fa.fa-stop
label
p TITLE:
input@progress-title(value='Title')
label
p VAL:
input@progress-value(type='number', oninput={ parent.progress-change }, value=0)
label
p MAX:
input@progress-max(type='number', oninput={ parent.progress-change }, value=100)
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
overflow auto
> section
padding 32px
// & + section
// margin-top 16px
> h1
display block
margin 0
padding 0 0 8px 0
font-size 1em
color #555
border-bottom solid 1px #eee
> label
display block
> p
display inline
margin 0
> .progress-dialog
button
display inline-block
margin 8px
script.
@mixin \open-window
@on \mount ~>
@progress-title = @tags['mk-window'].progress-title
@progress-value = @tags['mk-window'].progress-value
@progress-max = @tags['mk-window'].progress-max
@refs.window.on \closed ~>
@unmount!
################################
@progress-controller = riot.observable!
@progress-dialog = ~>
@open-window \mk-progress-dialog do
title: @progress-title.value
value: @progress-value.value
max: @progress-max.value
controller: @progress-controller
@progress-change = ~>
@progress-controller.trigger do
\update
@progress-value.value
@progress-max.value
@progress-dialog-destroy = ~>
@progress-controller.trigger \close

View File

@@ -0,0 +1,56 @@
mk-detect-slow-internet-connection-notice
i: i.fa.fa-exclamation
div: p インターネット回線が遅いようです。
style.
display block
pointer-events none
position fixed
z-index 16384
top 64px
right 16px
margin 0
padding 0
width 298px
font-size 0.9em
background #fff
box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
opacity 0
> i
display block
width 48px
line-height 48px
margin-right 0.25em
text-align center
color $theme-color-foreground
font-size 1.5em
background $theme-color
> div
display block
position absolute
top 0
left 48px
margin 0
width 250px
height 48px
color #666
> p
display block
margin 0
padding 8px
script.
@mixin \net
@net.on \detected-slow-network ~>
Velocity @root, {
opacity: 1
} 200ms \linear
set-timeout ~>
Velocity @root, {
opacity: 0
} 200ms \linear
, 10000ms

View File

@@ -0,0 +1,141 @@
mk-dialog
div.bg@bg(onclick={ bg-click })
div.main@main
header@header
div.body@body
div.buttons
virtual(each={ opts.buttons })
button(onclick={ _onclick }) { text }
style.
display block
> .bg
display block
position fixed
z-index 8192
top 0
left 0
width 100%
height 100%
background rgba(0, 0, 0, 0.7)
opacity 0
pointer-events none
> .main
display block
position fixed
z-index 8192
top 20%
left 0
right 0
margin 0 auto 0 auto
padding 32px 42px
width 480px
background #fff
> header
margin 1em 0
color $theme-color
// color #43A4EC
font-weight bold
> i
margin-right 0.5em
> .body
margin 1em 0
color #888
> .buttons
> button
display inline-block
float right
margin 0
padding 10px 10px
font-size 1.1em
font-weight normal
text-decoration none
color #888
background transparent
outline none
border none
border-radius 0
cursor pointer
transition color 0.1s ease
i
margin 0 0.375em
&:hover
color $theme-color
&:active
color darken($theme-color, 10%)
transition color 0s ease
script.
@can-through = if opts.can-through? then opts.can-through else true
@opts.buttons.for-each (button) ~>
button._onclick = ~>
if button.onclick?
button.onclick!
@close!
@on \mount ~>
@refs.header.innerHTML = @opts.title
@refs.body.innerHTML = @opts.text
@refs.bg.style.pointer-events = \auto
Velocity @refs.bg, \finish true
Velocity @refs.bg, {
opacity: 1
} {
queue: false
duration: 100ms
easing: \linear
}
Velocity @refs.main, {
opacity: 0
scale: 1.2
} {
duration: 0
}
Velocity @refs.main, {
opacity: 1
scale: 1
} {
duration: 300ms
easing: [ 0, 0.5, 0.5, 1 ]
}
@close = ~>
@refs.bg.style.pointer-events = \none
Velocity @refs.bg, \finish true
Velocity @refs.bg, {
opacity: 0
} {
queue: false
duration: 300ms
easing: \linear
}
@refs.main.style.pointer-events = \none
Velocity @refs.main, \finish true
Velocity @refs.main, {
opacity: 0
scale: 0.8
} {
queue: false
duration: 300ms
easing: [ 0.5, -0.5, 1, 0.5 ]
complete: ~>
@unmount!
}
@bg-click = ~>
if @can-through
if @opts.on-through?
@opts.on-through!
@close!

View File

@@ -0,0 +1,63 @@
mk-donation
button.close(onclick={ close }) 閉じる x
div.message
p 利用者の皆さま、
p
| 今日は、日本の皆さまにお知らせがあります。
| Misskeyの援助をお願いいたします。
| 私は独立性を守るため、一切の広告を掲載いたしません。
| 平均で約¥1,500の寄付をいただき、運営しております。
| 援助をしてくださる利用者はほんの少数です。
| お願いいたします。
| 今日、利用者の皆さまが¥300ご援助くだされば、募金活動を一時間で終了することができます。
| コーヒー1杯ほどの金額です。
| Misskeyを活用しておられるのでしたら、広告を掲載せずにもう1年活動できるよう、どうか1分だけお時間をください。
| 私は小さな非営利個人ですが、サーバー、プログラム、人件費など、世界でトップクラスのウェブサイト同等のコストがかかります。
| 利用者は何億人といますが、他の大きなサイトに比べてほんの少額の費用で運営しているのです。
| 人間の可能性、自由、そして機会。知識こそ、これらの基盤を成すものです。
| 私は、誰もが無料かつ制限なく知識に触れられるべきだと信じています。
| 募金活動を終了し、Misskeyの改善に戻れるようご援助ください。
| よろしくお願いいたします。
style.
display block
color #fff
background #03072C
> .close
position absolute
top 16px
right 16px
z-index 1
> .message
padding 32px
font-size 1.4em
font-family serif
> p
display block
margin 0 auto
max-width 1200px
> p:first-child
margin-bottom 16px
script.
@mixin \api
@mixin \i
@close = (e) ~>
e.prevent-default!
e.stop-propagation!
@I.data.no_donation = true
@api \i/appdata/set do
data: JSON.stringify do
no_donation: @I.data.no_donation
.then ~>
@update-i!
@unmount!
@parent.parent.set-root-layout!

View File

@@ -0,0 +1,28 @@
mk-drive-browser-base-contextmenu
mk-contextmenu@ctx
ul
li(onclick={ parent.create-folder }): p
i.fa.fa-folder-o
| フォルダーを作成
li(onclick={ parent.upload }): p
i.fa.fa-upload
| ファイルをアップロード
script.
@browser = @opts.browser
@on \mount ~>
@refs.ctx.on \closed ~>
@trigger \closed
@unmount!
@open = (pos) ~>
@refs.ctx.open pos
@create-folder = ~>
@browser.create-folder!
@refs.ctx.close!
@upload = ~>
@browser.select-local-file!
@refs.ctx.close!

View File

@@ -0,0 +1,29 @@
mk-drive-browser-window
mk-window@window(is-modal={ false }, width={ '800px' }, height={ '500px' })
<yield to="header">
i.fa.fa-cloud
| ドライブ
</yield>
<yield to="content">
mk-drive-browser(multiple={ true }, folder={ parent.folder })
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
> mk-drive-browser
height 100%
script.
@folder = if @opts.folder? then @opts.folder else null
@on \mount ~>
@refs.window.on \closed ~>
@unmount!
@close = ~>
@refs.window.close!

View File

@@ -0,0 +1,634 @@
mk-drive-browser
nav
div.path(oncontextmenu={ path-oncontextmenu })
mk-drive-browser-nav-folder(class={ current: folder == null }, folder={ null })
virtual(each={ folder in hierarchy-folders })
span.separator: i.fa.fa-angle-right
mk-drive-browser-nav-folder(folder={ folder })
span.separator(if={ folder != null }): i.fa.fa-angle-right
span.folder.current(if={ folder != null })
| { folder.name }
input.search(type='search', placeholder!='&#xf002; 検索')
div.main@main(class={ uploading: uploads.length > 0, loading: loading }, onmousedown={ onmousedown }, ondragover={ ondragover }, ondragenter={ ondragenter }, ondragleave={ ondragleave }, ondrop={ ondrop }, oncontextmenu={ oncontextmenu })
div.selection@selection
div.contents@contents
div.folders@folders-container(if={ folders.length > 0 })
virtual(each={ folder in folders })
mk-drive-browser-folder.folder(folder={ folder })
button(if={ more-folders })
| もっと読み込む
div.files@files-container(if={ files.length > 0 })
virtual(each={ file in files })
mk-drive-browser-file.file(file={ file })
button(if={ more-files })
| もっと読み込む
div.empty(if={ files.length == 0 && folders.length == 0 && !loading })
p(if={ draghover })
| ドロップですか?いいですよ、ボクはカワイイですからね
p(if={ !draghover && folder == null })
strong ドライブには何もありません。
br
| 右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできます。
p(if={ !draghover && folder != null })
| このフォルダーは空です
div.loading(if={ loading }).
<div class="spinner">
<div class="dot1"></div>
<div class="dot2"></div>
</div>
div.dropzone(if={ draghover })
mk-uploader@uploader
input@file-input(type='file', accept='*/*', multiple, tabindex='-1', onchange={ change-file-input })
style.
display block
> nav
display block
z-index 2
width 100%
overflow auto
font-size 0.9em
color #555
background #fff
//border-bottom 1px solid #dfdfdf
box-shadow 0 1px 0 rgba(0, 0, 0, 0.05)
&, *
user-select none
> .path
display inline-block
vertical-align bottom
margin 0
padding 0 8px
width calc(100% - 200px)
line-height 38px
white-space nowrap
> *
display inline-block
margin 0
padding 0 8px
line-height 38px
cursor pointer
i
margin-right 4px
*
pointer-events none
&:hover
text-decoration underline
&.current
font-weight bold
cursor default
&:hover
text-decoration none
&.separator
margin 0
padding 0
opacity 0.5
cursor default
> i
margin 0
> .search
display inline-block
vertical-align bottom
user-select text
cursor auto
margin 0
padding 0 18px
width 200px
font-size 1em
line-height 38px
background transparent
outline none
//border solid 1px #ddd
border none
border-radius 0
box-shadow none
transition color 0.5s ease, border 0.5s ease
font-family FontAwesome, sans-serif
&[data-active='true']
background #fff
&::-webkit-input-placeholder,
&:-ms-input-placeholder,
&:-moz-placeholder
color $ui-controll-foreground-color
> .main
padding 8px
height calc(100% - 38px)
overflow auto
&, *
user-select none
&.loading
cursor wait !important
*
pointer-events none
> .contents
opacity 0.5
&.uploading
height calc(100% - 38px - 100px)
> .selection
display none
position absolute
z-index 128
top 0
left 0
border solid 1px $theme-color
background rgba($theme-color, 0.5)
pointer-events none
> .contents
> .folders
&:after
content ""
display block
clear both
> .folder
float left
> .files
&:after
content ""
display block
clear both
> .file
float left
> .empty
padding 16px
text-align center
color #999
pointer-events none
> p
margin 0
> .loading
.spinner
margin 100px auto
width 40px
height 40px
text-align center
animation sk-rotate 2.0s infinite linear
.dot1, .dot2
width 60%
height 60%
display inline-block
position absolute
top 0
background-color rgba(0, 0, 0, 0.3)
border-radius 100%
animation sk-bounce 2.0s infinite ease-in-out
.dot2
top auto
bottom 0
animation-delay -1.0s
@keyframes sk-rotate { 100% { transform: rotate(360deg); }}
@keyframes sk-bounce {
0%, 100% {
transform: scale(0.0);
} 50% {
transform: scale(1.0);
}
}
> .dropzone
position absolute
left 0
top 38px
width 100%
height calc(100% - 38px)
border dashed 2px rgba($theme-color, 0.5)
pointer-events none
> mk-uploader
height 100px
padding 16px
background #fff
> input
display none
script.
@mixin \api
@mixin \dialog
@mixin \input-dialog
@mixin \stream
@files = []
@folders = []
@hierarchy-folders = []
@uploads = []
# 現在の階層(フォルダ)
# * null でルートを表す
@folder = null
@multiple = if @opts.multiple? then @opts.multiple else false
# ドロップされようとしているか
@draghover = false
# 自信の所有するアイテムがドラッグをスタートさせたか
# (自分自身の階層にドロップできないようにするためのフラグ)
@is-drag-source = false
@on \mount ~>
@refs.uploader.on \uploaded (file) ~>
@add-file file, true
@refs.uploader.on \change-uploads (uploads) ~>
@uploads = uploads
@update!
@stream.on \drive_file_created @on-stream-drive-file-created
@stream.on \drive_file_updated @on-stream-drive-file-updated
@stream.on \drive_folder_created @on-stream-drive-folder-created
@stream.on \drive_folder_updated @on-stream-drive-folder-updated
# Riotのバグでnullを渡しても""になる
# https://github.com/riot/riot/issues/2080
#if @opts.folder?
if @opts.folder? and @opts.folder != ''
@move @opts.folder
else
@load!
@on \unmount ~>
@stream.off \drive_file_created @on-stream-drive-file-created
@stream.off \drive_file_updated @on-stream-drive-file-updated
@stream.off \drive_folder_created @on-stream-drive-folder-created
@stream.off \drive_folder_updated @on-stream-drive-folder-updated
@on-stream-drive-file-created = (file) ~>
@add-file file, true
@on-stream-drive-file-updated = (file) ~>
current = if @folder? then @folder.id else null
if current != file.folder_id
@remove-file file
else
@add-file file, true
@on-stream-drive-folder-created = (folder) ~>
@add-folder folder, true
@on-stream-drive-folder-updated = (folder) ~>
current = if @folder? then @folder.id else null
if current != folder.parent_id
@remove-folder folder
else
@add-folder folder, true
@onmousedown = (e) ~>
if (contains @refs.folders-container, e.target) or (contains @refs.files-container, e.target)
return true
rect = @refs.main.get-bounding-client-rect!
left = e.page-x + @refs.main.scroll-left - rect.left - window.page-x-offset
top = e.page-y + @refs.main.scroll-top - rect.top - window.page-y-offset
move = (e) ~>
@refs.selection.style.display = \block
cursor-x = e.page-x + @refs.main.scroll-left - rect.left - window.page-x-offset
cursor-y = e.page-y + @refs.main.scroll-top - rect.top - window.page-y-offset
w = cursor-x - left
h = cursor-y - top
if w > 0
@refs.selection.style.width = w + \px
@refs.selection.style.left = left + \px
else
@refs.selection.style.width = -w + \px
@refs.selection.style.left = cursor-x + \px
if h > 0
@refs.selection.style.height = h + \px
@refs.selection.style.top = top + \px
else
@refs.selection.style.height = -h + \px
@refs.selection.style.top = cursor-y + \px
up = (e) ~>
document.document-element.remove-event-listener \mousemove move
document.document-element.remove-event-listener \mouseup up
@refs.selection.style.display = \none
document.document-element.add-event-listener \mousemove move
document.document-element.add-event-listener \mouseup up
@path-oncontextmenu = (e) ~>
e.prevent-default!
e.stop-immediate-propagation!
return false
@ondragover = (e) ~>
e.prevent-default!
e.stop-propagation!
# ドラッグ元が自分自身の所有するアイテムかどうか
if !@is-drag-source
# ドラッグされてきたものがファイルだったら
if e.data-transfer.effect-allowed == \all
e.data-transfer.drop-effect = \copy
else
e.data-transfer.drop-effect = \move
@draghover = true
else
# 自分自身にはドロップさせない
e.data-transfer.drop-effect = \none
return false
@ondragenter = (e) ~>
e.prevent-default!
if !@is-drag-source
@draghover = true
@ondragleave = (e) ~>
@draghover = false
@ondrop = (e) ~>
e.prevent-default!
e.stop-propagation!
@draghover = false
# ドロップされてきたものがファイルだったら
if e.data-transfer.files.length > 0
Array.prototype.for-each.call e.data-transfer.files, (file) ~>
@upload file, @folder
return false
# データ取得
data = e.data-transfer.get-data 'text'
if !data?
return false
# パース
obj = JSON.parse data
# (ドライブの)ファイルだったら
if obj.type == \file
file = obj.id
if (@files.some (f) ~> f.id == file)
return false
@remove-file file
@api \drive/files/update do
file_id: file
folder_id: if @folder? then @folder.id else \null
.then ~>
# something
.catch (err, text-status) ~>
console.error err
# (ドライブの)フォルダーだったら
else if obj.type == \folder
folder = obj.id
# 移動先が自分自身ならreject
if @folder? and folder == @folder.id
return false
if (@folders.some (f) ~> f.id == folder)
return false
@remove-folder folder
@api \drive/folders/update do
folder_id: folder
parent_id: if @folder? then @folder.id else \null
.then ~>
# something
.catch (err) ~>
if err == 'detected-circular-definition'
@dialog do
'<i class="fa fa-exclamation-triangle"></i>操作を完了できません'
'移動先のフォルダーは、移動するフォルダーのサブフォルダーです。'
[
text: \OK
]
return false
@oncontextmenu = (e) ~>
e.prevent-default!
e.stop-immediate-propagation!
ctx = document.body.append-child document.create-element \mk-drive-browser-base-contextmenu
ctx = riot.mount ctx, do
browser: @
ctx = ctx.0
ctx.open do
x: e.page-x - window.page-x-offset
y: e.page-y - window.page-y-offset
return false
@select-local-file = ~>
@refs.file-input.click!
@create-folder = ~>
name <~ @input-dialog do
'フォルダー作成'
'フォルダー名'
null
@api \drive/folders/create do
name: name
folder_id: if @folder? then @folder.id else undefined
.then (folder) ~>
@add-folder folder, true
@update!
.catch (err) ~>
console.error err
@change-file-input = ~>
files = @refs.file-input.files
for i from 0 to files.length - 1
file = files.item i
@upload file, @folder
@upload = (file, folder) ~>
if folder? and typeof folder == \object
folder = folder.id
@refs.uploader.upload file, folder
@get-selection = ~>
@files.filter (file) -> file._selected
@new-window = (folder-id) ~>
browser = document.body.append-child document.create-element \mk-drive-browser-window
riot.mount browser, do
folder: folder-id
@move = (target-folder) ~>
if target-folder? and typeof target-folder == \object
target-folder = target-folder.id
if target-folder == null
@go-root!
return
@loading = true
@update!
@api \drive/folders/show do
folder_id: target-folder
.then (folder) ~>
@folder = folder
@hierarchy-folders = []
x = (f) ~>
@hierarchy-folders.unshift f
if f.parent?
x f.parent
if folder.parent?
x folder.parent
@update!
@load!
.catch (err, text-status) ->
console.error err
@add-folder = (folder, unshift = false) ~>
current = if @folder? then @folder.id else null
if current != folder.parent_id
return
if (@folders.some (f) ~> f.id == folder.id)
exist = (@folders.map (f) -> f.id).index-of folder.id
@folders[exist] = folder
@update!
return
if unshift
@folders.unshift folder
else
@folders.push folder
@update!
@add-file = (file, unshift = false) ~>
current = if @folder? then @folder.id else null
if current != file.folder_id
return
if (@files.some (f) ~> f.id == file.id)
exist = (@files.map (f) -> f.id).index-of file.id
@files[exist] = file
@update!
return
if unshift
@files.unshift file
else
@files.push file
@update!
@remove-folder = (folder) ~>
if typeof folder == \object
folder = folder.id
@folders = @folders.filter (f) -> f.id != folder
@update!
@remove-file = (file) ~>
if typeof file == \object
file = file.id
@files = @files.filter (f) -> f.id != file
@update!
@go-root = ~>
if @folder != null
@folder = null
@hierarchy-folders = []
@update!
@load!
@load = ~>
@folders = []
@files = []
@more-folders = false
@more-files = false
@loading = true
@update!
load-folders = null
load-files = null
folders-max = 30
files-max = 30
# フォルダ一覧取得
@api \drive/folders do
folder_id: if @folder? then @folder.id else null
limit: folders-max + 1
.then (folders) ~>
if folders.length == folders-max + 1
@more-folders = true
folders.pop!
load-folders := folders
complete!
.catch (err, text-status) ~>
console.error err
# ファイル一覧取得
@api \drive/files do
folder_id: if @folder? then @folder.id else null
limit: files-max + 1
.then (files) ~>
if files.length == files-max + 1
@more-files = true
files.pop!
load-files := files
complete!
.catch (err, text-status) ~>
console.error err
flag = false
complete = ~>
if flag
load-folders.for-each (folder) ~>
@add-folder folder
load-files.for-each (file) ~>
@add-file file
@loading = false
@update!
else
flag := true
function contains(parent, child)
node = child.parent-node
while node?
if node == parent
return true
node = node.parent-node
return false

View File

@@ -0,0 +1,97 @@
mk-drive-browser-file-contextmenu
mk-contextmenu@ctx: ul
li(onclick={ parent.rename }): p
i.fa.fa-i-cursor
| 名前を変更
li(onclick={ parent.copy-url }): p
i.fa.fa-link
| URLをコピー
li: a(href={ parent.file.url + '?download' }, download={ parent.file.name }, onclick={ parent.download })
i.fa.fa-download
| ダウンロード
li.separator
li(onclick={ parent.delete }): p
i.fa.fa-trash-o
| 削除
li.separator
li.has-child
p
| その他...
i.fa.fa-caret-right
ul
li(onclick={ parent.set-avatar }): p
| アバターに設定
li(onclick={ parent.set-banner }): p
| バナーに設定
li(onclick={ parent.set-wallpaper }): p
| 壁紙に設定
li.has-child
p
| アプリで開く...
i.fa.fa-caret-right
ul
li(onclick={ parent.add-app }): p
| アプリを追加...
script.
@mixin \api
@mixin \i
@mixin \update-avatar
@mixin \update-banner
@mixin \update-wallpaper
@mixin \input-dialog
@mixin \NotImplementedException
@browser = @opts.browser
@file = @opts.file
@on \mount ~>
@refs.ctx.on \closed ~>
@trigger \closed
@unmount!
@open = (pos) ~>
@refs.ctx.open pos
@rename = ~>
@refs.ctx.close!
name <~ @input-dialog do
'ファイル名の変更'
'新しいファイル名を入力してください'
@file.name
@api \drive/files/update do
file_id: @file.id
name: name
.then ~>
# something
.catch (err) ~>
console.error err
@copy-url = ~>
@NotImplementedException!
@download = ~>
@refs.ctx.close!
@set-avatar = ~>
@refs.ctx.close!
@update-avatar @I, (i) ~>
@update-i i
, @file
@set-banner = ~>
@refs.ctx.close!
@update-banner @I, (i) ~>
@update-i i
, @file
@set-wallpaper = ~>
@refs.ctx.close!
@update-wallpaper @I, (i) ~>
@update-i i
, @file
@add-app = ~>
@NotImplementedException!

View File

@@ -0,0 +1,207 @@
mk-drive-browser-file(data-is-selected={ (file._selected || false).toString() }, data-is-contextmenu-showing={ is-contextmenu-showing.toString() }, onclick={ onclick }, oncontextmenu={ oncontextmenu }, draggable='true', ondragstart={ ondragstart }, ondragend={ ondragend }, title={ title })
div.label(if={ I.avatar_id == file.id })
img(src='/_/resources/label.svg')
p アバター
div.label(if={ I.banner_id == file.id })
img(src='/_/resources/label.svg')
p バナー
div.label(if={ I.data.wallpaper == file.id })
img(src='/_/resources/label.svg')
p 壁紙
div.thumbnail: img(src={ file.url + '?thumbnail&size=128' }, alt='')
p.name
span { file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }
span.ext(if={ file.name.lastIndexOf('.') != -1 }) { file.name.substr(file.name.lastIndexOf('.')) }
style.
display block
margin 4px
padding 8px 0 0 0
width 144px
height 180px
border-radius 4px
&, *
cursor pointer
&:hover
background rgba(0, 0, 0, 0.05)
> .label
&:before
&:after
background #0b65a5
&:active
background rgba(0, 0, 0, 0.1)
> .label
&:before
&:after
background #0b588c
&[data-is-selected='true']
background $theme-color
&:hover
background lighten($theme-color, 10%)
&:active
background darken($theme-color, 10%)
> .label
&:before
&:after
display none
> .name
color $theme-color-foreground
&[data-is-contextmenu-showing='true']
&:after
content ""
pointer-events none
position absolute
top -4px
right -4px
bottom -4px
left -4px
border 2px dashed rgba($theme-color, 0.3)
border-radius 4px
> .label
position absolute
top 0
left 0
pointer-events none
&:before
content ""
display block
position absolute
z-index 1
top 0
left 57px
width 28px
height 8px
background #0c7ac9
&:after
content ""
display block
position absolute
z-index 1
top 57px
left 0
width 8px
height 28px
background #0c7ac9
> img
position absolute
z-index 2
top 0
left 0
> p
position absolute
z-index 3
top 19px
left -28px
width 120px
margin 0
text-align center
line-height 28px
color #fff
transform rotate(-45deg)
> .thumbnail
width 128px
height 128px
left 8px
> img
display block
position absolute
top 0
left 0
right 0
bottom 0
margin auto
max-width 128px
max-height 128px
pointer-events none
> .name
display block
margin 4px 0 0 0
font-size 0.8em
text-align center
word-break break-all
color #444
overflow hidden
> .ext
opacity 0.5
script.
@mixin \i
@mixin \bytes-to-size
@file = @opts.file
@browser = @parent
@title = @file.name + '\n' + @file.type + ' ' + (@bytes-to-size @file.datasize)
@is-contextmenu-showing = false
@onclick = ~>
if @browser.multiple
if @file._selected?
@file._selected = !@file._selected
else
@file._selected = true
@browser.trigger \change-selection @browser.get-selection!
else
if @file._selected
@browser.trigger \selected @file
else
@browser.files.for-each (file) ~>
file._selected = false
@file._selected = true
@browser.trigger \change-selection @file
@oncontextmenu = (e) ~>
e.prevent-default!
e.stop-immediate-propagation!
@is-contextmenu-showing = true
@update!
ctx = document.body.append-child document.create-element \mk-drive-browser-file-contextmenu
ctx = riot.mount ctx, do
browser: @browser
file: @file
ctx = ctx.0
ctx.open do
x: e.page-x - window.page-x-offset
y: e.page-y - window.page-y-offset
ctx.on \closed ~>
@is-contextmenu-showing = false
@update!
return false
@ondragstart = (e) ~>
e.data-transfer.effect-allowed = \move
e.data-transfer.set-data 'text' JSON.stringify do
type: \file
id: @file.id
file: @file
@is-dragging = true
# 親ブラウザに対して、ドラッグが開始されたフラグを立てる
# (=あなたの子供が、ドラッグを開始しましたよ)
@browser.is-drag-source = true
@ondragend = (e) ~>
@is-dragging = false
@browser.is-drag-source = false

View File

@@ -0,0 +1,62 @@
mk-drive-browser-folder-contextmenu
mk-contextmenu@ctx: ul
li(onclick={ parent.move }): p
i.fa.fa-arrow-right
| このフォルダへ移動
li(onclick={ parent.new-window }): p
i.fa.fa-share-square-o
| 新しいウィンドウで表示
li.separator
li(onclick={ parent.rename }): p
i.fa.fa-i-cursor
| 名前を変更
li.separator
li(onclick={ parent.delete }): p
i.fa.fa-trash-o
| 削除
script.
@mixin \api
@mixin \input-dialog
@browser = @opts.browser
@folder = @opts.folder
@open = (pos) ~>
@refs.ctx.open pos
@refs.ctx.on \closed ~>
@trigger \closed
@unmount!
@move = ~>
@browser.move @folder.id
@refs.ctx.close!
@new-window = ~>
@browser.new-window @folder.id
@refs.ctx.close!
@create-folder = ~>
@browser.create-folder!
@refs.ctx.close!
@upload = ~>
@browser.select-lcoal-file!
@refs.ctx.close!
@rename = ~>
@refs.ctx.close!
name <~ @input-dialog do
'フォルダ名の変更'
'新しいフォルダ名を入力してください'
@folder.name
@api \drive/folders/update do
folder_id: @folder.id
name: name
.then ~>
# something
.catch (err) ~>
console.error err

View File

@@ -0,0 +1,183 @@
mk-drive-browser-folder(data-is-contextmenu-showing={ is-contextmenu-showing.toString() }, data-draghover={ draghover.toString() }, onclick={ onclick }, onmouseover={ onmouseover }, onmouseout={ onmouseout }, ondragover={ ondragover }, ondragenter={ ondragenter }, ondragleave={ ondragleave }, ondrop={ ondrop }, oncontextmenu={ oncontextmenu }, draggable='true', ondragstart={ ondragstart }, ondragend={ ondragend }, title={ title })
p.name
i.fa.fa-fw(class={ fa-folder-o: !hover, fa-folder-open-o: hover })
| { folder.name }
style.
display block
margin 4px
padding 8px
width 144px
height 64px
background lighten($theme-color, 95%)
border-radius 4px
&, *
cursor pointer
*
pointer-events none
&:hover
background lighten($theme-color, 90%)
&:active
background lighten($theme-color, 85%)
&[data-is-contextmenu-showing='true']
&[data-draghover='true']
&:after
content ""
pointer-events none
position absolute
top -4px
right -4px
bottom -4px
left -4px
border 2px dashed rgba($theme-color, 0.3)
border-radius 4px
&[data-draghover='true']
background lighten($theme-color, 90%)
> .name
margin 0
font-size 0.9em
color darken($theme-color, 30%)
> i
margin-right 4px
margin-left 2px
text-align left
script.
@mixin \api
@mixin \dialog
@folder = @opts.folder
@browser = @parent
@title = @folder.name
@hover = false
@draghover = false
@is-contextmenu-showing = false
@onclick = ~>
@browser.move @folder
@onmouseover = ~>
@hover = true
@onmouseout = ~>
@hover = false
@ondragover = (e) ~>
e.prevent-default!
e.stop-propagation!
# 自分自身がドラッグされていない場合
if !@is-dragging
# ドラッグされてきたものがファイルだったら
if e.data-transfer.effect-allowed == \all
e.data-transfer.drop-effect = \copy
else
e.data-transfer.drop-effect = \move
else
# 自分自身にはドロップさせない
e.data-transfer.drop-effect = \none
return false
@ondragenter = ~>
if !@is-dragging
@draghover = true
@ondragleave = ~>
@draghover = false
@ondrop = (e) ~>
e.stop-propagation!
@draghover = false
# ファイルだったら
if e.data-transfer.files.length > 0
Array.prototype.for-each.call e.data-transfer.files, (file) ~>
@browser.upload file, @folder
return false
# データ取得
data = e.data-transfer.get-data 'text'
if !data?
return false
# パース
obj = JSON.parse data
# (ドライブの)ファイルだったら
if obj.type == \file
file = obj.id
@browser.remove-file file
@api \drive/files/update do
file_id: file
folder_id: @folder.id
.then ~>
# something
.catch (err, text-status) ~>
console.error err
# (ドライブの)フォルダーだったら
else if obj.type == \folder
folder = obj.id
# 移動先が自分自身ならreject
if folder == @folder.id
return false
@browser.remove-folder folder
@api \drive/folders/update do
folder_id: folder
parent_id: @folder.id
.then ~>
# something
.catch (err) ~>
if err == 'detected-circular-definition'
@dialog do
'<i class="fa fa-exclamation-triangle"></i>操作を完了できません'
'移動先のフォルダーは、移動するフォルダーのサブフォルダーです。'
[
text: \OK
]
return false
@ondragstart = (e) ~>
e.data-transfer.effect-allowed = \move
e.data-transfer.set-data 'text' JSON.stringify do
type: \folder
id: @folder.id
@is-dragging = true
# 親ブラウザに対して、ドラッグが開始されたフラグを立てる
# (=あなたの子供が、ドラッグを開始しましたよ)
@browser.is-drag-source = true
@ondragend = (e) ~>
@is-dragging = false
@browser.is-drag-source = false
@oncontextmenu = (e) ~>
e.prevent-default!
e.stop-immediate-propagation!
@is-contextmenu-showing = true
@update!
ctx = document.body.append-child document.create-element \mk-drive-browser-folder-contextmenu
ctx = riot.mount ctx, do
browser: @browser
folder: @folder
ctx = ctx.0
ctx.open do
x: e.page-x - window.page-x-offset
y: e.page-y - window.page-y-offset
ctx.on \closed ~>
@is-contextmenu-showing = false
@update!
return false

View File

@@ -0,0 +1,96 @@
mk-drive-browser-nav-folder(data-draghover={ draghover }, onclick={ onclick }, ondragover={ ondragover }, ondragenter={ ondragenter }, ondragleave={ ondragleave }, ondrop={ ondrop })
i.fa.fa-cloud(if={ folder == null })
span { folder == null ? 'ドライブ' : folder.name }
style.
&[data-draghover]
background #eee
script.
@mixin \api
# Riotのバグでnullを渡しても""になる
# https://github.com/riot/riot/issues/2080
#@folder = @opts.folder
@folder = if @opts.folder? and @opts.folder != '' then @opts.folder else null
@browser = @parent
@hover = false
@onclick = ~>
@browser.move @folder
@onmouseover = ~>
@hover = true
@onmouseout = ~>
@hover = false
@ondragover = (e) ~>
e.prevent-default!
e.stop-propagation!
# このフォルダがルートかつカレントディレクトリならドロップ禁止
if @folder == null and @browser.folder == null
e.data-transfer.drop-effect = \none
# ドラッグされてきたものがファイルだったら
else if e.data-transfer.effect-allowed == \all
e.data-transfer.drop-effect = \copy
else
e.data-transfer.drop-effect = \move
return false
@ondragenter = ~>
if @folder != null or @browser.folder != null
@draghover = true
@ondragleave = ~>
if @folder != null or @browser.folder != null
@draghover = false
@ondrop = (e) ~>
e.stop-propagation!
@draghover = false
# ファイルだったら
if e.data-transfer.files.length > 0
Array.prototype.for-each.call e.data-transfer.files, (file) ~>
@browser.upload file, @folder
return false
# データ取得
data = e.data-transfer.get-data 'text'
if !data?
return false
# パース
obj = JSON.parse data
# (ドライブの)ファイルだったら
if obj.type == \file
file = obj.id
@browser.remove-file file
@api \drive/files/update do
file_id: file
folder_id: if @folder? then @folder.id else null
.then ~>
# something
.catch (err, text-status) ~>
console.error err
# (ドライブの)フォルダーだったら
else if obj.type == \folder
folder = obj.id
# 移動先が自分自身ならreject
if @folder? and folder == @folder.id
return false
@browser.remove-folder folder
@api \drive/folders/update do
folder_id: folder
parent_id: if @folder? then @folder.id else null
.then ~>
# something
.catch (err, text-status) ~>
console.error err
return false

View File

@@ -0,0 +1,34 @@
mk-ellipsis-icon
div
div
div
style.
display block
width 70px
margin 0 auto
text-align center
> div
display inline-block
width 18px
height 18px
background-color rgba(0, 0, 0, 0.3)
border-radius 100%
animation bounce 1.4s infinite ease-in-out both
&:nth-child(1)
animation-delay 0s
&:nth-child(2)
margin 0 6px
animation-delay 0.16s
&:nth-child(3)
animation-delay 0.32s
@keyframes bounce
0%, 80%, 100%
transform scale(0)
40%
transform scale(1)

View File

@@ -0,0 +1,127 @@
mk-follow-button
button(if={ !init }, class={ wait: wait, follow: !user.is_following, unfollow: user.is_following },
onclick={ onclick },
disabled={ wait },
title={ user.is_following ? 'フォロー解除' : 'フォローする' })
i.fa.fa-minus(if={ !wait && user.is_following })
i.fa.fa-plus(if={ !wait && !user.is_following })
i.fa.fa-spinner.fa-pulse.fa-fw(if={ wait })
div.init(if={ init }): i.fa.fa-spinner.fa-pulse.fa-fw
style.
display block
> button
> .init
display block
cursor pointer
padding 0
margin 0
width 32px
height 32px
font-size 1em
outline none
border-radius 4px
*
pointer-events 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
&.follow
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
&.unfollow
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
&.wait
cursor wait !important
opacity 0.7
script.
@mixin \api
@mixin \is-promise
@mixin \stream
@user = null
@user-promise = if @is-promise @opts.user then @opts.user else Promise.resolve @opts.user
@init = true
@wait = false
@on \mount ~>
@user-promise.then (user) ~>
@user = user
@init = false
@update!
@stream.on \follow @on-stream-follow
@stream.on \unfollow @on-stream-unfollow
@on \unmount ~>
@stream.off \follow @on-stream-follow
@stream.off \unfollow @on-stream-unfollow
@on-stream-follow = (user) ~>
if user.id == @user.id
@user = user
@update!
@on-stream-unfollow = (user) ~>
if user.id == @user.id
@user = user
@update!
@onclick = ~>
@wait = true
if @user.is_following
@api \following/delete do
user_id: @user.id
.then ~>
@user.is_following = false
.catch (err) ->
console.error err
.then ~>
@wait = false
@update!
else
@api \following/create do
user_id: @user.id
.then ~>
@user.is_following = true
.catch (err) ->
console.error err
.then ~>
@wait = false
@update!

View File

@@ -0,0 +1,163 @@
mk-following-setuper
p.title 気になるユーザーをフォロー:
div.users(if={ !loading && users.length > 0 })
div.user(each={ users })
a.avatar-anchor(href={ CONFIG.url + '/' + username })
img.avatar(src={ avatar_url + '?thumbnail&size=42' }, alt='', data-user-preview={ id })
div.body
a.name(href={ CONFIG.url + '/' + username }, target='_blank', data-user-preview={ id }) { name }
p.username @{ username }
mk-follow-button(user={ this })
p.empty(if={ !loading && users.length == 0 })
| おすすめのユーザーは見つかりませんでした。
p.loading(if={ loading })
i.fa.fa-spinner.fa-pulse.fa-fw
| 読み込んでいます
mk-ellipsis
a.refresh(onclick={ refresh }) もっと見る
button.close(onclick={ close }, title='閉じる'): i.fa.fa-times
style.
display block
padding 24px
background #fff
> .title
margin 0 0 12px 0
font-size 1em
font-weight bold
color #888
> .users
&:after
content ""
display block
clear both
> .user
padding 16px
width 238px
float left
&:after
content ""
display block
clear both
> .avatar-anchor
display block
float left
margin 0 12px 0 0
> .avatar
display block
width 42px
height 42px
margin 0
border-radius 8px
vertical-align bottom
> .body
float left
width calc(100% - 54px)
> .name
margin 0
font-size 16px
line-height 24px
color #555
> .username
margin 0
font-size 15px
line-height 16px
color #ccc
> mk-follow-button
position absolute
top 16px
right 16px
> .empty
margin 0
padding 16px
text-align center
color #aaa
> .loading
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
> .refresh
display block
margin 0 8px 0 0
text-align right
font-size 0.9em
color #999
> .close
cursor pointer
display block
position absolute
top 6px
right 6px
z-index 1
margin 0
padding 0
font-size 1.2em
color #999
border none
outline none
background transparent
&:hover
color #555
&:active
color #222
> i
padding 14px
script.
@mixin \api
@mixin \user-preview
@users = null
@loading = true
@limit = 6users
@page = 0
@on \mount ~>
@load!
@load = ~>
@loading = true
@users = null
@update!
@api \users/recommendation do
limit: @limit
offset: @limit * @page
.then (users) ~>
@loading = false
@users = users
@update!
.catch (err, text-status) ->
console.error err
@refresh = ~>
if @users.length < @limit
@page = 0
else
@page++
@load!
@close = ~>
@unmount!

View File

@@ -0,0 +1,15 @@
mk-go-top
button.hidden(title='一番上へ')
i.fa.fa-angle-up
script.
window.add-event-listener \load @on-scroll
window.add-event-listener \scroll @on-scroll
window.add-event-listener \resize @on-scroll
@on-scroll = ~>
if $ window .scroll-top! > 500px
@remove-class \hidden
else
@add-class \hidden

View File

@@ -0,0 +1,75 @@
mk-broadcast-home-widget
div.icon
svg(height='32', version='1.1', viewBox='0 0 32 32', width='32')
path.tower(d='M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z')
path.wave.a(d='M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z')
path.wave.b(d='M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z')
path.wave.c(d='M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z')
path.wave.d(d='M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z')
h1 開発者募集中!
p: a(href='https://github.com/syuilo/misskey', target='_blank') Misskeyはオープンソースで開発されています。Webのリポジトリはこちら
style.
display block
padding 10px 10px 10px 50px
background transparent
border-color #4078c0 !important
&:after
content ""
display block
clear both
> .icon
display block
float left
margin-left -40px
> svg
fill currentColor
color #4078c0
> .wave
opacity 1
&.a
animation wave 20s ease-in-out 2.1s infinite
&.b
animation wave 20s ease-in-out 2s infinite
&.c
animation wave 20s ease-in-out 2s infinite
&.d
animation wave 20s ease-in-out 2.1s infinite
@keyframes wave
0%
opacity 1
1.5%
opacity 0
3.5%
opacity 0
5%
opacity 1
6.5%
opacity 0
8.5%
opacity 0
10%
opacity 1
> h1
margin 0
font-size 0.95em
font-weight normal
color #4078c0
> p
display block
z-index 1
margin 0
font-size 0.7em
color #555
a
color #555

View File

@@ -0,0 +1,147 @@
mk-calendar-home-widget(data-special={ special })
div.calendar(data-is-holiday={ is-holiday })
p.month-and-year
span.year { year }年
span.month { month }月
p.day { day }日
p.week-day { week-day }曜日
div.info
div
p
| 今日:
b { day-p.to-fixed(1) }%
div.meter
div.val(style={ 'width:' + day-p + '%' })
div
p
| 今月:
b { month-p.to-fixed(1) }%
div.meter
div.val(style={ 'width:' + month-p + '%' })
div
p
| 今年:
b { year-p.to-fixed(1) }%
div.meter
div.val(style={ 'width:' + year-p + '%' })
style.
display block
padding 16px 0
color #777
background #fff
&[data-special='on-new-years-day']
border-color #ef95a0 !important
&:after
content ""
display block
clear both
> .calendar
float left
width 60%
text-align center
&[data-is-holiday]
> .day
color #ef95a0
> p
margin 0
line-height 18px
font-size 14px
> span
margin 0 4px
> .day
margin 10px 0
line-height 32px
font-size 28px
> .info
display block
float left
width 40%
padding 0 16px 0 0
> div
margin-bottom 8px
&:last-child
margin-bottom 4px
> p
margin 0 0 2px 0
font-size 12px
line-height 18px
color #888
> b
margin-left 2px
> .meter
width 100%
overflow hidden
background #eee
border-radius 8px
> .val
height 4px
background $theme-color
&:nth-child(1)
> .meter > .val
background #f7796c
&:nth-child(2)
> .meter > .val
background #a1de41
&:nth-child(3)
> .meter > .val
background #41ddde
script.
@draw = ~>
now = new Date!
nd = now.get-date!
nm = now.get-month!
ny = now.get-full-year!
@year = ny
@month = nm + 1
@day = nd
@week-day = [\日 \月 \火 \水 \木 \金 \土][now.get-day!]
@day-numer = (now - (new Date ny, nm, nd))
@day-denom = 1000ms * 60s * 60m * 24h
@month-numer = (now - (new Date ny, nm, 1))
@month-denom = (new Date ny, nm + 1, 1) - (new Date ny, nm, 1)
@year-numer = (now - (new Date ny, 0, 0))
@year-denom = (new Date ny + 1, 0, 0) - (new Date ny, 0, 0)
@day-p = @day-numer / @day-denom * 100
@month-p = @month-numer / @month-denom * 100
@year-p = @year-numer / @year-denom * 100
@is-holiday =
(now.get-day! == 0 or now.get-day! == 6)
@special =
| nm == 0 and nd == 1 => \on-new-years-day
| _ => false
@update!
@draw!
@on \mount ~>
@clock = set-interval @draw, 1000ms
@on \unmount ~>
clear-interval @clock

View File

@@ -0,0 +1,37 @@
mk-donation-home-widget
article
h1
i.fa.fa-heart
| 寄付のお願い
p
| Misskeyの運営にはドメイン、サーバー等のコストが掛かります。
| Misskeyは広告を掲載したりしないため、 収入を皆様からの寄付に頼っています。
| もしご興味があれば、
a(href='/syuilo', data-user-preview='@syuilo') @syuilo
| までご連絡ください。ご協力ありがとうございます。
style.
display block
background #fff
border-color #ead8bb !important
> article
padding 20px
> h1
margin 0 0 5px 0
font-size 1em
color #888
> i
margin-right 0.25em
> p
display block
z-index 1
margin 0
font-size 0.8em
color #999
script.
@mixin \user-preview

View File

@@ -0,0 +1,117 @@
mk-mentions-home-widget
header
span(data-is-active={ mode == 'all' }, onclick={ set-mode.bind(this, 'all') }) すべて
span(data-is-active={ mode == 'following' }, onclick={ set-mode.bind(this, 'following') }) フォロー中
div.loading(if={ is-loading })
mk-ellipsis-icon
p.empty(if={ is-empty })
i.fa.fa-comments-o
span(if={ mode == 'all' }) あなた宛ての投稿はありません。
span(if={ mode == 'following' }) あなたがフォローしているユーザーからの言及はありません。
mk-timeline@timeline
<yield to="footer">
i.fa.fa-moon-o(if={ !parent.more-loading })
i.fa.fa-spinner.fa-pulse.fa-fw(if={ parent.more-loading })
</yield>
style.
display block
background #fff
> header
padding 8px 16px
border-bottom solid 1px #eee
> span
margin-right 16px
line-height 27px
font-size 18px
color #555
&:not([data-is-active])
color $theme-color
cursor pointer
&:hover
text-decoration underline
> .loading
padding 64px 0
> .empty
display block
margin 0 auto
padding 32px
max-width 400px
text-align center
color #999
> i
display block
margin-bottom 16px
font-size 3em
color #ccc
script.
@mixin \i
@mixin \api
@is-loading = true
@is-empty = false
@more-loading = false
@mode = \all
@on \mount ~>
document.add-event-listener \keydown @on-document-keydown
window.add-event-listener \scroll @on-scroll
@fetch ~>
@trigger \loaded
@on \unmount ~>
document.remove-event-listener \keydown @on-document-keydown
window.remove-event-listener \scroll @on-scroll
@on-document-keydown = (e) ~>
tag = e.target.tag-name.to-lower-case!
if tag != \input and tag != \textarea
if e.which == 84 # t
@refs.timeline.focus!
@fetch = (cb) ~>
@api \posts/mentions do
following: @mode == \following
.then (posts) ~>
@is-loading = false
@is-empty = posts.length == 0
@update!
@refs.timeline.set-posts posts
if cb? then cb!
.catch (err) ~>
console.error err
if cb? then cb!
@more = ~>
if @more-loading or @is-loading or @refs.timeline.posts.length == 0
return
@more-loading = true
@update!
@api \posts/mentions do
following: @mode == \following
max_id: @refs.timeline.tail!.id
.then (posts) ~>
@more-loading = false
@update!
@refs.timeline.prepend-posts posts
.catch (err) ~>
console.error err
@on-scroll = ~>
current = window.scroll-y + window.inner-height
if current > document.body.offset-height - 8
@more!
@set-mode = (mode) ~>
@update do
mode: mode
@fetch!

View File

@@ -0,0 +1,23 @@
mk-nav-home-widget
a(href={ CONFIG.urls.about }) Misskeyについて
i ・
a(href={ CONFIG.urls.about + '/status' }) ステータス
i ・
a(href='https://github.com/syuilo/misskey') リポジトリ
i ・
a(href={ CONFIG.urls.dev }) 開発者
i ・
a(href='https://twitter.com/misskey_xyz', target='_blank') Follow us on <i class="fa fa-twitter"></i>
style.
display block
padding 16px
font-size 12px
color #aaa
background #fff
a
color #999
i
color #ccc

View File

@@ -0,0 +1,49 @@
mk-notifications-home-widget
p.title
i.fa.fa-bell-o
| 通知
button(onclick={ settings }, title='通知の設定'): i.fa.fa-cog
mk-notifications
style.
display block
background #fff
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> i
margin-right 4px
> button
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
> mk-notifications
max-height 300px
overflow auto
script.
@settings = ~>
w = riot.mount document.body.append-child document.create-element \mk-settings-window .0
w.switch \notification

View File

@@ -0,0 +1,86 @@
mk-photo-stream-home-widget
p.title
i.fa.fa-camera
| フォトストリーム
p.initializing(if={ initializing })
i.fa.fa-spinner.fa-pulse.fa-fw
| 読み込んでいます
mk-ellipsis
div.stream(if={ !initializing && images.length > 0 })
virtual(each={ image in images })
div.img(style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' })
p.empty(if={ !initializing && images.length == 0 })
| 写真はありません
style.
display block
background #fff
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> i
margin-right 4px
> .stream
display -webkit-flex
display -moz-flex
display -ms-flex
display flex
justify-content center
flex-wrap wrap
padding 8px
> .img
flex 1 1 33%
width 33%
height 80px
background-position center center
background-size cover
background-clip content-box
border solid 2px transparent
> .initializing
> .empty
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
script.
@mixin \api
@mixin \stream
@images = []
@initializing = true
@on \mount ~>
@stream.on \drive_file_created @on-stream-drive-file-created
@api \drive/stream do
type: 'image/*'
limit: 9images
.then (images) ~>
@initializing = false
@images = images
@update!
@on \unmount ~>
@stream.off \drive_file_created @on-stream-drive-file-created
@on-stream-drive-file-created = (file) ~>
if /^image\/.+$/.test file.type
@images.unshift file
if @images.length > 9
@images.pop!
@update!

View File

@@ -0,0 +1,55 @@
mk-profile-home-widget
div.banner(style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' }, onclick={ set-banner })
img.avatar(src={ I.avatar_url + '?thumbnail&size=64' }, onclick={ set-avatar }, alt='avatar', data-user-preview={ I.id })
a.name(href={ CONFIG.url + '/' + I.username }) { I.name }
p.username @{ I.username }
style.
display block
background #fff
> .banner
height 100px
background-color #f5f5f5
background-size cover
background-position center
> .avatar
display block
position absolute
top 76px
left 16px
width 58px
height 58px
margin 0
border solid 3px #fff
border-radius 8px
vertical-align bottom
> .name
display block
margin 10px 0 0 92px
line-height 16px
font-weight bold
color #555
> .username
display block
margin 4px 0 8px 92px
line-height 16px
font-size 0.9em
color #999
script.
@mixin \i
@mixin \user-preview
@mixin \update-avatar
@mixin \update-banner
@set-avatar = ~>
@update-avatar @I, (i) ~>
@update-i i
@set-banner = ~>
@update-banner @I, (i) ~>
@update-i i

View File

@@ -0,0 +1,94 @@
mk-rss-reader-home-widget
p.title
i.fa.fa-rss-square
| RSS
button(onclick={ settings }, title='設定'): i.fa.fa-cog
div.feed(if={ !initializing })
virtual(each={ item in items })
a(href={ item.link }, target='_blank') { item.title }
p.initializing(if={ initializing })
i.fa.fa-spinner.fa-pulse.fa-fw
| 読み込んでいます
mk-ellipsis
style.
display block
background #fff
> .title
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> i
margin-right 4px
> button
position absolute
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
> .feed
padding 12px 16px
font-size 0.9em
> a
display block
padding 4px 0
color #666
border-bottom dashed 1px #eee
&:last-child
border-bottom none
> .initializing
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
script.
@mixin \api
@mixin \NotImplementedException
@url = 'http://news.yahoo.co.jp/pickup/rss.xml'
@items = []
@initializing = true
@on \mount ~>
@fetch!
@clock = set-interval @fetch, 60000ms
@on \unmount ~>
clear-interval @clock
@fetch = ~>
@api CONFIG.url + '/api:rss' do
url: @url
.then (feed) ~>
@items = feed.rss.channel.item
@initializing = false
@update!
.catch (err) ->
console.error err
@settings = ~>
@NotImplementedException!

View File

@@ -0,0 +1,113 @@
mk-timeline-home-widget
mk-following-setuper(if={ no-following })
div.loading(if={ is-loading })
mk-ellipsis-icon
p.empty(if={ is-empty })
i.fa.fa-comments-o
| 自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。
mk-timeline@timeline
<yield to="footer">
i.fa.fa-moon-o(if={ !parent.more-loading })
i.fa.fa-spinner.fa-pulse.fa-fw(if={ parent.more-loading })
</yield>
style.
display block
background #fff
> mk-following-setuper
border-bottom solid 1px #eee
> .loading
padding 64px 0
> .empty
display block
margin 0 auto
padding 32px
max-width 400px
text-align center
color #999
> i
display block
margin-bottom 16px
font-size 3em
color #ccc
script.
@mixin \i
@mixin \api
@mixin \stream
@is-loading = true
@is-empty = false
@more-loading = false
@no-following = @I.following_count == 0
@on \mount ~>
@stream.on \post @on-stream-post
@stream.on \follow @on-stream-follow
@stream.on \unfollow @on-stream-unfollow
document.add-event-listener \keydown @on-document-keydown
window.add-event-listener \scroll @on-scroll
@load ~>
@trigger \loaded
@on \unmount ~>
@stream.off \post @on-stream-post
@stream.off \follow @on-stream-follow
@stream.off \unfollow @on-stream-unfollow
document.remove-event-listener \keydown @on-document-keydown
window.remove-event-listener \scroll @on-scroll
@on-document-keydown = (e) ~>
tag = e.target.tag-name.to-lower-case!
if tag != \input and tag != \textarea
if e.which == 84 # t
@refs.timeline.focus!
@load = (cb) ~>
@api \posts/timeline
.then (posts) ~>
@is-loading = false
@is-empty = posts.length == 0
@update!
@refs.timeline.set-posts posts
if cb? then cb!
.catch (err) ~>
console.error err
if cb? then cb!
@more = ~>
if @more-loading or @is-loading or @refs.timeline.posts.length == 0
return
@more-loading = true
@update!
@api \posts/timeline do
max_id: @refs.timeline.tail!.id
.then (posts) ~>
@more-loading = false
@update!
@refs.timeline.prepend-posts posts
.catch (err) ~>
console.error err
@on-stream-post = (post) ~>
@is-empty = false
@update!
@refs.timeline.add-post post
@on-stream-follow = ~>
@load!
@on-stream-unfollow = ~>
@load!
@on-scroll = ~>
current = window.scroll-y + window.inner-height
if current > document.body.offset-height - 8
@more!

View File

@@ -0,0 +1,70 @@
mk-tips-home-widget
p@tip
i.fa.fa-lightbulb-o
span@text
style.
display block
background transparent !important
border none !important
overflow visible !important
> p
display block
margin 0
padding 0 12px
text-align center
font-size 0.7em
color #999
> i
margin-right 4px
kbd
display inline
padding 0 6px
margin 0 2px
font-size 1em
font-family inherit
border solid 1px #999
border-radius 2px
script.
@tips = [
'<kbd>t</kbd>でタイムラインにフォーカスできます'
'<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます'
'投稿フォームにはファイルをドラッグ&ドロップできます'
'投稿フォームにクリップボードにある画像データをペーストできます'
'ドライブにファイルをドラッグ&ドロップしてアップロードできます'
'ドライブでファイルをドラッグしてフォルダ移動できます'
'ドライブでフォルダをドラッグしてフォルダ移動できます'
'ホームをカスタマイズできます(準備中)'
'MisskeyはMIT Licenseです'
]
@on \mount ~>
@set!
@clock = set-interval @change, 20000ms
@on \unmount ~>
clear-interval @clock
@set = ~>
@refs.text.innerHTML = @tips[Math.floor Math.random! * @tips.length]
@update!
@change = ~>
Velocity @refs.tip, {
opacity: 0
} {
duration: 500ms
easing: \linear
complete: @set
}
Velocity @refs.tip, {
opacity: 1
} {
duration: 500ms
easing: \linear
}

View File

@@ -0,0 +1,154 @@
mk-user-recommendation-home-widget
p.title
i.fa.fa-users
| おすすめユーザー
button(onclick={ refresh }, title='他を見る'): i.fa.fa-refresh
div.user(if={ !loading && users.length != 0 }, each={ _user in users })
a.avatar-anchor(href={ CONFIG.url + '/' + _user.username })
img.avatar(src={ _user.avatar_url + '?thumbnail&size=42' }, alt='', data-user-preview={ _user.id })
div.body
a.name(href={ CONFIG.url + '/' + _user.username }, data-user-preview={ _user.id }) { _user.name }
p.username @{ _user.username }
mk-follow-button(user={ _user })
p.empty(if={ !loading && users.length == 0 })
| いません!
p.loading(if={ loading })
i.fa.fa-spinner.fa-pulse.fa-fw
| 読み込んでいます
mk-ellipsis
style.
display block
background #fff
> .title
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
border-bottom solid 1px #eee
> i
margin-right 4px
> button
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
> .user
padding 16px
border-bottom solid 1px #eee
&:last-child
border-bottom none
&:after
content ""
display block
clear both
> .avatar-anchor
display block
float left
margin 0 12px 0 0
> .avatar
display block
width 42px
height 42px
margin 0
border-radius 8px
vertical-align bottom
> .body
float left
width calc(100% - 54px)
> .name
margin 0
font-size 16px
line-height 24px
color #555
> .username
display block
margin 0
font-size 15px
line-height 16px
color #ccc
> mk-follow-button
position absolute
top 16px
right 16px
> .empty
margin 0
padding 16px
text-align center
color #aaa
> .loading
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
script.
@mixin \api
@mixin \user-preview
@users = null
@loading = true
@limit = 3users
@page = 0
@on \mount ~>
@fetch!
@clock = set-interval ~>
if @users.length < @limit
@fetch true
, 60000ms
@on \unmount ~>
clear-interval @clock
@fetch = (quiet = false) ~>
@loading = true
@users = null
if not quiet then @update!
@api \users/recommendation do
limit: @limit
offset: @limit * @page
.then (users) ~>
@loading = false
@users = users
@update!
.catch (err, text-status) ->
console.error err
@refresh = ~>
if @users.length < @limit
@page = 0
else
@page++
@fetch!

View File

@@ -0,0 +1,86 @@
mk-home
div.main
div.left@left
main
mk-timeline-home-widget@tl(if={ mode == 'timeline' })
mk-mentions-home-widget@tl(if={ mode == 'mentions' })
div.right@right
mk-detect-slow-internet-connection-notice
style.
display block
> .main
margin 0 auto
max-width 1200px
&:after
content ""
display block
clear both
> *
float left
> *
display block
//border solid 1px #eaeaea
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
overflow hidden
&:not(:last-child)
margin-bottom 16px
> main
padding 16px
width calc(100% - 275px * 2)
> *:not(main)
width 275px
> .left
padding 16px 0 16px 16px
> .right
padding 16px 16px 16px 0
@media (max-width 1100px)
> *:not(main)
display none
> main
float none
width 100%
max-width 700px
margin 0 auto
script.
@mixin \i
@mode = @opts.mode || \timeline
# https://github.com/riot/riot/issues/2080
if @mode == '' then @mode = \timeline
@home = []
@on \mount ~>
@refs.tl.on \loaded ~>
@trigger \loaded
@I.data.home.for-each (widget) ~>
try
el = document.create-element \mk- + widget.name + \-home-widget
switch widget.place
| \left => @refs.left.append-child el
| \right => @refs.right.append-child el
@home.push (riot.mount el, do
id: widget.id
data: widget.data
.0)
catch e
# noop
@on \unmount ~>
@home.for-each (widget) ~>
widget.unmount!

View File

@@ -0,0 +1,73 @@
mk-image-dialog
div.bg@bg(onclick={ close })
img@img(src={ image.url }, alt={ image.name }, title={ image.name }, onclick={ close })
style.
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
script.
@image = @opts.image
@on \mount ~>
Velocity @root, {
opacity: 1
} {
duration: 100ms
easing: \linear
}
#Velocity @img, {
# scale: 1
# opacity: 1
#} {
# duration: 200ms
# easing: \ease-out
#}
@close = ~>
Velocity @root, {
opacity: 0
} {
duration: 100ms
easing: \linear
complete: ~> @unmount!
}
#Velocity @img, {
# scale: 0.9
# opacity: 0
#} {
# duration: 200ms
# easing: \ease-in
# complete: ~>
# @unmount!
#}

View File

@@ -0,0 +1,43 @@
mk-images-viewer
div.image@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 })
style.
display block
padding 8px
overflow hidden
box-shadow 0 0 4px rgba(0, 0, 0, 0.2)
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
script.
@images = @opts.images
@image = @images.0
@mousemove = (e) ~>
rect = @refs.view.get-bounding-client-rect!
mouse-x = e.client-x - rect.left
mouse-y = e.client-y - rect.top
xp = mouse-x / @refs.view.offset-width * 100
yp = mouse-y / @refs.view.offset-height * 100
@refs.view.style.background-position = xp + '% ' + yp + '%'
@click = ~>
dialog = document.body.append-child document.create-element \mk-image-dialog
riot.mount dialog, do
image: @image

View File

@@ -0,0 +1,156 @@
mk-input-dialog
mk-window@window(is-modal={ true }, width={ '500px' })
<yield to="header">
i.fa.fa-i-cursor
| { parent.title }
</yield>
<yield to="content">
div.body
input@text(oninput={ parent.update }, onkeydown={ parent.on-keydown }, placeholder={ parent.placeholder })
div.action
button.cancel(onclick={ parent.cancel }) キャンセル
button.ok(disabled={ !parent.allow-empty && refs.text.value.length == 0 }, onclick={ parent.ok }) 決定
</yield>
style.
display block
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
> .body
padding 16px
> input
display block
padding 8px
margin 0
width 100%
max-width 100%
min-width 100%
font-size 1em
color #333
background #fff
outline none
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
transition border-color .3s ease
&:hover
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
color $theme-color
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&::-webkit-input-placeholder
color rgba($theme-color, 0.3)
> .action
height 72px
background lighten($theme-color, 95%)
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
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
.ok
right 16px
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
.cancel
right 148px
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
script.
@done = false
@title = @opts.title
@placeholder = @opts.placeholder
@default = @opts.default
@allow-empty = if @opts.allow-empty? then @opts.allow-empty else true
@on \mount ~>
@text = @refs.window.refs.text
if @default?
@text.value = @default
@text.focus!
@refs.window.on \closing ~>
if @done
@opts.on-ok @text.value
else
if @opts.on-cancel?
@opts.on-cancel!
@refs.window.on \closed ~>
@unmount!
@cancel = ~>
@done = false
@refs.window.close!
@ok = ~>
if not @allow-empty and @text.value == '' then return
@done = true
@refs.window.close!
@on-keydown = (e) ~>
if e.which == 13 # Enter
e.prevent-default!
e.stop-propagation!
@ok!

View File

@@ -0,0 +1,100 @@
mk-list-user
a.avatar-anchor(href={ CONFIG.url + '/' + user.username })
img.avatar(src={ user.avatar_url + '?thumbnail&size=64' }, alt='avatar')
div.main
header
div.left
a.name(href={ CONFIG.url + '/' + user.username })
| { user.name }
span.username
| @{ user.username }
div.body
p.followed(if={ user.is_followed }) フォローされています
div.bio { user.bio }
mk-follow-button(user={ user })
style.
display block
margin 0
padding 16px
font-size 16px
&:after
content ""
display block
clear both
> .avatar-anchor
display block
float left
margin 0 16px 0 0
> .avatar
display block
width 58px
height 58px
margin 0
border-radius 8px
vertical-align bottom
> .main
float left
width calc(100% - 74px)
> header
margin-bottom 2px
white-space nowrap
&:after
content ""
display block
clear both
> .left
float left
> .name
display inline
margin 0
padding 0
color #777
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
text-align left
margin 0 0 0 8px
color #ccc
> .body
> .followed
display inline-block
margin 0 0 4px 0
padding 2px 8px
vertical-align top
font-size 10px
color #71afc7
background #eefaff
border-radius 4px
> .bio
cursor default
display block
margin 0
padding 0
word-wrap break-word
font-size 1.1em
color #717171
> mk-follow-button
position absolute
top 16px
right 16px
script.
@user = @opts.user

View File

@@ -0,0 +1,20 @@
mk-log-window
mk-window@window(width={ '600px' }, height={ '400px' })
<yield to="header">
i.fa.fa-terminal
| Log
</yield>
<yield to="content">
mk-log
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
script.
@on \mount ~>
@refs.window.on \closed ~>
@unmount!

View File

@@ -0,0 +1,62 @@
mk-log
header
button.follow(class={ following: following }, onclick={ follow }) Follow
div.logs@logs
code(each={ logs })
span.date { date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() }
span.message { message }
style.
display block
height 100%
color #fff
background #000
> header
height 32px
background #343a42
> button
line-height 32px
> .follow
position absolute
top 0
right 0
&.following
color #ff0
> .logs
height calc(100% - 32px)
overflow auto
> code
display block
padding 4px 8px
&:hover
background rgba(#fff, 0.15)
> .date
margin-right 8px
opacity 0.5
script.
@mixin \log
@following = true
@on \mount ~>
@log-event.on \log @on-log
@on \unmount ~>
@log-event.off \log @on-log
@follow = ~>
@following = true
@on-log = ~>
@update!
if @following
@refs.logs.scroll-top = @refs.logs.scroll-height

View File

@@ -0,0 +1,162 @@
mk-messaging-form
textarea@text(onkeypress={ onkeypress }, onpaste={ onpaste }, placeholder='ここにメッセージを入力')
div.files
mk-uploader@uploader
button.send(onclick={ send }, disabled={ sending }, title='メッセージを送信')
i.fa.fa-paper-plane(if={ !sending })
i.fa.fa-spinner.fa-spin(if={ sending })
button.attach-from-local(type='button', title='PCから画像を添付する')
i.fa.fa-upload
button.attach-from-drive(type='button', title='アルバムから画像を添付する')
i.fa.fa-folder-open
input(name='file', type='file', accept='image/*')
style.
display block
> textarea
cursor auto
display block
width 100%
min-width 100%
max-width 100%
height 64px
margin 0
padding 8px
font-size 1em
color #000
outline none
border none
border-top solid 1px #eee
border-radius 0
box-shadow none
background transparent
> .send
position absolute
bottom 0
right 0
margin 0
padding 10px 14px
line-height 1em
font-size 1em
color #aaa
transition color 0.1s ease
&:hover
color $theme-color
&:active
color darken($theme-color, 10%)
transition color 0s ease
.files
display block
margin 0
padding 0 8px
list-style none
&:after
content ''
display block
clear both
> li
display block
float left
margin 4px
padding 0
width 64px
height 64px
background-color #eee
background-repeat no-repeat
background-position center center
background-size cover
cursor move
&:hover
> .remove
display block
> .remove
display none
position absolute
right -6px
top -6px
margin 0
padding 0
background transparent
outline none
border none
border-radius 0
box-shadow none
cursor pointer
.attach-from-local
.attach-from-drive
margin 0
padding 10px 14px
line-height 1em
font-size 1em
font-weight normal
text-decoration none
color #aaa
transition color 0.1s ease
&:hover
color $theme-color
&:active
color darken($theme-color, 10%)
transition color 0s ease
input[type=file]
display none
script.
@mixin \api
@user = @opts.user
@onpaste = (e) ~>
data = e.clipboard-data
items = data.items
for i from 0 to items.length - 1
item = items[i]
switch (item.kind)
| \file =>
@upload item.get-as-file!
@onkeypress = (e) ~>
if (e.which == 10 || e.which == 13) && e.ctrl-key
@send!
@select-file = ~>
@refs.file.click!
@select-file-from-drive = ~>
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
event = riot.observable!
riot.mount browser, do
multiple: true
event: event
event.one \selected (files) ~>
files.for-each @add-file
@send = ~>
@sending = true
@api \messaging/messages/create do
user_id: @user.id
text: @refs.text.value
.then (message) ~>
@clear!
.catch (err) ~>
console.error err
.then ~>
@sending = false
@update!
@clear = ~>
@refs.text.value = ''
@files = []
@update!

View File

@@ -0,0 +1,302 @@
mk-messaging
div.search
div.form
label(for='search-input')
i.fa.fa-search
input@search-input(type='search', oninput={ search }, placeholder='ユーザーを探す')
div.result
ol.users(if={ search-result.length > 0 })
li(each={ user in search-result })
a(onclick={ user._click })
img.avatar(src={ user.avatar_url + '?thumbnail&size=32' }, alt='')
span.name { user.name }
span.username @{ user.username }
div.main
div.history(if={ history.length > 0 })
virtual(each={ history })
a.user(data-is-me={ is_me }, data-is-read={ is_read }, onclick={ _click }): div
img.avatar(src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' }, alt='')
header
span.name { is_me ? recipient.name : user.name }
span.username { '@' + (is_me ? recipient.username : user.username ) }
mk-time(time={ created_at })
div.body
p.text
span.me(if={ is_me }) あなた:
| { text }
p.no-history(if={ history.length == 0 })
| 履歴はありません。
br
| ユーザーを検索して、いつでもメッセージを送受信できます。
style.
display block
> .search
display block
position absolute
top 0
left 0
z-index 1
width 100%
background #fff
box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
> .form
padding 8px
background #f7f7f7
> label
display block
position absolute
top 0
left 8px
z-index 1
height 100%
width 38px
pointer-events none
> i
display block
position absolute
top 0
right 0
bottom 0
left 0
width 1em
height 1em
margin auto
color #555
> input
margin 0
padding 0 12px 0 38px
width 100%
font-size 1em
line-height 38px
color #000
outline none
border solid 1px #eee
border-radius 5px
box-shadow none
transition color 0.5s ease, border 0.5s ease
&:hover
border solid 1px #ddd
transition border 0.2s ease
&:focus
color darken($theme-color, 20%)
border solid 1px $theme-color
transition color 0, border 0
> .result
display block
top 0
left 0
z-index 2
width 100%
margin 0
padding 0
background #fff
> .users
margin 0
padding 0
list-style none
> li
> a
display inline-block
z-index 1
width 100%
padding 8px 32px
vertical-align top
white-space nowrap
overflow hidden
color rgba(0, 0, 0, 0.8)
text-decoration none
transition none
&:hover
color #fff
background $theme-color
.name
color #fff
.username
color #fff
&:active
color #fff
background darken($theme-color, 10%)
.name
color #fff
.username
color #fff
.avatar
vertical-align middle
min-width 32px
min-height 32px
max-width 32px
max-height 32px
margin 0 8px 0 0
border-radius 6px
.name
margin 0 8px 0 0
/*font-weight bold*/
font-weight normal
color rgba(0, 0, 0, 0.8)
.username
font-weight normal
color rgba(0, 0, 0, 0.3)
> .main
padding-top 56px
> .history
> a
display block
padding 20px 30px
text-decoration none
background #fff
border-bottom solid 1px #eee
*
pointer-events none
user-select none
&:hover
background #fafafa
> .avatar
filter saturate(200%)
&:active
background #eee
&[data-is-read]
&[data-is-me]
opacity 0.8
&:not([data-is-me]):not([data-is-read])
background-image url("/_/resources/desktop/unread.svg")
background-repeat no-repeat
background-position 0 center
&:after
content ""
display block
clear both
> div
max-width 500px
margin 0 auto
> header
margin-bottom 2px
white-space nowrap
overflow hidden
> .name
text-align left
display inline
margin 0
padding 0
font-size 1em
color rgba(0, 0, 0, 0.9)
font-weight bold
transition all 0.1s ease
> .username
text-align left
margin 0 0 0 8px
color rgba(0, 0, 0, 0.5)
> mk-time
position absolute
top 0
right 0
display inline
color rgba(0, 0, 0, 0.5)
font-size small
> .avatar
float left
width 54px
height 54px
margin 0 16px 0 0
border-radius 8px
transition all 0.1s ease
> .body
> .text
display block
margin 0 0 0 0
padding 0
overflow hidden
word-wrap break-word
font-size 1.1em
color rgba(0, 0, 0, 0.8)
.me
color rgba(0, 0, 0, 0.4)
> .image
display block
max-width 100%
max-height 512px
> .no-history
margin 0
padding 2em 1em
text-align center
color #999
font-weight 500
script.
@mixin \i
@mixin \api
@search-result = []
@on \mount ~>
@api \messaging/history
.then (history) ~>
@is-loading = false
history.for-each (message) ~>
message.is_me = message.user_id == @I.id
message._click = ~>
if message.is_me
@trigger \navigate-user message.recipient
else
@trigger \navigate-user message.user
@history = history
@update!
.catch (err) ~>
console.error err
@search = ~>
q = @refs.search-input.value
if q == ''
@search-result = []
else
@api \users/search do
query: q
.then (users) ~>
users.for-each (user) ~>
user._click = ~>
@trigger \navigate-user user
@search-result = []
@search-result = users
@update!
.catch (err) ~>
console.error err

View File

@@ -0,0 +1,227 @@
mk-messaging-message(data-is-me={ message.is_me })
a.avatar-anchor(href={ CONFIG.url + '/' + message.user.username }, title={ message.user.username }, target='_blank')
img.avatar(src={ message.user.avatar_url + '?thumbnail&size=64' }, alt='')
div.content-container
div.balloon
p.read(if={ message.is_me && message.is_read }) 既読
button.delete-button(if={ message.is_me }, title='メッセージを削除')
img(src='/_/resources/desktop/messaging/delete.png', alt='Delete')
div.content(if={ !message.is_deleted })
div@text
div.image(if={ message.file })
img(src={ message.file.url }, alt='image', title={ message.file.name })
div.content(if={ message.is_deleted })
p.is-deleted このメッセージは削除されました
footer
mk-time(time={ message.created_at })
i.fa.fa-pencil.is-edited(if={ message.is_edited })
style.
$me-balloon-color = #23A7B6
display block
padding 10px 12px 10px 12px
background-color transparent
&:after
content ""
display block
clear both
> .avatar-anchor
display block
> .avatar
display block
min-width 54px
min-height 54px
max-width 54px
max-height 54px
margin 0
border-radius 8px
transition all 0.1s ease
> .content-container
display block
margin 0 12px
padding 0
max-width calc(100% - 78px)
> .balloon
display block
float inherit
margin 0
padding 0
max-width 100%
min-height 38px
border-radius 16px
&:before
content ""
pointer-events none
display block
position absolute
top 12px
&:hover
> .delete-button
display block
> .delete-button
display none
position absolute
z-index 1
top -4px
right -4px
margin 0
padding 0
cursor pointer
outline none
border none
border-radius 0
box-shadow none
background transparent
> img
vertical-align bottom
width 16px
height 16px
cursor pointer
> .read
user-select none
display block
position absolute
z-index 1
bottom -4px
left -12px
margin 0
color rgba(0, 0, 0, 0.5)
font-size 11px
> .content
> .is-deleted
display block
margin 0
padding 0
overflow hidden
word-wrap break-word
font-size 1em
color rgba(0, 0, 0, 0.5)
> [ref='text']
display block
margin 0
padding 8px 16px
overflow hidden
word-wrap break-word
font-size 1em
color rgba(0, 0, 0, 0.8)
&, *
user-select text
cursor auto
& + .file
&.image
> img
border-radius 0 0 16px 16px
> .file
&.image
> img
display block
max-width 100%
max-height 512px
border-radius 16px
> footer
display block
clear both
margin 0
padding 2px
font-size 10px
color rgba(0, 0, 0, 0.4)
> .is-edited
margin-left 4px
&:not([data-is-me='true'])
> .avatar-anchor
float left
> .content-container
float left
> .balloon
background #eee
&:before
left -14px
border-top solid 8px transparent
border-right solid 8px #eee
border-bottom solid 8px transparent
border-left solid 8px transparent
> footer
text-align left
&[data-is-me='true']
> .avatar-anchor
float right
> .content-container
float right
> .balloon
background $me-balloon-color
&:before
right -14px
left auto
border-top solid 8px transparent
border-right solid 8px transparent
border-bottom solid 8px transparent
border-left solid 8px $me-balloon-color
> .content
> p.is-deleted
color rgba(255, 255, 255, 0.5)
> [ref='text']
&, *
color #fff !important
> footer
text-align right
&[data-is-deleted='true']
> .content-container
opacity 0.5
script.
@mixin \i
@mixin \text
@message = @opts.message
@message.is_me = @message.user.id == @I.id
@on \mount ~>
if @message.text?
tokens = @analyze @message.text
@refs.text.innerHTML = @compile tokens
@refs.text.children.for-each (e) ~>
if e.tag-name == \MK-URL
riot.mount e
# URLをプレビュー
tokens
.filter (t) -> t.type == \link
.map (t) ~>
@preview = @refs.text.append-child document.create-element \mk-url-preview
riot.mount @preview, do
url: t.content

View File

@@ -0,0 +1,26 @@
mk-messaging-room-window
mk-window@window(is-modal={ false }, width={ '500px' }, height={ '560px' })
<yield to="header">
i.fa.fa-comments
| メッセージ: { parent.user.name }
</yield>
<yield to="content">
mk-messaging-room(user={ parent.user })
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
> mk-messaging-room
height 100%
script.
@user = @opts.user
@on \mount ~>
@refs.window.on \closed ~>
@unmount!

View File

@@ -0,0 +1,227 @@
mk-messaging-room
div.stream@stream
p.initializing(if={ init })
i.fa.fa-spinner.fa-spin
| 読み込み中
p.empty(if={ !init && messages.length == 0 })
i.fa.fa-info-circle
| このユーザーとまだ会話したことがありません
virtual(each={ message, i in messages })
mk-messaging-message(message={ message })
p.date(if={ i != messages.length - 1 && message._date != messages[i + 1]._date })
span { messages[i + 1]._datetext }
div.typings
footer
div@notifications
div.grippie(title='ドラッグしてフォームの広さを調整')
mk-messaging-form(user={ user })
style.
display block
> .stream
position absolute
top 0
left 0
width 100%
height calc(100% - 100px)
overflow auto
> .empty
width 100%
margin 0
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
color rgba(0, 0, 0, 0.4)
i
margin-right 4px
> .no-history
display block
margin 0
padding 16px
text-align center
font-size 0.8em
color rgba(0, 0, 0, 0.4)
i
margin-right 4px
> .message
// something
> .date
display block
margin 8px 0
text-align center
&:before
content ''
display block
position absolute
height 1px
width 90%
top 16px
left 0
right 0
margin 0 auto
background rgba(0, 0, 0, 0.1)
> span
display inline-block
margin 0
padding 0 16px
//font-weight bold
line-height 32px
color rgba(0, 0, 0, 0.3)
background #fff
> footer
position absolute
z-index 2
bottom 0
width 600px
max-width 100%
margin 0 auto
padding 0
background rgba(255, 255, 255, 0.95)
background-clip content-box
> [ref='notifications']
position absolute
top -48px
width 100%
padding 8px 0
text-align center
> p
display inline-block
margin 0
padding 0 12px 0 28px
cursor pointer
line-height 32px
font-size 12px
color $theme-color-foreground
background $theme-color
border-radius 16px
transition opacity 1s ease
> i
position absolute
top 0
left 10px
line-height 32px
font-size 16px
> .grippie
height 10px
margin-top -10px
background transparent
cursor ns-resize
&:hover
//background rgba(0, 0, 0, 0.1)
&:active
//background rgba(0, 0, 0, 0.2)
script.
@mixin \i
@mixin \api
@mixin \messaging-stream
@user = @opts.user
@init = true
@sending = false
@messages = []
@connection = new @MessagingStreamConnection @I, @user.id
@on \mount ~>
@connection.event.on \message @on-message
@connection.event.on \read @on-read
document.add-event-listener \visibilitychange @on-visibilitychange
@api \messaging/messages do
user_id: @user.id
.then (messages) ~>
@init = false
@messages = messages.reverse!
@update!
@scroll-to-bottom!
.catch (err) ~>
console.error err
@on \unmount ~>
@connection.event.off \message @on-message
@connection.event.off \read @on-read
@connection.close!
document.remove-event-listener \visibilitychange @on-visibilitychange
@on \update ~>
@messages.for-each (message) ~>
date = (new Date message.created_at).get-date!
month = (new Date message.created_at).get-month! + 1
message._date = date
message._datetext = month + '月 ' + date + '日'
@on-message = (message) ~>
is-bottom = @is-bottom!
@messages.push message
if message.user_id != @I.id and not document.hidden
@connection.socket.send JSON.stringify do
type: \read
id: message.id
@update!
if is-bottom
# Scroll to bottom
@scroll-to-bottom!
else if message.user_id != @I.id
# Notify
@notify '新しいメッセージがあります'
@on-read = (ids) ~>
if not Array.isArray ids then ids = [ids]
ids.for-each (id) ~>
if (@messages.some (x) ~> x.id == id)
exist = (@messages.map (x) -> x.id).index-of id
@messages[exist].is_read = true
@update!
@is-bottom = ~>
current = @refs.stream.scroll-top + @refs.stream.offset-height
max = @refs.stream.scroll-height
current > (max - 32)
@scroll-to-bottom = ~>
@refs.stream.scroll-top = @refs.stream.scroll-height
@notify = (message) ~>
n = document.create-element \p
n.inner-HTML = '<i class="fa fa-arrow-circle-down"></i>' + message
n.onclick = ~>
@scroll-to-bottom!
n.parent-node.remove-child n
@refs.notifications.append-child n
set-timeout ~>
n.style.opacity = 0
set-timeout ~>
n.parent-node.remove-child n
, 1000ms
, 4000ms
@on-visibilitychange = ~>
if document.hidden then return
@messages.for-each (message) ~>
if message.user_id != @I.id and not message.is_read
@connection.socket.send JSON.stringify do
type: \read
id: message.id

View File

@@ -0,0 +1,29 @@
mk-messaging-window
mk-window@window(is-modal={ false }, width={ '500px' }, height={ '560px' })
<yield to="header">
i.fa.fa-comments
| メッセージ
</yield>
<yield to="content">
mk-messaging@index
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
> mk-messaging
height 100%
script.
@on \mount ~>
@refs.window.on \closed ~>
@unmount!
@refs.window.refs.index.on \navigate-user (user) ~>
w = document.body.append-child document.create-element \mk-messaging-room-window
riot.mount w, do
user: user

View File

@@ -0,0 +1,226 @@
mk-notifications
div.notifications(if={ notifications.length != 0 })
virtual(each={ notification, i in notifications })
div.notification(class={ notification.type })
mk-time(time={ notification.created_at })
div.main(if={ notification.type == 'like' })
a.avatar-anchor(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id })
img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
div.text
p
i.fa.fa-thumbs-o-up
a(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id }) { notification.user.name }
a.post-ref(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
div.main(if={ notification.type == 'repost' })
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
div.text
p
i.fa.fa-retweet
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
a.post-ref(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post.repost) }
div.main(if={ notification.type == 'quote' })
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
div.text
p
i.fa.fa-quote-left
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
div.main(if={ notification.type == 'follow' })
a.avatar-anchor(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id })
img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
div.text
p
i.fa.fa-user-plus
a(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id }) { notification.user.name }
div.main(if={ notification.type == 'reply' })
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
div.text
p
i.fa.fa-reply
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
div.main(if={ notification.type == 'mention' })
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
div.text
p
i.fa.fa-at
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
p.date(if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date })
span
i.fa.fa-angle-up
| { notification._datetext }
span
i.fa.fa-angle-down
| { notifications[i + 1]._datetext }
p.empty(if={ notifications.length == 0 && !loading })
| ありません!
p.loading(if={ loading })
i.fa.fa-spinner.fa-pulse.fa-fw
| 読み込んでいます
mk-ellipsis
style.
display block
> .notifications
> .notification
margin 0
padding 16px
font-size 0.9em
border-bottom solid 1px rgba(0, 0, 0, 0.05)
&:last-child
border-bottom none
> mk-time
display inline
position absolute
top 16px
right 12px
vertical-align top
color rgba(0, 0, 0, 0.6)
font-size small
> .main
word-wrap break-word
&:after
content ""
display block
clear both
.avatar-anchor
display block
float left
img
min-width 36px
min-height 36px
max-width 36px
max-height 36px
border-radius 6px
.text
float right
width calc(100% - 36px)
padding-left 8px
p
margin 0
i
margin-right 4px
.post-preview
color rgba(0, 0, 0, 0.7)
.post-ref
color rgba(0, 0, 0, 0.7)
&:before, &:after
font-family FontAwesome
font-size 1em
font-weight normal
font-style normal
display inline-block
margin-right 3px
&:before
content "\f10d"
&:after
content "\f10e"
&.like
.text p i
color #FFAC33
&.repost, &.quote
.text p i
color #77B255
&.follow
.text p i
color #53c7ce
&.reply, &.mention
.text p i
color #555
> .date
display block
margin 0
line-height 32px
text-align center
font-size 0.8em
color #aaa
background #fdfdfd
border-bottom solid 1px rgba(0, 0, 0, 0.05)
span
margin 0 16px
i
margin-right 8px
> .empty
margin 0
padding 16px
text-align center
color #aaa
> .loading
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
script.
@mixin \api
@mixin \stream
@mixin \user-preview
@mixin \get-post-summary
@notifications = []
@loading = true
@on \mount ~>
@api \i/notifications
.then (notifications) ~>
@notifications = notifications
@loading = false
@update!
.catch (err, text-status) ->
console.error err
@stream.on \notification @on-notification
@on \unmount ~>
@stream.off \notification @on-notification
@on-notification = (notification) ~>
@notifications.unshift notification
@update!
@on \update ~>
@notifications.for-each (notification) ~>
date = (new Date notification.created_at).get-date!
month = (new Date notification.created_at).get-month! + 1
notification._date = date
notification._datetext = month + '月 ' + date + '日'

View File

@@ -0,0 +1,77 @@
mk-entrance
main
img(src='/_/resources/title.svg', alt='Misskey')
mk-entrance-signin(if={ mode == 'signin' })
mk-entrance-signup(if={ mode == 'signup' })
div.introduction(if={ mode == 'introduction' })
mk-introduction
button(onclick={ signin }) わかった
mk-forkit
footer
mk-copyright
// ↓ https://github.com/riot/riot/issues/2134 (将来的)
style(data-disable-scope).
#wait {
right: auto;
left: 15px;
}
style.
display block
height 100%
> main
display block
> img
display block
width 160px
height 170px
margin 0 auto
pointer-events none
user-select none
> .introduction
max-width 360px
margin 0 auto
color #777
> mk-introduction
padding 32px
background #fff
box-shadow 0 4px 16px rgba(0, 0, 0, 0.2)
> button
display block
margin 16px auto 0 auto
color #666
&:hover
text-decoration underline
> footer
> mk-copyright
margin 0
text-align center
line-height 64px
font-size 10px
color rgba(#000, 0.5)
script.
@mode = \signin
@signup = ~>
@mode = \signup
@update!
@signin = ~>
@mode = \signin
@update!
@introduction = ~>
@mode = \introduction
@update!

View File

@@ -0,0 +1,128 @@
mk-entrance-signin
a.help(href={ CONFIG.urls.about + '/help' }, title='お困りですか?'): i.fa.fa-question
div.form
h1
img(if={ user }, src={ user.avatar_url + '?thumbnail&size=32' })
p { user ? user.name : 'アカウント' }
mk-signin@signin
div.divider: span or
button.signup(onclick={ parent.signup }) 新規登録
a.introduction(onclick={ introduction }) Misskeyについて
style.
display block
width 290px
margin 0 auto
text-align center
&:hover
> .help
opacity 1
> .help
cursor pointer
display block
position absolute
top 0
right 0
z-index 1
margin 0
padding 0
font-size 1.2em
color #999
border none
outline none
background transparent
opacity 0
transition opacity 0.1s ease
&:hover
color #444
&:active
color #222
> i
padding 14px
> .form
padding 10px 28px 16px 28px
background #fff
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
> h1
display block
margin 0
padding 0
height 54px
line-height 54px
text-align center
text-transform uppercase
font-size 1em
font-weight bold
color rgba(0, 0, 0, 0.5)
border-bottom solid 1px rgba(0, 0, 0, 0.1)
> p
display inline
margin 0
padding 0
> img
display inline-block
top 10px
width 32px
height 32px
margin-right 8px
border-radius 100%
&[src='']
display none
> .divider
padding 16px 0
text-align center
&:after
content ""
display block
position absolute
top 50%
width 100%
height 1px
border-top solid 1px rgba(0, 0, 0, 0.1)
> *
z-index 1
padding 0 8px
color rgba(0, 0, 0, 0.5)
background #fdfdfd
> .signup
width 100%
line-height 56px
font-size 1em
color #fff
background $theme-color
border-radius 64px
&:hover
background lighten($theme-color, 5%)
&:active
background darken($theme-color, 5%)
> .introduction
display inline-block
margin-top 16px
font-size 12px
color #666
script.
@on \mount ~>
@refs.signin.on \user (user) ~>
@update do
user: user
@introduction = ~>
@parent.introduction!

View File

@@ -0,0 +1,44 @@
mk-entrance-signup
mk-signup
button.cancel(type='button', onclick={ parent.signin }, title='キャンセル'): i.fa.fa-times
style.
display block
width 368px
margin 0 auto
&:hover
> .cancel
opacity 1
> mk-signup
padding 18px 32px 0 32px
background #fff
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
> .cancel
cursor pointer
display block
position absolute
top 0
right 0
z-index 1
margin 0
padding 0
font-size 1.2em
color #999
border none
outline none
box-shadow none
background transparent
opacity 0
transition opacity 0.1s ease
&:hover
color #555
&:active
color #222
> i
padding 14px

View File

@@ -0,0 +1,51 @@
mk-home-page
mk-ui@ui(page={ page }): mk-home@home(mode={ parent.opts.mode })
style.
display block
background-position center center
background-attachment fixed
background-size cover
script.
@mixin \i
@mixin \api
@mixin \ui-progress
@mixin \stream
@mixin \get-post-summary
@unread-count = 0
@page = switch @opts.mode
| \timelie => \home
| \mentions => \mentions
| _ => \home
@on \mount ~>
@refs.ui.refs.home.on \loaded ~>
@Progress.done!
document.title = 'Misskey'
if @I.data.wallpaper
@api \drive/files/show do
file_id: @I.data.wallpaper
.then (file) ~>
@root.style.background-image = 'url(' + file.url + ')'
@Progress.start!
@stream.on \post @on-stream-post
document.add-event-listener \visibilitychange @window-on-visibilitychange, false
@on \unmount ~>
@stream.off \post @on-stream-post
document.remove-event-listener \visibilitychange @window-on-visibilitychange
@on-stream-post = (post) ~>
if document.hidden and post.user_id !== @I.id
@unread-count++
document.title = '(' + @unread-count + ') ' + @get-post-summary post
@window-on-visibilitychange = ~>
if !document.hidden
@unread-count = 0
document.title = 'Misskey'

View File

@@ -0,0 +1,46 @@
mk-not-found
mk-ui
main
h1 Not Found
img(src='/_/resources/rogge.jpg', alt='')
div.mask
style.
display block
main
display block
width 600px
margin 32px auto
> img
display block
width 600px
height 459px
pointer-events none
user-select none
border-radius 16px
box-shadow 0 0 16px rgba(0, 0, 0, 0.1)
> h1
display block
margin 0
padding 0
position absolute
top 260px
left 225px
transform rotate(-12deg)
z-index 2
color #444
font-size 24px
line-height 20px
> .mask
position absolute
top 262px
left 217px
width 126px
height 18px
transform rotate(-12deg)
background #D6D5DA
border-radius 2px 6px 7px 6px

View File

@@ -0,0 +1,25 @@
mk-post-page
mk-ui@ui: main: mk-post-detail@detail(post={ parent.post })
style.
display block
main
padding 16px
> mk-post-detail
margin 0 auto
script.
@mixin \ui-progress
@post = @opts.post
@on \mount ~>
@Progress.start!
@refs.ui.refs.detail.on \post-fetched ~>
@Progress.set 0.5
@refs.ui.refs.detail.on \loaded ~>
@Progress.done!

View File

@@ -0,0 +1,14 @@
mk-search-page
mk-ui@ui: mk-search@search(query={ parent.opts.query })
style.
display block
script.
@mixin \ui-progress
@on \mount ~>
@Progress.start!
@refs.ui.refs.search.on \loaded ~>
@Progress.done!

View File

@@ -0,0 +1,20 @@
mk-user-page
mk-ui@ui: mk-user@user(user={ parent.user }, page={ parent.opts.page })
style.
display block
script.
@mixin \ui-progress
@user = @opts.user
@on \mount ~>
@Progress.start!
@refs.ui.refs.user.on \user-fetched (user) ~>
@Progress.set 0.5
document.title = user.name + ' | Misskey'
@refs.ui.refs.user.on \loaded ~>
@Progress.done!

View File

@@ -0,0 +1,141 @@
mk-post-detail-sub(title={ title })
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username })
img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ post.user_id })
div.main
header
div.left
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id })
| { post.user.name }
span.username
| @{ post.user.username }
div.right
a.time(href={ url })
mk-time(time={ post.created_at })
div.body
div.text@text
div.media(if={ post.media })
virtual(each={ file in post.media })
img(src={ file.url + '?thumbnail&size=512' }, alt={ file.name }, title={ file.name })
style.
display block
margin 0
padding 20px 32px
background #fdfdfd
&:after
content ""
display block
clear both
&:hover
> .main > footer > button
color #888
> .avatar-anchor
display block
float left
margin 0 16px 0 0
> .avatar
display block
width 44px
height 44px
margin 0
border-radius 4px
vertical-align bottom
> .main
float left
width calc(100% - 60px)
> header
margin-bottom 4px
white-space nowrap
&:after
content ""
display block
clear both
> .left
float left
> .name
display inline
margin 0
padding 0
color #777
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
text-align left
margin 0 0 0 8px
color #ccc
> .right
float right
> .time
font-size 0.9em
color #c0c0c0
> .body
> .text
cursor default
display block
margin 0
padding 0
word-wrap break-word
font-size 1em
color #717171
> mk-url-preview
margin-top 8px
> .media
> img
display block
max-width 100%
script.
@mixin \api
@mixin \text
@mixin \date-stringify
@mixin \user-preview
@post = @opts.post
@url = CONFIG.url + '/' + @post.user.username + '/' + @post.id
@title = @date-stringify @post.created_at
@on \mount ~>
if @post.text?
tokens = @analyze @post.text
@refs.text.innerHTML = @compile tokens
@refs.text.children.for-each (e) ~>
if e.tag-name == \MK-URL
riot.mount e
@like = ~>
if @post.is_liked
@api \posts/likes/delete do
post_id: @post.id
.then ~>
@post.is_liked = false
@update!
else
@api \posts/likes/create do
post_id: @post.id
.then ~>
@post.is_liked = true
@update!

View File

@@ -0,0 +1,415 @@
mk-post-detail(title={ title })
div.fetching(if={ fetching })
mk-ellipsis-icon
div.main(if={ !fetching })
button.read-more(if={ p.reply_to && p.reply_to.reply_to_id && context == null }, title='会話をもっと読み込む', onclick={ load-context }, disabled={ loading-context })
i.fa.fa-ellipsis-v(if={ !loading-context })
i.fa.fa-spinner.fa-pulse(if={ loading-context })
div.context
virtual(each={ post in context })
mk-post-detail-sub(post={ post })
div.reply-to(if={ p.reply_to })
mk-post-detail-sub(post={ p.reply_to })
div.repost(if={ is-repost })
p
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id }): img.avatar(src={ post.user.avatar_url + '?thumbnail&size=32' }, alt='avatar')
i.fa.fa-retweet
a.name(href={ CONFIG.url + '/' + post.user.username }) { post.user.name }
| がRepost
article
a.avatar-anchor(href={ CONFIG.url + '/' + p.user.username })
img.avatar(src={ p.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ p.user.id })
header
a.name(href={ CONFIG.url + '/' + p.user.username }, data-user-preview={ p.user.id })
| { p.user.name }
span.username
| @{ p.user.username }
a.time(href={ url })
mk-time(time={ p.created_at })
div.body
div.text@text
div.media(if={ p.media })
virtual(each={ file in p.media })
img(src={ file.url + '?thumbnail&size=512' }, alt={ file.name }, title={ file.name })
footer
button(onclick={ reply }, title='返信')
i.fa.fa-reply
p.count(if={ p.replies_count > 0 }) { p.replies_count }
button(onclick={ repost }, title='Repost')
i.fa.fa-retweet
p.count(if={ p.repost_count > 0 }) { p.repost_count }
button(class={ liked: p.is_liked }, onclick={ like }, title='善哉')
i.fa.fa-thumbs-o-up
p.count(if={ p.likes_count > 0 }) { p.likes_count }
button(onclick={ NotImplementedException }): i.fa.fa-ellipsis-h
div.reposts-and-likes
div.reposts(if={ reposts && reposts.length > 0 })
header
a { p.repost_count }
p Repost
ol.users
li.user(each={ reposts })
a.avatar-anchor(href={ CONFIG.url + '/' + user.username }, title={ user.name }, data-user-preview={ user.id })
img.avatar(src={ user.avatar_url + '?thumbnail&size=32' }, alt='')
div.likes(if={ likes && likes.length > 0 })
header
a { p.likes_count }
p いいね
ol.users
li.user(each={ likes })
a.avatar-anchor(href={ CONFIG.url + '/' + username }, title={ name }, data-user-preview={ id })
img.avatar(src={ avatar_url + '?thumbnail&size=32' }, alt='')
div.replies
virtual(each={ post in replies })
mk-post-detail-sub(post={ post })
style.
display block
margin 0
padding 0
width 640px
overflow hidden
background #fff
border solid 1px rgba(0, 0, 0, 0.1)
border-radius 8px
> .fetching
padding 64px 0
> .main
> .read-more
display block
margin 0
padding 10px 0
width 100%
font-size 1em
text-align center
color #999
cursor pointer
background #fafafa
outline none
border none
border-bottom solid 1px #eef0f2
border-radius 6px 6px 0 0
&:hover
background #f6f6f6
&:active
background #f0f0f0
&:disabled
color #ccc
> .context
> *
border-bottom 1px solid #eef0f2
> .repost
color #9dbb00
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
> p
margin 0
padding 16px 32px
.avatar-anchor
display inline-block
.avatar
vertical-align bottom
min-width 28px
min-height 28px
max-width 28px
max-height 28px
margin 0 8px 0 0
border-radius 6px
i
margin-right 4px
.name
font-weight bold
& + article
padding-top 8px
> .reply-to
border-bottom 1px solid #eef0f2
> article
padding 28px 32px 18px 32px
&:after
content ""
display block
clear both
&:hover
> .main > footer > button
color #888
> .avatar-anchor
display block
width 60px
height 60px
> .avatar
display block
width 60px
height 60px
margin 0
border-radius 8px
vertical-align bottom
> header
position absolute
top 28px
left 108px
width calc(100% - 108px)
> .name
display inline-block
margin 0
line-height 24px
color #777
font-size 18px
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
display block
text-align left
margin 0
color #ccc
> .time
position absolute
top 0
right 32px
font-size 1em
color #c0c0c0
> .body
padding 8px 0
> .text
cursor default
display block
margin 0
padding 0
word-wrap break-word
font-size 1.5em
color #717171
> mk-url-preview
margin-top 8px
> .media
> img
display block
max-width 100%
> footer
font-size 1.2em
> button
margin 0 28px 0 0
padding 8px
background transparent
border none
font-size 1em
color #ddd
cursor pointer
&:hover
color #666
> .count
display inline
margin 0 0 0 8px
color #999
&.liked
color $theme-color
> .reposts-and-likes
display flex
justify-content center
padding 0
margin 16px 0
&:empty
display none
> .reposts
> .likes
display flex
flex 1 1
padding 0
border-top solid 1px #F2EFEE
> header
flex 1 1 80px
max-width 80px
padding 8px 5px 0px 10px
> a
display block
font-size 1.5em
line-height 1.4em
> p
display block
margin 0
font-size 0.7em
line-height 1em
font-weight normal
color #a0a2a5
> .users
display block
flex 1 1
margin 0
padding 10px 10px 10px 5px
list-style none
> .user
display block
float left
margin 4px
padding 0
> .avatar-anchor
display:block
> .avatar
vertical-align bottom
width 24px
height 24px
border-radius 4px
> .reposts + .likes
margin-left 16px
> .replies
> *
border-top 1px solid #eef0f2
script.
@mixin \api
@mixin \text
@mixin \user-preview
@mixin \date-stringify
@mixin \NotImplementedException
@fetching = true
@loading-context = false
@content = null
@post = null
@on \mount ~>
@api \posts/show do
post_id: @opts.post
.then (post) ~>
@fetching = false
@post = post
@trigger \loaded
@is-repost = @post.repost?
@p = if @is-repost then @post.repost else @post
@title = @date-stringify @p.created_at
@update!
if @p.text?
tokens = @analyze @p.text
@refs.text.innerHTML = @compile tokens
@refs.text.children.for-each (e) ~>
if e.tag-name == \MK-URL
riot.mount e
# URLをプレビュー
tokens
.filter (t) -> t.type == \link
.map (t) ~>
@preview = @refs.text.append-child document.create-element \mk-url-preview
riot.mount @preview, do
url: t.content
# Get likes
@api \posts/likes do
post_id: @p.id
limit: 8
.then (likes) ~>
@likes = likes
@update!
# Get reposts
@api \posts/reposts do
post_id: @p.id
limit: 8
.then (reposts) ~>
@reposts = reposts
@update!
# Get replies
@api \posts/replies do
post_id: @p.id
limit: 8
.then (replies) ~>
@replies = replies
@update!
@update!
@reply = ~>
form = document.body.append-child document.create-element \mk-post-form-window
riot.mount form, do
reply: @p
@repost = ~>
form = document.body.append-child document.create-element \mk-repost-form-window
riot.mount form, do
post: @p
@like = ~>
if @p.is_liked
@api \posts/likes/delete do
post_id: @p.id
.then ~>
@p.is_liked = false
@update!
else
@api \posts/likes/create do
post_id: @p.id
.then ~>
@p.is_liked = true
@update!
@load-context = ~>
@loading-context = true
# Get context
@api \posts/context do
post_id: @p.reply_to_id
.then (context) ~>
@context = context.reverse!
@loading-context = false
@update!

View File

@@ -0,0 +1,60 @@
mk-post-form-window
mk-window@window(is-modal={ true }, colored={ true })
<yield to="header">
span(if={ !parent.opts.reply }) 新規投稿
span(if={ parent.opts.reply }) 返信
span.files(if={ parent.files.length != 0 }) 添付: { parent.files.length }ファイル
span.uploading-files(if={ parent.uploading-files.length != 0 })
| { parent.uploading-files.length }個のファイルをアップロード中
mk-ellipsis
</yield>
<yield to="content">
div.ref(if={ parent.opts.reply })
mk-post-preview(post={ parent.opts.reply })
div.body
mk-post-form@form(reply={ parent.opts.reply })
</yield>
style.
> mk-window
[data-yield='header']
> .files
> .uploading-files
margin-left 8px
opacity 0.8
&:before
content '('
&:after
content ')'
[data-yield='content']
> .ref
> mk-post-preview
margin 16px 22px
script.
@uploading-files = []
@files = []
@on \mount ~>
@refs.window.refs.form.focus!
@refs.window.on \closed ~>
@unmount!
@refs.window.refs.form.on \post ~>
@refs.window.close!
@refs.window.refs.form.on \change-uploading-files (files) ~>
@uploading-files = files
@update!
@refs.window.refs.form.on \change-files (files) ~>
@files = files
@update!

View File

@@ -0,0 +1,430 @@
mk-post-form(ondragover={ ondragover }, ondragenter={ ondragenter }, ondragleave={ ondragleave }, ondrop={ ondrop })
textarea@text(disabled={ wait }, class={ withfiles: files.length != 0 }, oninput={ update }, onkeydown={ onkeydown }, onpaste={ onpaste }, placeholder={ opts.reply ? 'この投稿への返信...' : 'いまどうしてる?' })
div.attaches(if={ files.length != 0 })
ul.files@attaches
li.file(each={ files })
div.img(style='background-image: url({ url + "?thumbnail&size=64" })', title={ name })
img.remove(onclick={ _remove }, src='/_/resources/desktop/remove.png', title='添付取り消し', alt='')
li.add(if={ files.length < 4 }, title='PCからファイルを添付', onclick={ select-file }): i.fa.fa-plus
p.remain
| 残り{ 4 - files.length }
mk-uploader@uploader
button@upload(title='PCからファイルを添付', onclick={ select-file }): i.fa.fa-upload
button@drive(title='ドライブからファイルを添付', onclick={ select-file-from-drive }): i.fa.fa-cloud
p.text-count(class={ over: refs.text.value.length > 300 }) のこり{ 300 - refs.text.value.length }文字
button@submit(class={ wait: wait }, disabled={ wait || (refs.text.value.length == 0 && files.length == 0) }, onclick={ post })
| { wait ? '投稿中' : opts.reply ? '返信' : '投稿' }
mk-ellipsis(if={ wait })
input@file(type='file', accept='image/*', multiple, tabindex='-1', onchange={ change-file })
div.dropzone(if={ draghover })
style.
display block
padding 16px
background lighten($theme-color, 95%)
&:after
content ""
display block
clear both
> .attaches
margin 0
padding 0
background lighten($theme-color, 98%)
border solid 1px rgba($theme-color, 0.1)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
> .remain
display block
position absolute
top 8px
right 8px
margin 0
padding 0
color rgba($theme-color, 0.4)
> .files
display block
margin 0
padding 4px
list-style none
&:after
content ""
display block
clear both
> .file
display block
float left
margin 4px
padding 0
cursor move
&:hover > .remove
display block
> .img
width 64px
height 64px
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)
> 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-uploader
margin 8px 0 0 0
padding 8px
border solid 1px rgba($theme-color, 0.2)
border-radius 4px
[ref='file']
display none
[ref='text']
display block
padding 12px
margin 0
width 100%
max-width 100%
min-width 100%
min-height calc(16px + 12px + 12px)
font-size 16px
color #333
background #fff
outline none
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
transition border-color .3s ease
&:hover
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
color $theme-color
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&:disabled
opacity 0.5
&::-webkit-input-placeholder
color rgba($theme-color, 0.3)
&.withfiles
border-bottom solid 1px rgba($theme-color, 0.1) !important
border-radius 4px 4px 0 0
&:hover + .attaches
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus + .attaches
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
.text-count
pointer-events none
display block
position absolute
bottom 16px
right 138px
margin 0
line-height 40px
color rgba($theme-color, 0.5)
&.over
color #ec3828
[ref='submit']
display block
position absolute
bottom 16px
right 16px
cursor pointer
padding 0
margin 0
width 110px
height 40px
font-size 1em
color $theme-color-foreground
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
outline none
border solid 1px lighten($theme-color, 15%)
border-radius 4px
&: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
&: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
&.wait
background linear-gradient(
45deg,
darken($theme-color, 10%) 25%,
$theme-color 25%,
$theme-color 50%,
darken($theme-color, 10%) 50%,
darken($theme-color, 10%) 75%,
$theme-color 75%,
$theme-color
)
background-size 32px 32px
animation stripe-bg 1.5s linear infinite
opacity 0.7
cursor wait
@keyframes stripe-bg
from {background-position: 0 0;}
to {background-position: -64px 32px;}
[ref='upload']
[ref='drive']
display inline-block
cursor pointer
padding 0
margin 8px 4px 0 0
width 40px
height 40px
font-size 1em
color rgba($theme-color, 0.5)
background transparent
outline none
border solid 1px transparent
border-radius 4px
&:hover
background transparent
border-color rgba($theme-color, 0.3)
&:active
color rgba($theme-color, 0.6)
background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%)
border-color rgba($theme-color, 0.5)
box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset
&: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
> .dropzone
position absolute
left 0
top 0
width 100%
height 100%
border dashed 2px rgba($theme-color, 0.5)
pointer-events none
script.
@mixin \api
@mixin \notify
@mixin \autocomplete
@mixin \sortable
@wait = false
@uploadings = []
@files = []
@autocomplete = null
@in-reply-to-post = @opts.reply
# https://github.com/riot/riot/issues/2080
if @in-reply-to-post == '' then @in-reply-to-post = null
@on \mount ~>
@refs.uploader.on \uploaded (file) ~>
@add-file file
@refs.uploader.on \change-uploads (uploads) ~>
@trigger \change-uploading-files uploads
@autocomplete = new @Autocomplete @refs.text
@autocomplete.attach!
@on \unmount ~>
@autocomplete.detach!
@focus = ~>
@refs.text.focus!
@clear = ~>
@refs.text.value = ''
@files = []
@trigger \change-files
@update!
@ondragover = (e) ~>
e.stop-propagation!
@draghover = true
# ドラッグされてきたものがファイルだったら
if e.data-transfer.effect-allowed == \all
e.data-transfer.drop-effect = \copy
else
e.data-transfer.drop-effect = \move
return false
@ondragenter = (e) ~>
@draghover = true
@ondragleave = (e) ~>
@draghover = false
@ondrop = (e) ~>
e.prevent-default!
e.stop-propagation!
@draghover = false
# ファイルだったら
if e.data-transfer.files.length > 0
Array.prototype.for-each.call e.data-transfer.files, (file) ~>
@upload file
return false
# データ取得
data = e.data-transfer.get-data 'text'
if !data?
return false
try
# パース
obj = JSON.parse data
# (ドライブの)ファイルだったら
if obj.type == \file
@add-file obj.file
catch
# ignore
return false
@onkeydown = (e) ~>
if (e.which == 10 || e.which == 13) && (e.ctrl-key || e.meta-key)
@post!
@onpaste = (e) ~>
data = e.clipboard-data
items = data.items
for i from 0 to items.length - 1
item = items[i]
switch (item.kind)
| \file =>
@upload item.get-as-file!
@select-file = ~>
@refs.file.click!
@select-file-from-drive = ~>
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
i = riot.mount browser, do
multiple: true
i[0].one \selected (files) ~>
files.for-each @add-file
@change-file = ~>
files = @refs.file.files
for i from 0 to files.length - 1
file = files.item i
@upload file
@upload = (file) ~>
@refs.uploader.upload file
@add-file = (file) ~>
file._remove = ~>
@files = @files.filter (x) -> x.id != file.id
@trigger \change-files @files
@update!
@files.push file
@trigger \change-files @files
@update!
new @Sortable @refs.attaches, do
draggable: \.file
animation: 150ms
@post = (e) ~>
@wait = true
files = if @files? and @files.length > 0
then @files.map (f) -> f.id
else undefined
@api \posts/create do
text: @refs.text.value
media_ids: files
reply_to_id: if @in-reply-to-post? then @in-reply-to-post.id else undefined
.then (data) ~>
@trigger \post
@notify if @in-reply-to-post? then '返信しました!' else '投稿しました!'
.catch (err) ~>
console.error err
@notify '投稿できませんでした'
.then ~>
@wait = false
@update!

View File

@@ -0,0 +1,94 @@
mk-post-preview(title={ title })
article
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username })
img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ post.user_id })
div.main
header
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id })
| { post.user.name }
span.username
| @{ post.user.username }
a.time(href={ CONFIG.url + '/' + post.user.username + '/' + post.id })
mk-time(time={ post.created_at })
div.body
mk-sub-post-content.text(post={ post })
style.
display block
margin 0
padding 0
font-size 0.9em
background #fff
> article
&:after
content ""
display block
clear both
&:hover
> .main > footer > button
color #888
> .avatar-anchor
display block
float left
margin 0 16px 0 0
> .avatar
display block
width 52px
height 52px
margin 0
border-radius 8px
vertical-align bottom
> .main
float left
width calc(100% - 68px)
> header
margin-bottom 4px
white-space nowrap
> .name
display inline
margin 0
padding 0
color #607073
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
text-align left
margin 0 0 0 8px
color #d1d8da
> .time
position absolute
top 0
right 0
color #b2b8bb
> .body
> .text
cursor default
margin 0
padding 0
font-size 1.1em
color #717171
script.
@mixin \date-stringify
@mixin \user-preview
@post = @opts.post
@title = @date-stringify @post.created_at

View File

@@ -0,0 +1,72 @@
mk-post-status-graph
canvas@canv(width={ opts.width }, height={ opts.height })
style.
display block
> canvas
margin 0 auto
script.
@mixin \api
@mixin \is-promise
@post = null
@post-promise = if @is-promise @opts.post then @opts.post else Promise.resolve @opts.post
@on \mount ~>
post <~ @post-promise.then
@post = post
@update!
@api \aggregation/posts/like do
post_id: @post.id
limit: 30days
.then (likes) ~>
likes = likes.reverse!
@api \aggregation/posts/repost do
post_id: @post.id
limit: 30days
.then (repost) ~>
repost = repost.reverse!
@api \aggregation/posts/reply do
post_id: @post.id
limit: 30days
.then (replies) ~>
replies = replies.reverse!
new Chart @refs.canv, do
type: \bar
data:
labels: likes.map (x, i) ~> if i % 3 == 2 then x.date.day + '日' else ''
datasets: [
{
label: \いいね
type: \line
data: likes.map (x) ~> x.count
line-tension: 0
border-width: 2
fill: true
background-color: 'rgba(247, 121, 108, 0.2)'
point-background-color: \#fff
point-radius: 4
point-border-width: 2
border-color: \#F7796C
},
{
label: \返信
type: \bar
data: replies.map (x) ~> x.count
background-color: \#555
},
{
label: \Repost
type: \bar
data: repost.map (x) ~> x.count
background-color: \#a2d61e
}
]
options:
responsive: false

View File

@@ -0,0 +1,92 @@
mk-progress-dialog
mk-window@window(is-modal={ false }, can-close={ false }, width={ '500px' })
<yield to="header">
| { parent.title }
mk-ellipsis
</yield>
<yield to="content">
div.body
p.init(if={ isNaN(parent.value) })
| 待機中
mk-ellipsis
p.percentage(if={ !isNaN(parent.value) }) { Math.floor((parent.value / parent.max) * 100) }
progress(if={ !isNaN(parent.value) && parent.value < parent.max }, value={ isNaN(parent.value) ? 0 : parent.value }, max={ parent.max })
div.progress.waiting(if={ parent.value >= parent.max })
</yield>
style.
display block
> mk-window
[data-yield='content']
> .body
padding 18px 24px 24px 24px
> .init
display block
margin 0
text-align center
color rgba(#000, 0.7)
> .percentage
display block
margin 0 0 4px 0
text-align center
line-height 16px
color rgba($theme-color, 0.7)
&:after
content '%'
> progress
> .progress
display block
margin 0
width 100%
height 10px
background transparent
border none
border-radius 4px
overflow hidden
&::-webkit-progress-value
background $theme-color
&::-webkit-progress-bar
background rgba($theme-color, 0.1)
> .progress
background linear-gradient(
45deg,
lighten($theme-color, 30%) 25%,
$theme-color 25%,
$theme-color 50%,
lighten($theme-color, 30%) 50%,
lighten($theme-color, 30%) 75%,
$theme-color 75%,
$theme-color
)
background-size 32px 32px
animation progress-dialog-tag-progress-waiting 1.5s linear infinite
@keyframes progress-dialog-tag-progress-waiting
from {background-position: 0 0;}
to {background-position: -64px 32px;}
script.
@title = @opts.title
@value = parse-int @opts.value, 10
@max = parse-int @opts.max, 10
@on \mount ~>
@refs.window.on \closed ~>
@unmount!
@update-progress = (value, max) ~>
@value = parse-int value, 10
@max = parse-int max, 10
@update!
@close = ~>
@refs.window.close!

View File

@@ -0,0 +1,38 @@
mk-repost-form-window
mk-window@window(is-modal={ true }, colored={ true })
<yield to="header">
i.fa.fa-retweet
| この投稿をRepostしますか
</yield>
<yield to="content">
mk-repost-form@form(post={ parent.opts.post })
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
script.
@on-document-keydown = (e) ~>
tag = e.target.tag-name.to-lower-case!
if tag != \input and tag != \textarea
if e.which == 27 # Esc
@refs.window.close!
@on \mount ~>
@refs.window.refs.form.on \cancel ~>
@refs.window.close!
@refs.window.refs.form.on \posted ~>
@refs.window.close!
document.add-event-listener \keydown @on-document-keydown
@refs.window.on \closed ~>
@unmount!
@on \unmount ~>
document.remove-event-listener \keydown @on-document-keydown

View File

@@ -0,0 +1,140 @@
mk-repost-form
mk-post-preview(post={ opts.post })
div.form(if={ quote })
textarea@text(disabled={ wait }, placeholder='この投稿を引用...')
footer
a.quote(if={ !quote }, onclick={ onquote }) 引用する...
button.cancel(onclick={ cancel }) キャンセル
button.ok(onclick={ ok }) Repost
style.
> mk-post-preview
margin 16px 22px
> .form
[ref='text']
display block
padding 12px
margin 0
width 100%
max-width 100%
min-width 100%
min-height calc(1em + 12px + 12px)
font-size 1em
color #333
background #fff
outline none
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
transition border-color .3s ease
&:hover
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
color $theme-color
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&:disabled
opacity 0.5
&::-webkit-input-placeholder
color rgba($theme-color, 0.3)
> div
padding 16px
> footer
height 72px
background lighten($theme-color, 95%)
> .quote
position absolute
bottom 16px
left 28px
line-height 40px
button
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
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
> .cancel
right 148px
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
> .ok
right 16px
font-weight bold
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%)
&:hover
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
border-color $theme-color
&:active
background $theme-color
border-color $theme-color
script.
@mixin \api
@mixin \notify
@wait = false
@quote = false
@cancel = ~>
@trigger \cancel
@ok = ~>
@wait = true
@api \posts/create do
repost_id: @opts.post.id
text: if @quote then @refs.text.value else undefined
.then (data) ~>
@trigger \posted
@notify 'Repostしました'
.catch (err) ~>
console.error err
@notify 'Repostできませんでした'
.then ~>
@wait = false
@update!
@onquote = ~>
@quote = true

View File

@@ -0,0 +1,88 @@
mk-search-posts
div.loading(if={ is-loading })
mk-ellipsis-icon
p.empty(if={ is-empty })
i.fa.fa-search
| 「{ query }」に関する投稿は見つかりませんでした。
mk-timeline@timeline
<yield to="footer">
i.fa.fa-moon-o(if={ !parent.more-loading })
i.fa.fa-spinner.fa-pulse.fa-fw(if={ parent.more-loading })
</yield>
style.
display block
background #fff
> .loading
padding 64px 0
> .empty
display block
margin 0 auto
padding 32px
max-width 400px
text-align center
color #999
> i
display block
margin-bottom 16px
font-size 3em
color #ccc
script.
@mixin \api
@mixin \get-post-summary
@query = @opts.query
@is-loading = true
@is-empty = false
@more-loading = false
@page = 0
@on \mount ~>
document.add-event-listener \keydown @on-document-keydown
window.add-event-listener \scroll @on-scroll
@api \posts/search do
query: @query
.then (posts) ~>
@is-loading = false
@is-empty = posts.length == 0
@update!
@refs.timeline.set-posts posts
@trigger \loaded
.catch (err) ~>
console.error err
@on \unmount ~>
document.remove-event-listener \keydown @on-document-keydown
window.remove-event-listener \scroll @on-scroll
@on-document-keydown = (e) ~>
tag = e.target.tag-name.to-lower-case!
if tag != \input and tag != \textarea
if e.which == 84 # t
@refs.timeline.focus!
@more = ~>
if @more-loading or @is-loading or @timeline.posts.length == 0
return
@more-loading = true
@update!
@api \posts/search do
query: @query
page: @page + 1
.then (posts) ~>
@more-loading = false
@page++
@update!
@refs.timeline.prepend-posts posts
.catch (err) ~>
console.error err
@on-scroll = ~>
current = window.scroll-y + window.inner-height
if current > document.body.offset-height - 16 # 遊び
@more!

View File

@@ -0,0 +1,28 @@
mk-search
header
h1 { query }
mk-search-posts@posts(query={ query })
style.
display block
padding-bottom 32px
> header
width 100%
max-width 600px
margin 0 auto
color #555
> mk-search-posts
max-width 600px
margin 0 auto
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
overflow hidden
script.
@query = @opts.query
@on \mount ~>
@refs.posts.on \loaded ~>
@trigger \loaded

View File

@@ -0,0 +1,160 @@
mk-select-file-from-drive-window
mk-window@window(is-modal={ true }, width={ '800px' }, height={ '500px' })
<yield to="header">
mk-raw(content={ parent.title })
span.count(if={ parent.multiple && parent.file.length > 0 }) ({ parent.file.length }ファイル選択中)
</yield>
<yield to="content">
mk-drive-browser@browser(multiple={ parent.multiple })
div
button.upload(title='PCからドライブにファイルをアップロード', onclick={ parent.upload }): i.fa.fa-upload
button.cancel(onclick={ parent.close }) キャンセル
button.ok(disabled={ parent.multiple && parent.file.length == 0 }, onclick={ parent.ok }) 決定
</yield>
style.
> mk-window
[data-yield='header']
> mk-raw
> i
margin-right 4px
.count
margin-left 8px
opacity 0.7
[data-yield='content']
> mk-drive-browser
height calc(100% - 72px)
> div
height 72px
background lighten($theme-color, 95%)
.upload
display inline-block
position absolute
top 8px
left 16px
cursor pointer
padding 0
margin 8px 4px 0 0
width 40px
height 40px
font-size 1em
color rgba($theme-color, 0.5)
background transparent
outline none
border solid 1px transparent
border-radius 4px
&:hover
background transparent
border-color rgba($theme-color, 0.3)
&:active
color rgba($theme-color, 0.6)
background transparent
border-color rgba($theme-color, 0.5)
box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
&: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
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
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
.ok
right 16px
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
.cancel
right 148px
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
script.
@file = []
@multiple = if @opts.multiple? then @opts.multiple else false
@title = @opts.title || '<i class="fa fa-file-o"></i>ファイルを選択'
@on \mount ~>
@refs.window.refs.browser.on \selected (file) ~>
@file = file
@ok!
@refs.window.refs.browser.on \change-selection (files) ~>
@file = files
@update!
@refs.window.on \closed ~>
@unmount!
@close = ~>
@refs.window.close!
@upload = ~>
@refs.window.refs.browser.select-local-file!
@ok = ~>
@trigger \selected @file
@refs.window.close!

View File

@@ -0,0 +1,44 @@
mk-set-avatar-suggestion(onclick={ set })
p
b アバターを設定
| してみませんか?
button(onclick={ close }): i.fa.fa-times
style.
display block
cursor pointer
color #fff
background #a8cad0
&:hover
background #70abb5
> p
display block
margin 0 auto
padding 8px
max-width 1024px
> a
font-weight bold
color #fff
> button
position absolute
top 0
right 0
padding 8px
color #fff
script.
@mixin \i
@mixin \update-avatar
@set = ~>
@update-avatar @I, (i) ~>
@update-i i
@close = (e) ~>
e.prevent-default!
e.stop-propagation!
@unmount!

View File

@@ -0,0 +1,44 @@
mk-set-banner-suggestion(onclick={ set })
p
b バナーを設定
| してみませんか?
button(onclick={ close }): i.fa.fa-times
style.
display block
cursor pointer
color #fff
background #a8cad0
&:hover
background #70abb5
> p
display block
margin 0 auto
padding 8px
max-width 1024px
> a
font-weight bold
color #fff
> button
position absolute
top 0
right 0
padding 8px
color #fff
script.
@mixin \i
@mixin \update-banner
@set = ~>
@update-banner @I, (i) ~>
@update-i i
@close = (e) ~>
e.prevent-default!
e.stop-propagation!
@unmount!

View File

@@ -0,0 +1,26 @@
mk-settings-window
mk-window@window(is-modal={ true }, width={ '700px' }, height={ '550px' })
<yield to="header">
i.fa.fa-cog
| 設定
</yield>
<yield to="content">
mk-settings
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
[data-yield='content']
overflow auto
script.
@on \mount ~>
@refs.window.on \closed ~>
@unmount!
@close = ~>
@refs.window.close!

View File

@@ -0,0 +1,255 @@
mk-settings
div.nav
p(class={ active: page == 'account' }, onmousedown={ set-page.bind(null, 'account') })
i.fa.fa-fw.fa-user
| アカウント
p(class={ active: page == 'web' }, onmousedown={ set-page.bind(null, 'web') })
i.fa.fa-fw.fa-desktop
| Web
p(class={ active: page == 'notification' }, onmousedown={ set-page.bind(null, 'notification') })
i.fa.fa-fw.fa-bell-o
| 通知
p(class={ active: page == 'drive' }, onmousedown={ set-page.bind(null, 'drive') })
i.fa.fa-fw.fa-cloud
| ドライブ
p(class={ active: page == 'apps' }, onmousedown={ set-page.bind(null, 'apps') })
i.fa.fa-fw.fa-puzzle-piece
| アプリ
p(class={ active: page == 'signin' }, onmousedown={ set-page.bind(null, 'signin') })
i.fa.fa-fw.fa-sign-in
| ログイン履歴
p(class={ active: page == 'password' }, onmousedown={ set-page.bind(null, 'password') })
i.fa.fa-fw.fa-unlock-alt
| パスワード
p(class={ active: page == 'api' }, onmousedown={ set-page.bind(null, 'api') })
i.fa.fa-fw.fa-key
| API
div.pages
section.account(show={ page == 'account' })
h1 アカウント
label.avatar
p アバター
img.avatar(src={ I.avatar_url + '?thumbnail&size=64' }, alt='avatar')
button.style-normal(onclick={ avatar }) 画像を選択
label
p 名前
input@account-name(type='text', value={ I.name })
label
p 場所
input@account-location(type='text', value={ I.location })
label
p 自己紹介
textarea@account-bio { I.bio }
button.style-primary(onclick={ update-account }) 保存
section.web(show={ page == 'web' })
h1 デザイン
label
p 壁紙
button.style-normal(onclick={ wallpaper }) 画像を選択
section.web(show={ page == 'web' })
h1 その他
label.checkbox
input(type='checkbox', checked={ I.data.cache }, onclick={ update-cache })
p 読み込みを高速化する
p API通信時に新鮮なユーザー情報をキャッシュすることでフェッチのオーバーヘッドを無くします。(実験的)
label.checkbox
input(type='checkbox', checked={ I.data.debug }, onclick={ update-debug })
p 開発者モード
p デバッグ等の開発者モードを有効にします。
section.signin(show={ page == 'signin' })
h1 ログイン履歴
mk-signin-history
section.api(show={ page == 'api' })
h1 API
p
| Token:
code { I.token }
p APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。
p アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
p
| 万が一このトークンが漏れたりその可能性がある場合は
button.regenerate(onclick={ regenerate-token }) トークンを再生成
| できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
style.
display block
input:not([type])
input[type='text']
input[type='password']
input[type='email']
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
position absolute
top 0
left 0
width 200px
height 100%
padding 16px 0 0 0
border-right solid 1px #ddd
> p
display block
padding 10px 16px
margin 0
color #666
cursor pointer
-ms-user-select none
-moz-user-select none
-webkit-user-select none
user-select none
transition margin-left 0.2s ease
> i
margin-right 4px
&:hover
color #555
&.active
margin-left 8px
color $theme-color !important
> .pages
position absolute
top 0
left 200px
width calc(100% - 200px)
> section
padding 32px
// & + section
// margin-top 16px
h1
display block
margin 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
> p
width calc(100% - 32px)
margin 0 0 0 32px
font-weight bold
&:last-child
font-weight normal
color #999
&.account
> .general
> .avatar
> img
display block
float left
width 64px
height 64px
border-radius 4px
> button
float left
margin-left 8px
&.api
code
padding 4px
background #eee
.regenerate
display inline
color $theme-color
&:hover
text-decoration underline
script.
@mixin \i
@mixin \api
@mixin \dialog
@mixin \update-avatar
@mixin \update-wallpaper
@page = \account
@set-page = (page) ~>
@page = page
@avatar = ~>
@update-avatar @I, (i) ~>
@update-i i
@wallpaper = ~>
@update-wallpaper @I, (i) ~>
@update-i i
@update-account = ~>
@api \i/update do
name: @refs.account-name.value
location: @refs.account-location.value
bio: @refs.account-bio.value
.then (i) ~>
@update-i i
alert \ok
.catch (err) ~>
console.error err
@update-cache = ~>
@I.data.cache = !@I.data.cache
@api \i/appdata/set do
data: JSON.stringify do
cache: @I.data.cache
.then ~>
@update-i!
@update-debug = ~>
@I.data.debug = !@I.data.debug
@api \i/appdata/set do
data: JSON.stringify do
debug: @I.data.debug
.then ~>
@update-i!

View File

@@ -0,0 +1,73 @@
mk-signin-history
div.records(if={ history.length != 0 })
div(each={ history })
mk-time(time={ created_at })
header
i.fa.fa-check(if={ success })
i.fa.fa-times(if={ !success })
span.ip { ip }
pre: code { JSON.stringify(headers, null, ' ') }
style.
display block
> .records
> div
padding 16px 0 0 0
border-bottom solid 1px #eee
> header
> i
margin-right 8px
&.fa-check
color #0fda82
&.fa-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
script.
@mixin \api
@mixin \stream
@history = []
@fetching = true
@on \mount ~>
@api \i/signin_history
.then (history) ~>
@history = history
@fetching = false
@update!
.catch (err) ~>
console.error err
@stream.on \signin @on-signin
@on \unmount ~>
@stream.off \signin @on-signin
@on-signin = (signin) ~>
@history.unshift signin
@update!

View File

@@ -0,0 +1,59 @@
mk-stream-indicator
p(if={ state == 'initializing' })
i.fa.fa-spinner.fa-spin
span
| 接続中
mk-ellipsis
p(if={ state == 'reconnecting' })
i.fa.fa-spinner.fa-spin
span
| 切断されました 接続中
mk-ellipsis
p(if={ state == 'connected' })
i.fa.fa-check
span 接続完了
style.
display block
pointer-events none
position fixed
z-index 16384
bottom 8px
right 8px
margin 0
padding 6px 12px
font-size 0.9em
color #fff
background rgba(0, 0, 0, 0.8)
> p
display block
margin 0
> i
margin-right 0.25em
script.
@mixin \stream
@on \before-mount ~>
@state = @get-stream-state!
if @state == \connected
@root.style.opacity = 0
@stream-state-ev.on \connected ~>
@state = @get-stream-state!
@update!
set-timeout ~>
Velocity @root, {
opacity: 0
} 200ms \linear
, 1000ms
@stream-state-ev.on \closed ~>
@state = @get-stream-state!
@update!
Velocity @root, {
opacity: 1
} 0ms

View File

@@ -0,0 +1,37 @@
mk-sub-post-content
div.body
a.reply(if={ post.reply_to_id }): i.fa.fa-reply
span@text
a.quote(if={ post.repost_id }, href={ '/post:' + post.repost_id }) RP: ...
details(if={ post.media })
summary ({ post.media.length }枚の画像)
mk-images-viewer(images={ post.media })
style.
display block
word-wrap break-word
> .body
> .reply
margin-right 6px
color #717171
> .quote
margin-left 4px
font-style oblique
color #a0bf46
script.
@mixin \text
@mixin \user-preview
@post = @opts.post
@on \mount ~>
if @post.text?
tokens = @analyze @post.text
@refs.text.innerHTML = @compile tokens, false
@refs.text.children.for-each (e) ~>
if e.tag-name == \MK-URL
riot.mount e

View File

@@ -0,0 +1,95 @@
mk-timeline-post-sub(title={ title })
article
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username })
img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ post.user_id })
div.main
header
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id })
| { post.user.name }
span.username
| @{ post.user.username }
a.created-at(href={ CONFIG.url + '/' + post.user.username + '/' + post.id })
mk-time(time={ post.created_at })
div.body
mk-sub-post-content.text(post={ post })
script.
@mixin \date-stringify
@mixin \user-preview
@post = @opts.post
@title = @date-stringify @post.created_at
style.
display block
margin 0
padding 0
font-size 0.9em
> article
padding 16px
&:after
content ""
display block
clear both
&:hover
> .main > footer > button
color #888
> .avatar-anchor
display block
float left
margin 0 14px 0 0
> .avatar
display block
width 52px
height 52px
margin 0
border-radius 8px
vertical-align bottom
> .main
float left
width calc(100% - 66px)
> header
margin-bottom 4px
white-space nowrap
line-height 21px
> .name
display inline
margin 0
padding 0
color #607073
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
text-align left
margin 0 0 0 8px
color #d1d8da
> .created-at
position absolute
top 0
right 0
color #b2b8bb
> .body
> .text
cursor default
margin 0
padding 0
font-size 1.1em
color #717171

View File

@@ -0,0 +1,376 @@
mk-timeline-post(tabindex='-1', title={ title }, onkeydown={ on-key-down })
div.reply-to(if={ p.reply_to })
mk-timeline-post-sub(post={ p.reply_to })
div.repost(if={ is-repost })
p
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id }): img.avatar(src={ post.user.avatar_url + '?thumbnail&size=32' }, alt='avatar')
i.fa.fa-retweet
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id }) { post.user.name }
| がRepost
mk-time(time={ post.created_at })
article
a.avatar-anchor(href={ CONFIG.url + '/' + p.user.username })
img.avatar(src={ p.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ p.user.id })
div.main
header
a.name(href={ CONFIG.url + '/' + p.user.username }, data-user-preview={ p.user.id })
| { p.user.name }
span.username
| @{ p.user.username }
a.created-at(href={ url })
mk-time(time={ p.created_at })
div.body
div.text
a.reply(if={ p.reply_to }): i.fa.fa-reply
span@text
a.quote(if={ p.repost != null }) RP:
div.media(if={ p.media })
mk-images-viewer(images={ p.media })
div.repost(if={ p.repost })
i.fa.fa-quote-right.fa-flip-horizontal
mk-post-preview.repost(post={ p.repost })
footer
button(onclick={ reply }, title='返信')
i.fa.fa-reply
p.count(if={ p.replies_count > 0 }) { p.replies_count }
button(onclick={ repost }, title='Repost')
i.fa.fa-retweet
p.count(if={ p.repost_count > 0 }) { p.repost_count }
button(class={ liked: p.is_liked }, onclick={ like }, title='善哉')
i.fa.fa-thumbs-o-up
p.count(if={ p.likes_count > 0 }) { p.likes_count }
button(onclick={ NotImplementedException }): i.fa.fa-ellipsis-h
button(onclick={ toggle-detail }, title='詳細')
i.fa.fa-caret-down(if={ !is-detail-opened })
i.fa.fa-caret-up(if={ is-detail-opened })
div.detail(if={ is-detail-opened })
mk-post-status-graph(width='462', height='130', post={ p })
style.
display block
margin 0
padding 0
background #fff
&:focus
z-index 1
&:after
content ""
pointer-events none
position absolute
top 2px
right 2px
bottom 2px
left 2px
border 2px solid rgba($theme-color, 0.3)
border-radius 4px
> .repost
color #9dbb00
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
> p
margin 0
padding 16px 32px
line-height 28px
.avatar-anchor
display inline-block
.avatar
vertical-align bottom
width 28px
height 28px
margin 0 8px 0 0
border-radius 6px
i
margin-right 4px
.name
font-weight bold
> mk-time
position absolute
top 16px
right 32px
font-size 0.9em
line-height 28px
& + article
padding-top 8px
> .reply-to
padding 0 16px
background rgba(0, 0, 0, 0.0125)
> mk-post-preview
background transparent
> article
padding 28px 32px 18px 32px
&:after
content ""
display block
clear both
&:hover
> .main > footer > button
color #888
> .avatar-anchor
display block
float left
margin 0 16px 0 0
> .avatar
display block
width 58px
height 58px
margin 0
border-radius 8px
vertical-align bottom
> .main
float left
width calc(100% - 74px)
> header
margin-bottom 4px
white-space nowrap
line-height 24px
> .name
display inline
margin 0
padding 0
color #777
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
text-align left
margin 0 0 0 8px
color #ccc
> .created-at
position absolute
top 0
right 0
font-size 0.9em
color #c0c0c0
> .body
> .text
cursor default
display block
margin 0
padding 0
word-wrap break-word
font-size 1.1em
color #717171
mk-url-preview
margin-top 8px
> .reply
margin-right 8px
color #717171
> .quote
margin-left 4px
font-style oblique
color #a0bf46
> .media
> img
display block
max-width 100%
> .repost
margin 8px 0
> i:first-child
position absolute
top -8px
left -8px
z-index 1
color #c0dac6
font-size 28px
background #fff
> mk-post-preview
padding 16px
border dashed 1px #c0dac6
border-radius 8px
> footer
> button
margin 0 28px 0 0
padding 0 8px
line-height 32px
font-size 1em
color #ddd
background transparent
border none
cursor pointer
&:hover
color #666
> .count
display inline
margin 0 0 0 8px
color #999
&.liked
color $theme-color
&:last-child
position absolute
right 0
margin 0
> .detail
padding-top 4px
background rgba(0, 0, 0, 0.0125)
style(theme='dark').
background #0D0D0D
> article
&:hover
> .main > footer > button
color #eee
> .main
> header
> .left
> .name
color #9e9c98
> .username
color #41403f
> .right
> .time
color #4e4d4b
> .body
> .text
color #9e9c98
> footer
> button
color #9e9c98
&:hover
color #fff
> .count
color #eee
script.
@mixin \api
@mixin \text
@mixin \date-stringify
@mixin \user-preview
@mixin \NotImplementedException
@post = @opts.post
@is-repost = @post.repost? and !@post.text?
@p = if @is-repost then @post.repost else @post
@title = @date-stringify @p.created_at
@url = CONFIG.url + '/' + @p.user.username + '/' + @p.id
@is-detail-opened = false
@on \mount ~>
if @p.text?
tokens = if @p._highlight?
then @analyze @p._highlight
else @analyze @p.text
@refs.text.innerHTML = if @p._highlight?
then @compile tokens, true, false
else @compile tokens
@refs.text.children.for-each (e) ~>
if e.tag-name == \MK-URL
riot.mount e
# URLをプレビュー
tokens
.filter (t) -> t.type == \link
.map (t) ~>
@preview = @refs.text.append-child document.create-element \mk-url-preview
riot.mount @preview, do
url: t.content
@reply = ~>
form = document.body.append-child document.create-element \mk-post-form-window
riot.mount form, do
reply: @p
@repost = ~>
form = document.body.append-child document.create-element \mk-repost-form-window
riot.mount form, do
post: @p
@like = ~>
if @p.is_liked
@api \posts/likes/delete do
post_id: @p.id
.then ~>
@p.is_liked = false
@update!
else
@api \posts/likes/create do
post_id: @p.id
.then ~>
@p.is_liked = true
@update!
@toggle-detail = ~>
@is-detail-opened = !@is-detail-opened
@update!
@on-key-down = (e) ~>
should-be-cancel = true
switch
| e.which == 38 or e.which == 74 or (e.which == 9 and e.shift-key) => # ↑, j or Shift+Tab
focus @root, (e) -> e.previous-element-sibling
| e.which == 40 or e.which == 75 or e.which == 9 => # ↓, k or Tab
focus @root, (e) -> e.next-element-sibling
| e.which == 69 => # e
@repost!
| e.which == 70 or e.which == 76 => # f or l
@like!
| e.which == 82 => # r
@reply!
| _ =>
should-be-cancel = false
if should-be-cancel
e.prevent-default!
function focus(el, fn)
target = fn el
if target?
if target.has-attribute \tabindex
target.focus!
else
focus target, fn

View File

@@ -0,0 +1,86 @@
mk-timeline
virtual(each={ post, i in posts })
mk-timeline-post(post={ post })
p.date(if={ i != posts.length - 1 && post._date != posts[i + 1]._date })
span
i.fa.fa-angle-up
| { post._datetext }
span
i.fa.fa-angle-down
| { posts[i + 1]._datetext }
footer(data-yield='footer')
| <yield from="footer"/>
style.
display block
> mk-timeline-post
border-bottom solid 1px #eaeaea
&:first-child
border-top-left-radius 4px
border-top-right-radius 4px
&:last-of-type
border-bottom none
> .date
display block
margin 0
line-height 32px
font-size 14px
text-align center
color #aaa
background #fdfdfd
border-bottom solid 1px #eaeaea
span
margin 0 16px
i
margin-right 8px
> footer
padding 16px
text-align center
color #ccc
border-top solid 1px #eaeaea
border-bottom-left-radius 4px
border-bottom-right-radius 4px
style(theme='dark').
> mk-timeline-post
border-bottom-color #222221
script.
@posts = []
@set-posts = (posts) ~>
@posts = posts
@update!
@prepend-posts = (posts) ~>
posts.for-each (post) ~>
@posts.push post
@update!
@add-post = (post) ~>
@posts.unshift post
@update!
@clear = ~>
@posts = []
@update!
@focus = ~>
@root.children.0.focus!
@on \update ~>
@posts.for-each (post) ~>
date = (new Date post.created_at).get-date!
month = (new Date post.created_at).get-month! + 1
post._date = date
post._datetext = month + '月 ' + date + '日'
@tail = ~>
@posts[@posts.length - 1]

View File

@@ -0,0 +1,219 @@
mk-ui-header-account
button.header(data-active={ is-open.toString() }, onclick={ toggle })
span.username
| { I.username }
i.fa.fa-angle-down(if={ !is-open })
i.fa.fa-angle-up(if={ is-open })
img.avatar(src={ I.avatar_url + '?thumbnail&size=64' }, alt='avatar')
div.menu(if={ is-open })
ul
li: a(href={ '/' + I.username })
i.fa.fa-user
| プロフィール
i.fa.fa-angle-right
li(onclick={ drive }): p
i.fa.fa-cloud
| ドライブ
i.fa.fa-angle-right
li: a(href='/i>mentions')
i.fa.fa-at
| あなた宛て
i.fa.fa-angle-right
ul
li(onclick={ settings }): p
i.fa.fa-cog
| 設定
i.fa.fa-angle-right
ul
li(onclick={ signout }): p
i(class='fa fa-power-off')
| サインアウト
i.fa.fa-angle-right
style.
display block
float left
> .header
display block
margin 0
padding 0
color #9eaba8
border none
background transparent
cursor pointer
*
pointer-events none
&:hover
color darken(#9eaba8, 20%)
&:active
color darken(#9eaba8, 30%)
&[data-active='true']
color darken(#9eaba8, 20%)
> .avatar
$saturate = 150%
filter saturate($saturate)
-webkit-filter saturate($saturate)
-moz-filter saturate($saturate)
-ms-filter saturate($saturate)
> .username
display block
float left
margin 0 12px 0 16px
max-width 16em
line-height 48px
font-weight bold
font-family Meiryo, sans-serif
text-decoration none
i
margin-left 8px
> .avatar
display block
float left
min-width 32px
max-width 32px
min-height 32px
max-height 32px
margin 8px 8px 8px 0
border-radius 4px
transition filter 100ms ease
> .menu
display block
position absolute
top 56px
right -2px
width 230px
font-size 0.8em
background #fff
border-radius 4px
box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
&:before
content ""
pointer-events none
display block
position absolute
top -28px
right 12px
border-top solid 14px transparent
border-right solid 14px transparent
border-bottom solid 14px rgba(0, 0, 0, 0.1)
border-left solid 14px transparent
&:after
content ""
pointer-events none
display block
position absolute
top -27px
right 12px
border-top solid 14px transparent
border-right solid 14px transparent
border-bottom solid 14px #fff
border-left solid 14px transparent
ul
display block
margin 10px 0
padding 0
list-style none
& + ul
padding-top 10px
border-top solid 1px #eee
> li
display block
margin 0
padding 0
> a
> p
display block
z-index 1
padding 0 28px
margin 0
line-height 40px
color #868C8C
cursor pointer
*
pointer-events none
> i:first-of-type
margin-right 6px
> i:last-of-type
display block
position absolute
top 0
right 8px
z-index 1
padding 0 20px
font-size 1.2em
line-height 40px
&:hover, &:active
text-decoration none
background $theme-color
color $theme-color-foreground
script.
@mixin \i
@mixin \signout
@is-open = false
@on \before-unmount ~>
@close!
@toggle = ~>
if @is-open
@close!
else
@open!
@open = ~>
@is-open = true
@update!
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.add-event-listener \mousedown @mousedown
@close = ~>
@is-open = false
@update!
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.remove-event-listener \mousedown @mousedown
@mousedown = (e) ~>
e.prevent-default!
if (!contains @root, e.target) and (@root != e.target)
@close!
return false
@drive = ~>
@close!
riot.mount document.body.append-child document.create-element \mk-drive-browser-window
@settings = ~>
@close!
riot.mount document.body.append-child document.create-element \mk-settings-window
function contains(parent, child)
node = child.parent-node
while node?
if node == parent
return true
node = node.parent-node
return false

View File

@@ -0,0 +1,82 @@
mk-ui-header-clock
div.header
time@time
div.content
mk-analog-clock
style.
display inline-block
overflow visible
> .header
padding 0 12px
text-align center
font-size 0.5em
&, *
cursor: default
&:hover
background #899492
& + .content
visibility visible
> time
color #fff !important
*
color #fff !important
&:after
content ""
display block
clear both
> time
display table-cell
vertical-align middle
height 48px
color #9eaba8
> .yyyymmdd
opacity 0.7
> .content
visibility hidden
display block
position absolute
top auto
right 0
z-index 3
margin 0
padding 0
width 256px
background #899492
script.
@draw = ~>
now = new Date!
yyyy = now.get-full-year!
mm = (\0 + (now.get-month! + 1)).slice -2
dd = (\0 + now.get-date!).slice -2
yyyymmdd = "<span class='yyyymmdd'>#yyyy/#mm/#dd</span>"
hh = (\0 + now.get-hours!).slice -2
mm = (\0 + now.get-minutes!).slice -2
hhmm = "<span class='hhmm'>#hh:#mm</span>"
if now.get-seconds! % 2 == 0
hhmm .= replace \: '<span style=\'visibility:visible\'>:</span>'
else
hhmm .= replace \: '<span style=\'visibility:hidden\'>:</span>'
@refs.time.innerHTML = "#yyyymmdd<br>#hhmm"
@on \mount ~>
@draw!
@clock = set-interval @draw, 1000ms
@on \unmount ~>
clear-interval @clock

View File

@@ -0,0 +1,113 @@
mk-ui-header-nav: ul(if={ SIGNIN })
li.home(class={ active: page == 'home' }): a(href={ CONFIG.url })
i.fa.fa-home
p ホーム
li.messaging: a(onclick={ messaging })
i.fa.fa-comments
p メッセージ
i.fa.fa-circle(if={ has-unread-messaging-messages })
li.info: a(href='https://twitter.com/misskey_xyz', target='_blank')
i.fa.fa-info
p お知らせ
li.tv: a(href='https://misskey.tk', target='_blank')
i.fa.fa-television
p MisskeyTV™
style.
display inline-block
margin 0
padding 0
line-height 3rem
vertical-align top
> ul
display inline-block
margin 0
padding 0
vertical-align top
line-height 3rem
list-style none
> li
display inline-block
vertical-align top
height 48px
line-height 48px
&.active
> a
border-bottom solid 3px $theme-color
> a
display inline-block
z-index 1
height 100%
padding 0 24px
font-size 1em
font-variant small-caps
color #9eaba8
text-decoration none
transition none
cursor pointer
*
pointer-events none
&:hover
color darken(#9eaba8, 20%)
text-decoration none
> i:first-child
margin-right 8px
> i:last-child
margin-left 5px
vertical-align super
font-size 10px
color $theme-color
@media (max-width 1100px)
margin-left -5px
> p
display inline
margin 0
@media (max-width 1100px)
display none
@media (max-width 700px)
padding 0 12px
script.
@mixin \i
@mixin \api
@mixin \stream
@page = @opts.page
@on \mount ~>
@stream.on \read_all_messaging_messages @on-read-all-messaging-messages
@stream.on \unread_messaging_message @on-unread-messaging-message
# Fetch count of unread messaging messages
@api \messaging/unread
.then (count) ~>
if count.count > 0
@has-unread-messaging-messages = true
@update!
@on \unmount ~>
@stream.off \read_all_messaging_messages @on-read-all-messaging-messages
@stream.off \unread_messaging_message @on-unread-messaging-message
@on-read-all-messaging-messages = ~>
@has-unread-messaging-messages = false
@update!
@on-unread-messaging-message = ~>
@has-unread-messaging-messages = true
@update!
@messaging = ~>
riot.mount document.body.append-child document.create-element \mk-messaging-window

View File

@@ -0,0 +1,111 @@
mk-ui-header-notifications
button.header(data-active={ is-open }, onclick={ toggle })
i.fa.fa-bell-o
div.notifications(if={ is-open })
mk-notifications
style.
display block
float left
> .header
display block
margin 0
padding 0
width 32px
color #9eaba8
border none
background transparent
cursor pointer
*
pointer-events none
&:hover
color darken(#9eaba8, 20%)
&:active
color darken(#9eaba8, 30%)
&[data-active='true']
color darken(#9eaba8, 20%)
> i
font-size 1.2em
line-height 48px
> .notifications
display block
position absolute
top 56px
right -72px
width 300px
background #fff
border-radius 4px
box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
&:before
content ""
pointer-events none
display block
position absolute
top -28px
right 74px
border-top solid 14px transparent
border-right solid 14px transparent
border-bottom solid 14px rgba(0, 0, 0, 0.1)
border-left solid 14px transparent
&:after
content ""
pointer-events none
display block
position absolute
top -27px
right 74px
border-top solid 14px transparent
border-right solid 14px transparent
border-bottom solid 14px #fff
border-left solid 14px transparent
> mk-notifications
max-height 350px
font-size 1rem
overflow auto
script.
@is-open = false
@toggle = ~>
if @is-open
@close!
else
@open!
@open = ~>
@is-open = true
@update!
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.add-event-listener \mousedown @mousedown
@close = ~>
@is-open = false
@update!
all = document.query-selector-all 'body *'
Array.prototype.for-each.call all, (el) ~>
el.remove-event-listener \mousedown @mousedown
@mousedown = (e) ~>
e.prevent-default!
if (!contains @root, e.target) and (@root != e.target)
@close!
return false
function contains(parent, child)
node = child.parent-node
while node?
if node == parent
return true
node = node.parent-node
return false

Some files were not shown because too many files have changed in this diff Show More