Merge branch 'develop' into l10n_develop

This commit is contained in:
syuilo 2018-10-08 15:37:24 +09:00 committed by GitHub
commit 9c170c426b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
504 changed files with 11762 additions and 8715 deletions

View file

@ -1,18 +1,19 @@
#!/usr/bin/env bash
# BEARER_TOKEN=
# CAMPAIGN_ID=
# GITHUB_TOKEN=
# HEAD='acid-chicken:patch-autogen'
# REPO='syuilo/misskey'
test "$(curl -LSs -w '\n' -- "https://api.github.com/repos/$REPO/pulls?access_token=$GITHUB_TOKEN" | jq -r '.[].head.label' | grep $HEAD)" && exit 1
# __MISSKEY_BEARER_TOKEN=
# __MISSKEY_CAMPAIGN_ID=
# __MISSKEY_GITHUB_TOKEN=
# __MISSKEY_HEAD=acid-chicken:patch-autogen
# __MISSKEY_REPO=syuilo/misskey
# __MISSKEY_BRANCH=develop
test "$(curl -LSs -w '\n' -- "https://api.github.com/repos/$REPO/pulls?access_token=$__MISSKEY_GITHUB_TOKEN" | jq -r '.[].head.label' | grep $__MISSKEY_HEAD)" && exit 1
cd "$(dirname $0)/.." && \
touch null.cache && \
rm *.cache && \
git checkout master && \
git pull origin master && \
git pull upstream master && \
git checkout $__MISSKEY_BRANCH && \
git pull origin $__MISSKEY_BRANCH && \
git pull upstream $__MISSKEY_BRANCH && \
git stash && \
git rebase -f upstream/master && \
git rebase -f upstream/$__MISSKEY_BRANCH && \
git branch patch-autogen && \
git checkout patch-autogen && \
git reset --hard HEAD || \
@ -20,12 +21,12 @@ exit 1
touch patreon.md.cache && \
rm patreon.md.cache && \
echo '<!-- PATREON_START -->' > patreon.md.cache && \
URL="https://www.patreon.com/api/oauth2/v2/campaigns/$CAMPAIGN_ID/members?include=currently_entitled_tiers,user&fields%5Btier%5D=title&fields%5Buser%5D=full_name,thumb_url,url,hide_pledges"
url="https://www.patreon.com/api/oauth2/v2/campaigns/$__MISSKEY_CAMPAIGN_ID/members?include=currently_entitled_tiers,user&fields%5Btier%5D=title&fields%5Buser%5D=full_name,thumb_url,url,hide_pledges"
while :
do
touch patreon.raw.cache && \
rm patreon.raw.cache && \
curl -LSs -w '\n' -H "Authorization: Bearer $BEARER_TOKEN" -- $URL > patreon.raw.cache && \
curl -LSs -w '\n' -H "Authorization: Bearer $__MISSKEY_BEARER_TOKEN" -- $url > patreon.raw.cache && \
touch patreon.cache && \
rm patreon.cache && \
cat patreon.raw.cache | \
@ -42,31 +43,31 @@ while :
xargs -I% echo '<td><a href="%</a></td>' >> patreon.md.cache && \
echo '</tr></table>' >> patreon.md.cache || \
exit 1
NEW_URL="$(cat patreon.raw.cache | jq -r '.links.next')"
test "$NEW_URL" = 'null' && \
new_url="$(cat patreon.raw.cache | jq -r '.links.next')"
test "$new_url" = 'null' && \
break || \
URL="$NEW_URL"
URL="$url"
done
IGNORE= && \
ignore= && \
echo -e "\n**Last updated:** $(date -uR | sed 's/\+0000/UTC/')\n<!-- PATREON_END -->" >> patreon.md.cache && \
touch README.md && \
touch .autogen/README.md && \
rm .autogen/README.md && \
mv README.md .autogen/README.md && \
cat .autogen/README.md | while IFS= read LINE;
cat .autogen/README.md | while IFS= read line;
do
if [[ -z "$IGNORE" ]]
if [[ -z "$ignore" ]]
then
if [[ "$LINE" = '<!-- PATREON_START -->' ]]
if [[ "$line" = '<!-- PATREON_START -->' ]]
then
IGNORE='PATREON_INSIDE'
ignore='PATREON_INSIDE'
else
echo "$LINE" >> README.md
echo "$line" >> README.md
fi
else
if [[ "$LINE" = '<!-- PATREON_END -->' ]]
then
IGNORE=
ignore=
cat patreon.md.cache >> README.md
fi
fi
@ -80,7 +81,7 @@ test 4 -lt $(cat diff.cache | wc -l) && \
git add README.md && \
git commit -m 'Update README.md [AUTOGEN]' && \
git push -f origin patch-autogen && \
curl -LSs -w '\n' -X POST -d '{"title":"[AUTOMATED] Update README.md","body":"*This pull request was created by a tool.*","head":"'$HEAD'","base":"master"}' -- "https://api.github.com/repos/$REPO/pulls?access_token=$GITHUB_TOKEN"
curl -LSs -w '\n' -X POST -d '{"title":"[AUTOMATED] Update README.md","body":"*This pull request was created by a tool.*","head":"'$__MISSKEY_HEAD'","base":"'$__MISSKEY_BRANCH'"}' -- "https://api.github.com/repos/$__MISSKEY_REPO/pulls?access_token=$__MISSKEY_GITHUB_TOKEN"
git stash
git checkout master
git checkout $__MISSKEY_BRANCH
git branch -D patch-autogen

View file

@ -7,27 +7,51 @@ maintainer:
repository_url: https://github.com/syuilo/misskey # Repository URL
feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue)
# URL and Port settings overview
# e.g., If you want to realize following structure:
#
# +--- https://example.com:123 ----------+
# +------+ |+-------------+ +---------------+|
# | User | ---> || Proxy (123) | ---> | Misskey (456) ||
# +------+ |+-------------+ +---------------+|
# +--------------------------------------+
#
# You need to set 'https://example.com:123' to 'url' prop and
# You need to set 456 to 'port' prop.
#
# In other words, the 'url' prop should be the final accessible URL seen by a user.
# 'port' prop is a port that the Misskey server should actually listen
# on and it is not necessarily the port that a user accesses.
url: http://localhost/
# Final accessible URL seen by a user.
url: https://example.tld/
### Port and TLS settings ######################################
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. Nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
# port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
# port: 443
#
# https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
################################################################
# A port that your Misskey server should listen.
# This value is not a port to use when accessing with a browser.
port: 80
mongodb:
host: localhost
@ -98,12 +122,6 @@ drive:
# Below settings are optional
#
# TLS
# https:
# # path for certification
# key: example-tls-key
# cert: example-tls-cert
# Elasticsearch
# elasticsearch:
# host: localhost

2
.npmrc
View file

@ -1,2 +1,2 @@
save-exact=true
save-exact = true
package-lock = false

View file

@ -5,6 +5,94 @@ ChangeLog
This document describes breaking changes only.
10.0.0
------
ストリーミングAPIに破壊的変更があります。運営者がすべきことはありません。
変更は以下の通りです
* ストリーミングでやり取りする際の snake_case が全て camelCase に
* リバーシのストリームエンドポイント名が reversi → gamesReversi、reversiGame → gamesReversiGame に
* ストリーミングの個々のエンドポイントが廃止され、一旦元となるストリームに接続してから、個々のチャンネル(今までのエンドポイント)に接続します。詳細は後述します。
* ストリームから流れてくる、キャプチャした投稿の更新イベントに投稿自体のデータは含まれず、代わりにアクションが設定されるようになります。詳細は後述します。
* ストリームに接続する際に追加で指定していたパラメータ(トークン除く)が、URLにクエリとして含むのではなくチャンネル接続時にパラメータ指定するように
### 個々のエンドポイントが廃止されることによる新しいストリーミングAPIの利用方法
具体的には、まず https://example.misskey/streaming にwebsocket接続します。
次に、例えば「messaging」ストリーム(チャンネルと呼びます)に接続したいときは、ストリームに次のようなデータを送信します:
``` javascript
{
type: 'connect',
body: {
channel: 'messaging',
id: 'foobar',
params: {
otherparty: 'xxxxxxxxxxxx'
}
}
}
```
ここで、`id`にはそのチャンネルとやり取りするための任意のIDを設定します。
IDはチャンネルごとではなく「チャンネルの接続ごと」です。なぜなら、同じチャンネルに異なるパラメータで複数接続するケースもあるからです。
`params`はチャンネルに接続する際のパラメータです。チャンネルによって接続時に必要とされるパラメータは異なります。パラメータ不要のチャンネルに接続する際は、このプロパティは省略可能です。
チャンネルにメッセージを送信するには、次のようなデータを送信します:
``` javascript
{
type: 'channel',
body: {
id: 'foobar',
type: 'something',
body: {
some: 'thing'
}
}
}
```
ここで、`id`にはチャンネルに接続するときに指定したIDを設定します。
逆に、チャンネルからメッセージが流れてくると、次のようなデータが受信されます:
``` javascript
{
type: 'channel',
body: {
id: 'foobar',
type: 'something',
body: {
some: 'thing'
}
}
}
```
ここで、`id`にはチャンネルに接続するときに指定したIDが設定されています。
### 投稿のキャプチャに関する変更
投稿の更新イベントに投稿情報は含まれなくなりました。代わりに、その投稿が「リアクションされた」「アンケートに投票された」「削除された」といったアクション情報が設定されます。
具体的には次のようなデータが受信されます:
``` javascript
{
type: 'noteUpdated',
body: {
id: 'xxxxxxxxxxx',
type: 'reacted',
body: {
reaction: 'hmm'
}
}
}
```
* reacted ... 投稿にリアクションされた。`reaction`プロパティにリアクションコードが含まれます。
* pollVoted ... アンケートに投票された。`choice`プロパティに選択肢ID、`userId`に投票者IDが含まれます。
9.0.0
-----
Misskey v8.64.0 を使っている方は、9.0.0に際しては特にすべきことはありません。
Misskey v8.64.0 に満たないバージョンをお使いの方は、一旦8.64.0にアップデートして(そして起動して)から9.0.0に再度アップデートしてください。
8.0.0
-----
@ -47,13 +135,13 @@ Please run `node cli/migration/5.0.0` before launch.
オセロがリバーシに変更されました。
Othello is now Reversi.
Othello is rename to Reversi.
### Migration
MongoDBの、`othelloGames`と`othelloMatchings`コレクションをそれぞれ`reversiGames`と`reversiMatchings`にリネームしてください。
You need to rename `othelloGames` and `othelloMatchings` MongoDB collections to `reversiGames` and `reversiMatchings`.
Please rename `othelloGames` and `othelloMatchings` MongoDB collections to `reversiGames` and `reversiMatchings` respectively.
3.0.0
-----

View file

