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,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

View File

@ -0,0 +1,19 @@
/**
* Authorize Form
*/
const riot = require('riot');
document.title = 'Misskey | アプリの連携';
require('./tags.ls');
const boot = require('../boot.ls');
/**
* Boot
*/
boot(me => {
mount(document.createElement('mk-index'));
});
function mount(content) {
riot.mount(document.getElementById('app').appendChild(content));
}

View File

@ -0,0 +1,14 @@
@import "../base"
html
background #eee
@media (max-width 600px)
background #fff
body
margin 0
padding 32px 0
@media (max-width 600px)
padding 0

2
src/web/app/auth/tags.ls Normal file
View File

@ -0,0 +1,2 @@
require './tags/index.tag'
require './tags/form.tag'

View File

@ -0,0 +1,126 @@
mk-form
header
h1
i { app.name }
| があなたの
b アカウント
| に
b アクセス
| することを
b 許可
| しますか?
img(src={ app.icon_url + '?thumbnail&size=64' })
div.app
section
h2 { app.name }
p.nid { app.name_id }
p.description { app.description }
section
h2 このアプリは次の権限を要求しています:
ul
virtual(each={ p in app.permission })
li(if={ p == 'account-read' }) アカウントの情報を見る。
li(if={ p == 'account-write' }) アカウントの情報を操作する。
li(if={ p == 'post-write' }) 投稿する。
li(if={ p == 'like-write' }) いいねしたりいいね解除する。
li(if={ p == 'following-write' }) フォローしたりフォロー解除する。
li(if={ p == 'drive-read' }) ドライブを見る。
li(if={ p == 'drive-write' }) ドライブを操作する。
li(if={ p == 'notification-read' }) 通知を見る。
li(if={ p == 'notification-write' }) 通知を操作する。
div.action
button(onclick={ cancel }) キャンセル
button(onclick={ accept }) アクセスを許可
style.
display block
> header
> h1
margin 0
padding 32px 32px 20px 32px
font-size 24px
font-weight normal
color #777
i
color #77aeca
&:before
content '「'
&:after
content '」'
b
color #666
> img
display block
z-index 1
width 84px
height 84px
margin 0 auto -38px auto
border solid 5px #fff
border-radius 100%
box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
> .app
padding 44px 16px 0 16px
color #555
background #eee
box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
&:after
content ''
display block
clear both
> section
float left
width 50%
padding 8px
text-align left
> h2
margin 0
font-size 16px
color #777
> .action
padding 16px
> button
margin 0 8px
@media (max-width 600px)
> header
> img
box-shadow none
> .app
box-shadow none
@media (max-width 500px)
> header
> h1
font-size 16px
script.
@mixin \api
@session = @opts.session
@app = @session.app
@cancel = ~>
@api \auth/deny do
token: @session.token
.then ~>
@trigger \denied
@accept = ~>
@api \auth/accept do
token: @session.token
.then ~>
@trigger \accepted

View File

@ -0,0 +1,129 @@
mk-index
main(if={ SIGNIN })
p.fetching(if={ fetching })
| 読み込み中
mk-ellipsis
mk-form@form(if={ state == null && !fetching }, session={ session })
div.denied(if={ state == 'denied' })
h1 アプリケーションの連携をキャンセルしました。
p このアプリがあなたのアカウントにアクセスすることはありません。
div.accepted(if={ state == 'accepted' })
h1 { session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}
p(if={ session.app.callback_url })
| アプリケーションに戻っています
mk-ellipsis
p(if={ !session.app.callback_url }) アプリケーションに戻って、やっていってください。
div.error(if={ state == 'fetch-session-error' })
p セッションが存在しません。
main.signin(if={ !SIGNIN })
h1 サインインしてください
mk-signin
footer
img(src='/_/resources/auth/logo.svg', alt='Misskey')
style.
display block
> main
width 100%
max-width 500px
margin 0 auto
text-align center
background #fff
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
> .fetching
margin 0
padding 32px
color #555
> div
padding 64px
> h1
margin 0 0 8px 0
padding 0
font-size 20px
font-weight normal
> p
margin 0
color #555
&.denied > h1
color #e65050
&.accepted > h1
color #50bbe6
&.signin
padding 32px 32px 16px 32px
> h1
margin 0 0 22px 0
padding 0
font-size 20px
font-weight normal
color #555
@media (max-width 600px)
max-width none
box-shadow none
@media (max-width 500px)
> div
> h1
font-size 16px
> footer
> img
display block
width 64px
height 64px
margin 0 auto
script.
@mixin \i
@mixin \api
@state = null
@fetching = true
@token = window.location.href.split \/ .pop!
@on \mount ~>
if not @SIGNIN then return
# Fetch session
@api \auth/session/show do
token: @token
.then (session) ~>
@session = session
@fetching = false
# 既に連携していた場合
if @session.app.is_authorized
@api \auth/accept do
token: @session.token
.then ~>
@accepted!
else
@update!
@refs.form.on \denied ~>
@state = \denied
@update!
@refs.form.on \accepted @accepted
.catch (error) ~>
@fetching = false
@state = \fetch-session-error
@update!
@accepted = ~>
@state = \accepted
@update!
if @session.app.callback_url
location.href = @session.app.callback_url + '?token=' + @session.token

View File

