Initial commit 🍀
This commit is contained in:
7
src/web/app/auth/resources/logo.svg
Normal file
7
src/web/app/auth/resources/logo.svg
Normal 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 |
19
src/web/app/auth/script.js
Normal file
19
src/web/app/auth/script.js
Normal 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));
|
||||
}
|
14
src/web/app/auth/style.styl
Normal file
14
src/web/app/auth/style.styl
Normal 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
2
src/web/app/auth/tags.ls
Normal file
@ -0,0 +1,2 @@
|
||||
require './tags/index.tag'
|
||||
require './tags/form.tag'
|
126
src/web/app/auth/tags/form.tag
Normal file
126
src/web/app/auth/tags/form.tag
Normal 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
|
129
src/web/app/auth/tags/index.tag
Normal file
129
src/web/app/auth/tags/index.tag
Normal 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
|
6
src/web/app/auth/view.pug
Normal file
6
src/web/app/auth/view.pug
Normal 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
23
src/web/app/base.pug
Normal 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
118
src/web/app/base.styl
Normal 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
154
src/web/app/boot.ls
Normal 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>'
|
40
src/web/app/client/script.js
Normal file
40
src/web/app/client/script.js
Normal 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);
|
||||
}
|
5
src/web/app/client/view.pug
Normal file
5
src/web/app/client/view.pug
Normal file
@ -0,0 +1,5 @@
|
||||
extends ../base
|
||||
|
||||
block head
|
||||
script
|
||||
include ./../../../../built/web/resources/client/script.js
|
40
src/web/app/common/mixins.ls
Normal file
40
src/web/app/common/mixins.ls
Normal 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'
|
13
src/web/app/common/pages/about/base.pug
Normal file
13
src/web/app/common/pages/about/base.pug
Normal 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
|
13
src/web/app/common/pages/about/pages/staff.pug
Normal file
13
src/web/app/common/pages/about/pages/staff.pug
Normal file
@ -0,0 +1,13 @@
|
||||
extends ../base
|
||||
|
||||
block title
|
||||
| スタッフ | Misskey
|
||||
|
||||
block header
|
||||
| スタッフ
|
||||
|
||||
block content
|
||||
div.members
|
||||
div.member
|
||||
p しゅいろ
|
||||
p 統括、設計、グラフィックデザイン、プログラム
|
67
src/web/app/common/scripts/api.ls
Normal file
67
src/web/app/common/scripts/api.ls
Normal 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
|
6
src/web/app/common/scripts/bytes-to-size.js
Normal file
6
src/web/app/common/scripts/bytes-to-size.js
Normal 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];
|
||||
}
|
9
src/web/app/common/scripts/check-for-update.ls
Normal file
9
src/web/app/common/scripts/check-for-update.ls
Normal 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
|
14
src/web/app/common/scripts/date-stringify.ls
Normal file
14
src/web/app/common/scripts/date-stringify.ls
Normal 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
|
27
src/web/app/common/scripts/generate-default-userdata.ls
Normal file
27
src/web/app/common/scripts/generate-default-userdata.ls
Normal 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
|
26
src/web/app/common/scripts/get-post-summary.ls
Normal file
26
src/web/app/common/scripts/get-post-summary.ls
Normal 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
|
16
src/web/app/common/scripts/i.ls
Normal file
16
src/web/app/common/scripts/i.ls
Normal 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
|
1
src/web/app/common/scripts/is-promise.ls
Normal file
1
src/web/app/common/scripts/is-promise.ls
Normal file
@ -0,0 +1 @@
|
||||
module.exports = (x) -> typeof x.then == \function
|
16
src/web/app/common/scripts/loading.ls
Normal file
16
src/web/app/common/scripts/loading.ls
Normal 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
|
18
src/web/app/common/scripts/log.ls
Normal file
18
src/web/app/common/scripts/log.ls
Normal 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
|
34
src/web/app/common/scripts/messaging-stream.ls
Normal file
34
src/web/app/common/scripts/messaging-stream.ls
Normal 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
|
4
src/web/app/common/scripts/signout.ls
Normal file
4
src/web/app/common/scripts/signout.ls
Normal 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 = \/
|
42
src/web/app/common/scripts/stream.ls
Normal file
42
src/web/app/common/scripts/stream.ls
Normal 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
|
||||
}
|
30
src/web/app/common/scripts/text-compiler.js
Normal file
30
src/web/app/common/scripts/text-compiler.js
Normal 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, '>')
|
||||
.replace(/</g, '<')
|
||||
.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('');
|
||||
}
|
12
src/web/app/common/scripts/uuid.js
Normal file
12
src/web/app/common/scripts/uuid.js
Normal 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;
|
||||
}
|
16
src/web/app/common/tags.ls
Normal file
16
src/web/app/common/tags.ls
Normal 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'
|
5
src/web/app/common/tags/copyright.tag
Normal file
5
src/web/app/common/tags/copyright.tag
Normal file
@ -0,0 +1,5 @@
|
||||
mk-copyright
|
||||
span (c) syuilo 2014-2016
|
||||
|
||||
style.
|
||||
display block
|
63
src/web/app/common/tags/core-error.tag
Normal file
63
src/web/app/common/tags/core-error.tag
Normal 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!
|
25
src/web/app/common/tags/ellipsis.tag
Normal file
25
src/web/app/common/tags/ellipsis.tag
Normal 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
|
9
src/web/app/common/tags/file-type-icon.tag
Normal file
9
src/web/app/common/tags/file-type-icon.tag
Normal 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
|
37
src/web/app/common/tags/forkit.tag
Normal file
37
src/web/app/common/tags/forkit.tag
Normal 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)
|
22
src/web/app/common/tags/introduction.tag
Normal file
22
src/web/app/common/tags/introduction.tag
Normal 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
|
15
src/web/app/common/tags/number.tag
Normal file
15
src/web/app/common/tags/number.tag
Normal 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!
|
7
src/web/app/common/tags/raw.tag
Normal file
7
src/web/app/common/tags/raw.tag
Normal file
@ -0,0 +1,7 @@
|
||||
mk-raw
|
||||
|
||||
style.
|
||||
display inline
|
||||
|
||||
script.
|
||||
@root.innerHTML = @opts.content
|
24
src/web/app/common/tags/ripple-string.tag
Normal file
24
src/web/app/common/tags/ripple-string.tag
Normal 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
|
136
src/web/app/common/tags/signin.tag
Normal file
136
src/web/app/common/tags/signin.tag
Normal 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
|
352
src/web/app/common/tags/signup.tag
Normal file
352
src/web/app/common/tags/signup.tag
Normal 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
|
24
src/web/app/common/tags/special-message.tag
Normal file
24
src/web/app/common/tags/special-message.tag
Normal 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
|
43
src/web/app/common/tags/time.tag
Normal file
43
src/web/app/common/tags/time.tag
Normal 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!
|
201
src/web/app/common/tags/uploader.tag
Normal file
201
src/web/app/common/tags/uploader.tag
Normal 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
|
105
src/web/app/common/tags/url-preview.tag
Normal file
105
src/web/app/common/tags/url-preview.tag
Normal 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!
|
50
src/web/app/common/tags/url.tag
Normal file
50
src/web/app/common/tags/url.tag
Normal 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!
|
47
src/web/app/desktop/mixins.ls
Normal file
47
src/web/app/desktop/mixins.ls
Normal 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'
|
7
src/web/app/desktop/resources/header-logo.svg
Normal file
7
src/web/app/desktop/resources/header-logo.svg
Normal 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 |
BIN
src/web/app/desktop/resources/remove.png
Normal file
BIN
src/web/app/desktop/resources/remove.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
77
src/web/app/desktop/router.ls
Normal file
77
src/web/app/desktop/router.ls
Normal 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
|
42
src/web/app/desktop/script.js
Normal file
42
src/web/app/desktop/script.js
Normal 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);
|
||||
});
|
108
src/web/app/desktop/scripts/autocomplete.ls
Normal file
108
src/web/app/desktop/scripts/autocomplete.ls
Normal 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
|
17
src/web/app/desktop/scripts/dialog.ls
Normal file
17
src/web/app/desktop/scripts/dialog.ls
Normal 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
|
56
src/web/app/desktop/scripts/follow-scroll.ls
Normal file
56
src/web/app/desktop/scripts/follow-scroll.ls
Normal 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
|
19
src/web/app/desktop/scripts/fuck-ad-block.ls
Normal file
19
src/web/app/desktop/scripts/fuck-ad-block.ls
Normal 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
|
||||
]
|
13
src/web/app/desktop/scripts/input-dialog.ls
Normal file
13
src/web/app/desktop/scripts/input-dialog.ls
Normal 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
|
6
src/web/app/desktop/scripts/notify.ls
Normal file
6
src/web/app/desktop/scripts/notify.ls
Normal 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
|
8
src/web/app/desktop/scripts/open-window.ls
Normal file
8
src/web/app/desktop/scripts/open-window.ls
Normal 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
|
38
src/web/app/desktop/scripts/stream.ls
Normal file
38
src/web/app/desktop/scripts/stream.ls
Normal 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
|
81
src/web/app/desktop/scripts/update-avatar.ls
Normal file
81
src/web/app/desktop/scripts/update-avatar.ls
Normal 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
|
81
src/web/app/desktop/scripts/update-banner.ls
Normal file
81
src/web/app/desktop/scripts/update-banner.ls
Normal 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
|
35
src/web/app/desktop/scripts/update-wallpaper.ls
Normal file
35
src/web/app/desktop/scripts/update-wallpaper.ls
Normal 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
|
74
src/web/app/desktop/scripts/user-preview.ls
Normal file
74
src/web/app/desktop/scripts/user-preview.ls
Normal 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
|
114
src/web/app/desktop/style.styl
Normal file
114
src/web/app/desktop/style.styl
Normal 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
103
src/web/app/desktop/tags.ls
Normal 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'
|
102
src/web/app/desktop/tags/analog-clock.tag
Normal file
102
src/web/app/desktop/tags/analog-clock.tag
Normal 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!
|
182
src/web/app/desktop/tags/autocomplete-suggestion.tag
Normal file
182
src/web/app/desktop/tags/autocomplete-suggestion.tag
Normal 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
|
134
src/web/app/desktop/tags/big-follow-button.tag
Normal file
134
src/web/app/desktop/tags/big-follow-button.tag
Normal 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!
|
138
src/web/app/desktop/tags/contextmenu.tag
Normal file
138
src/web/app/desktop/tags/contextmenu.tag
Normal 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
|
189
src/web/app/desktop/tags/crop-window.tag
Normal file
189
src/web/app/desktop/tags/crop-window.tag
Normal 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!
|
87
src/web/app/desktop/tags/debugger.tag
Normal file
87
src/web/app/desktop/tags/debugger.tag
Normal 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
|
@ -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
|
141
src/web/app/desktop/tags/dialog.tag
Normal file
141
src/web/app/desktop/tags/dialog.tag
Normal 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!
|
63
src/web/app/desktop/tags/donation.tag
Normal file
63
src/web/app/desktop/tags/donation.tag
Normal 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!
|
28
src/web/app/desktop/tags/drive/base-contextmenu.tag
Normal file
28
src/web/app/desktop/tags/drive/base-contextmenu.tag
Normal 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!
|
29
src/web/app/desktop/tags/drive/browser-window.tag
Normal file
29
src/web/app/desktop/tags/drive/browser-window.tag
Normal 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!
|
634
src/web/app/desktop/tags/drive/browser.tag
Normal file
634
src/web/app/desktop/tags/drive/browser.tag
Normal 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!=' 検索')
|
||||
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
|
97
src/web/app/desktop/tags/drive/file-contextmenu.tag
Normal file
97
src/web/app/desktop/tags/drive/file-contextmenu.tag
Normal 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!
|
207
src/web/app/desktop/tags/drive/file.tag
Normal file
207
src/web/app/desktop/tags/drive/file.tag
Normal 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
|
62
src/web/app/desktop/tags/drive/folder-contextmenu.tag
Normal file
62
src/web/app/desktop/tags/drive/folder-contextmenu.tag
Normal 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
|
183
src/web/app/desktop/tags/drive/folder.tag
Normal file
183
src/web/app/desktop/tags/drive/folder.tag
Normal 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
|
96
src/web/app/desktop/tags/drive/nav-folder.tag
Normal file
96
src/web/app/desktop/tags/drive/nav-folder.tag
Normal 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
|
34
src/web/app/desktop/tags/ellipsis-icon.tag
Normal file
34
src/web/app/desktop/tags/ellipsis-icon.tag
Normal 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)
|
127
src/web/app/desktop/tags/follow-button.tag
Normal file
127
src/web/app/desktop/tags/follow-button.tag
Normal 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!
|
163
src/web/app/desktop/tags/following-setuper.tag
Normal file
163
src/web/app/desktop/tags/following-setuper.tag
Normal 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!
|
15
src/web/app/desktop/tags/go-top.tag
Normal file
15
src/web/app/desktop/tags/go-top.tag
Normal 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
|
75
src/web/app/desktop/tags/home-widgets/broadcast.tag
Normal file
75
src/web/app/desktop/tags/home-widgets/broadcast.tag
Normal 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
|
147
src/web/app/desktop/tags/home-widgets/calendar.tag
Normal file
147
src/web/app/desktop/tags/home-widgets/calendar.tag
Normal 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
|
37
src/web/app/desktop/tags/home-widgets/donation.tag
Normal file
37
src/web/app/desktop/tags/home-widgets/donation.tag
Normal 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
|
117
src/web/app/desktop/tags/home-widgets/mentions.tag
Normal file
117
src/web/app/desktop/tags/home-widgets/mentions.tag
Normal 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!
|
23
src/web/app/desktop/tags/home-widgets/nav.tag
Normal file
23
src/web/app/desktop/tags/home-widgets/nav.tag
Normal 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
|
49
src/web/app/desktop/tags/home-widgets/notifications.tag
Normal file
49
src/web/app/desktop/tags/home-widgets/notifications.tag
Normal 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
|
86
src/web/app/desktop/tags/home-widgets/photo-stream.tag
Normal file
86
src/web/app/desktop/tags/home-widgets/photo-stream.tag
Normal 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!
|
55
src/web/app/desktop/tags/home-widgets/profile.tag
Normal file
55
src/web/app/desktop/tags/home-widgets/profile.tag
Normal 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
|
94
src/web/app/desktop/tags/home-widgets/rss-reader.tag
Normal file
94
src/web/app/desktop/tags/home-widgets/rss-reader.tag
Normal 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!
|
113
src/web/app/desktop/tags/home-widgets/timeline.tag
Normal file
113
src/web/app/desktop/tags/home-widgets/timeline.tag
Normal 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!
|
70
src/web/app/desktop/tags/home-widgets/tips.tag
Normal file
70
src/web/app/desktop/tags/home-widgets/tips.tag
Normal 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
|
||||
}
|
154
src/web/app/desktop/tags/home-widgets/user-recommendation.tag
Normal file
154
src/web/app/desktop/tags/home-widgets/user-recommendation.tag
Normal 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!
|
86
src/web/app/desktop/tags/home.tag
Normal file
86
src/web/app/desktop/tags/home.tag
Normal 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
Reference in New Issue
Block a user