@ -1,27 +1,27 @@
# Contribution guide
:v: Misskeyへの貢献ありがとうございます。 :v:
:v: Thanks for your contributions :v:
## Issueの報告
新機能の提案や不具合の報告は https://github.com/syuilo/misskey/issues で管理しています。
Issueを作成する前に、既に同じIssueが作成されていないかご確認ください。
もし既にIssueが作成されている場合は、既存のIssueにコメントをしたりリアクションをするようお願いします。
## Issues
Feature suggestions and bug reports are filed in https://github.com/syuilo/misskey/issues .
Before creating a new issue, please search existing issues to avoid duplication.
If you find the existing issue, please add your reaction or comment to the issue.
## Issueの解決
[pr-welcomeのラベルがついているIssue](https://github.com/syuilo/misskey/labels/pr-welcome)
の解決を目的としたPull Requestを作成してくださると非常にありがたいです。
## Internationalization (i18n)
Please see [Translation guide](./docs/translate.en.md).
## 翻訳の改善
ソースコード中の `%i18n:id%` という形の文字列は、言語ファイルの対応するテキストに置換されます。
言語ファイルは /locales ディレクトリに存在します。
## Localization (l10n)
Please use [Crowdin](https://crowdin.com/project/misskey) for localization.
## ドキュメントの編集
現在Misskeyはドキュメントが大きく不足しています。
ドキュメントは /docs ディレクトリに存在します。
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## テストの追加
現在Misskeyはテストが大きく不足しています。
テストコードは /test ディレクトリに存在します。
## Documentation
* Documents for contributors are located in `/docs`.
* Documents for instance admins are located in `/docs`.
* Documents for end users are located in `src/docs`.
## 自動テスト及び自動リリース
Travis CIで行っています。
設定ファイルは /.travis に存在します。
## Test
* Test codes are located in `/test`.
## Continuous integration
Misskey uses Travis for automated test.
Configuration files are located in `/.travis`.

View file

@ -1,4 +1,4 @@
<img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/>
<img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/>
[![Misskey](/assets/title.png)](https://misskey.xyz/)
================================================================
@ -7,12 +7,12 @@
[![][dependencies-badge]][dependencies-link]
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/)
Sophisticated microblogging platform, evolving forever.
**Sophisticated microblogging platform, evolving forever.**
[Misskey](https://misskey.xyz) is a decentralized microblogging platform born on Earth.
Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms.
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet?
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? [Find instance!](https://joinmisskey.github.io/)
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
@ -20,52 +20,70 @@ Why don't you take a short break from the hustle and bustle of the city, and div
:sparkles: Features
----------------------------------------------------------------
* Rich text contents
* Reactions
* User lists
* Customizable column view (called MisskeyDeck)
* and widgets!
* Private messages
* ActivityPub support
and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz).
<img src="/assets/about/post.png" align="left" height="200px"/>
<h3 align="left">Posting</h3>
<p align="left">
Just post your idea, hot topics and anything you want to share. You may want to decorate your words, attach your favorite pictures, send files including movies and create a poll - those are the things you can do on Misskey!
</p>
---
<img src="/assets/about/reaction.png" align="right" height="200px"/>
<h3 align="right">Reactions</h3>
<p align="right">
Easiest way to tell your emotions. Misskey allows you to add various type of reactions to others post. The emotional experience on Misskey will never be on other SNSs which only able to push “likes”.
</p>
---
<img src="/assets/about/ui.png" align="left" height="200px"/>
<h3 align="left">Interface</h3>
<p align="left">
No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
</p>
---
<img src="/assets/about/drive.png" align="right" width="300px"/>
<h3 align="right">Misskey Drive</h3>
<p align="right">
Wanna post a picture you have already uploaded? Wish to organize, name and create a folder for your uploaded files? Misskey Drive is the best solution for you. Very easy to share your files online.
</p>
---
and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz) or [other instances](https://joinmisskey.github.io/).
:package: Create your own instance
----------------------------------------------------------------
If you want to run your own instance of Misskey,
please see [Setup and installation guide](./docs/setup.en.md).
Please see [Setup and installation guide](./docs/setup.en.md).
:wrench: Contribute
:wrench: Contribution
----------------------------------------------------------------
**[PR](https://github.com/syuilo/misskey/pulls)s welcome!**
### i18n
Please see [Translation guide](./docs/translate.en.md).
### l10n
Misskey is using Crowdin for l10n.
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)](https://crowdin.com/project/misskey)
Please see [Contribution guide](./CONTRIBUTING.md).
:heart: Backers & Sponsors
----------------------------------------------------------------
<!-- PATREON_START -->
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D" alt="39ff"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=Yd60FK_SWfQO56SeiJpy1tDHOnCV4xdEywQe8gn5_Wo%3D" alt="negao"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13099460/43cecdbaa63a40d79bf50a96b9910b9d/1?token-time=2145916800&token-hash=d6P5MWHHsCMxUuBAEPAoVc5wLUR19mIhqAq7Ma9h9rI%3D" alt="ne_moni"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/1?token-time=2145916800&token-hash=f03BFb4S2FUx9YEt87TnEmifb4h33OywGBW2akQVtQY%3D" alt="Melilot"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/2?token-time=2145916800&token-hash=mgPdX9TqZxEg4TTPuc477dxhIgYk9246qafjWZEqZ7g%3D" alt="Melilot"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/2?token-time=2145916800&token-hash=rwZ8qvbm_kpA4ib3kc07tVKupXeySpY5ATQFGxfL9v0%3D" alt="Xeltica"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/3384329/8b713330cb27404ea6e9fac50ff96efe/1?token-time=2145916800&token-hash=0eu4-m1gTWA9PhptVZt6rdKcusqcD7RB87rJT23VVFI%3D" alt="べすれい"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D" alt="gutfuckllc"></td>
<td><img src="https://c8.patreon.com/2/100/12718187" alt="Peter G."></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=zwSu01tOtn5xTUucDZHuPsCxF2HBEMVs9ROJKTlEV_o%3D" alt="nemu"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=12378075">39ff</a></td>
<td><a href="https://www.patreon.com/user?u=12731202">negao</a></td>
<td><a href="https://www.patreon.com/negao">negao</a></td>
<td><a href="https://www.patreon.com/user?u=13099460">ne_moni</a></td>
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
<td><a href="https://www.patreon.com/AxellaMC">Xeltica</a></td>
<td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td>
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
@ -73,23 +91,17 @@ Misskey is using Crowdin for l10n.
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/2?token-time=2145916800&token-hash=zElv7ZcPL3viGsXbNG_KWiKrbV0vvw1gk0panx8DJoo%3D" alt="Naoki Kosaka"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12931605/ead494101f364dffa90efe49e36fb494/1?token-time=2145916800&token-hash=NzSFPjIlodXyv41rwK61aZWVZWfI4surJaNj8vWKvqM%3D" alt="Reiju"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=UERBN4OyP7Nh5XwwdDg0N0IE5cD6_qUQMO81Z5Wizso%3D" alt="Hiratake"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4950409/28e7d016209243759d9316be2e21381d/2?token-time=2145916800&token-hash=LuEaDkchH3GQWUcTOhBQ8xfKQYF0s5FjlZRd7Yduia8%3D" alt="mikan54951"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12959468/c249e15aebec4424b5c0f427173671b6/1?token-time=2145916800&token-hash=lubpCEdxAkxPlpR2O6bvZ7BIh8Q4nGf-U_mE1qpjVAQ%3D" alt="fujishan"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=5881381">Naoki Kosaka</a></td>
<td><a href="https://www.patreon.com/user?u=12931605">Reiju</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/user?u=4950409">mikan54951</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
<td><a href="https://www.patreon.com/fujishan">fujishan</a></td>
</tr></table>
**Last updated:** Wed, 22 Aug 2018 05:25:06 UTC
**Last updated:** Tue, 02 Oct 2018 09:25:07 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright

BIN
assets/about/drive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
assets/about/post.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

BIN
assets/about/reaction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/about/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
assets/ai-orig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
assets/ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View file

@ -54,7 +54,7 @@ Please visit https://www.google.com/recaptcha/intro/ and generate keys.
*(optional)* Generating VAPID keys
----------------------------------------------------------------
If you want to enable ServiceWroker, you need to generate VAPID keys:
If you want to enable ServiceWorker, you need to generate VAPID keys:
Unless you have set your global node_modules location elsewhere, you need to run this in root.
``` shell
@ -131,6 +131,7 @@ You can check if the service is running with `systemctl status misskey`.
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
3. `npm install`
4. `npm run build`
5. Check [ChangeLog](../CHANGELOG.md) for migration information
----------------------------------------------------------------

View file

@ -10,7 +10,7 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう
*1.* Misskeyユーザーの作成
----------------------------------------------------------------
Misskeyのrootで実行しない方がよいため、代わりにユーザーを作成します。
Misskeyはrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。
Debianの例:
```
@ -109,6 +109,7 @@ Restart=always
[Install]
WantedBy=multi-user.target
```
CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
3. `systemctl daemon-reload ; systemctl enable misskey` systemdを再読み込みしmisskeyサービスを有効化
4. `systemctl start misskey` misskeyサービスの起動
@ -120,6 +121,7 @@ WantedBy=multi-user.target
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
3. `npm install`
4. `npm run build`
5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
----------------------------------------------------------------

View file

@ -2,7 +2,6 @@
* Gulp tasks
*/
import * as fs from 'fs';
import * as gulp from 'gulp';
import * as gutil from 'gulp-util';
import * as ts from 'gulp-typescript';
@ -78,7 +77,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
]).pipe(gulp.dest('./built/'))
);
gulp.task('test', ['lint', 'mocha']);
gulp.task('test', ['mocha']);
gulp.task('lint', () =>
gulp.src('./src/**/*.ts')
@ -166,9 +165,7 @@ gulp.task('build:client:pug', [
.pipe(pug({
locals: {
themeColor: constants.themeColor,
facss: fa.dom.css(),
//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
hljscss: fs.readFileSync('./src/client/assets/code-highlight.css', 'utf8')
facss: fa.dom.css()
}
}))
.pipe(htmlmin({

View file

@ -1,5 +1,3 @@
# **Please DO NOT edit these files** except `ja-JP.yml`.
# **DO NOT edit locale files** except `ja-JP.yml`.
If you want to...
* i18n ... please see [Translation guide](../docs/translate.en.md).
* l10n ... please visit https://crowdin.com/project/misskey
Please see [Contribution guide](../CONTRIBUTING.md) for more information.

View file

@ -5,24 +5,9 @@
const fs = require('fs');
const yaml = require('js-yaml');
const loadLang = lang => yaml.safeLoad(
fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8'));
const langs = ['de-DE', 'en-US', 'fr-FR', 'ja-JP', 'ja-KS', 'pl-PL', 'es-ES', 'nl-NL'];
const native = loadLang('ja-JP');
const loadLocale = lang => yaml.safeLoad(fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8'));
const locales = langs.map(lang => ({ [lang]: loadLocale(lang) }));
const langs = {
'de-DE': loadLang('de-DE'),
'en-US': loadLang('en-US'),
'fr-FR': loadLang('fr-FR'),
'ja-JP': native,
'ja-KS': loadLang('ja-KS'),
'pl-PL': loadLang('pl-PL'),
'es-ES': loadLang('es-ES')
};
Object.values(langs).forEach(locale => {
// Extend native language (Japanese)
locale = Object.assign({}, native, locale);
});
module.exports = langs;
module.exports = locales.reduce((a, b) => ({ ...a, ...b }));

View file

@ -6,6 +6,19 @@ common:
misskey: "A ⭐ of fediverse"
about-title: "A ⭐ of fediverse."
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
intro:
title: "Misskeyって"
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
features: "特徴"
rich-contents: "投稿"
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
reaction: "リアクション"
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
ui: "インターフェース"
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
drive: "ドライブ"
drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんかもしくは、アップロードしたファイルをフォルダ分けして整理したくなったことはありませんかMisskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。"
outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!"
adblock:
detected: "広告ブロッカーを無効にしてください"
warning: "<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。"
@ -73,6 +86,16 @@ common:
rip: "RIP"
pudding: "Pudding"
note-visibility:
public: "公開"
home: "ホーム"
home-desc: "ホームタイムラインにのみ公開"
followers: "フォロワー"
followers-desc: "自分のフォロワーにのみ公開"
specified: "ダイレクト"
specified-desc: "指定したユーザーにのみ公開"
private: "非公開"
note-placeholders:
a: "今どうしてる?"
b: "何かありましたか?"
@ -93,6 +116,13 @@ common:
use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける"
verified-user: "公式アカウント"
disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない"
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi:
drawn: "引き分け"
@ -136,7 +166,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"
direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@ -248,6 +281,47 @@ common/views/components/connect-failed.troubleshooter.vue:
flush: "キャッシュの削除"
set-version: "バージョン指定"
common/views/components/media-banner.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
common/views/components/theme.vue:
light-theme: "非ダークモード時に使用するテーマ"
dark-theme: "ダークモード時に使用するテーマ"
light-themes: "明るいテーマ"
dark-themes: "暗いテーマ"
install-a-theme: "テーマのインストール"
theme-code: "テーマコード"
install: "インストール"
installed: "「{}」をインストールしました"
create-a-theme: "テーマの作成"
save-created-theme: "テーマを保存"
primary-color: "プライマリ カラー"
secondary-color: "セカンダリ カラー"
text-color: "文字色"
base-theme: "ベーステーマ"
base-theme-light: "Light"
base-theme-dark: "Dark"
theme-name: "テーマ名"
preview-created-theme: "プレビュー"
invalid-theme: "テーマが正しくありません。"
already-installed: "既にそのテーマはインストールされています。"
saved: "保存しました"
installed-themes: "インストールされたテーマ"
select-theme: "テーマを選択してください"
uninstall: "アンインストール"
uninstalled: "「{}」をアンインストールしました"
author: "作者"
desc: "説明"
export: "エクスポート"
import: "インポート"
import-by-code: "またはコードをペースト"
theme-name-required: "テーマ名は必須です。"
common/views/components/cw-button.vue:
hide: "隠す"
show: "もっと見る"
common/views/components/messaging.vue:
search-user: "ユーザーを探す"
you: "あなた"
@ -283,8 +357,11 @@ common/views/components/nav.vue:
feedback: "フィードバック"
common/views/components/note-menu.vue:
detail: "詳細"
copy-link: "リンクをコピー"
favorite: "お気に入り"
pin: "ピン留め"
unpin: "ピン留め解除"
delete: "削除"
delete-confirm: "この投稿を削除しますか?"
remote: "投稿元で見る"
@ -371,6 +448,10 @@ common/views/components/visibility-chooser.vue:
specified-desc: "指定したユーザーにのみ公開"
private: "非公開"
common/views/components/trends.vue:
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/broadcast.vue:
fetching: "確認中"
no-broadcasts: "お知らせはありません"
@ -399,8 +480,6 @@ common/views/widgets/posts-monitor.vue:
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/server.vue:
title: "サーバー情報"
@ -443,6 +522,7 @@ common/views/pages/follow.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
desktop:
@ -481,17 +561,21 @@ desktop/views/components/charts.vue:
notes: "投稿"
users: "ユーザー"
drive: "ドライブ"
network: "ネットワーク"
charts:
notes: "投稿の増減 (統合)"
local-notes: "投稿の増減 (ローカル)"
remote-notes: "投稿の増減 (リモート)"
notes-total: "投稿の累計"
notes-total: "投稿の積算"
users: "ユーザーの増減"
users-total: "ユーザーの累計"
users-total: "ユーザーの積算"
drive: "ドライブ使用量の増減"
drive-total: "ドライブ使用量の累計"
drive-total: "ドライブ使用量の積算"
drive-files: "ドライブのファイル数の増減"
drive-files-total: "ドライブのファイル数の累計"
drive-files-total: "ドライブのファイル数の積算"
network-requests: "リクエスト"
network-time: "応答時間"
network-usage: "通信量"
desktop/views/components/choose-file-from-drive-window.vue:
choose-file: "ファイル選択中"
@ -581,6 +665,7 @@ desktop/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
desktop/views/components/followers-window.vue:
@ -637,8 +722,6 @@ desktop/views/components/notes.note.vue:
detail: "詳細"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
hide: "隠す"
see-more: "もっと見る"
desktop/views/components/notes.vue:
error: "読み込みに失敗しました。"
@ -714,10 +797,14 @@ desktop/views/components/settings.vue:
2fa: "二段階認証"
other: "その他"
license: "ライセンス"
theme: "テーマ"
behaviour: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
note-visibility: "投稿の公開範囲"
default-note-visibility: "デフォルトの公開範囲"
remember-note-visibility: "投稿の公開範囲を記憶する"
auto-popout: "ウィンドウの自動ポップアウト"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
advanced: "詳細設定"
@ -729,8 +816,10 @@ desktop/views/components/settings.vue:
choose-wallpaper: "壁紙を選択"
delete-wallpaper: "壁紙を削除"
dark-mode: "ダークモード"
use-shadow: "UIに影を使用"
rounded-corners: "UIの角を丸める"
circle-icons: "円形のアイコンを使用"
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
contrasted-acct: "ユーザー名にコントラストを付ける"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
show-clock-on-header: "右上に時計を表示する"
@ -739,7 +828,6 @@ desktop/views/components/settings.vue:
show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示する"
show-maps: "マップの自動展開"
show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。"
sound: "サウンド"
enable-sounds: "サウンドを有効にする"
@ -845,7 +933,7 @@ desktop/views/components/settings.profile.vue:
birthday: "誕生日"
save: "保存"
locked-account: "アカウントの保護"
is-locked: "投稿を非公開にする"
is-locked: "フォローを承認制にする"
other: "その他"
is-bot: "このアカウントはBotです"
is-cat: "このアカウントはCatです"
@ -865,7 +953,13 @@ desktop/views/components/timeline.vue:
local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
list: "リスト"
hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加"
add-list: "リストを追加"
list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
@ -984,7 +1078,10 @@ desktop/views/pages/welcome.vue:
signin-button: "やってる"
signup-button: "やる"
timeline: "タイムライン"
announcements: "お知らせ"
photos: "最近の画像"
powered-by-misskey: "Powered by <b>Misskey</b>."
info: "情報"
desktop/views/pages/drive.vue:
title: "Misskey Drive"
@ -1145,6 +1242,7 @@ mobile/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
mobile/views/components/friends-maker.vue:
@ -1156,8 +1254,6 @@ mobile/views/components/friends-maker.vue:
mobile/views/components/note.vue:
reposted-by: "{}がRenote"
more: "もっと見る"
less: "隠す"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
location: "位置情報"
@ -1265,6 +1361,8 @@ mobile/views/pages/home.vue:
local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
@ -1317,6 +1415,9 @@ mobile/views/pages/settings/settings.profile.vue:
avatar: "アイコン"
banner: "バナー"
is-cat: "このアカウントはCatです"
is-locked: "フォローを承認制にする"
advanced: "その他"
privacy: "プライバシー"
save: "保存"
saved: "プロフィールを保存しました"
uploading: "アップロード中"
@ -1341,6 +1442,7 @@ mobile/views/pages/settings.vue:
dark-mode: "ダークモード"
i-am-under-limited-internet: "私は通信を制限されている"
circle-icons: "円形のアイコンを使用"
contrasted-acct: "ユーザー名にコントラストを付ける"
timeline: "タイムライン"
show-reply-target: "リプライ先を表示する"
show-my-renotes: "自分の行ったRenoteを表示する"
@ -1349,8 +1451,15 @@ mobile/views/pages/settings.vue:
post-style: "投稿の表示スタイル"
post-style-standard: "標準"
post-style-smart: "スマート"
notification-position: "通知の表示"
notification-position-bottom: "下"
notification-position-top: "上"
theme: "テーマ"
behavior: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
note-visibility: "投稿の公開範囲"
default-note-visibility: "デフォルトの公開範囲"
remember-note-visibility: "投稿の公開範囲を記憶する"
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
load-raw-images: "添付された画像を高画質で表示する"
load-remote-media: "リモートサーバーのメディアを表示する"
@ -1370,7 +1479,7 @@ mobile/views/pages/settings.vue:
settings: "設定"
signout: "サインアウト"
sound: "サウンド"
enableSounds: "サウンドを有効にする"
enable-sounds: "サウンドを有効にする"
mobile/views/pages/user.vue:
follows-you: "フォローされています"

View file

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "8.15.0",
"clientVersion": "1.0.9031",
"version": "9.7.1",
"clientVersion": "1.0.10090",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@ -20,16 +20,16 @@
"format": "gulp format"
},
"dependencies": {
"@fortawesome/fontawesome": "1.1.8",
"@fortawesome/fontawesome-free-brands": "5.0.13",
"@fortawesome/fontawesome-free-regular": "5.0.13",
"@fortawesome/fontawesome-free-solid": "5.0.13",
"@fortawesome/fontawesome-svg-core": "1.2.4",
"@fortawesome/free-brands-svg-icons": "5.3.1",
"@fortawesome/free-regular-svg-icons": "5.3.1",
"@fortawesome/free-solid-svg-icons": "5.3.1",
"@koa/cors": "2.2.2",
"@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1",
"@types/bcryptjs": "2.4.2",
"@types/dateformat": "1.0.1",
"@types/debug": "0.0.30",
"@types/debug": "0.0.31",
"@types/deep-equal": "1.0.1",
"@types/double-ended-queue": "2.1.0",
"@types/elasticsearch": "5.0.26",
@ -51,19 +51,19 @@
"@types/koa-logger": "3.1.0",
"@types/koa-mount": "3.0.1",
"@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.31",
"@types/koa-router": "7.0.32",
"@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3",
"@types/minio": "6.0.2",
"@types/minio": "7.0.0",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.3",
"@types/mongodb": "3.1.4",
"@types/mongodb": "3.1.10",
"@types/ms": "0.7.30",
"@types/node": "10.9.3",
"@types/node": "10.11.4",
"@types/portscanner": "2.1.0",
"@types/pug": "2.0.4",
"@types/qrcode": "1.2.0",
"@types/qrcode": "1.3.0",
"@types/ratelimiter": "2.1.28",
"@types/redis": "2.8.6",
"@types/request": "2.47.1",
@ -75,13 +75,15 @@
"@types/single-line-log": "1.1.0",
"@types/speakeasy": "2.0.2",
"@types/systeminformation": "3.23.0",
"@types/tinycolor2": "1.4.1",
"@types/tmp": "0.0.33",
"@types/uuid": "3.4.3",
"@types/webpack": "4.4.11",
"@types/uuid": "3.4.4",
"@types/webpack": "4.4.14",
"@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.39",
"@types/ws": "6.0.0",
"@types/websocket": "0.0.40",
"@types/ws": "6.0.1",
"animejs": "2.2.0",
"autobind-decorator": "2.1.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
@ -94,26 +96,25 @@
"crc-32": "1.2.0",
"css-loader": "1.0.0",
"dateformat": "3.0.3",
"debug": "3.1.0",
"debug": "4.0.1",
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",
"diskusage": "0.2.4",
"diskusage": "0.2.5",
"dompurify": "1.0.5",
"double-ended-queue": "2.1.0-0",
"elasticsearch": "15.1.1",
"element-ui": "2.4.6",
"emojilib": "2.3.0",
"escape-regexp": "0.0.1",
"eslint": "5.0.1",
"eslint-plugin-vue": "4.7.1",
"eventemitter3": "3.1.0",
"exif-js": "2.3.0",
"file-loader": "1.1.11",
"file-type": "9.0.0",
"file-loader": "2.0.0",
"file-type": "10.0.0",
"fuckadblock": "3.2.1",
"gulp": "3.9.1",
"gulp-cssnano": "2.1.3",
"gulp-htmlmin": "4.0.0",
"gulp-htmlmin": "5.0.1",
"gulp-imagemin": "4.1.0",
"gulp-mocha": "6.0.0",
"gulp-pug": "4.0.1",
@ -132,16 +133,17 @@
"insert-text-at-cursor": "0.1.1",
"is-root": "2.0.0",
"is-url": "1.2.4",
"jquery": "3.3.1",
"js-yaml": "3.12.0",
"jsdom": "11.12.0",
"jsdom": "12.2.0",
"json5": "2.1.0",
"json5-loader": "1.0.1",
"koa": "2.5.1",
"koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0",
"koa-favicon": "2.0.1",
"koa-json-body": "5.3.0",
"koa-logger": "3.2.0",
"koa-mount": "3.0.0",
"koa-mount": "4.0.0",
"koa-multer": "1.0.2",
"koa-router": "7.4.0",
"koa-send": "5.0.0",
@ -151,17 +153,15 @@
"lodash.assign": "4.2.0",
"mecab-async": "0.1.2",
"merge-options": "1.0.1",
"minio": "7.0.0",
"minio": "7.0.1",
"mkdirp": "0.5.1",
"mocha": "5.2.0",
"moji": "0.5.1",
"mongodb": "3.1.1",
"monk": "6.0.6",
"ms": "2.1.1",
"nan": "2.11.0",
"nan": "2.11.1",
"nested-property": "0.0.7",
"node-sass": "4.9.3",
"node-sass-json-importer": "3.3.1",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0",
@ -172,13 +172,14 @@
"promise-sequential": "1.1.1",
"pug": "2.0.3",
"punycode": "2.1.1",
"qrcode": "1.2.2",
"qrcode": "1.3.0",
"ratelimiter": "3.2.0",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2",
"reconnecting-websocket": "4.1.5",
"redis": "2.8.0",
"request": "2.88.0",
"request-promise-native": "1.0.5",
"request-stats": "3.0.0",
"rimraf": "2.6.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
@ -193,38 +194,42 @@
"style-loader": "0.23.0",
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
"summaly": "2.1.4",
"systeminformation": "3.44.2",
"summaly": "2.2.0",
"systeminformation": "3.45.7",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"tinycolor2": "1.4.1",
"tmp": "0.0.33",
"ts-loader": "4.4.1",
"ts-node": "7.0.1",
"tslint": "5.10.0",
"typescript": "2.9.2",
"typescript-eslint-parser": "18.0.0",
"typescript-eslint-parser": "19.0.2",
"uglify-es": "3.3.9",
"url-loader": "1.1.1",
"uuid": "3.3.2",
"v-animate-css": "0.0.2",
"vue": "2.5.17",
"vue-chartjs": "3.4.0",
"vue-cropperjs": "2.2.1",
"vue-js-modal": "1.3.23",
"vue-color": "2.6.0",
"vue-cropperjs": "2.2.2",
"vue-js-modal": "1.3.26",
"vue-json-tree-view": "2.1.4",
"vue-loader": "15.4.1",
"vue-loader": "15.4.2",
"vue-router": "3.0.1",
"vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.2.0",
"vue-template-compiler": "2.5.17",
"vuedraggable": "2.16.0",
"vuewordcloud": "18.7.11",
"vuex": "3.0.1",
"vuex-persistedstate": "2.5.4",
"web-push": "3.3.2",
"web-push": "3.3.3",
"webfinger.js": "2.6.6",
"webpack": "4.17.1",
"webpack-cli": "3.1.0",
"websocket": "1.0.26",
"ws": "6.0.0",
"webpack": "4.20.2",
"webpack-cli": "3.1.2",
"websocket": "1.0.28",
"ws": "6.1.0",
"xev": "2.0.1"
},
"greenkeeper": {

View file

@ -6,6 +6,10 @@ html
&, *
cursor progress !important
html
// iOS
overflow auto
body
overflow-wrap break-word
@ -23,7 +27,7 @@ body
z-index 65536
.bar
background $theme-color
background var(--primary)
position fixed
z-index 65537
@ -40,7 +44,7 @@ body
right 0px
width 100px
height 100%
box-shadow 0 0 10px $theme-color, 0 0 5px $theme-color
box-shadow 0 0 10px var(--primary), 0 0 5px var(--primary)
opacity 1
transform rotate(3deg) translate(0px, -4px)
@ -60,8 +64,8 @@ body
box-sizing border-box
border solid 2px transparent
border-top-color $theme-color
border-left-color $theme-color
border-top-color var(--primary)
border-left-color var(--primary)
border-radius 50%
animation progress-spinner 400ms linear infinite

View file

@ -1,3 +1,32 @@
<template>
<router-view id="app"></router-view>
<router-view id="app" v-hotkey.global="keymap"></router-view>
</template>
<script lang="ts">
import Vue from 'vue';
import { url, lang } from './config';
export default Vue.extend({
computed: {
keymap(): any {
return {
'h|slash': this.help,
'd': this.dark
};
}
},
methods: {
help() {
window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank');
},
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
value: !this.$store.state.device.darkmode
});
}
}
});
</script>

View file

@ -80,7 +80,7 @@ export default Vue.extend({
accepted() {
this.state = 'accepted';
if (this.session.app.callbackUrl) {
location.href = this.session.app.callbackUrl + '?token=' + this.session.token;
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
}
}
}

View file

@ -34,9 +34,6 @@ html
//- FontAwesome style
style #{facss}
//- highlight.js style
style #{hljscss}
body
noscript: p
| JavaScriptを有効にしてください

View file

@ -18,6 +18,17 @@
return;
}
const langs = LANGS;
//#region Apply theme
const theme = localStorage.getItem('theme');
if (theme) {
Object.entries(JSON.parse(theme)).forEach(([k, v]) => {
document.documentElement.style.setProperty(`--${k}`, v.toString());
});
}
//#endregion
//#region Load settings
let settings = null;
const vuex = localStorage.getItem('vuex');
@ -40,10 +51,10 @@
//#region Detect the user language
let lang = null;
if (LANGS.includes(navigator.language)) {
if (langs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = LANGS.find(x => x.split('-')[0] == navigator.language);
lang = langs.find(x => x.split('-')[0] == navigator.language);
if (lang == null) {
// Fallback
@ -52,7 +63,7 @@
}
if (settings && settings.device.lang &&
LANGS.includes(settings.device.lang)) {
langs.includes(settings.device.lang)) {
lang = settings.device.lang;
}
//#endregion
@ -82,19 +93,12 @@
app = isMobile ? 'mobile' : 'desktop';
}
// Dark/Light
if (settings) {
if (settings.device.darkmode) {
document.documentElement.setAttribute('data-darkmode', 'true');
}
}
// Script version
const ver = localStorage.getItem('v') || VERSION;
// Get salt query
const salt = localStorage.getItem('salt')
? '?salt=' + localStorage.getItem('salt')
? `?salt=${localStorage.getItem('salt')}`
: '';
// Load an app script
@ -140,7 +144,7 @@
// Random
localStorage.setItem('salt', Math.random().toString());
// Clear cache (serive worker)
// Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');

View file

@ -0,0 +1,110 @@
import keyCode from './keycode';
import { concat } from '../../../prelude/array';
type pattern = {
which: string[];
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
};
type action = {
patterns: pattern[];
callback: Function;
};
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
const result = {
patterns: [],
callback: callback
} as action;
result.patterns = patterns.split('|').map(part => {
const pattern = {
which: [],
ctrl: false,
alt: false,
shift: false
} as pattern;
part.trim().split('+').forEach(key => {
key = key.trim().toLowerCase();
switch (key) {
case 'ctrl': pattern.ctrl = true; break;
case 'alt': pattern.alt = true; break;
case 'shift': pattern.shift = true; break;
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
}
});
return pattern;
});
return result;
});
const ignoreElemens = ['input', 'textarea'];
export default {
install(Vue) {
Vue.directive('hotkey', {
bind(el, binding) {
el._hotkey_global = binding.modifiers.global === true;
const actions = getKeyMap(binding.value);
// flatten
const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
el._keyHandler = (e: KeyboardEvent) => {
const key = e.code.toLowerCase();
const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
for (const action of actions) {
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
const matched = action.patterns.some(pattern => {
const matched = pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey &&
e.metaKey == false;
if (matched) {
e.preventDefault();
e.stopPropagation();
action.callback(e);
return true;
} else {
return false;
}
});
if (matched) {
break;
}
}
};
if (el._hotkey_global) {
document.addEventListener('keydown', el._keyHandler);
} else {
el.addEventListener('keydown', el._keyHandler);
}
},
unbind(el) {
if (el._hotkey_global) {
document.removeEventListener('keydown', el._keyHandler);
} else {
el.removeEventListener('keydown', el._keyHandler);
}
}
});
}
};

View file

@ -0,0 +1,33 @@
export default (input: string): string[] => {
if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
const codes = aliases[input];
return Array.isArray(codes) ? codes : [codes];
} else {
return [input];
}
};
export const aliases = {
'esc': 'Escape',
'enter': ['Enter', 'NumpadEnter'],
'up': 'ArrowUp',
'down': 'ArrowDown',
'left': 'ArrowLeft',
'right': 'ArrowRight',
'plus': ['NumpadAdd', 'Semicolon'],
};
/*!
* Programatically add the following
*/
// lower case chars
for (let i = 97; i < 123; i++) {
const char = String.fromCharCode(i);
aliases[char] = `Key${char.toUpperCase()}`;
}
// numbers
for (let i = 0; i < 10; i++) {
aliases[i] = [`Numpad${i}`, `Digit${i}`];
}

View file

@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
localStorage.setItem('should-refresh', 'true');
localStorage.setItem('v', newer);
// Clear cache (serive worker)
// Clear cache (service worker)
try {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');

View file

@ -13,21 +13,21 @@ type Notification = {
export default function(type, data): Notification {
switch (type) {
case 'drive_file_created':
case 'driveFileCreated':
return {
title: '%i18n:common.notification.file-uploaded%',
body: data.name,
icon: data.url
};
case 'unread_messaging_message':
case 'unreadMessagingMessage':
return {
title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
body: data.text, // TODO: getMessagingMessageSummary(data),
icon: data.user.avatarUrl
};
case 'reversi_invited':
case 'reversiInvited':
return {
title: '%i18n:common.notification.reversi-invited%',
body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],

View file

@ -1,8 +1,8 @@
require('fuckadblock');
declare const fuckAdBlock: any;
export default (os) => {
require('fuckadblock');
function adBlockDetected() {
os.apis.dialog({
title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%',

View file

@ -1,2 +0,0 @@
const gcd = (a, b) => !b ? a : gcd(b, a % b);
export default gcd;

View file

@ -0,0 +1,8 @@
const crypto = require('crypto');
export default (data: ArrayBuffer) => {
const buf = new Buffer(data);
const hash = crypto.createHash("md5");
hash.update(buf);
return hash.digest("hex");
};

View file

@ -0,0 +1,116 @@
import Vue from 'vue';
export default prop => ({
data() {
return {
connection: null
};
},
computed: {
$_ns_note_(): any {
return this[prop];
},
$_ns_isRenote(): boolean {
return (this.$_ns_note_.renote &&
this.$_ns_note_.text == null &&
this.$_ns_note_.fileIds.length == 0 &&
this.$_ns_note_.poll == null);
},
$_ns_target(): any {
return this._ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_;
},
},
created() {
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream;
}
},
mounted() {
this.capture(true);
if (this.$store.getters.isSignedIn) {
this.connection.on('_connected_', this.onStreamConnected);
}
},
beforeDestroy() {
this.decapture(true);
if (this.$store.getters.isSignedIn) {
this.connection.off('_connected_', this.onStreamConnected);
}
},
methods: {
capture(withHandler = false) {
if (this.$store.getters.isSignedIn) {
const data = {
id: this.$_ns_target.id
} as any;
if (
(this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) ||
(this.$_ns_target.mentions || []).includes(this.$store.state.i.id)
) {
data.read = true;
}
this.connection.send('sn', data);
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
}
},
decapture(withHandler = false) {
if (this.$store.getters.isSignedIn) {
this.connection.send('un', {
id: this.$_ns_target.id
});
if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
}
},
onStreamConnected() {
this.capture();
},
onStreamNoteUpdated(data) {
const { type, id, body } = data;
if (id !== this.$_ns_target.id) return;
switch (type) {
case 'reacted': {
const reaction = body.reaction;
if (this.$_ns_target.reactionCounts == null) Vue.set(this.$_ns_target, 'reactionCounts', {});
this.$_ns_target.reactionCounts[reaction] = (this.$_ns_target.reactionCounts[reaction] || 0) + 1;
break;
}
case 'pollVoted': {
if (body.userId == this.$store.state.i.id) return;
const choice = body.choice;
this.$_ns_target.poll.choices.find(c => c.id === choice).votes++;
break;
}
case 'deleted': {
Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
this.$_ns_target.text = null;
this.$_ns_target.tags = [];
this.$_ns_target.fileIds = [];
this.$_ns_target.poll = null;
this.$_ns_target.geo = null;
this.$_ns_target.cw = null;
break;
}
}
this.$emit(`update:${prop}`, this.$_ns_note_);
},
}
});

View file

@ -1,53 +0,0 @@
export default function(qs: string) {
const q = {
text: ''
};
qs.split(' ').forEach(x => {
if (/^([a-z_]+?):(.+?)$/.test(x)) {
const [key, value] = x.split(':');
switch (key) {
case 'user':
q['includeUserUsernames'] = value.split(',');
break;
case 'exclude_user':
q['excludeUserUsernames'] = value.split(',');
break;
case 'follow':
q['following'] = value == 'null' ? null : value == 'true';
break;
case 'reply':
q['reply'] = value == 'null' ? null : value == 'true';
break;
case 'renote':
q['renote'] = value == 'null' ? null : value == 'true';
break;
case 'media':
q['media'] = value == 'null' ? null : value == 'true';
break;
case 'poll':
q['poll'] = value == 'null' ? null : value == 'true';
break;
case 'until':
case 'since':
// YYYY-MM-DD
if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
const [yyyy, mm, dd] = value.split('-');
q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
}
break;
default:
q[key] = value;
break;
}
} else {
q.text += x + ' ';
}
});
if (q.text) {
q.text = q.text.trim();
}
return q;
}

View file

@ -0,0 +1,318 @@
import autobind from 'autobind-decorator';
import { EventEmitter } from 'eventemitter3';
import ReconnectingWebsocket from 'reconnecting-websocket';
import { wsUrl } from '../../config';
import MiOS from '../../mios';
/**
* Misskey stream connection
*/
export default class Stream extends EventEmitter {
private stream: ReconnectingWebsocket;
private state: string;
private buffer: any[];
private sharedConnections: SharedConnection[] = [];
private nonSharedConnections: NonSharedConnection[] = [];
constructor(os: MiOS) {
super();
this.state = 'initializing';
this.buffer = [];
const user = os.store.state.i;
this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : ''));
this.stream.addEventListener('open', this.onOpen);
this.stream.addEventListener('close', this.onClose);
this.stream.addEventListener('message', this.onMessage);
if (user) {
const main = this.useSharedConnection('main');
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
os.store.dispatch('mergeMe', i);
});
main.on('readAllNotifications', () => {
os.store.dispatch('mergeMe', {
hasUnreadNotification: false
});
});
main.on('unreadNotification', () => {
os.store.dispatch('mergeMe', {
hasUnreadNotification: true
});
});
main.on('readAllMessagingMessages', () => {
os.store.dispatch('mergeMe', {
hasUnreadMessagingMessage: false
});
});
main.on('unreadMessagingMessage', () => {
os.store.dispatch('mergeMe', {
hasUnreadMessagingMessage: true
});
});
main.on('unreadMention', () => {
os.store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
main.on('readAllUnreadMentions', () => {
os.store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
main.on('unreadSpecifiedNote', () => {
os.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
main.on('readAllUnreadSpecifiedNotes', () => {
os.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
main.on('clientSettingUpdated', x => {
os.store.commit('settings/set', {
key: x.key,
value: x.value
});
});
main.on('homeUpdated', x => {
os.store.commit('settings/setHome', x);
});
main.on('mobileHomeUpdated', x => {
os.store.commit('settings/setMobileHome', x);
});
main.on('widgetUpdated', x => {
os.store.commit('settings/setWidget', {
id: x.id,
data: x.data
});
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
alert('%i18n:common.my-token-regenerated%');
os.signout();
});
}
}
public useSharedConnection = (channel: string): SharedConnection => {
const existConnection = this.sharedConnections.find(c => c.channel === channel);
if (existConnection) {
existConnection.use();
return existConnection;
} else {
const connection = new SharedConnection(this, channel);
connection.use();
this.sharedConnections.push(connection);
return connection;
}
}
@autobind
public removeSharedConnection(connection: SharedConnection) {
this.sharedConnections = this.sharedConnections.filter(c => c.id !== connection.id);
}
public connectToChannel = (channel: string, params?: any): NonSharedConnection => {
const connection = new NonSharedConnection(this, channel, params);
this.nonSharedConnections.push(connection);
return connection;
}
@autobind
public disconnectToChannel(connection: NonSharedConnection) {
this.nonSharedConnections = this.nonSharedConnections.filter(c => c.id !== connection.id);
}
/**
* Callback of when open connection
*/
@autobind
private onOpen() {
const isReconnect = this.state == 'reconnecting';
this.state = 'connected';
this.emit('_connected_');
// バッファーを処理
const _buffer = [].concat(this.buffer); // Shallow copy
this.buffer = []; // Clear buffer
_buffer.forEach(data => {
this.send(data); // Resend each buffered messages
});
// チャンネル再接続
if (isReconnect) {
this.sharedConnections.forEach(c => {
c.connect();
});
this.nonSharedConnections.forEach(c => {
c.connect();
});
}
}
/**
* Callback of when close connection
*/
@autobind
private onClose() {
this.state = 'reconnecting';
this.emit('_disconnected_');
}
/**
* Callback of when received a message from connection
*/
@autobind
private onMessage(message) {
const { type, body } = JSON.parse(message.data);
if (type == 'channel') {
const id = body.id;
const connection = this.sharedConnections.find(c => c.id === id) || this.nonSharedConnections.find(c => c.id === id);
connection.emit(body.type, body.body);
} else {
this.emit(type, body);
}
}
/**
* Send a message to connection
*/
@autobind
public send(typeOrPayload, payload?) {
const data = payload === undefined ? typeOrPayload : {
type: typeOrPayload,
body: payload
};
// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
if (this.state != 'connected') {
this.buffer.push(data);
return;
}
this.stream.send(JSON.stringify(data));
}
/**
* Close this connection
*/
@autobind
public close() {
this.stream.removeEventListener('open', this.onOpen);
this.stream.removeEventListener('message', this.onMessage);
}
}
abstract class Connection extends EventEmitter {
public channel: string;
public id: string;
protected params: any;
protected stream: Stream;
constructor(stream: Stream, channel: string, params?: any) {
super();
this.stream = stream;
this.channel = channel;
this.params = params;
this.id = Math.random().toString();
this.connect();
}
@autobind
public connect() {
this.stream.send('connect', {
channel: this.channel,
id: this.id,
params: this.params
});
}
@autobind
public send(typeOrPayload, payload?) {
const data = payload === undefined ? typeOrPayload : {
type: typeOrPayload,
body: payload
};
this.stream.send('channel', {
id: this.id,
body: data
});
}
public abstract dispose: () => void;
}
class SharedConnection extends Connection {
private users = 0;
private disposeTimerId: any;
constructor(stream: Stream, channel: string) {
super(stream, channel);
}
@autobind
public use() {
this.users++;
// タイマー解除
if (this.disposeTimerId) {
clearTimeout(this.disposeTimerId);
this.disposeTimerId = null;
}
}
@autobind
public dispose() {
this.users--;
// そのコネクションの利用者が誰もいなくなったら
if (this.users === 0) {
// また直ぐに再利用される可能性があるので、一定時間待ち、
// 新たな利用者が現れなければコネクションを切断する
this.disposeTimerId = setTimeout(() => {
this.disposeTimerId = null;
this.removeAllListeners();
this.stream.send('disconnect', { id: this.id });
this.stream.removeSharedConnection(this);
}, 3000);
}
}
}
class NonSharedConnection extends Connection {
constructor(stream: Stream, channel: string, params?: any) {
super(stream, channel, params);
}
@autobind
public dispose() {
this.removeAllListeners();
this.stream.send('disconnect', { id: this.id });
this.stream.disconnectToChannel(this);
}
}

View file

@ -1,34 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Drive stream connection
*/
export class DriveStream extends Stream {
constructor(os: MiOS, me) {
super(os, 'drive', {
i: me.token
});
}
}
export class DriveStreamManager extends StreamManager<DriveStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new DriveStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,11 +0,0 @@
import Stream from '../../stream';
import MiOS from '../../../../../mios';
export class ReversiGameStream extends Stream {
constructor(os: MiOS, me, game) {
super(os, 'games/reversi-game', {
i: me ? me.token : null,
game: game.id
});
}
}

View file

@ -1,31 +0,0 @@
import StreamManager from '../../stream-manager';
import Stream from '../../stream';
import MiOS from '../../../../../mios';
export class ReversiStream extends Stream {
constructor(os: MiOS, me) {
super(os, 'games/reversi', {
i: me.token
});
}
}
export class ReversiStreamManager extends StreamManager<ReversiStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new ReversiStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,34 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Global timeline stream connection
*/
export class GlobalTimelineStream extends Stream {
constructor(os: MiOS, me) {
super(os, 'global-timeline', {
i: me.token
});
}
}
export class GlobalTimelineStreamManager extends StreamManager<GlobalTimelineStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new GlobalTimelineStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,102 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Home stream connection
*/
export class HomeStream extends Stream {
constructor(os: MiOS, me) {
super(os, '', {
i: me.token
});
// 最終利用日時を更新するため定期的にaliveメッセージを送信
setInterval(() => {
this.send({ type: 'alive' });
me.lastUsedAt = new Date();
}, 1000 * 60);
// 自分の情報が更新されたとき
this.on('meUpdated', i => {
if (os.debug) {
console.log('I updated:', i);
}
os.store.dispatch('mergeMe', i);
});
this.on('read_all_notifications', () => {
os.store.dispatch('mergeMe', {
hasUnreadNotification: false
});
});
this.on('unread_notification', () => {
os.store.dispatch('mergeMe', {
hasUnreadNotification: true
});
});
this.on('read_all_messaging_messages', () => {
os.store.dispatch('mergeMe', {
hasUnreadMessagingMessage: false
});
});
this.on('unread_messaging_message', () => {
os.store.dispatch('mergeMe', {
hasUnreadMessagingMessage: true
});
});
this.on('clientSettingUpdated', x => {
os.store.commit('settings/set', {
key: x.key,
value: x.value
});
});
this.on('home_updated', x => {
os.store.commit('settings/setHome', x);
});
this.on('mobile_home_updated', x => {
os.store.commit('settings/setMobileHome', x);
});
this.on('widgetUpdated', x => {
os.store.commit('settings/setWidget', {
id: x.id,
data: x.data
});
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
this.on('my_token_regenerated', () => {
alert('%i18n:common.my-token-regenerated%');
os.signout();
});
}
}
export class HomeStreamManager extends StreamManager<HomeStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new HomeStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,34 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Hybrid timeline stream connection
*/
export class HybridTimelineStream extends Stream {
constructor(os: MiOS, me) {
super(os, 'hybrid-timeline', {
i: me.token
});
}
}
export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new HybridTimelineStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,34 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Local timeline stream connection
*/
export class LocalTimelineStream extends Stream {
constructor(os: MiOS, me) {
super(os, 'local-timeline', {
i: me.token
});
}
}
export class LocalTimelineStreamManager extends StreamManager<LocalTimelineStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new LocalTimelineStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,34 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Messaging index stream connection
*/
export class MessagingIndexStream extends Stream {
constructor(os: MiOS, me) {
super(os, 'messaging-index', {
i: me.token
});
}
}
export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> {
private me;
private os: MiOS;
constructor(os: MiOS, me) {
super();
this.me = me;
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new MessagingIndexStream(this.os, this.me);
}
return this.connection;
}
}

View file

@ -1,20 +0,0 @@
import Stream from './stream';
import MiOS from '../../../mios';
/**
* Messaging stream connection
*/
export class MessagingStream extends Stream {
constructor(os: MiOS, me, otherparty) {
super(os, 'messaging', {
i: me.token,
otherparty
});
(this as any).on('_connected_', () => {
this.send({
i: me.token
});
});
}
}

View file

@ -1,30 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Notes stats stream connection
*/
export class NotesStatsStream extends Stream {
constructor(os: MiOS) {
super(os, 'notes-stats');
}
}
export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
super();
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new NotesStatsStream(this.os);
}
return this.connection;
}
}

View file

@ -1,30 +0,0 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Server stats stream connection
*/
export class ServerStatsStream extends Stream {
constructor(os: MiOS) {
super(os, 'server-stats');
}
}
export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
super();
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new ServerStatsStream(this.os);
}
return this.connection;
}
}

View file

@ -1,108 +0,0 @@
import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import Connection from './stream';
/**
*
*
*/
export default abstract class StreamManager<T extends Connection> extends EventEmitter {
private _connection: T = null;
private disposeTimerId: any;
/**
*
*/
private users = [];
protected set connection(connection: T) {
this._connection = connection;
if (this._connection == null) {
this.emit('disconnected');
} else {
this.emit('connected', this._connection);
this._connection.on('_connected_', () => {
this.emit('_connected_');
});
this._connection.on('_disconnected_', () => {
this.emit('_disconnected_');
});
this._connection.user = 'Managed';
}
}
protected get connection() {
return this._connection;
}
/**
*
*/
public get hasConnection() {
return this._connection != null;
}
public get state(): string {
if (!this.hasConnection) return 'no-connection';
return this._connection.state;
}
/**
*
*/
public abstract getConnection(): T;
/**
*
*/
public borrow() {
return this._connection;
}
/**
* IDを発行します
*/
public use() {
// タイマー解除
if (this.disposeTimerId) {
clearTimeout(this.disposeTimerId);
this.disposeTimerId = null;
}
// ユーザーID生成
const userId = uuid();
this.users.push(userId);
this._connection.user = `Managed (${ this.users.length })`;
return userId;
}
/**
*
* @param userId use ID
*/
public dispose(userId) {
this.users = this.users.filter(id => id != userId);
this._connection.user = `Managed (${ this.users.length })`;
// 誰もコネクションの利用者がいなくなったら
if (this.users.length == 0) {
// また直ぐに再利用される可能性があるので、一定時間待ち、
// 新たな利用者が現れなければコネクションを切断する
this.disposeTimerId = setTimeout(() => {
this.disposeTimerId = null;
this.connection.close();
this.connection = null;
}, 3000);
}
}
}

View file

@ -1,137 +0,0 @@
import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import * as ReconnectingWebsocket from 'reconnecting-websocket';
import { wsUrl } from '../../../config';
import MiOS from '../../../mios';
/**
* Misskey stream connection
*/
export default class Connection extends EventEmitter {
public state: string;
private buffer: any[];
public socket: ReconnectingWebsocket;
public name: string;
public connectedAt: Date;
public user: string = null;
public in: number = 0;
public out: number = 0;
public inout: Array<{
type: 'in' | 'out',
at: Date,
data: string
}> = [];
public id: string;
public isSuspended = false;
private os: MiOS;
constructor(os: MiOS, endpoint, params?) {
super();
//#region BIND
this.onOpen = this.onOpen.bind(this);
this.onClose = this.onClose.bind(this);
this.onMessage = this.onMessage.bind(this);
this.send = this.send.bind(this);
this.close = this.close.bind(this);
//#endregion
this.id = uuid();
this.os = os;
this.name = endpoint;
this.state = 'initializing';
this.buffer = [];
const query = params
? Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&')
: null;
this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`);
this.socket.addEventListener('open', this.onOpen);
this.socket.addEventListener('close', this.onClose);
this.socket.addEventListener('message', this.onMessage);
// Register this connection for debugging
this.os.registerStreamConnection(this);
}
/**
* Callback of when open connection
*/
private onOpen() {
this.state = 'connected';
this.emit('_connected_');
this.connectedAt = new Date();
// バッファーを処理
const _buffer = [].concat(this.buffer); // Shallow copy
this.buffer = []; // Clear buffer
_buffer.forEach(data => {
this.send(data); // Resend each buffered messages
if (this.os.debug) {
this.out++;
this.inout.push({ type: 'out', at: new Date(), data });
}
});
}
/**
* Callback of when close connection
*/
private onClose() {
this.state = 'reconnecting';
this.emit('_disconnected_');
}
/**
* Callback of when received a message from connection
*/
private onMessage(message) {
if (this.isSuspended) return;
if (this.os.debug) {
this.in++;
this.inout.push({ type: 'in', at: new Date(), data: message.data });
}
try {
const msg = JSON.parse(message.data);
if (msg.type) this.emit(msg.type, msg.body);
} catch (e) {
// noop
}
}
/**
* Send a message to connection
*/
public send(data) {
if (this.isSuspended) return;
// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
if (this.state != 'connected') {
this.buffer.push(data);
return;
}
if (this.os.debug) {
this.out++;
this.inout.push({ type: 'out', at: new Date(), data });
}
this.socket.send(JSON.stringify(data));
}
/**
* Close this connection
*/
public close() {
this.os.unregisterStreamConnection(this);
this.socket.removeEventListener('open', this.onOpen);
this.socket.removeEventListener('message', this.onMessage);
}
}

