Merge branch 'master' into l10n_master

This commit is contained in:
syuilo 2018-06-18 14:43:56 +09:00 committed by GitHub
commit 5d3943ffa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
478 changed files with 22462 additions and 5728 deletions

View File

@ -1,3 +1,9 @@
# インスタンス名
name:
# インスタンスの紹介
description:
# サーバーのメンテナ情報 # サーバーのメンテナ情報
maintainer: maintainer:
# メンテナの名前 # メンテナの名前
@ -55,3 +61,7 @@ twitter:
# インテグレーション用アプリのコンシューマーシークレット # インテグレーション用アプリのコンシューマーシークレット
consumer_secret: consumer_secret:
# true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。
# ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。
preventCache: false

1
.gitattributes vendored
View File

@ -1,3 +1,4 @@
*.svg -diff -text *.svg -diff -text
*.psd -diff -text *.psd -diff -text
*.ai -diff -text *.ai -diff -text
yarn.lock -diff -text

2
.gitignore vendored
View File

@ -11,4 +11,4 @@ npm-debug.log
run.bat run.bat
api-docs.json api-docs.json
package-lock.json package-lock.json
yarn.lock *.log

1
.npmrc
View File

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

28
CHANGELOG.md Normal file
View File

@ -0,0 +1,28 @@
ChangeLog
=========
破壊的変更のみ記載。
This document describes breaking changes only.
4.0.0
-----
オセロがリバーシに変更されました。
Othello is now Reversi.
### Migration
MongoDBの、`othelloGames`と`othelloMatchings`コレクションをそれぞれ`reversiGames`と`reversiMatchings`にリネームしてください。
You need to rename `othelloGames` and `othelloMatchings` MongoDB collections to `reversiGames` and `reversiMatchings`.
3.0.0
-----
### Migration
起動する前に、`node cli/recount-stats`してください。
Please run `node cli/recount-stats` before launch.

View File