@ -0,0 +1,6 @@
extends ../base
block head
meta(name='viewport', content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no')
link(rel='stylesheet', href='/_/resources/auth/style.css')
script(src='/_/resources/auth/script.js', async, defer)

23
src/web/app/base.pug Normal file
View File

@ -0,0 +1,23 @@
doctype html
!= '\r\n<!-- Thank you for using Misskey! @syuilo -->\r\n'
html(lang='ja', dir='ltr')
head
meta(charset='utf-8')
meta(name='application-name', content='Misskey')
meta(name='theme-color', content= themeColor)
meta(name='referrer', content='origin')
title Misskey
style
include ./../../../built/web/resources/init.css
script(src='https://use.fontawesome.com/22aba0df4f.js', async)
block head
body
noscript: div: p JavaScriptを有効にしてください
div#init: p
span .
span .
span .

118
src/web/app/base.styl Normal file
View File

@ -0,0 +1,118 @@
@charset 'utf-8'
$theme-color = convert(themeColor)
$theme-color-foreground = convert(themeColorForeground)
@import './reset'
/*
::selection
background $theme-color
color #fff
*/
*
tap-highlight-color rgba($theme-color, 0.7)
-webkit-tap-highlight-color rgba($theme-color, 0.7)
html, body
margin 0
padding 0
scroll-behavior smooth
text-size-adjust 100%
font-family sans-serif
html
&.progress
&, *
cursor progress !important
#error
position fixed
z-index 32768
top 0
left 0
width 100%
height 100%
background #00f
color #fff
> p
text-align center
#nprogress
pointer-events none
position absolute
z-index 65536
.bar
background $theme-color
position fixed
z-index 65537
top 0
left 0
width 100%
height 2px
/* Fancy blur effect */
.peg
display block
position absolute
right 0px
width 100px
height 100%
box-shadow 0 0 10px $theme-color, 0 0 5px $theme-color
opacity 1
transform rotate(3deg) translate(0px, -4px)
#wait
display block
position fixed
z-index 65537
top 15px
right 15px
&:before
content ""
display block
width 18px
height 18px
box-sizing border-box
border solid 2px transparent
border-top-color $theme-color
border-left-color $theme-color
border-radius 50%
animation progress-spinner 400ms linear infinite
@keyframes progress-spinner
0%
transform rotate(0deg)
100%
transform rotate(360deg)
a
text-decoration none
color $theme-color
cursor pointer
&:hover
text-decoration underline
*
cursor pointer
mk-locker
display block
position fixed
top 0
left 0
z-index 65536
width 100%
height 100%
cursor wait

154
src/web/app/boot.ls Normal file
View File

@ -0,0 +1,154 @@
#================================
# MISSKEY BOOT LOADER
#
# Misskeyを起動します。
# 1. 初期化
# 2. ユーザー取得(ログインしていれば)
# 3. アプリケーションをマウント
#================================
# LOAD DEPENDENCIES
#--------------------------------
riot = require \riot
require \velocity
log = require './common/scripts/log.ls'
api = require './common/scripts/api.ls'
signout = require './common/scripts/signout.ls'
generate-default-userdata = require './common/scripts/generate-default-userdata.ls'
mixins = require './common/mixins.ls'
check-for-update = require './common/scripts/check-for-update.ls'
require './common/tags.ls'
# MISSKEY ENTORY POINT
#--------------------------------
# for subdomains
document.domain = CONFIG.host
# ↓ iOS待ちPolyfill (SEE: http://caniuse.com/#feat=fetch)
require \fetch
# ↓ NodeList、HTMLCollectionで forEach を使えるようにする
if NodeList.prototype.for-each == undefined
NodeList.prototype.for-each = Array.prototype.for-each
if HTMLCollection.prototype.for-each == undefined
HTMLCollection.prototype.for-each = Array.prototype.for-each
# ↓ iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする
try
local-storage.set-item \kyoppie \yuppie
catch e
Storage.prototype.set-item = ~> # noop
# MAIN PROCESS
#--------------------------------
log "Misskey (aoi) v:#{VERSION}"
# Check for Update
check-for-update!
# Get token from cookie
i = ((document.cookie.match /i=(\w+)/) || [null null]).1
if i? then log "ME: #{i}"
# ユーザーをフェッチしてコールバックする
module.exports = (callback) ~>
# Get cached account data
cached-me = JSON.parse local-storage.get-item \me
if cached-me?.data?.cache
fetched cached-me
# 後から新鮮なデータをフェッチ
fetchme i, true, (fresh-data) ~>
Object.assign cached-me, fresh-data
cached-me.trigger \updated
else
# キャッシュ無効なのにキャッシュが残ってたら掃除
if cached-me?
local-storage.remove-item \me
fetchme i, false, fetched
function fetched me
if me?
riot.observable me
if me.data.cache
local-storage.set-item \me JSON.stringify me
me.on \updated ~>
# キャッシュ更新
local-storage.set-item \me JSON.stringify me
log "Fetched! Hello #{me.username}."
# activate mixins
mixins me
# destroy loading screen
init = document.get-element-by-id \init
init.parent-node.remove-child init
# set main element
document.create-element \div
..set-attribute \id \app
.. |> document.body.append-child
# Call main proccess
try
callback me
catch error
panic error
# ユーザーをフェッチしてコールバックする
function fetchme token, silent, cb
me = null
# Return when not signed in
if not token? then return done!
# Fetch user
fetch "#{CONFIG.api.url}/i" do
method: \POST
headers:
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
body: "i=#token"
.then (res) ~>
# When failed to authenticate user
if res.status != 200 then signout!
i <~ res.json!.then
me := i
me.token = token
# initialize it if user data is empty
if me.data? then done! else init!
.catch ~>
if not silent
info = document.create-element \mk-core-error
|> document.body.append-child
riot.mount info, do
retry: ~> fetchme token, false, cb
else
# noop
function done
if cb? then cb me
function init
data = generate-default-userdata!
api token, \i/appdata/set do
data: JSON.stringify data
.then ~>
me.data = data
done!
function panic e
console.error e
document.body.innerHTML = '<div id="error"><p>致命的な問題が発生しました。</p></div>'

View File

@ -0,0 +1,40 @@
const head = document.getElementsByTagName('head')[0];
const ua = navigator.userAgent.toLowerCase();
const isMobile = /mobile|iphone|ipad|android/.test(ua);
if (isMobile) {
mountMobile();
} else {
mountDesktop();
}
function mountDesktop() {
const style = document.createElement('link');
style.setAttribute('href', '/_/resources/desktop/style.css');
style.setAttribute('rel', 'stylesheet');
head.appendChild(style);
const script = document.createElement('script');
script.setAttribute('src', '/_/resources/desktop/script.js');
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
}
function mountMobile() {
const meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content', 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no');
head.appendChild(meta);
const style = document.createElement('link');
style.setAttribute('href', '/_/resources/mobile/style.css');
style.setAttribute('rel', 'stylesheet');
head.appendChild(style);
const script = document.createElement('script');
script.setAttribute('src', '/_/resources/mobile/script.js');
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
}

View File

@ -0,0 +1,5 @@
extends ../base
block head
script
include ./../../../../built/web/resources/client/script.js

View File

@ -0,0 +1,40 @@
riot = require \riot
module.exports = (me) ~>
i = if me? then me.token else null
(require './scripts/i.ls') me
riot.mixin \api do
api: (require './scripts/api.ls').bind null i
riot.mixin \cropper do
Cropper: require \cropper
riot.mixin \signout do
signout: require './scripts/signout.ls'
riot.mixin \messaging-stream do
MessagingStreamConnection: require './scripts/messaging-stream.ls'
riot.mixin \is-promise do
is-promise: require './scripts/is-promise.ls'
riot.mixin \get-post-summary do
get-post-summary: require './scripts/get-post-summary.ls'
riot.mixin \date-stringify do
date-stringify: require './scripts/date-stringify.ls'
riot.mixin \text do
analyze: require 'misskey-text'
compile: require './scripts/text-compiler.js'
riot.mixin \get-password-strength do
get-password-strength: require 'strength.js'
riot.mixin \ui-progress do
Progress: require './scripts/loading.ls'
riot.mixin \bytes-to-size do
bytes-to-size: require './scripts/bytes-to-size.js'

View File

@ -0,0 +1,13 @@
extends ../../../base
block head
link(rel='stylesheet', href='/_/resources/common/pages/about/style.css')
script(src='/_/resources/common/pages/about/script.js', async, defer)
block body
article
header
h1
block header
div.body
block content

View File

@ -0,0 +1,13 @@
extends ../base
block title
| スタッフ | Misskey
block header
| スタッフ
block content
div.members
div.member
p しゅいろ
p 統括、設計、グラフィックデザイン、プログラム

View File

@ -0,0 +1,67 @@
riot = require \riot
spinner = null
pending = 0
net = riot.observable!
riot.mixin \net do
net: net
log = (riot.mixin \log).log
module.exports = (i, endpoint, data) ->
pending++
if i? and typeof i == \object then i = i.token
body = []
# append user token when signed in
if i? then body.push "i=#i"
for k, v of data
if v != undefined
v = encodeURIComponent v
body.push "#k=#v"
opts =
method: \POST
headers:
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
body: body.join \&
if endpoint == \signin
opts.credentials = \include
ep = if (endpoint.index-of '://') > -1
then endpoint
else "#{CONFIG.api.url}/#{endpoint}"
if pending == 1
spinner := document.create-element \div
..set-attribute \id \wait
document.body.append-child spinner
new Promise (resolve, reject) ->
timer = set-timeout ->
net.trigger \detected-slow-network
, 5000ms
log "API: #{ep}"
fetch ep, opts
.then (res) ->
pending--
clear-timeout timer
if pending == 0
spinner.parent-node.remove-child spinner
if res.status == 200
res.json!.then resolve
else if res.status == 204
resolve!
else
res.json!.then (err) ->
reject err.error
.catch reject

View File

@ -0,0 +1,6 @@
module.exports = function(bytes) {
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0Byte';
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + sizes[i];
}

View File

@ -0,0 +1,9 @@
module.exports = ->
fetch \/api:meta
.then (res) ~>
meta <~ res.json!.then
if meta.commit.hash != VERSION
if window.confirm '新しいMisskeyのバージョンがあります。更新しますか\r\n(このメッセージが繰り返し表示される場合は、サーバーにデータがまだ届いていない可能性があるので、少し時間を置いてから再度お試しください)'
location.reload true
.catch ~>
# ignore

View File

@ -0,0 +1,14 @@
module.exports = (date) ->
if typeof date == \string then date = new Date date
text =
date.get-full-year! + \年 +
date.get-month! + \月 +
date.get-date! + \日 +
' ' +
date.get-hours! + \時 +
date.get-minutes! + \分 +
' ' +
"(#{[\日 \月 \火 \水 \木 \金 \土][date.get-day!]})"
return text