View file

@ -1,17 +0,0 @@
import Stream from './stream';
import MiOS from '../../mios';
export class UserListStream extends Stream {
constructor(os: MiOS, me, listId) {
super(os, 'user-list', {
i: me.token,
listId
});
(this as any).on('_connected_', () => {
this.send({
i: me.token
});
});
}
}

View file

@ -1,19 +1,25 @@
<template>
<span class="mk-acct">
<span class="name">@{{ user.username }}</span>
<span class="host" v-if="user.host">@{{ user.host }}</span>
<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
import { host } from '../../../config';
export default Vue.extend({
props: ['user']
props: ['user', 'detail'],
data() {
return {
host
};
}
});
</script>
<style lang="stylus" scoped>
.mk-acct
> .host
> .host.fade
opacity 0.5
</style>

View file

@ -125,7 +125,7 @@ export default Vue.extend({
}
if (this.type == 'user') {
const cacheKey = 'autocomplete:user:' + this.q;
const cacheKey = `autocomplete:user:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const users = JSON.parse(cache);
@ -148,7 +148,7 @@ export default Vue.extend({
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
} else {
const cacheKey = 'autocomplete:hashtag:' + this.q;
const cacheKey = `autocomplete:hashtag:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
@ -259,15 +259,13 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mk-autocomplete
position fixed
z-index 65535
max-width 100%
margin-top calc(1em + 8px)
overflow hidden
background isDark ? #313543 : #fff
background var(--faceHeader)
border solid 1px rgba(#000, 0.1)
border-radius 4px
transition top 0.1s ease, left 0.1s ease
@ -299,16 +297,16 @@ root(isDark)
text-overflow ellipsis
&:hover
background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1)
background var(--autocompleteItemHoverBg)
&[data-selected='true']
background $theme-color
background var(--primary)
&, *
color #fff !important
&:active
background darken($theme-color, 10%)
background var(--primaryDarken10)
&, *
color #fff !important
@ -325,15 +323,15 @@ root(isDark)
.name
margin 0 8px 0 0
color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
color var(--autocompleteItemText)
.username
color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
color var(--autocompleteItemTextSub)
> .hashtags > li
.name
color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
color var(--autocompleteItemText)
> .emojis > li
@ -343,15 +341,9 @@ root(isDark)
width 24px
.name
color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
color var(--autocompleteItemText)
.alias
margin 0 0 0 8px
color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
.mk-autocomplete[data-darkmode]
root(true)
.mk-autocomplete:not([data-darkmode])
root(false)
color var(--autocompleteItemTextSub)
</style>

View file

@ -1,15 +1,15 @@
<template>
<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
<span class="inner" :style="style"></span>
<span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
<span class="inner" :style="icon"></span>
</span>
<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
<span class="inner" :style="style"></span>
<span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
<span class="inner" :style="icon"></span>
</span>
<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
<span class="inner" :style="style"></span>
<router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
<span class="inner" :style="icon"></span>
</router-link>
<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
<span class="inner" :style="style"></span>
<router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
<span class="inner" :style="icon"></span>
</router-link>
</template>
@ -42,6 +42,11 @@ export default Vue.extend({
return this.user.isCat && this.$store.state.settings.circleIcons;
},
style(): any {
return {
borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
};
},
icon(): any {
return {
backgroundColor: this.lightmode
? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
@ -53,6 +58,11 @@ export default Vue.extend({
};
}
},
mounted() {
if (this.user.avatarColor) {
this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`;
}
},
methods: {
onClick(e) {
this.$emit('click', e);
@ -62,8 +72,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
root(isDark)
.mk-avatar
display inline-block
vertical-align bottom
@ -74,7 +83,7 @@ root(isDark)
&.cat::before,
&.cat::after
background #df548f
border solid 4px isDark ? #e0eefd : #202224
border solid 4px currentColor
box-sizing border-box
content ''
display inline-block
@ -100,9 +109,4 @@ root(isDark)
transition border-radius 1s ease
z-index 1
.mk-avatar[data-darkmode]
root(true)
.mk-avatar:not([data-darkmode])
root(false)
</style>

View file

@ -57,7 +57,7 @@ export default Vue.extend({
}
// Check internet connection
fetch('https://google.com?rand=' + Math.random(), {
fetch(`https://google.com?rand=${Math.random()}`, {
mode: 'no-cors'
}).then(() => {
this.internet = true;

View file

@ -39,7 +39,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.mk-connect-failed
width 100%
@ -70,17 +70,17 @@ export default Vue.extend({
display block
margin 1em auto 0 auto
padding 8px 10px
color $theme-color-foreground
background $theme-color
color var(--primaryForeground)
background var(--primary)
&:focus
outline solid 3px rgba($theme-color, 0.3)
outline solid 3px var(--primaryAlpha03)
&:hover
background lighten($theme-color, 10%)
background var(--primaryLighten10)
&:active
background darken($theme-color, 10%)
background var(--primaryDarken10)
> .thanks
display block

View file

@ -0,0 +1,38 @@
<template>
<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
type: Boolean,
required: true
}
},
methods: {
toggle() {
this.$emit('input', !this.value);
}
}
});
</script>
<style lang="stylus" scoped>
.nrvgflfuaxwgkxoynpnumyookecqrrvh
display inline-block
padding 4px 8px
font-size 0.7em
color var(--cwButtonFg)
background var(--cwButtonBg)
border-radius 2px
cursor pointer
user-select none
&:hover
background var(--cwButtonHoverBg)
</style>

View file

@ -9,7 +9,7 @@
</template>
<style lang="stylus" scoped>
@import '~const.styl'
.a
display block
@ -18,8 +18,8 @@
display block
//fill #151513
//color #fff
fill $theme-color
color $theme-color-foreground
fill var(--primary)
color var(--primaryForeground)
.octo-arm
transform-origin 130px 106px

View file

@ -50,15 +50,15 @@
</div>
<div class="player" v-if="game.isEnded">
<el-button-group>
<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
</el-button-group>
<div>
<button @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</button>
<button @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</button>
</div>
<span>{{ logPos }} / {{ logs.length }}</span>
<el-button-group>
<el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button>
<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
</el-button-group>
<div>
<button @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</button>
<button @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</button>
</div>
</div>
<div class="info">
@ -159,11 +159,9 @@ export default Vue.extend({
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
});
this.logs.forEach((log, i) => {
if (i < v) {
this.o.put(log.color, log.pos);
}
});
for (const log of this.logs.slice(0, v)) {
this.o.put(log.color, log.pos);
}
this.$forceUpdate();
}
},
@ -306,9 +304,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.xqnhankfuuilcwvhgsopeqncafzsquya
text-align center
> .go-index
@ -321,7 +317,7 @@ root(isDark)
> header
padding 8px
border-bottom dashed 1px isDark ? #4c5761 : #c4cdd4
border-bottom dashed 1px var(--reversiGameHeaderLine)
a
color inherit
@ -388,30 +384,30 @@ root(isDark)
user-select none
&.empty
border solid 2px isDark ? #51595f : #eee
border solid 2px var(--reversiGameEmptyCell)
&.empty.can
background isDark ? #51595f : #eee
background var(--reversiGameEmptyCell)
&.empty.myTurn
border-color isDark ? #6a767f : #ddd
border-color var(--reversiGameEmptyCellMyTurn)
&.can
background isDark ? #51595f : #eee
background var(--reversiGameEmptyCellCanPut)
cursor pointer
&:hover
border-color darken($theme-color, 10%)
background $theme-color
border-color var(--primaryDarken10)
background var(--primary)
&:active
background darken($theme-color, 10%)
background var(--primaryDarken10)
&.prev
box-shadow 0 0 0 4px rgba($theme-color, 0.7)
box-shadow 0 0 0 4px var(--primaryAlpha07)
&.isEnded
border-color isDark ? #6a767f : #ddd
border-color var(--reversiGameEmptyCellMyTurn)
&.none
border-color transparent !important
@ -460,10 +456,4 @@ root(isDark)
margin 0 8px
min-width 70px
.xqnhankfuuilcwvhgsopeqncafzsquya[data-darkmode]
root(true)
.xqnhankfuuilcwvhgsopeqncafzsquya:not([data-darkmode])
root(false)
</style>

View file

@ -9,7 +9,6 @@
import Vue from 'vue';
import XGame from './reversi.game.vue';
import XRoom from './reversi.room.vue';
import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game';
export default Vue.extend({
components: {
@ -34,12 +33,13 @@ export default Vue.extend({
},
created() {
this.g = this.game;
this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game);
this.connection = (this as any).os.stream.connectToChannel('gamesReversiGame', {
gameId: this.game.id
});
this.connection.on('started', this.onStarted);
},
beforeDestroy() {
this.connection.off('started', this.onStarted);
this.connection.close();
this.connection.dispose();
},
methods: {
onStarted(game) {

View file

@ -3,7 +3,6 @@
<h1>%i18n:@title%</h1>
<p>%i18n:@sub-title%</p>
<div class="play">
<!--<el-button round>フリーマッチ(準備中)</el-button>-->
<form-button primary round @click="match">%i18n:@invite%</form-button>
<details>
<summary>%i18n:@rule%</summary>
@ -60,15 +59,13 @@ export default Vue.extend({
myGames: [],
matching: null,
invitations: [],
connection: null,
connectionId: null
connection: null
};
},
mounted() {
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.streams.reversiStream.getConnection();
this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
this.connection.on('invited', this.onInvited);
@ -91,8 +88,7 @@ export default Vue.extend({
beforeDestroy() {
if (this.connection) {
this.connection.off('invited', this.onInvited);
(this as any).os.streams.reversiStream.dispose(this.connectionId);
this.connection.dispose();
}
},
@ -139,9 +135,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx
> h1
margin 0
padding 24px
@ -149,7 +143,7 @@ root(isDark)
text-align center
font-weight normal
color #fff
background linear-gradient(to bottom, isDark ? #45730e : #8bca3e, isDark ? #464300 : #d6cf31)
background linear-gradient(to bottom, var(--reversiBannerGradientStart), var(--reversiBannerGradientEnd))
& + p
margin 0
@ -157,7 +151,7 @@ root(isDark)
margin-bottom 12px
text-align center
font-size 14px
border-bottom solid 1px isDark ? #535f65 : #d3d9dc
border-bottom solid 1px var(--faceDivider)
> .play
margin 0 auto
@ -172,14 +166,14 @@ root(isDark)
padding 16px
font-size 14px
text-align left
background isDark ? #282c37 : #f5f5f5
background var(--reversiDescBg)
border-radius 8px
> section
margin 0 auto
padding 0 16px 16px 16px
max-width 500px
border-top solid 1px isDark ? #535f65 : #d3d9dc
border-top solid 1px var(--faceDivider)
> h2
margin 0
@ -190,9 +184,9 @@ root(isDark)
.invitation
margin 8px 0
padding 8px
color isDark ? #fff : #677f84
background isDark ? #282c37 : #fff
box-shadow 0 2px 16px rgba(#000, isDark ? 0.7 : 0.15)
color var(--text)
background var(--face)
box-shadow 0 2px 16px var(--reversiListItemShadow)
border-radius 6px
cursor pointer
@ -201,13 +195,13 @@ root(isDark)
user-select none
&:focus
border-color $theme-color
border-color var(--primary)
&:hover
background isDark ? #313543 : #f5f5f5
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
background isDark ? #1e222b : #eee
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
> .avatar
width 32px
@ -222,9 +216,9 @@ root(isDark)
display block
margin 8px 0
padding 8px
color isDark ? #fff : #677f84
background isDark ? #282c37 : #fff
box-shadow 0 2px 16px rgba(#000, isDark ? 0.7 : 0.15)
color var(--text)
background var(--face)
box-shadow 0 2px 16px var(--reversiListItemShadow)
border-radius 6px
cursor pointer
@ -233,10 +227,10 @@ root(isDark)
user-select none
&:hover
background isDark ? #313543 : #f5f5f5
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
background isDark ? #1e222b : #eee
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
> .avatar
width 32px
@ -247,10 +241,4 @@ root(isDark)
margin 0 8px
line-height 32px
.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx[data-darkmode]
root(true)
.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx:not([data-darkmode])
root(false)
</style>

View file

@ -47,9 +47,9 @@
</header>
<div>
<mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="%i18n:@is-llotheo%"/>
<mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="%i18n:@looped-map%"/>
<mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="%i18n:@can-put-everywhere%"/>
<ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">%i18n:@is-llotheo%</ui-switch>
<ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">%i18n:@looped-map%</ui-switch>
<ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">%i18n:@can-put-everywhere%</ui-switch>
</div>
</div>
@ -59,13 +59,8 @@
</header>
<div>
<el-alert v-for="message in messages"
:title="message.text"
:type="message.type"
:key="message.id"/>
<template v-for="item in form">
<mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch>
<ui-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</ui-switch>
<div class="card" v-if="item.type == 'radio'" :key="item.id">
<header>
@ -93,7 +88,7 @@
</header>
<div>
<el-input v-model="item.value" @change="onChangeForm(item)"/>
<input v-model="item.value" @change="onChangeForm(item)"/>
</div>
</div>
</template>
@ -257,11 +252,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.urbixznjwwuukfsckrwzwsqzsxornqij
text-align center
background isDark ? #191b22 : #f9f9f9
background var(--bg)
> header
padding 8px
@ -278,10 +271,10 @@ root(isDark)
> select
width 100%
padding 12px 14px
background isDark ? #282C37 : #fff
border 1px solid isDark ? #6a707d : #dcdfe6
background var(--face)
border 1px solid var(--reversiMapSelectBorder)
border-radius 4px
color isDark ? #fff : #606266
color var(--text)
cursor pointer
transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)
-webkit-appearance none
@ -289,17 +282,18 @@ root(isDark)
appearance none
&:hover
border-color isDark ? #a7aebd : #c0c4cc
border-color var(--reversiMapSelectHoverBorder)
&:focus
&:active
border-color $theme-color
border-color var(--primary)
> div
> .random
padding 32px 0
font-size 64px
color isDark ? #4e5961 : #d8d8d8
color var(--text)
opacity 0.7
> .board
display grid
@ -307,11 +301,11 @@ root(isDark)
width 300px
height 300px
margin 0 auto
color isDark ? #fff : #444
color var(--text)
> div
background transparent
border solid 2px isDark ? #6a767f : #ddd
border solid 2px var(--faceDivider)
border-radius 6px
overflow hidden
cursor pointer
@ -336,32 +330,26 @@ root(isDark)
.card
max-width 400px
border-radius 4px
background isDark ? #282C37 : #fff
color isDark ? #fff : #303133
box-shadow 0 2px 12px 0 rgba(#000, isDark ? 0.7 : 0.1)
background var(--face)
color var(--text)
box-shadow 0 2px 12px 0 var(--reversiRoomFormShadow)
> header
padding 18px 20px
border-bottom 1px solid isDark ? #1c2023 : #ebeef5
border-bottom 1px solid var(--faceDivider)
> div
padding 20px
color isDark ? #fff : #606266
color var(--text)
> footer
position sticky
bottom 0
padding 16px
background rgba(isDark ? #191b22 : #fff, 0.9)
border-top solid 1px isDark ? #606266 : #c4cdd4
background var(--reversiRoomFooterBg)
border-top solid 1px var(--faceDivider)
> .status
margin 0 0 16px 0
.urbixznjwwuukfsckrwzwsqzsxornqij[data-darkmode]
root(true)
.urbixznjwwuukfsckrwzwsqzsxornqij:not([data-darkmode])
root(false)
</style>

View file

@ -47,7 +47,6 @@ export default Vue.extend({
game: null,
matching: null,
connection: null,
connectionId: null,
pingClock: null
};
},
@ -66,8 +65,7 @@ export default Vue.extend({
this.fetch();
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.streams.reversiStream.getConnection();
this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
this.connection.on('matched', this.onMatched);
@ -84,9 +82,7 @@ export default Vue.extend({
beforeDestroy() {
if (this.connection) {
this.connection.off('matched', this.onMatched);
(this as any).os.streams.reversiStream.dispose(this.connectionId);
this.connection.dispose();
clearInterval(this.pingClock);
}
},
@ -156,11 +152,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
color isDark ? #fff : #677f84
background isDark ? #191b22 : #fff
.vchtoekanapleubgzioubdtmlkribzfd
color var(--text)
background var(--bg)
> .matching
> h1
@ -177,10 +171,4 @@ root(isDark)
text-align center
border-top dashed 1px #c4cdd4
.vchtoekanapleubgzioubdtmlkribzfd[data-darkmode]
root(true)
.vchtoekanapleubgzioubdtmlkribzfd:not([data-darkmode])
root(false)
</style>

View file

@ -26,7 +26,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
root(isDark)
.mk-google
display flex
margin 8px 0
@ -37,31 +37,25 @@ root(isDark)
height 40px
font-family sans-serif
font-size 16px
color isDark ? #dee4e8 : #55595c
background isDark ? #191b22 : #fff
border solid 1px isDark ? #495156 : #dadada
color var(--googleSearchFg)
background var(--googleSearchBg)
border solid 1px var(--googleSearchBorder)
border-radius 4px 0 0 4px
&:hover
border-color isDark ? #777c86 : #b0b0b0
border-color var(--googleSearchHoverBorder)
> button
flex-shrink 0
padding 0 16px
border solid 1px isDark ? #495156 : #dadada
border solid 1px var(--googleSearchBorder)
border-left none
border-radius 0 4px 4px 0
&:hover
background-color isDark ? #2e3440 : #eee
background-color var(--googleSearchHoverButton)
&:active
box-shadow 0 2px 4px rgba(#000, 0.15) inset
.mk-google[data-darkmode]
root(true)
.mk-google:not([data-darkmode])
root(false)
</style>

View file

@ -1,5 +1,10 @@
import Vue from 'vue';
import theme from './theme.vue';
import instance from './instance.vue';
import cwButton from './cw-button.vue';
import tagCloud from './tag-cloud.vue';
import trends from './trends.vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
@ -26,7 +31,6 @@ import messagingRoom from './messaging-room.vue';
import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue';
import fileTypeIcon from './file-type-icon.vue';
import Switch from './switch.vue';
import Reversi from './games/reversi/reversi.vue';
import welcomeTimeline from './welcome-timeline.vue';
import uiInput from './ui/input.vue';
@ -40,6 +44,11 @@ import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
Vue.component('mk-theme', theme);
Vue.component('mk-instance', instance);
Vue.component('mk-cw-button', cwButton);
Vue.component('mk-tag-cloud', tagCloud);
Vue.component('mk-trends', trends);
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
@ -66,7 +75,6 @@ Vue.component('mk-messaging-room', messagingRoom);
Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting);
Vue.component('mk-file-type-icon', fileTypeIcon);
Vue.component('mk-switch', Switch);
Vue.component('mk-reversi', Reversi);
Vue.component('mk-welcome-timeline', welcomeTimeline);
Vue.component('ui-input', uiInput);

View file

@ -0,0 +1,51 @@
<template>
<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta">
<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
<h1>{{ meta.name }}</h1>
<p v-html="meta.description || '%i18n:common.about%'"></p>
<router-link to="/">%i18n:@start%</router-link>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
meta: null
}
},
created() {
(this as any).os.getMeta().then(meta => {
this.meta = meta;
});
}
});
</script>
<style lang="stylus" scoped>
.nhasjydimbopojusarffqjyktglcuxjy
color var(--text)
background var(--face)
text-align center
> .banner
height 100px
background-position center
background-size cover
> h1
margin 16px
font-size 16px
> p
margin 16px
font-size 14px
> a
display block
padding-bottom 16px
</style>

View file

@ -0,0 +1,85 @@
<template>
<div class="mk-media-banner">
<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
<span class="icon">%fa:exclamation-triangle%</span>
<b>%i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
</div>
<div class="audio" v-else-if="media.type.startsWith('audio')">
<audio class="audio"
:src="media.url"
:title="media.name"
controls
ref="audio"
preload="metadata" />
</div>
<a class="download" v-else
:href="media.url"
:title="media.name"
:download="media.name"
>
<span class="icon">%fa:download%</span>
<b>{{ media.name }}</b>
</a>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
media: {
type: Object,
required: true
}
},
data() {
return {
hide: true
};
}
})
</script>
<style lang="stylus" scoped>
.mk-media-banner
width 100%
border-radius 4px
margin-top 4px
overflow hidden
> .download,
> .sensitive
display flex
align-items center
font-size 12px
padding 8px 12px
white-space nowrap
> *
display block
> b
overflow hidden
text-overflow ellipsis
> *:not(:last-child)
margin-right .2em
> .icon
font-size 1.6em
> .download
background var(--noteAttachedFile)
> .sensitive
background #111
color #fff
> .audio
.audio
display block
width 100%
</style>

View file

@ -1,18 +1,27 @@
<template>
<div class="mk-media-list">
<div :data-count="mediaList.length" ref="grid">
<template v-for="media in mediaList">
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
<mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
</template>
<template v-for="media in mediaList.filter(media => !previewable(media))">
<x-banner :media="media" :key="media.id"/>
</template>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
<template v-for="media in mediaList">
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
<mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
</template>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import XBanner from './media-banner.vue';
export default Vue.extend({
components: {
XBanner
},
props: {
mediaList: {
required: true
@ -22,70 +31,80 @@ export default Vue.extend({
}
},
mounted() {
// for Safari bug
this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
//#region for Safari bug
if (this.$refs.grid) {
this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
}
//#endregion
},
methods: {
previewable(file) {
return file.type.startsWith('video') || file.type.startsWith('image');
}
}
});
</script>
<style lang="stylus" scoped>
.mk-media-list
width 100%
> .gird-container
width 100%
margin-top 4px
&:before
content ''
display block
padding-top 56.25% // 16:9
&:before
content ''
display block
padding-top 56.25% // 16:9
> div
position absolute
top 0
right 0
bottom 0
left 0
display grid
grid-gap 4px
> div
position absolute
top 0
right 0
bottom 0
left 0
display grid
grid-gap 4px
> *
overflow hidden
border-radius 4px
> *
overflow hidden
border-radius 4px
&[data-count="1"]
grid-template-rows 1fr
&[data-count="1"]
grid-template-rows 1fr
&[data-count="2"]
grid-template-columns 1fr 1fr
grid-template-rows 1fr
&[data-count="2"]
grid-template-columns 1fr 1fr
grid-template-rows 1fr
&[data-count="3"]
grid-template-columns 1fr 0.5fr
grid-template-rows 1fr 1fr
&[data-count="3"]
grid-template-columns 1fr 0.5fr
grid-template-rows 1fr 1fr
> *:nth-child(1)
grid-row 1 / 3
> *:nth-child(3)
grid-column 2 / 3
grid-row 2 / 3
&[data-count="4"]
grid-template-columns 1fr 1fr
grid-template-rows 1fr 1fr
> *:nth-child(1)
grid-row 1 / 3
grid-column 1 / 2
grid-row 1 / 2
> *:nth-child(2)
grid-column 2 / 3
grid-row 1 / 2
> *:nth-child(3)
grid-column 1 / 2
grid-row 2 / 3
> *:nth-child(4)
grid-column 2 / 3
grid-row 2 / 3
&[data-count="4"]
grid-template-columns 1fr 1fr
grid-template-rows 1fr 1fr
> *:nth-child(1)
grid-column 1 / 2
grid-row 1 / 2
> *:nth-child(2)
grid-column 2 / 3
grid-row 1 / 2
> *:nth-child(3)
grid-column 1 / 2
grid-row 2 / 3
> *:nth-child(4)
grid-column 2 / 3
grid-row 2 / 3
</style>

View file

@ -1,10 +1,10 @@
<template>
<div class="mk-menu">
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item in items">
<template v-for="item, i in items">
<div v-if="item === null"></div>
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button>
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text" :tabindex="i"></button>
</template>
</div>
</div>
@ -108,7 +108,7 @@ export default Vue.extend({
easing: 'easeInBack',
complete: () => {
this.$emit('closed');
this.$destroy();
this.destroyDom();
}
});
}
@ -117,11 +117,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.onchrpzrvnoruiaenfcqvccjfuupzzwv
$bg-color = var(--popupBg)
$border-color = rgba(27, 31, 35, 0.15)
$border-color = rgba(27, 31, 35, 0.15)
.mk-menu
position initial
> .backdrop
@ -131,14 +130,14 @@ $border-color = rgba(27, 31, 35, 0.15)
z-index 10000
width 100%
height 100%
background rgba(#000, 0.1)
background var(--modalBackdrop)
opacity 0
> .popover
position absolute
z-index 10001
padding 8px 0
background #fff
background $bg-color
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
@ -172,25 +171,26 @@ $border-color = rgba(27, 31, 35, 0.15)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size #fff
border-bottom solid $balloon-size $bg-color
> button
display block
padding 8px 16px
width 100%
color var(--popupFg)
&:hover
color $theme-color-foreground
background $theme-color
color var(--primaryForeground)
background var(--primary)
text-decoration none
&:active
color $theme-color-foreground
background darken($theme-color, 10%)
color var(--primaryForeground)
background var(--primaryDarken10)
> div
margin 8px 0
height 1px
background #eee
background var(--faceDivider)
</style>

View file

@ -195,9 +195,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mk-messaging-form
> textarea
cursor auto
display block
@ -209,10 +207,10 @@ root(isDark)
padding 8px
resize none
font-size 1em
color isDark ? #fff : #000
color var(--inputText)
outline none
border none
border-top solid 1px isDark ? #4b5056 : #eee
border-top solid 1px var(--faceDivider)
border-radius 0
box-shadow none
background transparent
@ -234,10 +232,10 @@ root(isDark)
transition color 0.1s ease
&:hover
color $theme-color
color var(--primary)
&:active
color darken($theme-color, 10%)
color var(--primaryDarken10)
transition color 0s ease
.files
@ -293,19 +291,13 @@ root(isDark)
transition color 0.1s ease
&:hover
color $theme-color
color var(--primary)
&:active
color darken($theme-color, 10%)
color var(--primaryDarken10)
transition color 0s ease
input[type=file]
display none
.mk-messaging-form[data-darkmode]
root(true)
.mk-messaging-form:not([data-darkmode])
root(false)
</style>

View file

@ -59,10 +59,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
$me-balloon-color = $theme-color
.message
$me-balloon-color = var(--primary)
padding 10px 12px 10px 12px
background-color transparent
@ -179,7 +177,7 @@ root(isDark)
display block
margin 2px 0 0 0
font-size 10px
color isDark ? rgba(#fff, 0.4) : rgba(#000, 0.4)
color var(--messagingRoomMessageInfo)
> [data-fa]
margin-left 4px
@ -192,7 +190,7 @@ root(isDark)
padding-left 66px
> .balloon
$color = isDark ? #2d3338 : #eee
$color = var(--messagingRoomMessageBg)
float left
background $color
@ -208,8 +206,7 @@ root(isDark)
> .content
> .text
if isDark
color #fff
color var(--messagingRoomMessageFg)
> footer
text-align left
@ -250,18 +247,9 @@ root(isDark)
> .read
user-select none
margin 0 4px 0 0
color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5)
font-size 11px
&[data-is-deleted]
> .balloon
opacity 0.5
.message[data-darkmode]
root(true)
.message:not([data-darkmode])
root(false)
</style>

View file

@ -3,7 +3,7 @@
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
<div class="stream">
<div class="body">
<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p>
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p>
@ -30,7 +30,6 @@
<script lang="ts">
import Vue from 'vue';
import { MessagingStream } from '../../scripts/streaming/messaging';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import { url } from '../../../config';
@ -72,11 +71,17 @@ export default Vue.extend({
},
mounted() {
this.connection = new MessagingStream((this as any).os, this.$store.state.i, this.user.id);
this.connection =((this as any).os.stream.connectToChannel('messaging', { otherparty: this.user.id });
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
if (this.isNaked) {
window.addEventListener('scroll', this.onScroll, { passive: true });
} else {
this.$el.addEventListener('scroll', this.onScroll, { passive: true });
}
document.addEventListener('visibilitychange', this.onVisibilitychange);
this.fetchMessages().then(() => {
@ -86,9 +91,13 @@ export default Vue.extend({
},
beforeDestroy() {
this.connection.off('message', this.onMessage);
this.connection.off('read', this.onRead);
this.connection.close();
this.connection.dispose();
if (this.isNaked) {
window.removeEventListener('scroll', this.onScroll);
} else {
this.$el.removeEventListener('scroll', this.onScroll);
}
document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
@ -226,6 +235,14 @@ export default Vue.extend({
}, 4000);
},
onScroll() {
const el = this.isNaked ? window.document.documentElement : this.$el;
const current = el.scrollTop + el.clientHeight;
if (current > el.scrollHeight - 1) {
this.showIndicator = false;
}
},
onVisibilitychange() {
if (document.hidden) return;
this.messages.forEach(message => {
@ -242,39 +259,28 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mk-messaging-room
display flex
flex 1
flex-direction column
height 100%
background isDark ? #191b22 : #fff
background var(--messagingRoomBg)
> .stream
> .body
width 100%
max-width 600px
margin 0 auto
flex 1
> .init
width 100%
margin 0
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
> .init,
> .empty
width 100%
margin 0
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
color rgba(isDark ? #fff : #000, 0.4)
color var(--messagingRoomInfo)
opacity 0.5
[data-fa]
margin-right 4px
@ -285,7 +291,8 @@ root(isDark)
padding 16px
text-align center
font-size 0.8em
color rgba(isDark ? #fff : #000, 0.4)
color var(--messagingRoomInfo)
opacity 0.5
[data-fa]
margin-right 4px
@ -329,7 +336,7 @@ root(isDark)
left 0
right 0
margin 0 auto
background rgba(isDark ? #fff : #000, 0.1)
background var(--messagingRoomDateDividerLine)
> span
display inline-block
@ -337,8 +344,8 @@ root(isDark)
padding 0 16px
//font-weight bold
line-height 32px
color rgba(isDark ? #fff : #000, 0.3)
background isDark ? #191b22 : #fff
color var(--messagingRoomDateDividerText)
background var(--messagingRoomBg)
> footer
position -webkit-sticky
@ -349,7 +356,7 @@ root(isDark)
max-width 600px
margin 0 auto
padding 0
background rgba(isDark ? #282c37 : #fff, 0.95)
//background rgba(var(--face), 0.95)
background-clip content-box
> .new-message
@ -366,15 +373,15 @@ root(isDark)
cursor pointer
line-height 32px
font-size 12px
color $theme-color-foreground
background $theme-color
color var(--primaryForeground)
background var(--primary)
border-radius 16px
&:hover
background lighten($theme-color, 10%)
background var(--primaryLighten10)
&:active
background darken($theme-color, 10%)
background var(--primaryDarken10)
> [data-fa]
position absolute
@ -390,10 +397,4 @@ root(isDark)
transition opacity 0.5s
opacity 0
.mk-messaging-room[data-darkmode]
root(true)
.mk-messaging-room:not([data-darkmode])
root(false)
</style>

View file

@ -71,13 +71,11 @@ export default Vue.extend({
messages: [],
q: null,
result: [],
connection: null,
connectionId: null
connection: null
};
},
mounted() {
this.connection = (this as any).os.streams.messagingIndexStream.getConnection();
this.connectionId = (this as any).os.streams.messagingIndexStream.use();
this.connection = (this as any).os.stream.useSharedConnection('messagingIndex');
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
@ -88,9 +86,7 @@ export default Vue.extend({
});
},
beforeDestroy() {
this.connection.off('message', this.onMessage);
this.connection.off('read', this.onRead);
(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
this.connection.dispose();
},
methods: {
getAcct,
@ -167,9 +163,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mk-messaging
&[data-compact]
font-size 0.8em
@ -204,12 +198,10 @@ root(isDark)
left 0
z-index 1
width 100%
background #fff
box-shadow 0 0px 2px rgba(#000, 0.2)
> .form
padding 8px
background isDark ? #282c37 : #f7f7f7
background rgba(0, 0, 0, 0.02)
> label
display block
@ -229,32 +221,22 @@ root(isDark)
bottom 0
left 0
width 1em
line-height 56px
line-height 48px
margin auto
color #555
> input
margin 0
padding 0 0 0 32px
padding 0 0 0 42px
width 100%
font-size 1em
line-height 38px
color #000
line-height 48px
color var(--faceText)
outline none
background isDark ? #191b22 : #fff
border solid 1px isDark ? #495156 : #eee
background transparent
border none
border-radius 5px
box-shadow none
transition color 0.5s ease, border 0.5s ease
&:hover
border solid 1px isDark ? #b0b0b0 : #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
@ -287,7 +269,7 @@ root(isDark)
&:hover
&:focus
color #fff
background $theme-color
background var(--primary)
.name
color #fff
@ -297,7 +279,7 @@ root(isDark)
&:active
color #fff
background darken($theme-color, 10%)
background var(--primaryDarken10)
.name
color #fff
@ -329,21 +311,21 @@ root(isDark)
> a
display block
text-decoration none
background isDark ? #282c37 : #fff
border-bottom solid 1px isDark ? #1c2023 : #eee
background var(--face)
border-bottom solid 1px var(--faceDivider)
*
pointer-events none
user-select none
&:hover
background isDark ? #1e2129 : #fafafa
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
> .avatar
.avatar
filter saturate(200%)
&:active
background isDark ? #14161b : #eee
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
&[data-is-read]
&[data-is-me]
@ -383,17 +365,17 @@ root(isDark)
overflow hidden
text-overflow ellipsis
font-size 1em
color isDark ? #fff : rgba(#000, 0.9)
color var(--noteHeaderName)
font-weight bold
transition all 0.1s ease
> .username
margin 0 8px
color isDark ? #606984 : rgba(#000, 0.5)
color var(--noteHeaderAcct)
> .mk-time
margin 0 0 0 auto
color isDark ? #606984 : rgba(#000, 0.5)
color var(--noteHeaderInfo)
font-size 80%
> .avatar
@ -413,10 +395,10 @@ root(isDark)
overflow hidden
overflow-wrap break-word
font-size 1.1em
color isDark ? #fff : rgba(#000, 0.8)
color var(--faceText)
.me
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.4)
opacity 0.7
> .image
display block
@ -461,10 +443,4 @@ root(isDark)
> .avatar
margin 0 12px 0 0
.mk-messaging[data-darkmode]
root(true)
.mk-messaging:not([data-darkmode])
root(false)
</style>

View file

@ -1,4 +1,4 @@
import Vue from 'vue';
import Vue, { VNode } from 'vue';
import * as emojilib from 'emojilib';
import { length } from 'stringz';
import parse from '../../../../../mfm/parse';
@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/acct/render';
import { url } from '../../../config';
import MkUrl from './url.vue';
import MkGoogle from './google.vue';
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
);
import { concat } from '../../../../../prelude/array';
export default Vue.component('misskey-flavored-markdown', {
props: {
@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', {
},
render(createElement) {
let ast;
let ast: any[];
if (this.ast == null) {
// Parse text to ast
ast = parse(this.text);
} else {
ast = this.ast;
ast = this.ast as any[];
}
let bigCount = 0;
let motionCount = 0;
// Parse ast to DOM
const els = flatten(ast.map(token => {
const els = concat(ast.map((token): VNode[] => {
switch (token.type) {
case 'text': {
const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', {
x[x.length - 1].pop();
return x;
} else {
return createElement('span', text.replace(/\n/g, ' '));
return [createElement('span', text.replace(/\n/g, ' '))];
}
}
case 'bold': {
return createElement('b', token.bold);
return [createElement('b', token.bold)];
}
case 'big': {
@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'url': {
return createElement(MkUrl, {
return [createElement(MkUrl, {
props: {
url: token.content,
target: '_blank'
}
});
})];
}
case 'link': {
return createElement('a', {
return [createElement('a', {
attrs: {
class: 'link',
href: token.url,
target: '_blank',
title: token.url
}
}, token.title);
}, token.title)];
}
case 'mention': {
@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'hashtag': {
return createElement('a', {
return [createElement('a', {
attrs: {
href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank'
}
}, token.content);
}, token.content)];
}
case 'code': {
return createElement('pre', {
return [createElement('pre', {
class: 'code'
}, [
createElement('code', {
@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', {
innerHTML: token.html
}
})
]);
])];
}
case 'inline-code': {
return createElement('code', {
return [createElement('code', {
domProps: {
innerHTML: token.html
}
});
})];
}
case 'quote': {
@ -164,58 +161,51 @@ export default Vue.component('misskey-flavored-markdown', {
const x = text2.split('\n')
.map(t => [createElement('span', t), createElement('br')]);
x[x.length - 1].pop();
return createElement('div', {
return [createElement('div', {
attrs: {
class: 'quote'
}
}, x);
}, x)];
} else {
return createElement('span', {
return [createElement('span', {
attrs: {
class: 'quote'
}
}, text2.replace(/\n/g, ' '));
}, text2.replace(/\n/g, ' '))];
}
}
case 'title': {
return createElement('div', {
return [createElement('div', {
attrs: {
class: 'title'
}
}, token.title);
}, token.title)];
}
case 'emoji': {
const emoji = emojilib.lib[token.emoji];
return createElement('span', emoji ? emoji.char : token.content);
return [createElement('span', emoji ? emoji.char : token.content)];
}
case 'search': {
return createElement(MkGoogle, {
return [createElement(MkGoogle, {
props: {
q: token.query
}
});
})];
}
default: {
console.log('unknown ast type:', token.type);
return [];
}
}
}));
const _els = [];
els.forEach((el, i) => {
if (el.tag == 'br') {
if (!['div', 'pre'].includes(els[i - 1].tag)) {
_els.push(el);
}
} else {
_els.push(el);
}
});
// el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない
const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag)));
return createElement('span', _els);
}
});

View file

@ -2,6 +2,8 @@
<span class="mk-nav">
<a :href="aboutUrl">%i18n:@about%</a>
<i></i>
<a href="/stats">%i18n:@stats%</a>
<i></i>
<a :href="repositoryUrl">%i18n:@repository%</a>
<i></i>
<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>

View file

@ -42,9 +42,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.bvonvjxbwzaiskogyhbwgyxvcgserpmu
display flex
align-items baseline
white-space nowrap
@ -61,7 +59,7 @@ root(isDark)
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
color var(--noteHeaderName)
font-size 1em
font-weight bold
text-decoration none
@ -82,19 +80,19 @@ root(isDark)
margin 0 .5em 0 0
padding 1px 6px
font-size 80%
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
color var(--noteHeaderBadgeFg)
background var(--noteHeaderBadgeBg)
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
background var(--noteHeaderAdminBg)
color var(--noteHeaderAdminFg)
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
color var(--noteHeaderAcct)
flex-shrink 2147483647
> .info
@ -102,7 +100,7 @@ root(isDark)
font-size 0.9em
> *
color isDark ? #606984 : #c0c0c0
color var(--noteHeaderInfo)
> .mobile
margin-right 8px
@ -110,15 +108,9 @@ root(isDark)
> .app
margin-right 8px
padding-right 8px
border-right solid 1px isDark ? #1c2023 : #eaeaea
border-right solid 1px var(--faceDivider)
> .visibility
margin-left 8px
.bvonvjxbwzaiskogyhbwgyxvcgserpmu[data-darkmode]
root(true)
.bvonvjxbwzaiskogyhbwgyxvcgserpmu:not([data-darkmode])
root(false)
</style>

View file

@ -6,29 +6,51 @@
<script lang="ts">
import Vue from 'vue';
import { url } from '../../../config';
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
export default Vue.extend({
props: ['note', 'source', 'compact'],
computed: {
items() {
const items = [];
items.push({
const items = [{
icon: '%fa:info-circle%',
text: '%i18n:@detail%',
action: this.detail
}, {
icon: '%fa:link%',
text: '%i18n:@copy-link%',
action: this.copyLink
}, null, {
icon: '%fa:star%',
text: '%i18n:@favorite%',
action: this.favorite
});
}];
if (this.note.userId == this.$store.state.i.id) {
items.push({
icon: '%fa:thumbtack%',
text: '%i18n:@pin%',
action: this.pin
});
if ((this.$store.state.i.pinnedNoteIds || []).includes(this.note.id)) {
items.push({
icon: '%fa:thumbtack%',
text: '%i18n:@unpin%',
action: this.unpin
});
} else {
items.push({
icon: '%fa:thumbtack%',
text: '%i18n:@pin%',
action: this.pin
});
}
}
if (this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin) {
items.push({
icon: '%fa:trash-alt R%',
text: '%i18n:@delete%',
action: this.del
});
}
if (this.note.uri) {
items.push({
icon: '%fa:external-link-square-alt%',
@ -38,15 +60,33 @@ export default Vue.extend({
}
});
}
return items;
}
},
methods: {
detail() {
this.$router.push(`/notes/${ this.note.id }`);
},
copyLink() {
copyToClipboard(`${url}/notes/${ this.note.id }`);
},
pin() {
(this as any).api('i/pin', {
noteId: this.note.id
}).then(() => {
this.$destroy();
this.destroyDom();
});
},
unpin() {
(this as any).api('i/unpin', {
noteId: this.note.id
}).then(() => {
this.destroyDom();
});
},
@ -55,7 +95,7 @@ export default Vue.extend({
(this as any).api('notes/delete', {
noteId: this.note.id
}).then(() => {
this.$destroy();
this.destroyDom();
});
},
@ -63,13 +103,13 @@ export default Vue.extend({
(this as any).api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
this.$destroy();
this.destroyDom();
});
},
closed() {
this.$nextTick(() => {
this.$destroy();
this.destroyDom();
});
}
}

View file

@ -20,6 +20,7 @@
<script lang="ts">
import Vue from 'vue';
import { erase } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
@ -53,7 +54,7 @@ export default Vue.extend({
get() {
return {
choices: this.choices.filter(choice => choice != '')
choices: erase('', this.choices)
}
},
@ -67,9 +68,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mk-poll-editor
padding 8px
> .caution
@ -102,49 +101,43 @@ root(isDark)
padding 6px 8px
width 300px
font-size 14px
color isDark ? #fff : #000
background isDark ? #191b22 : #fff
border solid 1px rgba($theme-color, 0.1)
color var(--inputText)
background var(--pollEditorInputBg)
border solid 1px var(--primaryAlpha01)
border-radius 4px
&:hover
border-color rgba($theme-color, 0.2)
border-color var(--primaryAlpha02)
&:focus
border-color rgba($theme-color, 0.5)
border-color var(--primaryAlpha05)
> button
padding 4px 8px
color rgba($theme-color, 0.4)
color var(--primaryAlpha04)
&:hover
color rgba($theme-color, 0.6)
color var(--primaryAlpha06)
&:active
color darken($theme-color, 30%)
color var(--primaryDarken30)
> .add
margin 8px 0 0 0
vertical-align top
color $theme-color
color var(--primary)
> .destroy
position absolute
top 0
right 0
padding 4px 8px
color rgba($theme-color, 0.4)
color var(--primaryAlpha04)
&:hover
color rgba($theme-color, 0.6)
color var(--primaryAlpha06)
&:active
color darken($theme-color, 30%)
.mk-poll-editor[data-darkmode]
root(true)
.mk-poll-editor:not([data-darkmode])
root(false)
color var(--primaryDarken30)
</style>

View file

@ -21,6 +21,7 @@
<script lang="ts">
import Vue from 'vue';
import { sum } from '../../../../../prelude/array';
export default Vue.extend({
props: ['note'],
data() {
@ -33,7 +34,7 @@ export default Vue.extend({
return this.note.poll;
},
total(): number {
return this.poll.choices.reduce((a, b) => a + b.votes, 0);
return sum(this.poll.choices.map(x => x.votes));
},
isVoted(): boolean {
return this.poll.choices.some(c => c.isVoted);
@ -66,10 +67,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mk-poll
> ul
display block
margin 0
@ -81,8 +79,8 @@ root(isDark)
margin 4px 0
padding 4px 8px
width 100%
color isDark ? #fff : #000
border solid 1px isDark ? #5e636f : #eee
color var(--pollChoiceText)
border solid 1px var(--pollChoiceBorder)
border-radius 4px
overflow hidden
cursor pointer
@ -98,7 +96,7 @@ root(isDark)
top 0
left 0
height 100%
background $theme-color
background var(--primary)
transition width 1s ease
> span
@ -109,7 +107,7 @@ root(isDark)
margin-left 4px
> p
color isDark ? #a3aebf : #000
color var(--text)
a
color inherit
@ -124,10 +122,4 @@ root(isDark)
&:active
background transparent
.mk-poll[data-darkmode]
root(true)
.mk-poll:not([data-darkmode])
root(false)
</style>

View file

@ -1,17 +1,17 @@
<template>
<span class="mk-reaction-icon">
<img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
<img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
<img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
<img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
<img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
<img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%">
<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%">
<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%">
<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%">
<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%">
<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%">
<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%">
<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%">
<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%">
<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%">
<template v-if="reaction == 'pudding'">
<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
<img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%">
<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%">
</template>
</span>
</template>

View file

@ -1,9 +1,9 @@
<template>
<div class="mk-reaction-picker">
<div class="mk-reaction-picker" v-hotkey.global="keymap">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact, big }" ref="popover">
<p v-if="!compact">{{ title }}</p>
<div>
<div ref="buttons" :class="{ showFocus }">
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
@ -31,30 +31,84 @@ export default Vue.extend({
type: Object,
required: true
},
source: {
required: true
},
compact: {
type: Boolean,
required: false,
default: false
},
cb: {
required: false
},
big: {
type: Boolean,
required: false,
default: false
},
showFocus: {
type: Boolean,
required: false,
default: false
},
animation: {
type: Boolean,
required: false,
default: true
}
},
data() {
return {
title: placeholder
title: placeholder,
focus: null
};
},
computed: {
keymap(): any {
return {
'esc': this.close,
'enter|space|plus': this.choose,
'up|k': this.focusUp,
'left|h|shift+tab': this.focusLeft,
'right|l|tab': this.focusRight,
'down|j': this.focusDown,
'1': () => this.react('like'),
'2': () => this.react('love'),
'3': () => this.react('laugh'),
'4': () => this.react('hmm'),
'5': () => this.react('surprise'),
'6': () => this.react('congrats'),
'7': () => this.react('angry'),
'8': () => this.react('confused'),
'9': () => this.react('rip'),
'0': () => this.react('pudding'),
};
}
},
watch: {
focus(i) {
this.$refs.buttons.children[i].focus();
if (this.showFocus) {
this.title = this.$refs.buttons.children[i].title;
}
}
},
mounted() {
this.$nextTick(() => {
this.focus = 0;
const popover = this.$refs.popover as any;
const rect = this.source.getBoundingClientRect();
@ -76,7 +130,7 @@ export default Vue.extend({
anime({
targets: this.$refs.backdrop,
opacity: 1,
duration: 100,
duration: this.animation ? 100 : 0,
easing: 'linear'
});
@ -84,10 +138,11 @@ export default Vue.extend({
targets: this.$refs.popover,
opacity: 1,
scale: [0.5, 1],
duration: 500
duration: this.animation ? 500 : 0
});
});
},
methods: {
react(reaction) {
(this as any).api('notes/reactions/create', {
@ -95,21 +150,25 @@ export default Vue.extend({
reaction: reaction
}).then(() => {
if (this.cb) this.cb();
this.$destroy();
this.$emit('closed');
this.destroyDom();
});
},
onMouseover(e) {
this.title = e.target.title;
},
onMouseout(e) {
this.title = placeholder;
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
targets: this.$refs.backdrop,
opacity: 0,
duration: 200,
duration: this.animation ? 200 : 0,
easing: 'linear'
});
@ -118,21 +177,42 @@ export default Vue.extend({
targets: this.$refs.popover,
opacity: 0,
scale: 0.5,
duration: 200,
duration: this.animation ? 200 : 0,
easing: 'easeInBack',
complete: () => this.$destroy()
complete: () => {
this.$emit('closed');
this.destroyDom();
}
});
},
focusUp() {
this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
},
focusDown() {
this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
},
focusRight() {
this.focus = this.focus == 9 ? 0 : (this.focus + 1);
},
focusLeft() {
this.focus = this.focus == 0 ? 9 : (this.focus - 1);
},
choose() {
this.$refs.buttons.childNodes[this.focus].click();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
$border-color = rgba(27, 31, 35, 0.15)
root(isDark)
.mk-reaction-picker
position initial
> .backdrop
@ -142,11 +222,11 @@ root(isDark)
z-index 10000
width 100%
height 100%
background isDark ? rgba(#000, 0.4) : rgba(#000, 0.1)
background var(--modalBackdrop)
opacity 0
> .popover
$bgcolor = isDark ? #2c303c : #fff
$bgcolor = var(--popupBg)
position absolute
z-index 10001
background $bgcolor
@ -199,14 +279,29 @@ root(isDark)
margin 0
padding 8px 10px
font-size 14px
color isDark ? #d6dce2 : #586069
border-bottom solid 1px isDark ? #1c2023 : #e1e4e8
color var(--popupFg)
border-bottom solid 1px var(--faceDivider)
> div
padding 4px
width 240px
text-align center
&.showFocus
> button:focus
z-index 1
&:after
content ""
pointer-events none
position absolute
top 0
right 0
bottom 0
left 0
border 2px solid var(--primaryAlpha03)
border-radius 4px
> button
padding 0
width 40px
@ -215,16 +310,10 @@ root(isDark)
border-radius 2px
&:hover
background isDark ? #252731 : #eee
background var(--reactionPickerButtonHoverBg)
&:active
background $theme-color
background var(--primary)
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
.mk-reaction-picker[data-darkmode]
root(true)
.mk-reaction-picker:not([data-darkmode])
root(false)
</style>

View file

@ -39,10 +39,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
root(isDark)
$borderColor = isDark ? #5e6673 : #eee
border-top dashed 1px $borderColor
border-bottom dashed 1px $borderColor
.mk-reactions-viewer
border-top dashed 1px var(--reactionViewerBorder)
border-bottom dashed 1px var(--reactionViewerBorder)
margin 4px 0
&:empty
@ -60,12 +59,6 @@ root(isDark)
> span
margin-left 4px
font-size 1.2em
color isDark ? #d1d5dc : #444
.mk-reactions-viewer[data-darkmode]
root(true)
.mk-reactions-viewer:not([data-darkmode])
root(false)
color var(--text)
</style>

View file

@ -1,16 +1,16 @@
<template>
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange" styl="fill">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
</ui-input>
<ui-input v-model="password" type="password" required>
<ui-input v-model="password" type="password" required styl="fill">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
</ui-input>
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
</form>
@ -56,7 +56,7 @@ export default Vue.extend({
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
}).then(() => {
}, true).then(() => {
location.reload();
}).catch(() => {
alert('%i18n:@login-failed%');
@ -68,7 +68,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.mk-signin
color #555
@ -78,7 +78,7 @@ export default Vue.extend({
cursor wait !important
> .avatar
margin 16px auto 0 auto
margin 0 auto 0 auto
width 64px
height 64px
background #ddd

View file

@ -1,12 +1,12 @@
<template>
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta">
<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
<span>%i18n:@invitation-code%</span>
<span slot="prefix">%fa:id-card-alt%</span>
<p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p>
</ui-input>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
@ -18,7 +18,7 @@
<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
</ui-input>
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
@ -27,7 +27,7 @@
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
</div>
</ui-input>
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill">
<span>%i18n:@password% (%i18n:@retype%)</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
@ -131,11 +131,11 @@ export default Vue.extend({
password: this.password,
invitationCode: this.invitationCode,
'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
}).then(() => {
}, true).then(() => {
(this as any).api('signin', {
username: this.username,
password: this.password
}).then(() => {
}, true).then(() => {
location.href = '/';
});
}).catch(() => {
@ -151,7 +151,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.mk-signup
min-width 302px

View file

@ -1,14 +1,14 @@
<template>
<div class="mk-stream-indicator">
<p v-if=" stream.state == 'initializing' ">
<p v-if="stream.state == 'initializing'">
%fa:spinner .pulse%
<span>%i18n:@connecting%<mk-ellipsis/></span>
</p>
<p v-if=" stream.state == 'reconnecting' ">
<p v-if="stream.state == 'reconnecting'">
%fa:spinner .pulse%
<span>%i18n:@reconnecting%<mk-ellipsis/></span>
</p>
<p v-if=" stream.state == 'connected' ">
<p v-if="stream.state == 'connected'">
%fa:check%
<span>%i18n:@connected%</span>
</p>

View file

@ -1,199 +0,0 @@
<template>
<div
class="mk-switch"
:class="{ disabled, checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="disabled"
@click="switchValue"
@mouseover="mouseenter"
>
<input
type="checkbox"
@change="handleChange"
ref="input"
:disabled="disabled"
@keydown.enter="switchValue"
>
<span class="button">
<span :style="{ transform }"></span>
</span>
<span class="label">
<span :aria-hidden="!checked">{{ text }}</span>
<p :aria-hidden="!checked">
<slot></slot>
</p>
</span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
text: String
},/*
created() {
if (!~[true, false].indexOf(this.value)) {
this.$emit('input', false);
}
},*/
computed: {
checked(): boolean {
return this.value;
},
transform(): string {
return this.checked ? 'translate3d(20px, 0, 0)' : '';
}
},
watch: {
value() {
(this.$el).style.transition = 'all 0.3s';
(this.$refs.input as any).checked = this.checked;
}
},
mounted() {
(this.$refs.input as any).checked = this.checked;
},
methods: {
mouseenter() {
(this.$el).style.transition = 'all 0s';
},
handleChange() {
(this.$el).style.transition = 'all 0.3s';
this.$emit('input', !this.checked);
this.$emit('change', !this.checked);
this.$nextTick(() => {
// set input's checked property
// in case parent refuses to change component's value
(this.$refs.input as any).checked = this.checked;
});
},
switchValue() {
!this.disabled && this.handleChange();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display flex
margin 12px 0
cursor pointer
transition all 0.3s
> *
user-select none
&.disabled
opacity 0.6
cursor not-allowed
&.checked
> .button
background-color $theme-color
border-color $theme-color
> .label
> span
color $theme-color
&:hover
> .label
> span
color darken($theme-color, 10%)
> .button
background darken($theme-color, 10%)
border-color darken($theme-color, 10%)
&:hover
> .label
> span
color isDark ? #fff : #2e3338
> .button
$color = isDark ? #15181d : #ced2da
background $color
border-color $color
> input
position absolute
width 0
height 0
opacity 0
margin 0
&:focus + .button
&: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 14px
> .button
$color = isDark ? #1c1f25 : #dcdfe6
display inline-block
margin 0
width 40px
min-width 40px
height 20px
min-height 20px
background $color
border 1px solid $color
outline none
border-radius 10px
transition inherit
> *
position absolute
top 1px
left 1px
border-radius 100%
transition transform 0.3s
width 16px
height 16px
background-color #fff
> .label
margin-left 8px
display block
font-size 15px
cursor pointer
transition inherit
> span
display block
line-height 20px
color isDark ? #c4ccd2 : #4a535a
transition inherit
> p
margin 0
//font-size 90%
color isDark ? #78858e : #9daab3
.mk-switch[data-darkmode]
root(true)
.mk-switch:not([data-darkmode])
root(false)
</style>

View file

@ -0,0 +1,84 @@
<template>
<div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
<div v-else>
<vue-word-cloud
:words="tags.slice(0, 20).map(x => [x.name, x.count])"
:color="color"
:spacing="1">
<template slot-scope="{word, text, weight}">
<div style="cursor: pointer;" :title="weight">
{{ text }}
</div>
</template>
</vue-word-cloud>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import * as VueWordCloud from 'vuewordcloud';
export default Vue.extend({
components: {
[VueWordCloud.name]: VueWordCloud
},
data() {
return {
tags: [],
fetching: true,
clock: null
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
fetch() {
(this as any).api('aggregation/hashtags').then(tags => {
this.tags = tags;
this.fetching = false;
});
},
color([, weight]) {
const peak = Math.max.apply(null, this.tags.map(x => x.count));
const w = weight / peak;
if (w > 0.9) {
return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69';
} else if (w > 0.5) {
return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7';
} else {
return this.$store.state.device.darkmode ? '#fff' : '#555';
}
}
}
});
</script>
<style lang="stylus" scoped>
.jtivnzhfwquxpsfidertopbmwmchmnmo
height 100%
width 100%
> .fetching
> .empty
margin 0
padding 16px
text-align center
color #aaa
> [data-fa]
margin-right 4px
> div
height 100%
width 100%
</style>

View file

@ -0,0 +1,308 @@
<template>
<div class="nicnklzforebnpfgasiypmpdaaglujqm">
<label>
<span>%i18n:@light-theme%</span>
<ui-select v-model="light" placeholder="%i18n:@light-theme%">
<optgroup label="%i18n:@light-themes%">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup label="%i18n:@dark-themes%">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</ui-select>
</label>
<label>
<span>%i18n:@dark-theme%</span>
<ui-select v-model="dark" placeholder="%i18n:@dark-theme%">
<optgroup label="%i18n:@dark-themes%">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup label="%i18n:@light-themes%">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</ui-select>
</label>
<details class="creator">
<summary>%fa:palette% %i18n:@create-a-theme%</summary>
<div>
<span>%i18n:@base-theme%:</span>
<ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio>
<ui-radio v-model="myThemeBase" value="dark">%i18n:@base-theme-dark%</ui-radio>
</div>
<div>
<ui-input v-model="myThemeName">
<span>%i18n:@theme-name%</span>
</ui-input>
<ui-textarea v-model="myThemeDesc">
<span>%i18n:@desc%</span>
</ui-textarea>
</div>
<div>
<div style="padding-bottom:8px;">%i18n:@primary-color%:</div>
<color-picker v-model="myThemePrimary"/>
</div>
<div>
<div style="padding-bottom:8px;">%i18n:@secondary-color%:</div>
<color-picker v-model="myThemeSecondary"/>
</div>
<div>
<div style="padding-bottom:8px;">%i18n:@text-color%:</div>
<color-picker v-model="myThemeText"/>
</div>
<ui-button @click="preview()">%fa:eye% %i18n:@preview-created-theme%</ui-button>
<ui-button primary @click="gen()">%fa:save R% %i18n:@save-created-theme%</ui-button>
</details>
<details>
<summary>%fa:download% %i18n:@install-a-theme%</summary>
<ui-button @click="import_()">%fa:file-import% %i18n:@import%</ui-button>
<input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
<p>%i18n:@import-by-code%:</p>
<ui-textarea v-model="installThemeCode">
<span>%i18n:@theme-code%</span>
</ui-textarea>
<ui-button @click="() => install(this.installThemeCode)">%fa:check% %i18n:@install%</ui-button>
</details>
<details>
<summary>%fa:folder-open% %i18n:@installed-themes%</summary>
<ui-select v-model="selectedInstalledThemeId" placeholder="%i18n:@select-theme%">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select>
<template v-if="selectedInstalledTheme">
<ui-input readonly :value="selectedInstalledTheme.author">
<span>%i18n:@author%</span>
</ui-input>
<ui-textarea v-if="selectedInstalledTheme.desc" readonly :value="selectedInstalledTheme.desc">
<span>%i18n:@desc%</span>
</ui-textarea>
<ui-textarea readonly :value="selectedInstalledThemeCode">
<span>%i18n:@theme-code%</span>
</ui-textarea>
<ui-button @click="export_()" link :download="`${selectedInstalledTheme.name}.misskeytheme`" ref="export">%fa:box% %i18n:@export%</ui-button>
<ui-button @click="uninstall()">%fa:trash-alt R% %i18n:@uninstall%</ui-button>
</template>
</details>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../theme';
import { Chrome } from 'vue-color';
import * as uuid from 'uuid';
import * as tinycolor from 'tinycolor2';
import * as JSON5 from 'json5';
//
function convertOldThemedefinition(t) {
const t2 = {
id: t.meta.id,
name: t.meta.name,
author: t.meta.author,
base: t.meta.base,
vars: t.meta.vars,
props: t
};
delete t2.props.meta;
return t2;
}
export default Vue.extend({
components: {
ColorPicker: Chrome
},
data() {
return {
installThemeCode: null,
selectedInstalledThemeId: null,
myThemeBase: 'light',
myThemeName: '',
myThemeDesc: '',
myThemePrimary: lightTheme.vars.primary,
myThemeSecondary: lightTheme.vars.secondary,
myThemeText: lightTheme.vars.text
};
},
computed: {
themes(): Theme[] {
return builtinThemes.concat(this.$store.state.device.themes);
},
darkThemes(): Theme[] {
return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
},
lightThemes(): Theme[] {
return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
},
installedThemes(): Theme[] {
return this.$store.state.device.themes;
},
light: {
get() { return this.$store.state.device.lightTheme; },
set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
},
dark: {
get() { return this.$store.state.device.darkTheme; },
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
},
selectedInstalledTheme() {
if (this.selectedInstalledThemeId == null) return null;
return this.installedThemes.find(x => x.id == this.selectedInstalledThemeId);
},
selectedInstalledThemeCode() {
if (this.selectedInstalledTheme == null) return null;
return JSON5.stringify(this.selectedInstalledTheme, null, '\t');
},
myTheme(): any {
return {
name: this.myThemeName,
author: this.$store.state.i.username,
desc: this.myThemeDesc,
base: this.myThemeBase,
vars: {
primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
}
};
}
},
watch: {
myThemeBase(v) {
const theme = v == 'light' ? lightTheme : darkTheme;
this.myThemePrimary = theme.vars.primary;
this.myThemeSecondary = theme.vars.secondary;
this.myThemeText = theme.vars.text;
}
},
beforeCreate() {
// migrate old theme definitions
//
this.$store.commit('device/set', {
key: 'themes', value: this.$store.state.device.themes.map(t => {
if (t.id == null) {
return convertOldThemedefinition(t);
} else {
return t;
}
})
});
},
methods: {
install(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
alert('%i18n:@invalid-theme%');
return;
}
//
if (theme.id == null && theme.meta != null) {
theme = convertOldThemedefinition(theme);
}
if (theme.id == null) {
alert('%i18n:@invalid-theme%');
return;
}
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
alert('%i18n:@already-installed%');
return;
}
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
alert('%i18n:@installed%'.replace('{}', theme.name));
},
uninstall() {
const theme = this.selectedInstalledTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
alert('%i18n:@uninstalled%'.replace('{}', theme.name));
},
import_() {
(this.$refs.file as any).click();
}
export_() {
const blob = new Blob([this.selectedInstalledThemeCode], {
type: 'application/json5'
});
this.$refs.export.$el.href = window.URL.createObjectURL(blob);
},
onUpdateImportFile() {
const f = (this.$refs.file as any).files[0];
const reader = new FileReader();
reader.onload = e => {
this.install(e.target.result);
};
reader.readAsText(f);
},
preview() {
applyTheme(this.myTheme, false);
},
gen() {
const theme = this.myTheme;
if (theme.name == null || theme.name.trim() == '') {
alert('%i18n:@theme-name-required%');
return;
}
theme.id = uuid();
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
alert('%i18n:@saved%');
}
}
});
</script>
<style lang="stylus" scoped>
.nicnklzforebnpfgasiypmpdaaglujqm
> details
border-top solid 1px var(--faceDivider)
> summary
padding 16px 0
> *:last-child
margin-bottom 16px
> .creator
> div
padding 16px 0
border-bottom solid 1px var(--faceDivider)
</style>

View file

@ -0,0 +1,98 @@
<template>
<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
<!-- トランジションを有効にするとなぜかメモリリークする -->
<transition-group v-else tag="div" name="chart">
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
</div>
</transition-group>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import XChart from './trends.chart.vue';
export default Vue.extend({
components: {
XChart
},
data() {
return {
stats: [],
fetching: true,
clock: null
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
fetch() {
(this as any).api('hashtags/trend').then(stats => {
this.stats = stats;
this.fetching = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
.csqvmxybqbycalfhkxvyfrgbrdalkaoc
> .fetching
> .empty
margin 0
padding 16px
text-align center
color var(--text)
opacity 0.7
> [data-fa]
margin-right 4px
> div
.chart-move
transition transform 1s ease
> div
display flex
align-items center
padding 14px 16px
&:not(:last-child)
border-bottom solid 1px var(--faceDivider)
> .tag
flex 1
overflow hidden
font-size 14px
color var(--text)
> a
display block
width 100%
white-space nowrap
overflow hidden
text-overflow ellipsis
color inherit
> p
margin 0
font-size 75%
opacity 0.7
> .chart
height 30px
</style>

View file

@ -1,9 +1,7 @@
<template>
<div class="ui-button" :class="[styl]">
<button :type="type" @click="$emit('click')">
<slot></slot>
</button>
</div>
<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr" :is="link ? 'a' : 'button'" :class="[styl, { inline, primary }]" :type="type" @click="$emit('click')">
<slot></slot>
</component>
</template>
<script lang="ts">
@ -13,70 +11,100 @@ export default Vue.extend({
type: {
type: String,
required: false
},
primary: {
type: Boolean,
required: false,
default: false
},
inline: {
type: Boolean,
required: false,
default: false
},
link: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
styl: 'fill'
};
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.dmtdnykelhudezerjlfpbhgovrgnqqgr
display block
width 100%
margin 0
padding 8px
text-align center
font-weight normal
font-size 16px
border none
border-radius 6px
outline none
box-shadow none
text-decoration none
user-select none
root(isDark, fill)
> button
display block
width 100%
margin 0
padding 0
*
pointer-events none
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 10px
&:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr
margin-top 16px
&.inline
display inline-block
width auto
&.primary
font-weight bold
font-size 16px
line-height 44px
border none
border-radius 6px
outline none
box-shadow none
if fill
color $theme-color-foreground
background $theme-color
&.fill
color var(--text)
background var(--buttonBg)
&:hover
background var(--buttonHoverBg)
&:active
background var(--buttonActiveBg)
&.primary
color var(--primaryForeground)
background var(--primary)
&:hover
background lighten($theme-color, 5%)
background var(--primaryLighten5)
&:active
background darken($theme-color, 5%)
else
color $theme-color
background none
background var(--primaryDarken5)
&:hover
color darken($theme-color, 5%)
&:active
background rgba($theme-color, 0.3)
.ui-button[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
color var(--primary)
background none
.ui-button:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
&:hover
color var(--primaryDarken5)
&:active
background var(--primaryAlpha03)
</style>

View file

@ -20,27 +20,33 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.ui-card
margin 16px
padding 16px
color isDark ? #fff : #000
background isDark ? #282C37 : #fff
color var(--faceText)
background var(--face)
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
@media (min-width 500px)
padding 32px
> header
font-weight normal
font-size 24px
color isDark ? #fff : #444
padding 16px
font-weight bold
font-size 20px
color var(--faceText)
.ui-card[data-darkmode]
root(true)
@media (min-width 500px)
padding 24px 32px
.ui-card:not([data-darkmode])
root(false)
> section
padding 20px 16px
border-top solid 1px var(--faceDivider)
@media (min-width 500px)
padding 32px
&.fit-top
padding-top 0
> header
margin-bottom 16px
font-weight bold
color var(--faceText)
</style>

View file

@ -19,7 +19,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.ui-form
> fieldset

View file

@ -25,9 +25,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
display inline-block
& + .nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
@ -38,11 +36,11 @@ root(isDark)
margin 0
padding 12px 20px
font-size 14px
border 1px solid isDark ? #6d727d : #dcdfe6
border 1px solid var(--formButtonBorder)
border-radius 4px
outline none
box-shadow none
color isDark ? #fff : #606266
color var(--text)
transition 0.1s
*
@ -50,40 +48,34 @@ root(isDark)
&:hover
&:focus
color $theme-color
background rgba($theme-color, isDark ? 0.2 : 0.12)
border-color rgba($theme-color, isDark ? 0.5 : 0.3)
color var(--primary)
background var(--formButtonHoverBg)
border-color var(--formButtonHoverBorder)
&:active
color darken($theme-color, 20%)
background rgba($theme-color, 0.12)
border-color $theme-color
color var(--primaryDarken20)
background var(--formButtonActiveBg)
border-color var(--primary)
transition all 0s
&.primary
> button
border 1px solid $theme-color
background $theme-color
color $theme-color-foreground
border 1px solid var(--primary)
background var(--primary)
color var(--primaryForeground)
&:hover
&:focus
background lighten($theme-color, 20%)
border-color lighten($theme-color, 20%)
background var(--primaryLighten20)
border-color var(--primaryLighten20)
&:active
background darken($theme-color, 20%)
border-color darken($theme-color, 20%)
background var(--primaryDarken20)
border-color var(--primaryDarken20)
transition all 0s
&.round
> button
border-radius 64px
.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg[data-darkmode]
root(true)
.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg:not([data-darkmode])
root(false)
</style>

View file

@ -49,9 +49,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.uywduthvrdnlpsvsjkqigicixgyfctto
display inline-flex
margin 0 16px 0 0
cursor pointer
@ -62,7 +60,7 @@ root(isDark)
&:hover
> .button
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
border solid 2px var(--inputLabel)
&.disabled
opacity 0.6
@ -70,15 +68,15 @@ root(isDark)
&.checked
> .button
border-color $theme-color
border-color var(--primary)
&:after
background-color $theme-color
background-color var(--primary)
transform scale(1)
opacity 1
> .label
color $theme-color
color var(--primary)
> input
position absolute
@ -93,7 +91,7 @@ root(isDark)
width 20px
height 20px
background none
border solid 2px isDark ? rgba(#fff, 0.6) : rgba(#000, 0.4)
border solid 2px var(--radioBorder)
border-radius 100%
transition inherit
@ -117,10 +115,4 @@ root(isDark)
line-height 20px
cursor pointer
.uywduthvrdnlpsvsjkqigicixgyfctto[data-darkmode]
root(true)
.uywduthvrdnlpsvsjkqigicixgyfctto:not([data-darkmode])
root(false)
</style>

View file

@ -71,14 +71,18 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
},
styl: {
type: String,
required: false,
default: 'line'
}
},
data() {
return {
v: this.value,
focused: false,
passwordStrength: '',
styl: 'fill'
passwordStrength: ''
};
},
computed: {
@ -117,14 +121,6 @@ export default Vue.extend({
}
}
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
@ -155,9 +151,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
root(fill)
margin 32px 0
> .icon
@ -167,7 +161,7 @@ root(isDark, fill)
width 24px
text-align center
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
color var(--inputLabel)
&:not(:empty) + .input
margin-left 28px
@ -183,7 +177,7 @@ root(isDark, fill)
left 0
right 0
height 1px
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
background var(--inputBorder)
&:after
content ''
@ -193,7 +187,7 @@ root(isDark, fill)
left 0
right 0
height 2px
background $theme-color
background var(--primary)
opacity 0
transform scaleX(0.12)
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
@ -242,7 +236,7 @@ root(isDark, fill)
transition-duration 0.3s
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
color var(--inputLabel)
pointer-events none
//will-change transform
transform-origin top left
@ -257,7 +251,7 @@ root(isDark, fill)
font-weight fill ? bold : normal
font-size 16px
line-height 32px
color isDark ? #fff : #000
color var(--inputText)
background transparent
border none
border-radius 0
@ -280,7 +274,7 @@ root(isDark, fill)
top 0
font-size 16px
line-height fill ? 44px : 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
color var(--inputLabel)
pointer-events none
&:empty
@ -325,7 +319,7 @@ root(isDark, fill)
transform scaleX(1)
> .label
color $theme-color
color var(--primary)
&.focused
&.filled
@ -335,16 +329,10 @@ root(isDark, fill)
left 0 !important
transform scale(0.75)
.ui-input[data-darkmode]
.ui-input
&.fill
root(true, true)
root(true)
&:not(.fill)
root(true, false)
.ui-input:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
root(false)
</style>

View file

@ -51,11 +51,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.ui-radio
display inline-block
margin 32px 32px 32px 0
margin 0 32px 0 0
cursor pointer
transition all 0.3s
@ -68,10 +66,10 @@ root(isDark)
&.checked
> .button
border-color $theme-color
border-color var(--primary)
&:after
background-color $theme-color
background-color var(--primary)
transform scale(1)
opacity 1
@ -87,7 +85,7 @@ root(isDark)
width 20px
height 20px
background none
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
border solid 2px var(--inputLabel)
border-radius 100%
transition inherit
@ -111,10 +109,4 @@ root(isDark)
line-height 20px
cursor pointer
.ui-radio[data-darkmode]
root(true)
.ui-radio:not([data-darkmode])
root(false)
</style>

View file

@ -29,13 +29,17 @@ export default Vue.extend({
required: {
type: Boolean,
required: false
},
styl: {
type: String,
required: false,
default: 'line'
}
},
data() {
return {
v: this.value,
focused: false,
styl: 'fill'
focused: false
};
},
computed: {
@ -48,14 +52,6 @@ export default Vue.extend({
this.v = v;
}
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
@ -70,9 +66,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
root(fill)
margin 32px 0
> .icon
@ -103,7 +97,7 @@ root(isDark, fill)
left 0
right 0
height 1px
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
background var(--inputBorder)
&:after
content ''
@ -113,7 +107,7 @@ root(isDark, fill)
left 0
right 0
height 2px
background $theme-color
background var(--primary)
opacity 0
transform scaleX(0.12)
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
@ -143,7 +137,7 @@ root(isDark, fill)
font-weight fill ? bold : normal
font-size 16px
height 32px
color isDark ? #fff : #000
color var(--inputText)
background transparent
border none
border-radius 0
@ -190,7 +184,7 @@ root(isDark, fill)
transform scaleX(1)
> .label
color $theme-color
color var(--primary)
&.focused
&.filled
@ -200,16 +194,10 @@ root(isDark, fill)
left 0 !important
transform scale(0.75)
.ui-select[data-darkmode]
.ui-select
&.fill
root(true, true)
root(true)
&:not(.fill)
root(true, false)
.ui-select:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
root(false)
</style>

View file

@ -19,7 +19,7 @@
<span class="label">
<span :aria-hidden="!checked"><slot></slot></span>
<p :aria-hidden="!checked">
<slot name="text"></slot>
<slot name="desc"></slot>
</p>
</span>
</div>
@ -56,14 +56,18 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.ui-switch
display flex
margin 32px 0
cursor pointer
transition all 0.3s
&:first-child
margin-top 0
&:last-child
margin-bottom 0
> *
user-select none
@ -73,11 +77,11 @@ root(isDark)
&.checked
> .button
background-color rgba($theme-color, 0.4)
border-color rgba($theme-color, 0.4)
background-color var(--primaryAlpha04)
border-color var(--primaryAlpha04)
> *
background-color $theme-color
background-color var(--primary)
transform translateX(14px)
> input
@ -89,10 +93,11 @@ root(isDark)
> .button
display inline-block
flex-shrink 0
margin 3px 0 0 0
width 34px
height 14px
background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
background var(--switchTrack)
outline none
border-radius 14px
transition inherit
@ -118,18 +123,11 @@ root(isDark)
> span
display block
line-height 20px
color isDark ? #c4ccd2 : rgba(#000, 0.75)
color currentColor
transition inherit
> p
margin 0
//font-size 90%
color isDark ? #78858e : #9daab3
.ui-switch[data-darkmode]
root(true)
.ui-switch:not([data-darkmode])
root(false)
opacity 0.7
</style>

View file

@ -63,9 +63,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
root(fill)
margin 42px 0 32px 0
> .input
@ -84,7 +82,7 @@ root(isDark, fill)
left 0
right 0
background none
border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
border solid 1px var(--inputBorder)
border-radius 3px
pointer-events none
@ -97,7 +95,7 @@ root(isDark, fill)
left 0
right 0
background none
border solid 2px $theme-color
border solid 2px var(--primary)
border-radius 3px
opacity 0
transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
@ -112,7 +110,7 @@ root(isDark, fill)
transition-duration 0.3s
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
color var(--inputLabel)
pointer-events none
//will-change transform
transform-origin top left
@ -126,7 +124,7 @@ root(isDark, fill)
font inherit
font-weight fill ? bold : normal
font-size 16px
color isDark ? #fff : #000
color var(--inputText)
background transparent
border none
border-radius 0
@ -149,7 +147,7 @@ root(isDark, fill)
opacity 1
> .label
color $theme-color
color var(--primary)
&.focused
&.filled
@ -159,16 +157,10 @@ root(isDark, fill)
left 0 !important
transform scale(0.75)
.ui-textarea[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-textarea.fill
root(true)
.ui-textarea:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
.ui-textarea:not(.fill)
root(false)
</style>

View file

@ -20,6 +20,7 @@
<script lang="ts">
import Vue from 'vue';
import { apiUrl } from '../../../config';
import getMD5 from '../../scripts/get-md5';
export default Vue.extend({
data() {
@ -28,61 +29,83 @@ export default Vue.extend({
};
},
methods: {
upload(file, folder) {
checkExistence(fileData: ArrayBuffer): Promise<any> {
return new Promise((resolve, reject) => {
const data = new FormData();
data.append('md5', getMD5(fileData));
(this as any).api('drive/files/check_existence', {
md5: getMD5(fileData)
}).then(resp => {
resolve(resp.file);
});
});
},
upload(file: File, folder: any) {
if (folder && typeof folder == 'object') folder = folder.id;
const id = Math.random();
const ctx = {
id: id,
name: file.name || 'untitled',
progress: undefined,
img: undefined
};
this.uploads.push(ctx);
this.$emit('change', this.uploads);
const reader = new FileReader();
reader.onload = (e: any) => {
ctx.img = e.target.result;
};
reader.readAsDataURL(file);
this.checkExistence(e.target.result).then(result => {
if (result !== null) {
this.$emit('uploaded', result);
return;
}
const data = new FormData();
data.append('i', this.$store.state.i.token);
data.append('file', file);
// Upload if the file didn't exist yet
const buf = new Uint8Array(e.target.result);
let bin = '';
// We use for-of loop instead of apply() to avoid RangeError
// SEE: https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
for (const byte of buf) bin += String.fromCharCode(byte);
const ctx = {
id: id,
name: file.name || 'untitled',
progress: undefined,
img: 'data:*/*;base64,' + btoa(bin)
};
if (folder) data.append('folderId', folder);
this.uploads.push(ctx);
this.$emit('change', this.uploads);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = (e: any) => {
const driveFile = JSON.parse(e.target.response);
const data = new FormData();
data.append('i', this.$store.state.i.token);
data.append('file', file);
this.$emit('uploaded', driveFile);
if (folder) data.append('folderId', folder);
this.uploads = this.uploads.filter(x => x.id != id);
this.$emit('change', this.uploads);
};
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = (e: any) => {
const driveFile = JSON.parse(e.target.response);
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
if (ctx.progress == undefined) ctx.progress = {};
ctx.progress.max = e.total;
ctx.progress.value = e.loaded;
}
};
this.$emit('uploaded', driveFile);
xhr.send(data);
this.uploads = this.uploads.filter(x => x.id != id);
this.$emit('change', this.uploads);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
if (ctx.progress == undefined) ctx.progress = {};
ctx.progress.max = e.total;
ctx.progress.value = e.loaded;
}
};
xhr.send(data);
})
}
reader.readAsArrayBuffer(file);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.mk-uploader
overflow auto
@ -100,7 +123,7 @@ export default Vue.extend({
margin 8px 0 0 0
padding 0
height 36px
box-shadow 0 -1px 0 rgba($theme-color, 0.1)
box-shadow 0 -1px 0 var(--primaryAlpha01)
border-top solid 8px transparent
&:first-child
@ -127,7 +150,7 @@ export default Vue.extend({
padding 0
max-width 256px
font-size 0.8em
color rgba($theme-color, 0.7)
color var(--primaryAlpha07)
white-space nowrap
text-overflow ellipsis
overflow hidden
@ -145,17 +168,17 @@ export default Vue.extend({
font-size 0.8em
> .initing
color rgba($theme-color, 0.5)
color var(--primaryAlpha05)
> .kb
color rgba($theme-color, 0.5)
color var(--primaryAlpha05)
> .percentage
display inline-block
width 48px
text-align right
color rgba($theme-color, 0.7)
color var(--primaryAlpha07)
&:after
content '%'
@ -174,10 +197,10 @@ export default Vue.extend({
overflow hidden
&::-webkit-progress-value
background $theme-color
background var(--primary)
&::-webkit-progress-bar
background rgba($theme-color, 0.1)
background var(--primaryAlpha01)
> .progress
display block
@ -191,13 +214,13 @@ export default Vue.extend({
border-radius 4px
background linear-gradient(
45deg,
lighten($theme-color, 30%) 25%,
$theme-color 25%,
$theme-color 50%,
lighten($theme-color, 30%) 50%,
lighten($theme-color, 30%) 75%,
$theme-color 75%,
$theme-color
var(--primaryLighten30) 25%,
var(--primary) 25%,
var(--primary) 50%,
var(--primaryLighten30) 50%,
var(--primaryLighten30) 75%,
var(--primary) 75%,
var(--primary)
)
background-size 32px 32px
animation bg 1.5s linear infinite

View file

@ -8,13 +8,13 @@
</blockquote>
</div>
<div v-else class="mk-url-preview">
<a :href="url" target="_blank" :title="url" v-if="!fetching">
<a :class="{ mini }" :href="url" target="_blank" :title="url" v-if="!fetching">
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
<article>
<header>
<h1>{{ title }}</h1>
</header>
<p>{{ description }}</p>
<p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer>
<img class="icon" v-if="icon" :src="icon"/>
<p>{{ sitename }}</p>
@ -118,6 +118,12 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
},
mini: {
type: Boolean,
required: false,
default: false
}
},
@ -164,7 +170,7 @@ export default Vue.extend({
return;
}
fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
fetch(`/url?url=${encodeURIComponent(this.url)}`).then(res => {
res.json().then(info => {
if (info.url == null) return;
this.title = info.title;
@ -194,17 +200,17 @@ export default Vue.extend({
top 0
width 100%
root(isDark)
.mk-url-preview
> a
display block
font-size 14px
border solid 1px isDark ? #191b1f : #eee
border solid 1px var(--urlPreviewBorder)
border-radius 4px
overflow hidden
&:hover
text-decoration none
border-color isDark ? #4f5561 : #ddd
border-color var(--urlPreviewBorderHover)
> article > header > h1
text-decoration underline
@ -229,11 +235,11 @@ root(isDark)
> h1
margin 0
font-size 1em
color isDark ? #d6dae0 : #555
color var(--urlPreviewTitle)
> p
margin 0
color isDark ? #a4aab3 : #777
color var(--urlPreviewText)
font-size 0.8em
> footer
@ -250,7 +256,7 @@ root(isDark)
> p
display inline-block
margin 0
color isDark ? #b0b4bf : #666
color var(--urlPreviewInfo)
font-size 0.8em
line-height 16px
vertical-align top
@ -293,10 +299,27 @@ root(isDark)
width 12px
height 12px
.mk-url-preview[data-darkmode]
root(true)
&.mini
font-size 10px
.mk-url-preview:not([data-darkmode])
root(false)
> .thumbnail
position relative
width 100%
height 60px
> article
left 0
width 100%
padding 8px
> header
margin-bottom 4px
> footer
margin-top 4px
> img
width 12px
height 12px
</style>

View file

@ -12,6 +12,7 @@
<script lang="ts">
import Vue from 'vue';
import { toUnicode as decodePunycode } from 'punycode';
export default Vue.extend({
props: ['url', 'target'],
data() {
@ -27,11 +28,11 @@ export default Vue.extend({
created() {
const url = new URL(this.url);
this.schema = url.protocol;
this.hostname = url.hostname;
this.hostname = decodePunycode(url.hostname);
this.port = url.port;
this.pathname = url.pathname;
this.query = url.search;
this.hash = url.hash;
this.pathname = decodeURIComponent(url.pathname);
this.query = decodeURIComponent(url.search);
this.hash = decodeURIComponent(url.hash);
}
});
</script>

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