@ -12,20 +12,24 @@
> Lead Maintainer: [syuilo][syuilo-link] > Lead Maintainer: [syuilo][syuilo-link]
**[Misskey](https://misskey.xyz)** is a completely open source, **[Misskey](https://misskey.xyz)** is a completely open source,
ultimately sophisticated new type of mini-blog based SNS. ultimately sophisticated professional microblogging software.
<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> <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>
![](https://c10.patreonusercontent.com/3/e30%3D/patreon-posts/RsKWEDEKf8D_wYDQWAbex9CSb-1DnXW1nfqfLvuys5ROj2k0VF6_luuzHMTyf95n.png?token-time=1529539200&token-hash=RmcSP0947mw5o2-B6g1L6aU_OoDXANe198kLU6HMO30%3D)
:sparkles: Features :sparkles: Features
---------------------------------------------------------------- ----------------------------------------------------------------
* Reactions * Reactions
* User lists * User lists
* Customizable column view (known as MisskeyDeck)
* and widgets!
* Private messages * Private messages
* Mute * Mute
* Real time contents * Streaming
* ActivityPub compatible * ActivityPub compatible
and more! You can touch with your own eyes at [misskey.xyz](https://misskey.xyz). and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz).
:package: Create your instance :package: Create your instance
---------------------------------------------------------------- ----------------------------------------------------------------
@ -45,18 +49,9 @@ If you want to...
[![Backers][backers-image]][support-url] [![Backers][backers-image]][support-url]
[![Sponsors][sponsors-image]][support-url] [![Sponsors][sponsors-image]][support-url]
:mortar_board: Notable contributors | ![][ooo-icon] |
---------------------------------------------------------------- |:-:|
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![tamaina][tamaina-icon] | ![rinsuki][rinsuki-icon] | | [ooo][ooo-link] |
|:-:|:-:|:-:|:-:|:-:|:-:|
| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [tamaina][tamaina-link] | [rinsuki][rinsuki-link] |
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
### :earth_americas: Translators
| ![][mirro-san-icon] | ![][Conan-kun-icon] | ![][m4sk1n-icon] |
|:-:|:-:|:-:|
| [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French | [Marcin Mikołajczak][m4sk1n-link]<br>Polish |
:four_leaf_clover: Copyright :four_leaf_clover: Copyright
---------------------------------------------------------------- ----------------------------------------------------------------
@ -84,23 +79,8 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
[sponsors-image]: https://opencollective.com/misskey/sponsors.svg [sponsors-image]: https://opencollective.com/misskey/sponsors.svg
[support-url]: https://opencollective.com/misskey#support [support-url]: https://opencollective.com/misskey#support
<!-- Contributors Info -->
[syuilo-link]: https://syuilo.com [syuilo-link]: https://syuilo.com
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
[ayamorisawa-link]: https://github.com/ayamorisawa
[ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
[otofune-link]: https://github.com/otofune
[otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
[akihikodaki-link]: https://github.com/akihikodaki
[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4
[rinsuki-link]: https://github.com/rinsuki
[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4
[tamaina-link]: https://github.com/tamaina
[tamaina-icon]: https://avatars1.githubusercontent.com/u/7973572?s=70&v=4
[mirro-san-link]: https://github.com/mirro-san [ooo-link]: https://www.patreon.com/user/creators?u=11601413
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4 [ooo-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D
[Conan-kun-link]: https://github.com/Conan-kun
[Conan-kun-icon]: https://avatars3.githubusercontent.com/u/30003708?s=70&v=4
[m4sk1n-link]: https://github.com/m4sk1n
[m4sk1n-icon]: https://avatars3.githubusercontent.com/u/21127288?s=70&v=4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/favicon/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

42
cli/recount-stats.js Normal file
View File

@ -0,0 +1,42 @@
const { default: Note } = require('../built/models/note');
const { default: Meta } = require('../built/models/meta');
const { default: User } = require('../built/models/user');
async function main() {
const meta = await Meta.findOne({});
const notesCount = await Note.count();
const usersCount = await User.count();
const originalNotesCount = await Note.count({
'_user.host': null
});
const originalUsersCount = await User.count({
host: null
});
const stats = {
notesCount,
usersCount,
originalNotesCount,
originalUsersCount
};
if (meta) {
await Meta.update({}, {
$set: {
stats
}
});
} else {
await Meta.insert({
stats
});
}
}
main().then(() => {
console.log('done');
}).catch(console.error);

View File

@ -3,16 +3,21 @@ const User = require('../built/models/user').default;
const args = process.argv.slice(2); const args = process.argv.slice(2);
const userId = new mongo.ObjectID(args[0]); const user = args[0];
console.log(`Suspending ${userId}...`); const q = user.startsWith('@') ? {
username: user.split('@')[1],
host: user.split('@')[2] || null
} : { _id: new mongo.ObjectID(user) };
User.update({ _id: userId }, { console.log(`Suspending ${user}...`);
User.update(q, {
$set: { $set: {
isSuspended: true isSuspended: true
} }
}).then(() => { }).then(() => {
console.log(`Suspended ${userId}`); console.log(`Suspended ${user}`);
}, e => { }, e => {
console.error(e); console.error(e);
}); });

12
cli/update-remote-user.js Normal file
View File

@ -0,0 +1,12 @@
const updatePerson = require('../built/remote/activitypub/models/person').updatePerson;
const args = process.argv.slice(2);
const user = args[0];
console.log(`Updating ${user}...`);
updatePerson(user).then(() => {
console.log(`Updated ${user}`);
}, e => {
console.error(e);
});

View File

@ -47,7 +47,14 @@ You need to generate config file via `npm run config` command.
*5.* Build Misskey *5.* Build Misskey
---------------------------------------------------------------- ----------------------------------------------------------------
We need to use `node-gyp` to build the `crypto` module.
Build misskey with the following:
`npm run build`
If you're on Debian, you will need to install the `build-essential` package.
If you're still encountering errors about some modules, use node-gyp:
1. `npm install -g node-gyp` 1. `npm install -g node-gyp`
2. `node-gyp configure` 2. `node-gyp configure`

View File

@ -8,12 +8,12 @@ import * as gutil from 'gulp-util';
import * as ts from 'gulp-typescript'; import * as ts from 'gulp-typescript';
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
import tslint from 'gulp-tslint'; import tslint from 'gulp-tslint';
import cssnano = require('gulp-cssnano'); const cssnano = require('gulp-cssnano');
import * as uglifyComposer from 'gulp-uglify/composer'; import * as uglifyComposer from 'gulp-uglify/composer';
import pug = require('gulp-pug'); import pug = require('gulp-pug');
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import chalk from 'chalk'; import chalk from 'chalk';
import imagemin = require('gulp-imagemin'); const imagemin = require('gulp-imagemin');
import * as rename from 'gulp-rename'; import * as rename from 'gulp-rename';
import * as mocha from 'gulp-mocha'; import * as mocha from 'gulp-mocha';
import * as replace from 'gulp-replace'; import * as replace from 'gulp-replace';

View File

@ -334,7 +334,7 @@ desktop/views/components/friends-maker.vue:
refresh: "Mehr" refresh: "Mehr"
close: "Schließen" close: "Schließen"
desktop/views/components/game-window.vue: desktop/views/components/game-window.vue:
game: "リバーシ" game: "Reversi"
desktop/views/components/home.vue: desktop/views/components/home.vue:
done: "Verbunden" done: "Verbunden"
add-widget: "Widget hinzufügen:" add-widget: "Widget hinzufügen:"

View File

@ -334,7 +334,7 @@ desktop/views/components/friends-maker.vue:
refresh: "More" refresh: "More"
close: "Close" close: "Close"
desktop/views/components/game-window.vue: desktop/views/components/game-window.vue:
game: "リバーシ" game: "Reversi"
desktop/views/components/home.vue: desktop/views/components/home.vue:
done: "Submit" done: "Submit"
add-widget: "Add widget:" add-widget: "Add widget:"
@ -550,7 +550,7 @@ desktop/views/components/ui.header.nav.vue:
home: "Home" home: "Home"
deck: "Deck" deck: "Deck"
messaging: "Messages" messaging: "Messages"
game: "Play Othello" game: "Play Reversi"
desktop/views/components/ui.header.notifications.vue: desktop/views/components/ui.header.notifications.vue:
title: "Notifications" title: "Notifications"
desktop/views/components/ui.header.post.vue: desktop/views/components/ui.header.post.vue:

View File

@ -334,7 +334,7 @@ desktop/views/components/friends-maker.vue:
refresh: "Plus" refresh: "Plus"
close: "Fermer" close: "Fermer"
desktop/views/components/game-window.vue: desktop/views/components/game-window.vue:
game: "リバーシ" game: "Reversi"
desktop/views/components/home.vue: desktop/views/components/home.vue:
done: "Envoyer" done: "Envoyer"
add-widget: "Ajouter un widget" add-widget: "Ajouter un widget"

View File

@ -5,12 +5,15 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
const loadLang = lang => yaml.safeLoad( export type LangKey = 'de' | 'en' | 'fr' | 'ja' | 'pl';
fs.readFileSync(`./locales/${lang}.yml`, 'utf-8')); export type LocaleObject = { [key: string]: any };
const loadLang = (lang: LangKey) => yaml.safeLoad(
fs.readFileSync(`./locales/${lang}.yml`, 'utf-8')) as LocaleObject;
const native = loadLang('ja'); const native = loadLang('ja');
const langs = { const langs: { [key: string]: LocaleObject } = {
'de': loadLang('de'), 'de': loadLang('de'),
'en': loadLang('en'), 'en': loadLang('en'),
'fr': loadLang('fr'), 'fr': loadLang('fr'),
@ -23,4 +26,8 @@ Object.entries(langs).map(([, locale]) => {
locale = Object.assign({}, native, locale); locale = Object.assign({}, native, locale);
}); });
export function isAvailableLanguage(lang: string): lang is LangKey {
return lang in langs;
}
export default langs; export default langs;

View File

@ -3,7 +3,9 @@ meta:
divider: "" divider: ""
common: common:
misskey: "Misskeyで皆と共有しよう。" misskey: "A ⭐ of fediverse"
about-title: "A ⭐ of fediverse."
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
time: time:
unknown: "なぞのじかん" unknown: "なぞのじかん"
@ -37,12 +39,62 @@ common:
confused: "こまこまのこまり" confused: "こまこまのこまり"
pudding: "Pudding" pudding: "Pudding"
note-placeholders:
a: "今どうしてる?"
b: "何かありましたか?"
c: "何をお考えですか?"
d: "言いたいことは?"
e: "ここに書いてください"
f: "あなたが書くのを待っています..."
delete: "削除" delete: "削除"
loading: "読み込み中" loading: "読み込み中"
ok: "わかった" ok: "わかった"
update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。" update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。"
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
widgets:
analog-clock: "アナログ時計"
profile: "プロフィール"
calendar: "カレンダー"
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
local: "ローカル"
global: "グローバル"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue: common/views/components/connect-failed.vue:
title: "サーバーに接続できません" title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -104,6 +156,8 @@ common/views/components/nav.vue:
common/views/components/note-menu.vue: common/views/components/note-menu.vue:
favorite: "お気に入り" favorite: "お気に入り"
pin: "ピン留め" pin: "ピン留め"
delete: "削除"
delete-confirm: "この投稿を削除しますか?"
remote: "投稿元で見る" remote: "投稿元で見る"
common/views/components/poll.vue: common/views/components/poll.vue:
@ -115,11 +169,11 @@ common/views/components/poll.vue:
voted: "投票済み" voted: "投票済み"
common/views/components/poll-editor.vue: common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です" no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}" choice-n: "選択肢{}"
remove: "この選択肢を削除" remove: "この選択肢を削除"
add: "+選択肢を追加" add: "+選択肢を追加"
destroy: "投票を破棄" destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択" choose-reaction: "リアクションを選択"
@ -197,10 +251,24 @@ common/views/widgets/photo-stream.vue:
title: "フォトストリーム" title: "フォトストリーム"
no-photos: "写真はありません" no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/server.vue: common/views/widgets/server.vue:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue: desktop/views/components/activity.chart.vue:
total: "Black ... Total" total: "Black ... Total"
notes: "Blue ... Notes" notes: "Blue ... Notes"
@ -291,8 +359,10 @@ desktop/views/components/drive.vue:
url-upload: "URLからアップロード" url-upload: "URLからアップロード"
desktop/views/components/follow-button.vue: desktop/views/components/follow-button.vue:
unfollow: "フォロー解除" following: "フォロー中"
follow: "フォローする" follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-request: "フォロー申請"
desktop/views/components/followers-window.vue: desktop/views/components/followers-window.vue:
followers: "{} のフォロワー" followers: "{} のフォロワー"
@ -314,30 +384,11 @@ desktop/views/components/friends-maker.vue:
close: "閉じる" close: "閉じる"
desktop/views/components/game-window.vue: desktop/views/components/game-window.vue:
game: "オセロ" game: "リバーシ"
desktop/views/components/home.vue: desktop/views/components/home.vue:
done: "完了" done: "完了"
add-widget: "ウィジェットを追加:" add-widget: "ウィジェットを追加:"
profile: "プロフィール"
calendar: "カレンダー"
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
trends: "トレンド"
photostream: "フォトストリーム"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
add: "追加" add: "追加"
desktop/views/input-dialog.vue: desktop/views/input-dialog.vue:
@ -352,21 +403,21 @@ desktop/views/components/messaging-window.vue:
desktop/views/components/note-detail.vue: desktop/views/components/note-detail.vue:
more: "会話をもっと読み込む" more: "会話をもっと読み込む"
private: "(この投稿は非公開です)" private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
location: "位置情報" location: "位置情報"
renote: "Renote" renote: "Renote"
add-reaction: "リアクション" add-reaction: "リアクション"
desktop/views/components/note-detail.sub.vue:
private: "(この投稿は非公開です)"
desktop/views/components/notes.note.vue: desktop/views/components/notes.note.vue:
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
reply: "返信" reply: "返信"
renote: "Renote" renote: "Renote"
add-reaction: "リアクション" add-reaction: "リアクション"
detail: "詳細" detail: "詳細"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/components/notes.vue: desktop/views/components/notes.vue:
error: "読み込みに失敗しました。" error: "読み込みに失敗しました。"
@ -377,10 +428,9 @@ desktop/views/components/notifications.vue:
empty: "ありません!" empty: "ありません!"
desktop/views/components/post-form.vue: desktop/views/components/post-form.vue:
note-placeholder: "いまどうしてる?"
reply-placeholder: "この投稿への返信..." reply-placeholder: "この投稿への返信..."
quote-placeholder: "この投稿を引用..." quote-placeholder: "この投稿を引用..."
note: "投稿" submit: "投稿"
reply: "返信" reply: "返信"
renote: "Renote" renote: "Renote"
posted: "投稿しました!" posted: "投稿しました!"
@ -394,7 +444,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し" attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v" insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成" create-poll: "アンケートを作成"
text-remain: "残り{}文字" text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue: desktop/views/components/post-form-window.vue:
@ -531,7 +581,7 @@ desktop/views/components/settings.api.vue:
token: "Token:" token: "Token:"
enter-password: "パスワードを入力してください" enter-password: "パスワードを入力してください"
desktop/views/components/settings.app.vue: desktop/views/components/settings.apps.vue:
no-apps: "連携しているアプリケーションはありません" no-apps: "連携しているアプリケーションはありません"
desktop/views/components/settings.mute.vue: desktop/views/components/settings.mute.vue:
@ -557,9 +607,10 @@ desktop/views/components/settings.profile.vue:
is-cat: "このアカウントはCatです" is-cat: "このアカウントはCatです"
desktop/views/components/sub-note-content.vue: desktop/views/components/sub-note-content.vue:
hidden: "(この投稿は非公開です)" private: "この投稿は非公開です"
media: "つのメディア" deleted: "この投稿は削除されました"
poll: "投票" media-count: "{}つのメディア"
poll: "アンケート"
desktop/views/components/taskmanager.vue: desktop/views/components/taskmanager.vue:
title: "タスクマネージャ" title: "タスクマネージャ"
@ -575,6 +626,7 @@ desktop/views/components/ui.header.account.vue:
drive: "ドライブ" drive: "ドライブ"
favorites: "お気に入り" favorites: "お気に入り"
lists: "リスト" lists: "リスト"
follow-requests: "フォロー申請"
customize: "カスタマイズ" customize: "カスタマイズ"
settings: "設定" settings: "設定"
signout: "サインアウト" signout: "サインアウト"
@ -582,6 +634,7 @@ desktop/views/components/ui.header.account.vue:
desktop/views/components/ui.header.nav.vue: desktop/views/components/ui.header.nav.vue:
home: "ホーム" home: "ホーム"
deck: "デッキ"
messaging: "メッセージ" messaging: "メッセージ"
game: "ゲーム" game: "ゲーム"
@ -594,7 +647,13 @@ desktop/views/components/ui.header.post.vue:
desktop/views/components/ui.header.search.vue: desktop/views/components/ui.header.search.vue:
placeholder: "検索" placeholder: "検索"
desktop/views/components/received-follow-requests-window.vue:
title: "フォロー申請"
accept: "承認"
reject: "拒否"
desktop/views/components/user-lists-window.vue: desktop/views/components/user-lists-window.vue:
title: "リスト"
create-list: "リストを作成" create-list: "リストを作成"
desktop/views/components/user-preview.vue: desktop/views/components/user-preview.vue:
@ -615,7 +674,18 @@ desktop/views/components/window.vue:
popout: "ポップアウト" popout: "ポップアウト"
close: "閉じる" close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue: desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
signin: "ログイン" signin: "ログイン"
signup: "新規登録" signup: "新規登録"
signin-button: "やってる" signin-button: "やってる"
@ -692,14 +762,13 @@ desktop/views/widgets/notifications.vue:
settings: "通知の設定" settings: "通知の設定"
desktop/views/widgets/polls.vue: desktop/views/widgets/polls.vue:
title: "投票" title: "アンケート"
refresh: "他を見る" refresh: "他を見る"
nothing: "ありません!" nothing: "ありません!"
desktop/views/widgets/post-form.vue: desktop/views/widgets/post-form.vue:
title: "投稿" title: "投稿"
note: "投稿" note: "投稿"
placeholder: "いまどうしてる?"
desktop/views/widgets/profile.vue: desktop/views/widgets/profile.vue:
update-banner: "クリックでバナー編集" update-banner: "クリックでバナー編集"
@ -724,6 +793,16 @@ mobile/views/components/drive.vue:
load-more: "もっと読み込む" load-more: "もっと読み込む"
nothing-in-drive: "ドライブには何もありません" nothing-in-drive: "ドライブには何もありません"
folder-is-empty: "このフォルダは空です" folder-is-empty: "このフォルダは空です"
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-detail.vue:
rename: "名前を変更"
mobile/views/components/drive-file-chooser.vue: mobile/views/components/drive-file-chooser.vue:
select-file: "ファイルを選択" select-file: "ファイルを選択"
@ -739,42 +818,86 @@ mobile/views/components/drive.file-detail.vue:
exif: "EXIF" exif: "EXIF"
mobile/views/components/follow-button.vue: mobile/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー" follow: "フォロー"
unfollow: "フォロー解除" request-pending: "フォロー許可待ち"
follow-request: "フォロー申請"
mobile/views/components/friends-maker.vue:
title: "気になるユーザーをフォロー"
empty: "おすすめのユーザーは見つかりませんでした。"
fetching: "読み込んでいます"
refresh: "もっと見る"
close: "閉じる"
mobile/views/components/note.vue: mobile/views/components/note.vue:
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
more: "もっと見る"
less: "隠す"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
location: "位置情報"
mobile/views/components/note-detail.vue: mobile/views/components/note-detail.vue:
reply: "返信" reply: "返信"
reaction: "リアクション" reaction: "リアクション"
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
location: "位置情報"
mobile/views/components/note-preview.vue:
admin: "admin"
bot: "bot"
cat: "cat"
mobile/views/components/note-sub.vue:
admin: "admin"
bot: "bot"
cat: "cat"
mobile/views/components/notes.vue:
failed: "読み込みに失敗しました。"
retry: "リトライ"
mobile/views/components/notifications.vue: mobile/views/components/notifications.vue:
more: "もっと見る" more: "もっと見る"
empty: "ありません!" empty: "ありません!"
mobile/views/components/post-form.vue: mobile/views/components/post-form.vue:
add-visible-user: "ユーザーを追加"
submit: "投稿" submit: "投稿"
reply: "返信" reply: "返信"
renote: "Renote" renote: "Renote"
renote-placeholder: "この投稿を引用... (オプション)" quote-placeholder: "この投稿を引用... (オプション)"
reply-placeholder: "この投稿への返信..." reply-placeholder: "この投稿への返信..."
note-placeholder: "いまどうしてる?" cw-placeholder: "内容への注釈 (オプション)"
location-alert: "お使いの端末は位置情報に対応していません"
error: "エラー"
username-prompt: "ユーザー名を入力してください"
mobile/views/components/sub-note-content.vue: mobile/views/components/sub-note-content.vue:
media-count: "{}個のメディア" private: "この投稿は非公開です"
poll: "投票" deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "アンケート"
mobile/views/components/timeline.vue: mobile/views/components/timeline.vue:
empty: "投稿がありません" empty: "投稿がありません"
load-more: "もっと" load-more: "もっと"
mobile/views/components/ui.nav.vue: mobile/views/components/ui.nav.vue:
home: "ホーム" timeline: "タイムライン"
notifications: "通知" notifications: "通知"
messaging: "メッセージ" messaging: "メッセージ"
follow-requests: "フォロー申請"
search: "検索" search: "検索"
drive: "ドライブ" drive: "ドライブ"
favorites: "お気に入り"
user-lists: "リスト"
widgets: "ウィジェット"
game: "ゲーム"
darkmode: "ダークモード"
settings: "設定" settings: "設定"
about: "Misskeyについて" about: "Misskeyについて"
@ -788,8 +911,16 @@ mobile/views/components/users-list.vue:
known: "知り合い" known: "知り合い"
load-more: "もっと" load-more: "もっと"
mobile/views/pages/favorites.vue:
title: "お気に入り"
mobile/views/pages/user-lists.vue:
title: "リスト"
enter-list-name: "リスト名を入力してください"
mobile/views/pages/drive.vue: mobile/views/pages/drive.vue:
drive: "ドライブ" drive: "ドライブ"
more: "もっと見る"
mobile/views/pages/followers.vue: mobile/views/pages/followers.vue:
followers-of: "{}のフォロワー" followers-of: "{}のフォロワー"
@ -808,6 +939,11 @@ mobile/views/pages/messaging.vue:
mobile/views/pages/messaging-room.vue: mobile/views/pages/messaging-room.vue:
messaging: "メッセージ" messaging: "メッセージ"
mobile/views/pages/received-follow-requests.vue:
title: "フォロー申請"
accept: "承認"
reject: "拒否"
mobile/views/pages/note.vue: mobile/views/pages/note.vue:
title: "投稿" title: "投稿"
prev: "前の投稿" prev: "前の投稿"

View File

@ -334,7 +334,7 @@ desktop/views/components/friends-maker.vue:
refresh: "Więcej" refresh: "Więcej"
close: "Zamknij" close: "Zamknij"
desktop/views/components/game-window.vue: desktop/views/components/game-window.vue:
game: "リバーシ" game: "Reversi"
desktop/views/components/home.vue: desktop/views/components/home.vue:
done: "Wyślij" done: "Wyślij"
add-widget: "Dodaj widżet:" add-widget: "Dodaj widżet:"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "2.17.0", "version": "4.1.1",
"clientVersion": "1.0.5731", "clientVersion": "1.0.6542",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@ -23,10 +23,10 @@
"format": "gulp format" "format": "gulp format"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome": "1.0.1", "@fortawesome/fontawesome": "1.1.8",
"@fortawesome/fontawesome-free-brands": "5.0.2", "@fortawesome/fontawesome-free-brands": "5.0.13",
"@fortawesome/fontawesome-free-regular": "5.0.2", "@fortawesome/fontawesome-free-regular": "5.0.13",
"@fortawesome/fontawesome-free-solid": "5.0.2", "@fortawesome/fontawesome-free-solid": "5.0.13",
"@koa/cors": "2.2.1", "@koa/cors": "2.2.1",
"@prezzemolo/rap": "0.1.2", "@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3", "@prezzemolo/zip": "0.0.3",
@ -34,7 +34,6 @@
"@types/debug": "0.0.30", "@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1", "@types/deep-equal": "1.0.1",
"@types/elasticsearch": "5.0.23", "@types/elasticsearch": "5.0.23",
"@types/eventemitter3": "2.0.2",
"@types/gm": "1.18.0", "@types/gm": "1.18.0",
"@types/gulp": "3.8.36", "@types/gulp": "3.8.36",
"@types/gulp-htmlmin": "1.3.32", "@types/gulp-htmlmin": "1.3.32",
@ -63,7 +62,6 @@
"@types/mkdirp": "0.5.2", "@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.0", "@types/mocha": "5.2.0",
"@types/mongodb": "3.0.18", "@types/mongodb": "3.0.18",
"@types/monk": "6.0.0",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.1.2", "@types/node": "10.1.2",
"@types/nopt": "3.0.29", "@types/nopt": "3.0.29",
@ -114,7 +112,7 @@
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
"gulp-htmlmin": "4.0.0", "gulp-htmlmin": "4.0.0",
"gulp-imagemin": "4.1.0", "gulp-imagemin": "4.1.0",
"gulp-mocha": "5.0.0", "gulp-mocha": "6.0.0",
"gulp-pug": "4.0.1", "gulp-pug": "4.0.1",
"gulp-rename": "1.2.3", "gulp-rename": "1.2.3",
"gulp-replace": "1.0.0", "gulp-replace": "1.0.0",
@ -124,17 +122,17 @@
"gulp-typescript": "4.0.2", "gulp-typescript": "4.0.2",
"gulp-uglify": "3.0.0", "gulp-uglify": "3.0.0",
"gulp-util": "3.0.8", "gulp-util": "3.0.8",
"hard-source-webpack-plugin": "0.6.9", "hard-source-webpack-plugin": "0.6.10",
"highlight.js": "9.12.0", "highlight.js": "9.12.0",
"html-minifier": "3.5.15", "html-minifier": "3.5.16",
"http-signature": "1.2.0", "http-signature": "1.2.0",
"inquirer": "5.2.0", "inquirer": "5.2.0",
"is-root": "2.0.0", "is-root": "2.0.0",
"is-url": "1.2.4", "is-url": "1.2.4",
"js-yaml": "3.11.0", "js-yaml": "3.11.0",
"jsdom": "11.10.0", "jsdom": "11.11.0",
"koa": "2.5.1", "koa": "2.5.1",
"koa-bodyparser": "4.2.0", "koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0", "koa-compress": "3.0.0",
"koa-favicon": "2.0.1", "koa-favicon": "2.0.1",
"koa-json-body": "5.3.0", "koa-json-body": "5.3.0",
@ -152,7 +150,7 @@
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"mocha": "5.2.0", "mocha": "5.2.0",
"moji": "0.5.1", "moji": "0.5.1",
"mongodb": "3.0.8", "mongodb": "3.0.10",
"monk": "6.0.6", "monk": "6.0.6",
"ms": "2.1.1", "ms": "2.1.1",
"nan": "2.10.0", "nan": "2.10.0",
@ -163,18 +161,18 @@
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0", "on-build-webpack": "0.1.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "4.0.0", "parse5": "5.0.0",
"progress-bar-webpack-plugin": "1.11.0", "progress-bar-webpack-plugin": "1.11.0",
"prominence": "0.2.0", "prominence": "0.2.0",
"promise-sequential": "1.1.1", "promise-sequential": "1.1.1",
"pug": "2.0.3", "pug": "2.0.3",
"punycode": "2.1.0", "punycode": "2.1.1",
"qrcode": "1.2.0", "qrcode": "1.2.0",
"ratelimiter": "3.0.3", "ratelimiter": "3.0.3",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2", "reconnecting-websocket": "3.2.2",
"redis": "2.8.0", "redis": "2.8.0",
"request": "2.86.0", "request": "2.87.0",
"request-promise-native": "1.0.5", "request-promise-native": "1.0.5",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"rndstr": "1.0.0", "rndstr": "1.0.0",
@ -193,7 +191,7 @@
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"tmp": "0.0.33", "tmp": "0.0.33",
"ts-loader": "4.3.0", "ts-loader": "4.3.0",
"ts-node": "6.0.3", "ts-node": "6.0.4",
"tslint": "5.10.0", "tslint": "5.10.0",
"typescript": "2.8.3", "typescript": "2.8.3",
"typescript-eslint-parser": "15.0.0", "typescript-eslint-parser": "15.0.0",
@ -205,8 +203,7 @@
"vue-cropperjs": "2.2.0", "vue-cropperjs": "2.2.0",
"vue-js-modal": "1.3.13", "vue-js-modal": "1.3.13",
"vue-json-tree-view": "2.1.4", "vue-json-tree-view": "2.1.4",
"vue-loader": "15.1.0", "vue-loader": "15.2.1",
"vue-material": "^1.0.0-beta-10.2",
"vue-router": "3.0.1", "vue-router": "3.0.1",
"vue-template-compiler": "2.5.16", "vue-template-compiler": "2.5.16",
"vuedraggable": "2.16.0", "vuedraggable": "2.16.0",
@ -214,10 +211,14 @@
"vuex-persistedstate": "^2.5.4", "vuex-persistedstate": "^2.5.4",
"web-push": "3.3.1", "web-push": "3.3.1",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.8.3", "webpack": "4.9.1",
"webpack-cli": "2.1.3", "webpack-cli": "2.1.4",
"websocket": "1.0.26", "websocket": "1.0.26",
"ws": "5.1.1", "ws": "5.2.0",
"xev": "2.0.0" "xev": "2.0.1"
},
"devDependencies": {
"@types/file-type": "5.2.1",
"@types/jsdom": "11.0.5"
} }
} }

View File

@ -1,4 +1,4 @@
export default acct => { export default (acct: string) => {
const splitted = acct.split('@', 2); const splitted = acct.split('@', 2);
return { username: splitted[0], host: splitted[1] || null }; return { username: splitted[0], host: splitted[1] || null };
}; };

View File

@ -1,3 +1,5 @@
export default user => { import { IUser } from '../models/user';
export default (user: IUser) => {
return user.host === null ? user.username : `${user.username}@${user.host}`; return user.host === null ? user.username : `${user.username}@${user.host}`;
}; };

View File

@ -3,18 +3,18 @@
*/ */
import * as fontawesome from '@fortawesome/fontawesome'; import * as fontawesome from '@fortawesome/fontawesome';
import * as regular from '@fortawesome/fontawesome-free-regular'; import regular from '@fortawesome/fontawesome-free-regular';
import * as solid from '@fortawesome/fontawesome-free-solid'; import solid from '@fortawesome/fontawesome-free-solid';
import * as brands from '@fortawesome/fontawesome-free-brands'; import brands from '@fortawesome/fontawesome-free-brands';
fontawesome.library.add(regular, solid, brands); fontawesome.library.add(regular, solid, brands);
export const pattern = /%fa:(.+?)%/g; export const pattern = /%fa:(.+?)%/g;
export const replacement = (match, key) => { export const replacement = (match: string, key: string) => {
const args = key.split(' '); const args = key.split(' ');
let prefix = 'fas'; let prefix = 'fas';
const classes = []; const classes: string[] = [];
let transform = ''; let transform = '';
let name; let name;
@ -34,12 +34,12 @@ export const replacement = (match, key) => {
} }
}); });
const icon = fontawesome.icon({ prefix, iconName: name }, { const icon = fontawesome.icon({ prefix, iconName: name } as fontawesome.IconLookup, {
classes: classes classes: classes,
transform: fontawesome.parse.transform(transform)
}); });
if (icon) { if (icon) {
icon.transform = fontawesome.parse.transform(transform);
return `<i data-fa class="${name}">${icon.html[0]}</i>`; return `<i data-fa class="${name}">${icon.html[0]}</i>`;
} else { } else {
console.warn(`'${name}' not found in fa`); console.warn(`'${name}' not found in fa`);

View File

@ -2,7 +2,7 @@
* Replace i18n texts * Replace i18n texts
*/ */
import locale from '../../locales'; import locale, { isAvailableLanguage, LocaleObject } from '../../locales';
export default class Replacer { export default class Replacer {
private lang: string; private lang: string;
@ -16,19 +16,19 @@ export default class Replacer {
this.replacement = this.replacement.bind(this); this.replacement = this.replacement.bind(this);
} }
private get(path: string, key: string) { private get(path: string, key: string): string {
const texts = locale[this.lang]; if (!isAvailableLanguage(this.lang)) {
if (texts == null) {
console.warn(`lang '${this.lang}' is not supported`); console.warn(`lang '${this.lang}' is not supported`);
return key; // Fallback return key; // Fallback
} }
const texts = locale[this.lang];
let text = texts; let text = texts;
if (path) { if (path) {
if (text.hasOwnProperty(path)) { if (text.hasOwnProperty(path)) {
text = text[path]; text = text[path] as LocaleObject;
} else { } else {
console.warn(`path '${path}' not found in '${this.lang}'`); console.warn(`path '${path}' not found in '${this.lang}'`);
return key; // Fallback return key; // Fallback
@ -38,7 +38,7 @@ export default class Replacer {
// Check the key existance // Check the key existance
const error = key.split('.').some(k => { const error = key.split('.').some(k => {
if (text.hasOwnProperty(k)) { if (text.hasOwnProperty(k)) {
text = text[k]; text = (text as LocaleObject)[k];
return false; return false;
} else { } else {
return true; return true;
@ -48,12 +48,15 @@ export default class Replacer {
if (error) { if (error) {
console.warn(`key '${key}' not found in '${path}' of '${this.lang}'`); console.warn(`key '${key}' not found in '${path}' of '${this.lang}'`);
return key; // Fallback return key; // Fallback
} else if (typeof text !== 'string') {
console.warn(`key '${key}' is not string in '${path}' of '${this.lang}'`);
return key; // Fallback
} else { } else {
return text; return text;
} }
} }
public replacement(match, key) { public replacement(match: string, key: string) {
let path = null; let path = null;
if (key.indexOf('|') != -1) { if (key.indexOf('|') != -1) {

View File

@ -1,8 +1,8 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { Query } from 'cafy'; import { Query } from 'cafy';
export const isAnId = x => mongo.ObjectID.isValid(x); export const isAnId = (x: any) => mongo.ObjectID.isValid(x);
export const isNotAnId = x => !isAnId(x); export const isNotAnId = (x: any) => !isAnId(x);
/** /**
* ID * ID

View File

@ -7,11 +7,6 @@ html
cursor progress !important cursor progress !important
body body
// for md
font-size 16px !important
line-height initial !important
letter-spacing initial !important
overflow-wrap break-word overflow-wrap break-word
#error #error

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 646 B

View File

@ -20,6 +20,7 @@ init(launch => {
// Init router // Init router
const router = new VueRouter({ const router = new VueRouter({
mode: 'history', mode: 'history',
base: '/auth/',
routes: [ routes: [
{ path: '/:token', component: Index }, { path: '/:token', component: Index },
] ]

View File

@ -1,8 +1,9 @@
<template> <template>
<div class="index"> <div class="index">
<main v-if="os.isSignedIn"> <main v-if="$store.getters.isSignedIn">
<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p> <p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
<x-form <x-form
class="form"
ref="form" ref="form"
v-if="state == 'waiting'" v-if="state == 'waiting'"
:session="session" :session="session"
@ -22,11 +23,11 @@
<p>セッションが存在しません</p> <p>セッションが存在しません</p>
</div> </div>
</main> </main>
<main class="signin" v-if="!os.isSignedIn"> <main class="signin" v-if="!$store.getters.isSignedIn">
<h1>サインインしてください</h1> <h1>サインインしてください</h1>
<mk-signin/> <mk-signin/>
</main> </main>
<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer> <footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer>
</div> </div>
</template> </template>
@ -51,7 +52,7 @@ export default Vue.extend({
} }
}, },
mounted() { mounted() {
if (!this.$root.$data.os.isSignedIn) return; if (!this.$store.getters.isSignedIn) return;
// Fetch session // Fetch session
(this as any).api('auth/session/show', { (this as any).api('auth/session/show', {
@ -62,7 +63,7 @@ export default Vue.extend({
// //
if (this.session.app.isAuthorized) { if (this.session.app.isAuthorized) {
this.$root.$data.os.api('auth/accept', { (this as any).api('auth/accept', {
token: this.session.token token: this.session.token
}).then(() => { }).then(() => {
this.accepted(); this.accepted();
@ -72,6 +73,7 @@ export default Vue.extend({
} }
}).catch(error => { }).catch(error => {
this.state = 'fetch-session-error'; this.state = 'fetch-session-error';
this.fetching = false;
}); });
}, },
methods: { methods: {
@ -101,7 +103,7 @@ export default Vue.extend({
padding 32px padding 32px
color #555 color #555
> div > div:not(.form)
padding 64px padding 64px
> h1 > h1
@ -142,8 +144,8 @@ export default Vue.extend({
> footer > footer
> img > img
display block display block
width 64px width 32px
height 64px height 32px
margin 0 auto margin 16px auto
</style> </style>

View File

@ -19,7 +19,7 @@ html
| Misskey | Misskey
block desc block desc
meta(name='description' content='A SNS') meta(name='description' content='A planet of fediverse')
block meta block meta
@ -42,7 +42,7 @@ html
| JavaScriptを有効にしてください | JavaScriptを有効にしてください
br br
| Please turn on your JavaScript | Please turn on your JavaScript
div#ini: p div#ini.
span . <svg viewBox="0 0 50 50">
span . <path fill=#{themeColor} d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
span . </svg>

View File

@ -32,9 +32,9 @@
//#region Detect app name //#region Detect app name
let app = null; let app = null;
if (url.pathname == '/docs') app = 'docs'; if (url.pathname == '/docs' || url.pathname.startsWith('/docs/')) app = 'docs';
if (url.pathname == '/dev') app = 'dev'; if (url.pathname == '/dev' || url.pathname.startsWith('/dev/')) app = 'dev';
if (url.pathname == '/auth') app = 'auth'; if (url.pathname == '/auth' || url.pathname.startsWith('/auth/')) app = 'auth';
//#endregion //#endregion
//#region Detect the user language //#region Detect the user language

View File

@ -9,9 +9,9 @@ export default function<T extends object>(data: {
widget: { widget: {
type: Object type: Object
}, },
isMobile: { platform: {
type: Boolean, type: String,
default: false required: true
}, },
isCustomizeMode: { isCustomizeMode: {
type: Boolean, type: Boolean,
@ -66,17 +66,10 @@ export default function<T extends object>(data: {
this.bakeProps(); this.bakeProps();
if (this.isMobile) { (this as any).api('i/update_widget', {
(this as any).api('i/update_mobile_home', { id: this.id,
id: this.id, data: this.props
data: this.props });
});
} else {
(this as any).api('i/update_home', {
id: this.id,
data: this.props
});
}
} }
} }
}); });

View File

@ -55,7 +55,7 @@ export default function(type, data): Notification {
icon: data.user.avatarUrl + '?thumbnail&size=64' icon: data.user.avatarUrl + '?thumbnail&size=64'
}; };
case 'othello_invited': case 'reversi_invited':
return { return {
title: '対局への招待があります', title: '対局への招待があります',
body: `${getUserName(data.parent)}さんから`, body: `${getUserName(data.parent)}さんから`,

View File

@ -1,5 +1,3 @@
import * as merge from 'object-assign-deep';
import Stream from './stream'; import Stream from './stream';
import StreamManager from './stream-manager'; import StreamManager from './stream-manager';
import MiOS from '../../../mios'; import MiOS from '../../../mios';
@ -20,14 +18,36 @@ export class HomeStream extends Stream {
}, 1000 * 60); }, 1000 * 60);
// 自分の情報が更新されたとき // 自分の情報が更新されたとき
this.on('i_updated', i => { this.on('meUpdated', i => {
if (os.debug) { if (os.debug) {
console.log('I updated:', i); console.log('I updated:', i);
} }
merge(me, i);
// キャッシュ更新 os.store.dispatch('mergeMe', i);
os.bakeMe(); });
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 => { this.on('clientSettingUpdated', x => {
@ -38,25 +58,18 @@ export class HomeStream extends Stream {
}); });
this.on('home_updated', x => { this.on('home_updated', x => {
if (x.home) { os.store.commit('settings/setHome', x);
os.store.commit('settings/setHome', x.home);
} else {
os.store.commit('settings/setHomeWidget', {
id: x.id,
data: x.data
});
}
}); });
this.on('mobile_home_updated', x => { this.on('mobile_home_updated', x => {
if (x.home) { os.store.commit('settings/setMobileHome', x);
os.store.commit('settings/setMobileHome', x.home); });
} else {
os.store.commit('settings/setMobileHomeWidget', { this.on('widgetUpdated', x => {
id: x.id, os.store.commit('settings/setWidget', {
data: x.data id: x.id,
}); data: x.data
} });
}); });
// トークンが再生成されたとき // トークンが再生成されたとき

View File

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

View File

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

View File

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

View File

@ -0,0 +1,30 @@
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

@ -0,0 +1,127 @@
<template>
<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none">
<circle v-for="angle, i in graduations"
:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
:r="i % 5 == 0 ? 0.125 : 0.05"
:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/>
<line
:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
:x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
:y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
:stroke="sHandColor"
stroke-width="0.05"/>
<line
:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
:stroke="mHandColor"
stroke-width="0.1"/>
<line
:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
:stroke="hHandColor"
stroke-width="0.1"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
import { themeColor } from '../../../config';
export default Vue.extend({
props: {
dark: {
type: Boolean,
default: false
}
},
data() {
return {
now: new Date(),
clock: null,
graduationsPadding: 0.5,
handsPadding: 1,
handsTailLength: 0.7,
hHandLengthRatio: 0.75,
mHandLengthRatio: 1,
sHandLengthRatio: 1
};
},
computed: {
majorGraduationColor(): string {
return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
},
minorGraduationColor(): string {
return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
},
sHandColor(): string {
return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
},
mHandColor(): string {
return this.dark ? '#fff' : '#777';
},
hHandColor(): string {
return themeColor;
},
s(): number {
return this.now.getSeconds();
},
m(): number {
return this.now.getMinutes();
},
h(): number {
return this.now.getHours();
},
hAngle(): number {
return Math.PI * (this.h % 12 + this.m / 60) / 6;
},
mAngle(): number {
return Math.PI * (this.m + this.s / 60) / 30;
},
sAngle(): number {
return Math.PI * this.s / 30;
},
graduations(): any {
const angles = [];
for (let i = 0; i < 60; i++) {
const angle = Math.PI * i / 30;
angles.push(angle);
}
return angles;
}
},
mounted() {
this.clock = setInterval(this.tick, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
tick() {
this.now = new Date();
}
}
});
</script>
<style lang="stylus" scoped>
.mk-analog-clock
display block
</style>

View File

@ -32,7 +32,7 @@ export default Vue.extend({
? `rgb(${ this.user.avatarColor.join(',') })` ? `rgb(${ this.user.avatarColor.join(',') })`
: null, : null,
backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`, backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`,
borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
}; };
} }
} }

View File

@ -13,9 +13,6 @@
.a .a
display block display block
position absolute
top 0
right 0
> svg > svg
display block display block

View File

@ -1,5 +1,8 @@
import Vue from 'vue'; import Vue from 'vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
import signin from './signin.vue'; import signin from './signin.vue';
import signup from './signup.vue'; import signup from './signup.vue';
import forkit from './forkit.vue'; import forkit from './forkit.vue';
@ -24,9 +27,20 @@ import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue'; import twitterSetting from './twitter-setting.vue';
import fileTypeIcon from './file-type-icon.vue'; import fileTypeIcon from './file-type-icon.vue';
import Switch from './switch.vue'; import Switch from './switch.vue';
import Othello from './othello.vue'; import Reversi from './reversi.vue';
import welcomeTimeline from './welcome-timeline.vue'; import welcomeTimeline from './welcome-timeline.vue';
import uiInput from './ui/input.vue';
import uiButton from './ui/button.vue';
import uiCard from './ui/card.vue';
import uiForm from './ui/form.vue';
import uiTextarea from './ui/textarea.vue';
import uiSwitch from './ui/switch.vue';
import uiRadio from './ui/radio.vue';
import uiSelect from './ui/select.vue';
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
Vue.component('mk-signin', signin); Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup); Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit); Vue.component('mk-forkit', forkit);
@ -51,5 +65,13 @@ Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting); Vue.component('mk-twitter-setting', twitterSetting);
Vue.component('mk-file-type-icon', fileTypeIcon); Vue.component('mk-file-type-icon', fileTypeIcon);
Vue.component('mk-switch', Switch); Vue.component('mk-switch', Switch);
Vue.component('mk-othello', Othello); Vue.component('mk-reversi', Reversi);
Vue.component('mk-welcome-timeline', welcomeTimeline); Vue.component('mk-welcome-timeline', welcomeTimeline);
Vue.component('ui-input', uiInput);
Vue.component('ui-button', uiButton);
Vue.component('ui-card', uiCard);
Vue.component('ui-form', uiForm);
Vue.component('ui-textarea', uiTextarea);
Vue.component('ui-switch', uiSwitch);
Vue.component('ui-radio', uiRadio);
Vue.component('ui-select', uiSelect);

View File

@ -1,9 +1,11 @@
<template> <template>
<div class="mk-media-list" :data-count="mediaList.length"> <div class="mk-media-list">
<template v-for="media in mediaList"> <div :data-count="mediaList.length" ref="grid">
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/> <template v-for="media in mediaList">
<mk-media-image :image="media" :key="media.id" v-else :raw="raw"/> <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
</template> <mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
</template>
</div>
</div> </div>
</template> </template>
@ -18,47 +20,60 @@ export default Vue.extend({
raw: { raw: {
default: false default: false
} }
},
mounted() {
// for Safari bug
this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
} }
}); });
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-media-list .mk-media-list
display grid width 100%
grid-gap 4px
height 256px
@media (max-width 500px) &:before
height 192px 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
&[data-count="1"]
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
: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
&[data-count="1"]
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
:nth-child(1) :nth-child(1)
grid-row 1 / 3 grid-column 1 / 2
:nth-child(3) grid-row 1 / 2
:nth-child(2)
grid-column 2 / 3 grid-column 2 / 3
grid-row 2/3 grid-row 1 / 2
&[data-count="4"] :nth-child(3)
grid-template-columns 1fr 1fr grid-column 1 / 2
grid-template-rows 1fr 1fr grid-row 2 / 3
:nth-child(4)
:nth-child(1) grid-column 2 / 3
grid-column 1 / 2 grid-row 2 / 3
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> </style>

View File

@ -0,0 +1,196 @@
<template>
<div class="mk-menu">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item 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>
</template>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import * as anime from 'animejs';
export default Vue.extend({
props: {
source: {
required: true
},
items: {
type: Array,
required: true
},
compact: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
hukidasi: !this.compact
};
},
mounted() {
this.$nextTick(() => {
const popover = this.$refs.popover as any;
const rect = this.source.getBoundingClientRect();
const width = popover.offsetWidth;
const height = popover.offsetHeight;
let left;
let top;
if (this.compact) {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
left = (x - (width / 2));
top = (y - (height / 2));
} else {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
left = (x - (width / 2));
top = y;
}
if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset;
this.hukidasi = false;
}
if (top + height - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - height + window.pageYOffset;
this.hukidasi = false;
}
popover.style.left = left + 'px';
popover.style.top = top + 'px';
anime({
targets: this.$refs.backdrop,
opacity: 1,
duration: 100,
easing: 'linear'
});
anime({
targets: this.$refs.popover,
opacity: 1,
scale: [0.5, 1],
duration: 500
});
});
},
methods: {
clicked(fn) {
fn();
this.close();
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
targets: this.$refs.backdrop,
opacity: 0,
duration: 200,
easing: 'linear'
});
(this.$refs.popover as any).style.pointerEvents = 'none';
anime({
targets: this.$refs.popover,
opacity: 0,
scale: 0.5,
duration: 200,
easing: 'easeInBack',
complete: () => {
this.$emit('closed');
this.$destroy();
}
});
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
$border-color = rgba(27, 31, 35, 0.15)
.mk-menu
position initial
> .backdrop
position fixed
top 0
left 0
z-index 10000
width 100%
height 100%
background rgba(#000, 0.1)
opacity 0
> .popover
position absolute
z-index 10001
padding 8px 0
background #fff
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
transform scale(0.5)
opacity 0
$balloon-size = 16px
&.hukidasi
margin-top $balloon-size
transform-origin center -($balloon-size)
&:before
&:after
content ""
display block
position absolute
pointer-events none
&:before
top -($balloon-size * 2)
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size $border-color
&:after
top -($balloon-size * 2) + 1.5px
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size #fff
> button
display block
padding 8px 16px
width 100%
&:hover
color $theme-color-foreground
background $theme-color
text-decoration none
&:active
color $theme-color-foreground
background darken($theme-color, 10%)
> div
margin 8px 0
height 1px
background #eee
</style>

View File

@ -8,7 +8,7 @@
<img src="/assets/desktop/messaging/delete.png" alt="Delete"/> <img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
</button> </button>
<div class="content" v-if="!message.isDeleted"> <div class="content" v-if="!message.isDeleted">
<mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/> <mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<div class="file" v-if="message.file"> <div class="file" v-if="message.file">
<a :href="message.file.url" target="_blank" :title="message.file.name"> <a :href="message.file.url" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@ -42,7 +42,7 @@ export default Vue.extend({
}, },
computed: { computed: {
isMe(): boolean { isMe(): boolean {
return this.message.userId == (this as any).os.i.id; return this.message.userId == this.$store.state.i.id;
}, },
urls(): string[] { urls(): string[] {
if (this.message.text) { if (this.message.text) {

View File

@ -72,7 +72,7 @@ export default Vue.extend({
}, },
mounted() { mounted() {
this.connection = new MessagingStream((this as any).os, (this as any).os.i, this.user.id); this.connection = new MessagingStream((this as any).os, this.$store.state.i, this.user.id);
this.connection.on('message', this.onMessage); this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead); this.connection.on('read', this.onRead);
@ -164,7 +164,7 @@ export default Vue.extend({
const isBottom = this.isBottom(); const isBottom = this.isBottom();
this.messages.push(message); this.messages.push(message);
if (message.userId != (this as any).os.i.id && !document.hidden) { if (message.userId != this.$store.state.i.id && !document.hidden) {
this.connection.send({ this.connection.send({
type: 'read', type: 'read',
id: message.id id: message.id
@ -176,7 +176,7 @@ export default Vue.extend({
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToBottom(); this.scrollToBottom();
}); });
} else if (message.userId != (this as any).os.i.id) { } else if (message.userId != this.$store.state.i.id) {
// Notify // Notify
this.notifyNewMessage(); this.notifyNewMessage();
} }
@ -229,7 +229,7 @@ export default Vue.extend({
onVisibilitychange() { onVisibilitychange() {
if (document.hidden) return; if (document.hidden) return;
this.messages.forEach(message => { this.messages.forEach(message => {
if (message.userId !== (this as any).os.i.id && !message.isRead) { if (message.userId !== this.$store.state.i.id && !message.isRead) {
this.connection.send({ this.connection.send({
type: 'read', type: 'read',
id: message.id id: message.id

View File

@ -95,7 +95,7 @@ export default Vue.extend({
methods: { methods: {
getAcct, getAcct,
isMe(message) { isMe(message) {
return message.userId == (this as any).os.i.id; return message.userId == this.$store.state.i.id;
}, },
onMessage(message) { onMessage(message) {
this.messages = this.messages.filter(m => !( this.messages = this.messages.filter(m => !(

View File

@ -0,0 +1,117 @@
<template>
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
<span class="is-admin" v-if="note.user.isAdmin">admin</span>
<span class="is-bot" v-if="note.user.isBot">bot</span>
<span class="is-cat" v-if="note.user.isCat">cat</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="app" v-if="note.app && !mini">via <b>{{ note.app.name }}</b></span>
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
note: {
type: Object,
required: true
},
mini: {
type: Boolean,
required: false,
default: false
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display flex
align-items baseline
white-space nowrap
> .avatar
flex-shrink 0
margin-right 8px
width 20px
height 20px
border-radius 100%
> .name
display block
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
font-size 1em
font-weight bold
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 .5em 0 0
padding 1px 6px
font-size 80%
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #c0c0c0
> .mobile
margin-right 8px
> .app
margin-right 8px
padding-right 8px
border-right solid 1px isDark ? #1c2023 : #eaeaea
> .visibility
margin-left 8px
.bvonvjxbwzaiskogyhbwgyxvcgserpmu[data-darkmode]
root(true)
.bvonvjxbwzaiskogyhbwgyxvcgserpmu:not([data-darkmode])
root(false)
</style>

View File

@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast; ast = this.ast;
} }
if (ast.filter(x => x.type != 'hashtag').length == 0) {
return;
}
while (ast[ast.length - 1] && (
ast[ast.length - 1].type == 'hashtag' ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
ast.pop();
}
// Parse ast to DOM // Parse ast to DOM
const els = flatten(ast.map(token => { const els = flatten(ast.map(token => {
switch (token.type) { switch (token.type) {
@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag': case 'hashtag':
return createElement('a', { return createElement('a', {
attrs: { attrs: {
href: `${url}/search?q=${token.content}`, href: `${url}/tags/${token.hashtag}`,
target: '_blank' target: '_blank'
} }
}, token.content); }, token.content);

View File

@ -1,54 +1,45 @@
<template> <template>
<div class="mk-note-menu"> <div style="position:initial">
<div class="backdrop" ref="backdrop" @click="close"></div> <mk-menu :source="source" :compact="compact" :items="items" @closed="closed"/>
<div class="popover" :class="{ compact }" ref="popover">
<button @click="favorite">%i18n:@favorite%</button>
<button v-if="note.userId == os.i.id" @click="pin">%i18n:@pin%</button>
<a v-if="note.uri" :href="note.uri" target="_blank">%i18n:@remote%</a>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as anime from 'animejs';
export default Vue.extend({ export default Vue.extend({
props: ['note', 'source', 'compact'], props: ['note', 'source', 'compact'],
mounted() { computed: {
this.$nextTick(() => { items() {
const popover = this.$refs.popover as any; const items = [];
items.push({
const rect = this.source.getBoundingClientRect(); icon: '%fa:star%',
const width = popover.offsetWidth; text: '%i18n:@favorite%',
const height = popover.offsetHeight; action: this.favorite
});
if (this.compact) { if (this.note.userId == this.$store.state.i.id) {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); items.push({
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); icon: '%fa:thumbtack%',
popover.style.left = (x - (width / 2)) + 'px'; text: '%i18n:@pin%',
popover.style.top = (y - (height / 2)) + 'px'; action: this.pin
} else { });
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); items.push({
const y = rect.top + window.pageYOffset + this.source.offsetHeight; icon: '%fa:trash-alt R%',
popover.style.left = (x - (width / 2)) + 'px'; text: '%i18n:@delete%',
popover.style.top = y + 'px'; action: this.del
});
} }
if (this.note.uri) {
anime({ items.push({
targets: this.$refs.backdrop, icon: '%fa:external-link-square-alt%',
opacity: 1, text: '%i18n:@remote%',
duration: 100, action: () => {
easing: 'linear' window.open(this.note.uri, '_blank');
}); }
});
anime({ }
targets: this.$refs.popover, return items;
opacity: 1, }
scale: [0.5, 1],
duration: 500
});
});
}, },
methods: { methods: {
pin() { pin() {
@ -59,6 +50,15 @@ export default Vue.extend({
}); });
}, },
del() {
if (!window.confirm('%i18n:@delete-confirm%')) return;
(this as any).api('notes/delete', {
noteId: this.note.id
}).then(() => {
this.$destroy();
});
},
favorite() { favorite() {
(this as any).api('notes/favorites/create', { (this as any).api('notes/favorites/create', {
noteId: this.note.id noteId: this.note.id
@ -67,99 +67,11 @@ export default Vue.extend({
}); });
}, },
close() { closed() {
(this.$refs.backdrop as any).style.pointerEvents = 'none'; this.$nextTick(() => {
anime({ this.$destroy();
targets: this.$refs.backdrop,
opacity: 0,
duration: 200,
easing: 'linear'
});
(this.$refs.popover as any).style.pointerEvents = 'none';
anime({
targets: this.$refs.popover,
opacity: 0,
scale: 0.5,
duration: 200,
easing: 'easeInBack',
complete: () => this.$destroy()
}); });
} }
} }
}); });
</script> </script>
<style lang="stylus" scoped>
@import '~const.styl'
$border-color = rgba(27, 31, 35, 0.15)
.mk-note-menu
position initial
> .backdrop
position fixed
top 0
left 0
z-index 10000
width 100%
height 100%
background rgba(#000, 0.1)
opacity 0
> .popover
position absolute
z-index 10001
padding 8px 0
background #fff
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
transform scale(0.5)
opacity 0
$balloon-size = 16px
&:not(.compact)
margin-top $balloon-size
transform-origin center -($balloon-size)
&:before
content ""
display block
position absolute
top -($balloon-size * 2)
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size $border-color
&:after
content ""
display block
position absolute
top -($balloon-size * 2) + 1.5px
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
border-bottom solid $balloon-size #fff
> button
> a
display block
padding 8px 16px
width 100%
&:hover
color $theme-color-foreground
background $theme-color
text-decoration none
&:active
color $theme-color-foreground
background darken($theme-color, 10%)
</style>

View File

@ -43,7 +43,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as CRC32 from 'crc-32'; import * as CRC32 from 'crc-32';
import Othello, { Color } from '../../../../../othello/core'; import Reversi, { Color } from '../../../../../reversi/core';
import { url } from '../../../config'; import { url } from '../../../config';
export default Vue.extend({ export default Vue.extend({
@ -52,7 +52,7 @@ export default Vue.extend({
data() { data() {
return { return {
game: null, game: null,
o: null as Othello, o: null as Reversi,
logs: [], logs: [],
logPos: 0, logPos: 0,
pollingClock: null pollingClock: null
@ -61,13 +61,13 @@ export default Vue.extend({
computed: { computed: {
iAmPlayer(): boolean { iAmPlayer(): boolean {
if (!(this as any).os.isSignedIn) return false; if (!this.$store.getters.isSignedIn) return false;
return this.game.user1Id == (this as any).os.i.id || this.game.user2Id == (this as any).os.i.id; return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id;
}, },
myColor(): Color { myColor(): Color {
if (!this.iAmPlayer) return null; if (!this.iAmPlayer) return null;
if (this.game.user1Id == (this as any).os.i.id && this.game.black == 1) return true; if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true;
if (this.game.user2Id == (this as any).os.i.id && this.game.black == 2) return true; if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true;
return false; return false;
}, },
opColor(): Color { opColor(): Color {
@ -91,14 +91,14 @@ export default Vue.extend({
}, },
isMyTurn(): boolean { isMyTurn(): boolean {
if (this.turnUser == null) return null; if (this.turnUser == null) return null;
return this.turnUser.id == (this as any).os.i.id; return this.turnUser.id == this.$store.state.i.id;
} }
}, },
watch: { watch: {
logPos(v) { logPos(v) {
if (!this.game.isEnded) return; if (!this.game.isEnded) return;
this.o = new Othello(this.game.settings.map, { this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo, isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere, canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard loopedBoard: this.game.settings.loopedBoard
@ -115,7 +115,7 @@ export default Vue.extend({
created() { created() {
this.game = this.initGame; this.game = this.initGame;
this.o = new Othello(this.game.settings.map, { this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo, isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere, canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard loopedBoard: this.game.settings.loopedBoard
@ -163,7 +163,7 @@ export default Vue.extend({
// //
if (this.$store.state.device.enableSounds) { if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/othello-put-me.mp3`); const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
sound.volume = this.$store.state.device.soundVolume; sound.volume = this.$store.state.device.soundVolume;
sound.play(); sound.play();
} }
@ -187,7 +187,7 @@ export default Vue.extend({
// //
if (this.$store.state.device.enableSounds && x.color != this.myColor) { if (this.$store.state.device.enableSounds && x.color != this.myColor) {
const sound = new Audio(`${url}/assets/othello-put-you.mp3`); const sound = new Audio(`${url}/assets/reversi-put-you.mp3`);
sound.volume = this.$store.state.device.soundVolume; sound.volume = this.$store.state.device.soundVolume;
sound.play(); sound.play();
} }
@ -213,7 +213,7 @@ export default Vue.extend({
onRescue(game) { onRescue(game) {
this.game = game; this.game = game;
this.o = new Othello(this.game.settings.map, { this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo, isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere, canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard loopedBoard: this.game.settings.loopedBoard

View File

@ -7,9 +7,9 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XGame from './othello.game.vue'; import XGame from './reversi.game.vue';
import XRoom from './othello.room.vue'; import XRoom from './reversi.room.vue';
import { OthelloGameStream } from '../../scripts/streaming/othello-game'; import { ReversiGameStream } from '../../scripts/streaming/reversi-game';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -25,7 +25,7 @@ export default Vue.extend({
}, },
created() { created() {
this.g = this.game; this.g = this.game;
this.connection = new OthelloGameStream((this as any).os, (this as any).os.i, this.game); this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game);
this.connection.on('started', this.onStarted); this.connection.on('started', this.onStarted);
}, },
beforeDestroy() { beforeDestroy() {

View File

@ -94,7 +94,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as maps from '../../../../../othello/maps'; import * as maps from '../../../../../reversi/maps';
export default Vue.extend({ export default Vue.extend({
props: ['game', 'connection'], props: ['game', 'connection'],
@ -116,13 +116,13 @@ export default Vue.extend({
return categories.filter((item, pos) => categories.indexOf(item) == pos); return categories.filter((item, pos) => categories.indexOf(item) == pos);
}, },
isAccepted(): boolean { isAccepted(): boolean {
if (this.game.user1Id == (this as any).os.i.id && this.game.user1Accepted) return true; if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true;
if (this.game.user2Id == (this as any).os.i.id && this.game.user2Accepted) return true; if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true;
return false; return false;
}, },
isOpAccepted(): boolean { isOpAccepted(): boolean {
if (this.game.user1Id != (this as any).os.i.id && this.game.user1Accepted) return true; if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true;
if (this.game.user2Id != (this as any).os.i.id && this.game.user2Accepted) return true; if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true;
return false; return false;
} }
}, },
@ -133,8 +133,8 @@ export default Vue.extend({
this.connection.on('init-form', this.onInitForm); this.connection.on('init-form', this.onInitForm);
this.connection.on('message', this.onMessage); this.connection.on('message', this.onMessage);
if (this.game.user1Id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1; if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
if (this.game.user2Id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2; if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
}, },
beforeDestroy() { beforeDestroy() {
@ -185,12 +185,12 @@ export default Vue.extend({
}, },
onInitForm(x) { onInitForm(x) {
if (x.userId == (this as any).os.i.id) return; if (x.userId == this.$store.state.i.id) return;
this.form = x.form; this.form = x.form;
}, },
onMessage(x) { onMessage(x) {
if (x.userId == (this as any).os.i.id) return; if (x.userId == this.$store.state.i.id) return;
this.messages.unshift(x.message); this.messages.unshift(x.message);
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-othello"> <div class="mk-reversi">
<div v-if="game"> <div v-if="game">
<x-gameroom :game="game"/> <x-gameroom :game="game"/>
</div> </div>
@ -11,14 +11,14 @@
</div> </div>
<div class="index" v-else> <div class="index" v-else>
<h1>Misskey %fa:circle%thell%fa:circle R%</h1> <h1>Misskey %fa:circle%thell%fa:circle R%</h1>
<p>他のMisskeyユーザーとオセロで対戦しよう</p> <p>他のMisskeyユーザーとリバーシで対戦しよう</p>
<div class="play"> <div class="play">
<el-button round>フリーマッチ(準備中)</el-button> <el-button round>フリーマッチ(準備中)</el-button>
<el-button type="primary" round @click="match">指名</el-button> <el-button type="primary" round @click="match">指名</el-button>
<details> <details>
<summary>遊び方</summary> <summary>遊び方</summary>
<div> <div>
<p>オセロ相手と交互に石をボードに置いてゆき相手の石を挟んでひっくり返しながら最終的に残った石が多い方が勝ちというボードゲームです</p> <p>リバーシ相手と交互に石をボードに置いてゆき相手の石を挟んでひっくり返しながら最終的に残った石が多い方が勝ちというボードゲームです</p>
<dl> <dl>
<dt><b>フリーマッチ</b></dt> <dt><b>フリーマッチ</b></dt>
<dd>ランダムなユーザーと対戦するモードです</dd> <dd>ランダムなユーザーと対戦するモードです</dd>
@ -39,7 +39,7 @@
</section> </section>
<section v-if="myGames.length > 0"> <section v-if="myGames.length > 0">
<h2>自分の対局</h2> <h2>自分の対局</h2>
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`"> <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/> <mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/> <mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span> <span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
@ -48,7 +48,7 @@
</section> </section>
<section v-if="games.length > 0"> <section v-if="games.length > 0">
<h2>みんなの対局</h2> <h2>みんなの対局</h2>
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`"> <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/> <mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/> <mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span> <span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
@ -61,7 +61,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XGameroom from './othello.gameroom.vue'; import XGameroom from './reversi.gameroom.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -93,24 +93,24 @@ export default Vue.extend({
} }
}, },
mounted() { mounted() {
this.connection = (this as any).os.streams.othelloStream.getConnection(); this.connection = (this as any).os.streams.reversiStream.getConnection();
this.connectionId = (this as any).os.streams.othelloStream.use(); this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('matched', this.onMatched); this.connection.on('matched', this.onMatched);
this.connection.on('invited', this.onInvited); this.connection.on('invited', this.onInvited);
(this as any).api('othello/games', { (this as any).api('reversi/games', {
my: true my: true
}).then(games => { }).then(games => {
this.myGames = games; this.myGames = games;
}); });
(this as any).api('othello/games').then(games => { (this as any).api('reversi/games').then(games => {
this.games = games; this.games = games;
this.gamesFetching = false; this.gamesFetching = false;
}); });
(this as any).api('othello/invitations').then(invitations => { (this as any).api('reversi/invitations').then(invitations => {
this.invitations = this.invitations.concat(invitations); this.invitations = this.invitations.concat(invitations);
}); });
@ -126,13 +126,13 @@ export default Vue.extend({
beforeDestroy() { beforeDestroy() {
this.connection.off('matched', this.onMatched); this.connection.off('matched', this.onMatched);
this.connection.off('invited', this.onInvited); this.connection.off('invited', this.onInvited);
(this as any).os.streams.othelloStream.dispose(this.connectionId); (this as any).os.streams.reversiStream.dispose(this.connectionId);
clearInterval(this.pingClock); clearInterval(this.pingClock);
}, },
methods: { methods: {
go(game) { go(game) {
(this as any).api('othello/games/show', { (this as any).api('reversi/games/show', {
gameId: game.id gameId: game.id
}).then(game => { }).then(game => {
this.matching = null; this.matching = null;
@ -146,7 +146,7 @@ export default Vue.extend({
(this as any).api('users/show', { (this as any).api('users/show', {
username username
}).then(user => { }).then(user => {
(this as any).api('othello/match', { (this as any).api('reversi/match', {
userId: user.id userId: user.id
}).then(res => { }).then(res => {
if (res == null) { if (res == null) {
@ -160,10 +160,10 @@ export default Vue.extend({
}, },
cancel() { cancel() {
this.matching = null; this.matching = null;
(this as any).api('othello/match/cancel'); (this as any).api('reversi/match/cancel');
}, },
accept(invitation) { accept(invitation) {
(this as any).api('othello/match', { (this as any).api('reversi/match', {
userId: invitation.parent.id userId: invitation.parent.id
}).then(game => { }).then(game => {
if (game) { if (game) {
@ -186,7 +186,7 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.mk-othello .mk-reversi
color #677f84 color #677f84
background #fff background #fff

View File

@ -1,24 +1,33 @@
<template> <template>
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit"> <form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
<label class="user-name"> <div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="%i18n:@username%" autofocus required @change="onUsernameChange"/>%fa:at% <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
</label> <span>%i18n:@username%</span>
<label class="password"> <span slot="prefix">@</span>
<input v-model="password" type="password" placeholder="%i18n:@password%" required/>%fa:lock% <span slot="suffix">@{{ host }}</span>
</label> </ui-input>
<label class="token" v-if="user && user.twoFactorEnabled"> <ui-input v-model="password" type="password" required>
<input v-model="token" type="number" placeholder="%i18n:@token%" required/>%fa:lock% <span>%i18n:@password%</span>
</label> <span slot="prefix">%fa:lock%</span>
<button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</button> </ui-input>
もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> <ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
<p style="margin: 8px 0;">または<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a></p>
</form> </form>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { apiUrl } from '../../../config'; import { apiUrl, host } from '../../../config';
export default Vue.extend({ export default Vue.extend({
props: {
withAvatar: {
type: Boolean,
required: false,
default: true
}
},
data() { data() {
return { return {
signing: false, signing: false,
@ -27,6 +36,7 @@ export default Vue.extend({
password: '', password: '',
token: '', token: '',
apiUrl, apiUrl,
host
}; };
}, },
methods: { methods: {
@ -35,6 +45,8 @@ export default Vue.extend({
username: this.username username: this.username
}).then(user => { }).then(user => {
this.user = user; this.user = user;
}, () => {
this.user = null;
}); });
}, },
onSubmit() { onSubmit() {
@ -59,84 +71,19 @@ export default Vue.extend({
@import '~const.styl' @import '~const.styl'
.mk-signin .mk-signin
color #555
&.signing &.signing
&, * &, *
cursor wait !important cursor wait !important
label > .avatar
display block margin 16px auto 0 auto
margin 12px 0 width 64px
height 64px
[data-fa] background #ddd
display block background-position center
pointer-events none background-size cover
position absolute border-radius 100%
bottom 0
top 0
left 0
z-index 1
margin auto
padding 0 16px
height 1em
color #898786
input[type=text]
input[type=password]
input[type=number]
user-select text
display inline-block
cursor auto
padding 0 0 0 38px
margin 0
width 100%
line-height 44px
font-size 1em
color rgba(#000, 0.7)
background #fff
outline none
border solid 1px #eee
border-radius 4px
&:hover
background rgba(255, 255, 255, 0.7)
border-color #ddd
& + i
color #797776
&:focus
background #fff
border-color #ccc
& + i
color #797776
[type=submit]
cursor pointer
padding 16px
margin -6px 0 0 0
width 100%
font-size 1.2em
color rgba(#000, 0.5)
outline none
border none
border-radius 0
background transparent
transition all .5s ease
&:hover
color $theme-color
transition all .2s ease
&:focus
color $theme-color
transition all .2s ease
&:active
color darken($theme-color, 30%)
transition all .2s ease
&:disabled
opacity 0.7
</style> </style>

View File

@ -1,60 +1,58 @@
<template> <template>
<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off"> <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<label class="username"> <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
<p class="caption">%fa:at%%i18n:@username%</p> <span>%i18n:@username%</span>
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/> <span slot="prefix">@</span>
<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p> <span slot="suffix">@{{ host }}</span>
<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:@checking%</p> <p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p>
<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p> <p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p>
<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p> <p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p>
<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p> <p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p>
<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p> <p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p>
<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p> <p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p> <p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
</label> </ui-input>
<label class="password"> <ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
<p class="caption">%fa:lock%%i18n:@password%</p> <span>%i18n:@password%</span>
<input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/> <span slot="prefix">%fa:lock%</span>
<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> <div slot="text">
<div class="value" ref="passwordMetar"></div> <p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p>
<p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p>
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
</div> </div>
<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p> </ui-input>
<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p> <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p> <span>%i18n:@password% (%i18n:@retype%)</span>
</label> <span slot="prefix">%fa:lock%</span>
<label class="retype-password"> <div slot="text">
<p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p> <p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p>
<input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/> <p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p>
<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p> </div>
<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@password-not-matched%</p> </ui-input>
</label> <div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div>
<label class="recaptcha"> <label class="agree-tou" style="display: block; margin: 16px 0;">
<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:@recaptcha%</p> <input name="agree-tou" type="checkbox" required/>
<div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
</label>
<label class="agree-tou">
<input name="agree-tou" type="checkbox" autocomplete="off" required/>
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> <p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
</label> </label>
<button type="submit">%i18n:@create%</button> <ui-button type="submit">%i18n:@create%</ui-button>
</form> </form>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength'); const getPasswordStrength = require('syuilo-password-strength');
import { url, docsUrl, lang, recaptchaSitekey } from '../../../config'; import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config';
export default Vue.extend({ export default Vue.extend({
data() { data() {
return { return {
host,
username: '', username: '',
password: '', password: '',
retypedPassword: '', retypedPassword: '',
url, url,
touUrl: `${docsUrl}/${lang}/tou`, touUrl: `${docsUrl}/${lang}/tou`,
recaptchaSitekey, recaptchaSitekey,
recaptchaed: false,
usernameState: null, usernameState: null,
passwordStrength: '', passwordStrength: '',
passwordRetypeState: null passwordRetypeState: null
@ -104,7 +102,6 @@ export default Vue.extend({
const strength = getPasswordStrength(this.password); const strength = getPasswordStrength(this.password);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
}, },
onChangePasswordRetype() { onChangePasswordRetype() {
if (this.retypedPassword == '') { if (this.retypedPassword == '') {
@ -130,19 +127,9 @@ export default Vue.extend({
alert('%i18n:@some-error%'); alert('%i18n:@some-error%');
(window as any).grecaptcha.reset(); (window as any).grecaptcha.reset();
this.recaptchaed = false;
}); });
} }
}, },
created() {
(window as any).onRecaptchaed = () => {
this.recaptchaed = true;
};
(window as any).onRecaptchaExpired = () => {
this.recaptchaed = false;
};
},
mounted() { mounted() {
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script'); const script = document.createElement('script');
@ -158,100 +145,6 @@ export default Vue.extend({
.mk-signup .mk-signup
min-width 302px min-width 302px
label
display block
margin 0 0 16px 0
> .caption
margin 0 0 4px 0
color #828888
font-size 0.95em
> [data-fa]
margin-right 0.25em
color #96adac
> .info
display block
margin 4px 0
font-size 0.8em
> [data-fa]
margin-right 0.3em
&.username
.profile-page-url-preview
display block
margin 4px 8px 0 4px
font-size 0.8em
color #888
&:empty
display none
&:not(:empty) + .info
margin-top 0
&.password
.meter
display block
margin-top 8px
width 100%
height 8px
&[data-strength='']
display none
&[data-strength='low']
> .value
background #d73612
&[data-strength='medium']
> .value
background #d7ca12
&[data-strength='high']
> .value
background #61bb22
> .value
display block
width 0%
height 100%
background transparent
border-radius 4px
transition all 0.1s ease
[type=text], [type=password]
user-select text
display inline-block
cursor auto
padding 0 12px
margin 0
width 100%
line-height 44px
font-size 1em
color #333 !important
background #fff !important
outline none
border solid 1px rgba(#000, 0.1)
border-radius 4px
box-shadow 0 0 0 114514px #fff inset
transition all .3s ease
&:hover
border-color rgba(#000, 0.2)
transition all .1s ease
&:focus
color $theme-color !important
border-color $theme-color
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
transition all 0s ease
&:disabled
opacity 0.5
.agree-tou .agree-tou
padding 4px padding 4px
border-radius 4px border-radius 4px
@ -269,19 +162,4 @@ export default Vue.extend({
display inline display inline
color #555 color #555
button
margin 0
padding 16px
width 100%
font-size 1em
color #fff
background $theme-color
border-radius 3px
&:hover
background lighten($theme-color, 5%)
&:active
background darken($theme-color, 5%)
</style> </style>

View File

@ -58,18 +58,21 @@ export default Vue.extend({
}, },
created() { created() {
if (this.mode == 'relative' || this.mode == 'detail') { if (this.mode == 'relative' || this.mode == 'detail') {
this.tick(); this.tickId = window.requestAnimationFrame(this.tick);
this.tickId = setInterval(this.tick, 1000);
} }
}, },
destroyed() { destroyed() {
if (this.mode === 'relative' || this.mode === 'detail') { if (this.mode === 'relative' || this.mode === 'detail') {
clearInterval(this.tickId); window.clearTimeout(this.tickId);
} }
}, },
methods: { methods: {
tick() { tick() {
this.now = new Date(); this.now = new Date();
this.tickId = setTimeout(() => {
window.requestAnimationFrame(this.tick);
}, 10000);
} }
} }
}); });

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="mk-twitter-setting"> <div class="mk-twitter-setting">
<p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p> <p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p>
<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p> <p class="account" v-if="$store.state.i.twitter" :title="`Twitter ID: ${$store.state.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
<p> <p>
<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a> <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ $store.state.i.twitter ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
<span v-if="os.i.twitter"> or </span> <span v-if="$store.state.i.twitter"> or </span>
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a> <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a>
</p> </p>
<p class="id" v-if="os.i.twitter">Twitter ID: {{ os.i.twitter.userId }}</p> <p class="id" v-if="$store.state.i.twitter">Twitter ID: {{ $store.state.i.twitter.userId }}</p>
</div> </div>
</template> </template>
@ -24,8 +24,8 @@ export default Vue.extend({
}; };
}, },
mounted() { mounted() {
this.$watch('os.i', () => { this.$watch('$store.state.i', () => {
if ((this as any).os.i.twitter) { if (this.$store.state.i.twitter) {
if (this.form) this.form.close(); if (this.form) this.form.close();
} }
}, { }, {

View File

@ -0,0 +1,82 @@
<template>
<div class="ui-button" :class="[styl]">
<button :type="type" @click="$emit('click')">
<slot></slot>
</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
type: {
type: String,
required: false
}
},
data() {
return {
styl: 'fill'
};
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
> button
display block
width 100%
margin 0
padding 0
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
&:hover
background lighten($theme-color, 5%)
&:active
background darken($theme-color, 5%)
else
color $theme-color
background none
&: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)
.ui-button:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="ui-card">
<header>
<slot name="title"></slot>
</header>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
provide() {
return {
isCardChild: true
};
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
margin 16px
padding 16px
color isDark ? #fff : #000
background isDark ? #282C37 : #fff
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
.ui-card[data-darkmode]
root(true)
.ui-card:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="ui-form">
<fieldset :disabled="disabled">
<slot></slot>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
disabled: {
type: Boolean,
required: false
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.ui-form
> fieldset
margin 0
padding 0
border none
</style>

View File

@ -0,0 +1,350 @@
<template>
<div class="ui-input" :class="[{ focused, filled }, styl]">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input">
<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
<div class="value" ref="passwordMetar"></div>
</div>
<span class="label" ref="label"><slot></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'">
<input ref="input"
:type="type"
v-model="v"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false">
</template>
<template v-else>
<input ref="input"
type="text"
:value="placeholder"
readonly
@click="chooseFile">
<input ref="file"
type="file"
:value="value"
@change="onChangeFile">
</template>
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
export default Vue.extend({
props: {
value: {
required: false
},
type: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
required: false
},
spellcheck: {
required: false
},
withPasswordMeter: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
v: this.value,
focused: false,
passwordStrength: '',
styl: 'fill'
};
},
computed: {
filled(): boolean {
return this.v != '' && this.v != null;
},
placeholder(): string {
if (this.type != 'file') return null;
if (this.v == null) return null;
if (typeof this.v == 'string') return this.v;
if (Array.isArray(this.v)) {
return this.v.map(file => file.name).join(', ');
} else {
return this.v.name;
}
}
},
watch: {
value(v) {
this.v = v;
},
v(v) {
this.$emit('input', v);
if (this.withPasswordMeter) {
if (v == '') {
this.passwordStrength = '';
return;
}
const strength = getPasswordStrength(v);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
}
}
},
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';
if (this.$refs.prefix.offsetWidth) {
this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px';
}
}
if (this.$refs.suffix) {
if (this.$refs.suffix.offsetWidth) {
this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px';
}
}
},
methods: {
focus() {
this.$refs.input.focus();
},
chooseFile() {
this.$refs.file.click();
},
onChangeFile() {
this.v = Array.from((this.$refs.file as any).files);
this.$emit('input', this.v);
this.$emit('change', this.v);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
margin 32px 0
> .icon
position absolute
top 0
left 0
width 24px
text-align center
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
&:not(:empty) + .input
margin-left 28px
> .input
if !fill
&:before
content ''
display block
position absolute
bottom 0
left 0
right 0
height 1px
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
&:after
content ''
display block
position absolute
bottom 0
left 0
right 0
height 2px
background $theme-color
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)
will-change border opacity transform
> .password-meter
position absolute
top 0
left 0
width 100%
height 100%
border-radius 6px
overflow hidden
opacity 0.3
&[data-strength='']
display none
&[data-strength='low']
> .value
background #d73612
&[data-strength='medium']
> .value
background #d7ca12
&[data-strength='high']
> .value
background #61bb22
> .value
display block
width 0%
height 100%
background transparent
border-radius 6px
transition all 0.1s ease
> .label
position absolute
z-index 1
top fill ? 6px : 0
left 0
pointer-events none
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
transition-duration 0.3s
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
pointer-events none
//will-change transform
transform-origin top left
transform scale(1)
> input
display block
width 100%
margin 0
padding 0
font inherit
font-weight fill ? bold : normal
font-size 16px
line-height 32px
color isDark ? #fff : #000
background transparent
border none
border-radius 0
outline none
box-shadow none
if fill
padding 6px 12px
background rgba(#000, 0.035)
border-radius 6px
&[type='file']
display none
> .prefix
> .suffix
display block
position absolute
z-index 1
top 0
font-size 16px
line-height fill ? 44px : 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
pointer-events none
&:empty
display none
> *
display block
min-width 16px
max-width 150px
overflow hidden
white-space nowrap
text-overflow ellipsis
> .prefix
left 0
padding-right 4px
if fill
padding-left 12px
> .suffix
right 0
padding-left 4px
if fill
padding-right 12px
> .text
margin 6px 0
font-size 13px
*
margin 0
&.focused
> .input
if fill
background rgba(#000, 0.05)
else
&:after
opacity 1
transform scaleX(1)
> .label
color $theme-color
&.focused
&.filled
> .input
> .label
top fill ? -24px : -17px
left 0 !important
transform scale(0.75)
.ui-input[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-input:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -0,0 +1,120 @@
<template>
<div
class="ui-radio"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input type="radio"
:disabled="disabled"
>
<span class="button">
<span></span>
</span>
<span class="label"><slot></slot></span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'model',
event: 'change'
},
props: {
model: {
type: String,
required: false
},
value: {
type: String,
required: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.model === this.value;
}
},
methods: {
toggle() {
this.$emit('change', this.value);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display inline-block
margin 32px 32px 32px 0
cursor pointer
transition all 0.3s
> *
user-select none
&.disabled
opacity 0.6
cursor not-allowed
&.checked
> .button
border-color $theme-color
&:after
background-color $theme-color
transform scale(1)
opacity 1
> input
position absolute
width 0
height 0
opacity 0
margin 0
> .button
position absolute
width 20px
height 20px
background none
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
border-radius 100%
transition inherit
&:after
content ''
display block
position absolute
top 3px
right 3px
bottom 3px
left 3px
border-radius 100%
opacity 0
transform scale(0)
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
> .label
margin-left 28px
display block
font-size 16px
line-height 20px
cursor pointer
.ui-radio[data-darkmode]
root(true)
.ui-radio:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,215 @@
<template>
<div class="ui-select" :class="[{ focused, filled }, styl]">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input" @click="focus">
<span class="label" ref="label"><slot name="label"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
:value="v"
:required="required"
@input="$emit('input', $event.target.value)"
@focus="focused = true"
@blur="focused = false">
<slot></slot>
</select>
<div class="suffix"><slot name="suffix"></slot></div>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
}
},
data() {
return {
v: this.value,
focused: false,
styl: 'fill'
};
},
computed: {
filled(): boolean {
return this.v != '' && this.v != null;
}
},
watch: {
value(v) {
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';
}
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
margin 32px 0
> .icon
position absolute
top 0
left 0
width 24px
text-align center
line-height 32px
color rgba(#000, 0.54)
&:not(:empty) + .input
margin-left 28px
> .input
display flex
if fill
padding 6px 12px
background rgba(#000, 0.035)
border-radius 6px
else
&:before
content ''
display block
position absolute
bottom 0
left 0
right 0
height 1px
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
&:after
content ''
display block
position absolute
bottom 0
left 0
right 0
height 2px
background $theme-color
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)
will-change border opacity transform
> .label
position absolute
top fill ? 6px : 0
left 0
pointer-events none
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
transition-duration 0.3s
font-size 16px
line-height 32px
color rgba(#000, 0.54)
pointer-events none
//will-change transform
transform-origin top left
transform scale(1)
> select
display block
flex 1
width 100%
padding 0
font inherit
font-weight fill ? bold : normal
font-size 16px
height 32px
color isDark ? #fff : #000
background transparent
border none
border-radius 0
outline none
box-shadow none
*
color #000
> .prefix
> .suffix
display block
align-self center
justify-self center
font-size 16px
line-height 32px
color rgba(#000, 0.54)
pointer-events none
> *
display block
min-width 16px
> .prefix
padding-right 4px
> .suffix
padding-left 4px
> .text
margin 6px 0
font-size 13px
*
margin 0
&.focused
> .input
if fill
background rgba(#000, 0.05)
else
&:after
opacity 1
transform scaleX(1)
> .label
color $theme-color
&.focused
&.filled
> .input
> .label
top fill ? -24px : -17px
left 0 !important
transform scale(0.75)
.ui-select[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-select:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -0,0 +1,135 @@
<template>
<div
class="ui-switch"
:class="{ disabled, checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
>
<span class="button">
<span></span>
</span>
<span class="label">
<span :aria-hidden="!checked"><slot></slot></span>
<p :aria-hidden="!checked">
<slot name="text"></slot>
</p>
</span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.value;
}
},
methods: {
toggle() {
this.$emit('change', !this.checked);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display flex
margin 32px 0
cursor pointer
transition all 0.3s
> *
user-select none
&.disabled
opacity 0.6
cursor not-allowed
&.checked
> .button
background-color rgba($theme-color, 0.4)
border-color rgba($theme-color, 0.4)
> *
background-color $theme-color
transform translateX(14px)
> input
position absolute
width 0
height 0
opacity 0
margin 0
> .button
display inline-block
margin 3px 0 0 0
width 34px
height 14px
background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
outline none
border-radius 14px
transition inherit
> *
position absolute
top -3px
left 0
border-radius 100%
transition background-color 0.3s, transform 0.3s
width 20px
height 20px
background-color #fff
box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12)
> .label
margin-left 8px
display block
font-size 16px
cursor pointer
transition inherit
> span
display block
line-height 20px
color isDark ? #c4ccd2 : rgba(#000, 0.75)
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)
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="ui-textarea" :class="{ focused, filled }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
<textarea ref="input"
:value="value"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
@input="$emit('input', $event.target.value)"
@focus="focused = true"
@blur="focused = false">
</textarea>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
export default Vue.extend({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
type: String,
required: false
}
},
data() {
return {
focused: false,
passwordStrength: ''
}
},
computed: {
filled(): boolean {
return this.value != '' && this.value != null;
}
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
margin 42px 0 32px 0
> .input
padding 12px
if fill
background rgba(#000, 0.035)
border-radius 6px
else
&:before
content ''
display block
position absolute
top 0
bottom 0
left 0
right 0
background none
border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
border-radius 3px
pointer-events none
&:after
content ''
display block
position absolute
top 0
bottom 0
left 0
right 0
background none
border solid 2px $theme-color
border-radius 3px
opacity 0
transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
pointer-events none
> .label
position absolute
top 6px
left 12px
pointer-events none
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
transition-duration 0.3s
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
pointer-events none
//will-change transform
transform-origin top left
transform scale(1)
> textarea
display block
width 100%
min-height 100px
padding 0
font inherit
font-weight fill ? bold : normal
font-size 16px
color isDark ? #fff : #000
background transparent
border none
border-radius 0
outline none
box-shadow none
> .text
margin 6px 0
font-size 13px
*
margin 0
&.focused
> .input
if fill
background rgba(#000, 0.05)
else
&:after
opacity 1
> .label
color $theme-color
&.focused
&.filled
> .input
> .label
top -24px
left 0 !important
transform scale(0.75)
.ui-textarea[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-textarea:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -50,7 +50,7 @@ export default Vue.extend({
reader.readAsDataURL(file); reader.readAsDataURL(file);
const data = new FormData(); const data = new FormData();
data.append('i', (this as any).os.i.token); data.append('i', this.$store.state.i.token);
data.append('file', file); data.append('file', file);
if (folder) data.append('folderId', folder); if (folder) data.append('folderId', folder);

View File

@ -68,7 +68,7 @@ iframe
root(isDark) root(isDark)
> a > a
display block display block
font-size 16px font-size 14px
border solid 1px isDark ? #191b1f : #eee border solid 1px isDark ? #191b1f : #eee
border-radius 4px border-radius 4px
overflow hidden overflow hidden
@ -136,8 +136,17 @@ root(isDark)
left 0 left 0
width 100% width 100%
@media (max-width 550px)
font-size 12px
> .thumbnail
height 80px
> article
padding 12px
@media (max-width 500px) @media (max-width 500px)
font-size 8px font-size 10px
> .thumbnail > .thumbnail
height 70px height 70px
@ -145,6 +154,16 @@ root(isDark)
> article > article
padding 8px padding 8px
> header
margin-bottom 4px
> footer
margin-top 4px
> img
width 12px
height 12px
.mk-url-preview[data-darkmode] .mk-url-preview[data-darkmode]
root(true) root(true)

View File

@ -203,6 +203,7 @@ root(isDark)
justify-content center justify-content center
align-items center align-items center
margin-right 10px margin-right 10px
width 16px
> *:last-child > *:last-child
flex 1 1 auto flex 1 1 auto

View File

@ -13,7 +13,7 @@
</div> </div>
</header> </header>
<div class="text"> <div class="text">
<mk-note-html :text="note.text"/> <mk-note-html v-if="note.text" :text="note.text"/>
</div> </div>
</div> </div>
</div> </div>
@ -109,6 +109,9 @@ root(isDark)
> .created-at > .created-at
color isDark ? #606984 : #c0c0c0 color isDark ? #606984 : #c0c0c0
> .text
text-align left
.mk-welcome-timeline[data-darkmode] .mk-welcome-timeline[data-darkmode]
root(true) root(true)

View File

@ -0,0 +1,41 @@
<template>
<div class="mkw-analog-clock">
<mk-widget-container :naked="props.naked" :show-header="false">
<div class="mkw-analog-clock--body">
<mk-analog-clock :dark="$store.state.device.darkmode"/>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
export default define({
name: 'analog-clock',
props: () => ({
naked: false
})
}).extend({
methods: {
func() {
this.props.naked = !this.props.naked;
this.save();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mkw-analog-clock--body
padding 8px
.mkw-analog-clock[data-darkmode]
root(true)
.mkw-analog-clock:not([data-darkmode])
root(false)
</style>

View File

@ -2,7 +2,7 @@
<div class="mkw-broadcast" <div class="mkw-broadcast"
:data-found="broadcasts.length != 0" :data-found="broadcasts.length != 0"
:data-melt="props.design == 1" :data-melt="props.design == 1"
:data-mobile="isMobile" :data-mobile="platform == 'mobile'"
> >
<div class="icon"> <div class="icon">
<svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> <svg height="32" version="1.1" viewBox="0 0 32 32" width="32">

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-calendar" :data-special="special" :data-mobile="isMobile"> <div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'">
<mk-widget-container :naked="props.design == 1" :show-header="false"> <mk-widget-container :naked="props.design == 1" :show-header="false">
<div class="mkw-calendar--body"> <div class="mkw-calendar--body">
<div class="calendar" :data-is-holiday="isHoliday"> <div class="calendar" :data-is-holiday="isHoliday">
@ -67,7 +67,7 @@ export default define({
}, },
methods: { methods: {
func() { func() {
if (this.isMobile) return; if (this.platform == 'mobile') return;
if (this.props.design == 2) { if (this.props.design == 2) {
this.props.design = 0; this.props.design = 0;
} else { } else {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-donation" :data-mobile="isMobile"> <div class="mkw-donation" :data-mobile="platform == 'mobile'">
<article> <article>
<h1>%fa:heart%%i18n:@title%</h1> <h1>%fa:heart%%i18n:@title%</h1>
<p> <p>

View File

@ -0,0 +1,89 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
<defs>
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="polygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="polylinePoints"
fill="none"
stroke="#fff"
stroke-width="2"/>
<circle
:cx="headX"
:cy="headY"
r="3"
fill="#fff"/>
</mask>
</defs>
<rect
x="-10" y="-10"
:width="viewBoxX + 20" :height="viewBoxY + 20"
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
import * as uuid from 'uuid';
export default Vue.extend({
props: {
src: {
type: Array,
required: true
}
},
data() {
return {
viewBoxX: 50,
viewBoxY: 30,
gradientId: uuid(),
maskId: uuid(),
polylinePoints: '',
polygonPoints: '',
headX: null,
headY: null,
clock: null
};
},
watch: {
src() {
this.draw();
}
},
created() {
this.draw();
// VueWatch
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
draw() {
const stats = this.src.slice().reverse();
const peak = Math.max.apply(null, stats) || 1;
const polylinePoints = stats.map((n, i) => [
i * (this.viewBoxX / (stats.length - 1)),
(1 - (n / peak)) * this.viewBoxY
]);
this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.headX = polylinePoints[polylinePoints.length - 1][0];
this.headY = polylinePoints[polylinePoints.length - 1][1];
}
}
});
</script>

View File

@ -0,0 +1,118 @@
<template>
<div class="mkw-hashtags">
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:hashtag%%i18n:@title%</template>
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
<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/${ 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>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import XChart from './hashtags.chart.vue';
export default define({
name: 'hashtags',
props: () => ({
compact: false
})
}).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: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
fetch() {
(this as any).api('hashtags/trend').then(stats => {
this.stats = stats;
this.fetching = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
.mkw-hashtags--body
> .fetching
> .empty
margin 0
padding 16px
text-align center
color #aaa
> [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 isDark ? #393f4f : #eee
> .tag
flex 1
overflow hidden
font-size 14px
color isDark ? #9baec8 : #65727b
> 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
.mkw-hashtags[data-darkmode]
root(true)
.mkw-hashtags:not([data-darkmode])
root(false)
</style>

View File

@ -1,8 +1,11 @@
import Vue from 'vue'; import Vue from 'vue';
import wAnalogClock from './analog-clock.vue';
import wVersion from './version.vue'; import wVersion from './version.vue';
import wRss from './rss.vue'; import wRss from './rss.vue';
import wServer from './server.vue'; import wServer from './server.vue';
import wPostsMonitor from './posts-monitor.vue';
import wMemo from './memo.vue';
import wBroadcast from './broadcast.vue'; import wBroadcast from './broadcast.vue';
import wCalendar from './calendar.vue'; import wCalendar from './calendar.vue';
import wPhotoStream from './photo-stream.vue'; import wPhotoStream from './photo-stream.vue';
@ -10,7 +13,9 @@ import wSlideshow from './slideshow.vue';
import wTips from './tips.vue'; import wTips from './tips.vue';
import wDonation from './donation.vue'; import wDonation from './donation.vue';
import wNav from './nav.vue'; import wNav from './nav.vue';
import wHashtags from './hashtags.vue';
Vue.component('mkw-analog-clock', wAnalogClock);
Vue.component('mkw-nav', wNav); Vue.component('mkw-nav', wNav);
Vue.component('mkw-calendar', wCalendar); Vue.component('mkw-calendar', wCalendar);
Vue.component('mkw-photo-stream', wPhotoStream); Vue.component('mkw-photo-stream', wPhotoStream);
@ -19,5 +24,8 @@ Vue.component('mkw-tips', wTips);
Vue.component('mkw-donation', wDonation); Vue.component('mkw-donation', wDonation);
Vue.component('mkw-broadcast', wBroadcast); Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-server', wServer); Vue.component('mkw-server', wServer);
Vue.component('mkw-posts-monitor', wPostsMonitor);
Vue.component('mkw-memo', wMemo);
Vue.component('mkw-rss', wRss); Vue.component('mkw-rss', wRss);
Vue.component('mkw-version', wVersion); Vue.component('mkw-version', wVersion);
Vue.component('mkw-hashtags', wHashtags);

View File

@ -0,0 +1,111 @@
<template>
<div class="mkw-memo">
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:R sticky-note%%i18n:@title%</template>
<div class="mkw-memo--body">
<textarea v-model="text" placeholder="%i18n:@memo%" @input="onChange"></textarea>
<button @click="saveMemo" :disabled="!changed">%i18n:@save%</button>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../define-widget';
export default define({
name: 'memo',
props: () => ({
compact: false
})
}).extend({
data() {
return {
text: null,
changed: false
};
},
created() {
this.text = this.$store.state.settings.memo;
this.$watch('$store.state.settings.memo', text => {
this.text = text;
});
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
onChange() {
this.changed = true;
},
saveMemo() {
this.$store.dispatch('settings/set', {
key: 'memo',
value: this.text
});
this.changed = false;
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
.mkw-memo--body
padding-bottom 28px + 16px
> textarea
display block
width 100%
max-width 100%
min-width 100%
padding 16px
color isDark ? #fff : #222
background isDark ? #282c37 : #fff
border none
border-bottom solid 1px isDark ? #1c2023 : #eee
border-radius 0
> button
display block
position absolute
bottom 8px
right 8px
margin 0
padding 0 10px
height 28px
color $theme-color-foreground
background $theme-color !important
outline none
border none
border-radius 4px
transition background 0.1s ease
cursor pointer
&:hover
background lighten($theme-color, 10%) !important
&:active
background darken($theme-color, 10%) !important
transition background 0s ease
&:disabled
opacity 0.7
cursor default
.mkw-memo[data-darkmode]
root(true)
.mkw-memo:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,211 @@
<template>
<div class="mkw-posts-monitor">
<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
<template slot="header">%fa:chart-line%%i18n:@title%</template>
<button slot="func" @click="toggle" title="%i18n:@toggle%">%fa:sort%</button>
<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }" :data-darkmode="$store.state.device.darkmode">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
<defs>
<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="localPolygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="localPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"/>
<circle
:cx="localHeadX"
:cy="localHeadY"
r="1.5"
fill="#fff"/>
</mask>
</defs>
<rect
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/>
<text x="1" y="5">Local</text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1">
<defs>
<linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="fediPolygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="fediPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"/>
<circle
:cx="fediHeadX"
:cy="fediHeadY"
r="1.5"
fill="#fff"/>
</mask>
</defs>
<rect
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/>
<text x="1" y="5">Fedi</text>
</svg>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import * as uuid from 'uuid';
export default define({
name: 'server',
props: () => ({
design: 0,
view: 0
})
}).extend({
data() {
return {
connection: null,
connectionId: null,
viewBoxY: 30,
stats: [],
fediGradientId: uuid(),
fediMaskId: uuid(),
localGradientId: uuid(),
localMaskId: uuid(),
fediPolylinePoints: '',
localPolylinePoints: '',
fediPolygonPoints: '',
localPolygonPoints: '',
fediHeadX: null,
fediHeadY: null,
localHeadX: null,
localHeadY: null
};
},
computed: {
viewBoxX(): number {
return this.props.view == 0 ? 50 : 100;
}
},
watch: {
viewBoxX() {
this.draw();
}
},
mounted() {
this.connection = (this as any).os.streams.notesStatsStream.getConnection();
this.connectionId = (this as any).os.streams.notesStatsStream.use();
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send({
type: 'requestLog',
id: Math.random().toString()
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
(this as any).os.streams.notesStatsStream.dispose(this.connectionId);
},
methods: {
toggle() {
if (this.props.view == 2) {
this.props.view = 0;
} else {
this.props.view++;
}
this.save();
},
func() {
if (this.props.design == 2) {
this.props.design = 0;
} else {
this.props.design++;
}
this.save();
},
draw() {
const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats;
const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1;
const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1;
const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]);
const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]);
this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0];
this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1];
this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0];
this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1];
},
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
this.draw();
},
onStatsLog(statsLog) {
statsLog.forEach(stats => this.onStats(stats));
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
&.dual
> svg
width 50%
float left
&:first-child
padding-right 5px
&:last-child
padding-left 5px
> svg
display block
padding 10px
width 100%
> text
font-size 5px
fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
> tspan
opacity 0.5
&:after
content ""
display block
clear both
.qpdmibaztplkylerhdbllwcokyrfxeyj[data-darkmode]
root(true)
.qpdmibaztplkylerhdbllwcokyrfxeyj:not([data-darkmode])
root(false)
</style>

View File

@ -4,7 +4,7 @@
<template slot="header">%fa:rss-square%RSS</template> <template slot="header">%fa:rss-square%RSS</template>
<button slot="func" title="設定" @click="setting">%fa:cog%</button> <button slot="func" title="設定" @click="setting">%fa:cog%</button>
<div class="mkw-rss--body" :data-mobile="isMobile"> <div class="mkw-rss--body" :data-mobile="platform == 'mobile'">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div class="feed" v-else> <div class="feed" v-else>
<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a> <a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
@ -19,12 +19,12 @@ import define from '../../../common/define-widget';
export default define({ export default define({
name: 'rss', name: 'rss',
props: () => ({ props: () => ({
compact: false compact: false,
url: 'http://news.yahoo.co.jp/pickup/rss.xml'
}) })
}).extend({ }).extend({
data() { data() {
return { return {
url: 'http://news.yahoo.co.jp/pickup/rss.xml',
items: [], items: [],
fetching: true, fetching: true,
clock: null clock: null
@ -43,7 +43,7 @@ export default define({
this.save(); this.save();
}, },
fetch() { fetch() {
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, { fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
cache: 'no-cache' cache: 'no-cache'
}).then(res => { }).then(res => {
res.json().then(feed => { res.json().then(feed => {
@ -53,7 +53,12 @@ export default define({
}); });
}, },
setting() { setting() {
alert('not implemented yet'); const url = window.prompt('URL', this.props.url);
if (url && url != '') {
this.props.url = url;
this.save();
this.fetch();
}
} }
} }
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="cpu-memory"> <div class="cpu-memory">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none"> <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs> <defs>
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
@ -16,15 +16,20 @@
fill="none" fill="none"
stroke="#fff" stroke="#fff"
stroke-width="1"/> stroke-width="1"/>
<circle
:cx="cpuHeadX"
:cy="cpuHeadY"
r="1.5"
fill="#fff"/>
</mask> </mask>
</defs> </defs>
<rect <rect
x="-1" y="-1" x="-2" y="-2"
:width="viewBoxX + 2" :height="viewBoxY + 2" :width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
</svg> </svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none"> <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs> <defs>
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
@ -40,11 +45,16 @@
fill="none" fill="none"
stroke="#fff" stroke="#fff"
stroke-width="1"/> stroke-width="1"/>
<circle
:cx="memHeadX"
:cy="memHeadY"
r="1.5"
fill="#fff"/>
</mask> </mask>
</defs> </defs>
<rect <rect
x="-1" y="-1" x="-2" y="-2"
:width="viewBoxX + 2" :height="viewBoxY + 2" :width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
</svg> </svg>
@ -70,15 +80,25 @@ export default Vue.extend({
memPolylinePoints: '', memPolylinePoints: '',
cpuPolygonPoints: '', cpuPolygonPoints: '',
memPolygonPoints: '', memPolygonPoints: '',
cpuHeadX: null,
cpuHeadY: null,
memHeadX: null,
memHeadY: null,
cpuP: '', cpuP: '',
memP: '' memP: ''
}; };
}, },
mounted() { mounted() {
this.connection.on('stats', this.onStats); this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send({
type: 'requestLog',
id: Math.random().toString()
});
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('stats', this.onStats); this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
}, },
methods: { methods: {
onStats(stats) { onStats(stats) {
@ -86,14 +106,24 @@ export default Vue.extend({
this.stats.push(stats); this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift(); if (this.stats.length > 50) this.stats.shift();
this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' '); const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' '); const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
this.cpuP = (stats.cpu_usage * 100).toFixed(0); this.cpuP = (stats.cpu_usage * 100).toFixed(0);
this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
},
onStatsLog(statsLog) {
statsLog.forEach(stats => this.onStats(stats));
} }
} }
}); });

View File

@ -55,11 +55,11 @@ export default define({
this.fetching = false; this.fetching = false;
}); });
this.connection = (this as any).os.streams.serverStream.getConnection(); this.connection = (this as any).os.streams.serverStatsStream.getConnection();
this.connectionId = (this as any).os.streams.serverStream.use(); this.connectionId = (this as any).os.streams.serverStatsStream.use();
}, },
beforeDestroy() { beforeDestroy() {
(this as any).os.streams.serverStream.dispose(this.connectionId); (this as any).os.streams.serverStatsStream.dispose(this.connectionId);
}, },
methods: { methods: {
toggle() { toggle() {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-slideshow" :data-mobile="isMobile"> <div class="mkw-slideshow" :data-mobile="platform == 'mobile'">
<div @click="choose"> <div @click="choose">
<p v-if="props.folder === undefined"> <p v-if="props.folder === undefined">
<template v-if="isCustomizeMode">フォルダを指定するにはカスタマイズモードを終了してください</template> <template v-if="isCustomizeMode">フォルダを指定するにはカスタマイズモードを終了してください</template>

View File

@ -1,6 +1,8 @@
declare const _HOST_: string; declare const _HOST_: string;
declare const _HOSTNAME_: string; declare const _HOSTNAME_: string;
declare const _URL_: string; declare const _URL_: string;
declare const _NAME_: string;
declare const _DESCRIPTION_: string;
declare const _API_URL_: string; declare const _API_URL_: string;
declare const _WS_URL_: string; declare const _WS_URL_: string;
declare const _DOCS_URL_: string; declare const _DOCS_URL_: string;
@ -17,10 +19,13 @@ declare const _VERSION_: string;
declare const _CODENAME_: string; declare const _CODENAME_: string;
declare const _LICENSE_: string; declare const _LICENSE_: string;
declare const _GOOGLE_MAPS_API_KEY_: string; declare const _GOOGLE_MAPS_API_KEY_: string;
declare const _WELCOME_BG_URL_: string;
export const host = _HOST_; export const host = _HOST_;
export const hostname = _HOSTNAME_; export const hostname = _HOSTNAME_;
export const url = _URL_; export const url = _URL_;
export const name = _NAME_;
export const description = _DESCRIPTION_;
export const apiUrl = _API_URL_; export const apiUrl = _API_URL_;
export const wsUrl = _WS_URL_; export const wsUrl = _WS_URL_;
export const docsUrl = _DOCS_URL_; export const docsUrl = _DOCS_URL_;
@ -37,3 +42,4 @@ export const version = _VERSION_;
export const codename = _CODENAME_; export const codename = _CODENAME_;
export const license = _LICENSE_; export const license = _LICENSE_;
export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_; export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
export const welcomeBgUrl = _WELCOME_BG_URL_;

View File

@ -1,18 +1,17 @@
import OS from '../../mios';
import { url } from '../../config'; import { url } from '../../config';
import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue'; import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue';
export default function(opts) { export default (os: OS) => opts => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const o = opts || {}; const o = opts || {};
if (document.body.clientWidth > 800) { if (document.body.clientWidth > 800) {
const w = new MkChooseFileFromDriveWindow({ const w = os.new(MkChooseFileFromDriveWindow, {
propsData: { title: o.title,
title: o.title, multiple: o.multiple,
multiple: o.multiple, initFolder: o.currentFolder
initFolder: o.currentFolder });
}
}).$mount();
w.$once('selected', file => { w.$once('selected', file => {
res(file); res(file);
}); });
@ -22,9 +21,9 @@ export default function(opts) {
res(file); res(file);
}; };
window.open(url + '/selectdrive', window.open(url + `/selectdrive?multiple=${o.multiple}`,
'choose_drive_window', 'choose_drive_window',
'height=500, width=800'); 'height=500, width=800');
} }
}); });
} };

View File

@ -1,17 +1,16 @@
import OS from '../../mios';
import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue'; import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue';
export default function(opts) { export default (os: OS) => opts => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const o = opts || {}; const o = opts || {};
const w = new MkChooseFolderFromDriveWindow({ const w = os.new(MkChooseFolderFromDriveWindow, {
propsData: { title: o.title,
title: o.title, initFolder: o.currentFolder
initFolder: o.currentFolder });
}
}).$mount();
w.$once('selected', folder => { w.$once('selected', folder => {
res(folder); res(folder);
}); });
document.body.appendChild(w.$el); document.body.appendChild(w.$el);
}); });
} };

View File

@ -1,16 +1,15 @@
import OS from '../../mios';
import Ctx from '../views/components/context-menu.vue'; import Ctx from '../views/components/context-menu.vue';
export default function(e, menu, opts?) { export default (os: OS) => (e, menu, opts?) => {
const o = opts || {}; const o = opts || {};
const vm = new Ctx({ const vm = os.new(Ctx, {
propsData: { menu,
menu, x: e.pageX - window.pageXOffset,
x: e.pageX - window.pageXOffset, y: e.pageY - window.pageYOffset,
y: e.pageY - window.pageYOffset, });
}
}).$mount();
vm.$once('closed', () => { vm.$once('closed', () => {
if (o.closed) o.closed(); if (o.closed) o.closed();
}); });
document.body.appendChild(vm.$el); document.body.appendChild(vm.$el);
} };

View File

@ -1,19 +1,18 @@
import OS from '../../mios';
import Dialog from '../views/components/dialog.vue'; import Dialog from '../views/components/dialog.vue';
export default function(opts) { export default (os: OS) => opts => {
return new Promise<string>((res, rej) => { return new Promise<string>((res, rej) => {
const o = opts || {}; const o = opts || {};
const d = new Dialog({ const d = os.new(Dialog, {
propsData: { title: o.title,
title: o.title, text: o.text,
text: o.text, modal: o.modal,
modal: o.modal, buttons: o.actions
buttons: o.actions });
}
}).$mount();
d.$once('clicked', id => { d.$once('clicked', id => {
res(id); res(id);
}); });
document.body.appendChild(d.$el); document.body.appendChild(d.$el);
}); });
} };

View File

@ -1,20 +1,19 @@
import OS from '../../mios';
import InputDialog from '../views/components/input-dialog.vue'; import InputDialog from '../views/components/input-dialog.vue';
export default function(opts) { export default (os: OS) => opts => {
return new Promise<string>((res, rej) => { return new Promise<string>((res, rej) => {
const o = opts || {}; const o = opts || {};
const d = new InputDialog({ const d = os.new(InputDialog, {
propsData: { title: o.title,
title: o.title, placeholder: o.placeholder,
placeholder: o.placeholder, default: o.default,
default: o.default, type: o.type || 'text',
type: o.type || 'text', allowEmpty: o.allowEmpty
allowEmpty: o.allowEmpty });
}
}).$mount();
d.$once('done', text => { d.$once('done', text => {
res(text); res(text);
}); });
document.body.appendChild(d.$el); document.body.appendChild(d.$el);
}); });
} };

View File

@ -1,10 +1,9 @@
import OS from '../../mios';
import Notification from '../views/components/ui-notification.vue'; import Notification from '../views/components/ui-notification.vue';
export default function(message) { export default (os: OS) => message => {
const vm = new Notification({ const vm = os.new(Notification, {
propsData: { message
message });
}
}).$mount();
document.body.appendChild(vm.$el); document.body.appendChild(vm.$el);
} };

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