View File

@ -0,0 +1,27 @@
uuid = require './uuid.js'
home =
left: [ \profile \calendar \rss-reader \photo-stream ]
right: [ \broadcast \notifications \user-recommendation \donation \nav \tips ]
module.exports = ~>
home-data = []
home.left.for-each (widget) ~>
home-data.push do
name: widget
id: uuid!
place: \left
home.right.for-each (widget) ~>
home-data.push do
name: widget
id: uuid!
place: \right
data =
cache: true
debug: false
home: home-data
return data

View File

@ -0,0 +1,26 @@
get-post-summary = (post) ~>
summary = if post.text? then post.text else ''
# メディアが添付されているとき
if post.media?
summary += " (#{post.media.length}枚の画像)"
# 返信のとき
if post.reply_to_id?
if post.reply_to?
reply-summary = get-post-summary post.reply_to
summary += " RE: #{reply-summary}"
else
summary += " RE: ..."
# Repostのとき
if post.repost_id?
if post.repost?
repost-summary = get-post-summary post.repost
summary += " RP: #{repost-summary}"
else
summary += " RP: ..."
return summary.trim!
module.exports = get-post-summary

View File

@ -0,0 +1,16 @@
riot = require \riot
module.exports = (me) ->
riot.mixin \i do
init: ->
@I = me
@SIGNIN = me?
if @SIGNIN
@on \mount ~> me.on \updated @update
@on \unmount ~> me.off \updated @update
update-i: (data) ->
if data?
Object.assign me, data
me.trigger \updated

View File

@ -0,0 +1 @@
module.exports = (x) -> typeof x.then == \function

View File

@ -0,0 +1,16 @@
NProgress = require 'NProgress'
NProgress.configure do
trickle-speed: 500ms
show-spinner: false
root = document.get-elements-by-tag-name \html .0
module.exports =
start: ~>
root.class-list.add \progress
NProgress.start!
done: ~>
root.class-list.remove \progress
NProgress.done!
set: (val) ~>
NProgress.set val

View File

@ -0,0 +1,18 @@
riot = require \riot
logs = []
ev = riot.observable!
function log(msg)
logs.push do
date: new Date!
message: msg
ev.trigger \log
riot.mixin \log do
logs: logs
log: log
log-event: ev
module.exports = log

View File

@ -0,0 +1,34 @@
# Stream
#================================
ReconnectingWebSocket = require 'reconnecting-websocket'
riot = require 'riot'
class Connection
(me, otherparty) ~>
@event = riot.observable!
@me = me
host = CONFIG.api.url.replace \http \ws
@socket = new ReconnectingWebSocket "#{host}/messaging?otherparty=#{otherparty}"
@socket.add-event-listener \open @on-open
@socket.add-event-listener \message @on-message
on-open: ~>
@socket.send JSON.stringify do
i: @me.token
on-message: (message) ~>
try
message = JSON.parse message.data
if message.type?
@event.trigger message.type, message.body
catch
# ignore
close: ~>
@socket.remove-event-listener \open @on-open
@socket.remove-event-listener \message @on-message
@socket.close!
module.exports = Connection

View File

@ -0,0 +1,4 @@
module.exports = ->
local-storage.remove-item \me
document.cookie = "i=; domain=.#{CONFIG.host}; expires=Thu, 01 Jan 1970 00:00:01 GMT;"
location.href = \/

View File

@ -0,0 +1,42 @@
# Stream
#================================
ReconnectingWebSocket = require \reconnecting-websocket
riot = require \riot
module.exports = (me) ~>
state = \initializing
state-ev = riot.observable!
event = riot.observable!
socket = new ReconnectingWebSocket CONFIG.api.url.replace \http \ws
socket.onopen = ~>
state := \connected
state-ev.trigger \connected
socket.send JSON.stringify do
i: me.token
socket.onclose = ~>
state := \reconnecting
state-ev.trigger \closed
socket.onmessage = (message) ~>
try
message = JSON.parse message.data
if message.type?
event.trigger message.type, message.body
catch
# ignore
get-state = ~> state
event.on \i_updated (data) ~>
Object.assign me, data
me.trigger \updated
{
state-ev
get-state
event
}

View File

@ -0,0 +1,30 @@
module.exports = function(tokens, canBreak, escape) {
if (canBreak == null) {
canBreak = true;
}
if (escape == null) {
escape = true;
}
return tokens.map(function(token) {
switch (token.type) {
case 'text':
if (escape) {
return token.content
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/(\r\n|\n|\r)/g, canBreak ? '<br>' : ' ');
} else {
return token.content
.replace(/(\r\n|\n|\r)/g, canBreak ? '<br>' : ' ');
}
case 'bold':
return '<strong>' + token.bold + '</strong>';
case 'link':
return '<mk-url href="' + token.content + '" target="_blank"></mk-url>';
case 'mention':
return '<a href="' + CONFIG.url + '/' + token.username + '" target="_blank" data-user-preview="' + token.content + '">' + token.content + '</a>';
case 'hashtag': // TODO
return '<a>' + token.content + '</a>';
}
}).join('');
}

View File

@ -0,0 +1,12 @@
module.exports = function () {
var uuid = '', i, random;
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i == 8 || i == 12 || i == 16 || i == 20) {
uuid += '-'
}
uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16);
}
return uuid;
}

