Merge branch 'develop' into l10n_develop
This commit is contained in:
commit
4e11da98d9
171 changed files with 2193 additions and 14550 deletions
|
@ -138,3 +138,6 @@ drive:
|
||||||
|
|
||||||
# Clustering
|
# Clustering
|
||||||
# clusterLimit: 1
|
# clusterLimit: 1
|
||||||
|
|
||||||
|
# Summaly proxy
|
||||||
|
# summalyProxy: "http://example.com"
|
||||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,6 +5,16 @@ ChangeLog
|
||||||
|
|
||||||
This document describes breaking changes only.
|
This document describes breaking changes only.
|
||||||
|
|
||||||
|
8.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
起動する前に、`node cli/migration/8.0.0`してください。
|
||||||
|
|
||||||
|
Please run `node cli/migration/8.0.0` before launch.
|
||||||
|
|
||||||
|
|
||||||
7.0.0
|
7.0.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
const { default: User, deleteUser } = require('../built/models/user');
|
|
||||||
const { default: zip } = require('@prezzemolo/zip')
|
|
||||||
|
|
||||||
const migrate = async (user) => {
|
|
||||||
try {
|
|
||||||
await deleteUser(user._id);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const count = await User.count({
|
|
||||||
uri: /#/
|
|
||||||
});
|
|
||||||
|
|
||||||
const dop = 1
|
|
||||||
const idop = ((count - (count % dop)) / dop) + 1
|
|
||||||
|
|
||||||
return zip(
|
|
||||||
1,
|
|
||||||
async (time) => {
|
|
||||||
console.log(`${time} / ${idop}`)
|
|
||||||
const doc = await User.find({
|
|
||||||
uri: /#/
|
|
||||||
}, {
|
|
||||||
limit: dop, skip: time * dop
|
|
||||||
})
|
|
||||||
return Promise.all(doc.map(migrate))
|
|
||||||
},
|
|
||||||
idop
|
|
||||||
).then(a => {
|
|
||||||
const rv = []
|
|
||||||
a.forEach(e => rv.push(...e))
|
|
||||||
return rv
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main().then(console.dir).catch(console.error)
|
|
144
cli/migration/8.0.0.js
Normal file
144
cli/migration/8.0.0.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
const { default: Stats } = require('../../built/models/stats');
|
||||||
|
const { default: User } = require('../../built/models/user');
|
||||||
|
const { default: Note } = require('../../built/models/note');
|
||||||
|
const { default: DriveFile } = require('../../built/models/drive-file');
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = now.getMonth();
|
||||||
|
const d = now.getDate();
|
||||||
|
const h = now.getHours();
|
||||||
|
const date = new Date(y, m, d, h);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await Stats.update({}, {
|
||||||
|
$set: {
|
||||||
|
span: 'day'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
multi: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const localUsersCount = await User.count({
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteUsersCount = await User.count({
|
||||||
|
host: { $ne: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
const localNotesCount = await Note.count({
|
||||||
|
'_user.host': null
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteNotesCount = await Note.count({
|
||||||
|
'_user.host': { $ne: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
const localDriveFilesCount = await DriveFile.count({
|
||||||
|
'metadata._user.host': null
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteDriveFilesCount = await DriveFile.count({
|
||||||
|
'metadata._user.host': { $ne: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
const localDriveFilesSize = await DriveFile
|
||||||
|
.aggregate([{
|
||||||
|
$match: {
|
||||||
|
'metadata._user.host': null,
|
||||||
|
'metadata.deletedAt': { $exists: false }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$project: {
|
||||||
|
length: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
usage: { $sum: '$length' }
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
.then(aggregates => {
|
||||||
|
if (aggregates.length > 0) {
|
||||||
|
return aggregates[0].usage;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteDriveFilesSize = await DriveFile
|
||||||
|
.aggregate([{
|
||||||
|
$match: {
|
||||||
|
'metadata._user.host': { $ne: null },
|
||||||
|
'metadata.deletedAt': { $exists: false }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$project: {
|
||||||
|
length: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
usage: { $sum: '$length' }
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
.then(aggregates => {
|
||||||
|
if (aggregates.length > 0) {
|
||||||
|
return aggregates[0].usage;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Stats.insert({
|
||||||
|
date: date,
|
||||||
|
span: 'hour',
|
||||||
|
users: {
|
||||||
|
local: {
|
||||||
|
total: localUsersCount,
|
||||||
|
diff: 0
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
total: remoteUsersCount,
|
||||||
|
diff: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
local: {
|
||||||
|
total: localNotesCount,
|
||||||
|
diff: 0,
|
||||||
|
diffs: {
|
||||||
|
normal: 0,
|
||||||
|
reply: 0,
|
||||||
|
renote: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
total: remoteNotesCount,
|
||||||
|
diff: 0,
|
||||||
|
diffs: {
|
||||||
|
normal: 0,
|
||||||
|
reply: 0,
|
||||||
|
renote: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drive: {
|
||||||
|
local: {
|
||||||
|
totalCount: localDriveFilesCount,
|
||||||
|
totalSize: localDriveFilesSize,
|
||||||
|
diffCount: 0,
|
||||||
|
diffSize: 0
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
totalCount: remoteDriveFilesCount,
|
||||||
|
totalSize: remoteDriveFilesSize,
|
||||||
|
diffCount: 0,
|
||||||
|
diffSize: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('done');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
11
gulpfile.ts
11
gulpfile.ts
|
@ -59,7 +59,16 @@ gulp.task('build:copy:views', () =>
|
||||||
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
|
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('build:copy', ['build:copy:views'], () =>
|
// 互換性のため
|
||||||
|
gulp.task('build:copy:lang', () =>
|
||||||
|
gulp.src(['./built/client/assets/*.*-*.js'])
|
||||||
|
.pipe(rename(path => {
|
||||||
|
path.basename = path.basename.replace(/\-(.*)$/, '');
|
||||||
|
}))
|
||||||
|
.pipe(gulp.dest('./built/client/assets/'))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
|
||||||
gulp.src([
|
gulp.src([
|
||||||
'./build/Release/crypto_key.node',
|
'./build/Release/crypto_key.node',
|
||||||
'./src/const.json',
|
'./src/const.json',
|
||||||
|
|
1219
locales/ca.yml
1219
locales/ca.yml
File diff suppressed because it is too large
Load diff
1219
locales/de.yml
1219
locales/de.yml
File diff suppressed because it is too large
Load diff
1219
locales/en.yml
1219
locales/en.yml
File diff suppressed because it is too large
Load diff
1219
locales/es.yml
1219
locales/es.yml
File diff suppressed because it is too large
Load diff
1219
locales/fr.yml
1219
locales/fr.yml
File diff suppressed because it is too large
Load diff
|
@ -11,13 +11,13 @@ const loadLang = lang => yaml.safeLoad(
|
||||||
const native = loadLang('ja-JP');
|
const native = loadLang('ja-JP');
|
||||||
|
|
||||||
const langs = {
|
const langs = {
|
||||||
'de': loadLang('de'),
|
'de-DE': loadLang('de-DE'),
|
||||||
'en': loadLang('en'),
|
'en-US': loadLang('en-US'),
|
||||||
'fr': loadLang('fr'),
|
'fr-FR': loadLang('fr-FR'),
|
||||||
'ja': native,
|
'ja-JP': native,
|
||||||
'ja-KS': loadLang('ja-KS'),
|
'ja-KS': loadLang('ja-KS'),
|
||||||
'pl': loadLang('pl'),
|
'pl-PL': loadLang('pl-PL'),
|
||||||
'es': loadLang('es')
|
'es-ES': loadLang('es-ES')
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(langs).forEach(locale => {
|
Object.values(langs).forEach(locale => {
|
||||||
|
|
1219
locales/it.yml
1219
locales/it.yml
File diff suppressed because it is too large
Load diff
|
@ -456,6 +456,7 @@ desktop:
|
||||||
uploading-avatar: "新しいアバターをアップロードしています"
|
uploading-avatar: "新しいアバターをアップロードしています"
|
||||||
avatar-updated: "アバターを更新しました"
|
avatar-updated: "アバターを更新しました"
|
||||||
choose-avatar: "アバターにする画像を選択"
|
choose-avatar: "アバターにする画像を選択"
|
||||||
|
invalid-filetype: "この形式のファイルはサポートされていません"
|
||||||
|
|
||||||
desktop/views/components/activity.chart.vue:
|
desktop/views/components/activity.chart.vue:
|
||||||
total: "Black ... Total"
|
total: "Black ... Total"
|
||||||
|
@ -473,6 +474,25 @@ desktop/views/components/calendar.vue:
|
||||||
next: "次の月"
|
next: "次の月"
|
||||||
go: "クリックして時間遡行"
|
go: "クリックして時間遡行"
|
||||||
|
|
||||||
|
desktop/views/components/charts.vue:
|
||||||
|
title: "チャート"
|
||||||
|
per-day: "1日ごと"
|
||||||
|
per-hour: "1時間ごと"
|
||||||
|
notes: "投稿"
|
||||||
|
users: "ユーザー"
|
||||||
|
drive: "ドライブ"
|
||||||
|
charts:
|
||||||
|
notes: "投稿の増減 (統合)"
|
||||||
|
local-notes: "投稿の増減 (ローカル)"
|
||||||
|
remote-notes: "投稿の増減 (リモート)"
|
||||||
|
notes-total: "投稿の累計"
|
||||||
|
users: "ユーザーの増減"
|
||||||
|
users-total: "ユーザーの累計"
|
||||||
|
drive: "ドライブ使用量の増減"
|
||||||
|
drive-total: "ドライブ使用量の累計"
|
||||||
|
drive-files: "ドライブのファイル数の増減"
|
||||||
|
drive-files-total: "ドライブのファイル数の累計"
|
||||||
|
|
||||||
desktop/views/components/choose-file-from-drive-window.vue:
|
desktop/views/components/choose-file-from-drive-window.vue:
|
||||||
choose-file: "ファイル選択中"
|
choose-file: "ファイル選択中"
|
||||||
upload: "PCからドライブにファイルをアップロード"
|
upload: "PCからドライブにファイルをアップロード"
|
||||||
|
@ -713,6 +733,7 @@ desktop/views/components/settings.vue:
|
||||||
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
|
||||||
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
|
||||||
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
|
||||||
|
show-clock-on-header: "右上に時計を表示する"
|
||||||
show-reply-target: "リプライ先を表示する"
|
show-reply-target: "リプライ先を表示する"
|
||||||
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
|
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
|
||||||
show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
|
show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
|
||||||
|
@ -857,6 +878,7 @@ desktop/views/components/ui.header.account.vue:
|
||||||
lists: "リスト"
|
lists: "リスト"
|
||||||
follow-requests: "フォロー申請"
|
follow-requests: "フォロー申請"
|
||||||
customize: "ホームのカスタマイズ"
|
customize: "ホームのカスタマイズ"
|
||||||
|
admin: "管理"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
signout: "サインアウト"
|
signout: "サインアウト"
|
||||||
dark: "闇に飲まれる"
|
dark: "闇に飲まれる"
|
||||||
|
@ -914,8 +936,8 @@ desktop/views/pages/admin/admin.dashboard.vue:
|
||||||
dashboard: "ダッシュボード"
|
dashboard: "ダッシュボード"
|
||||||
all-users: "全てのユーザー"
|
all-users: "全てのユーザー"
|
||||||
original-users: "このインスタンスのユーザー"
|
original-users: "このインスタンスのユーザー"
|
||||||
all-notes: "全てのノート"
|
all-notes: "全ての投稿"
|
||||||
original-notes: "このインスタンスのノート"
|
original-notes: "このインスタンスの投稿"
|
||||||
invite: "招待"
|
invite: "招待"
|
||||||
|
|
||||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||||
|
@ -938,21 +960,6 @@ desktop/views/pages/admin/admin.unverify-user.vue:
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
|
||||||
desktop/views/pages/admin/admin.notes-chart.vue:
|
|
||||||
title: "投稿"
|
|
||||||
local: "ローカル"
|
|
||||||
remote: "リモート"
|
|
||||||
|
|
||||||
desktop/views/pages/admin/admin.users-chart.vue:
|
|
||||||
title: "ユーザー"
|
|
||||||
local: "ローカル"
|
|
||||||
remote: "リモート"
|
|
||||||
|
|
||||||
desktop/views/pages/admin/admin.drive-chart.vue:
|
|
||||||
title: "ドライブ"
|
|
||||||
local: "ローカル"
|
|
||||||
remote: "リモート"
|
|
||||||
|
|
||||||
desktop/views/pages/deck/deck.tl-column.vue:
|
desktop/views/pages/deck/deck.tl-column.vue:
|
||||||
is-media-only: "メディア投稿のみ"
|
is-media-only: "メディア投稿のみ"
|
||||||
is-media-view: "メディアビュー"
|
is-media-view: "メディアビュー"
|
||||||
|
@ -963,6 +970,12 @@ desktop/views/pages/deck/deck.note.vue:
|
||||||
private: "この投稿は非公開です"
|
private: "この投稿は非公開です"
|
||||||
deleted: "この投稿は削除されました"
|
deleted: "この投稿は削除されました"
|
||||||
|
|
||||||
|
desktop/views/pages/stats/stats.vue:
|
||||||
|
all-users: "全てのユーザー"
|
||||||
|
original-users: "このインスタンスのユーザー"
|
||||||
|
all-notes: "全ての投稿"
|
||||||
|
original-notes: "このインスタンスの投稿"
|
||||||
|
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
|
@ -1214,6 +1227,7 @@ mobile/views/components/ui.nav.vue:
|
||||||
game: "ゲーム"
|
game: "ゲーム"
|
||||||
darkmode: "ダークモード"
|
darkmode: "ダークモード"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
|
admin: "管理"
|
||||||
about: "Misskeyについて"
|
about: "Misskeyについて"
|
||||||
|
|
||||||
mobile/views/components/user-timeline.vue:
|
mobile/views/components/user-timeline.vue:
|
||||||
|
@ -1355,6 +1369,8 @@ mobile/views/pages/settings.vue:
|
||||||
update-available-desc: "ページを再度読み込みすると更新が適用されます。"
|
update-available-desc: "ページを再度読み込みすると更新が適用されます。"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
signout: "サインアウト"
|
signout: "サインアウト"
|
||||||
|
sound: "サウンド"
|
||||||
|
enableSounds: "サウンドを有効にする"
|
||||||
|
|
||||||
mobile/views/pages/user.vue:
|
mobile/views/pages/user.vue:
|
||||||
follows-you: "フォローされています"
|
follows-you: "フォローされています"
|
||||||
|
|
1219
locales/ko.yml
1219
locales/ko.yml
File diff suppressed because it is too large
Load diff
1219
locales/pl.yml
1219
locales/pl.yml
File diff suppressed because it is too large
Load diff
1219
locales/pt.yml
1219
locales/pt.yml
File diff suppressed because it is too large
Load diff
1219
locales/ru.yml
1219
locales/ru.yml
File diff suppressed because it is too large
Load diff
1219
locales/zh.yml
1219
locales/zh.yml
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "7.3.0",
|
"version": "8.15.0",
|
||||||
"clientVersion": "1.0.8741",
|
"clientVersion": "1.0.9031",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"@types/debug": "0.0.30",
|
"@types/debug": "0.0.30",
|
||||||
"@types/deep-equal": "1.0.1",
|
"@types/deep-equal": "1.0.1",
|
||||||
"@types/double-ended-queue": "2.1.0",
|
"@types/double-ended-queue": "2.1.0",
|
||||||
"@types/elasticsearch": "5.0.25",
|
"@types/elasticsearch": "5.0.26",
|
||||||
"@types/file-type": "5.2.1",
|
"@types/file-type": "5.2.1",
|
||||||
"@types/gulp": "3.8.36",
|
"@types/gulp": "3.8.36",
|
||||||
"@types/gulp-htmlmin": "1.3.32",
|
"@types/gulp-htmlmin": "1.3.32",
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
"@types/mocha": "5.2.3",
|
"@types/mocha": "5.2.3",
|
||||||
"@types/mongodb": "3.1.4",
|
"@types/mongodb": "3.1.4",
|
||||||
"@types/ms": "0.7.30",
|
"@types/ms": "0.7.30",
|
||||||
"@types/node": "10.7.1",
|
"@types/node": "10.9.3",
|
||||||
"@types/portscanner": "2.1.0",
|
"@types/portscanner": "2.1.0",
|
||||||
"@types/pug": "2.0.4",
|
"@types/pug": "2.0.4",
|
||||||
"@types/qrcode": "1.2.0",
|
"@types/qrcode": "1.2.0",
|
||||||
|
@ -70,14 +70,14 @@
|
||||||
"@types/request-promise-native": "1.0.15",
|
"@types/request-promise-native": "1.0.15",
|
||||||
"@types/rimraf": "2.0.2",
|
"@types/rimraf": "2.0.2",
|
||||||
"@types/seedrandom": "2.4.27",
|
"@types/seedrandom": "2.4.27",
|
||||||
"@types/sharp": "0.17.9",
|
"@types/sharp": "0.17.10",
|
||||||
"@types/showdown": "1.7.5",
|
"@types/showdown": "1.7.5",
|
||||||
"@types/single-line-log": "1.1.0",
|
"@types/single-line-log": "1.1.0",
|
||||||
"@types/speakeasy": "2.0.2",
|
"@types/speakeasy": "2.0.2",
|
||||||
"@types/systeminformation": "3.23.0",
|
"@types/systeminformation": "3.23.0",
|
||||||
"@types/tmp": "0.0.33",
|
"@types/tmp": "0.0.33",
|
||||||
"@types/uuid": "3.4.3",
|
"@types/uuid": "3.4.3",
|
||||||
"@types/webpack": "4.4.10",
|
"@types/webpack": "4.4.11",
|
||||||
"@types/webpack-stream": "3.2.10",
|
"@types/webpack-stream": "3.2.10",
|
||||||
"@types/websocket": "0.0.39",
|
"@types/websocket": "0.0.39",
|
||||||
"@types/ws": "6.0.0",
|
"@types/ws": "6.0.0",
|
||||||
|
@ -89,6 +89,7 @@
|
||||||
"bootstrap-vue": "2.0.0-rc.11",
|
"bootstrap-vue": "2.0.0-rc.11",
|
||||||
"cafy": "11.3.0",
|
"cafy": "11.3.0",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.1",
|
||||||
|
"chart.js": "2.7.2",
|
||||||
"commander": "2.17.1",
|
"commander": "2.17.1",
|
||||||
"crc-32": "1.2.0",
|
"crc-32": "1.2.0",
|
||||||
"css-loader": "1.0.0",
|
"css-loader": "1.0.0",
|
||||||
|
@ -149,6 +150,7 @@
|
||||||
"loader-utils": "1.1.0",
|
"loader-utils": "1.1.0",
|
||||||
"lodash.assign": "4.2.0",
|
"lodash.assign": "4.2.0",
|
||||||
"mecab-async": "0.1.2",
|
"mecab-async": "0.1.2",
|
||||||
|
"merge-options": "1.0.1",
|
||||||
"minio": "7.0.0",
|
"minio": "7.0.0",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"mocha": "5.2.0",
|
"mocha": "5.2.0",
|
||||||
|
@ -156,7 +158,7 @@
|
||||||
"mongodb": "3.1.1",
|
"mongodb": "3.1.1",
|
||||||
"monk": "6.0.6",
|
"monk": "6.0.6",
|
||||||
"ms": "2.1.1",
|
"ms": "2.1.1",
|
||||||
"nan": "2.10.0",
|
"nan": "2.11.0",
|
||||||
"nested-property": "0.0.7",
|
"nested-property": "0.0.7",
|
||||||
"node-sass": "4.9.3",
|
"node-sass": "4.9.3",
|
||||||
"node-sass-json-importer": "3.3.1",
|
"node-sass-json-importer": "3.3.1",
|
||||||
|
@ -188,11 +190,11 @@
|
||||||
"single-line-log": "1.1.2",
|
"single-line-log": "1.1.2",
|
||||||
"speakeasy": "2.0.0",
|
"speakeasy": "2.0.0",
|
||||||
"stringz": "1.0.0",
|
"stringz": "1.0.0",
|
||||||
"style-loader": "0.22.1",
|
"style-loader": "0.23.0",
|
||||||
"stylus": "0.54.5",
|
"stylus": "0.54.5",
|
||||||
"stylus-loader": "3.0.2",
|
"stylus-loader": "3.0.2",
|
||||||
"summaly": "2.1.4",
|
"summaly": "2.1.4",
|
||||||
"systeminformation": "3.42.9",
|
"systeminformation": "3.44.2",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"tmp": "0.0.33",
|
"tmp": "0.0.33",
|
||||||
|
@ -206,10 +208,11 @@
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"v-animate-css": "0.0.2",
|
"v-animate-css": "0.0.2",
|
||||||
"vue": "2.5.17",
|
"vue": "2.5.17",
|
||||||
|
"vue-chartjs": "3.4.0",
|
||||||
"vue-cropperjs": "2.2.1",
|
"vue-cropperjs": "2.2.1",
|
||||||
"vue-js-modal": "1.3.17",
|
"vue-js-modal": "1.3.23",
|
||||||
"vue-json-tree-view": "2.1.4",
|
"vue-json-tree-view": "2.1.4",
|
||||||
"vue-loader": "15.4.0",
|
"vue-loader": "15.4.1",
|
||||||
"vue-router": "3.0.1",
|
"vue-router": "3.0.1",
|
||||||
"vue-style-loader": "4.1.2",
|
"vue-style-loader": "4.1.2",
|
||||||
"vue-template-compiler": "2.5.17",
|
"vue-template-compiler": "2.5.17",
|
||||||
|
@ -218,7 +221,7 @@
|
||||||
"vuex-persistedstate": "2.5.4",
|
"vuex-persistedstate": "2.5.4",
|
||||||
"web-push": "3.3.2",
|
"web-push": "3.3.2",
|
||||||
"webfinger.js": "2.6.6",
|
"webfinger.js": "2.6.6",
|
||||||
"webpack": "4.17.0",
|
"webpack": "4.17.1",
|
||||||
"webpack-cli": "3.1.0",
|
"webpack-cli": "3.1.0",
|
||||||
"websocket": "1.0.26",
|
"websocket": "1.0.26",
|
||||||
"ws": "6.0.0",
|
"ws": "6.0.0",
|
||||||
|
|
|
@ -38,15 +38,22 @@
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Detect the user language
|
//#region Detect the user language
|
||||||
let lang = navigator.language;
|
let lang = null;
|
||||||
|
|
||||||
if (!LANGS.includes(lang)) lang = lang.split('-')[0];
|
if (LANGS.includes(navigator.language)) {
|
||||||
|
lang = navigator.language;
|
||||||
|
} else {
|
||||||
|
lang = LANGS.find(x => x.split('-')[0] == navigator.language);
|
||||||
|
|
||||||
// The default language is English
|
if (lang == null) {
|
||||||
if (!LANGS.includes(lang)) lang = 'en';
|
// Fallback
|
||||||
|
lang = 'en-US';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (settings) {
|
if (settings && settings.device.lang &&
|
||||||
if (settings.device.lang) lang = settings.device.lang;
|
LANGS.includes(settings.device.lang)) {
|
||||||
|
lang = settings.device.lang;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
(this as any).os.getMeta().then(meta => {
|
(this as any).os.getMeta().then(meta => {
|
||||||
if (meta.repositoryUrl) this.repositoryUrl = meta.repositoryUrl;
|
if (meta.maintainer.repository_url) this.repositoryUrl = meta.maintainer.repository_url;
|
||||||
if (meta.feedbackUrl) this.feedbackUrl = meta.feedbackUrl;
|
if (meta.maintainer.feedback_url) this.feedbackUrl = meta.maintainer.feedback_url;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,69 +28,8 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { url as misskeyUrl } from '../../../config';
|
import { url as misskeyUrl } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
// THIS IS THE WHITELIST FOR THE EMBED PLAYER
|
||||||
props: {
|
const whiteList = [
|
||||||
url: {
|
|
||||||
type: String,
|
|
||||||
require: true
|
|
||||||
},
|
|
||||||
detail: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
fetching: true,
|
|
||||||
title: null,
|
|
||||||
description: null,
|
|
||||||
thumbnail: null,
|
|
||||||
icon: null,
|
|
||||||
sitename: null,
|
|
||||||
player: {
|
|
||||||
url: null,
|
|
||||||
width: null,
|
|
||||||
height: null
|
|
||||||
},
|
|
||||||
tweetUrl: null,
|
|
||||||
misskeyUrl
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const url = new URL(this.url);
|
|
||||||
|
|
||||||
if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) {
|
|
||||||
this.tweetUrl = url;
|
|
||||||
const twttr = (window as any).twttr || {};
|
|
||||||
const loadTweet = () => twttr.widgets.load(this.$refs.tweet);
|
|
||||||
|
|
||||||
if (twttr.widgets) {
|
|
||||||
Vue.nextTick(loadTweet);
|
|
||||||
} else {
|
|
||||||
const wjsId = 'twitter-wjs';
|
|
||||||
if (!document.getElementById(wjsId)) {
|
|
||||||
const head = document.getElementsByTagName('head')[0];
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.setAttribute('id', wjsId);
|
|
||||||
script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
|
|
||||||
head.appendChild(script);
|
|
||||||
}
|
|
||||||
twttr.ready = loadTweet;
|
|
||||||
(window as any).twttr = twttr;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
|
|
||||||
res.json().then(info => {
|
|
||||||
if (info.url != null) {
|
|
||||||
this.title = info.title;
|
|
||||||
this.description = info.description;
|
|
||||||
this.thumbnail = info.thumbnail;
|
|
||||||
this.icon = info.icon;
|
|
||||||
this.sitename = info.sitename;
|
|
||||||
this.fetching = false;
|
|
||||||
if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER
|
|
||||||
'afreecatv.com',
|
'afreecatv.com',
|
||||||
'aparat.com',
|
'aparat.com',
|
||||||
'applemusic.com',
|
'applemusic.com',
|
||||||
|
@ -166,17 +105,85 @@ export default Vue.extend({
|
||||||
'web.tv',
|
'web.tv',
|
||||||
'youtube.com',
|
'youtube.com',
|
||||||
'youtu.be'
|
'youtu.be'
|
||||||
].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`)))
|
];
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
require: true
|
||||||
|
},
|
||||||
|
|
||||||
|
detail: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fetching: true,
|
||||||
|
title: null,
|
||||||
|
description: null,
|
||||||
|
thumbnail: null,
|
||||||
|
icon: null,
|
||||||
|
sitename: null,
|
||||||
|
player: {
|
||||||
|
url: null,
|
||||||
|
width: null,
|
||||||
|
height: null
|
||||||
|
},
|
||||||
|
tweetUrl: null,
|
||||||
|
misskeyUrl
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
const url = new URL(this.url);
|
||||||
|
|
||||||
|
if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) {
|
||||||
|
this.tweetUrl = url;
|
||||||
|
const twttr = (window as any).twttr || {};
|
||||||
|
const loadTweet = () => twttr.widgets.load(this.$refs.tweet);
|
||||||
|
|
||||||
|
if (twttr.widgets) {
|
||||||
|
Vue.nextTick(loadTweet);
|
||||||
|
} else {
|
||||||
|
const wjsId = 'twitter-wjs';
|
||||||
|
if (!document.getElementById(wjsId)) {
|
||||||
|
const head = document.getElementsByTagName('head')[0];
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.setAttribute('id', wjsId);
|
||||||
|
script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
|
||||||
|
head.appendChild(script);
|
||||||
|
}
|
||||||
|
twttr.ready = loadTweet;
|
||||||
|
(window as any).twttr = twttr;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
|
||||||
|
res.json().then(info => {
|
||||||
|
if (info.url == null) return;
|
||||||
|
this.title = info.title;
|
||||||
|
this.description = info.description;
|
||||||
|
this.thumbnail = info.thumbnail;
|
||||||
|
this.icon = info.icon;
|
||||||
|
this.sitename = info.sitename;
|
||||||
|
this.fetching = false;
|
||||||
|
if (whiteList.some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) {
|
||||||
this.player = info.player;
|
this.player = info.player;
|
||||||
} // info.url
|
}
|
||||||
}) // json
|
})
|
||||||
}); // fetch
|
});
|
||||||
} // created
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.twitter
|
.player
|
||||||
position relative
|
position relative
|
||||||
width 100%
|
width 100%
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
Vue.filter('bytes', (v, digits = 0) => {
|
Vue.filter('bytes', (v, digits = 0) => {
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
if (v == 0) return '0Byte';
|
if (v == 0) return '0';
|
||||||
|
const isMinus = v < 0;
|
||||||
|
if (isMinus) v = -v;
|
||||||
const i = Math.floor(Math.log(v) / Math.log(1024));
|
const i = Math.floor(Math.log(v) / Math.log(1024));
|
||||||
return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
|
return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="mkw-donation" :data-mobile="platform == 'mobile'">
|
<div class="mkw-donation" :data-mobile="platform == 'mobile'">
|
||||||
<article>
|
<article>
|
||||||
<h1>%fa:heart%%i18n:@title%</h1>
|
<h1>%fa:heart%%i18n:@title%</h1>
|
||||||
<p>
|
<p v-if="meta">
|
||||||
{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
|
{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
|
||||||
<a href="https://syuilo.com">@syuilo</a>
|
<a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a>
|
||||||
{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
|
{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
@ -15,6 +15,17 @@
|
||||||
import define from '../../../common/define-widget';
|
import define from '../../../common/define-widget';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'donation'
|
name: 'donation'
|
||||||
|
}).extend({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
meta: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
(this as any).os.getMeta().then(meta => {
|
||||||
|
this.meta = meta;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,21 @@ import { apiUrl } from '../../config';
|
||||||
import CropWindow from '../views/components/crop-window.vue';
|
import CropWindow from '../views/components/crop-window.vue';
|
||||||
import ProgressDialog from '../views/components/progress-dialog.vue';
|
import ProgressDialog from '../views/components/progress-dialog.vue';
|
||||||
|
|
||||||
export default (os: OS) => (cb, file = null) => {
|
export default (os: OS) => {
|
||||||
const fileSelected = file => {
|
|
||||||
|
const cropImage = file => new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
|
||||||
|
if (!regex.test(file.name) ) {
|
||||||
|
os.apis.dialog({
|
||||||
|
title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
|
||||||
|
text: null,
|
||||||
|
actions: [{
|
||||||
|
text: '%i18n:common.got-it%'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
const w = os.new(CropWindow, {
|
const w = os.new(CropWindow, {
|
||||||
image: file,
|
image: file,
|
||||||
|
@ -19,27 +32,29 @@ export default (os: OS) => (cb, file = null) => {
|
||||||
|
|
||||||
os.api('drive/folders/find', {
|
os.api('drive/folders/find', {
|
||||||
name: '%i18n:desktop.avatar%'
|
name: '%i18n:desktop.avatar%'
|
||||||
}).then(iconFolder => {
|
}).then(avatarFolder => {
|
||||||
if (iconFolder.length === 0) {
|
if (avatarFolder.length === 0) {
|
||||||
os.api('drive/folders/create', {
|
os.api('drive/folders/create', {
|
||||||
name: '%i18n:desktop.avatar%'
|
name: '%i18n:desktop.avatar%'
|
||||||
}).then(iconFolder => {
|
}).then(iconFolder => {
|
||||||
upload(data, iconFolder);
|
resolve(upload(data, iconFolder));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
upload(data, iconFolder[0]);
|
resolve(upload(data, avatarFolder[0]));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
w.$once('skipped', () => {
|
w.$once('skipped', () => {
|
||||||
set(file);
|
resolve(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(w.$el);
|
w.$once('cancelled', reject);
|
||||||
};
|
|
||||||
|
|
||||||
const upload = (data, folder) => {
|
document.body.appendChild(w.$el);
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = (data, folder) => new Promise((resolve, reject) => {
|
||||||
const dialog = os.new(ProgressDialog, {
|
const dialog = os.new(ProgressDialog, {
|
||||||
title: '%i18n:desktop.uploading-avatar%'
|
title: '%i18n:desktop.uploading-avatar%'
|
||||||
});
|
});
|
||||||
|
@ -52,18 +67,19 @@ export default (os: OS) => (cb, file = null) => {
|
||||||
xhr.onload = e => {
|
xhr.onload = e => {
|
||||||
const file = JSON.parse((e.target as any).response);
|
const file = JSON.parse((e.target as any).response);
|
||||||
(dialog as any).close();
|
(dialog as any).close();
|
||||||
set(file);
|
resolve(file);
|
||||||
};
|
};
|
||||||
|
xhr.onerror = reject;
|
||||||
|
|
||||||
xhr.upload.onprogress = e => {
|
xhr.upload.onprogress = e => {
|
||||||
if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
|
if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.send(data);
|
xhr.send(data);
|
||||||
};
|
});
|
||||||
|
|
||||||
const set = file => {
|
const setAvatar = file => {
|
||||||
os.api('i/update', {
|
return os.api('i/update', {
|
||||||
avatarId: file.id
|
avatarId: file.id
|
||||||
}).then(i => {
|
}).then(i => {
|
||||||
os.store.commit('updateIKeyValue', {
|
os.store.commit('updateIKeyValue', {
|
||||||
|
@ -83,18 +99,21 @@ export default (os: OS) => (cb, file = null) => {
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cb) cb(i);
|
return i;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file) {
|
return (file = null) => {
|
||||||
fileSelected(file);
|
const selectedFile = file
|
||||||
} else {
|
? Promise.resolve(file)
|
||||||
os.apis.chooseDriveFile({
|
: os.apis.chooseDriveFile({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
title: '%fa:image% %i18n:desktop.choose-avatar%'
|
title: '%fa:image% %i18n:desktop.choose-avatar%'
|
||||||
}).then(file => {
|
|
||||||
fileSelected(file);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return selectedFile
|
||||||
|
.then(cropImage)
|
||||||
|
.then(setAvatar)
|
||||||
|
.catch(err => err && console.warn(err));
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,19 @@ import ProgressDialog from '../views/components/progress-dialog.vue';
|
||||||
export default (os: OS) => {
|
export default (os: OS) => {
|
||||||
|
|
||||||
const cropImage = file => new Promise((resolve, reject) => {
|
const cropImage = file => new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
|
||||||
|
if (!regex.test(file.name) ) {
|
||||||
|
os.apis.dialog({
|
||||||
|
title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
|
||||||
|
text: null,
|
||||||
|
actions: [{
|
||||||
|
text: '%i18n:common.got-it%'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
const w = os.new(CropWindow, {
|
const w = os.new(CropWindow, {
|
||||||
image: file,
|
image: file,
|
||||||
title: '%i18n:desktop.banner-crop-title%',
|
title: '%i18n:desktop.banner-crop-title%',
|
||||||
|
|
|
@ -25,6 +25,7 @@ import updateBanner from './api/update-banner';
|
||||||
import MkIndex from './views/pages/index.vue';
|
import MkIndex from './views/pages/index.vue';
|
||||||
import MkDeck from './views/pages/deck/deck.vue';
|
import MkDeck from './views/pages/deck/deck.vue';
|
||||||
import MkAdmin from './views/pages/admin/admin.vue';
|
import MkAdmin from './views/pages/admin/admin.vue';
|
||||||
|
import MkStats from './views/pages/stats/stats.vue';
|
||||||
import MkUser from './views/pages/user/user.vue';
|
import MkUser from './views/pages/user/user.vue';
|
||||||
import MkFavorites from './views/pages/favorites.vue';
|
import MkFavorites from './views/pages/favorites.vue';
|
||||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||||
|
@ -57,6 +58,7 @@ init(async (launch) => {
|
||||||
{ path: '/', name: 'index', component: MkIndex },
|
{ path: '/', name: 'index', component: MkIndex },
|
||||||
{ path: '/deck', name: 'deck', component: MkDeck },
|
{ path: '/deck', name: 'deck', component: MkDeck },
|
||||||
{ path: '/admin', name: 'admin', component: MkAdmin },
|
{ path: '/admin', name: 'admin', component: MkAdmin },
|
||||||
|
{ path: '/stats', name: 'stats', component: MkStats },
|
||||||
{ path: '/i/customize-home', component: MkHomeCustomize },
|
{ path: '/i/customize-home', component: MkHomeCustomize },
|
||||||
{ path: '/i/favorites', component: MkFavorites },
|
{ path: '/i/favorites', component: MkFavorites },
|
||||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||||
|
@ -94,7 +96,7 @@ init(async (launch) => {
|
||||||
/**
|
/**
|
||||||
* Init Notification
|
* Init Notification
|
||||||
*/
|
*/
|
||||||
if ('Notification' in window) {
|
if ('Notification' in window && os.store.getters.isSignedIn) {
|
||||||
// 許可を得ていなかったらリクエスト
|
// 許可を得ていなかったらリクエスト
|
||||||
if ((Notification as any).permission == 'default') {
|
if ((Notification as any).permission == 'default') {
|
||||||
await Notification.requestPermission();
|
await Notification.requestPermission();
|
||||||
|
|
42
src/client/app/desktop/views/components/charts.chart.ts
Normal file
42
src/client/app/desktop/views/components/charts.chart.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { Line } from 'vue-chartjs';
|
||||||
|
import * as mergeOptions from 'merge-options';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
extends: Line,
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
opts: {
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data() {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
render() {
|
||||||
|
this.renderChart(this.data, mergeOptions({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
distribution: 'series'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'x',
|
||||||
|
position: 'nearest'
|
||||||
|
}
|
||||||
|
}, this.opts || {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
587
src/client/app/desktop/views/components/charts.vue
Normal file
587
src/client/app/desktop/views/components/charts.vue
Normal file
|
@ -0,0 +1,587 @@
|
||||||
|
<template>
|
||||||
|
<div class="gkgckalzgidaygcxnugepioremxvxvpt">
|
||||||
|
<header>
|
||||||
|
<b>%i18n:@title%:</b>
|
||||||
|
<select v-model="chartType">
|
||||||
|
<optgroup label="%i18n:@users%">
|
||||||
|
<option value="users">%i18n:@charts.users%</option>
|
||||||
|
<option value="users-total">%i18n:@charts.users-total%</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="%i18n:@notes%">
|
||||||
|
<option value="notes">%i18n:@charts.notes%</option>
|
||||||
|
<option value="local-notes">%i18n:@charts.local-notes%</option>
|
||||||
|
<option value="remote-notes">%i18n:@charts.remote-notes%</option>
|
||||||
|
<option value="notes-total">%i18n:@charts.notes-total%</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="%i18n:@drive%">
|
||||||
|
<option value="drive-files">%i18n:@charts.drive-files%</option>
|
||||||
|
<option value="drive-files-total">%i18n:@charts.drive-files-total%</option>
|
||||||
|
<option value="drive">%i18n:@charts.drive%</option>
|
||||||
|
<option value="drive-total">%i18n:@charts.drive-total%</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<div>
|
||||||
|
<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<x-chart v-if="chart" :data="data[0]" :opts="data[1]"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import XChart from './charts.chart.ts';
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
local: 'rgb(246, 88, 79)',
|
||||||
|
remote: 'rgb(65, 221, 222)',
|
||||||
|
|
||||||
|
localPlus: 'rgb(52, 178, 118)',
|
||||||
|
remotePlus: 'rgb(158, 255, 209)',
|
||||||
|
localMinus: 'rgb(255, 97, 74)',
|
||||||
|
remoteMinus: 'rgb(255, 149, 134)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgba = (color: string): string => {
|
||||||
|
return color.replace('rgb', 'rgba').replace(')', ', 0.1)');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
XChart
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null,
|
||||||
|
chartType: 'notes',
|
||||||
|
span: 'hour'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
data(): any {
|
||||||
|
if (this.chart == null) return null;
|
||||||
|
switch (this.chartType) {
|
||||||
|
case 'users': return this.usersChart(false);
|
||||||
|
case 'users-total': return this.usersChart(true);
|
||||||
|
case 'notes': return this.notesChart('combined');
|
||||||
|
case 'local-notes': return this.notesChart('local');
|
||||||
|
case 'remote-notes': return this.notesChart('remote');
|
||||||
|
case 'notes-total': return this.notesTotalChart();
|
||||||
|
case 'drive': return this.driveChart();
|
||||||
|
case 'drive-total': return this.driveTotalChart();
|
||||||
|
case 'drive-files': return this.driveFilesChart();
|
||||||
|
case 'drive-files-total': return this.driveFilesTotalChart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stats(): any[] {
|
||||||
|
return (
|
||||||
|
this.span == 'day' ? this.chart.perDay :
|
||||||
|
this.span == 'hour' ? this.chart.perHour :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
(this as any).api('chart', {
|
||||||
|
limit: 32
|
||||||
|
}).then(chart => {
|
||||||
|
this.chart = chart;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
notesChart(type: string): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
normal: type == 'local' ? x.notes.local.diffs.normal : type == 'remote' ? x.notes.remote.diffs.normal : x.notes.local.diffs.normal + x.notes.remote.diffs.normal,
|
||||||
|
reply: type == 'local' ? x.notes.local.diffs.reply : type == 'remote' ? x.notes.remote.diffs.reply : x.notes.local.diffs.reply + x.notes.remote.diffs.reply,
|
||||||
|
renote: type == 'local' ? x.notes.local.diffs.renote : type == 'remote' ? x.notes.remote.diffs.renote : x.notes.local.diffs.renote + x.notes.remote.diffs.renote,
|
||||||
|
all: type == 'local' ? (x.notes.local.inc + -x.notes.local.dec) : type == 'remote' ? (x.notes.remote.inc + -x.notes.remote.dec) : (x.notes.local.inc + -x.notes.local.dec) + (x.notes.remote.inc + -x.notes.remote.dec)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'All',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.all }))
|
||||||
|
}, {
|
||||||
|
label: 'Renotes',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(161, 222, 65, 0.1)',
|
||||||
|
borderColor: '#a1de41',
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.renote }))
|
||||||
|
}, {
|
||||||
|
label: 'Replies',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(247, 121, 108, 0.1)',
|
||||||
|
borderColor: '#f7796c',
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.reply }))
|
||||||
|
}, {
|
||||||
|
label: 'Normal',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(65, 221, 222, 0.1)',
|
||||||
|
borderColor: '#41ddde',
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.normal }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('number')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
notesTotalChart(): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
localCount: x.notes.local.total,
|
||||||
|
remoteCount: x.notes.remote.total
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'Combined',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount }))
|
||||||
|
}, {
|
||||||
|
label: 'Local',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.local),
|
||||||
|
borderColor: colors.local,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localCount }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remote),
|
||||||
|
borderColor: colors.remote,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteCount }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('number')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
usersChart(total: boolean): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
localCount: total ? x.users.local.total : (x.users.local.inc + -x.users.local.dec),
|
||||||
|
remoteCount: total ? x.users.remote.total : (x.users.remote.inc + -x.users.remote.dec)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'Combined',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount }))
|
||||||
|
}, {
|
||||||
|
label: 'Local',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.local),
|
||||||
|
borderColor: colors.local,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localCount }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remote),
|
||||||
|
borderColor: colors.remote,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteCount }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('number')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
driveChart(): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
localInc: x.drive.local.incSize,
|
||||||
|
localDec: -x.drive.local.decSize,
|
||||||
|
remoteInc: x.drive.remote.incSize,
|
||||||
|
remoteDec: -x.drive.remote.decSize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'All',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec }))
|
||||||
|
}, {
|
||||||
|
label: 'Local +',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.localPlus),
|
||||||
|
borderColor: colors.localPlus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localInc }))
|
||||||
|
}, {
|
||||||
|
label: 'Local -',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.localMinus),
|
||||||
|
borderColor: colors.localMinus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localDec }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote +',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remotePlus),
|
||||||
|
borderColor: colors.remotePlus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteInc }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote -',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remoteMinus),
|
||||||
|
borderColor: colors.remoteMinus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteDec }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('bytes')(value, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
driveTotalChart(): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
localSize: x.drive.local.totalSize,
|
||||||
|
remoteSize: x.drive.remote.totalSize
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'Combined',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteSize + x.localSize }))
|
||||||
|
}, {
|
||||||
|
label: 'Local',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.local),
|
||||||
|
borderColor: colors.local,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localSize }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remote),
|
||||||
|
borderColor: colors.remote,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteSize }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('bytes')(value, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
driveFilesChart(): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
localInc: x.drive.local.incCount,
|
||||||
|
localDec: -x.drive.local.decCount,
|
||||||
|
remoteInc: x.drive.remote.incCount,
|
||||||
|
remoteDec: -x.drive.remote.decCount
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'All',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec }))
|
||||||
|
}, {
|
||||||
|
label: 'Local +',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.localPlus),
|
||||||
|
borderColor: colors.localPlus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localInc }))
|
||||||
|
}, {
|
||||||
|
label: 'Local -',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.localMinus),
|
||||||
|
borderColor: colors.localMinus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localDec }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote +',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remotePlus),
|
||||||
|
borderColor: colors.remotePlus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteInc }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote -',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remoteMinus),
|
||||||
|
borderColor: colors.remoteMinus,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteDec }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('number')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
driveFilesTotalChart(): any {
|
||||||
|
const data = this.stats.slice().reverse().map(x => ({
|
||||||
|
date: new Date(x.date),
|
||||||
|
localCount: x.drive.local.totalCount,
|
||||||
|
remoteCount: x.drive.remote.totalCount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [{
|
||||||
|
datasets: [{
|
||||||
|
label: 'Combined',
|
||||||
|
fill: false,
|
||||||
|
borderColor: '#555',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [4, 4],
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localCount + x.remoteCount }))
|
||||||
|
}, {
|
||||||
|
label: 'Local',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.local),
|
||||||
|
borderColor: colors.local,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.localCount }))
|
||||||
|
}, {
|
||||||
|
label: 'Remote',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: rgba(colors.remote),
|
||||||
|
borderColor: colors.remote,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
lineTension: 0,
|
||||||
|
data: data.map(x => ({ t: x.date, y: x.remoteCount }))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
callback: value => {
|
||||||
|
return Vue.filter('number')(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
.gkgckalzgidaygcxnugepioremxvxvpt
|
||||||
|
padding 32px
|
||||||
|
background #fff
|
||||||
|
box-shadow 0 2px 8px rgba(#000, 0.1)
|
||||||
|
|
||||||
|
*
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
> header
|
||||||
|
display flex
|
||||||
|
margin 0 0 1em 0
|
||||||
|
padding 0 0 8px 0
|
||||||
|
font-size 1em
|
||||||
|
color #555
|
||||||
|
border-bottom solid 1px #eee
|
||||||
|
|
||||||
|
> b
|
||||||
|
margin-right 8px
|
||||||
|
|
||||||
|
> *:last-child
|
||||||
|
margin-left auto
|
||||||
|
|
||||||
|
*
|
||||||
|
&:not(.active)
|
||||||
|
color $theme-color
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
> div
|
||||||
|
> *
|
||||||
|
display block
|
||||||
|
height 320px
|
||||||
|
|
||||||
|
</style>
|
|
@ -47,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p"/>
|
<mk-poll v-if="p.poll" :note="p"/>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
||||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||||
<div class="map" v-if="p.geo" ref="map"></div>
|
<div class="map" v-if="p.geo" ref="map"></div>
|
||||||
<div class="renote" v-if="p.renote">
|
<div class="renote" v-if="p.renote">
|
||||||
<mk-note-preview :note="p.renote"/>
|
<mk-note-preview :note="p.renote"/>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<mk-media-list :media-list="p.media"/>
|
<mk-media-list :media-list="p.media"/>
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
||||||
<div class="map" v-if="p.geo" ref="map"></div>
|
<div class="map" v-if="p.geo" ref="map"></div>
|
||||||
<div class="renote" v-if="p.renote">
|
<div class="renote" v-if="p.renote">
|
||||||
<mk-note-preview :note="p.renote"/>
|
<mk-note-preview :note="p.renote"/>
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
</div>
|
</div>
|
||||||
<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
|
<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
|
||||||
<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
|
<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
|
||||||
|
<mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/>
|
||||||
<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
|
<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
|
||||||
<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
|
<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
|
||||||
<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
|
<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
|
||||||
|
@ -333,6 +334,12 @@ export default Vue.extend({
|
||||||
value: v
|
value: v
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onChangeShowClockOnHeader(v) {
|
||||||
|
this.$store.dispatch('settings/set', {
|
||||||
|
key: 'showClockOnHeader',
|
||||||
|
value: v
|
||||||
|
});
|
||||||
|
},
|
||||||
onChangeShowReplyTarget(v) {
|
onChangeShowReplyTarget(v) {
|
||||||
this.$store.dispatch('settings/set', {
|
this.$store.dispatch('settings/set', {
|
||||||
key: 'showReplyTarget',
|
key: 'showReplyTarget',
|
||||||
|
|
|
@ -30,10 +30,8 @@
|
||||||
<li @click="settings">
|
<li @click="settings">
|
||||||
<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
|
<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li v-if="$store.state.i.isAdmin">
|
||||||
<ul>
|
<router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link>
|
||||||
<li @click="signout">
|
|
||||||
<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -41,6 +39,11 @@
|
||||||
<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
|
<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li @click="signout">
|
||||||
|
<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
|
<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
|
||||||
<router-link to="/deck">
|
<router-link to="/deck">
|
||||||
%fa:columns%
|
%fa:columns%
|
||||||
<p>%i18n:@deck% <small>(beta)</small></p>
|
<p>%i18n:@deck%</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li class="messaging">
|
<li class="messaging">
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<x-account v-if="$store.getters.isSignedIn"/>
|
<x-account v-if="$store.getters.isSignedIn"/>
|
||||||
<x-notifications v-if="$store.getters.isSignedIn"/>
|
<x-notifications v-if="$store.getters.isSignedIn"/>
|
||||||
<x-post v-if="$store.getters.isSignedIn"/>
|
<x-post v-if="$store.getters.isSignedIn"/>
|
||||||
<x-clock/>
|
<x-clock v-if="$store.state.settings.showClockOnHeader"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default Vue.extend({
|
||||||
this.open();
|
this.open();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const query = this.user[0] == '@' ?
|
const query = this.user.startsWith('@') ?
|
||||||
parseAcct(this.user.substr(1)) :
|
parseAcct(this.user.substr(1)) :
|
||||||
{ userId: this.user };
|
{ userId: this.user };
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="obdskegsannmntldydackcpzezagxqfy card">
|
<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
|
||||||
<header>%i18n:@dashboard%</header>
|
<header>%i18n:@dashboard%</header>
|
||||||
<div v-if="stats" class="stats">
|
<div v-if="stats" class="stats">
|
||||||
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
|
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
|
||||||
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
|
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
|
||||||
<div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
|
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
|
||||||
<div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
|
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpu-memory">
|
<div class="cpu-memory">
|
||||||
<x-cpu-memory :connection="connection"/>
|
<x-cpu-memory :connection="connection"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="disableRegistration" @change="updateMeta">
|
||||||
|
<span>disableRegistration</span>
|
||||||
|
</label>
|
||||||
<button class="ui" @click="invite">%i18n:@invite%</button>
|
<button class="ui" @click="invite">%i18n:@invite%</button>
|
||||||
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
|
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,6 +32,7 @@ export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
stats: null,
|
stats: null,
|
||||||
|
disableRegistration: false,
|
||||||
inviteCode: null,
|
inviteCode: null,
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null
|
connectionId: null
|
||||||
|
@ -37,6 +42,10 @@ export default Vue.extend({
|
||||||
this.connection = (this as any).os.streams.serverStatsStream.getConnection();
|
this.connection = (this as any).os.streams.serverStatsStream.getConnection();
|
||||||
this.connectionId = (this as any).os.streams.serverStatsStream.use();
|
this.connectionId = (this as any).os.streams.serverStatsStream.use();
|
||||||
|
|
||||||
|
(this as any).os.getMeta().then(meta => {
|
||||||
|
this.disableRegistration = meta.disableRegistration;
|
||||||
|
});
|
||||||
|
|
||||||
(this as any).api('stats').then(stats => {
|
(this as any).api('stats').then(stats => {
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
});
|
});
|
||||||
|
@ -49,6 +58,11 @@ export default Vue.extend({
|
||||||
(this as any).api('admin/invite').then(x => {
|
(this as any).api('admin/invite').then(x => {
|
||||||
this.inviteCode = x.code;
|
this.inviteCode = x.code;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
updateMeta() {
|
||||||
|
(this as any).api('admin/update-meta', {
|
||||||
|
disableRegistration: this.disableRegistration
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
|
||||||
<polyline
|
|
||||||
:points="points"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke="#555"/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
props: {
|
|
||||||
chart: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
viewBoxX: 365,
|
|
||||||
viewBoxY: 70,
|
|
||||||
points: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize));
|
|
||||||
|
|
||||||
if (peak != 0) {
|
|
||||||
const data = this.chart.slice().reverse().map(x => ({
|
|
||||||
size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
svg
|
|
||||||
display block
|
|
||||||
padding 10px
|
|
||||||
width 100%
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,34 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@title%</header>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@local%</header>
|
|
||||||
<x-chart v-if="chart" :chart="chart" type="local"/>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@remote%</header>
|
|
||||||
<x-chart v-if="chart" :chart="chart" type="remote"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from "vue";
|
|
||||||
import XChart from "./admin.drive-chart.chart.vue";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
components: {
|
|
||||||
XChart
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
chart: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
@import '~const.styl'
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,76 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
|
||||||
<polyline
|
|
||||||
:points="pointsNote"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke="#41ddde"/>
|
|
||||||
<polyline
|
|
||||||
:points="pointsReply"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke="#f7796c"/>
|
|
||||||
<polyline
|
|
||||||
:points="pointsRenote"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke="#a1de41"/>
|
|
||||||
<polyline
|
|
||||||
:points="pointsTotal"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke="#555"
|
|
||||||
stroke-dasharray="2 2"/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
props: {
|
|
||||||
chart: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
viewBoxX: 365,
|
|
||||||
viewBoxY: 70,
|
|
||||||
pointsNote: null,
|
|
||||||
pointsReply: null,
|
|
||||||
pointsRenote: null,
|
|
||||||
pointsTotal: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff));
|
|
||||||
|
|
||||||
if (peak != 0) {
|
|
||||||
const data = this.chart.slice().reverse().map(x => ({
|
|
||||||
normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal,
|
|
||||||
reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply,
|
|
||||||
renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote,
|
|
||||||
total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' ');
|
|
||||||
this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' ');
|
|
||||||
this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' ');
|
|
||||||
this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
svg
|
|
||||||
display block
|
|
||||||
padding 10px
|
|
||||||
width 100%
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,34 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@title%</header>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@local%</header>
|
|
||||||
<x-chart v-if="chart" :chart="chart" type="local"/>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@remote%</header>
|
|
||||||
<x-chart v-if="chart" :chart="chart" type="remote"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from "vue";
|
|
||||||
import XChart from "./admin.notes-chart.chart.vue";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
components: {
|
|
||||||
XChart
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
chart: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
@import '~const.styl'
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="mk-admin-card">
|
||||||
<header>%i18n:@suspend-user%</header>
|
<header>%i18n:@suspend-user%</header>
|
||||||
<input v-model="username" type="text" class="ui"/>
|
<input v-model="username" type="text" class="ui"/>
|
||||||
<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
|
<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="mk-admin-card">
|
||||||
<header>%i18n:@unsuspend-user%</header>
|
<header>%i18n:@unsuspend-user%</header>
|
||||||
<input v-model="username" type="text" class="ui"/>
|
<input v-model="username" type="text" class="ui"/>
|
||||||
<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
|
<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="mk-admin-card">
|
||||||
<header>%i18n:@unverify-user%</header>
|
<header>%i18n:@unverify-user%</header>
|
||||||
<input v-model="username" type="text" class="ui"/>
|
<input v-model="username" type="text" class="ui"/>
|
||||||
<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button>
|
<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button>
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
|
||||||
<polyline
|
|
||||||
:points="points"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke="#555"/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
props: {
|
|
||||||
chart: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
viewBoxX: 365,
|
|
||||||
viewBoxY: 70,
|
|
||||||
points: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff));
|
|
||||||
|
|
||||||
if (peak != 0) {
|
|
||||||
const data = this.chart.slice().reverse().map(x => ({
|
|
||||||
count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
svg
|
|
||||||
display block
|
|
||||||
padding 10px
|
|
||||||
width 100%
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,34 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@title%</header>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@local%</header>
|
|
||||||
<x-chart v-if="chart" :chart="chart" type="local"/>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<header>%i18n:@remote%</header>
|
|
||||||
<x-chart v-if="chart" :chart="chart" type="remote"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from "vue";
|
|
||||||
import XChart from "./admin.users-chart.chart.vue";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
components: {
|
|
||||||
XChart
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
chart: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
@import '~const.styl'
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="mk-admin-card">
|
||||||
<header>%i18n:@verify-user%</header>
|
<header>%i18n:@verify-user%</header>
|
||||||
<input v-model="username" type="text" class="ui"/>
|
<input v-model="username" type="text" class="ui"/>
|
||||||
<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button>
|
<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button>
|
||||||
|
|
|
@ -11,9 +11,7 @@
|
||||||
<main>
|
<main>
|
||||||
<div v-show="page == 'dashboard'">
|
<div v-show="page == 'dashboard'">
|
||||||
<x-dashboard/>
|
<x-dashboard/>
|
||||||
<x-users-chart :chart="chart"/>
|
<x-charts/>
|
||||||
<x-notes-chart :chart="chart"/>
|
|
||||||
<x-drive-chart :chart="chart"/>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="page == 'users'">
|
<div v-if="page == 'users'">
|
||||||
<x-suspend-user/>
|
<x-suspend-user/>
|
||||||
|
@ -34,9 +32,7 @@ import XSuspendUser from "./admin.suspend-user.vue";
|
||||||
import XUnsuspendUser from "./admin.unsuspend-user.vue";
|
import XUnsuspendUser from "./admin.unsuspend-user.vue";
|
||||||
import XVerifyUser from "./admin.verify-user.vue";
|
import XVerifyUser from "./admin.verify-user.vue";
|
||||||
import XUnverifyUser from "./admin.unverify-user.vue";
|
import XUnverifyUser from "./admin.unverify-user.vue";
|
||||||
import XUsersChart from "./admin.users-chart.vue";
|
import XCharts from "../../components/charts.vue";
|
||||||
import XNotesChart from "./admin.notes-chart.vue";
|
|
||||||
import XDriveChart from "./admin.drive-chart.vue";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
|
@ -45,21 +41,13 @@ export default Vue.extend({
|
||||||
XUnsuspendUser,
|
XUnsuspendUser,
|
||||||
XVerifyUser,
|
XVerifyUser,
|
||||||
XUnverifyUser,
|
XUnverifyUser,
|
||||||
XUsersChart,
|
XCharts
|
||||||
XNotesChart,
|
|
||||||
XDriveChart
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
page: 'dashboard',
|
page: 'dashboard'
|
||||||
chart: null
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
(this as any).api('admin/chart').then(chart => {
|
|
||||||
this.chart = chart;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
nav(page: string) {
|
nav(page: string) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
|
@ -115,7 +103,7 @@ export default Vue.extend({
|
||||||
> div
|
> div
|
||||||
max-width 800px
|
max-width 800px
|
||||||
|
|
||||||
.card
|
.mk-admin-card
|
||||||
padding 32px
|
padding 32px
|
||||||
background #fff
|
background #fff
|
||||||
box-shadow 0 2px 8px rgba(#000, 0.1)
|
box-shadow 0 2px 8px rgba(#000, 0.1)
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<mk-media-list :media-list="p.media"/>
|
<mk-media-list :media-list="p.media"/>
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||||
<div class="renote" v-if="p.renote">
|
<div class="renote" v-if="p.renote">
|
||||||
<mk-note-preview :note="p.renote" :mini="true"/>
|
<mk-note-preview :note="p.renote" :mini="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: (this as any).os.instanceName,
|
name: null,
|
||||||
posted: false,
|
posted: false,
|
||||||
text: new URLSearchParams(location.search).get('text')
|
text: new URLSearchParams(location.search).get('text')
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,11 @@ export default Vue.extend({
|
||||||
close() {
|
close() {
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
(this as any).os.getMeta().then(meta => {
|
||||||
|
this.name = meta.name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
64
src/client/app/desktop/views/pages/stats/stats.vue
Normal file
64
src/client/app/desktop/views/pages/stats/stats.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey">
|
||||||
|
<div v-if="stats" class="stats">
|
||||||
|
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
|
||||||
|
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
|
||||||
|
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
|
||||||
|
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<x-charts/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from "vue";
|
||||||
|
import XCharts from "../../components/charts.vue";
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
XCharts
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
stats: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
(this as any).api('stats').then(stats => {
|
||||||
|
this.stats = stats;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
.tcrwdhwpuxrwmcttxjcsehgpagpstqey
|
||||||
|
width 100%
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
> .stats
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
margin-bottom 16px
|
||||||
|
padding 32px
|
||||||
|
background #fff
|
||||||
|
box-shadow 0 2px 8px rgba(#000, 0.1)
|
||||||
|
|
||||||
|
> div
|
||||||
|
flex 1
|
||||||
|
text-align center
|
||||||
|
|
||||||
|
> *:first-child
|
||||||
|
display block
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
> *:last-child
|
||||||
|
font-size 70%
|
||||||
|
|
||||||
|
> div
|
||||||
|
max-width 850px
|
||||||
|
</style>
|
|
@ -40,10 +40,12 @@ export default Vue.extend({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
root(isDark)
|
||||||
.friends
|
.friends
|
||||||
background #fff
|
background isDark ? #282C37 : #fff
|
||||||
border solid 1px rgba(#000, 0.075)
|
border solid 1px rgba(#000, 0.075)
|
||||||
border-radius 6px
|
border-radius 6px
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
> .title
|
> .title
|
||||||
z-index 1
|
z-index 1
|
||||||
|
@ -52,7 +54,8 @@ export default Vue.extend({
|
||||||
line-height 42px
|
line-height 42px
|
||||||
font-size 0.9em
|
font-size 0.9em
|
||||||
font-weight bold
|
font-weight bold
|
||||||
color #888
|
background isDark ? #313543 : inherit
|
||||||
|
color isDark ? #e3e5e8 : #888
|
||||||
box-shadow 0 1px rgba(#000, 0.07)
|
box-shadow 0 1px rgba(#000, 0.07)
|
||||||
|
|
||||||
> i
|
> i
|
||||||
|
@ -70,7 +73,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
> .user
|
> .user
|
||||||
padding 16px
|
padding 16px
|
||||||
border-bottom solid 1px #eee
|
border-bottom solid 1px isDark ? #21242f : #eee
|
||||||
|
|
||||||
&:last-child
|
&:last-child
|
||||||
border-bottom none
|
border-bottom none
|
||||||
|
@ -96,18 +99,24 @@ export default Vue.extend({
|
||||||
margin 0
|
margin 0
|
||||||
font-size 16px
|
font-size 16px
|
||||||
line-height 24px
|
line-height 24px
|
||||||
color #555
|
color isDark ? #ccc : #555
|
||||||
|
|
||||||
> .username
|
> .username
|
||||||
display block
|
display block
|
||||||
margin 0
|
margin 0
|
||||||
font-size 15px
|
font-size 15px
|
||||||
line-height 16px
|
line-height 16px
|
||||||
color #ccc
|
color isDark ? #555 : #ccc
|
||||||
|
|
||||||
> .mk-follow-button
|
> .mk-follow-button
|
||||||
position absolute
|
position absolute
|
||||||
top 16px
|
top 16px
|
||||||
right 16px
|
right 16px
|
||||||
|
|
||||||
|
.friends[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.friends:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -39,10 +39,12 @@ export default Vue.extend({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
root(isDark)
|
||||||
.photos
|
.photos
|
||||||
background #fff
|
background isDark ? #282C37 : #fff
|
||||||
border solid 1px rgba(#000, 0.075)
|
border solid 1px rgba(#000, 0.075)
|
||||||
border-radius 6px
|
border-radius 6px
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
> .title
|
> .title
|
||||||
z-index 1
|
z-index 1
|
||||||
|
@ -51,7 +53,8 @@ export default Vue.extend({
|
||||||
line-height 42px
|
line-height 42px
|
||||||
font-size 0.9em
|
font-size 0.9em
|
||||||
font-weight bold
|
font-weight bold
|
||||||
color #888
|
background: isDark ? #313543 : inherit
|
||||||
|
color isDark ? #e3e5e8 : #888
|
||||||
box-shadow 0 1px rgba(#000, 0.07)
|
box-shadow 0 1px rgba(#000, 0.07)
|
||||||
|
|
||||||
> i
|
> i
|
||||||
|
@ -85,4 +88,10 @@ export default Vue.extend({
|
||||||
> i
|
> i
|
||||||
margin-right 4px
|
margin-right 4px
|
||||||
|
|
||||||
|
.photos[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.photos:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -138,7 +138,7 @@ root(isDark)
|
||||||
padding 16px
|
padding 16px
|
||||||
font-size 12px
|
font-size 12px
|
||||||
color #aaa
|
color #aaa
|
||||||
background #fff
|
background isDark ? #21242f : #fff
|
||||||
border solid 1px rgba(#000, 0.075)
|
border solid 1px rgba(#000, 0.075)
|
||||||
border-radius 6px
|
border-radius 6px
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,8 @@ import { version, codename, lang } from './config';
|
||||||
|
|
||||||
let elementLocale;
|
let elementLocale;
|
||||||
switch (lang) {
|
switch (lang) {
|
||||||
case 'ja': elementLocale = ElementLocaleJa; break;
|
case 'ja-JP': elementLocale = ElementLocaleJa; break;
|
||||||
case 'en': elementLocale = ElementLocaleEn; break;
|
case 'en-US': elementLocale = ElementLocaleEn; break;
|
||||||
default: elementLocale = ElementLocaleEn; break;
|
default: elementLocale = ElementLocaleEn; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p"/>
|
<mk-poll v-if="p.poll" :note="p"/>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
||||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||||
<div class="map" v-if="p.geo" ref="map"></div>
|
<div class="map" v-if="p.geo" ref="map"></div>
|
||||||
<div class="renote" v-if="p.renote">
|
<div class="renote" v-if="p.renote">
|
||||||
<mk-note-preview :note="p.renote"/>
|
<mk-note-preview :note="p.renote"/>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
|
||||||
<div class="map" v-if="p.geo" ref="map"></div>
|
<div class="map" v-if="p.geo" ref="map"></div>
|
||||||
<div class="renote" v-if="p.renote">
|
<div class="renote" v-if="p.renote">
|
||||||
<mk-note-preview :note="p.renote"/>
|
<mk-note-preview :note="p.renote"/>
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
|
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
|
||||||
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
|
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
|
||||||
|
<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li>
|
||||||
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
|
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,6 +41,12 @@
|
||||||
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
|
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|
||||||
|
<ui-card>
|
||||||
|
<div slot="title">%fa:volume-up% %i18n:@sound%</div>
|
||||||
|
|
||||||
|
<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">%fa:language% %i18n:@lang%</div>
|
<div slot="title">%fa:language% %i18n:@lang%</div>
|
||||||
|
|
||||||
|
@ -142,6 +148,11 @@ export default Vue.extend({
|
||||||
get() { return this.$store.state.device.lang; },
|
get() { return this.$store.state.device.lang; },
|
||||||
set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
|
set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enableSounds: {
|
||||||
|
get() { return this.$store.state.device.enableSounds; },
|
||||||
|
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: (this as any).os.instanceName,
|
name: null,
|
||||||
posted: false,
|
posted: false,
|
||||||
text: new URLSearchParams(location.search).get('text')
|
text: new URLSearchParams(location.search).get('text')
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,11 @@ export default Vue.extend({
|
||||||
close() {
|
close() {
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
(this as any).os.getMeta().then(meta => {
|
||||||
|
this.name = meta.name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<a class="avatar">
|
<a class="avatar">
|
||||||
<img :src="user.avatarUrl" alt="avatar"/>
|
<img :src="user.avatarUrl" alt="avatar"/>
|
||||||
</a>
|
</a>
|
||||||
<mk-mute-button v-if="$store.state.i.id != user.id" :user="user"/>
|
<mk-mute-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
|
||||||
<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
|
<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -13,6 +13,7 @@ const defaultSettings = {
|
||||||
showMaps: true,
|
showMaps: true,
|
||||||
showPostFormOnTopOfTl: false,
|
showPostFormOnTopOfTl: false,
|
||||||
suggestRecentHashtags: true,
|
suggestRecentHashtags: true,
|
||||||
|
showClockOnHeader: true,
|
||||||
circleIcons: true,
|
circleIcons: true,
|
||||||
gradientWindowHeader: false,
|
gradientWindowHeader: false,
|
||||||
showReplyTarget: true,
|
showReplyTarget: true,
|
||||||
|
|
|
@ -53,5 +53,5 @@ export default function load() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrl(url: string) {
|
function normalizeUrl(url: string) {
|
||||||
return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
|
return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,8 @@ export type Source = {
|
||||||
*/
|
*/
|
||||||
ghost?: string;
|
ghost?: string;
|
||||||
|
|
||||||
|
summalyProxy?: string;
|
||||||
|
|
||||||
accesslog?: string;
|
accesslog?: string;
|
||||||
twitter?: {
|
twitter?: {
|
||||||
consumer_key: string;
|
consumer_key: string;
|
||||||
|
|
|
@ -15,7 +15,7 @@ block main
|
||||||
span.path= endpointUrl.path
|
span.path= endpointUrl.path
|
||||||
|
|
||||||
if endpoint.desc
|
if endpoint.desc
|
||||||
p#desc= endpoint.desc[lang] || endpoint.desc['ja']
|
p#desc= endpoint.desc[lang] || endpoint.desc['ja-JP']
|
||||||
|
|
||||||
if endpoint.requireCredential
|
if endpoint.requireCredential
|
||||||
div.ui.info: p
|
div.ui.info: p
|
||||||
|
|
|
@ -1,90 +1,90 @@
|
||||||
name: "DriveFile"
|
name: "DriveFile"
|
||||||
|
|
||||||
desc:
|
desc:
|
||||||
ja: "ドライブのファイル。"
|
ja-JP: "ドライブのファイル。"
|
||||||
en: "A file of Drive."
|
en-US: "A file of Drive."
|
||||||
|
|
||||||
props:
|
props:
|
||||||
id:
|
id:
|
||||||
type: "id"
|
type: "id"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ファイルID"
|
ja-JP: "ファイルID"
|
||||||
en: "The ID of this file"
|
en-US: "The ID of this file"
|
||||||
|
|
||||||
createdAt:
|
createdAt:
|
||||||
type: "date"
|
type: "date"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "アップロード日時"
|
ja-JP: "アップロード日時"
|
||||||
en: "The upload date of this file"
|
en-US: "The upload date of this file"
|
||||||
|
|
||||||
userId:
|
userId:
|
||||||
type: "id(User)"
|
type: "id(User)"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "所有者ID"
|
ja-JP: "所有者ID"
|
||||||
en: "The ID of the owner of this file"
|
en-US: "The ID of the owner of this file"
|
||||||
|
|
||||||
user:
|
user:
|
||||||
type: "entity(User)"
|
type: "entity(User)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "所有者"
|
ja-JP: "所有者"
|
||||||
en: "The owner of this file"
|
en-US: "The owner of this file"
|
||||||
|
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ファイル名"
|
ja-JP: "ファイル名"
|
||||||
en: "The name of this file"
|
en-US: "The name of this file"
|
||||||
|
|
||||||
md5:
|
md5:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ファイルのMD5ハッシュ値"
|
ja-JP: "ファイルのMD5ハッシュ値"
|
||||||
en: "The md5 hash value of this file"
|
en-US: "The md5 hash value of this file"
|
||||||
|
|
||||||
type:
|
type:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ファイルの種類"
|
ja-JP: "ファイルの種類"
|
||||||
en: "The type of this file"
|
en-US: "The type of this file"
|
||||||
|
|
||||||
datasize:
|
datasize:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ファイルサイズ(bytes)"
|
ja-JP: "ファイルサイズ(bytes)"
|
||||||
en: "The size of this file (bytes)"
|
en-US: "The size of this file (bytes)"
|
||||||
|
|
||||||
url:
|
url:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ファイルのURL"
|
ja-JP: "ファイルのURL"
|
||||||
en: "The URL of this file"
|
en-US: "The URL of this file"
|
||||||
|
|
||||||
folderId:
|
folderId:
|
||||||
type: "id(DriveFolder)"
|
type: "id(DriveFolder)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "フォルダID"
|
ja-JP: "フォルダID"
|
||||||
en: "The ID of the folder of this file"
|
en-US: "The ID of the folder of this file"
|
||||||
|
|
||||||
folder:
|
folder:
|
||||||
type: "entity(DriveFolder)"
|
type: "entity(DriveFolder)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "フォルダ"
|
ja-JP: "フォルダ"
|
||||||
en: "The folder of this file"
|
en-US: "The folder of this file"
|
||||||
|
|
||||||
isSensitive:
|
isSensitive:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "このメディアが「閲覧注意」(NSFW)かどうか"
|
ja-JP: "このメディアが「閲覧注意」(NSFW)かどうか"
|
||||||
en: "Whether this media is NSFW"
|
en-US: "Whether this media is NSFW"
|
||||||
|
|
|
@ -1,41 +1,41 @@
|
||||||
name: "DriveFolder"
|
name: "DriveFolder"
|
||||||
|
|
||||||
desc:
|
desc:
|
||||||
ja: "ドライブのフォルダを表します。"
|
ja-JP: "ドライブのフォルダを表します。"
|
||||||
en: "A folder of Drive."
|
en-US: "A folder of Drive."
|
||||||
|
|
||||||
props:
|
props:
|
||||||
id:
|
id:
|
||||||
type: "id"
|
type: "id"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "フォルダID"
|
ja-JP: "フォルダID"
|
||||||
en: "The ID of this folder"
|
en-US: "The ID of this folder"
|
||||||
|
|
||||||
createdAt:
|
createdAt:
|
||||||
type: "date"
|
type: "date"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "作成日時"
|
ja-JP: "作成日時"
|
||||||
en: "The created date of this folder"
|
en-US: "The created date of this folder"
|
||||||
|
|
||||||
userId:
|
userId:
|
||||||
type: "id(User)"
|
type: "id(User)"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "所有者ID"
|
ja-JP: "所有者ID"
|
||||||
en: "The ID of the owner of this folder"
|
en-US: "The ID of the owner of this folder"
|
||||||
|
|
||||||
parentId:
|
parentId:
|
||||||
type: "entity(DriveFolder)"
|
type: "entity(DriveFolder)"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "親フォルダのID (ルートなら null)"
|
ja-JP: "親フォルダのID (ルートなら null)"
|
||||||
en: "The ID of parent folder"
|
en-US: "The ID of parent folder"
|
||||||
|
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "フォルダ名"
|
ja-JP: "フォルダ名"
|
||||||
en: "The name of this folder"
|
en-US: "The name of this folder"
|
||||||
|
|
|
@ -1,190 +1,190 @@
|
||||||
name: "Note"
|
name: "Note"
|
||||||
|
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿。"
|
ja-JP: "投稿。"
|
||||||
en: "A note."
|
en-US: "A note."
|
||||||
|
|
||||||
props:
|
props:
|
||||||
id:
|
id:
|
||||||
type: "id"
|
type: "id"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿ID"
|
ja-JP: "投稿ID"
|
||||||
en: "The ID of this note"
|
en-US: "The ID of this note"
|
||||||
|
|
||||||
createdAt:
|
createdAt:
|
||||||
type: "date"
|
type: "date"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿日時"
|
ja-JP: "投稿日時"
|
||||||
en: "The posted date of this note"
|
en-US: "The posted date of this note"
|
||||||
|
|
||||||
viaMobile:
|
viaMobile:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "モバイル端末から投稿したか否か(自己申告であることに留意)"
|
ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)"
|
||||||
en: "Whether this note sent via a mobile device"
|
en-US: "Whether this note sent via a mobile device"
|
||||||
|
|
||||||
text:
|
text:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿の本文"
|
ja-JP: "投稿の本文"
|
||||||
en: "The text of this note"
|
en-US: "The text of this note"
|
||||||
|
|
||||||
mediaIds:
|
mediaIds:
|
||||||
type: "id(DriveFile)[]"
|
type: "id(DriveFile)[]"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "添付されているメディアのID (なければレスポンスでは空配列)"
|
ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)"
|
||||||
en: "The IDs of the attached media (empty array for response if no media is attached)"
|
en-US: "The IDs of the attached media (empty array for response if no media is attached)"
|
||||||
|
|
||||||
media:
|
media:
|
||||||
type: "entity(DriveFile)[]"
|
type: "entity(DriveFile)[]"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "添付されているメディア"
|
ja-JP: "添付されているメディア"
|
||||||
en: "The attached media"
|
en-US: "The attached media"
|
||||||
|
|
||||||
userId:
|
userId:
|
||||||
type: "id(User)"
|
type: "id(User)"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿者ID"
|
ja-JP: "投稿者ID"
|
||||||
en: "The ID of author of this note"
|
en-US: "The ID of author of this note"
|
||||||
|
|
||||||
user:
|
user:
|
||||||
type: "entity(User)"
|
type: "entity(User)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿者"
|
ja-JP: "投稿者"
|
||||||
en: "The author of this note"
|
en-US: "The author of this note"
|
||||||
|
|
||||||
myReaction:
|
myReaction:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
|
ja-JP: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
|
||||||
en: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
|
en-US: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
|
||||||
|
|
||||||
reactionCounts:
|
reactionCounts:
|
||||||
type: "object"
|
type: "object"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
|
ja-JP: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
|
||||||
|
|
||||||
replyId:
|
replyId:
|
||||||
type: "id(Note)"
|
type: "id(Note)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "返信した投稿のID"
|
ja-JP: "返信した投稿のID"
|
||||||
en: "The ID of the replyed note"
|
en-US: "The ID of the replyed note"
|
||||||
|
|
||||||
reply:
|
reply:
|
||||||
type: "entity(Note)"
|
type: "entity(Note)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "返信した投稿"
|
ja-JP: "返信した投稿"
|
||||||
en: "The replyed note"
|
en-US: "The replyed note"
|
||||||
|
|
||||||
renoteId:
|
renoteId:
|
||||||
type: "id(Note)"
|
type: "id(Note)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "引用した投稿のID"
|
ja-JP: "引用した投稿のID"
|
||||||
en: "The ID of the quoted note"
|
en-US: "The ID of the quoted note"
|
||||||
|
|
||||||
renote:
|
renote:
|
||||||
type: "entity(Note)"
|
type: "entity(Note)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "引用した投稿"
|
ja-JP: "引用した投稿"
|
||||||
en: "The quoted note"
|
en-US: "The quoted note"
|
||||||
|
|
||||||
poll:
|
poll:
|
||||||
type: "object"
|
type: "object"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "投票"
|
ja-JP: "投票"
|
||||||
en: "The poll"
|
en-US: "The poll"
|
||||||
|
|
||||||
props:
|
props:
|
||||||
choices:
|
choices:
|
||||||
type: "object[]"
|
type: "object[]"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "投票の選択肢"
|
ja-JP: "投票の選択肢"
|
||||||
en: "The choices of this poll"
|
en-US: "The choices of this poll"
|
||||||
|
|
||||||
props:
|
props:
|
||||||
id:
|
id:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "選択肢ID"
|
ja-JP: "選択肢ID"
|
||||||
en: "The ID of this choice"
|
en-US: "The ID of this choice"
|
||||||
|
|
||||||
isVoted:
|
isVoted:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "自分がこの選択肢に投票したかどうか"
|
ja-JP: "自分がこの選択肢に投票したかどうか"
|
||||||
en: "Whether you voted to this choice"
|
en-US: "Whether you voted to this choice"
|
||||||
|
|
||||||
text:
|
text:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "選択肢本文"
|
ja-JP: "選択肢本文"
|
||||||
en: "The text of this choice"
|
en-US: "The text of this choice"
|
||||||
|
|
||||||
votes:
|
votes:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "この選択肢に投票された数"
|
ja-JP: "この選択肢に投票された数"
|
||||||
en: "The number voted for this choice"
|
en-US: "The number voted for this choice"
|
||||||
geo:
|
geo:
|
||||||
type: "object"
|
type: "object"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "位置情報"
|
ja-JP: "位置情報"
|
||||||
en: "Geo location"
|
en-US: "Geo location"
|
||||||
|
|
||||||
props:
|
props:
|
||||||
coordinates:
|
coordinates:
|
||||||
type: "number[]"
|
type: "number[]"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
|
ja-JP: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
|
||||||
|
|
||||||
altitude:
|
altitude:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "高度。メートル単位で表す。"
|
ja-JP: "高度。メートル単位で表す。"
|
||||||
|
|
||||||
accuracy:
|
accuracy:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "緯度、経度の精度。メートル単位で表す。"
|
ja-JP: "緯度、経度の精度。メートル単位で表す。"
|
||||||
|
|
||||||
altitudeAccuracy:
|
altitudeAccuracy:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "高度の精度。メートル単位で表す。"
|
ja-JP: "高度の精度。メートル単位で表す。"
|
||||||
|
|
||||||
heading:
|
heading:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
|
ja-JP: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
|
||||||
|
|
||||||
speed:
|
speed:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "速度。メートル / 秒数で表す。"
|
ja-JP: "速度。メートル / 秒数で表す。"
|
||||||
|
|
|
@ -1,174 +1,174 @@
|
||||||
name: "User"
|
name: "User"
|
||||||
|
|
||||||
desc:
|
desc:
|
||||||
ja: "ユーザー。"
|
ja-JP: "ユーザー。"
|
||||||
en: "A user."
|
en-US: "A user."
|
||||||
|
|
||||||
props:
|
props:
|
||||||
id:
|
id:
|
||||||
type: "id"
|
type: "id"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ユーザーID"
|
ja-JP: "ユーザーID"
|
||||||
en: "The ID of this user"
|
en-US: "The ID of this user"
|
||||||
|
|
||||||
createdAt:
|
createdAt:
|
||||||
type: "date"
|
type: "date"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "アカウント作成日時"
|
ja-JP: "アカウント作成日時"
|
||||||
en: "The registered date of this user"
|
en-US: "The registered date of this user"
|
||||||
|
|
||||||
username:
|
username:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ユーザー名"
|
ja-JP: "ユーザー名"
|
||||||
en: "The username of this user"
|
en-US: "The username of this user"
|
||||||
|
|
||||||
description:
|
description:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "アカウントの説明(自己紹介)"
|
ja-JP: "アカウントの説明(自己紹介)"
|
||||||
en: "The description of this user"
|
en-US: "The description of this user"
|
||||||
|
|
||||||
avatarId:
|
avatarId:
|
||||||
type: "id(DriveFile)"
|
type: "id(DriveFile)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "アバターのID"
|
ja-JP: "アバターのID"
|
||||||
en: "The ID of the avatar of this user"
|
en-US: "The ID of the avatar of this user"
|
||||||
|
|
||||||
avatarUrl:
|
avatarUrl:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "アバターのURL"
|
ja-JP: "アバターのURL"
|
||||||
en: "The URL of the avatar of this user"
|
en-US: "The URL of the avatar of this user"
|
||||||
|
|
||||||
bannerId:
|
bannerId:
|
||||||
type: "id(DriveFile)"
|
type: "id(DriveFile)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "バナーのID"
|
ja-JP: "バナーのID"
|
||||||
en: "The ID of the banner of this user"
|
en-US: "The ID of the banner of this user"
|
||||||
|
|
||||||
bannerUrl:
|
bannerUrl:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "バナーのURL"
|
ja-JP: "バナーのURL"
|
||||||
en: "The URL of the banner of this user"
|
en-US: "The URL of the banner of this user"
|
||||||
|
|
||||||
followersCount:
|
followersCount:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "フォロワーの数"
|
ja-JP: "フォロワーの数"
|
||||||
en: "The number of the followers for this user"
|
en-US: "The number of the followers for this user"
|
||||||
|
|
||||||
followingCount:
|
followingCount:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "フォローしているユーザーの数"
|
ja-JP: "フォローしているユーザーの数"
|
||||||
en: "The number of the following users for this user"
|
en-US: "The number of the following users for this user"
|
||||||
|
|
||||||
isFollowing:
|
isFollowing:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "自分がこのユーザーをフォローしているか"
|
ja-JP: "自分がこのユーザーをフォローしているか"
|
||||||
|
|
||||||
isFollowed:
|
isFollowed:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "自分がこのユーザーにフォローされているか"
|
ja-JP: "自分がこのユーザーにフォローされているか"
|
||||||
|
|
||||||
isMuted:
|
isMuted:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "自分がこのユーザーをミュートしているか"
|
ja-JP: "自分がこのユーザーをミュートしているか"
|
||||||
en: "Whether you muted this user"
|
en-US: "Whether you muted this user"
|
||||||
|
|
||||||
notesCount:
|
notesCount:
|
||||||
type: "number"
|
type: "number"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "投稿の数"
|
ja-JP: "投稿の数"
|
||||||
en: "The number of the notes of this user"
|
en-US: "The number of the notes of this user"
|
||||||
|
|
||||||
pinnedNote:
|
pinnedNote:
|
||||||
type: "entity(Note)"
|
type: "entity(Note)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "ピン留めされた投稿"
|
ja-JP: "ピン留めされた投稿"
|
||||||
en: "The pinned note of this user"
|
en-US: "The pinned note of this user"
|
||||||
|
|
||||||
pinnedNoteId:
|
pinnedNoteId:
|
||||||
type: "id(Note)"
|
type: "id(Note)"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "ピン留めされた投稿のID"
|
ja-JP: "ピン留めされた投稿のID"
|
||||||
en: "The ID of the pinned note of this user"
|
en-US: "The ID of the pinned note of this user"
|
||||||
|
|
||||||
host:
|
host:
|
||||||
type: "string | null"
|
type: "string | null"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ホスト (例: example.com:3000)"
|
ja-JP: "ホスト (例: example.com:3000)"
|
||||||
en: "Host (e.g. example.com:3000)"
|
en-US: "Host (e.g. example.com:3000)"
|
||||||
|
|
||||||
twitter:
|
twitter:
|
||||||
type: "object"
|
type: "object"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "連携されているTwitterアカウント情報"
|
ja-JP: "連携されているTwitterアカウント情報"
|
||||||
en: "The info of the connected twitter account of this user"
|
en-US: "The info of the connected twitter account of this user"
|
||||||
|
|
||||||
props:
|
props:
|
||||||
userId:
|
userId:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ユーザーID"
|
ja-JP: "ユーザーID"
|
||||||
en: "The user ID"
|
en-US: "The user ID"
|
||||||
|
|
||||||
screenName:
|
screenName:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "ユーザー名"
|
ja-JP: "ユーザー名"
|
||||||
en: "The screen name of this user"
|
en-US: "The screen name of this user"
|
||||||
|
|
||||||
isBot:
|
isBot:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "botか否か(自己申告であることに留意)"
|
ja-JP: "botか否か(自己申告であることに留意)"
|
||||||
en: "Whether is bot or not"
|
en-US: "Whether is bot or not"
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
type: "object"
|
type: "object"
|
||||||
optional: false
|
optional: false
|
||||||
desc:
|
desc:
|
||||||
ja: "プロフィール"
|
ja-JP: "プロフィール"
|
||||||
en: "The profile of this user"
|
en-US: "The profile of this user"
|
||||||
|
|
||||||
props:
|
props:
|
||||||
location:
|
location:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "場所"
|
ja-JP: "場所"
|
||||||
en: "The location of this user"
|
en-US: "The location of this user"
|
||||||
|
|
||||||
birthday:
|
birthday:
|
||||||
type: "string"
|
type: "string"
|
||||||
optional: true
|
optional: true
|
||||||
desc:
|
desc:
|
||||||
ja: "誕生日 (YYYY-MM-DD)"
|
ja-JP: "誕生日 (YYYY-MM-DD)"
|
||||||
en: "The birthday of this user (YYYY-MM-DD)"
|
en-US: "The birthday of this user (YYYY-MM-DD)"
|
||||||
|
|
|
@ -7,7 +7,7 @@ block meta
|
||||||
block main
|
block main
|
||||||
h1= name
|
h1= name
|
||||||
|
|
||||||
p#desc= desc[lang] || desc['ja']
|
p#desc= desc[lang] || desc['ja-JP']
|
||||||
|
|
||||||
section
|
section
|
||||||
h2= i18n('docs.api.entities.properties')
|
h2= i18n('docs.api.entities.properties')
|
||||||
|
|
|
@ -31,4 +31,4 @@ mixin propTable(props)
|
||||||
td.name= prop.name
|
td.name= prop.name
|
||||||
td.type
|
td.type
|
||||||
+type(prop)
|
+type(prop)
|
||||||
td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja'] : null
|
td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja-JP'] : null
|
||||||
|
|
|
@ -16,7 +16,7 @@ html(lang= lang)
|
||||||
nav
|
nav
|
||||||
ul
|
ul
|
||||||
each doc in docs
|
each doc in docs
|
||||||
li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
|
li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja-JP']
|
||||||
section
|
section
|
||||||
h2 API
|
h2 API
|
||||||
ul
|
ul
|
||||||
|
|
|
@ -197,7 +197,7 @@ const elements: Element[] = [
|
||||||
|
|
||||||
if (thisIsNotARegexp) return null;
|
if (thisIsNotARegexp) return null;
|
||||||
if (regexp == '') return null;
|
if (regexp == '') return null;
|
||||||
if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null;
|
if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `<span class="regexp">/${escape(regexp)}/</span>`,
|
html: `<span class="regexp">/${escape(regexp)}/</span>`,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export type TextElementHashtag = {
|
||||||
|
|
||||||
export default function(text: string, i: number) {
|
export default function(text: string, i: number) {
|
||||||
if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
|
if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
|
||||||
const isHead = text[0] == '#';
|
const isHead = text.startsWith('#');
|
||||||
const hashtag = text.match(/^\s?#[^\s]+/)[0];
|
const hashtag = text.match(/^\s?#[^\s]+/)[0];
|
||||||
const res: any[] = !isHead ? [{
|
const res: any[] = !isHead ? [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
|
@ -13,7 +13,7 @@ export type TextElementLink = {
|
||||||
export default function(text: string) {
|
export default function(text: string) {
|
||||||
const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
|
const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const silent = text[0] == '?';
|
const silent = text.startsWith('?');
|
||||||
const link = match[0];
|
const link = match[0];
|
||||||
const title = match[1];
|
const title = match[1];
|
||||||
const url = match[2];
|
const url = match[2];
|
||||||
|
|
|
@ -25,9 +25,9 @@ export const replacement = (match: string, key: string) => {
|
||||||
arg == 'S' ? 'fas' :
|
arg == 'S' ? 'fas' :
|
||||||
arg == 'B' ? 'fab' :
|
arg == 'B' ? 'fab' :
|
||||||
'';
|
'';
|
||||||
} else if (arg[0] == '.') {
|
} else if (arg.startsWith('.')) {
|
||||||
classes.push('fa-' + arg.substr(1));
|
classes.push('fa-' + arg.substr(1));
|
||||||
} else if (arg[0] == '-') {
|
} else if (arg.startsWith('-')) {
|
||||||
transform = arg.substr(1).split('|').join(' ');
|
transform = arg.substr(1).split('|').join(' ');
|
||||||
} else {
|
} else {
|
||||||
name = arg;
|
name = arg;
|
||||||
|
|
|
@ -27,10 +27,12 @@ export default class Replacer {
|
||||||
let text = texts;
|
let text = texts;
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
|
path = path.replace('.ts', '');
|
||||||
|
|
||||||
if (text.hasOwnProperty(path)) {
|
if (text.hasOwnProperty(path)) {
|
||||||
text = text[path];
|
text = text[path];
|
||||||
} else {
|
} else {
|
||||||
if (this.lang === 'ja') console.warn(`path '${path}' not found`);
|
if (this.lang === 'ja-JP') console.warn(`path '${path}' not found`);
|
||||||
return key; // Fallback
|
return key; // Fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,10 +48,10 @@ export default class Replacer {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (this.lang === 'ja') console.warn(`key '${key}' not found in '${path}'`);
|
if (this.lang === 'ja-JP') console.warn(`key '${key}' not found in '${path}'`);
|
||||||
return key; // Fallback
|
return key; // Fallback
|
||||||
} else if (typeof text !== 'string') {
|
} else if (typeof text !== 'string') {
|
||||||
if (this.lang === 'ja') console.warn(`key '${key}' is not string in '${path}'`);
|
if (this.lang === 'ja-JP') console.warn(`key '${key}' is not string in '${path}'`);
|
||||||
return key; // Fallback
|
return key; // Fallback
|
||||||
} else {
|
} else {
|
||||||
return text;
|
return text;
|
||||||
|
|
|
@ -2,40 +2,59 @@ import * as mongo from 'mongodb';
|
||||||
import db from '../db/mongodb';
|
import db from '../db/mongodb';
|
||||||
|
|
||||||
const Stats = db.get<IStats>('stats');
|
const Stats = db.get<IStats>('stats');
|
||||||
Stats.createIndex({ date: -1 }, { unique: true });
|
Stats.dropIndex({ date: -1 }); // 後方互換性のため
|
||||||
|
Stats.createIndex({ span: -1, date: -1 }, { unique: true });
|
||||||
export default Stats;
|
export default Stats;
|
||||||
|
|
||||||
export interface IStats {
|
export interface IStats {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 集計日時
|
||||||
|
*/
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 集計期間
|
||||||
|
*/
|
||||||
|
span: 'day' | 'hour';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ユーザーに関する統計
|
* ユーザーに関する統計
|
||||||
*/
|
*/
|
||||||
users: {
|
users: {
|
||||||
local: {
|
local: {
|
||||||
/**
|
/**
|
||||||
* この日時点での、ローカルのユーザーの総計
|
* 集計期間時点での、全ユーザー数 (ローカル)
|
||||||
*/
|
*/
|
||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ローカルのユーザー数の前日比
|
* 増加したユーザー数 (ローカル)
|
||||||
*/
|
*/
|
||||||
diff: number;
|
inc: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少したユーザー数 (ローカル)
|
||||||
|
*/
|
||||||
|
dec: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
remote: {
|
remote: {
|
||||||
/**
|
/**
|
||||||
* この日時点での、リモートのユーザーの総計
|
* 集計期間時点での、全ユーザー数 (リモート)
|
||||||
*/
|
*/
|
||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモートのユーザー数の前日比
|
* 増加したユーザー数 (リモート)
|
||||||
*/
|
*/
|
||||||
diff: number;
|
inc: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少したユーザー数 (リモート)
|
||||||
|
*/
|
||||||
|
dec: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,28 +64,33 @@ export interface IStats {
|
||||||
notes: {
|
notes: {
|
||||||
local: {
|
local: {
|
||||||
/**
|
/**
|
||||||
* この日時点での、ローカルの投稿の総計
|
* 集計期間時点での、全投稿数 (ローカル)
|
||||||
*/
|
*/
|
||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ローカルの投稿数の前日比
|
* 増加した投稿数 (ローカル)
|
||||||
*/
|
*/
|
||||||
diff: number;
|
inc: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少した投稿数 (ローカル)
|
||||||
|
*/
|
||||||
|
dec: number;
|
||||||
|
|
||||||
diffs: {
|
diffs: {
|
||||||
/**
|
/**
|
||||||
* ローカルの通常の投稿数の前日比
|
* 通常の投稿数の差分 (ローカル)
|
||||||
*/
|
*/
|
||||||
normal: number;
|
normal: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ローカルのリプライの投稿数の前日比
|
* リプライの投稿数の差分 (ローカル)
|
||||||
*/
|
*/
|
||||||
reply: number;
|
reply: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ローカルのRenoteの投稿数の前日比
|
* Renoteの投稿数の差分 (ローカル)
|
||||||
*/
|
*/
|
||||||
renote: number;
|
renote: number;
|
||||||
};
|
};
|
||||||
|
@ -74,28 +98,33 @@ export interface IStats {
|
||||||
|
|
||||||
remote: {
|
remote: {
|
||||||
/**
|
/**
|
||||||
* この日時点での、リモートの投稿の総計
|
* 集計期間時点での、全投稿数 (リモート)
|
||||||
*/
|
*/
|
||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモートの投稿数の前日比
|
* 増加した投稿数 (リモート)
|
||||||
*/
|
*/
|
||||||
diff: number;
|
inc: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少した投稿数 (リモート)
|
||||||
|
*/
|
||||||
|
dec: number;
|
||||||
|
|
||||||
diffs: {
|
diffs: {
|
||||||
/**
|
/**
|
||||||
* リモートの通常の投稿数の前日比
|
* 通常の投稿数の差分 (リモート)
|
||||||
*/
|
*/
|
||||||
normal: number;
|
normal: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモートのリプライの投稿数の前日比
|
* リプライの投稿数の差分 (リモート)
|
||||||
*/
|
*/
|
||||||
reply: number;
|
reply: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモートのRenoteの投稿数の前日比
|
* Renoteの投稿数の差分 (リモート)
|
||||||
*/
|
*/
|
||||||
renote: number;
|
renote: number;
|
||||||
};
|
};
|
||||||
|
@ -108,46 +137,66 @@ export interface IStats {
|
||||||
drive: {
|
drive: {
|
||||||
local: {
|
local: {
|
||||||
/**
|
/**
|
||||||
* この日時点での、ローカルのドライブファイル数の総計
|
* 集計期間時点での、全ドライブファイル数 (ローカル)
|
||||||
*/
|
*/
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* この日時点での、ローカルのドライブファイルサイズの総計
|
* 集計期間時点での、全ドライブファイルの合計サイズ (ローカル)
|
||||||
*/
|
*/
|
||||||
totalSize: number;
|
totalSize: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ローカルのドライブファイル数の前日比
|
* 増加したドライブファイル数 (ローカル)
|
||||||
*/
|
*/
|
||||||
diffCount: number;
|
incCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ローカルのドライブファイルサイズの前日比
|
* 増加したドライブ使用量 (ローカル)
|
||||||
*/
|
*/
|
||||||
diffSize: number;
|
incSize: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少したドライブファイル数 (ローカル)
|
||||||
|
*/
|
||||||
|
decCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少したドライブ使用量 (ローカル)
|
||||||
|
*/
|
||||||
|
decSize: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
remote: {
|
remote: {
|
||||||
/**
|
/**
|
||||||
* この日時点での、リモートのドライブファイル数の総計
|
* 集計期間時点での、全ドライブファイル数 (リモート)
|
||||||
*/
|
*/
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* この日時点での、リモートのドライブファイルサイズの総計
|
* 集計期間時点での、全ドライブファイルの合計サイズ (リモート)
|
||||||
*/
|
*/
|
||||||
totalSize: number;
|
totalSize: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモートのドライブファイル数の前日比
|
* 増加したドライブファイル数 (リモート)
|
||||||
*/
|
*/
|
||||||
diffCount: number;
|
incCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* リモートのドライブファイルサイズの前日比
|
* 増加したドライブ使用量 (リモート)
|
||||||
*/
|
*/
|
||||||
diffSize: number;
|
incSize: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少したドライブファイル数 (リモート)
|
||||||
|
*/
|
||||||
|
decCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 減少したドライブ使用量 (リモート)
|
||||||
|
*/
|
||||||
|
decSize: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||||
|
|
||||||
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
|
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
user = await resolvePerson(signature.keyId) as IRemoteUser;
|
user = await resolvePerson(activity.actor) as IRemoteUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,5 +131,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
return await createNote(value, resolver);
|
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||||
|
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||||
|
return await createNote(uri, resolver);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,25 @@ import * as debug from 'debug';
|
||||||
|
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
|
import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
|
||||||
import webFinger from '../../webfinger';
|
|
||||||
import Resolver from '../resolver';
|
import Resolver from '../resolver';
|
||||||
import { resolveImage } from './image';
|
import { resolveImage } from './image';
|
||||||
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
|
import { isCollectionOrOrderedCollection, IPerson } from '../type';
|
||||||
import { IDriveFile } from '../../../models/drive-file';
|
import { IDriveFile } from '../../../models/drive-file';
|
||||||
import Meta from '../../../models/meta';
|
import Meta from '../../../models/meta';
|
||||||
import htmlToMFM from '../../../mfm/html-to-mfm';
|
import htmlToMFM from '../../../mfm/html-to-mfm';
|
||||||
import { updateUserStats } from '../../../services/update-chart';
|
import { updateUserStats } from '../../../services/update-chart';
|
||||||
|
import { URL } from 'url';
|
||||||
|
|
||||||
const log = debug('misskey:activitypub');
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
function validatePerson(x: any) {
|
/**
|
||||||
|
* Validate Person object
|
||||||
|
* @param x Fetched person object
|
||||||
|
* @param uri Fetch target URI
|
||||||
|
*/
|
||||||
|
function validatePerson(x: any, uri: string) {
|
||||||
|
const expectHost = toUnicode(new URL(uri).hostname.toLowerCase());
|
||||||
|
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
return new Error('invalid person: object is null');
|
return new Error('invalid person: object is null');
|
||||||
}
|
}
|
||||||
|
@ -40,6 +47,24 @@ function validatePerson(x: any) {
|
||||||
return new Error('invalid person: invalid name');
|
return new Error('invalid person: invalid name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof x.id !== 'string') {
|
||||||
|
return new Error('invalid person: id is not a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const idHost = toUnicode(new URL(x.id).hostname.toLowerCase());
|
||||||
|
if (idHost !== expectHost) {
|
||||||
|
return new Error('invalid person: id has different host');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof x.publicKey.id !== 'string') {
|
||||||
|
return new Error('invalid person: publicKey.id is not a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase());
|
||||||
|
if (publicKeyIdHost !== expectHost) {
|
||||||
|
return new Error('invalid person: publicKey.id has different host');
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,8 +73,8 @@ function validatePerson(x: any) {
|
||||||
*
|
*
|
||||||
* Misskeyに対象のPersonが登録されていればそれを返します。
|
* Misskeyに対象のPersonが登録されていればそれを返します。
|
||||||
*/
|
*/
|
||||||
export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> {
|
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> {
|
||||||
const uri = typeof value == 'string' ? value : value.id;
|
if (typeof uri !== 'string') throw 'uri is not string';
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||||
if (uri.startsWith(config.url + '/')) {
|
if (uri.startsWith(config.url + '/')) {
|
||||||
|
@ -71,12 +96,14 @@ export async function fetchPerson(value: string | IObject, resolver?: Resolver):
|
||||||
/**
|
/**
|
||||||
* Personを作成します。
|
* Personを作成します。
|
||||||
*/
|
*/
|
||||||
export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> {
|
export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> {
|
||||||
|
if (typeof uri !== 'string') throw 'uri is not string';
|
||||||
|
|
||||||
if (resolver == null) resolver = new Resolver();
|
if (resolver == null) resolver = new Resolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value) as any;
|
const object = await resolver.resolve(uri) as any;
|
||||||
|
|
||||||
const err = validatePerson(object);
|
const err = validatePerson(object, uri);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -86,7 +113,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
||||||
|
|
||||||
log(`Creating the Person: ${person.id}`);
|
log(`Creating the Person: ${person.id}`);
|
||||||
|
|
||||||
const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([
|
const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
|
||||||
resolver.resolve(person.followers).then(
|
resolver.resolve(person.followers).then(
|
||||||
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
|
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
|
||||||
() => undefined
|
() => undefined
|
||||||
|
@ -98,11 +125,10 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
||||||
resolver.resolve(person.outbox).then(
|
resolver.resolve(person.outbox).then(
|
||||||
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
|
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
|
||||||
() => undefined
|
() => undefined
|
||||||
),
|
)
|
||||||
webFinger(person.id)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase();
|
const host = toUnicode(new URL(object.id).hostname.toLowerCase());
|
||||||
|
|
||||||
const isBot = object.type == 'Service';
|
const isBot = object.type == 'Service';
|
||||||
|
|
||||||
|
@ -166,8 +192,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
||||||
|
|
||||||
const avatarId = avatar ? avatar._id : null;
|
const avatarId = avatar ? avatar._id : null;
|
||||||
const bannerId = banner ? banner._id : null;
|
const bannerId = banner ? banner._id : null;
|
||||||
const avatarUrl = avatar && avatar.metadata.url ? avatar.metadata.url : null;
|
const avatarUrl = (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null;
|
||||||
const bannerUrl = banner && banner.metadata.url ? banner.metadata.url : null;
|
const bannerUrl = (banner && banner.metadata.url) ? banner.metadata.url : null;
|
||||||
|
|
||||||
await User.update({ _id: user._id }, {
|
await User.update({ _id: user._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
|
@ -192,8 +218,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
||||||
*
|
*
|
||||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
* Misskeyに対象のPersonが登録されていなければ無視します。
|
||||||
*/
|
*/
|
||||||
export async function updatePerson(value: string | IObject, resolver?: Resolver): Promise<void> {
|
export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> {
|
||||||
const uri = typeof value == 'string' ? value : value.id;
|
if (typeof uri !== 'string') throw 'uri is not string';
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(config.url + '/')) {
|
if (uri.startsWith(config.url + '/')) {
|
||||||
|
@ -210,9 +236,9 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
|
||||||
|
|
||||||
if (resolver == null) resolver = new Resolver();
|
if (resolver == null) resolver = new Resolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value) as any;
|
const object = await resolver.resolve(uri) as any;
|
||||||
|
|
||||||
const err = validatePerson(object);
|
const err = validatePerson(object, uri);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -255,7 +281,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox,
|
||||||
avatarId: avatar ? avatar._id : null,
|
avatarId: avatar ? avatar._id : null,
|
||||||
bannerId: banner ? banner._id : null,
|
bannerId: banner ? banner._id : null,
|
||||||
avatarUrl: avatar && avatar.metadata.url ? avatar.metadata.url : null,
|
avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null,
|
||||||
bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null,
|
bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null,
|
||||||
description: htmlToMFM(person.summary),
|
description: htmlToMFM(person.summary),
|
||||||
followersCount,
|
followersCount,
|
||||||
|
@ -275,8 +301,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
|
||||||
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
|
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
|
||||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||||
*/
|
*/
|
||||||
export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> {
|
export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> {
|
||||||
const uri = typeof value == 'string' ? value : value.id;
|
if (typeof uri !== 'string') throw 'uri is not string';
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await fetchPerson(uri);
|
const exist = await fetchPerson(uri);
|
||||||
|
@ -287,5 +313,5 @@ export async function resolvePerson(value: string | IObject, verifier?: string):
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
return await createPerson(value);
|
return await createPerson(uri);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ export default (object: any, note: INote) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${config.url}/notes/${note._id}`,
|
id: `${config.url}/notes/${note._id}`,
|
||||||
|
actor: `${config.url}/users/${note.userId}`,
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
export default (object: any) => ({
|
import config from '../../../config';
|
||||||
|
import { INote } from '../../../models/note';
|
||||||
|
|
||||||
|
export default (object: any, note: INote) => {
|
||||||
|
const activity = {
|
||||||
|
id: `${config.url}/notes/${note._id}/activity`,
|
||||||
|
actor: `${config.url}/users/${note.userId}`,
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
|
published: note.createdAt.toISOString(),
|
||||||
object
|
object
|
||||||
});
|
} as any;
|
||||||
|
|
||||||
|
if (object.to) activity.to = object.to;
|
||||||
|
if (object.cc) activity.cc = object.cc;
|
||||||
|
|
||||||
|
return activity;
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
export default (object: any) => ({
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from "../../../models/user";
|
||||||
|
|
||||||
|
export default (object: any, user: ILocalUser) => ({
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
|
actor: `${config.url}/users/${user._id}`,
|
||||||
object
|
object
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
export default (x: any) => Object.assign({
|
import config from '../../../config';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
|
||||||
|
export default (x: any) => {
|
||||||
|
if (x !== null && typeof x === 'object' && x.id == null) {
|
||||||
|
x.id = `${config.url}/${uuid.v4()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
'https://w3id.org/security/v1',
|
'https://w3id.org/security/v1',
|
||||||
{ Hashtag: 'as:Hashtag' }
|
{ Hashtag: 'as:Hashtag' }
|
||||||
]
|
]
|
||||||
}, x);
|
}, x);
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
export default (object: any) => ({
|
import config from '../../../config';
|
||||||
|
import { ILocalUser, IUser } from "../../../models/user";
|
||||||
|
|
||||||
|
export default (object: any, user: ILocalUser | IUser) => ({
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
|
actor: `${config.url}/users/${user._id}`,
|
||||||
object
|
object
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,9 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
|
||||||
port,
|
port,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: pathname + search,
|
path: pathname + search,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/activity+json'
|
||||||
|
}
|
||||||
}, res => {
|
}, res => {
|
||||||
log(`${url} --> ${res.statusCode}`);
|
log(`${url} --> ${res.statusCode}`);
|
||||||
|
|
||||||
|
@ -32,7 +35,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
|
||||||
sign(req, {
|
sign(req, {
|
||||||
authorizationHeaderName: 'Signature',
|
authorizationHeaderName: 'Signature',
|
||||||
key: user.keypair,
|
key: user.keypair,
|
||||||
keyId: `acct:${user.username}@${config.host}`
|
keyId: `${config.url}/users/${user._id}/publickey`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Signature: Signature ... => Signature: ...
|
// Signature: Signature ... => Signature: ...
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default async (username: string, _host: string, option?: any): Promise<IU
|
||||||
const host = toUnicode(hostAscii);
|
const host = toUnicode(hostAscii);
|
||||||
|
|
||||||
if (config.host == host) {
|
if (config.host == host) {
|
||||||
return await User.findOne({ usernameLower });
|
return await User.findOne({ usernameLower, host: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await User.findOne({ usernameLower, host }, option);
|
let user = await User.findOne({ usernameLower, host }, option);
|
||||||
|
|
|
@ -25,7 +25,7 @@ function inbox(ctx: Router.IRouterContext) {
|
||||||
ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
|
ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(ctx.req);
|
signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export default (token: string) => token[0] == '!';
|
export default (token: string) => token.startsWith('!');
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
import Stats, { IStats } from '../../../../models/stats';
|
|
||||||
|
|
||||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
requireCredential: true,
|
|
||||||
requireAdmin: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (params: any) => new Promise(async (res, rej) => {
|
|
||||||
const now = new Date();
|
|
||||||
const y = now.getFullYear();
|
|
||||||
const m = now.getMonth();
|
|
||||||
const d = now.getDate();
|
|
||||||
|
|
||||||
const stats = await Stats.find({
|
|
||||||
date: {
|
|
||||||
$gt: new Date(y - 1, m, d)
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
sort: {
|
|
||||||
date: -1
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
_id: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const chart: Array<Omit<IStats, '_id'>> = [];
|
|
||||||
|
|
||||||
for (let i = 364; i >= 0; i--) {
|
|
||||||
const day = new Date(y, m, d - i);
|
|
||||||
|
|
||||||
const stat = stats.find(s => s.date.getTime() == day.getTime());
|
|
||||||
|
|
||||||
if (stat) {
|
|
||||||
chart.unshift(stat);
|
|
||||||
} else { // 隙間埋め
|
|
||||||
const mostRecent = stats.find(s => s.date.getTime() < day.getTime());
|
|
||||||
if (mostRecent) {
|
|
||||||
chart.unshift(Object.assign({}, mostRecent, {
|
|
||||||
date: day
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
chart.unshift({
|
|
||||||
date: day,
|
|
||||||
users: {
|
|
||||||
local: {
|
|
||||||
total: 0,
|
|
||||||
diff: 0
|
|
||||||
},
|
|
||||||
remote: {
|
|
||||||
total: 0,
|
|
||||||
diff: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
local: {
|
|
||||||
total: 0,
|
|
||||||
diff: 0,
|
|
||||||
diffs: {
|
|
||||||
normal: 0,
|
|
||||||
reply: 0,
|
|
||||||
renote: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remote: {
|
|
||||||
total: 0,
|
|
||||||
diff: 0,
|
|
||||||
diffs: {
|
|
||||||
normal: 0,
|
|
||||||
reply: 0,
|
|
||||||
renote: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
drive: {
|
|
||||||
local: {
|
|
||||||
totalCount: 0,
|
|
||||||
totalSize: 0,
|
|
||||||
diffCount: 0,
|
|
||||||
diffSize: 0
|
|
||||||
},
|
|
||||||
remote: {
|
|
||||||
totalCount: 0,
|
|
||||||
totalSize: 0,
|
|
||||||
diffCount: 0,
|
|
||||||
diffSize: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.forEach(x => {
|
|
||||||
delete x.date;
|
|
||||||
});
|
|
||||||
|
|
||||||
res(chart);
|
|
||||||
});
|
|
|
@ -3,7 +3,7 @@ import RegistrationTicket from '../../../../models/registration-tickets';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
ja: '招待コードを発行します。'
|
'ja-JP': '招待コードを発行します。'
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue