Initial commit 🍀
This commit is contained in:
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!
|
||||
73
src/web/app/desktop/tags/image-dialog.tag
Normal file
73
src/web/app/desktop/tags/image-dialog.tag
Normal file
@@ -0,0 +1,73 @@
|
||||
mk-image-dialog
|
||||
div.bg@bg(onclick={ close })
|
||||
img@img(src={ image.url }, alt={ image.name }, title={ image.name }, onclick={ close })
|
||||
|
||||
style.
|
||||
display block
|
||||
position fixed
|
||||
z-index 2048
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
opacity 0
|
||||
|
||||
> .bg
|
||||
display block
|
||||
position fixed
|
||||
z-index 1
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.7)
|
||||
|
||||
> img
|
||||
position fixed
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
max-width 100%
|
||||
max-height 100%
|
||||
margin auto
|
||||
cursor zoom-out
|
||||
|
||||
script.
|
||||
@image = @opts.image
|
||||
|
||||
@on \mount ~>
|
||||
Velocity @root, {
|
||||
opacity: 1
|
||||
} {
|
||||
duration: 100ms
|
||||
easing: \linear
|
||||
}
|
||||
|
||||
#Velocity @img, {
|
||||
# scale: 1
|
||||
# opacity: 1
|
||||
#} {
|
||||
# duration: 200ms
|
||||
# easing: \ease-out
|
||||
#}
|
||||
|
||||
@close = ~>
|
||||
Velocity @root, {
|
||||
opacity: 0
|
||||
} {
|
||||
duration: 100ms
|
||||
easing: \linear
|
||||
complete: ~> @unmount!
|
||||
}
|
||||
|
||||
#Velocity @img, {
|
||||
# scale: 0.9
|
||||
# opacity: 0
|
||||
#} {
|
||||
# duration: 200ms
|
||||
# easing: \ease-in
|
||||
# complete: ~>
|
||||
# @unmount!
|
||||
#}
|
||||
43
src/web/app/desktop/tags/images-viewer.tag
Normal file
43
src/web/app/desktop/tags/images-viewer.tag
Normal file
@@ -0,0 +1,43 @@
|
||||
mk-images-viewer
|
||||
div.image@view(onmousemove={ mousemove }, style={ 'background-image: url(' + image.url + '?thumbnail' }, onclick={ click })
|
||||
img(src={ image.url + '?thumbnail&size=512' }, alt={ image.name }, title={ image.name })
|
||||
|
||||
style.
|
||||
display block
|
||||
padding 8px
|
||||
overflow hidden
|
||||
box-shadow 0 0 4px rgba(0, 0, 0, 0.2)
|
||||
border-radius 4px
|
||||
|
||||
> .image
|
||||
cursor zoom-in
|
||||
|
||||
> img
|
||||
display block
|
||||
max-height 256px
|
||||
max-width 100%
|
||||
margin 0 auto
|
||||
|
||||
&:hover
|
||||
> img
|
||||
visibility hidden
|
||||
|
||||
&:not(:hover)
|
||||
background-image none !important
|
||||
|
||||
script.
|
||||
@images = @opts.images
|
||||
@image = @images.0
|
||||
|
||||
@mousemove = (e) ~>
|
||||
rect = @refs.view.get-bounding-client-rect!
|
||||
mouse-x = e.client-x - rect.left
|
||||
mouse-y = e.client-y - rect.top
|
||||
xp = mouse-x / @refs.view.offset-width * 100
|
||||
yp = mouse-y / @refs.view.offset-height * 100
|
||||
@refs.view.style.background-position = xp + '% ' + yp + '%'
|
||||
|
||||
@click = ~>
|
||||
dialog = document.body.append-child document.create-element \mk-image-dialog
|
||||
riot.mount dialog, do
|
||||
image: @image
|
||||
156
src/web/app/desktop/tags/input-dialog.tag
Normal file
156
src/web/app/desktop/tags/input-dialog.tag
Normal file
@@ -0,0 +1,156 @@
|
||||
mk-input-dialog
|
||||
mk-window@window(is-modal={ true }, width={ '500px' })
|
||||
<yield to="header">
|
||||
i.fa.fa-i-cursor
|
||||
| { parent.title }
|
||||
</yield>
|
||||
<yield to="content">
|
||||
div.body
|
||||
input@text(oninput={ parent.update }, onkeydown={ parent.on-keydown }, placeholder={ parent.placeholder })
|
||||
div.action
|
||||
button.cancel(onclick={ parent.cancel }) キャンセル
|
||||
button.ok(disabled={ !parent.allow-empty && refs.text.value.length == 0 }, onclick={ parent.ok }) 決定
|
||||
</yield>
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
[data-yield='content']
|
||||
> .body
|
||||
padding 16px
|
||||
|
||||
> input
|
||||
display block
|
||||
padding 8px
|
||||
margin 0
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
font-size 1em
|
||||
color #333
|
||||
background #fff
|
||||
outline none
|
||||
border solid 1px rgba($theme-color, 0.1)
|
||||
border-radius 4px
|
||||
transition border-color .3s ease
|
||||
|
||||
&:hover
|
||||
border-color rgba($theme-color, 0.2)
|
||||
transition border-color .1s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color
|
||||
border-color rgba($theme-color, 0.5)
|
||||
transition border-color 0s ease
|
||||
|
||||
&::-webkit-input-placeholder
|
||||
color rgba($theme-color, 0.3)
|
||||
|
||||
> .action
|
||||
height 72px
|
||||
background lighten($theme-color, 95%)
|
||||
|
||||
.ok
|
||||
.cancel
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 0
|
||||
width 120px
|
||||
height 40px
|
||||
font-size 1em
|
||||
outline none
|
||||
border-radius 4px
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
cursor default
|
||||
|
||||
.ok
|
||||
right 16px
|
||||
color $theme-color-foreground
|
||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||
border solid 1px lighten($theme-color, 15%)
|
||||
|
||||
&:not(:disabled)
|
||||
font-weight bold
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||
border-color $theme-color
|
||||
|
||||
&:active:not(:disabled)
|
||||
background $theme-color
|
||||
border-color $theme-color
|
||||
|
||||
.cancel
|
||||
right 148px
|
||||
color #888
|
||||
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||
border solid 1px #e2e2e2
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||
border-color #dcdcdc
|
||||
|
||||
&:active
|
||||
background #ececec
|
||||
border-color #dcdcdc
|
||||
|
||||
script.
|
||||
@done = false
|
||||
|
||||
@title = @opts.title
|
||||
@placeholder = @opts.placeholder
|
||||
@default = @opts.default
|
||||
@allow-empty = if @opts.allow-empty? then @opts.allow-empty else true
|
||||
|
||||
@on \mount ~>
|
||||
@text = @refs.window.refs.text
|
||||
if @default?
|
||||
@text.value = @default
|
||||
@text.focus!
|
||||
|
||||
@refs.window.on \closing ~>
|
||||
if @done
|
||||
@opts.on-ok @text.value
|
||||
else
|
||||
if @opts.on-cancel?
|
||||
@opts.on-cancel!
|
||||
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@cancel = ~>
|
||||
@done = false
|
||||
@refs.window.close!
|
||||
|
||||
@ok = ~>
|
||||
if not @allow-empty and @text.value == '' then return
|
||||
@done = true
|
||||
@refs.window.close!
|
||||
|
||||
@on-keydown = (e) ~>
|
||||
if e.which == 13 # Enter
|
||||
e.prevent-default!
|
||||
e.stop-propagation!
|
||||
@ok!
|
||||
100
src/web/app/desktop/tags/list-user.tag
Normal file
100
src/web/app/desktop/tags/list-user.tag
Normal file
@@ -0,0 +1,100 @@
|
||||
mk-list-user
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + user.username })
|
||||
img.avatar(src={ user.avatar_url + '?thumbnail&size=64' }, alt='avatar')
|
||||
div.main
|
||||
header
|
||||
div.left
|
||||
a.name(href={ CONFIG.url + '/' + user.username })
|
||||
| { user.name }
|
||||
span.username
|
||||
| @{ user.username }
|
||||
div.body
|
||||
p.followed(if={ user.is_followed }) フォローされています
|
||||
div.bio { user.bio }
|
||||
mk-follow-button(user={ user })
|
||||
|
||||
style.
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
font-size 16px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 16px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 58px
|
||||
height 58px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 74px)
|
||||
|
||||
> header
|
||||
margin-bottom 2px
|
||||
white-space nowrap
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .left
|
||||
float left
|
||||
|
||||
> .name
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
color #777
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color #ccc
|
||||
|
||||
> .body
|
||||
> .followed
|
||||
display inline-block
|
||||
margin 0 0 4px 0
|
||||
padding 2px 8px
|
||||
vertical-align top
|
||||
font-size 10px
|
||||
color #71afc7
|
||||
background #eefaff
|
||||
border-radius 4px
|
||||
|
||||
> .bio
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
word-wrap break-word
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
> mk-follow-button
|
||||
position absolute
|
||||
top 16px
|
||||
right 16px
|
||||
|
||||
script.
|
||||
@user = @opts.user
|
||||
20
src/web/app/desktop/tags/log-window.tag
Normal file
20
src/web/app/desktop/tags/log-window.tag
Normal file
@@ -0,0 +1,20 @@
|
||||
mk-log-window
|
||||
mk-window@window(width={ '600px' }, height={ '400px' })
|
||||
<yield to="header">
|
||||
i.fa.fa-terminal
|
||||
| Log
|
||||
</yield>
|
||||
<yield to="content">
|
||||
mk-log
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
script.
|
||||
@on \mount ~>
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
62
src/web/app/desktop/tags/log.tag
Normal file
62
src/web/app/desktop/tags/log.tag
Normal file
@@ -0,0 +1,62 @@
|
||||
mk-log
|
||||
header
|
||||
button.follow(class={ following: following }, onclick={ follow }) Follow
|
||||
div.logs@logs
|
||||
code(each={ logs })
|
||||
span.date { date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() }
|
||||
span.message { message }
|
||||
|
||||
style.
|
||||
display block
|
||||
height 100%
|
||||
color #fff
|
||||
background #000
|
||||
|
||||
> header
|
||||
height 32px
|
||||
background #343a42
|
||||
|
||||
> button
|
||||
line-height 32px
|
||||
|
||||
> .follow
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
|
||||
&.following
|
||||
color #ff0
|
||||
|
||||
> .logs
|
||||
height calc(100% - 32px)
|
||||
overflow auto
|
||||
|
||||
> code
|
||||
display block
|
||||
padding 4px 8px
|
||||
|
||||
&:hover
|
||||
background rgba(#fff, 0.15)
|
||||
|
||||
> .date
|
||||
margin-right 8px
|
||||
opacity 0.5
|
||||
|
||||
script.
|
||||
@mixin \log
|
||||
|
||||
@following = true
|
||||
|
||||
@on \mount ~>
|
||||
@log-event.on \log @on-log
|
||||
|
||||
@on \unmount ~>
|
||||
@log-event.off \log @on-log
|
||||
|
||||
@follow = ~>
|
||||
@following = true
|
||||
|
||||
@on-log = ~>
|
||||
@update!
|
||||
if @following
|
||||
@refs.logs.scroll-top = @refs.logs.scroll-height
|
||||
162
src/web/app/desktop/tags/messaging/form.tag
Normal file
162
src/web/app/desktop/tags/messaging/form.tag
Normal file
@@ -0,0 +1,162 @@
|
||||
mk-messaging-form
|
||||
textarea@text(onkeypress={ onkeypress }, onpaste={ onpaste }, placeholder='ここにメッセージを入力')
|
||||
div.files
|
||||
mk-uploader@uploader
|
||||
button.send(onclick={ send }, disabled={ sending }, title='メッセージを送信')
|
||||
i.fa.fa-paper-plane(if={ !sending })
|
||||
i.fa.fa-spinner.fa-spin(if={ sending })
|
||||
button.attach-from-local(type='button', title='PCから画像を添付する')
|
||||
i.fa.fa-upload
|
||||
button.attach-from-drive(type='button', title='アルバムから画像を添付する')
|
||||
i.fa.fa-folder-open
|
||||
input(name='file', type='file', accept='image/*')
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> textarea
|
||||
cursor auto
|
||||
display block
|
||||
width 100%
|
||||
min-width 100%
|
||||
max-width 100%
|
||||
height 64px
|
||||
margin 0
|
||||
padding 8px
|
||||
font-size 1em
|
||||
color #000
|
||||
outline none
|
||||
border none
|
||||
border-top solid 1px #eee
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
background transparent
|
||||
|
||||
> .send
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
padding 10px 14px
|
||||
line-height 1em
|
||||
font-size 1em
|
||||
color #aaa
|
||||
transition color 0.1s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 10%)
|
||||
transition color 0s ease
|
||||
|
||||
.files
|
||||
display block
|
||||
margin 0
|
||||
padding 0 8px
|
||||
list-style none
|
||||
|
||||
&:after
|
||||
content ''
|
||||
display block
|
||||
clear both
|
||||
|
||||
> li
|
||||
display block
|
||||
float left
|
||||
margin 4px
|
||||
padding 0
|
||||
width 64px
|
||||
height 64px
|
||||
background-color #eee
|
||||
background-repeat no-repeat
|
||||
background-position center center
|
||||
background-size cover
|
||||
cursor move
|
||||
|
||||
&:hover
|
||||
> .remove
|
||||
display block
|
||||
|
||||
> .remove
|
||||
display none
|
||||
position absolute
|
||||
right -6px
|
||||
top -6px
|
||||
margin 0
|
||||
padding 0
|
||||
background transparent
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
cursor pointer
|
||||
|
||||
.attach-from-local
|
||||
.attach-from-drive
|
||||
margin 0
|
||||
padding 10px 14px
|
||||
line-height 1em
|
||||
font-size 1em
|
||||
font-weight normal
|
||||
text-decoration none
|
||||
color #aaa
|
||||
transition color 0.1s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 10%)
|
||||
transition color 0s ease
|
||||
|
||||
input[type=file]
|
||||
display none
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
|
||||
@user = @opts.user
|
||||
|
||||
@onpaste = (e) ~>
|
||||
data = e.clipboard-data
|
||||
items = data.items
|
||||
for i from 0 to items.length - 1
|
||||
item = items[i]
|
||||
switch (item.kind)
|
||||
| \file =>
|
||||
@upload item.get-as-file!
|
||||
|
||||
@onkeypress = (e) ~>
|
||||
if (e.which == 10 || e.which == 13) && e.ctrl-key
|
||||
@send!
|
||||
|
||||
@select-file = ~>
|
||||
@refs.file.click!
|
||||
|
||||
@select-file-from-drive = ~>
|
||||
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
|
||||
event = riot.observable!
|
||||
riot.mount browser, do
|
||||
multiple: true
|
||||
event: event
|
||||
event.one \selected (files) ~>
|
||||
files.for-each @add-file
|
||||
|
||||
@send = ~>
|
||||
@sending = true
|
||||
@api \messaging/messages/create do
|
||||
user_id: @user.id
|
||||
text: @refs.text.value
|
||||
.then (message) ~>
|
||||
@clear!
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
.then ~>
|
||||
@sending = false
|
||||
@update!
|
||||
|
||||
@clear = ~>
|
||||
@refs.text.value = ''
|
||||
@files = []
|
||||
@update!
|
||||
302
src/web/app/desktop/tags/messaging/index.tag
Normal file
302
src/web/app/desktop/tags/messaging/index.tag
Normal file
@@ -0,0 +1,302 @@
|
||||
mk-messaging
|
||||
div.search
|
||||
div.form
|
||||
label(for='search-input')
|
||||
i.fa.fa-search
|
||||
input@search-input(type='search', oninput={ search }, placeholder='ユーザーを探す')
|
||||
div.result
|
||||
ol.users(if={ search-result.length > 0 })
|
||||
li(each={ user in search-result })
|
||||
a(onclick={ user._click })
|
||||
img.avatar(src={ user.avatar_url + '?thumbnail&size=32' }, alt='')
|
||||
span.name { user.name }
|
||||
span.username @{ user.username }
|
||||
div.main
|
||||
div.history(if={ history.length > 0 })
|
||||
virtual(each={ history })
|
||||
a.user(data-is-me={ is_me }, data-is-read={ is_read }, onclick={ _click }): div
|
||||
img.avatar(src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' }, alt='')
|
||||
header
|
||||
span.name { is_me ? recipient.name : user.name }
|
||||
span.username { '@' + (is_me ? recipient.username : user.username ) }
|
||||
mk-time(time={ created_at })
|
||||
div.body
|
||||
p.text
|
||||
span.me(if={ is_me }) あなた:
|
||||
| { text }
|
||||
p.no-history(if={ history.length == 0 })
|
||||
| 履歴はありません。
|
||||
br
|
||||
| ユーザーを検索して、いつでもメッセージを送受信できます。
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> .search
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
width 100%
|
||||
background #fff
|
||||
box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .form
|
||||
padding 8px
|
||||
background #f7f7f7
|
||||
|
||||
> label
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 8px
|
||||
z-index 1
|
||||
height 100%
|
||||
width 38px
|
||||
pointer-events none
|
||||
|
||||
> i
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
width 1em
|
||||
height 1em
|
||||
margin auto
|
||||
color #555
|
||||
|
||||
> input
|
||||
margin 0
|
||||
padding 0 12px 0 38px
|
||||
width 100%
|
||||
font-size 1em
|
||||
line-height 38px
|
||||
color #000
|
||||
outline none
|
||||
border solid 1px #eee
|
||||
border-radius 5px
|
||||
box-shadow none
|
||||
transition color 0.5s ease, border 0.5s ease
|
||||
|
||||
&:hover
|
||||
border solid 1px #ddd
|
||||
transition border 0.2s ease
|
||||
|
||||
&:focus
|
||||
color darken($theme-color, 20%)
|
||||
border solid 1px $theme-color
|
||||
transition color 0, border 0
|
||||
|
||||
> .result
|
||||
display block
|
||||
top 0
|
||||
left 0
|
||||
z-index 2
|
||||
width 100%
|
||||
margin 0
|
||||
padding 0
|
||||
background #fff
|
||||
|
||||
> .users
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
> a
|
||||
display inline-block
|
||||
z-index 1
|
||||
width 100%
|
||||
padding 8px 32px
|
||||
vertical-align top
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
text-decoration none
|
||||
transition none
|
||||
|
||||
&:hover
|
||||
color #fff
|
||||
background $theme-color
|
||||
|
||||
.name
|
||||
color #fff
|
||||
|
||||
.username
|
||||
color #fff
|
||||
|
||||
&:active
|
||||
color #fff
|
||||
background darken($theme-color, 10%)
|
||||
|
||||
.name
|
||||
color #fff
|
||||
|
||||
.username
|
||||
color #fff
|
||||
|
||||
.avatar
|
||||
vertical-align middle
|
||||
min-width 32px
|
||||
min-height 32px
|
||||
max-width 32px
|
||||
max-height 32px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
.name
|
||||
margin 0 8px 0 0
|
||||
/*font-weight bold*/
|
||||
font-weight normal
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
.username
|
||||
font-weight normal
|
||||
color rgba(0, 0, 0, 0.3)
|
||||
|
||||
> .main
|
||||
padding-top 56px
|
||||
|
||||
> .history
|
||||
|
||||
> a
|
||||
display block
|
||||
padding 20px 30px
|
||||
text-decoration none
|
||||
background #fff
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
&:hover
|
||||
background #fafafa
|
||||
|
||||
> .avatar
|
||||
filter saturate(200%)
|
||||
|
||||
&:active
|
||||
background #eee
|
||||
|
||||
&[data-is-read]
|
||||
&[data-is-me]
|
||||
opacity 0.8
|
||||
|
||||
&:not([data-is-me]):not([data-is-read])
|
||||
background-image url("/_/resources/desktop/unread.svg")
|
||||
background-repeat no-repeat
|
||||
background-position 0 center
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> div
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
|
||||
> header
|
||||
margin-bottom 2px
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
|
||||
> .name
|
||||
text-align left
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.9)
|
||||
font-weight bold
|
||||
transition all 0.1s ease
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
|
||||
> mk-time
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
display inline
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
font-size small
|
||||
|
||||
> .avatar
|
||||
float left
|
||||
width 54px
|
||||
height 54px
|
||||
margin 0 16px 0 0
|
||||
border-radius 8px
|
||||
transition all 0.1s ease
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
display block
|
||||
margin 0 0 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
word-wrap break-word
|
||||
font-size 1.1em
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
.me
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
> .image
|
||||
display block
|
||||
max-width 100%
|
||||
max-height 512px
|
||||
|
||||
> .no-history
|
||||
margin 0
|
||||
padding 2em 1em
|
||||
text-align center
|
||||
color #999
|
||||
font-weight 500
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \api
|
||||
|
||||
@search-result = []
|
||||
|
||||
@on \mount ~>
|
||||
@api \messaging/history
|
||||
.then (history) ~>
|
||||
@is-loading = false
|
||||
history.for-each (message) ~>
|
||||
message.is_me = message.user_id == @I.id
|
||||
message._click = ~>
|
||||
if message.is_me
|
||||
@trigger \navigate-user message.recipient
|
||||
else
|
||||
@trigger \navigate-user message.user
|
||||
@history = history
|
||||
@update!
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
|
||||
@search = ~>
|
||||
q = @refs.search-input.value
|
||||
if q == ''
|
||||
@search-result = []
|
||||
else
|
||||
@api \users/search do
|
||||
query: q
|
||||
.then (users) ~>
|
||||
users.for-each (user) ~>
|
||||
user._click = ~>
|
||||
@trigger \navigate-user user
|
||||
@search-result = []
|
||||
@search-result = users
|
||||
@update!
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
227
src/web/app/desktop/tags/messaging/message.tag
Normal file
227
src/web/app/desktop/tags/messaging/message.tag
Normal file
@@ -0,0 +1,227 @@
|
||||
mk-messaging-message(data-is-me={ message.is_me })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + message.user.username }, title={ message.user.username }, target='_blank')
|
||||
img.avatar(src={ message.user.avatar_url + '?thumbnail&size=64' }, alt='')
|
||||
div.content-container
|
||||
div.balloon
|
||||
p.read(if={ message.is_me && message.is_read }) 既読
|
||||
button.delete-button(if={ message.is_me }, title='メッセージを削除')
|
||||
img(src='/_/resources/desktop/messaging/delete.png', alt='Delete')
|
||||
div.content(if={ !message.is_deleted })
|
||||
div@text
|
||||
div.image(if={ message.file })
|
||||
img(src={ message.file.url }, alt='image', title={ message.file.name })
|
||||
div.content(if={ message.is_deleted })
|
||||
p.is-deleted このメッセージは削除されました
|
||||
footer
|
||||
mk-time(time={ message.created_at })
|
||||
i.fa.fa-pencil.is-edited(if={ message.is_edited })
|
||||
|
||||
style.
|
||||
$me-balloon-color = #23A7B6
|
||||
|
||||
display block
|
||||
padding 10px 12px 10px 12px
|
||||
background-color transparent
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
min-width 54px
|
||||
min-height 54px
|
||||
max-width 54px
|
||||
max-height 54px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
transition all 0.1s ease
|
||||
|
||||
> .content-container
|
||||
display block
|
||||
margin 0 12px
|
||||
padding 0
|
||||
max-width calc(100% - 78px)
|
||||
|
||||
> .balloon
|
||||
display block
|
||||
float inherit
|
||||
margin 0
|
||||
padding 0
|
||||
max-width 100%
|
||||
min-height 38px
|
||||
border-radius 16px
|
||||
|
||||
&:before
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top 12px
|
||||
|
||||
&:hover
|
||||
> .delete-button
|
||||
display block
|
||||
|
||||
> .delete-button
|
||||
display none
|
||||
position absolute
|
||||
z-index 1
|
||||
top -4px
|
||||
right -4px
|
||||
margin 0
|
||||
padding 0
|
||||
cursor pointer
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
background transparent
|
||||
|
||||
> img
|
||||
vertical-align bottom
|
||||
width 16px
|
||||
height 16px
|
||||
cursor pointer
|
||||
|
||||
> .read
|
||||
user-select none
|
||||
display block
|
||||
position absolute
|
||||
z-index 1
|
||||
bottom -4px
|
||||
left -12px
|
||||
margin 0
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
font-size 11px
|
||||
|
||||
> .content
|
||||
|
||||
> .is-deleted
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
word-wrap break-word
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
|
||||
> [ref='text']
|
||||
display block
|
||||
margin 0
|
||||
padding 8px 16px
|
||||
overflow hidden
|
||||
word-wrap break-word
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
&, *
|
||||
user-select text
|
||||
cursor auto
|
||||
|
||||
& + .file
|
||||
&.image
|
||||
> img
|
||||
border-radius 0 0 16px 16px
|
||||
|
||||
> .file
|
||||
&.image
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
max-height 512px
|
||||
border-radius 16px
|
||||
|
||||
> footer
|
||||
display block
|
||||
clear both
|
||||
margin 0
|
||||
padding 2px
|
||||
font-size 10px
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
> .is-edited
|
||||
margin-left 4px
|
||||
|
||||
&:not([data-is-me='true'])
|
||||
> .avatar-anchor
|
||||
float left
|
||||
|
||||
> .content-container
|
||||
float left
|
||||
|
||||
> .balloon
|
||||
background #eee
|
||||
|
||||
&:before
|
||||
left -14px
|
||||
border-top solid 8px transparent
|
||||
border-right solid 8px #eee
|
||||
border-bottom solid 8px transparent
|
||||
border-left solid 8px transparent
|
||||
|
||||
> footer
|
||||
text-align left
|
||||
|
||||
&[data-is-me='true']
|
||||
> .avatar-anchor
|
||||
float right
|
||||
|
||||
> .content-container
|
||||
float right
|
||||
|
||||
> .balloon
|
||||
background $me-balloon-color
|
||||
|
||||
&:before
|
||||
right -14px
|
||||
left auto
|
||||
border-top solid 8px transparent
|
||||
border-right solid 8px transparent
|
||||
border-bottom solid 8px transparent
|
||||
border-left solid 8px $me-balloon-color
|
||||
|
||||
> .content
|
||||
|
||||
> p.is-deleted
|
||||
color rgba(255, 255, 255, 0.5)
|
||||
|
||||
> [ref='text']
|
||||
&, *
|
||||
color #fff !important
|
||||
|
||||
> footer
|
||||
text-align right
|
||||
|
||||
&[data-is-deleted='true']
|
||||
> .content-container
|
||||
opacity 0.5
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \text
|
||||
|
||||
@message = @opts.message
|
||||
@message.is_me = @message.user.id == @I.id
|
||||
|
||||
@on \mount ~>
|
||||
if @message.text?
|
||||
tokens = @analyze @message.text
|
||||
|
||||
@refs.text.innerHTML = @compile tokens
|
||||
|
||||
@refs.text.children.for-each (e) ~>
|
||||
if e.tag-name == \MK-URL
|
||||
riot.mount e
|
||||
|
||||
# URLをプレビュー
|
||||
tokens
|
||||
.filter (t) -> t.type == \link
|
||||
.map (t) ~>
|
||||
@preview = @refs.text.append-child document.create-element \mk-url-preview
|
||||
riot.mount @preview, do
|
||||
url: t.content
|
||||
26
src/web/app/desktop/tags/messaging/room-window.tag
Normal file
26
src/web/app/desktop/tags/messaging/room-window.tag
Normal file
@@ -0,0 +1,26 @@
|
||||
mk-messaging-room-window
|
||||
mk-window@window(is-modal={ false }, width={ '500px' }, height={ '560px' })
|
||||
<yield to="header">
|
||||
i.fa.fa-comments
|
||||
| メッセージ: { parent.user.name }
|
||||
</yield>
|
||||
<yield to="content">
|
||||
mk-messaging-room(user={ parent.user })
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
[data-yield='content']
|
||||
> mk-messaging-room
|
||||
height 100%
|
||||
|
||||
script.
|
||||
@user = @opts.user
|
||||
|
||||
@on \mount ~>
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
227
src/web/app/desktop/tags/messaging/room.tag
Normal file
227
src/web/app/desktop/tags/messaging/room.tag
Normal file
@@ -0,0 +1,227 @@
|
||||
mk-messaging-room
|
||||
div.stream@stream
|
||||
p.initializing(if={ init })
|
||||
i.fa.fa-spinner.fa-spin
|
||||
| 読み込み中
|
||||
p.empty(if={ !init && messages.length == 0 })
|
||||
i.fa.fa-info-circle
|
||||
| このユーザーとまだ会話したことがありません
|
||||
virtual(each={ message, i in messages })
|
||||
mk-messaging-message(message={ message })
|
||||
p.date(if={ i != messages.length - 1 && message._date != messages[i + 1]._date })
|
||||
span { messages[i + 1]._datetext }
|
||||
|
||||
div.typings
|
||||
footer
|
||||
div@notifications
|
||||
div.grippie(title='ドラッグしてフォームの広さを調整')
|
||||
mk-messaging-form(user={ user })
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> .stream
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height calc(100% - 100px)
|
||||
overflow auto
|
||||
|
||||
> .empty
|
||||
width 100%
|
||||
margin 0
|
||||
padding 16px 8px 8px 8px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
i
|
||||
margin-right 4px
|
||||
|
||||
> .no-history
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
i
|
||||
margin-right 4px
|
||||
|
||||
> .message
|
||||
// something
|
||||
|
||||
> .date
|
||||
display block
|
||||
margin 8px 0
|
||||
text-align center
|
||||
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
position absolute
|
||||
height 1px
|
||||
width 90%
|
||||
top 16px
|
||||
left 0
|
||||
right 0
|
||||
margin 0 auto
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
|
||||
> span
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 16px
|
||||
//font-weight bold
|
||||
line-height 32px
|
||||
color rgba(0, 0, 0, 0.3)
|
||||
background #fff
|
||||
|
||||
> footer
|
||||
position absolute
|
||||
z-index 2
|
||||
bottom 0
|
||||
width 600px
|
||||
max-width 100%
|
||||
margin 0 auto
|
||||
padding 0
|
||||
background rgba(255, 255, 255, 0.95)
|
||||
background-clip content-box
|
||||
|
||||
> [ref='notifications']
|
||||
position absolute
|
||||
top -48px
|
||||
width 100%
|
||||
padding 8px 0
|
||||
text-align center
|
||||
|
||||
> p
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 12px 0 28px
|
||||
cursor pointer
|
||||
line-height 32px
|
||||
font-size 12px
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
border-radius 16px
|
||||
transition opacity 1s ease
|
||||
|
||||
> i
|
||||
position absolute
|
||||
top 0
|
||||
left 10px
|
||||
line-height 32px
|
||||
font-size 16px
|
||||
|
||||
> .grippie
|
||||
height 10px
|
||||
margin-top -10px
|
||||
background transparent
|
||||
cursor ns-resize
|
||||
|
||||
&:hover
|
||||
//background rgba(0, 0, 0, 0.1)
|
||||
|
||||
&:active
|
||||
//background rgba(0, 0, 0, 0.2)
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \api
|
||||
@mixin \messaging-stream
|
||||
|
||||
@user = @opts.user
|
||||
@init = true
|
||||
@sending = false
|
||||
@messages = []
|
||||
|
||||
@connection = new @MessagingStreamConnection @I, @user.id
|
||||
|
||||
@on \mount ~>
|
||||
@connection.event.on \message @on-message
|
||||
@connection.event.on \read @on-read
|
||||
|
||||
document.add-event-listener \visibilitychange @on-visibilitychange
|
||||
|
||||
@api \messaging/messages do
|
||||
user_id: @user.id
|
||||
.then (messages) ~>
|
||||
@init = false
|
||||
@messages = messages.reverse!
|
||||
@update!
|
||||
@scroll-to-bottom!
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
|
||||
@on \unmount ~>
|
||||
@connection.event.off \message @on-message
|
||||
@connection.event.off \read @on-read
|
||||
@connection.close!
|
||||
|
||||
document.remove-event-listener \visibilitychange @on-visibilitychange
|
||||
|
||||
@on \update ~>
|
||||
@messages.for-each (message) ~>
|
||||
date = (new Date message.created_at).get-date!
|
||||
month = (new Date message.created_at).get-month! + 1
|
||||
message._date = date
|
||||
message._datetext = month + '月 ' + date + '日'
|
||||
|
||||
@on-message = (message) ~>
|
||||
is-bottom = @is-bottom!
|
||||
|
||||
@messages.push message
|
||||
if message.user_id != @I.id and not document.hidden
|
||||
@connection.socket.send JSON.stringify do
|
||||
type: \read
|
||||
id: message.id
|
||||
@update!
|
||||
|
||||
if is-bottom
|
||||
# Scroll to bottom
|
||||
@scroll-to-bottom!
|
||||
else if message.user_id != @I.id
|
||||
# Notify
|
||||
@notify '新しいメッセージがあります'
|
||||
|
||||
@on-read = (ids) ~>
|
||||
if not Array.isArray ids then ids = [ids]
|
||||
ids.for-each (id) ~>
|
||||
if (@messages.some (x) ~> x.id == id)
|
||||
exist = (@messages.map (x) -> x.id).index-of id
|
||||
@messages[exist].is_read = true
|
||||
@update!
|
||||
|
||||
@is-bottom = ~>
|
||||
current = @refs.stream.scroll-top + @refs.stream.offset-height
|
||||
max = @refs.stream.scroll-height
|
||||
current > (max - 32)
|
||||
|
||||
@scroll-to-bottom = ~>
|
||||
@refs.stream.scroll-top = @refs.stream.scroll-height
|
||||
|
||||
@notify = (message) ~>
|
||||
n = document.create-element \p
|
||||
n.inner-HTML = '<i class="fa fa-arrow-circle-down"></i>' + message
|
||||
n.onclick = ~>
|
||||
@scroll-to-bottom!
|
||||
n.parent-node.remove-child n
|
||||
@refs.notifications.append-child n
|
||||
|
||||
set-timeout ~>
|
||||
n.style.opacity = 0
|
||||
set-timeout ~>
|
||||
n.parent-node.remove-child n
|
||||
, 1000ms
|
||||
, 4000ms
|
||||
|
||||
@on-visibilitychange = ~>
|
||||
if document.hidden then return
|
||||
@messages.for-each (message) ~>
|
||||
if message.user_id != @I.id and not message.is_read
|
||||
@connection.socket.send JSON.stringify do
|
||||
type: \read
|
||||
id: message.id
|
||||
29
src/web/app/desktop/tags/messaging/window.tag
Normal file
29
src/web/app/desktop/tags/messaging/window.tag
Normal file
@@ -0,0 +1,29 @@
|
||||
mk-messaging-window
|
||||
mk-window@window(is-modal={ false }, width={ '500px' }, height={ '560px' })
|
||||
<yield to="header">
|
||||
i.fa.fa-comments
|
||||
| メッセージ
|
||||
</yield>
|
||||
<yield to="content">
|
||||
mk-messaging@index
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
[data-yield='content']
|
||||
> mk-messaging
|
||||
height 100%
|
||||
|
||||
script.
|
||||
@on \mount ~>
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@refs.window.refs.index.on \navigate-user (user) ~>
|
||||
w = document.body.append-child document.create-element \mk-messaging-room-window
|
||||
riot.mount w, do
|
||||
user: user
|
||||
226
src/web/app/desktop/tags/notifications.tag
Normal file
226
src/web/app/desktop/tags/notifications.tag
Normal file
@@ -0,0 +1,226 @@
|
||||
mk-notifications
|
||||
div.notifications(if={ notifications.length != 0 })
|
||||
virtual(each={ notification, i in notifications })
|
||||
div.notification(class={ notification.type })
|
||||
mk-time(time={ notification.created_at })
|
||||
|
||||
div.main(if={ notification.type == 'like' })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id })
|
||||
img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
|
||||
div.text
|
||||
p
|
||||
i.fa.fa-thumbs-o-up
|
||||
a(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id }) { notification.user.name }
|
||||
a.post-ref(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
|
||||
|
||||
div.main(if={ notification.type == 'repost' })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
|
||||
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
|
||||
div.text
|
||||
p
|
||||
i.fa.fa-retweet
|
||||
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
|
||||
a.post-ref(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post.repost) }
|
||||
|
||||
div.main(if={ notification.type == 'quote' })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
|
||||
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
|
||||
div.text
|
||||
p
|
||||
i.fa.fa-quote-left
|
||||
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
|
||||
a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
|
||||
|
||||
div.main(if={ notification.type == 'follow' })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id })
|
||||
img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
|
||||
div.text
|
||||
p
|
||||
i.fa.fa-user-plus
|
||||
a(href={ CONFIG.url + '/' + notification.user.username }, data-user-preview={ notification.user.id }) { notification.user.name }
|
||||
|
||||
div.main(if={ notification.type == 'reply' })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
|
||||
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
|
||||
div.text
|
||||
p
|
||||
i.fa.fa-reply
|
||||
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
|
||||
a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
|
||||
|
||||
div.main(if={ notification.type == 'mention' })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id })
|
||||
img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=48' }, alt='avatar')
|
||||
div.text
|
||||
p
|
||||
i.fa.fa-at
|
||||
a(href={ CONFIG.url + '/' + notification.post.user.username }, data-user-preview={ notification.post.user_id }) { notification.post.user.name }
|
||||
a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) }
|
||||
|
||||
p.date(if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date })
|
||||
span
|
||||
i.fa.fa-angle-up
|
||||
| { notification._datetext }
|
||||
span
|
||||
i.fa.fa-angle-down
|
||||
| { notifications[i + 1]._datetext }
|
||||
|
||||
p.empty(if={ notifications.length == 0 && !loading })
|
||||
| ありません!
|
||||
p.loading(if={ loading })
|
||||
i.fa.fa-spinner.fa-pulse.fa-fw
|
||||
| 読み込んでいます
|
||||
mk-ellipsis
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> .notifications
|
||||
> .notification
|
||||
margin 0
|
||||
padding 16px
|
||||
font-size 0.9em
|
||||
border-bottom solid 1px rgba(0, 0, 0, 0.05)
|
||||
|
||||
&:last-child
|
||||
border-bottom none
|
||||
|
||||
> mk-time
|
||||
display inline
|
||||
position absolute
|
||||
top 16px
|
||||
right 12px
|
||||
vertical-align top
|
||||
color rgba(0, 0, 0, 0.6)
|
||||
font-size small
|
||||
|
||||
> .main
|
||||
word-wrap break-word
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
.avatar-anchor
|
||||
display block
|
||||
float left
|
||||
|
||||
img
|
||||
min-width 36px
|
||||
min-height 36px
|
||||
max-width 36px
|
||||
max-height 36px
|
||||
border-radius 6px
|
||||
|
||||
.text
|
||||
float right
|
||||
width calc(100% - 36px)
|
||||
padding-left 8px
|
||||
|
||||
p
|
||||
margin 0
|
||||
|
||||
i
|
||||
margin-right 4px
|
||||
|
||||
.post-preview
|
||||
color rgba(0, 0, 0, 0.7)
|
||||
|
||||
.post-ref
|
||||
color rgba(0, 0, 0, 0.7)
|
||||
|
||||
&:before, &:after
|
||||
font-family FontAwesome
|
||||
font-size 1em
|
||||
font-weight normal
|
||||
font-style normal
|
||||
display inline-block
|
||||
margin-right 3px
|
||||
|
||||
&:before
|
||||
content "\f10d"
|
||||
|
||||
&:after
|
||||
content "\f10e"
|
||||
|
||||
&.like
|
||||
.text p i
|
||||
color #FFAC33
|
||||
|
||||
&.repost, &.quote
|
||||
.text p i
|
||||
color #77B255
|
||||
|
||||
&.follow
|
||||
.text p i
|
||||
color #53c7ce
|
||||
|
||||
&.reply, &.mention
|
||||
.text p i
|
||||
color #555
|
||||
|
||||
> .date
|
||||
display block
|
||||
margin 0
|
||||
line-height 32px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color #aaa
|
||||
background #fdfdfd
|
||||
border-bottom solid 1px rgba(0, 0, 0, 0.05)
|
||||
|
||||
span
|
||||
margin 0 16px
|
||||
|
||||
i
|
||||
margin-right 8px
|
||||
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .loading
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \stream
|
||||
@mixin \user-preview
|
||||
@mixin \get-post-summary
|
||||
|
||||
@notifications = []
|
||||
@loading = true
|
||||
|
||||
@on \mount ~>
|
||||
@api \i/notifications
|
||||
.then (notifications) ~>
|
||||
@notifications = notifications
|
||||
@loading = false
|
||||
@update!
|
||||
.catch (err, text-status) ->
|
||||
console.error err
|
||||
|
||||
@stream.on \notification @on-notification
|
||||
|
||||
@on \unmount ~>
|
||||
@stream.off \notification @on-notification
|
||||
|
||||
@on-notification = (notification) ~>
|
||||
@notifications.unshift notification
|
||||
@update!
|
||||
|
||||
@on \update ~>
|
||||
@notifications.for-each (notification) ~>
|
||||
date = (new Date notification.created_at).get-date!
|
||||
month = (new Date notification.created_at).get-month! + 1
|
||||
notification._date = date
|
||||
notification._datetext = month + '月 ' + date + '日'
|
||||
77
src/web/app/desktop/tags/pages/entrance.tag
Normal file
77
src/web/app/desktop/tags/pages/entrance.tag
Normal file
@@ -0,0 +1,77 @@
|
||||
mk-entrance
|
||||
main
|
||||
img(src='/_/resources/title.svg', alt='Misskey')
|
||||
|
||||
mk-entrance-signin(if={ mode == 'signin' })
|
||||
mk-entrance-signup(if={ mode == 'signup' })
|
||||
div.introduction(if={ mode == 'introduction' })
|
||||
mk-introduction
|
||||
button(onclick={ signin }) わかった
|
||||
|
||||
mk-forkit
|
||||
|
||||
footer
|
||||
mk-copyright
|
||||
|
||||
// ↓ https://github.com/riot/riot/issues/2134 (将来的)
|
||||
style(data-disable-scope).
|
||||
#wait {
|
||||
right: auto;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
style.
|
||||
display block
|
||||
height 100%
|
||||
|
||||
> main
|
||||
display block
|
||||
|
||||
> img
|
||||
display block
|
||||
width 160px
|
||||
height 170px
|
||||
margin 0 auto
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
> .introduction
|
||||
max-width 360px
|
||||
margin 0 auto
|
||||
color #777
|
||||
|
||||
> mk-introduction
|
||||
padding 32px
|
||||
background #fff
|
||||
box-shadow 0 4px 16px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> button
|
||||
display block
|
||||
margin 16px auto 0 auto
|
||||
color #666
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> footer
|
||||
> mk-copyright
|
||||
margin 0
|
||||
text-align center
|
||||
line-height 64px
|
||||
font-size 10px
|
||||
color rgba(#000, 0.5)
|
||||
|
||||
script.
|
||||
@mode = \signin
|
||||
|
||||
@signup = ~>
|
||||
@mode = \signup
|
||||
@update!
|
||||
|
||||
@signin = ~>
|
||||
@mode = \signin
|
||||
@update!
|
||||
|
||||
@introduction = ~>
|
||||
@mode = \introduction
|
||||
@update!
|
||||
128
src/web/app/desktop/tags/pages/entrance/signin.tag
Normal file
128
src/web/app/desktop/tags/pages/entrance/signin.tag
Normal file
@@ -0,0 +1,128 @@
|
||||
mk-entrance-signin
|
||||
a.help(href={ CONFIG.urls.about + '/help' }, title='お困りですか?'): i.fa.fa-question
|
||||
div.form
|
||||
h1
|
||||
img(if={ user }, src={ user.avatar_url + '?thumbnail&size=32' })
|
||||
p { user ? user.name : 'アカウント' }
|
||||
mk-signin@signin
|
||||
div.divider: span or
|
||||
button.signup(onclick={ parent.signup }) 新規登録
|
||||
a.introduction(onclick={ introduction }) Misskeyについて
|
||||
|
||||
style.
|
||||
display block
|
||||
width 290px
|
||||
margin 0 auto
|
||||
text-align center
|
||||
|
||||
&:hover
|
||||
> .help
|
||||
opacity 1
|
||||
|
||||
> .help
|
||||
cursor pointer
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.2em
|
||||
color #999
|
||||
border none
|
||||
outline none
|
||||
background transparent
|
||||
opacity 0
|
||||
transition opacity 0.1s ease
|
||||
|
||||
&:hover
|
||||
color #444
|
||||
|
||||
&:active
|
||||
color #222
|
||||
|
||||
> i
|
||||
padding 14px
|
||||
|
||||
> .form
|
||||
padding 10px 28px 16px 28px
|
||||
background #fff
|
||||
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
height 54px
|
||||
line-height 54px
|
||||
text-align center
|
||||
text-transform uppercase
|
||||
font-size 1em
|
||||
font-weight bold
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
border-bottom solid 1px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> p
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
top 10px
|
||||
width 32px
|
||||
height 32px
|
||||
margin-right 8px
|
||||
border-radius 100%
|
||||
|
||||
&[src='']
|
||||
display none
|
||||
|
||||
> .divider
|
||||
padding 16px 0
|
||||
text-align center
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top 50%
|
||||
width 100%
|
||||
height 1px
|
||||
border-top solid 1px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> *
|
||||
z-index 1
|
||||
padding 0 8px
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
background #fdfdfd
|
||||
|
||||
> .signup
|
||||
width 100%
|
||||
line-height 56px
|
||||
font-size 1em
|
||||
color #fff
|
||||
background $theme-color
|
||||
border-radius 64px
|
||||
|
||||
&:hover
|
||||
background lighten($theme-color, 5%)
|
||||
|
||||
&:active
|
||||
background darken($theme-color, 5%)
|
||||
|
||||
> .introduction
|
||||
display inline-block
|
||||
margin-top 16px
|
||||
font-size 12px
|
||||
color #666
|
||||
|
||||
script.
|
||||
@on \mount ~>
|
||||
@refs.signin.on \user (user) ~>
|
||||
@update do
|
||||
user: user
|
||||
|
||||
@introduction = ~>
|
||||
@parent.introduction!
|
||||
44
src/web/app/desktop/tags/pages/entrance/signup.tag
Normal file
44
src/web/app/desktop/tags/pages/entrance/signup.tag
Normal file
@@ -0,0 +1,44 @@
|
||||
mk-entrance-signup
|
||||
mk-signup
|
||||
button.cancel(type='button', onclick={ parent.signin }, title='キャンセル'): i.fa.fa-times
|
||||
|
||||
style.
|
||||
display block
|
||||
width 368px
|
||||
margin 0 auto
|
||||
|
||||
&:hover
|
||||
> .cancel
|
||||
opacity 1
|
||||
|
||||
> mk-signup
|
||||
padding 18px 32px 0 32px
|
||||
background #fff
|
||||
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .cancel
|
||||
cursor pointer
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.2em
|
||||
color #999
|
||||
border none
|
||||
outline none
|
||||
box-shadow none
|
||||
background transparent
|
||||
opacity 0
|
||||
transition opacity 0.1s ease
|
||||
|
||||
&:hover
|
||||
color #555
|
||||
|
||||
&:active
|
||||
color #222
|
||||
|
||||
> i
|
||||
padding 14px
|
||||
51
src/web/app/desktop/tags/pages/home.tag
Normal file
51
src/web/app/desktop/tags/pages/home.tag
Normal file
@@ -0,0 +1,51 @@
|
||||
mk-home-page
|
||||
mk-ui@ui(page={ page }): mk-home@home(mode={ parent.opts.mode })
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
background-position center center
|
||||
background-attachment fixed
|
||||
background-size cover
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \api
|
||||
@mixin \ui-progress
|
||||
@mixin \stream
|
||||
@mixin \get-post-summary
|
||||
|
||||
@unread-count = 0
|
||||
|
||||
@page = switch @opts.mode
|
||||
| \timelie => \home
|
||||
| \mentions => \mentions
|
||||
| _ => \home
|
||||
|
||||
@on \mount ~>
|
||||
@refs.ui.refs.home.on \loaded ~>
|
||||
@Progress.done!
|
||||
|
||||
document.title = 'Misskey'
|
||||
if @I.data.wallpaper
|
||||
@api \drive/files/show do
|
||||
file_id: @I.data.wallpaper
|
||||
.then (file) ~>
|
||||
@root.style.background-image = 'url(' + file.url + ')'
|
||||
@Progress.start!
|
||||
@stream.on \post @on-stream-post
|
||||
document.add-event-listener \visibilitychange @window-on-visibilitychange, false
|
||||
|
||||
@on \unmount ~>
|
||||
@stream.off \post @on-stream-post
|
||||
document.remove-event-listener \visibilitychange @window-on-visibilitychange
|
||||
|
||||
@on-stream-post = (post) ~>
|
||||
if document.hidden and post.user_id !== @I.id
|
||||
@unread-count++
|
||||
document.title = '(' + @unread-count + ') ' + @get-post-summary post
|
||||
|
||||
@window-on-visibilitychange = ~>
|
||||
if !document.hidden
|
||||
@unread-count = 0
|
||||
document.title = 'Misskey'
|
||||
46
src/web/app/desktop/tags/pages/not-found.tag
Normal file
46
src/web/app/desktop/tags/pages/not-found.tag
Normal file
@@ -0,0 +1,46 @@
|
||||
mk-not-found
|
||||
mk-ui
|
||||
main
|
||||
h1 Not Found
|
||||
img(src='/_/resources/rogge.jpg', alt='')
|
||||
div.mask
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
main
|
||||
display block
|
||||
width 600px
|
||||
margin 32px auto
|
||||
|
||||
> img
|
||||
display block
|
||||
width 600px
|
||||
height 459px
|
||||
pointer-events none
|
||||
user-select none
|
||||
border-radius 16px
|
||||
box-shadow 0 0 16px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
position absolute
|
||||
top 260px
|
||||
left 225px
|
||||
transform rotate(-12deg)
|
||||
z-index 2
|
||||
color #444
|
||||
font-size 24px
|
||||
line-height 20px
|
||||
|
||||
> .mask
|
||||
position absolute
|
||||
top 262px
|
||||
left 217px
|
||||
width 126px
|
||||
height 18px
|
||||
transform rotate(-12deg)
|
||||
background #D6D5DA
|
||||
border-radius 2px 6px 7px 6px
|
||||
25
src/web/app/desktop/tags/pages/post.tag
Normal file
25
src/web/app/desktop/tags/pages/post.tag
Normal file
@@ -0,0 +1,25 @@
|
||||
mk-post-page
|
||||
mk-ui@ui: main: mk-post-detail@detail(post={ parent.post })
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
main
|
||||
padding 16px
|
||||
|
||||
> mk-post-detail
|
||||
margin 0 auto
|
||||
|
||||
script.
|
||||
@mixin \ui-progress
|
||||
|
||||
@post = @opts.post
|
||||
|
||||
@on \mount ~>
|
||||
@Progress.start!
|
||||
|
||||
@refs.ui.refs.detail.on \post-fetched ~>
|
||||
@Progress.set 0.5
|
||||
|
||||
@refs.ui.refs.detail.on \loaded ~>
|
||||
@Progress.done!
|
||||
14
src/web/app/desktop/tags/pages/search.tag
Normal file
14
src/web/app/desktop/tags/pages/search.tag
Normal file
@@ -0,0 +1,14 @@
|
||||
mk-search-page
|
||||
mk-ui@ui: mk-search@search(query={ parent.opts.query })
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
script.
|
||||
@mixin \ui-progress
|
||||
|
||||
@on \mount ~>
|
||||
@Progress.start!
|
||||
|
||||
@refs.ui.refs.search.on \loaded ~>
|
||||
@Progress.done!
|
||||
20
src/web/app/desktop/tags/pages/user.tag
Normal file
20
src/web/app/desktop/tags/pages/user.tag
Normal file
@@ -0,0 +1,20 @@
|
||||
mk-user-page
|
||||
mk-ui@ui: mk-user@user(user={ parent.user }, page={ parent.opts.page })
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
script.
|
||||
@mixin \ui-progress
|
||||
|
||||
@user = @opts.user
|
||||
|
||||
@on \mount ~>
|
||||
@Progress.start!
|
||||
|
||||
@refs.ui.refs.user.on \user-fetched (user) ~>
|
||||
@Progress.set 0.5
|
||||
document.title = user.name + ' | Misskey'
|
||||
|
||||
@refs.ui.refs.user.on \loaded ~>
|
||||
@Progress.done!
|
||||
141
src/web/app/desktop/tags/post-detail-sub.tag
Normal file
141
src/web/app/desktop/tags/post-detail-sub.tag
Normal file
@@ -0,0 +1,141 @@
|
||||
mk-post-detail-sub(title={ title })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username })
|
||||
img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ post.user_id })
|
||||
div.main
|
||||
header
|
||||
div.left
|
||||
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id })
|
||||
| { post.user.name }
|
||||
span.username
|
||||
| @{ post.user.username }
|
||||
div.right
|
||||
a.time(href={ url })
|
||||
mk-time(time={ post.created_at })
|
||||
div.body
|
||||
div.text@text
|
||||
div.media(if={ post.media })
|
||||
virtual(each={ file in post.media })
|
||||
img(src={ file.url + '?thumbnail&size=512' }, alt={ file.name }, title={ file.name })
|
||||
|
||||
style.
|
||||
display block
|
||||
margin 0
|
||||
padding 20px 32px
|
||||
background #fdfdfd
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 16px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 44px
|
||||
height 44px
|
||||
margin 0
|
||||
border-radius 4px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 60px)
|
||||
|
||||
> header
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .left
|
||||
float left
|
||||
|
||||
> .name
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
color #777
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color #ccc
|
||||
|
||||
> .right
|
||||
float right
|
||||
|
||||
> .time
|
||||
font-size 0.9em
|
||||
color #c0c0c0
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
word-wrap break-word
|
||||
font-size 1em
|
||||
color #717171
|
||||
|
||||
> mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .media
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \text
|
||||
@mixin \date-stringify
|
||||
@mixin \user-preview
|
||||
|
||||
@post = @opts.post
|
||||
|
||||
@url = CONFIG.url + '/' + @post.user.username + '/' + @post.id
|
||||
|
||||
@title = @date-stringify @post.created_at
|
||||
|
||||
@on \mount ~>
|
||||
if @post.text?
|
||||
tokens = @analyze @post.text
|
||||
@refs.text.innerHTML = @compile tokens
|
||||
|
||||
@refs.text.children.for-each (e) ~>
|
||||
if e.tag-name == \MK-URL
|
||||
riot.mount e
|
||||
|
||||
@like = ~>
|
||||
if @post.is_liked
|
||||
@api \posts/likes/delete do
|
||||
post_id: @post.id
|
||||
.then ~>
|
||||
@post.is_liked = false
|
||||
@update!
|
||||
else
|
||||
@api \posts/likes/create do
|
||||
post_id: @post.id
|
||||
.then ~>
|
||||
@post.is_liked = true
|
||||
@update!
|
||||
415
src/web/app/desktop/tags/post-detail.tag
Normal file
415
src/web/app/desktop/tags/post-detail.tag
Normal file
@@ -0,0 +1,415 @@
|
||||
mk-post-detail(title={ title })
|
||||
|
||||
div.fetching(if={ fetching })
|
||||
mk-ellipsis-icon
|
||||
|
||||
div.main(if={ !fetching })
|
||||
|
||||
button.read-more(if={ p.reply_to && p.reply_to.reply_to_id && context == null }, title='会話をもっと読み込む', onclick={ load-context }, disabled={ loading-context })
|
||||
i.fa.fa-ellipsis-v(if={ !loading-context })
|
||||
i.fa.fa-spinner.fa-pulse(if={ loading-context })
|
||||
|
||||
div.context
|
||||
virtual(each={ post in context })
|
||||
mk-post-detail-sub(post={ post })
|
||||
|
||||
div.reply-to(if={ p.reply_to })
|
||||
mk-post-detail-sub(post={ p.reply_to })
|
||||
|
||||
div.repost(if={ is-repost })
|
||||
p
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id }): img.avatar(src={ post.user.avatar_url + '?thumbnail&size=32' }, alt='avatar')
|
||||
i.fa.fa-retweet
|
||||
a.name(href={ CONFIG.url + '/' + post.user.username }) { post.user.name }
|
||||
| がRepost
|
||||
|
||||
article
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + p.user.username })
|
||||
img.avatar(src={ p.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ p.user.id })
|
||||
header
|
||||
a.name(href={ CONFIG.url + '/' + p.user.username }, data-user-preview={ p.user.id })
|
||||
| { p.user.name }
|
||||
span.username
|
||||
| @{ p.user.username }
|
||||
a.time(href={ url })
|
||||
mk-time(time={ p.created_at })
|
||||
div.body
|
||||
div.text@text
|
||||
div.media(if={ p.media })
|
||||
virtual(each={ file in p.media })
|
||||
img(src={ file.url + '?thumbnail&size=512' }, alt={ file.name }, title={ file.name })
|
||||
footer
|
||||
button(onclick={ reply }, title='返信')
|
||||
i.fa.fa-reply
|
||||
p.count(if={ p.replies_count > 0 }) { p.replies_count }
|
||||
button(onclick={ repost }, title='Repost')
|
||||
i.fa.fa-retweet
|
||||
p.count(if={ p.repost_count > 0 }) { p.repost_count }
|
||||
button(class={ liked: p.is_liked }, onclick={ like }, title='善哉')
|
||||
i.fa.fa-thumbs-o-up
|
||||
p.count(if={ p.likes_count > 0 }) { p.likes_count }
|
||||
button(onclick={ NotImplementedException }): i.fa.fa-ellipsis-h
|
||||
div.reposts-and-likes
|
||||
div.reposts(if={ reposts && reposts.length > 0 })
|
||||
header
|
||||
a { p.repost_count }
|
||||
p Repost
|
||||
ol.users
|
||||
li.user(each={ reposts })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + user.username }, title={ user.name }, data-user-preview={ user.id })
|
||||
img.avatar(src={ user.avatar_url + '?thumbnail&size=32' }, alt='')
|
||||
div.likes(if={ likes && likes.length > 0 })
|
||||
header
|
||||
a { p.likes_count }
|
||||
p いいね
|
||||
ol.users
|
||||
li.user(each={ likes })
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + username }, title={ name }, data-user-preview={ id })
|
||||
img.avatar(src={ avatar_url + '?thumbnail&size=32' }, alt='')
|
||||
|
||||
div.replies
|
||||
virtual(each={ post in replies })
|
||||
mk-post-detail-sub(post={ post })
|
||||
|
||||
style.
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
width 640px
|
||||
overflow hidden
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.1)
|
||||
border-radius 8px
|
||||
|
||||
> .fetching
|
||||
padding 64px 0
|
||||
|
||||
> .main
|
||||
|
||||
> .read-more
|
||||
display block
|
||||
margin 0
|
||||
padding 10px 0
|
||||
width 100%
|
||||
font-size 1em
|
||||
text-align center
|
||||
color #999
|
||||
cursor pointer
|
||||
background #fafafa
|
||||
outline none
|
||||
border none
|
||||
border-bottom solid 1px #eef0f2
|
||||
border-radius 6px 6px 0 0
|
||||
|
||||
&:hover
|
||||
background #f6f6f6
|
||||
|
||||
&:active
|
||||
background #f0f0f0
|
||||
|
||||
&:disabled
|
||||
color #ccc
|
||||
|
||||
> .context
|
||||
> *
|
||||
border-bottom 1px solid #eef0f2
|
||||
|
||||
> .repost
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px 32px
|
||||
|
||||
.avatar-anchor
|
||||
display inline-block
|
||||
|
||||
.avatar
|
||||
vertical-align bottom
|
||||
min-width 28px
|
||||
min-height 28px
|
||||
max-width 28px
|
||||
max-height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
i
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
border-bottom 1px solid #eef0f2
|
||||
|
||||
> article
|
||||
padding 28px 32px 18px 32px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
width 60px
|
||||
height 60px
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 60px
|
||||
height 60px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> header
|
||||
position absolute
|
||||
top 28px
|
||||
left 108px
|
||||
width calc(100% - 108px)
|
||||
|
||||
> .name
|
||||
display inline-block
|
||||
margin 0
|
||||
line-height 24px
|
||||
color #777
|
||||
font-size 18px
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
display block
|
||||
text-align left
|
||||
margin 0
|
||||
color #ccc
|
||||
|
||||
> .time
|
||||
position absolute
|
||||
top 0
|
||||
right 32px
|
||||
font-size 1em
|
||||
color #c0c0c0
|
||||
|
||||
> .body
|
||||
padding 8px 0
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
word-wrap break-word
|
||||
font-size 1.5em
|
||||
color #717171
|
||||
|
||||
> mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .media
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
||||
> footer
|
||||
font-size 1.2em
|
||||
|
||||
> button
|
||||
margin 0 28px 0 0
|
||||
padding 8px
|
||||
background transparent
|
||||
border none
|
||||
font-size 1em
|
||||
color #ddd
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
color #666
|
||||
|
||||
> .count
|
||||
display inline
|
||||
margin 0 0 0 8px
|
||||
color #999
|
||||
|
||||
&.liked
|
||||
color $theme-color
|
||||
|
||||
> .reposts-and-likes
|
||||
display flex
|
||||
justify-content center
|
||||
padding 0
|
||||
margin 16px 0
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> .reposts
|
||||
> .likes
|
||||
display flex
|
||||
flex 1 1
|
||||
padding 0
|
||||
border-top solid 1px #F2EFEE
|
||||
|
||||
> header
|
||||
flex 1 1 80px
|
||||
max-width 80px
|
||||
padding 8px 5px 0px 10px
|
||||
|
||||
> a
|
||||
display block
|
||||
font-size 1.5em
|
||||
line-height 1.4em
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
font-size 0.7em
|
||||
line-height 1em
|
||||
font-weight normal
|
||||
color #a0a2a5
|
||||
|
||||
> .users
|
||||
display block
|
||||
flex 1 1
|
||||
margin 0
|
||||
padding 10px 10px 10px 5px
|
||||
list-style none
|
||||
|
||||
> .user
|
||||
display block
|
||||
float left
|
||||
margin 4px
|
||||
padding 0
|
||||
|
||||
> .avatar-anchor
|
||||
display:block
|
||||
|
||||
> .avatar
|
||||
vertical-align bottom
|
||||
width 24px
|
||||
height 24px
|
||||
border-radius 4px
|
||||
|
||||
> .reposts + .likes
|
||||
margin-left 16px
|
||||
|
||||
> .replies
|
||||
> *
|
||||
border-top 1px solid #eef0f2
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \text
|
||||
@mixin \user-preview
|
||||
@mixin \date-stringify
|
||||
@mixin \NotImplementedException
|
||||
|
||||
@fetching = true
|
||||
@loading-context = false
|
||||
@content = null
|
||||
@post = null
|
||||
|
||||
@on \mount ~>
|
||||
|
||||
@api \posts/show do
|
||||
post_id: @opts.post
|
||||
.then (post) ~>
|
||||
@fetching = false
|
||||
@post = post
|
||||
@trigger \loaded
|
||||
|
||||
@is-repost = @post.repost?
|
||||
@p = if @is-repost then @post.repost else @post
|
||||
|
||||
@title = @date-stringify @p.created_at
|
||||
|
||||
@update!
|
||||
|
||||
if @p.text?
|
||||
tokens = @analyze @p.text
|
||||
@refs.text.innerHTML = @compile tokens
|
||||
|
||||
@refs.text.children.for-each (e) ~>
|
||||
if e.tag-name == \MK-URL
|
||||
riot.mount e
|
||||
|
||||
# URLをプレビュー
|
||||
tokens
|
||||
.filter (t) -> t.type == \link
|
||||
.map (t) ~>
|
||||
@preview = @refs.text.append-child document.create-element \mk-url-preview
|
||||
riot.mount @preview, do
|
||||
url: t.content
|
||||
|
||||
# Get likes
|
||||
@api \posts/likes do
|
||||
post_id: @p.id
|
||||
limit: 8
|
||||
.then (likes) ~>
|
||||
@likes = likes
|
||||
@update!
|
||||
|
||||
# Get reposts
|
||||
@api \posts/reposts do
|
||||
post_id: @p.id
|
||||
limit: 8
|
||||
.then (reposts) ~>
|
||||
@reposts = reposts
|
||||
@update!
|
||||
|
||||
# Get replies
|
||||
@api \posts/replies do
|
||||
post_id: @p.id
|
||||
limit: 8
|
||||
.then (replies) ~>
|
||||
@replies = replies
|
||||
@update!
|
||||
|
||||
@update!
|
||||
|
||||
@reply = ~>
|
||||
form = document.body.append-child document.create-element \mk-post-form-window
|
||||
riot.mount form, do
|
||||
reply: @p
|
||||
|
||||
@repost = ~>
|
||||
form = document.body.append-child document.create-element \mk-repost-form-window
|
||||
riot.mount form, do
|
||||
post: @p
|
||||
|
||||
@like = ~>
|
||||
if @p.is_liked
|
||||
@api \posts/likes/delete do
|
||||
post_id: @p.id
|
||||
.then ~>
|
||||
@p.is_liked = false
|
||||
@update!
|
||||
else
|
||||
@api \posts/likes/create do
|
||||
post_id: @p.id
|
||||
.then ~>
|
||||
@p.is_liked = true
|
||||
@update!
|
||||
|
||||
@load-context = ~>
|
||||
@loading-context = true
|
||||
|
||||
# Get context
|
||||
@api \posts/context do
|
||||
post_id: @p.reply_to_id
|
||||
.then (context) ~>
|
||||
@context = context.reverse!
|
||||
@loading-context = false
|
||||
@update!
|
||||
60
src/web/app/desktop/tags/post-form-window.tag
Normal file
60
src/web/app/desktop/tags/post-form-window.tag
Normal file
@@ -0,0 +1,60 @@
|
||||
mk-post-form-window
|
||||
|
||||
mk-window@window(is-modal={ true }, colored={ true })
|
||||
|
||||
<yield to="header">
|
||||
span(if={ !parent.opts.reply }) 新規投稿
|
||||
span(if={ parent.opts.reply }) 返信
|
||||
span.files(if={ parent.files.length != 0 }) 添付: { parent.files.length }ファイル
|
||||
span.uploading-files(if={ parent.uploading-files.length != 0 })
|
||||
| { parent.uploading-files.length }個のファイルをアップロード中
|
||||
mk-ellipsis
|
||||
</yield>
|
||||
|
||||
<yield to="content">
|
||||
div.ref(if={ parent.opts.reply })
|
||||
mk-post-preview(post={ parent.opts.reply })
|
||||
div.body
|
||||
mk-post-form@form(reply={ parent.opts.reply })
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
|
||||
[data-yield='header']
|
||||
> .files
|
||||
> .uploading-files
|
||||
margin-left 8px
|
||||
opacity 0.8
|
||||
|
||||
&:before
|
||||
content '('
|
||||
|
||||
&:after
|
||||
content ')'
|
||||
|
||||
[data-yield='content']
|
||||
> .ref
|
||||
> mk-post-preview
|
||||
margin 16px 22px
|
||||
|
||||
script.
|
||||
@uploading-files = []
|
||||
@files = []
|
||||
|
||||
@on \mount ~>
|
||||
@refs.window.refs.form.focus!
|
||||
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@refs.window.refs.form.on \post ~>
|
||||
@refs.window.close!
|
||||
|
||||
@refs.window.refs.form.on \change-uploading-files (files) ~>
|
||||
@uploading-files = files
|
||||
@update!
|
||||
|
||||
@refs.window.refs.form.on \change-files (files) ~>
|
||||
@files = files
|
||||
@update!
|
||||
430
src/web/app/desktop/tags/post-form.tag
Normal file
430
src/web/app/desktop/tags/post-form.tag
Normal file
@@ -0,0 +1,430 @@
|
||||
mk-post-form(ondragover={ ondragover }, ondragenter={ ondragenter }, ondragleave={ ondragleave }, ondrop={ ondrop })
|
||||
textarea@text(disabled={ wait }, class={ withfiles: files.length != 0 }, oninput={ update }, onkeydown={ onkeydown }, onpaste={ onpaste }, placeholder={ opts.reply ? 'この投稿への返信...' : 'いまどうしてる?' })
|
||||
div.attaches(if={ files.length != 0 })
|
||||
ul.files@attaches
|
||||
li.file(each={ files })
|
||||
div.img(style='background-image: url({ url + "?thumbnail&size=64" })', title={ name })
|
||||
img.remove(onclick={ _remove }, src='/_/resources/desktop/remove.png', title='添付取り消し', alt='')
|
||||
li.add(if={ files.length < 4 }, title='PCからファイルを添付', onclick={ select-file }): i.fa.fa-plus
|
||||
p.remain
|
||||
| 残り{ 4 - files.length }
|
||||
mk-uploader@uploader
|
||||
button@upload(title='PCからファイルを添付', onclick={ select-file }): i.fa.fa-upload
|
||||
button@drive(title='ドライブからファイルを添付', onclick={ select-file-from-drive }): i.fa.fa-cloud
|
||||
p.text-count(class={ over: refs.text.value.length > 300 }) のこり{ 300 - refs.text.value.length }文字
|
||||
button@submit(class={ wait: wait }, disabled={ wait || (refs.text.value.length == 0 && files.length == 0) }, onclick={ post })
|
||||
| { wait ? '投稿中' : opts.reply ? '返信' : '投稿' }
|
||||
mk-ellipsis(if={ wait })
|
||||
input@file(type='file', accept='image/*', multiple, tabindex='-1', onchange={ change-file })
|
||||
div.dropzone(if={ draghover })
|
||||
|
||||
style.
|
||||
display block
|
||||
padding 16px
|
||||
background lighten($theme-color, 95%)
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .attaches
|
||||
margin 0
|
||||
padding 0
|
||||
background lighten($theme-color, 98%)
|
||||
border solid 1px rgba($theme-color, 0.1)
|
||||
border-top none
|
||||
border-radius 0 0 4px 4px
|
||||
transition border-color .3s ease
|
||||
|
||||
> .remain
|
||||
display block
|
||||
position absolute
|
||||
top 8px
|
||||
right 8px
|
||||
margin 0
|
||||
padding 0
|
||||
color rgba($theme-color, 0.4)
|
||||
|
||||
> .files
|
||||
display block
|
||||
margin 0
|
||||
padding 4px
|
||||
list-style none
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .file
|
||||
display block
|
||||
float left
|
||||
margin 4px
|
||||
padding 0
|
||||
cursor move
|
||||
|
||||
&:hover > .remove
|
||||
display block
|
||||
|
||||
> .img
|
||||
width 64px
|
||||
height 64px
|
||||
background-size cover
|
||||
background-position center center
|
||||
|
||||
> .remove
|
||||
display none
|
||||
position absolute
|
||||
top -6px
|
||||
right -6px
|
||||
width 16px
|
||||
height 16px
|
||||
cursor pointer
|
||||
|
||||
> .add
|
||||
display block
|
||||
float left
|
||||
margin 4px
|
||||
padding 0
|
||||
border dashed 2px rgba($theme-color, 0.2)
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
border-color rgba($theme-color, 0.3)
|
||||
|
||||
> i
|
||||
color rgba($theme-color, 0.4)
|
||||
|
||||
> i
|
||||
display block
|
||||
width 60px
|
||||
height 60px
|
||||
line-height 60px
|
||||
text-align center
|
||||
font-size 1.2em
|
||||
color rgba($theme-color, 0.2)
|
||||
|
||||
> mk-uploader
|
||||
margin 8px 0 0 0
|
||||
padding 8px
|
||||
border solid 1px rgba($theme-color, 0.2)
|
||||
border-radius 4px
|
||||
|
||||
[ref='file']
|
||||
display none
|
||||
|
||||
[ref='text']
|
||||
display block
|
||||
padding 12px
|
||||
margin 0
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height calc(16px + 12px + 12px)
|
||||
font-size 16px
|
||||
color #333
|
||||
background #fff
|
||||
outline none
|
||||
border solid 1px rgba($theme-color, 0.1)
|
||||
border-radius 4px
|
||||
transition border-color .3s ease
|
||||
|
||||
&:hover
|
||||
border-color rgba($theme-color, 0.2)
|
||||
transition border-color .1s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color
|
||||
border-color rgba($theme-color, 0.5)
|
||||
transition border-color 0s ease
|
||||
|
||||
&:disabled
|
||||
opacity 0.5
|
||||
|
||||
&::-webkit-input-placeholder
|
||||
color rgba($theme-color, 0.3)
|
||||
|
||||
&.withfiles
|
||||
border-bottom solid 1px rgba($theme-color, 0.1) !important
|
||||
border-radius 4px 4px 0 0
|
||||
|
||||
&:hover + .attaches
|
||||
border-color rgba($theme-color, 0.2)
|
||||
transition border-color .1s ease
|
||||
|
||||
&:focus + .attaches
|
||||
border-color rgba($theme-color, 0.5)
|
||||
transition border-color 0s ease
|
||||
|
||||
.text-count
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
right 138px
|
||||
margin 0
|
||||
line-height 40px
|
||||
color rgba($theme-color, 0.5)
|
||||
|
||||
&.over
|
||||
color #ec3828
|
||||
|
||||
[ref='submit']
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
right 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 0
|
||||
width 110px
|
||||
height 40px
|
||||
font-size 1em
|
||||
color $theme-color-foreground
|
||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||
outline none
|
||||
border solid 1px lighten($theme-color, 15%)
|
||||
border-radius 4px
|
||||
|
||||
&:not(:disabled)
|
||||
font-weight bold
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||
border-color $theme-color
|
||||
|
||||
&:active:not(:disabled)
|
||||
background $theme-color
|
||||
border-color $theme-color
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
cursor default
|
||||
|
||||
&.wait
|
||||
background linear-gradient(
|
||||
45deg,
|
||||
darken($theme-color, 10%) 25%,
|
||||
$theme-color 25%,
|
||||
$theme-color 50%,
|
||||
darken($theme-color, 10%) 50%,
|
||||
darken($theme-color, 10%) 75%,
|
||||
$theme-color 75%,
|
||||
$theme-color
|
||||
)
|
||||
background-size 32px 32px
|
||||
animation stripe-bg 1.5s linear infinite
|
||||
opacity 0.7
|
||||
cursor wait
|
||||
|
||||
@keyframes stripe-bg
|
||||
from {background-position: 0 0;}
|
||||
to {background-position: -64px 32px;}
|
||||
|
||||
[ref='upload']
|
||||
[ref='drive']
|
||||
display inline-block
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 8px 4px 0 0
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 1em
|
||||
color rgba($theme-color, 0.5)
|
||||
background transparent
|
||||
outline none
|
||||
border solid 1px transparent
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background transparent
|
||||
border-color rgba($theme-color, 0.3)
|
||||
|
||||
&:active
|
||||
color rgba($theme-color, 0.6)
|
||||
background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%)
|
||||
border-color rgba($theme-color, 0.5)
|
||||
box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
> .dropzone
|
||||
position absolute
|
||||
left 0
|
||||
top 0
|
||||
width 100%
|
||||
height 100%
|
||||
border dashed 2px rgba($theme-color, 0.5)
|
||||
pointer-events none
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \notify
|
||||
@mixin \autocomplete
|
||||
@mixin \sortable
|
||||
|
||||
@wait = false
|
||||
@uploadings = []
|
||||
@files = []
|
||||
@autocomplete = null
|
||||
|
||||
@in-reply-to-post = @opts.reply
|
||||
|
||||
# https://github.com/riot/riot/issues/2080
|
||||
if @in-reply-to-post == '' then @in-reply-to-post = null
|
||||
|
||||
@on \mount ~>
|
||||
@refs.uploader.on \uploaded (file) ~>
|
||||
@add-file file
|
||||
|
||||
@refs.uploader.on \change-uploads (uploads) ~>
|
||||
@trigger \change-uploading-files uploads
|
||||
|
||||
@autocomplete = new @Autocomplete @refs.text
|
||||
@autocomplete.attach!
|
||||
|
||||
@on \unmount ~>
|
||||
@autocomplete.detach!
|
||||
|
||||
@focus = ~>
|
||||
@refs.text.focus!
|
||||
|
||||
@clear = ~>
|
||||
@refs.text.value = ''
|
||||
@files = []
|
||||
@trigger \change-files
|
||||
@update!
|
||||
|
||||
@ondragover = (e) ~>
|
||||
e.stop-propagation!
|
||||
@draghover = true
|
||||
# ドラッグされてきたものがファイルだったら
|
||||
if e.data-transfer.effect-allowed == \all
|
||||
e.data-transfer.drop-effect = \copy
|
||||
else
|
||||
e.data-transfer.drop-effect = \move
|
||||
return false
|
||||
|
||||
@ondragenter = (e) ~>
|
||||
@draghover = true
|
||||
|
||||
@ondragleave = (e) ~>
|
||||
@draghover = false
|
||||
|
||||
@ondrop = (e) ~>
|
||||
e.prevent-default!
|
||||
e.stop-propagation!
|
||||
@draghover = false
|
||||
|
||||
# ファイルだったら
|
||||
if e.data-transfer.files.length > 0
|
||||
Array.prototype.for-each.call e.data-transfer.files, (file) ~>
|
||||
@upload file
|
||||
return false
|
||||
|
||||
# データ取得
|
||||
data = e.data-transfer.get-data 'text'
|
||||
if !data?
|
||||
return false
|
||||
|
||||
try
|
||||
# パース
|
||||
obj = JSON.parse data
|
||||
|
||||
# (ドライブの)ファイルだったら
|
||||
if obj.type == \file
|
||||
@add-file obj.file
|
||||
catch
|
||||
# ignore
|
||||
|
||||
return false
|
||||
|
||||
@onkeydown = (e) ~>
|
||||
if (e.which == 10 || e.which == 13) && (e.ctrl-key || e.meta-key)
|
||||
@post!
|
||||
|
||||
@onpaste = (e) ~>
|
||||
data = e.clipboard-data
|
||||
items = data.items
|
||||
for i from 0 to items.length - 1
|
||||
item = items[i]
|
||||
switch (item.kind)
|
||||
| \file =>
|
||||
@upload item.get-as-file!
|
||||
|
||||
@select-file = ~>
|
||||
@refs.file.click!
|
||||
|
||||
@select-file-from-drive = ~>
|
||||
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
|
||||
i = riot.mount browser, do
|
||||
multiple: true
|
||||
i[0].one \selected (files) ~>
|
||||
files.for-each @add-file
|
||||
|
||||
@change-file = ~>
|
||||
files = @refs.file.files
|
||||
for i from 0 to files.length - 1
|
||||
file = files.item i
|
||||
@upload file
|
||||
|
||||
@upload = (file) ~>
|
||||
@refs.uploader.upload file
|
||||
|
||||
@add-file = (file) ~>
|
||||
file._remove = ~>
|
||||
@files = @files.filter (x) -> x.id != file.id
|
||||
@trigger \change-files @files
|
||||
@update!
|
||||
|
||||
@files.push file
|
||||
@trigger \change-files @files
|
||||
@update!
|
||||
|
||||
new @Sortable @refs.attaches, do
|
||||
draggable: \.file
|
||||
animation: 150ms
|
||||
|
||||
@post = (e) ~>
|
||||
@wait = true
|
||||
|
||||
files = if @files? and @files.length > 0
|
||||
then @files.map (f) -> f.id
|
||||
else undefined
|
||||
|
||||
@api \posts/create do
|
||||
text: @refs.text.value
|
||||
media_ids: files
|
||||
reply_to_id: if @in-reply-to-post? then @in-reply-to-post.id else undefined
|
||||
.then (data) ~>
|
||||
@trigger \post
|
||||
@notify if @in-reply-to-post? then '返信しました!' else '投稿しました!'
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
@notify '投稿できませんでした'
|
||||
.then ~>
|
||||
@wait = false
|
||||
@update!
|
||||
94
src/web/app/desktop/tags/post-preview.tag
Normal file
94
src/web/app/desktop/tags/post-preview.tag
Normal file
@@ -0,0 +1,94 @@
|
||||
mk-post-preview(title={ title })
|
||||
article
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username })
|
||||
img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ post.user_id })
|
||||
div.main
|
||||
header
|
||||
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id })
|
||||
| { post.user.name }
|
||||
span.username
|
||||
| @{ post.user.username }
|
||||
a.time(href={ CONFIG.url + '/' + post.user.username + '/' + post.id })
|
||||
mk-time(time={ post.created_at })
|
||||
div.body
|
||||
mk-sub-post-content.text(post={ post })
|
||||
|
||||
style.
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 0.9em
|
||||
background #fff
|
||||
|
||||
> article
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 16px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 52px
|
||||
height 52px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 68px)
|
||||
|
||||
> header
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
|
||||
> .name
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
color #607073
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color #d1d8da
|
||||
|
||||
> .time
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
color #b2b8bb
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
script.
|
||||
@mixin \date-stringify
|
||||
@mixin \user-preview
|
||||
|
||||
@post = @opts.post
|
||||
|
||||
@title = @date-stringify @post.created_at
|
||||
72
src/web/app/desktop/tags/post-status-graph.tag
Normal file
72
src/web/app/desktop/tags/post-status-graph.tag
Normal file
@@ -0,0 +1,72 @@
|
||||
mk-post-status-graph
|
||||
canvas@canv(width={ opts.width }, height={ opts.height })
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> canvas
|
||||
margin 0 auto
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \is-promise
|
||||
|
||||
@post = null
|
||||
@post-promise = if @is-promise @opts.post then @opts.post else Promise.resolve @opts.post
|
||||
|
||||
@on \mount ~>
|
||||
post <~ @post-promise.then
|
||||
@post = post
|
||||
@update!
|
||||
|
||||
@api \aggregation/posts/like do
|
||||
post_id: @post.id
|
||||
limit: 30days
|
||||
.then (likes) ~>
|
||||
likes = likes.reverse!
|
||||
|
||||
@api \aggregation/posts/repost do
|
||||
post_id: @post.id
|
||||
limit: 30days
|
||||
.then (repost) ~>
|
||||
repost = repost.reverse!
|
||||
|
||||
@api \aggregation/posts/reply do
|
||||
post_id: @post.id
|
||||
limit: 30days
|
||||
.then (replies) ~>
|
||||
replies = replies.reverse!
|
||||
|
||||
new Chart @refs.canv, do
|
||||
type: \bar
|
||||
data:
|
||||
labels: likes.map (x, i) ~> if i % 3 == 2 then x.date.day + '日' else ''
|
||||
datasets: [
|
||||
{
|
||||
label: \いいね
|
||||
type: \line
|
||||
data: likes.map (x) ~> x.count
|
||||
line-tension: 0
|
||||
border-width: 2
|
||||
fill: true
|
||||
background-color: 'rgba(247, 121, 108, 0.2)'
|
||||
point-background-color: \#fff
|
||||
point-radius: 4
|
||||
point-border-width: 2
|
||||
border-color: \#F7796C
|
||||
},
|
||||
{
|
||||
label: \返信
|
||||
type: \bar
|
||||
data: replies.map (x) ~> x.count
|
||||
background-color: \#555
|
||||
},
|
||||
{
|
||||
label: \Repost
|
||||
type: \bar
|
||||
data: repost.map (x) ~> x.count
|
||||
background-color: \#a2d61e
|
||||
}
|
||||
]
|
||||
options:
|
||||
responsive: false
|
||||
92
src/web/app/desktop/tags/progress-dialog.tag
Normal file
92
src/web/app/desktop/tags/progress-dialog.tag
Normal file
@@ -0,0 +1,92 @@
|
||||
mk-progress-dialog
|
||||
mk-window@window(is-modal={ false }, can-close={ false }, width={ '500px' })
|
||||
<yield to="header">
|
||||
| { parent.title }
|
||||
mk-ellipsis
|
||||
</yield>
|
||||
<yield to="content">
|
||||
div.body
|
||||
p.init(if={ isNaN(parent.value) })
|
||||
| 待機中
|
||||
mk-ellipsis
|
||||
p.percentage(if={ !isNaN(parent.value) }) { Math.floor((parent.value / parent.max) * 100) }
|
||||
progress(if={ !isNaN(parent.value) && parent.value < parent.max }, value={ isNaN(parent.value) ? 0 : parent.value }, max={ parent.max })
|
||||
div.progress.waiting(if={ parent.value >= parent.max })
|
||||
</yield>
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> mk-window
|
||||
[data-yield='content']
|
||||
|
||||
> .body
|
||||
padding 18px 24px 24px 24px
|
||||
|
||||
> .init
|
||||
display block
|
||||
margin 0
|
||||
text-align center
|
||||
color rgba(#000, 0.7)
|
||||
|
||||
> .percentage
|
||||
display block
|
||||
margin 0 0 4px 0
|
||||
text-align center
|
||||
line-height 16px
|
||||
color rgba($theme-color, 0.7)
|
||||
|
||||
&:after
|
||||
content '%'
|
||||
|
||||
> progress
|
||||
> .progress
|
||||
display block
|
||||
margin 0
|
||||
width 100%
|
||||
height 10px
|
||||
background transparent
|
||||
border none
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
|
||||
&::-webkit-progress-value
|
||||
background $theme-color
|
||||
|
||||
&::-webkit-progress-bar
|
||||
background rgba($theme-color, 0.1)
|
||||
|
||||
> .progress
|
||||
background linear-gradient(
|
||||
45deg,
|
||||
lighten($theme-color, 30%) 25%,
|
||||
$theme-color 25%,
|
||||
$theme-color 50%,
|
||||
lighten($theme-color, 30%) 50%,
|
||||
lighten($theme-color, 30%) 75%,
|
||||
$theme-color 75%,
|
||||
$theme-color
|
||||
)
|
||||
background-size 32px 32px
|
||||
animation progress-dialog-tag-progress-waiting 1.5s linear infinite
|
||||
|
||||
@keyframes progress-dialog-tag-progress-waiting
|
||||
from {background-position: 0 0;}
|
||||
to {background-position: -64px 32px;}
|
||||
|
||||
script.
|
||||
@title = @opts.title
|
||||
@value = parse-int @opts.value, 10
|
||||
@max = parse-int @opts.max, 10
|
||||
|
||||
@on \mount ~>
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@update-progress = (value, max) ~>
|
||||
@value = parse-int value, 10
|
||||
@max = parse-int max, 10
|
||||
@update!
|
||||
|
||||
@close = ~>
|
||||
@refs.window.close!
|
||||
38
src/web/app/desktop/tags/repost-form-window.tag
Normal file
38
src/web/app/desktop/tags/repost-form-window.tag
Normal file
@@ -0,0 +1,38 @@
|
||||
mk-repost-form-window
|
||||
mk-window@window(is-modal={ true }, colored={ true })
|
||||
<yield to="header">
|
||||
i.fa.fa-retweet
|
||||
| この投稿をRepostしますか?
|
||||
</yield>
|
||||
<yield to="content">
|
||||
mk-repost-form@form(post={ parent.opts.post })
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
script.
|
||||
|
||||
@on-document-keydown = (e) ~>
|
||||
tag = e.target.tag-name.to-lower-case!
|
||||
if tag != \input and tag != \textarea
|
||||
if e.which == 27 # Esc
|
||||
@refs.window.close!
|
||||
|
||||
@on \mount ~>
|
||||
@refs.window.refs.form.on \cancel ~>
|
||||
@refs.window.close!
|
||||
|
||||
@refs.window.refs.form.on \posted ~>
|
||||
@refs.window.close!
|
||||
|
||||
document.add-event-listener \keydown @on-document-keydown
|
||||
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@on \unmount ~>
|
||||
document.remove-event-listener \keydown @on-document-keydown
|
||||
140
src/web/app/desktop/tags/repost-form.tag
Normal file
140
src/web/app/desktop/tags/repost-form.tag
Normal file
@@ -0,0 +1,140 @@
|
||||
mk-repost-form
|
||||
mk-post-preview(post={ opts.post })
|
||||
div.form(if={ quote })
|
||||
textarea@text(disabled={ wait }, placeholder='この投稿を引用...')
|
||||
footer
|
||||
a.quote(if={ !quote }, onclick={ onquote }) 引用する...
|
||||
button.cancel(onclick={ cancel }) キャンセル
|
||||
button.ok(onclick={ ok }) Repost
|
||||
|
||||
style.
|
||||
|
||||
> mk-post-preview
|
||||
margin 16px 22px
|
||||
|
||||
> .form
|
||||
[ref='text']
|
||||
display block
|
||||
padding 12px
|
||||
margin 0
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height calc(1em + 12px + 12px)
|
||||
font-size 1em
|
||||
color #333
|
||||
background #fff
|
||||
outline none
|
||||
border solid 1px rgba($theme-color, 0.1)
|
||||
border-radius 4px
|
||||
transition border-color .3s ease
|
||||
|
||||
&:hover
|
||||
border-color rgba($theme-color, 0.2)
|
||||
transition border-color .1s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color
|
||||
border-color rgba($theme-color, 0.5)
|
||||
transition border-color 0s ease
|
||||
|
||||
&:disabled
|
||||
opacity 0.5
|
||||
|
||||
&::-webkit-input-placeholder
|
||||
color rgba($theme-color, 0.3)
|
||||
|
||||
> div
|
||||
padding 16px
|
||||
|
||||
> footer
|
||||
height 72px
|
||||
background lighten($theme-color, 95%)
|
||||
|
||||
> .quote
|
||||
position absolute
|
||||
bottom 16px
|
||||
left 28px
|
||||
line-height 40px
|
||||
|
||||
button
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 0
|
||||
width 120px
|
||||
height 40px
|
||||
font-size 1em
|
||||
outline none
|
||||
border-radius 4px
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
> .cancel
|
||||
right 148px
|
||||
color #888
|
||||
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||
border solid 1px #e2e2e2
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||
border-color #dcdcdc
|
||||
|
||||
&:active
|
||||
background #ececec
|
||||
border-color #dcdcdc
|
||||
|
||||
> .ok
|
||||
right 16px
|
||||
font-weight bold
|
||||
color $theme-color-foreground
|
||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||
border solid 1px lighten($theme-color, 15%)
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||
border-color $theme-color
|
||||
|
||||
&:active
|
||||
background $theme-color
|
||||
border-color $theme-color
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \notify
|
||||
|
||||
@wait = false
|
||||
@quote = false
|
||||
|
||||
@cancel = ~>
|
||||
@trigger \cancel
|
||||
|
||||
@ok = ~>
|
||||
@wait = true
|
||||
@api \posts/create do
|
||||
repost_id: @opts.post.id
|
||||
text: if @quote then @refs.text.value else undefined
|
||||
.then (data) ~>
|
||||
@trigger \posted
|
||||
@notify 'Repostしました!'
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
@notify 'Repostできませんでした'
|
||||
.then ~>
|
||||
@wait = false
|
||||
@update!
|
||||
|
||||
@onquote = ~>
|
||||
@quote = true
|
||||
88
src/web/app/desktop/tags/search-posts.tag
Normal file
88
src/web/app/desktop/tags/search-posts.tag
Normal file
@@ -0,0 +1,88 @@
|
||||
mk-search-posts
|
||||
div.loading(if={ is-loading })
|
||||
mk-ellipsis-icon
|
||||
p.empty(if={ is-empty })
|
||||
i.fa.fa-search
|
||||
| 「{ query }」に関する投稿は見つかりませんでした。
|
||||
mk-timeline@timeline
|
||||
<yield to="footer">
|
||||
i.fa.fa-moon-o(if={ !parent.more-loading })
|
||||
i.fa.fa-spinner.fa-pulse.fa-fw(if={ parent.more-loading })
|
||||
</yield>
|
||||
|
||||
style.
|
||||
display block
|
||||
background #fff
|
||||
|
||||
> .loading
|
||||
padding 64px 0
|
||||
|
||||
> .empty
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 32px
|
||||
max-width 400px
|
||||
text-align center
|
||||
color #999
|
||||
|
||||
> i
|
||||
display block
|
||||
margin-bottom 16px
|
||||
font-size 3em
|
||||
color #ccc
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \get-post-summary
|
||||
|
||||
@query = @opts.query
|
||||
@is-loading = true
|
||||
@is-empty = false
|
||||
@more-loading = false
|
||||
@page = 0
|
||||
|
||||
@on \mount ~>
|
||||
document.add-event-listener \keydown @on-document-keydown
|
||||
window.add-event-listener \scroll @on-scroll
|
||||
|
||||
@api \posts/search do
|
||||
query: @query
|
||||
.then (posts) ~>
|
||||
@is-loading = false
|
||||
@is-empty = posts.length == 0
|
||||
@update!
|
||||
@refs.timeline.set-posts posts
|
||||
@trigger \loaded
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
|
||||
@on \unmount ~>
|
||||
document.remove-event-listener \keydown @on-document-keydown
|
||||
window.remove-event-listener \scroll @on-scroll
|
||||
|
||||
@on-document-keydown = (e) ~>
|
||||
tag = e.target.tag-name.to-lower-case!
|
||||
if tag != \input and tag != \textarea
|
||||
if e.which == 84 # t
|
||||
@refs.timeline.focus!
|
||||
|
||||
@more = ~>
|
||||
if @more-loading or @is-loading or @timeline.posts.length == 0
|
||||
return
|
||||
@more-loading = true
|
||||
@update!
|
||||
@api \posts/search do
|
||||
query: @query
|
||||
page: @page + 1
|
||||
.then (posts) ~>
|
||||
@more-loading = false
|
||||
@page++
|
||||
@update!
|
||||
@refs.timeline.prepend-posts posts
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
|
||||
@on-scroll = ~>
|
||||
current = window.scroll-y + window.inner-height
|
||||
if current > document.body.offset-height - 16 # 遊び
|
||||
@more!
|
||||
28
src/web/app/desktop/tags/search.tag
Normal file
28
src/web/app/desktop/tags/search.tag
Normal file
@@ -0,0 +1,28 @@
|
||||
mk-search
|
||||
header
|
||||
h1 { query }
|
||||
mk-search-posts@posts(query={ query })
|
||||
|
||||
style.
|
||||
display block
|
||||
padding-bottom 32px
|
||||
|
||||
> header
|
||||
width 100%
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
color #555
|
||||
|
||||
> mk-search-posts
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
script.
|
||||
@query = @opts.query
|
||||
|
||||
@on \mount ~>
|
||||
@refs.posts.on \loaded ~>
|
||||
@trigger \loaded
|
||||
160
src/web/app/desktop/tags/select-file-from-drive-window.tag
Normal file
160
src/web/app/desktop/tags/select-file-from-drive-window.tag
Normal file
@@ -0,0 +1,160 @@
|
||||
mk-select-file-from-drive-window
|
||||
mk-window@window(is-modal={ true }, width={ '800px' }, height={ '500px' })
|
||||
<yield to="header">
|
||||
mk-raw(content={ parent.title })
|
||||
span.count(if={ parent.multiple && parent.file.length > 0 }) ({ parent.file.length }ファイル選択中)
|
||||
</yield>
|
||||
<yield to="content">
|
||||
mk-drive-browser@browser(multiple={ parent.multiple })
|
||||
div
|
||||
button.upload(title='PCからドライブにファイルをアップロード', onclick={ parent.upload }): i.fa.fa-upload
|
||||
button.cancel(onclick={ parent.close }) キャンセル
|
||||
button.ok(disabled={ parent.multiple && parent.file.length == 0 }, onclick={ parent.ok }) 決定
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> mk-raw
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
.count
|
||||
margin-left 8px
|
||||
opacity 0.7
|
||||
|
||||
[data-yield='content']
|
||||
> mk-drive-browser
|
||||
height calc(100% - 72px)
|
||||
|
||||
> div
|
||||
height 72px
|
||||
background lighten($theme-color, 95%)
|
||||
|
||||
.upload
|
||||
display inline-block
|
||||
position absolute
|
||||
top 8px
|
||||
left 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 8px 4px 0 0
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 1em
|
||||
color rgba($theme-color, 0.5)
|
||||
background transparent
|
||||
outline none
|
||||
border solid 1px transparent
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background transparent
|
||||
border-color rgba($theme-color, 0.3)
|
||||
|
||||
&:active
|
||||
color rgba($theme-color, 0.6)
|
||||
background transparent
|
||||
border-color rgba($theme-color, 0.5)
|
||||
box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
.ok
|
||||
.cancel
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 0
|
||||
width 120px
|
||||
height 40px
|
||||
font-size 1em
|
||||
outline none
|
||||
border-radius 4px
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
cursor default
|
||||
|
||||
.ok
|
||||
right 16px
|
||||
color $theme-color-foreground
|
||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||
border solid 1px lighten($theme-color, 15%)
|
||||
|
||||
&:not(:disabled)
|
||||
font-weight bold
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||
border-color $theme-color
|
||||
|
||||
&:active:not(:disabled)
|
||||
background $theme-color
|
||||
border-color $theme-color
|
||||
|
||||
.cancel
|
||||
right 148px
|
||||
color #888
|
||||
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||
border solid 1px #e2e2e2
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||
border-color #dcdcdc
|
||||
|
||||
&:active
|
||||
background #ececec
|
||||
border-color #dcdcdc
|
||||
|
||||
script.
|
||||
@file = []
|
||||
|
||||
@multiple = if @opts.multiple? then @opts.multiple else false
|
||||
@title = @opts.title || '<i class="fa fa-file-o"></i>ファイルを選択'
|
||||
|
||||
@on \mount ~>
|
||||
@refs.window.refs.browser.on \selected (file) ~>
|
||||
@file = file
|
||||
@ok!
|
||||
|
||||
@refs.window.refs.browser.on \change-selection (files) ~>
|
||||
@file = files
|
||||
@update!
|
||||
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@close = ~>
|
||||
@refs.window.close!
|
||||
|
||||
@upload = ~>
|
||||
@refs.window.refs.browser.select-local-file!
|
||||
|
||||
@ok = ~>
|
||||
@trigger \selected @file
|
||||
@refs.window.close!
|
||||
44
src/web/app/desktop/tags/set-avatar-suggestion.tag
Normal file
44
src/web/app/desktop/tags/set-avatar-suggestion.tag
Normal file
@@ -0,0 +1,44 @@
|
||||
mk-set-avatar-suggestion(onclick={ set })
|
||||
p
|
||||
b アバターを設定
|
||||
| してみませんか?
|
||||
button(onclick={ close }): i.fa.fa-times
|
||||
|
||||
style.
|
||||
display block
|
||||
cursor pointer
|
||||
color #fff
|
||||
background #a8cad0
|
||||
|
||||
&:hover
|
||||
background #70abb5
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 8px
|
||||
max-width 1024px
|
||||
|
||||
> a
|
||||
font-weight bold
|
||||
color #fff
|
||||
|
||||
> button
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 8px
|
||||
color #fff
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \update-avatar
|
||||
|
||||
@set = ~>
|
||||
@update-avatar @I, (i) ~>
|
||||
@update-i i
|
||||
|
||||
@close = (e) ~>
|
||||
e.prevent-default!
|
||||
e.stop-propagation!
|
||||
@unmount!
|
||||
44
src/web/app/desktop/tags/set-banner-suggestion.tag
Normal file
44
src/web/app/desktop/tags/set-banner-suggestion.tag
Normal file
@@ -0,0 +1,44 @@
|
||||
mk-set-banner-suggestion(onclick={ set })
|
||||
p
|
||||
b バナーを設定
|
||||
| してみませんか?
|
||||
button(onclick={ close }): i.fa.fa-times
|
||||
|
||||
style.
|
||||
display block
|
||||
cursor pointer
|
||||
color #fff
|
||||
background #a8cad0
|
||||
|
||||
&:hover
|
||||
background #70abb5
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 8px
|
||||
max-width 1024px
|
||||
|
||||
> a
|
||||
font-weight bold
|
||||
color #fff
|
||||
|
||||
> button
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 8px
|
||||
color #fff
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \update-banner
|
||||
|
||||
@set = ~>
|
||||
@update-banner @I, (i) ~>
|
||||
@update-i i
|
||||
|
||||
@close = (e) ~>
|
||||
e.prevent-default!
|
||||
e.stop-propagation!
|
||||
@unmount!
|
||||
26
src/web/app/desktop/tags/settings-window.tag
Normal file
26
src/web/app/desktop/tags/settings-window.tag
Normal file
@@ -0,0 +1,26 @@
|
||||
mk-settings-window
|
||||
mk-window@window(is-modal={ true }, width={ '700px' }, height={ '550px' })
|
||||
<yield to="header">
|
||||
i.fa.fa-cog
|
||||
| 設定
|
||||
</yield>
|
||||
<yield to="content">
|
||||
mk-settings
|
||||
</yield>
|
||||
|
||||
style.
|
||||
> mk-window
|
||||
[data-yield='header']
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
[data-yield='content']
|
||||
overflow auto
|
||||
|
||||
script.
|
||||
@on \mount ~>
|
||||
@refs.window.on \closed ~>
|
||||
@unmount!
|
||||
|
||||
@close = ~>
|
||||
@refs.window.close!
|
||||
255
src/web/app/desktop/tags/settings.tag
Normal file
255
src/web/app/desktop/tags/settings.tag
Normal file
@@ -0,0 +1,255 @@
|
||||
mk-settings
|
||||
div.nav
|
||||
p(class={ active: page == 'account' }, onmousedown={ set-page.bind(null, 'account') })
|
||||
i.fa.fa-fw.fa-user
|
||||
| アカウント
|
||||
p(class={ active: page == 'web' }, onmousedown={ set-page.bind(null, 'web') })
|
||||
i.fa.fa-fw.fa-desktop
|
||||
| Web
|
||||
p(class={ active: page == 'notification' }, onmousedown={ set-page.bind(null, 'notification') })
|
||||
i.fa.fa-fw.fa-bell-o
|
||||
| 通知
|
||||
p(class={ active: page == 'drive' }, onmousedown={ set-page.bind(null, 'drive') })
|
||||
i.fa.fa-fw.fa-cloud
|
||||
| ドライブ
|
||||
p(class={ active: page == 'apps' }, onmousedown={ set-page.bind(null, 'apps') })
|
||||
i.fa.fa-fw.fa-puzzle-piece
|
||||
| アプリ
|
||||
p(class={ active: page == 'signin' }, onmousedown={ set-page.bind(null, 'signin') })
|
||||
i.fa.fa-fw.fa-sign-in
|
||||
| ログイン履歴
|
||||
p(class={ active: page == 'password' }, onmousedown={ set-page.bind(null, 'password') })
|
||||
i.fa.fa-fw.fa-unlock-alt
|
||||
| パスワード
|
||||
p(class={ active: page == 'api' }, onmousedown={ set-page.bind(null, 'api') })
|
||||
i.fa.fa-fw.fa-key
|
||||
| API
|
||||
|
||||
div.pages
|
||||
section.account(show={ page == 'account' })
|
||||
h1 アカウント
|
||||
label.avatar
|
||||
p アバター
|
||||
img.avatar(src={ I.avatar_url + '?thumbnail&size=64' }, alt='avatar')
|
||||
button.style-normal(onclick={ avatar }) 画像を選択
|
||||
label
|
||||
p 名前
|
||||
input@account-name(type='text', value={ I.name })
|
||||
label
|
||||
p 場所
|
||||
input@account-location(type='text', value={ I.location })
|
||||
label
|
||||
p 自己紹介
|
||||
textarea@account-bio { I.bio }
|
||||
button.style-primary(onclick={ update-account }) 保存
|
||||
|
||||
section.web(show={ page == 'web' })
|
||||
h1 デザイン
|
||||
label
|
||||
p 壁紙
|
||||
button.style-normal(onclick={ wallpaper }) 画像を選択
|
||||
section.web(show={ page == 'web' })
|
||||
h1 その他
|
||||
label.checkbox
|
||||
input(type='checkbox', checked={ I.data.cache }, onclick={ update-cache })
|
||||
p 読み込みを高速化する
|
||||
p API通信時に新鮮なユーザー情報をキャッシュすることでフェッチのオーバーヘッドを無くします。(実験的)
|
||||
label.checkbox
|
||||
input(type='checkbox', checked={ I.data.debug }, onclick={ update-debug })
|
||||
p 開発者モード
|
||||
p デバッグ等の開発者モードを有効にします。
|
||||
|
||||
section.signin(show={ page == 'signin' })
|
||||
h1 ログイン履歴
|
||||
mk-signin-history
|
||||
|
||||
section.api(show={ page == 'api' })
|
||||
h1 API
|
||||
p
|
||||
| Token:
|
||||
code { I.token }
|
||||
p APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。
|
||||
p アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
|
||||
p
|
||||
| 万が一このトークンが漏れたりその可能性がある場合は
|
||||
button.regenerate(onclick={ regenerate-token }) トークンを再生成
|
||||
| できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
input:not([type])
|
||||
input[type='text']
|
||||
input[type='password']
|
||||
input[type='email']
|
||||
textarea
|
||||
padding 8px
|
||||
width 100%
|
||||
font-size 16px
|
||||
color #55595c
|
||||
border solid 1px #dadada
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
border-color #aeaeae
|
||||
|
||||
&:focus
|
||||
border-color #aeaeae
|
||||
|
||||
> .nav
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 200px
|
||||
height 100%
|
||||
padding 16px 0 0 0
|
||||
border-right solid 1px #ddd
|
||||
|
||||
> p
|
||||
display block
|
||||
padding 10px 16px
|
||||
margin 0
|
||||
color #666
|
||||
cursor pointer
|
||||
|
||||
-ms-user-select none
|
||||
-moz-user-select none
|
||||
-webkit-user-select none
|
||||
user-select none
|
||||
|
||||
transition margin-left 0.2s ease
|
||||
|
||||
> i
|
||||
margin-right 4px
|
||||
|
||||
&:hover
|
||||
color #555
|
||||
|
||||
&.active
|
||||
margin-left 8px
|
||||
color $theme-color !important
|
||||
|
||||
> .pages
|
||||
position absolute
|
||||
top 0
|
||||
left 200px
|
||||
width calc(100% - 200px)
|
||||
|
||||
> section
|
||||
padding 32px
|
||||
|
||||
// & + section
|
||||
// margin-top 16px
|
||||
|
||||
h1
|
||||
display block
|
||||
margin 0
|
||||
padding 0 0 8px 0
|
||||
font-size 1em
|
||||
color #555
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
label
|
||||
display block
|
||||
margin 16px 0
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> p
|
||||
margin 0 0 8px 0
|
||||
font-weight bold
|
||||
color #373a3c
|
||||
|
||||
&.checkbox
|
||||
> input
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
|
||||
&:checked + p
|
||||
color $theme-color
|
||||
|
||||
> p
|
||||
width calc(100% - 32px)
|
||||
margin 0 0 0 32px
|
||||
font-weight bold
|
||||
|
||||
&:last-child
|
||||
font-weight normal
|
||||
color #999
|
||||
|
||||
&.account
|
||||
> .general
|
||||
> .avatar
|
||||
> img
|
||||
display block
|
||||
float left
|
||||
width 64px
|
||||
height 64px
|
||||
border-radius 4px
|
||||
|
||||
> button
|
||||
float left
|
||||
margin-left 8px
|
||||
|
||||
&.api
|
||||
code
|
||||
padding 4px
|
||||
background #eee
|
||||
|
||||
.regenerate
|
||||
display inline
|
||||
color $theme-color
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \api
|
||||
@mixin \dialog
|
||||
@mixin \update-avatar
|
||||
@mixin \update-wallpaper
|
||||
|
||||
@page = \account
|
||||
|
||||
@set-page = (page) ~>
|
||||
@page = page
|
||||
|
||||
@avatar = ~>
|
||||
@update-avatar @I, (i) ~>
|
||||
@update-i i
|
||||
|
||||
@wallpaper = ~>
|
||||
@update-wallpaper @I, (i) ~>
|
||||
@update-i i
|
||||
|
||||
@update-account = ~>
|
||||
@api \i/update do
|
||||
name: @refs.account-name.value
|
||||
location: @refs.account-location.value
|
||||
bio: @refs.account-bio.value
|
||||
.then (i) ~>
|
||||
@update-i i
|
||||
alert \ok
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
|
||||
@update-cache = ~>
|
||||
@I.data.cache = !@I.data.cache
|
||||
@api \i/appdata/set do
|
||||
data: JSON.stringify do
|
||||
cache: @I.data.cache
|
||||
.then ~>
|
||||
@update-i!
|
||||
|
||||
@update-debug = ~>
|
||||
@I.data.debug = !@I.data.debug
|
||||
@api \i/appdata/set do
|
||||
data: JSON.stringify do
|
||||
debug: @I.data.debug
|
||||
.then ~>
|
||||
@update-i!
|
||||
73
src/web/app/desktop/tags/signin-history.tag
Normal file
73
src/web/app/desktop/tags/signin-history.tag
Normal file
@@ -0,0 +1,73 @@
|
||||
mk-signin-history
|
||||
div.records(if={ history.length != 0 })
|
||||
div(each={ history })
|
||||
mk-time(time={ created_at })
|
||||
header
|
||||
i.fa.fa-check(if={ success })
|
||||
i.fa.fa-times(if={ !success })
|
||||
span.ip { ip }
|
||||
pre: code { JSON.stringify(headers, null, ' ') }
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> .records
|
||||
> div
|
||||
padding 16px 0 0 0
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> header
|
||||
|
||||
> i
|
||||
margin-right 8px
|
||||
|
||||
&.fa-check
|
||||
color #0fda82
|
||||
|
||||
&.fa-times
|
||||
color #ff3100
|
||||
|
||||
> .ip
|
||||
display inline-block
|
||||
color #444
|
||||
background #f8f8f8
|
||||
|
||||
> mk-time
|
||||
position absolute
|
||||
top 16px
|
||||
right 0
|
||||
color #777
|
||||
|
||||
> pre
|
||||
overflow auto
|
||||
max-height 100px
|
||||
|
||||
> code
|
||||
white-space pre-wrap
|
||||
word-break break-all
|
||||
color #4a535a
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \stream
|
||||
|
||||
@history = []
|
||||
@fetching = true
|
||||
|
||||
@on \mount ~>
|
||||
@api \i/signin_history
|
||||
.then (history) ~>
|
||||
@history = history
|
||||
@fetching = false
|
||||
@update!
|
||||
.catch (err) ~>
|
||||
console.error err
|
||||
|
||||
@stream.on \signin @on-signin
|
||||
|
||||
@on \unmount ~>
|
||||
@stream.off \signin @on-signin
|
||||
|
||||
@on-signin = (signin) ~>
|
||||
@history.unshift signin
|
||||
@update!
|
||||
59
src/web/app/desktop/tags/stream-indicator.tag
Normal file
59
src/web/app/desktop/tags/stream-indicator.tag
Normal file
@@ -0,0 +1,59 @@
|
||||
mk-stream-indicator
|
||||
p(if={ state == 'initializing' })
|
||||
i.fa.fa-spinner.fa-spin
|
||||
span
|
||||
| 接続中
|
||||
mk-ellipsis
|
||||
p(if={ state == 'reconnecting' })
|
||||
i.fa.fa-spinner.fa-spin
|
||||
span
|
||||
| 切断されました 接続中
|
||||
mk-ellipsis
|
||||
p(if={ state == 'connected' })
|
||||
i.fa.fa-check
|
||||
span 接続完了
|
||||
|
||||
style.
|
||||
display block
|
||||
pointer-events none
|
||||
position fixed
|
||||
z-index 16384
|
||||
bottom 8px
|
||||
right 8px
|
||||
margin 0
|
||||
padding 6px 12px
|
||||
font-size 0.9em
|
||||
color #fff
|
||||
background rgba(0, 0, 0, 0.8)
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
|
||||
> i
|
||||
margin-right 0.25em
|
||||
|
||||
script.
|
||||
@mixin \stream
|
||||
|
||||
@on \before-mount ~>
|
||||
@state = @get-stream-state!
|
||||
|
||||
if @state == \connected
|
||||
@root.style.opacity = 0
|
||||
|
||||
@stream-state-ev.on \connected ~>
|
||||
@state = @get-stream-state!
|
||||
@update!
|
||||
set-timeout ~>
|
||||
Velocity @root, {
|
||||
opacity: 0
|
||||
} 200ms \linear
|
||||
, 1000ms
|
||||
|
||||
@stream-state-ev.on \closed ~>
|
||||
@state = @get-stream-state!
|
||||
@update!
|
||||
Velocity @root, {
|
||||
opacity: 1
|
||||
} 0ms
|
||||
37
src/web/app/desktop/tags/sub-post-content.tag
Normal file
37
src/web/app/desktop/tags/sub-post-content.tag
Normal file
@@ -0,0 +1,37 @@
|
||||
mk-sub-post-content
|
||||
div.body
|
||||
a.reply(if={ post.reply_to_id }): i.fa.fa-reply
|
||||
span@text
|
||||
a.quote(if={ post.repost_id }, href={ '/post:' + post.repost_id }) RP: ...
|
||||
details(if={ post.media })
|
||||
summary ({ post.media.length }枚の画像)
|
||||
mk-images-viewer(images={ post.media })
|
||||
|
||||
style.
|
||||
display block
|
||||
word-wrap break-word
|
||||
|
||||
> .body
|
||||
> .reply
|
||||
margin-right 6px
|
||||
color #717171
|
||||
|
||||
> .quote
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
script.
|
||||
@mixin \text
|
||||
@mixin \user-preview
|
||||
|
||||
@post = @opts.post
|
||||
|
||||
@on \mount ~>
|
||||
if @post.text?
|
||||
tokens = @analyze @post.text
|
||||
@refs.text.innerHTML = @compile tokens, false
|
||||
|
||||
@refs.text.children.for-each (e) ~>
|
||||
if e.tag-name == \MK-URL
|
||||
riot.mount e
|
||||
95
src/web/app/desktop/tags/timeline-post-sub.tag
Normal file
95
src/web/app/desktop/tags/timeline-post-sub.tag
Normal file
@@ -0,0 +1,95 @@
|
||||
mk-timeline-post-sub(title={ title })
|
||||
article
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username })
|
||||
img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ post.user_id })
|
||||
div.main
|
||||
header
|
||||
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id })
|
||||
| { post.user.name }
|
||||
span.username
|
||||
| @{ post.user.username }
|
||||
a.created-at(href={ CONFIG.url + '/' + post.user.username + '/' + post.id })
|
||||
mk-time(time={ post.created_at })
|
||||
div.body
|
||||
mk-sub-post-content.text(post={ post })
|
||||
|
||||
script.
|
||||
@mixin \date-stringify
|
||||
@mixin \user-preview
|
||||
|
||||
@post = @opts.post
|
||||
|
||||
@title = @date-stringify @post.created_at
|
||||
|
||||
style.
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 0.9em
|
||||
|
||||
> article
|
||||
padding 16px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 14px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 52px
|
||||
height 52px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 66px)
|
||||
|
||||
> header
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
line-height 21px
|
||||
|
||||
> .name
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
color #607073
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color #d1d8da
|
||||
|
||||
> .created-at
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
color #b2b8bb
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
376
src/web/app/desktop/tags/timeline-post.tag
Normal file
376
src/web/app/desktop/tags/timeline-post.tag
Normal file
@@ -0,0 +1,376 @@
|
||||
mk-timeline-post(tabindex='-1', title={ title }, onkeydown={ on-key-down })
|
||||
|
||||
div.reply-to(if={ p.reply_to })
|
||||
mk-timeline-post-sub(post={ p.reply_to })
|
||||
|
||||
div.repost(if={ is-repost })
|
||||
p
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id }): img.avatar(src={ post.user.avatar_url + '?thumbnail&size=32' }, alt='avatar')
|
||||
i.fa.fa-retweet
|
||||
a.name(href={ CONFIG.url + '/' + post.user.username }, data-user-preview={ post.user_id }) { post.user.name }
|
||||
| がRepost
|
||||
mk-time(time={ post.created_at })
|
||||
|
||||
article
|
||||
a.avatar-anchor(href={ CONFIG.url + '/' + p.user.username })
|
||||
img.avatar(src={ p.user.avatar_url + '?thumbnail&size=64' }, alt='avatar', data-user-preview={ p.user.id })
|
||||
div.main
|
||||
header
|
||||
a.name(href={ CONFIG.url + '/' + p.user.username }, data-user-preview={ p.user.id })
|
||||
| { p.user.name }
|
||||
span.username
|
||||
| @{ p.user.username }
|
||||
a.created-at(href={ url })
|
||||
mk-time(time={ p.created_at })
|
||||
div.body
|
||||
div.text
|
||||
a.reply(if={ p.reply_to }): i.fa.fa-reply
|
||||
span@text
|
||||
a.quote(if={ p.repost != null }) RP:
|
||||
div.media(if={ p.media })
|
||||
mk-images-viewer(images={ p.media })
|
||||
div.repost(if={ p.repost })
|
||||
i.fa.fa-quote-right.fa-flip-horizontal
|
||||
mk-post-preview.repost(post={ p.repost })
|
||||
footer
|
||||
button(onclick={ reply }, title='返信')
|
||||
i.fa.fa-reply
|
||||
p.count(if={ p.replies_count > 0 }) { p.replies_count }
|
||||
button(onclick={ repost }, title='Repost')
|
||||
i.fa.fa-retweet
|
||||
p.count(if={ p.repost_count > 0 }) { p.repost_count }
|
||||
button(class={ liked: p.is_liked }, onclick={ like }, title='善哉')
|
||||
i.fa.fa-thumbs-o-up
|
||||
p.count(if={ p.likes_count > 0 }) { p.likes_count }
|
||||
button(onclick={ NotImplementedException }): i.fa.fa-ellipsis-h
|
||||
button(onclick={ toggle-detail }, title='詳細')
|
||||
i.fa.fa-caret-down(if={ !is-detail-opened })
|
||||
i.fa.fa-caret-up(if={ is-detail-opened })
|
||||
div.detail(if={ is-detail-opened })
|
||||
mk-post-status-graph(width='462', height='130', post={ p })
|
||||
|
||||
style.
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
background #fff
|
||||
|
||||
&:focus
|
||||
z-index 1
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top 2px
|
||||
right 2px
|
||||
bottom 2px
|
||||
left 2px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 4px
|
||||
|
||||
> .repost
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px 32px
|
||||
line-height 28px
|
||||
|
||||
.avatar-anchor
|
||||
display inline-block
|
||||
|
||||
.avatar
|
||||
vertical-align bottom
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
i
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
> mk-time
|
||||
position absolute
|
||||
top 16px
|
||||
right 32px
|
||||
font-size 0.9em
|
||||
line-height 28px
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
padding 0 16px
|
||||
background rgba(0, 0, 0, 0.0125)
|
||||
|
||||
> mk-post-preview
|
||||
background transparent
|
||||
|
||||
> article
|
||||
padding 28px 32px 18px 32px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 16px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 58px
|
||||
height 58px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 74px)
|
||||
|
||||
> header
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
line-height 24px
|
||||
|
||||
> .name
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
color #777
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color #ccc
|
||||
|
||||
> .created-at
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
font-size 0.9em
|
||||
color #c0c0c0
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
word-wrap break-word
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
||||
> .quote
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
> .media
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
||||
> .repost
|
||||
margin 8px 0
|
||||
|
||||
> i:first-child
|
||||
position absolute
|
||||
top -8px
|
||||
left -8px
|
||||
z-index 1
|
||||
color #c0dac6
|
||||
font-size 28px
|
||||
background #fff
|
||||
|
||||
> mk-post-preview
|
||||
padding 16px
|
||||
border dashed 1px #c0dac6
|
||||
border-radius 8px
|
||||
|
||||
> footer
|
||||
> button
|
||||
margin 0 28px 0 0
|
||||
padding 0 8px
|
||||
line-height 32px
|
||||
font-size 1em
|
||||
color #ddd
|
||||
background transparent
|
||||
border none
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
color #666
|
||||
|
||||
> .count
|
||||
display inline
|
||||
margin 0 0 0 8px
|
||||
color #999
|
||||
|
||||
&.liked
|
||||
color $theme-color
|
||||
|
||||
&:last-child
|
||||
position absolute
|
||||
right 0
|
||||
margin 0
|
||||
|
||||
> .detail
|
||||
padding-top 4px
|
||||
background rgba(0, 0, 0, 0.0125)
|
||||
|
||||
style(theme='dark').
|
||||
background #0D0D0D
|
||||
|
||||
> article
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #eee
|
||||
|
||||
> .main
|
||||
> header
|
||||
> .left
|
||||
> .name
|
||||
color #9e9c98
|
||||
|
||||
> .username
|
||||
color #41403f
|
||||
|
||||
> .right
|
||||
> .time
|
||||
color #4e4d4b
|
||||
|
||||
> .body
|
||||
> .text
|
||||
color #9e9c98
|
||||
|
||||
> footer
|
||||
> button
|
||||
color #9e9c98
|
||||
|
||||
&:hover
|
||||
color #fff
|
||||
|
||||
> .count
|
||||
color #eee
|
||||
|
||||
script.
|
||||
@mixin \api
|
||||
@mixin \text
|
||||
@mixin \date-stringify
|
||||
@mixin \user-preview
|
||||
@mixin \NotImplementedException
|
||||
|
||||
@post = @opts.post
|
||||
@is-repost = @post.repost? and !@post.text?
|
||||
@p = if @is-repost then @post.repost else @post
|
||||
|
||||
@title = @date-stringify @p.created_at
|
||||
|
||||
@url = CONFIG.url + '/' + @p.user.username + '/' + @p.id
|
||||
@is-detail-opened = false
|
||||
|
||||
@on \mount ~>
|
||||
if @p.text?
|
||||
tokens = if @p._highlight?
|
||||
then @analyze @p._highlight
|
||||
else @analyze @p.text
|
||||
|
||||
@refs.text.innerHTML = if @p._highlight?
|
||||
then @compile tokens, true, false
|
||||
else @compile tokens
|
||||
|
||||
@refs.text.children.for-each (e) ~>
|
||||
if e.tag-name == \MK-URL
|
||||
riot.mount e
|
||||
|
||||
# URLをプレビュー
|
||||
tokens
|
||||
.filter (t) -> t.type == \link
|
||||
.map (t) ~>
|
||||
@preview = @refs.text.append-child document.create-element \mk-url-preview
|
||||
riot.mount @preview, do
|
||||
url: t.content
|
||||
|
||||
@reply = ~>
|
||||
form = document.body.append-child document.create-element \mk-post-form-window
|
||||
riot.mount form, do
|
||||
reply: @p
|
||||
|
||||
@repost = ~>
|
||||
form = document.body.append-child document.create-element \mk-repost-form-window
|
||||
riot.mount form, do
|
||||
post: @p
|
||||
|
||||
@like = ~>
|
||||
if @p.is_liked
|
||||
@api \posts/likes/delete do
|
||||
post_id: @p.id
|
||||
.then ~>
|
||||
@p.is_liked = false
|
||||
@update!
|
||||
else
|
||||
@api \posts/likes/create do
|
||||
post_id: @p.id
|
||||
.then ~>
|
||||
@p.is_liked = true
|
||||
@update!
|
||||
|
||||
@toggle-detail = ~>
|
||||
@is-detail-opened = !@is-detail-opened
|
||||
@update!
|
||||
|
||||
@on-key-down = (e) ~>
|
||||
should-be-cancel = true
|
||||
switch
|
||||
| e.which == 38 or e.which == 74 or (e.which == 9 and e.shift-key) => # ↑, j or Shift+Tab
|
||||
focus @root, (e) -> e.previous-element-sibling
|
||||
| e.which == 40 or e.which == 75 or e.which == 9 => # ↓, k or Tab
|
||||
focus @root, (e) -> e.next-element-sibling
|
||||
| e.which == 69 => # e
|
||||
@repost!
|
||||
| e.which == 70 or e.which == 76 => # f or l
|
||||
@like!
|
||||
| e.which == 82 => # r
|
||||
@reply!
|
||||
| _ =>
|
||||
should-be-cancel = false
|
||||
|
||||
if should-be-cancel
|
||||
e.prevent-default!
|
||||
|
||||
function focus(el, fn)
|
||||
target = fn el
|
||||
if target?
|
||||
if target.has-attribute \tabindex
|
||||
target.focus!
|
||||
else
|
||||
focus target, fn
|
||||
86
src/web/app/desktop/tags/timeline.tag
Normal file
86
src/web/app/desktop/tags/timeline.tag
Normal file
@@ -0,0 +1,86 @@
|
||||
mk-timeline
|
||||
virtual(each={ post, i in posts })
|
||||
mk-timeline-post(post={ post })
|
||||
p.date(if={ i != posts.length - 1 && post._date != posts[i + 1]._date })
|
||||
span
|
||||
i.fa.fa-angle-up
|
||||
| { post._datetext }
|
||||
span
|
||||
i.fa.fa-angle-down
|
||||
| { posts[i + 1]._datetext }
|
||||
footer(data-yield='footer')
|
||||
| <yield from="footer"/>
|
||||
|
||||
style.
|
||||
display block
|
||||
|
||||
> mk-timeline-post
|
||||
border-bottom solid 1px #eaeaea
|
||||
|
||||
&:first-child
|
||||
border-top-left-radius 4px
|
||||
border-top-right-radius 4px
|
||||
|
||||
&:last-of-type
|
||||
border-bottom none
|
||||
|
||||
> .date
|
||||
display block
|
||||
margin 0
|
||||
line-height 32px
|
||||
font-size 14px
|
||||
text-align center
|
||||
color #aaa
|
||||
background #fdfdfd
|
||||
border-bottom solid 1px #eaeaea
|
||||
|
||||
span
|
||||
margin 0 16px
|
||||
|
||||
i
|
||||
margin-right 8px
|
||||
|
||||
> footer
|
||||
padding 16px
|
||||
text-align center
|
||||
color #ccc
|
||||
border-top solid 1px #eaeaea
|
||||
border-bottom-left-radius 4px
|
||||
border-bottom-right-radius 4px
|
||||
|
||||
style(theme='dark').
|
||||
> mk-timeline-post
|
||||
border-bottom-color #222221
|
||||
|
||||
script.
|
||||
@posts = []
|
||||
|
||||
@set-posts = (posts) ~>
|
||||
@posts = posts
|
||||
@update!
|
||||
|
||||
@prepend-posts = (posts) ~>
|
||||
posts.for-each (post) ~>
|
||||
@posts.push post
|
||||
@update!
|
||||
|
||||
@add-post = (post) ~>
|
||||
@posts.unshift post
|
||||
@update!
|
||||
|
||||
@clear = ~>
|
||||
@posts = []
|
||||
@update!
|
||||
|
||||
@focus = ~>
|
||||
@root.children.0.focus!
|
||||
|
||||
@on \update ~>
|
||||
@posts.for-each (post) ~>
|
||||
date = (new Date post.created_at).get-date!
|
||||
month = (new Date post.created_at).get-month! + 1
|
||||
post._date = date
|
||||
post._datetext = month + '月 ' + date + '日'
|
||||
|
||||
@tail = ~>
|
||||
@posts[@posts.length - 1]
|
||||
219
src/web/app/desktop/tags/ui-header-account.tag
Normal file
219
src/web/app/desktop/tags/ui-header-account.tag
Normal file
@@ -0,0 +1,219 @@
|
||||
mk-ui-header-account
|
||||
button.header(data-active={ is-open.toString() }, onclick={ toggle })
|
||||
span.username
|
||||
| { I.username }
|
||||
i.fa.fa-angle-down(if={ !is-open })
|
||||
i.fa.fa-angle-up(if={ is-open })
|
||||
img.avatar(src={ I.avatar_url + '?thumbnail&size=64' }, alt='avatar')
|
||||
div.menu(if={ is-open })
|
||||
ul
|
||||
li: a(href={ '/' + I.username })
|
||||
i.fa.fa-user
|
||||
| プロフィール
|
||||
i.fa.fa-angle-right
|
||||
li(onclick={ drive }): p
|
||||
i.fa.fa-cloud
|
||||
| ドライブ
|
||||
i.fa.fa-angle-right
|
||||
li: a(href='/i>mentions')
|
||||
i.fa.fa-at
|
||||
| あなた宛て
|
||||
i.fa.fa-angle-right
|
||||
ul
|
||||
li(onclick={ settings }): p
|
||||
i.fa.fa-cog
|
||||
| 設定
|
||||
i.fa.fa-angle-right
|
||||
ul
|
||||
li(onclick={ signout }): p
|
||||
i(class='fa fa-power-off')
|
||||
| サインアウト
|
||||
i.fa.fa-angle-right
|
||||
|
||||
style.
|
||||
display block
|
||||
float left
|
||||
|
||||
> .header
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
color #9eaba8
|
||||
border none
|
||||
background transparent
|
||||
cursor pointer
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
|
||||
&:hover
|
||||
color darken(#9eaba8, 20%)
|
||||
|
||||
&:active
|
||||
color darken(#9eaba8, 30%)
|
||||
|
||||
&[data-active='true']
|
||||
color darken(#9eaba8, 20%)
|
||||
|
||||
> .avatar
|
||||
$saturate = 150%
|
||||
filter saturate($saturate)
|
||||
-webkit-filter saturate($saturate)
|
||||
-moz-filter saturate($saturate)
|
||||
-ms-filter saturate($saturate)
|
||||
|
||||
> .username
|
||||
display block
|
||||
float left
|
||||
margin 0 12px 0 16px
|
||||
max-width 16em
|
||||
line-height 48px
|
||||
font-weight bold
|
||||
font-family Meiryo, sans-serif
|
||||
text-decoration none
|
||||
|
||||
i
|
||||
margin-left 8px
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
float left
|
||||
min-width 32px
|
||||
max-width 32px
|
||||
min-height 32px
|
||||
max-height 32px
|
||||
margin 8px 8px 8px 0
|
||||
border-radius 4px
|
||||
transition filter 100ms ease
|
||||
|
||||
> .menu
|
||||
display block
|
||||
position absolute
|
||||
top 56px
|
||||
right -2px
|
||||
width 230px
|
||||
font-size 0.8em
|
||||
background #fff
|
||||
border-radius 4px
|
||||
box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top -28px
|
||||
right 12px
|
||||
border-top solid 14px transparent
|
||||
border-right solid 14px transparent
|
||||
border-bottom solid 14px rgba(0, 0, 0, 0.1)
|
||||
border-left solid 14px transparent
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top -27px
|
||||
right 12px
|
||||
border-top solid 14px transparent
|
||||
border-right solid 14px transparent
|
||||
border-bottom solid 14px #fff
|
||||
border-left solid 14px transparent
|
||||
|
||||
ul
|
||||
display block
|
||||
margin 10px 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
& + ul
|
||||
padding-top 10px
|
||||
border-top solid 1px #eee
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
> a
|
||||
> p
|
||||
display block
|
||||
z-index 1
|
||||
padding 0 28px
|
||||
margin 0
|
||||
line-height 40px
|
||||
color #868C8C
|
||||
cursor pointer
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
|
||||
> i:first-of-type
|
||||
margin-right 6px
|
||||
|
||||
> i:last-of-type
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 8px
|
||||
z-index 1
|
||||
padding 0 20px
|
||||
font-size 1.2em
|
||||
line-height 40px
|
||||
|
||||
&:hover, &:active
|
||||
text-decoration none
|
||||
background $theme-color
|
||||
color $theme-color-foreground
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \signout
|
||||
|
||||
@is-open = false
|
||||
|
||||
@on \before-unmount ~>
|
||||
@close!
|
||||
|
||||
@toggle = ~>
|
||||
if @is-open
|
||||
@close!
|
||||
else
|
||||
@open!
|
||||
|
||||
@open = ~>
|
||||
@is-open = true
|
||||
@update!
|
||||
all = document.query-selector-all 'body *'
|
||||
Array.prototype.for-each.call all, (el) ~>
|
||||
el.add-event-listener \mousedown @mousedown
|
||||
|
||||
@close = ~>
|
||||
@is-open = false
|
||||
@update!
|
||||
all = document.query-selector-all 'body *'
|
||||
Array.prototype.for-each.call all, (el) ~>
|
||||
el.remove-event-listener \mousedown @mousedown
|
||||
|
||||
@mousedown = (e) ~>
|
||||
e.prevent-default!
|
||||
if (!contains @root, e.target) and (@root != e.target)
|
||||
@close!
|
||||
return false
|
||||
|
||||
@drive = ~>
|
||||
@close!
|
||||
riot.mount document.body.append-child document.create-element \mk-drive-browser-window
|
||||
|
||||
@settings = ~>
|
||||
@close!
|
||||
riot.mount document.body.append-child document.create-element \mk-settings-window
|
||||
|
||||
function contains(parent, child)
|
||||
node = child.parent-node
|
||||
while node?
|
||||
if node == parent
|
||||
return true
|
||||
node = node.parent-node
|
||||
return false
|
||||
82
src/web/app/desktop/tags/ui-header-clock.tag
Normal file
82
src/web/app/desktop/tags/ui-header-clock.tag
Normal file
@@ -0,0 +1,82 @@
|
||||
mk-ui-header-clock
|
||||
div.header
|
||||
time@time
|
||||
div.content
|
||||
mk-analog-clock
|
||||
|
||||
style.
|
||||
display inline-block
|
||||
overflow visible
|
||||
|
||||
> .header
|
||||
padding 0 12px
|
||||
text-align center
|
||||
font-size 0.5em
|
||||
|
||||
&, *
|
||||
cursor: default
|
||||
|
||||
&:hover
|
||||
background #899492
|
||||
|
||||
& + .content
|
||||
visibility visible
|
||||
|
||||
> time
|
||||
color #fff !important
|
||||
|
||||
*
|
||||
color #fff !important
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> time
|
||||
display table-cell
|
||||
vertical-align middle
|
||||
height 48px
|
||||
color #9eaba8
|
||||
|
||||
> .yyyymmdd
|
||||
opacity 0.7
|
||||
|
||||
> .content
|
||||
visibility hidden
|
||||
display block
|
||||
position absolute
|
||||
top auto
|
||||
right 0
|
||||
z-index 3
|
||||
margin 0
|
||||
padding 0
|
||||
width 256px
|
||||
background #899492
|
||||
|
||||
script.
|
||||
@draw = ~>
|
||||
now = new Date!
|
||||
|
||||
yyyy = now.get-full-year!
|
||||
mm = (\0 + (now.get-month! + 1)).slice -2
|
||||
dd = (\0 + now.get-date!).slice -2
|
||||
yyyymmdd = "<span class='yyyymmdd'>#yyyy/#mm/#dd</span>"
|
||||
|
||||
hh = (\0 + now.get-hours!).slice -2
|
||||
mm = (\0 + now.get-minutes!).slice -2
|
||||
hhmm = "<span class='hhmm'>#hh:#mm</span>"
|
||||
|
||||
if now.get-seconds! % 2 == 0
|
||||
hhmm .= replace \: '<span style=\'visibility:visible\'>:</span>'
|
||||
else
|
||||
hhmm .= replace \: '<span style=\'visibility:hidden\'>:</span>'
|
||||
|
||||
@refs.time.innerHTML = "#yyyymmdd<br>#hhmm"
|
||||
|
||||
@on \mount ~>
|
||||
@draw!
|
||||
@clock = set-interval @draw, 1000ms
|
||||
|
||||
@on \unmount ~>
|
||||
clear-interval @clock
|
||||
113
src/web/app/desktop/tags/ui-header-nav.tag
Normal file
113
src/web/app/desktop/tags/ui-header-nav.tag
Normal file
@@ -0,0 +1,113 @@
|
||||
mk-ui-header-nav: ul(if={ SIGNIN })
|
||||
li.home(class={ active: page == 'home' }): a(href={ CONFIG.url })
|
||||
i.fa.fa-home
|
||||
p ホーム
|
||||
li.messaging: a(onclick={ messaging })
|
||||
i.fa.fa-comments
|
||||
p メッセージ
|
||||
i.fa.fa-circle(if={ has-unread-messaging-messages })
|
||||
li.info: a(href='https://twitter.com/misskey_xyz', target='_blank')
|
||||
i.fa.fa-info
|
||||
p お知らせ
|
||||
li.tv: a(href='https://misskey.tk', target='_blank')
|
||||
i.fa.fa-television
|
||||
p MisskeyTV™
|
||||
|
||||
style.
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0
|
||||
line-height 3rem
|
||||
vertical-align top
|
||||
|
||||
> ul
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0
|
||||
vertical-align top
|
||||
line-height 3rem
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display inline-block
|
||||
vertical-align top
|
||||
height 48px
|
||||
line-height 48px
|
||||
|
||||
&.active
|
||||
> a
|
||||
border-bottom solid 3px $theme-color
|
||||
|
||||
> a
|
||||
display inline-block
|
||||
z-index 1
|
||||
height 100%
|
||||
padding 0 24px
|
||||
font-size 1em
|
||||
font-variant small-caps
|
||||
color #9eaba8
|
||||
text-decoration none
|
||||
transition none
|
||||
cursor pointer
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
|
||||
&:hover
|
||||
color darken(#9eaba8, 20%)
|
||||
text-decoration none
|
||||
|
||||
> i:first-child
|
||||
margin-right 8px
|
||||
|
||||
> i:last-child
|
||||
margin-left 5px
|
||||
vertical-align super
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
@media (max-width 1100px)
|
||||
margin-left -5px
|
||||
|
||||
> p
|
||||
display inline
|
||||
margin 0
|
||||
|
||||
@media (max-width 1100px)
|
||||
display none
|
||||
|
||||
@media (max-width 700px)
|
||||
padding 0 12px
|
||||
|
||||
script.
|
||||
@mixin \i
|
||||
@mixin \api
|
||||
@mixin \stream
|
||||
|
||||
@page = @opts.page
|
||||
|
||||
@on \mount ~>
|
||||
@stream.on \read_all_messaging_messages @on-read-all-messaging-messages
|
||||
@stream.on \unread_messaging_message @on-unread-messaging-message
|
||||
|
||||
# Fetch count of unread messaging messages
|
||||
@api \messaging/unread
|
||||
.then (count) ~>
|
||||
if count.count > 0
|
||||
@has-unread-messaging-messages = true
|
||||
@update!
|
||||
|
||||
@on \unmount ~>
|
||||
@stream.off \read_all_messaging_messages @on-read-all-messaging-messages
|
||||
@stream.off \unread_messaging_message @on-unread-messaging-message
|
||||
|
||||
@on-read-all-messaging-messages = ~>
|
||||
@has-unread-messaging-messages = false
|
||||
@update!
|
||||
|
||||
@on-unread-messaging-message = ~>
|
||||
@has-unread-messaging-messages = true
|
||||
@update!
|
||||
|
||||
@messaging = ~>
|
||||
riot.mount document.body.append-child document.create-element \mk-messaging-window
|
||||
111
src/web/app/desktop/tags/ui-header-notifications.tag
Normal file
111
src/web/app/desktop/tags/ui-header-notifications.tag
Normal file
@@ -0,0 +1,111 @@
|
||||
mk-ui-header-notifications
|
||||
button.header(data-active={ is-open }, onclick={ toggle })
|
||||
i.fa.fa-bell-o
|
||||
div.notifications(if={ is-open })
|
||||
mk-notifications
|
||||
|
||||
style.
|
||||
display block
|
||||
float left
|
||||
|
||||
> .header
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
width 32px
|
||||
color #9eaba8
|
||||
border none
|
||||
background transparent
|
||||
cursor pointer
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
|
||||
&:hover
|
||||
color darken(#9eaba8, 20%)
|
||||
|
||||
&:active
|
||||
color darken(#9eaba8, 30%)
|
||||
|
||||
&[data-active='true']
|
||||
color darken(#9eaba8, 20%)
|
||||
|
||||
> i
|
||||
font-size 1.2em
|
||||
line-height 48px
|
||||
|
||||
> .notifications
|
||||
display block
|
||||
position absolute
|
||||
top 56px
|
||||
right -72px
|
||||
width 300px
|
||||
background #fff
|
||||
border-radius 4px
|
||||
box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top -28px
|
||||
right 74px
|
||||
border-top solid 14px transparent
|
||||
border-right solid 14px transparent
|
||||
border-bottom solid 14px rgba(0, 0, 0, 0.1)
|
||||
border-left solid 14px transparent
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top -27px
|
||||
right 74px
|
||||
border-top solid 14px transparent
|
||||
border-right solid 14px transparent
|
||||
border-bottom solid 14px #fff
|
||||
border-left solid 14px transparent
|
||||
|
||||
> mk-notifications
|
||||
max-height 350px
|
||||
font-size 1rem
|
||||
overflow auto
|
||||
|
||||
script.
|
||||
@is-open = false
|
||||
|
||||
@toggle = ~>
|
||||
if @is-open
|
||||
@close!
|
||||
else
|
||||
@open!
|
||||
|
||||
@open = ~>
|
||||
@is-open = true
|
||||
@update!
|
||||
all = document.query-selector-all 'body *'
|
||||
Array.prototype.for-each.call all, (el) ~>
|
||||
el.add-event-listener \mousedown @mousedown
|
||||
|
||||
@close = ~>
|
||||
@is-open = false
|
||||
@update!
|
||||
all = document.query-selector-all 'body *'
|
||||
Array.prototype.for-each.call all, (el) ~>
|
||||
el.remove-event-listener \mousedown @mousedown
|
||||
|
||||
@mousedown = (e) ~>
|
||||
e.prevent-default!
|
||||
if (!contains @root, e.target) and (@root != e.target)
|
||||
@close!
|
||||
return false
|
||||
|
||||
function contains(parent, child)
|
||||
node = child.parent-node
|
||||
while node?
|
||||
if node == parent
|
||||
return true
|
||||
node = node.parent-node
|
||||
return false
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user