View File

@ -0,0 +1,16 @@
require './tags/core-error.tag'
require './tags/url.tag'
require './tags/url-preview.tag'
require './tags/ripple-string.tag'
require './tags/time.tag'
require './tags/file-type-icon.tag'
require './tags/uploader.tag'
require './tags/ellipsis.tag'
require './tags/raw.tag'
require './tags/number.tag'
require './tags/special-message.tag'
require './tags/signin.tag'
require './tags/signup.tag'
require './tags/forkit.tag'
require './tags/introduction.tag'
require './tags/copyright.tag'

View File

@ -0,0 +1,5 @@
mk-copyright
span (c) syuilo 2014-2016
style.
display block

View File

@ -0,0 +1,63 @@
mk-core-error
//i: i.fa.fa-times-circle
img(src='/_/resources/error.jpg', alt='')
h1: mk-ripple-string サーバーに接続できません
p.text
| インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから
a(onclick={ retry }) 再度お試し
| ください。
p.thanks いつもMisskeyをご利用いただきありがとうございます。
style.
position fixed
z-index 16385
top 0
left 0
width 100%
height 100%
text-align center
background #f8f8f8
> i
display block
margin-top 64px
font-size 5em
color #6998a0
> img
display block
height 200px
margin 64px auto 0 auto
pointer-events none
-ms-user-select none
-moz-user-select none
-webkit-user-select none
user-select none
> h1
display block
margin 32px auto 16px auto
font-size 1.5em
color #555
> .text
display block
margin 0 auto
max-width 600px
font-size 1em
color #666
> .thanks
display block
margin 32px auto 0 auto
padding 32px 0 32px 0
max-width 600px
font-size 0.9em
font-style oblique
color #aaa
border-top solid 1px #eee
script.
@retry = ~>
@unmount!
@opts.retry!

View File

@ -0,0 +1,25 @@
mk-ellipsis
span .
span .
span .
style.
display inline
> span
animation ellipsis 1.4s infinite ease-in-out both
&:nth-child(1)
animation-delay 0s
&:nth-child(2)
animation-delay 0.16s
&:nth-child(3)
animation-delay 0.32s
@keyframes ellipsis
0%, 80%, 100%
opacity 1
40%
opacity 0

View File

@ -0,0 +1,9 @@
mk-file-type-icon
i.fa.fa-file-image-o(if={ kind == 'image' })
style.
display inline
script.
@file = @opts.file
@kind = @file.type.split \/ .0

View File

@ -0,0 +1,37 @@
mk-forkit
a(href='https://github.com/syuilo/misskey', target='_blank', title='View source on Github', aria-label='View source on Github')
svg(width='80', height='80', viewBox='0 0 250 250', aria-hidden)
path(d='M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z')
path.octo-arm(d='M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2', fill='currentColor')
path(d='M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z', fill='currentColor')
style.
display block
position absolute
top 0
right 0
> a
display block
> svg
display block
//fill #151513
//color #fff
fill $theme-color
color $theme-color-foreground
.octo-arm
transform-origin 130px 106px
&:hover
.octo-arm
animation octocat-wave 560ms ease-in-out
@keyframes octocat-wave
0%, 100%
transform rotate(0)
20%, 60%
transform rotate(-25deg)
40%, 80%
transform rotate(10deg)

View File

@ -0,0 +1,22 @@
mk-introduction
article
h1 Misskeyとは
<p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p>
<p>Twitter, Facebook, LINE, Google+ などを<del>パクって</del><i>参考にして</i>います。</p>
<p>無料で誰でも利用でき、広告なども一切ありません。</p>
<p><a href={ CONFIG.urls.about } target="_blank">もっと知りたい方はこちら</a></p>
style.
display block
h1
margin 0
text-align center
font-size 1.2em
p
margin 16px 0
&:last-child
margin 0
text-align center

View File

@ -0,0 +1,15 @@
mk-number
style.
display inline
script.
@on \mount ~>
# バグ? https://github.com/riot/riot/issues/2103
#value = @opts.value
value = @opts.riot-value
max = @opts.max
if max? then if value > max then value = max
@root.innerHTML = value.to-locale-string!

View File

@ -0,0 +1,7 @@
mk-raw
style.
display inline
script.
@root.innerHTML = @opts.content

View File

@ -0,0 +1,24 @@
mk-ripple-string
<yield/>
style.
display inline
> span
animation ripple-string 5s infinite ease-in-out both
@keyframes ripple-string
0%, 50%, 100%
opacity 1
25%
opacity 0.5
script.
@on \mount ~>
text = @root.innerHTML
@root.innerHTML = ''
(text.split '').for-each (c, i) ~>
ce = document.create-element \span
ce.innerHTML = c
ce.style.animation-delay = (i / 10) + 's'
@root.append-child ce

View File

@ -0,0 +1,136 @@
mk-signin
form(onsubmit={ onsubmit }, class={ signing: signing })
label.user-name
input@username(
type='text'
pattern='^[a-zA-Z0-9\-]+$'
placeholder='ユーザー名'
autofocus
required
oninput={ oninput })
i.fa.fa-at
label.password
input@password(
type='password'
placeholder='パスワード'
required)
i.fa.fa-lock
button(type='submit', disabled={ signing }) { signing ? 'やっています...' : 'サインイン' }
style.
display block
> form
display block
z-index 2
&.signing
&, *
cursor wait !important
label
display block
margin 12px 0
i
display block
pointer-events none
position absolute
bottom 0
top 0
left 0
z-index 1
margin auto
padding 0 16px
height 1em
color #898786
input[type=text]
input[type=password]
user-select text
display inline-block
cursor auto
padding 0 0 0 38px
margin 0
width 100%
line-height 44px
font-size 1em
color rgba(0, 0, 0, 0.7)
background #fff
outline none
border solid 1px #eee
border-radius 4px
&:hover
background rgba(255, 255, 255, 0.7)
border-color #ddd
& + i
color #797776
&:focus
background #fff
border-color #ccc
& + i
color #797776
[type=submit]
cursor pointer
padding 16px
margin -6px 0 0 0
width 100%
font-size 1.2em
color rgba(0, 0, 0, 0.5)
outline none
border none
border-radius 0
background transparent
transition all .5s ease
&:hover
color $theme-color
transition all .2s ease
&:focus
color $theme-color
transition all .2s ease
&:active
color darken($theme-color, 30%)
transition all .2s ease
&:disabled
opacity 0.7
script.
@mixin \api
@user = null
@signing = false
@oninput = ~>
@api \users/show do
username: @refs.username.value
.then (user) ~>
@user = user
@trigger \user user
@update!
@onsubmit = (e) ~>
e.prevent-default!
@signing = true
@update!
@api \signin do
username: @refs.username.value
password: @refs.password.value
.then ~>
location.reload!
.catch ~>
alert 'something happened'
@signing = false
@update!
false

View File

@ -0,0 +1,352 @@
mk-signup
form(onsubmit={ onsubmit }, autocomplete='off')
label.username
p.caption
i.fa.fa-at
| ユーザー名
input@username(
type='text'
pattern='^[a-zA-Z0-9\-]{3,20}$'
placeholder='a~z、A~Z、0~9、-'
autocomplete='off'
required
onkeyup={ on-change-username })
p.profile-page-url-preview(if={ refs.username.value != '' && username-state != 'invalid-format' && username-state != 'min-range' && username-state != 'max-range' }) { CONFIG.url + '/' + refs.username.value }
p.info(if={ username-state == 'wait' }, style='color:#999')
i.fa.fa-fw.fa-spinner.fa-pulse
| 確認しています...
p.info(if={ username-state == 'ok' }, style='color:#3CB7B5')
i.fa.fa-fw.fa-check
| 利用できます
p.info(if={ username-state == 'unavailable' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| 既に利用されています
p.info(if={ username-state == 'error' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| 通信エラー
p.info(if={ username-state == 'invalid-format' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| a~z、A~Z、0~9、-(ハイフン)が使えます
p.info(if={ username-state == 'min-range' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| 3文字以上でお願いします
p.info(if={ username-state == 'max-range' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| 20文字以内でお願いします
label.password
p.caption
i.fa.fa-lock
| パスワード
input@password(
type='password'
placeholder='8文字以上を推奨します'
autocomplete='off'
required
onkeyup={ on-change-password })
div.meter(if={ password-strength != '' }, data-strength={ password-strength })
div.value@password-metar
p.info(if={ password-strength == 'low' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| 弱いパスワード
p.info(if={ password-strength == 'medium' }, style='color:#3CB7B5')
i.fa.fa-fw.fa-check
| まあまあのパスワード
p.info(if={ password-strength == 'high' }, style='color:#3CB7B5')
i.fa.fa-fw.fa-check
| 強いパスワード
label.retype-password
p.caption
i.fa.fa-lock
| パスワード(再入力)
input@password-retype(
type='password'
placeholder='確認のため再入力してください'
autocomplete='off'
required
onkeyup={ on-change-password-retype })
p.info(if={ password-retype-state == 'match' }, style='color:#3CB7B5')
i.fa.fa-fw.fa-check
| 確認されました
p.info(if={ password-retype-state == 'not-match' }, style='color:#FF1161')
i.fa.fa-fw.fa-exclamation-triangle
| 一致していません
label.recaptcha
p.caption
i.fa.fa-toggle-on(if={ recaptchaed })
i.fa.fa-toggle-off(if={ !recaptchaed })
| 認証
div.g-recaptcha(
data-callback='onRecaptchaed'
data-expired-callback='onRecaptchaExpired'
data-sitekey={ CONFIG.recaptcha.site-key })
label.agree-tou
input(
name='agree-tou',
type='checkbox',
autocomplete='off',
required)
p
a() 利用規約
| に同意する
button(onclick={ onsubmit })
| アカウント作成
style.
display block
min-width 302px
overflow hidden
> form
label
display block
margin 16px 0
> .caption
margin 0 0 4px 0
color #828888
font-size 0.95em
> i
margin-right 0.25em
color #96adac
> .info
display block
margin 4px 0
font-size 0.8em
> i
margin-right 0.3em
&.username
.profile-page-url-preview
display block
margin 4px 8px 0 4px
font-size 0.8em
color #888
&:empty
display none
&:not(:empty) + .info
margin-top 0
&.password
.meter
display block
margin-top 8px
width 100%
height 8px
&[data-strength='']
display none
&[data-strength='low']
> .value
background #d73612
&[data-strength='medium']
> .value
background #d7ca12
&[data-strength='high']
> .value
background #61bb22
> .value
display block
width 0%
height 100%
background transparent
border-radius 4px
transition all 0.1s ease
[type=text], [type=password]
user-select text
display inline-block
cursor auto
padding 0 12px
margin 0
width 100%
line-height 44px
font-size 1em
color #333 !important
background #fff !important
outline none
border solid 1px rgba(0, 0, 0, 0.1)
border-radius 4px
box-shadow 0 0 0 114514px #fff inset
transition all .3s ease
&:hover
border-color rgba(0, 0, 0, 0.2)
transition all .1s ease
&:focus
color $theme-color !important
border-color $theme-color
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
transition all 0s ease
&:disabled
opacity 0.5
.agree-tou
padding 4px
border-radius 4px
&:hover
background #f4f4f4
&:active
background #eee
&, *
cursor pointer
p
display inline
color #555
button
margin 0 0 32px 0
padding 16px
width 100%
font-size 1em
color #fff
background $theme-color
border-radius 3px
&:hover
background lighten($theme-color, 5%)
&:active
background darken($theme-color, 5%)
script.
@mixin \api
@mixin \get-password-strength
@username-state = null
@password-strength = ''
@password-retype-state = null
@recaptchaed = false
window.on-recaptchaed = ~>
@recaptchaed = true
@update!
window.on-recaptcha-expired = ~>
@recaptchaed = false
@update!
@on \mount ~>
head = (document.get-elements-by-tag-name \head).0
script = document.create-element \script
..set-attribute \src \https://www.google.com/recaptcha/api.js
head.append-child script
@on-change-username = ~>
username = @refs.username.value
if username == ''
@username-state = null
@update!
return
err = switch
| not username.match /^[a-zA-Z0-9\-]+$/ => \invalid-format
| username.length < 3chars => \min-range
| username.length > 20chars => \max-range
| _ => null
if err?
@username-state = err
@update!
else
@username-state = \wait
@update!
@api \username/available do
username: username
.then (result) ~>
if result.available
@username-state = \ok
else
@username-state = \unavailable
@update!
.catch (err) ~>
@username-state = \error
@update!
@on-change-password = ~>
password = @refs.password.value
if password == ''
@password-strength = ''
return
strength = @get-password-strength password
if strength > 0.3
@password-strength = \medium
if strength > 0.7
@password-strength = \high
else
@password-strength = \low
@update!
@refs.password-metar.style.width = (strength * 100) + \%
@on-change-password-retype = ~>
password = @refs.password.value
retyped-password = @refs.password-retype.value
if retyped-password == ''
@password-retype-state = null
return
if password == retyped-password
@password-retype-state = \match
else
@password-retype-state = \not-match
@onsubmit = (e) ~>
e.prevent-default!
username = @refs.username.value
password = @refs.password.value
locker = document.body.append-child document.create-element \mk-locker
@api \signup do
username: username
password: password
'g-recaptcha-response': grecaptcha.get-response!
.then ~>
@api \signin do
username: username
password: password
.then ~>
location.href = CONFIG.url
.catch ~>
alert '何らかの原因によりアカウントの作成に失敗しました。再度お試しください。'
grecaptcha.reset!
@recaptchaed = false
locker.parent-node.remove-child locker
false

View File

@ -0,0 +1,24 @@
mk-special-message
p(if={ m == 1 && d == 1 }) Happy New Year!
p(if={ m == 12 && d == 25 }) Merry Christmas!
style.
display block
&:empty
display none
> p
margin 0
padding 4px
text-align center
font-size 14px
font-weight bold
text-transform uppercase
color #fff
background #ff1036
script.
now = new Date!
@d = now.get-date!
@m = now.get-month! + 1

View File

@ -0,0 +1,43 @@
mk-time
time(datetime={ opts.time })
span(if={ mode == 'relative' }) { relative }
span(if={ mode == 'absolute' }) { absolute }
span(if={ mode == 'detail' }) { absolute } ({ relative })
script.
@time = new Date @opts.time
@mode = @opts.mode || \relative
@tickid = null
@absolute =
@time.get-full-year! + \年 +
@time.get-month! + \月 +
@time.get-date! + \日 +
' ' +
@time.get-hours! + \時 +
@time.get-minutes! + \分
@on \mount ~>
if @mode == \relative or @mode == \detail
@tick!
@tickid = set-interval @tick, 1000ms
@on \unmount ~>
if @mode == \relative or @mode == \detail
clear-interval @tickid
@tick = ~>
now = new Date!
ago = (now - @time) / 1000ms
@relative = switch
| ago >= 31536000s => ~~(ago / 31536000s) + '年前'
| ago >= 2592000s => ~~(ago / 2592000s) + 'ヶ月前'
| ago >= 604800s => ~~(ago / 604800s) + '週間前'
| ago >= 86400s => ~~(ago / 86400s) + '日前'
| ago >= 3600s => ~~(ago / 3600s) + '時間前'
| ago >= 60s => ~~(ago / 60s) + '分前'
| ago >= 10s => ~~(ago % 60s) + '秒前'
| ago >= 0s => 'たった今'
| ago < 0s => '未来'
| _ => 'なぞのじかん'
@update!

View File

@ -0,0 +1,201 @@
mk-uploader
ol(if={ uploads.length > 0 })
li(each={ uploads })
div.img(style='background-image: url({ img })')
p.name
i.fa.fa-spinner.fa-pulse
| { name }
p.status
span.initing(if={ progress == undefined })
| 待機中
mk-ellipsis
span.kb(if={ progress != undefined })
| { String(Math.floor(progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }
i KB
= ' / '
| { String(Math.floor(progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }
i KB
span.percentage(if={ progress != undefined }) { Math.floor((progress.value / progress.max) * 100) }
progress(if={ progress != undefined && progress.value != progress.max }, value={ progress.value }, max={ progress.max })
div.progress.initing(if={ progress == undefined })
div.progress.waiting(if={ progress != undefined && progress.value == progress.max })
style.
display block
overflow auto
&:empty
display none
> ol
display block
margin 0
padding 0
list-style none
> li
display block
margin 8px 0 0 0
padding 0
height 36px
box-shadow 0 -1px 0 rgba($theme-color, 0.1)
border-top solid 8px transparent
&:first-child
margin 0
box-shadow none
border-top none
> .img
display block
position absolute
top 0
left 0
width 36px
height 36px
background-size cover
background-position center center
> .name
display block
position absolute
top 0
left 44px
margin 0
padding 0
max-width 256px
font-size 0.8em
color rgba($theme-color, 0.7)
white-space nowrap
text-overflow ellipsis
overflow hidden
> i
margin-right 4px
> .status
display block
position absolute
top 0
right 0
margin 0
padding 0
font-size 0.8em
> .initing
color rgba($theme-color, 0.5)
> .kb
color rgba($theme-color, 0.5)
> .percentage
display inline-block
width 48px
text-align right
color rgba($theme-color, 0.7)
&:after
content '%'
> progress
display block
position absolute
bottom 0
right 0
margin 0
width calc(100% - 44px)
height 8px
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
display block
position absolute
bottom 0
right 0
margin 0
width calc(100% - 44px)
height 8px
border none
border-radius 4px
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 bg 1.5s linear infinite
&.initing
opacity 0.3
@keyframes bg
from {background-position: 0 0;}
to {background-position: -64px 32px;}
script.
@mixin \i
@uploads = []
@upload = (file, folder) ~>
id = Math.random!
ctx =
id: id
name: file.name || \untitled
progress: undefined
@uploads.push ctx
@trigger \change-uploads @uploads
@update!
reader = new FileReader!
reader.onload = (e) ~>
ctx.img = e.target.result
@update!
reader.read-as-data-URL file
data = new FormData!
data.append \i @I.token
data.append \file file
if folder?
data.append \folder_id folder
xhr = new XMLHttpRequest!
xhr.open \POST CONFIG.api.url + '/drive/files/create' true
xhr.onload = (e) ~>
drive-file = JSON.parse e.target.response
@trigger \uploaded drive-file
@uploads = @uploads.filter (x) -> x.id != id
@trigger \change-uploads @uploads
@update!
xhr.upload.onprogress = (e) ~>
if e.length-computable
if ctx.progress == undefined
ctx.progress = {}
ctx.progress.max = e.total
ctx.progress.value = e.loaded
@update!
xhr.send data

View File

@ -0,0 +1,105 @@
mk-url-preview
a(href={ url }, target='_blank', title={ url }, if={ !loading })
div.thumbnail(if={ thumbnail }, style={ 'background-image: url(' + thumbnail + ')' })
article
header: h1 { title }
p { description }
footer
img.icon(if={ icon }, src={ icon })
p { sitename }
style.
display block
font-size 16px
> a
display block
border solid 1px #eee
border-radius 4px
overflow hidden
&:hover
text-decoration none
border-color #ddd
> article > header > h1
text-decoration underline
> .thumbnail
position absolute
width 100px
height 100%
background-position center
background-size cover
& + article
left 100px
width calc(100% - 100px)
> article
padding 16px
> header
margin-bottom 8px
> h1
margin 0
font-size 1em
color #555
> p
margin 0
color #777
font-size 0.8em
> footer
margin-top 8px
> img
display inline-block
width 16px
heigth 16px
margin-right 4px
vertical-align bottom
> p
display inline-block
margin 0
color #666
font-size 0.8em
line-height 16px
@media (max-width 500px)
font-size 8px
> a
border none
> .thumbnail
width 70px
& + article
left 70px
width calc(100% - 70px)
> article
padding 8px
script.
@mixin \api
@url = @opts.url
@loading = true
@on \mount ~>
fetch CONFIG.url + '/api:url?url=' + @url
.then (res) ~>
info <~ res.json!.then
@title = info.title
@description = info.description
@thumbnail = info.thumbnail
@icon = info.icon
@sitename = info.sitename
@loading = false
@update!

View File

@ -0,0 +1,50 @@
mk-url
a(href={ url }, target={ opts.target })
span.schema { schema }//
span.hostname { hostname }
span.port(if={ port != '' }) :{ port }
span.pathname(if={ pathname != '' }) { pathname }
span.query { query }
span.hash { hash }
style.
> a
&:after
content "\f14c"
display inline-block
padding-left 2px
font-family FontAwesome
font-size .9em
font-weight 400
font-style normal
> .schema
opacity 0.5
> .hostname
font-weight bold
> .pathname
opacity 0.8
> .query
opacity 0.5
> .hash
font-style italic
script.
@url = @opts.href
@on \before-mount ~>
parser = document.create-element \a
parser.href = @url
@schema = parser.protocol
@hostname = parser.hostname
@port = parser.port
@pathname = parser.pathname
@query = parser.search
@hash = parser.hash
@update!

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!

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