Merge branch 'master' into l10n_master

This commit is contained in:
syuilo 2018-05-18 15:34:38 +09:00 committed by GitHub
commit ef643f329d
55 changed files with 575 additions and 1526 deletions

View File

@ -7,7 +7,7 @@ notifications:
language: node_js
node_js:
- 9.8.0
- 10.1.0
env:
- CXX=g++-4.8 NODE_ENV=production
@ -22,19 +22,14 @@ addons:
cache:
directories:
# パッケージをキャッシュすると本来は動かないはずなのに動いてしまう
# 場合があり危険なのでキャッシュはしない:
#- node_modules
- node_modules
services:
- mongodb
- redis-server
before_script:
# Travisはproduction環境なので(10行目により)、
# npm install しただけでは devDependencies はインストールされないので、
# --only=dev オプションを付けてそれらもインストールされるようにする:
- npm install --only=dev
- npm install
# 設定ファイルを配置
- cp ./.travis/default.yml ./.config

41
appveyor.yml Normal file
View File

@ -0,0 +1,41 @@
# appveyor file
# http://www.appveyor.com/docs/appveyor-yml
environment:
matrix:
- nodejs_version: 10.1.0
cache:
- node_modules
build: off
install:
# Update Node.js
# 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準)
- ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
- node --version
# Update NPM
- npm install -g npm
- npm --version
# Update node-gyp
# 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します
- npm install -g node-gyp
- npm install
init:
# git clone の際の改行を変換しないようにします
- git config --global core.autocrlf false
before_test:
# 設定ファイルを配置
- cp ./.travis/default.yml ./.config
- cp ./.travis/test.yml ./.config
- npm run build
test_script:
- npm test

View File

@ -20,6 +20,7 @@ import * as replace from 'gulp-replace';
import * as htmlmin from 'gulp-htmlmin';
const uglifyes = require('uglify-es');
import locales from './locales';
import { fa } from './src/build/fa';
const client = require('./built/client/meta.json');
import config from './src/config';
@ -122,6 +123,7 @@ gulp.task('build:client:script', () =>
.pipe(replace('VERSION', JSON.stringify(client.version)))
.pipe(replace('API', JSON.stringify(config.api_url)))
.pipe(replace('ENV', JSON.stringify(env)))
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
.pipe(isProduction ? uglify({
toplevel: true
} as any) : gutil.noop())

View File

@ -1,7 +1,7 @@
---
meta:
lang: "日本語"
divider: ""
lang: "English"
divider: " "
common:
misskey: "Share everything with others using Misskey."
time:

View File

@ -11,6 +11,7 @@ const loadLang = lang => yaml.safeLoad(
const native = loadLang('ja');
const langs = {
'de': loadLang('de'),
'en': loadLang('en'),
'fr': loadLang('fr'),
'ja': native,

View File

@ -1,3 +1,7 @@
meta:
lang: "日本語"
divider: ""
common:
misskey: "Misskeyで皆と共有しよう。"
@ -253,6 +257,32 @@ desktop/views/components/drive.vue:
upload: "ファイルをアップロード"
url-upload: "URLからアップロード"
desktop/views/components/home.vue:
done: "完了"
add-widget: "ウィジェットを追加:"
profile: "プロフィール"
calendar: "カレンダー"
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
trends: "トレンド"
photostream: "フォトストリーム"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
post-form: "投稿フォーム"
messaging: "メッセージ"
channel: "チャンネル"
access-log: "アクセスログ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
add: "追加"
desktop/views/components/messaging-window.vue:
title: "メッセージ"
@ -312,6 +342,7 @@ desktop/views/components/settings.vue:
mute: "ミュート"
drive: "ドライブ"
security: "セキュリティ"
signin: "サインイン履歴"
password: "パスワード"
2fa: "二段階認証"
other: "その他"
@ -341,6 +372,7 @@ desktop/views/components/settings.api.vue:
caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
regenerate-token: "トークンを再生成"
token: "Token:"
enter-password: "パスワードを入力してください"
desktop/views/components/settings.app.vue:
@ -396,6 +428,20 @@ desktop/views/components/ui.header.post.vue:
desktop/views/components/ui.header.search.vue:
placeholder: "検索"
desktop/views/components/user-lists-window.vue:
create-list: "リストを作成"
desktop/views/components/user-preview.vue:
notes: "投稿"
following: "フォロー"
followers: "フォロワー"
desktop/views/components/users-list.vue:
all: "すべて"
iknow: "知り合い"
load-more: "もっと"
fetching: "読み込んでいます"
desktop/views/pages/note.vue:
prev: "前の投稿"
next: "次の投稿"
@ -510,6 +556,9 @@ mobile/views/components/notifications.vue:
mobile/views/components/post-form.vue:
submit: "投稿"
reply: "返信"
renote: "Renote"
renote-placeholder: "この投稿を引用... (オプション)"
reply-placeholder: "この投稿への返信..."
note-placeholder: "いまどうしてる?"

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "2.6.2",
"clientVersion": "1.0.5260",
"version": "2.10.0",
"clientVersion": "1.0.5406",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@ -31,13 +31,11 @@
"@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1",
"@types/chai": "4.1.3",
"@types/chai-http": "3.0.4",
"@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1",
"@types/elasticsearch": "5.0.22",
"@types/elasticsearch": "5.0.23",
"@types/eventemitter3": "2.0.2",
"@types/gm": "1.17.33",
"@types/gm": "1.18.0",
"@types/gulp": "3.8.36",
"@types/gulp-htmlmin": "1.3.32",
"@types/gulp-mocha": "0.0.32",
@ -58,18 +56,18 @@
"@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.28",
"@types/koa-send": "4.1.1",
"@types/koa-views": "^2.0.3",
"@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.2",
"@types/kue": "0.11.8",
"@types/license-checker": "15.0.0",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.0",
"@types/mongodb": "3.0.15",
"@types/mongodb": "3.0.18",
"@types/monk": "6.0.0",
"@types/ms": "0.7.30",
"@types/node": "9.6.6",
"@types/node": "10.1.0",
"@types/nopt": "3.0.29",
"@types/parse5": "^3.0.0",
"@types/parse5": "3.0.0",
"@types/pug": "2.0.4",
"@types/qrcode": "0.8.1",
"@types/ratelimiter": "2.1.28",
@ -78,22 +76,20 @@
"@types/request-promise-native": "1.0.14",
"@types/rimraf": "2.0.2",
"@types/seedrandom": "2.4.27",
"@types/single-line-log": "^1.1.0",
"@types/single-line-log": "1.1.0",
"@types/speakeasy": "2.0.2",
"@types/tmp": "0.0.33",
"@types/uuid": "3.4.3",
"@types/webpack": "4.1.4",
"@types/webpack": "4.1.7",
"@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.38",
"@types/ws": "4.0.2",
"@types/websocket": "0.0.39",
"@types/ws": "5.1.1",
"animejs": "2.2.0",
"autosize": "4.0.1",
"autosize": "4.0.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"bootstrap-vue": "2.0.0-rc.6",
"cafy": "8.0.0",
"chai": "4.1.2",
"chai-http": "4.0.0",
"chalk": "2.4.1",
"crc-32": "1.2.0",
"css-loader": "0.28.11",
@ -101,9 +97,9 @@
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",
"diskusage": "0.2.4",
"dompurify": "1.0.3",
"dompurify": "1.0.4",
"elasticsearch": "14.2.2",
"element-ui": "2.3.6",
"element-ui": "2.3.8",
"emojilib": "2.2.12",
"escape-regexp": "0.0.1",
"eslint": "4.19.1",
@ -111,7 +107,7 @@
"eventemitter3": "3.1.0",
"exif-js": "2.3.0",
"file-loader": "1.1.11",
"file-type": "7.6.0",
"file-type": "8.0.0",
"fuckadblock": "3.2.1",
"gm": "1.23.1",
"gulp": "3.9.1",
@ -120,15 +116,15 @@
"gulp-imagemin": "4.1.0",
"gulp-mocha": "5.0.0",
"gulp-pug": "4.0.1",
"gulp-rename": "1.2.2",
"gulp-replace": "0.6.1",
"gulp-rename": "1.2.3",
"gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.4",
"gulp-stylus": "2.7.0",
"gulp-tslint": "8.1.3",
"gulp-typescript": "4.0.2",
"gulp-uglify": "3.0.0",
"gulp-util": "3.0.8",
"hard-source-webpack-plugin": "0.6.4",
"hard-source-webpack-plugin": "0.6.7",
"highlight.js": "9.12.0",
"html-minifier": "3.5.15",
"http-signature": "1.2.0",
@ -136,7 +132,7 @@
"is-root": "2.0.0",
"is-url": "1.2.4",
"js-yaml": "3.11.0",
"jsdom": "11.9.0",
"jsdom": "11.10.0",
"koa": "2.5.1",
"koa-bodyparser": "4.2.0",
"koa-compress": "3.0.0",
@ -148,16 +144,16 @@
"koa-router": "7.4.0",
"koa-send": "4.1.3",
"koa-slow": "2.1.0",
"koa-views": "^6.1.4",
"koa-views": "6.1.4",
"kue": "0.11.6",
"license-checker": "18.0.0",
"license-checker": "19.0.0",
"loader-utils": "1.1.0",
"mecab-async": "0.1.2",
"mkdirp": "0.5.1",
"mocha": "5.1.1",
"moji": "0.5.1",
"mongodb": "3.0.7",
"monk": "6.0.5",
"mongodb": "3.0.8",
"monk": "6.0.6",
"ms": "2.1.1",
"nan": "2.10.0",
"node-sass": "4.9.0",
@ -167,10 +163,10 @@
"object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0",
"os-utils": "0.0.14",
"parse5": "^4.0.0",
"parse5": "4.0.0",
"progress-bar-webpack-plugin": "1.11.0",
"prominence": "0.2.0",
"promise-sequential": "^1.1.1",
"promise-sequential": "1.1.1",
"pug": "2.0.3",
"punycode": "2.1.0",
"qrcode": "1.2.0",
@ -178,14 +174,14 @@
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2",
"redis": "2.8.0",
"request": "2.85.0",
"request": "2.86.0",
"request-promise-native": "1.0.5",
"rimraf": "2.6.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass-loader": "7.0.1",
"seedrandom": "2.4.3",
"single-line-log": "^1.1.2",
"single-line-log": "1.1.2",
"speakeasy": "2.0.0",
"style-loader": "0.21.0",
"stylus": "0.54.5",
@ -196,9 +192,9 @@
"tcp-port-used": "0.1.2",
"textarea-caret": "3.1.0",
"tmp": "0.0.33",
"ts-loader": "4.2.0",
"ts-node": "6.0.1",
"tslint": "5.9.1",
"ts-loader": "4.3.0",
"ts-node": "6.0.3",
"tslint": "5.10.0",
"typescript": "2.8.3",
"typescript-eslint-parser": "15.0.0",
"uglify-es": "3.3.9",
@ -209,16 +205,15 @@
"vue-cropperjs": "2.2.0",
"vue-js-modal": "1.3.13",
"vue-json-tree-view": "2.1.4",
"vue-loader": "15.0.3",
"vue-loader": "15.0.11",
"vue-router": "3.0.1",
"vue-template-compiler": "2.5.16",
"vuedraggable": "2.16.0",
"vuex": "3.0.1",
"web-push": "3.3.0",
"web-push": "3.3.1",
"webfinger.js": "2.6.6",
"webpack": "4.6.0",
"webpack-cli": "2.0.15",
"webpack-replace-loader": "1.3.0",
"webpack": "4.8.3",
"webpack-cli": "2.1.3",
"websocket": "1.0.26",
"ws": "5.1.1",
"xev": "2.0.0"

View File

@ -7,10 +7,7 @@ import * as regular from '@fortawesome/fontawesome-free-regular';
import * as solid from '@fortawesome/fontawesome-free-solid';
import * as brands from '@fortawesome/fontawesome-free-brands';
// Add icons
fontawesome.library.add(regular);
fontawesome.library.add(solid);
fontawesome.library.add(brands);
fontawesome.library.add(regular, solid, brands);
export const pattern = /%fa:(.+?)%/g;

View File

@ -32,7 +32,7 @@
// Detect the user language
// Note: The default language is Japanese
let lang = navigator.language.split('-')[0];
if (!/^(en|ja)$/.test(lang)) lang = 'ja';
if (!LANGS.includes(lang)) lang = 'en';
if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
// Detect the user agent

View File

@ -0,0 +1,16 @@
export default function(note) {
if (note.text == null) return true;
let txt = note.text;
if (note.media) {
note.media.forEach(file => {
txt = txt.replace(file.url, '');
if (file.src) txt = txt.replace(file.src, '');
});
if (txt == '') return true;
}
return false;
}

View File

@ -48,6 +48,17 @@ export class HomeStream extends Stream {
}
});
this.on('mobile_home_updated', x => {
if (x.home) {
os.store.commit('settings/setMobileHome', x.home);
} else {
os.store.commit('settings/setMobileHomeWidget', {
id: x.id,
data: x.data
});
}
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
this.on('my_token_regenerated', () => {

View File

@ -0,0 +1,19 @@
<template>
<span class="mk-acct">
<span class="name">@{{ user.username }}</span>
<span class="host" v-if="user.host">@{{ user.host }}</span>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: ['user']
});
</script>
<style lang="stylus" scoped>
.mk-acct
> .host
opacity 0.5
</style>

View File

@ -23,7 +23,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
backgroundColor: this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null,
backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
};

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import signin from './signin.vue';
import signup from './signup.vue';
import forkit from './forkit.vue';
import acct from './acct.vue';
import avatar from './avatar.vue';
import nav from './nav.vue';
import noteHtml from './note-html';
@ -29,6 +30,7 @@ import welcomeTimeline from './welcome-timeline.vue';
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);
Vue.component('mk-acct', acct);
Vue.component('mk-avatar', avatar);
Vue.component('mk-nav', nav);
Vue.component('mk-note-html', noteHtml);

View File

@ -126,16 +126,21 @@ root(isDark)
line-height 16px
vertical-align top
@media (max-width 500px)
font-size 8px
border none
@media (max-width 700px)
> .thumbnail
width 70px
position relative
width 100%
height 100px
& + article
left 70px
width calc(100% - 70px)
left 0
width 100%
@media (max-width 500px)
font-size 8px
> .thumbnail
height 70px
> article
padding 8px

View File

@ -1,37 +1,37 @@
<template>
<div class="mkw-calendar"
:data-melt="props.design == 1"
:data-special="special"
:data-mobile="isMobile"
>
<div class="calendar" :data-is-holiday="isHoliday">
<p class="month-and-year">
<span class="year">{{ year }}</span>
<span class="month">{{ month }}</span>
</p>
<p class="day">{{ day }}</p>
<p class="week-day">{{ weekDay }}曜日</p>
</div>
<div class="info">
<div>
<p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${dayP}%` }"></div>
<div class="mkw-calendar" :data-special="special" :data-mobile="isMobile">
<mk-widget-container :naked="props.design == 1" :show-header="false">
<div class="mkw-calendar--body">
<div class="calendar" :data-is-holiday="isHoliday">
<p class="month-and-year">
<span class="year">{{ year }}</span>
<span class="month">{{ month }}</span>
</p>
<p class="day">{{ day }}</p>
<p class="week-day">{{ weekDay }}曜日</p>
</div>
<div class="info">
<div>
<p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${dayP}%` }"></div>
</div>
</div>
<div>
<p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${monthP}%` }"></div>
</div>
</div>
<div>
<p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${yearP}%` }"></div>
</div>
</div>
</div>
</div>
<div>
<p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${monthP}%` }"></div>
</div>
</div>
<div>
<p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${yearP}%` }"></div>
</div>
</div>
</div>
</mk-widget-container>
</div>
</template>
@ -111,93 +111,82 @@ export default define({
@import '~const.styl'
root(isDark)
padding 16px 0
color isDark ? #c5ced6 :#777
background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
&[data-special='on-new-years-day']
border-color #ef95a0
&[data-melt]
background transparent
border none
.mkw-calendar--body
padding 16px 0
color isDark ? #c5ced6 : #777
&[data-mobile]
border none
border-radius 8px
box-shadow 0 0 0 1px rgba(#000, 0.2)
&:after
content ""
display block
clear both
&:after
content ""
display block
clear both
> .calendar
float left
width 60%
text-align center
> .calendar
float left
width 60%
text-align center
&[data-is-holiday]
> .day
color #ef95a0
> p
margin 0
line-height 18px
font-size 14px
> span
margin 0 4px
> .day
margin 10px 0
line-height 32px
font-size 28px
> .info
display block
float left
width 40%
padding 0 16px 0 0
> div
margin-bottom 8px
&:last-child
margin-bottom 4px
&[data-is-holiday]
> .day
color #ef95a0
> p
margin 0 0 2px 0
font-size 12px
margin 0
line-height 18px
color isDark ? #7a8692 : #888
font-size 14px
> b
margin-left 2px
> span
margin 0 4px
> .meter
width 100%
overflow hidden
background isDark ? #1c1f25 : #eee
border-radius 8px
> .day
margin 10px 0
line-height 32px
font-size 28px
> .val
height 4px
background $theme-color
> .info
display block
float left
width 40%
padding 0 16px 0 0
&:nth-child(1)
> .meter > .val
background #f7796c
> div
margin-bottom 8px
&:nth-child(2)
> .meter > .val
background #a1de41
&:last-child
margin-bottom 4px
&:nth-child(3)
> .meter > .val
background #41ddde
> p
margin 0 0 2px 0
font-size 12px
line-height 18px
color isDark ? #7a8692 : #888
> b
margin-left 2px
> .meter
width 100%
overflow hidden
background isDark ? #1c1f25 : #eee
border-radius 8px
> .val
height 4px
background $theme-color
&:nth-child(1)
> .meter > .val
background #f7796c
&:nth-child(2)
> .meter > .val
background #a1de41
&:nth-child(3)
> .meter > .val
background #41ddde
.mkw-calendar[data-darkmode]
root(true)

View File

@ -1,10 +1,10 @@
<template>
<div class="mkw-rss" :data-mobile="isMobile">
<div class="mkw-rss">
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:rss-square%RSS</template>
<button slot="func" title="設定" @click="setting">%fa:cog%</button>
<div class="mkw-rss--body">
<div class="mkw-rss--body" :data-mobile="isMobile">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div class="feed" v-else>
<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
@ -85,15 +85,17 @@ root(isDark)
margin-right 4px
&[data-mobile]
background isDark ? #21242f : #f3f3f3
.feed
padding 0
font-size 1em
> a
padding 8px 16px
border-bottom none
&:nth-child(even)
background rgba(#000, 0.05)
background isDark ? rgba(#000, 0.05) : rgba(#fff, 0.7)
.mkw-rss[data-darkmode]
root(true)

View File

@ -50,7 +50,7 @@ export default Vue.extend({
return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
},
background(): string {
return this.file.properties.avgColor
return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
? `rgb(${this.file.properties.avgColor.join(',')})`
: 'transparent';
}
@ -129,7 +129,7 @@ export default Vue.extend({
},
onThumbnailLoaded() {
if (this.file.properties.avgColor) {
if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,

View File

@ -1,34 +1,34 @@
<template>
<div class="mk-home" :data-customize="customize">
<div class="customize" v-if="customize">
<router-link to="/">%fa:check%完了</router-link>
<router-link to="/">%fa:check%%i18n:@done%</router-link>
<div>
<div class="adder">
<p>ウィジェットを追加:</p>
<p>%i18n:@add-widget%</p>
<select v-model="widgetAdderSelected">
<option value="profile">プロフィール</option>
<option value="calendar">カレンダー</option>
<option value="timemachine">カレンダー(タイムマシン)</option>
<option value="activity">アクティビティ</option>
<option value="rss">RSSリーダー</option>
<option value="trends">トレンド</option>
<option value="photo-stream">フォトストリーム</option>
<option value="slideshow">スライドショー</option>
<option value="version">バージョン</option>
<option value="broadcast">ブロードキャスト</option>
<option value="notifications">通知</option>
<option value="users">おすすめユーザー</option>
<option value="polls">投票</option>
<option value="post-form">投稿フォーム</option>
<option value="messaging">メッセージ</option>
<option value="channel">チャンネル</option>
<option value="access-log">アクセスログ</option>
<option value="server">サーバー情報</option>
<option value="donation">寄付のお願い</option>
<option value="nav">ナビゲーション</option>
<option value="tips">ヒント</option>
<option value="profile">%i18n:@profile%</option>
<option value="calendar">%i18n:@calendar%</option>
<option value="timemachine">%i18n:@timemachine%</option>
<option value="activity">%i18n:@activity%</option>
<option value="rss">%i18n:@rss%</option>
<option value="trends">%i18n:@trends%</option>
<option value="photo-stream">%i18n:@photo-stream%</option>
<option value="slideshow">%i18n:@slideshow%</option>
<option value="version">%i18n:@version%</option>
<option value="broadcast">%i18n:@broadcast%</option>
<option value="notifications">%i18n:@notifications%</option>
<option value="users">%i18n:@users%</option>
<option value="polls">%i18n:@polls%</option>
<option value="post-form">%i18n:@post-form%</option>
<option value="messaging">%i18n:@messaging%</option>
<option value="channel">%i18n:@channel%</option>
<option value="access-log">%i18n:@access-log%</option>
<option value="server">%i18n:@server%</option>
<option value="donation">%i18n:@donation%</option>
<option value="nav">%i18n:@nav%</option>
<option value="tips">%i18n:@tips%</option>
</select>
<button @click="addWidget">追加</button>
<button @click="addWidget">%i18n:@add%</button>
</div>
<div class="trash">
<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>

View File

@ -26,7 +26,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}

View File

@ -5,7 +5,7 @@
<header>
<div class="left">
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span class="username">@{{ note.user | acct }}</span>
<span class="username"><mk-acct :user="note.user"/></span>
</div>
<div class="right">
<router-link class="time" :to="note | notePage">

View File

@ -28,7 +28,7 @@
<mk-avatar class="avatar" :user="p.user"/>
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
<span class="username">@{{ p.user | acct }}</span>
<span class="username"><mk-acct :user="p.user"/></span>
<router-link class="time" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>

View File

@ -4,7 +4,7 @@
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span class="username">@{{ note.user | acct }}</span>
<span class="username"><mk-acct :user="note.user"/></span>
<router-link class="time" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
@ -59,17 +59,20 @@ root(isDark)
> .name
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight bold
text-decoration none
white-space normal
text-overflow ellipsis
&:hover
text-decoration underline
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #d1d8da
> .time

View File

@ -4,7 +4,7 @@
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span class="username">@{{ note.user | acct }}</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">

View File

@ -17,7 +17,7 @@
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
<span class="username">@{{ p.user | acct }}</span>
<span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
@ -44,7 +44,7 @@
<div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
<a class="reply" v-if="p.reply">%fa:reply%</a>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
@ -94,6 +94,7 @@
<script lang="ts">
import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify';
import canHideText from '../../../common/scripts/can-hide-text';
import parse from '../../../../../text/parse';
import MkPostFormWindow from './post-form-window.vue';
@ -130,16 +131,17 @@ export default Vue.extend({
},
computed: {
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.mediaIds.length == 0 &&
this.note.poll == null);
},
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.p.reactionCounts
? Object.keys(this.p.reactionCounts)
@ -147,9 +149,11 @@ export default Vue.extend({
.reduce((a, b) => a + b)
: 0;
},
title(): string {
return dateStringify(this.p.createdAt);
},
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@ -205,6 +209,8 @@ export default Vue.extend({
},
methods: {
canHideText,
capture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@ -214,6 +220,7 @@ export default Vue.extend({
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
}
},
decapture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@ -223,9 +230,11 @@ export default Vue.extend({
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
}
},
onStreamConnected() {
this.capture();
},
onStreamNoteUpdated(data) {
const note = data.note;
if (note.id == this.note.id) {
@ -234,28 +243,33 @@ export default Vue.extend({
this.note.renote = note;
}
},
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
});
},
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
});
},
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
},
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p
});
},
onKeydown(e) {
let shouldBeCancel = true;
@ -336,6 +350,7 @@ root(isDark)
align-items center
padding 16px 32px
line-height 28px
white-space pre
color #9dbb00
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)

View File

@ -1,6 +1,6 @@
<template>
<div class="root api">
<p>Token: <code>{{ os.i.token }}</code></p>
<p>%i18n:@token% <code>{{ os.i.token }}</code></p>
<p>%i18n:@intro%</p>
<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div>
<p>%i18n:@regeneration-of-token%</p>

View File

@ -80,10 +80,11 @@
<el-option label="自動" value=""/>
</el-option-group>
<el-option-group label="言語を指定">
<el-option label="ja-JP" value="ja"/>
<el-option label="en-US" value="en"/>
<el-option label="ja" value="ja"/>
<el-option label="en" value="en"/>
<el-option label="fr" value="fr"/>
<el-option label="pl" value="pl"/>
<el-option label="de" value="de"/>
</el-option-group>
</el-select>
<div class="none ui info">
@ -100,7 +101,7 @@
</section>
<section class="notification" v-show="page == 'notification'">
<h1>通知</h1>
<h1>%i18n:@notification%</h1>
<mk-switch v-model="os.i.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします</span>
</mk-switch>
@ -117,7 +118,7 @@
</section>
<section class="apps" v-show="page == 'apps'">
<h1>アプリケーション</h1>
<h1>%i18n:@apps%</h1>
<x-apps/>
</section>
@ -137,7 +138,7 @@
</section>
<section class="signin" v-show="page == 'security'">
<h1>サインイン履歴</h1>
<h1>%i18n:@signin%</h1>
<x-signins/>
</section>

View File

@ -3,7 +3,7 @@
<span slot="header">%fa:list% リスト</span>
<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
<button class="ui" @click="add">リストを作成</button>
<button class="ui" @click="add">%i18n:@create-list%</button>
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
</div>
</mk-window>

View File

@ -10,13 +10,13 @@
<div class="description">{{ u.description }}</div>
<div class="status">
<div>
<p>投稿</p><a>{{ u.notesCount }}</a>
<p>%i18n:@notes%</p><a>{{ u.notesCount }}</a>
</div>
<div>
<p>フォロー</p><a>{{ u.followingCount }}</a>
<p>%i18n:@following%</p><a>{{ u.followingCount }}</a>
</div>
<div>
<p>フォロワー</p><a>{{ u.followersCount }}</a>
<p>%i18n:@followers%</p><a>{{ u.followersCount }}</a>
</div>
</div>
<mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>

View File

@ -2,8 +2,8 @@
<div class="mk-users-list">
<nav>
<div>
<span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
<span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
<span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
<span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@iknow%<span>{{ youKnowCount }}</span></span>
</div>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
@ -12,13 +12,13 @@
</div>
</div>
<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
<span v-if="!moreFetching">もっと</span>
<span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
<span v-if="!moreFetching">%i18n:@load-more%</span>
<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
</button>
<p class="no" v-if="!fetching && users.length == 0">
<slot></slot>
</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
</div>
</template>

View File

@ -29,7 +29,7 @@ export default Vue.extend({
style(): any {
if (this.user.bannerUrl == null) return {};
return {
backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}

View File

@ -1,43 +1,24 @@
import PostForm from '../views/components/post-form.vue';
//import RenoteForm from '../views/components/renote-form.vue';
import getNoteSummary from '../../../../renderers/get-note-summary';
export default (os) => (opts) => {
const o = opts || {};
if (o.renote) {
/*const vm = new RenoteForm({
propsData: {
renote: o.renote
}
}).$mount();
vm.$once('cancel', recover);
vm.$once('note', recover);
document.body.appendChild(vm.$el);*/
const app = document.getElementById('app');
app.style.display = 'none';
const text = window.prompt(`${getNoteSummary(o.renote)}」をRenote`);
if (text == null) return;
os.api('notes/create', {
renoteId: o.renote.id,
text: text == '' ? undefined : text
});
} else {
const app = document.getElementById('app');
app.style.display = 'none';
function recover() {
app.style.display = 'block';
}
const vm = new PostForm({
parent: os.app,
propsData: {
reply: o.reply
}
}).$mount();
vm.$once('cancel', recover);
vm.$once('note', recover);
document.body.appendChild(vm.$el);
(vm as any).focus();
function recover() {
app.style.display = 'block';
}
const vm = new PostForm({
parent: os.app,
propsData: {
reply: o.reply,
renote: o.renote
}
}).$mount();
vm.$once('cancel', recover);
vm.$once('note', recover);
document.body.appendChild(vm.$el);
(vm as any).focus();
};

View File

@ -23,6 +23,7 @@ import MkUser from './views/pages/user.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
import MkNotifications from './views/pages/notifications.vue';
import MkWidgets from './views/pages/widgets.vue';
import MkMessaging from './views/pages/messaging.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
@ -56,6 +57,7 @@ init((launch) => {
{ path: '/i/settings', component: MkSettings },
{ path: '/i/settings/profile', component: MkProfileSetting },
{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', name: 'drive', component: MkDrive },

View File

@ -86,7 +86,7 @@ export default Vue.extend({
return this.file.type.split('/')[0];
},
style(): any {
return this.file.properties.avgColor ? {
return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
} : {};
}

View File

@ -42,7 +42,7 @@ export default Vue.extend({
},
thumbnail(): any {
return {
'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
'background-image': `url(${this.file.url}?thumbnail&size=128)`
};
}

View File

@ -18,7 +18,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}

View File

@ -69,8 +69,9 @@ root(isDark)
text-decoration underline
> .username
text-align left
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #d1d8da
> .time

View File

@ -41,7 +41,7 @@
<div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
<a class="reply" v-if="p.reply">%fa:reply%</a>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
@ -85,6 +85,7 @@
<script lang="ts">
import Vue from 'vue';
import parse from '../../../../../text/parse';
import canHideText from '../../../common/scripts/can-hide-text';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
@ -112,9 +113,11 @@ export default Vue.extend({
this.note.mediaIds.length == 0 &&
this.note.poll == null);
},
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.p.reactionCounts
? Object.keys(this.p.reactionCounts)
@ -122,6 +125,7 @@ export default Vue.extend({
.reduce((a, b) => a + b)
: 0;
},
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@ -177,6 +181,8 @@ export default Vue.extend({
},
methods: {
canHideText,
capture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@ -186,6 +192,7 @@ export default Vue.extend({
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
}
},
decapture(withHandler = false) {
if ((this as any).os.isSignedIn) {
this.connection.send({
@ -195,9 +202,11 @@ export default Vue.extend({
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
}
},
onStreamConnected() {
this.capture();
},
onStreamNoteUpdated(data) {
const note = data.note;
if (note.id == this.note.id) {
@ -206,16 +215,19 @@ export default Vue.extend({
this.note.renote = note;
}
},
reply() {
(this as any).apis.post({
reply: this.p
});
},
renote() {
(this as any).apis.post({
renote: this.p
});
},
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
@ -223,6 +235,7 @@ export default Vue.extend({
compact: true
});
},
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@ -255,6 +268,7 @@ root(isDark)
align-items center
padding 8px 16px
line-height 28px
white-space pre
color #9dbb00
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)

View File

@ -1,7 +1,5 @@
<template>
<div class="mk-notes">
<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
<slot name="head"></slot>
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
@ -71,6 +69,16 @@ export default Vue.extend({
}
},
watch: {
queue(x) {
if (x.length > 0) {
this.$store.commit('indicate', true);
} else {
this.$store.commit('indicate', false);
}
}
},
mounted() {
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
window.addEventListener('scroll', this.onScroll);
@ -238,13 +246,6 @@ root(isDark)
[data-fa]
margin-right 8px
> .newer-indicator
position -webkit-sticky
position sticky
z-index 100
height 3px
background $theme-color
> .init
padding 64px 0
text-align center

View File

@ -5,17 +5,22 @@
<div>
<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
<button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:!@submit%' }}</button>
<button class="submit" :disabled="posting" @click="post">
<template v-if="reply">%i18n:@reply%</template>
<template v-else-if="renote">%i18n:@renote%</template>
<template v-else>%i18n:@submit%</template>
</button>
</div>
</header>
<div class="form">
<mk-note-preview v-if="reply" :note="reply"/>
<mk-note-preview v-if="renote" :note="renote"/>
<div v-if="visibility == 'specified'" class="visibleUsers">
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
<a @click="addVisibleUser">+ユーザーを追加</a>
</div>
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : renote ? '%i18n:!@renote-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
<div class="file" v-for="file in files" :key="file.id">
@ -51,7 +56,7 @@ export default Vue.extend({
MkVisibilityChooser
},
props: ['reply'],
props: ['reply', 'renote'],
data() {
return {
@ -177,6 +182,7 @@ export default Vue.extend({
text: this.text == '' ? undefined : this.text,
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? {

View File

@ -13,6 +13,7 @@
<slot name="func"></slot>
</div>
</div>
<div class="indicator" v-show="$store.state.indicate"></div>
</div>
</template>
@ -156,6 +157,10 @@ root(isDark)
&, *
user-select none
> .indicator
height 3px
background $theme-color
> .main
color rgba(#fff, 0.9)

View File

@ -21,6 +21,7 @@
<li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li>
</ul>
<ul>
<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'">%fa:quidditch%%i18n:@widgets%%fa:angle-right%</router-link></li>
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li>
</ul>
<ul>

View File

@ -25,27 +25,27 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
.mk-widget-container
background #eee
root(isDark)
background isDark ? #21242f : #eee
border-radius 8px
box-shadow 0 0 0 1px rgba(#000, 0.2)
box-shadow 0 4px 16px rgba(#000, 0.1)
overflow hidden
&.hideHeader
background #fff
&.naked
background transparent !important
box-shadow none !important
&.hideHeader
background isDark ? #21242f : #fff
> header
> .title
margin 0
padding 8px 10px
font-size 15px
font-weight normal
color #465258
background #fff
color isDark ? #b8c5cc : #465258
background isDark ? #282c37 : #fff
border-radius 8px 8px 0 0
> [data-fa]
@ -65,4 +65,10 @@ export default Vue.extend({
font-size 15px
color #465258
.mk-widget-container[data-darkmode]
root(true)
.mk-widget-container:not([data-darkmode])
root(false)
</style>

View File

@ -84,7 +84,7 @@ export default Vue.extend({
style(): any {
if (this.user.bannerUrl == null) return {};
return {
backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}

View File

@ -40,7 +40,7 @@
</x-draggable>
</template>
<template v-else>
<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true"/>
</template>
</main>
</mk-ui>
@ -55,17 +55,24 @@ export default Vue.extend({
components: {
XDraggable
},
data() {
return {
showNav: false,
widgets: [],
customizing: false,
widgetAdderSelected: null
};
},
computed: {
widgets(): any[] {
return this.$store.state.settings.data.mobileHome;
}
},
created() {
if ((this as any).clientSettings.mobileHome == null) {
Vue.set((this as any).clientSettings, 'mobileHome', [{
if (this.widgets.length == 0) {
this.widgets = [{
name: 'calendar',
id: 'a', data: {}
}, {
@ -86,18 +93,9 @@ export default Vue.extend({
}, {
name: 'version',
id: 'g', data: {}
}]);
this.widgets = (this as any).clientSettings.mobileHome;
}];
this.saveHome();
} else {
this.widgets = (this as any).clientSettings.mobileHome;
}
this.$watch('clientSettings', i => {
this.widgets = (this as any).clientSettings.mobileHome;
}, {
deep: true
});
},
mounted() {
@ -105,46 +103,33 @@ export default Vue.extend({
},
methods: {
onHomeUpdated(data) {
if (data.home) {
(this as any).clientSettings.mobileHome = data.home;
this.widgets = data.home;
} else {
const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
if (w != null) {
w.data = data.data;
this.$refs[w.id][0].preventSave = true;
this.$refs[w.id][0].props = w.data;
this.widgets = (this as any).clientSettings.mobileHome;
}
}
},
hint() {
alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
},
widgetFunc(id) {
const w = this.$refs[id][0];
if (w.func) w.func();
},
onWidgetSort() {
this.saveHome();
},
addWidget() {
const widget = {
this.$store.dispatch('settings/addMobileHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
data: {}
};
});
},
this.widgets.unshift(widget);
this.saveHome();
},
removeWidget(widget) {
this.widgets = this.widgets.filter(w => w.id != widget.id);
this.saveHome();
this.$store.dispatch('settings/removeMobileHomeWidget', widget);
},
saveHome() {
(this as any).clientSettings.mobileHome = this.widgets;
this.$store.commit('settings/setMobileHome', this.widgets);
(this as any).api('i/update_mobile_home', {
home: this.widgets
});
@ -156,17 +141,25 @@ export default Vue.extend({
<style lang="stylus" scoped>
main
margin 0 auto
padding 8px
max-width 500px
width 100%
@media (min-width 500px)
padding 8px
padding 16px 8px
@media (min-width 600px)
padding 32px 8px
> header
padding 8px
background #fff
.widget
margin 8px
margin-bottom 8px
@media (min-width 600px)
margin-bottom 16px
.customize-container
margin 8px

View File

@ -3,6 +3,7 @@ import MiOS from './mios';
const defaultSettings = {
home: [],
mobileHome: [],
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,
@ -23,10 +24,15 @@ export default (os: MiOS) => new Vuex.Store({
}],
state: {
indicate: false,
uiHeaderHeight: 0
},
mutations: {
indicate(state, x) {
state.indicate = x;
},
setUiHeaderHeight(state, height) {
state.uiHeaderHeight = height;
}
@ -58,6 +64,25 @@ export default (os: MiOS) => new Vuex.Store({
addHomeWidget(state, widget) {
state.data.home.unshift(widget);
},
setMobileHome(state, data) {
state.data.mobileHome = data;
},
setMobileHomeWidget(state, x) {
const w = state.data.mobileHome.find(w => w.id == x.id);
if (w) {
w.data = x.data;
}
},
addMobileHomeWidget(state, widget) {
state.data.mobileHome.unshift(widget);
},
removeMobileHomeWidget(state, widget) {
state.data.mobileHome = state.data.mobileHome.filter(w => w.id != widget.id);
}
},
@ -85,6 +110,22 @@ export default (os: MiOS) => new Vuex.Store({
os.api('i/update_home', {
home: ctx.state.data.home
});
},
addMobileHomeWidget(ctx, widget) {
ctx.commit('addMobileHomeWidget', widget);
os.api('i/update_mobile_home', {
home: ctx.state.data.mobileHome
});
},
removeMobileHomeWidget(ctx, widget) {
ctx.commit('removeMobileHomeWidget', widget);
os.api('i/update_mobile_home', {
home: ctx.state.data.mobileHome.filter(w => w.id != widget.id)
});
}
}
}

View File

@ -127,7 +127,7 @@ gulp.task('doc:api:endpoints', async () => {
return;
}
const i18n = new I18nReplacer(lang);
html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {
@ -171,7 +171,7 @@ gulp.task('doc:api:entities', async () => {
return;
}
const i18n = new I18nReplacer(lang);
html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {

View File

@ -53,7 +53,7 @@ gulp.task('doc:docs', async () => {
return;
}
const i18n = new I18nReplacer(lang);
html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/${name}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {

View File

@ -154,6 +154,7 @@ export const pack = (
_target = Object.assign(_target, _file.metadata);
_target.src = _file.metadata.url;
_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
if (_target.properties == null) _target.properties = {};

View File

@ -2,14 +2,20 @@ import * as Koa from 'koa';
import summaly from 'summaly';
module.exports = async (ctx: Koa.Context) => {
const summary = await summaly(ctx.query.url);
summary.icon = wrap(summary.icon);
summary.thumbnail = wrap(summary.thumbnail);
try {
const summary = await summaly(ctx.query.url, {
followRedirects: false
});
summary.icon = wrap(summary.icon);
summary.thumbnail = wrap(summary.thumbnail);
// Cache 7days
ctx.set('Cache-Control', 'max-age=604800, immutable');
// Cache 7days
ctx.set('Cache-Control', 'max-age=604800, immutable');
ctx.body = summary;
ctx.body = summary;
} catch (e) {
ctx.status = 500;
}
};
function wrap(url: string): string {

View File

@ -171,6 +171,9 @@ const addFile = async (
log('calculate average color...');
const info = await prominence(gm(fs.createReadStream(path), name)).identify();
const isTransparent = info ? info['Channel depth'].Alpha != null : false;
const buffer = await prominence(gm(fs.createReadStream(path), name)
.setFormat('ppm')
.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
@ -182,7 +185,7 @@ const addFile = async (
log(`average color is calculated: ${r}, ${g}, ${b}`);
return [r, g, b];
return isTransparent ? [r, g, b, 255] : [r, g, b];
})(),
// folder
(async () => {

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,7 @@
/**
* Text Tests!
*/
import * as assert from 'assert';
const assert = require('assert');
const analyze = require('../built/text/parse').default;
const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter').default;
import analyze from '../src/text/parse';
import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter';
describe('Text', () => {
it('can be analyzed', () => {

View File

@ -110,14 +110,14 @@ const plugins = [
//#region i18n
langs.forEach(lang => {
Object.keys(entry).forEach(file => {
let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf8');
let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf-8');
const i18nReplacer = new I18nReplacer(lang);
src = src.replace(i18nReplacer.pattern, i18nReplacer.replacement);
src = src.replace('%lang%', lang);
fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf8');
fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf-8');
});
});
//#endregion
@ -146,27 +146,20 @@ module.exports = {
}, {
loader: 'replace',
query: {
search: /%base64:(.+?)%/g.toString(),
replace: 'base64replacement'
}
}, {
loader: 'replace',
query: {
search: i18nPattern.toString(),
replace: 'i18nReplacement',
i18n: true
}
}, {
loader: 'replace',
query: {
search: faPattern.toString(),
replace: 'faReplacement'
}
}, {
loader: 'replace',
query: {
search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
replace: 'collapseSpacesReplacement'
qs: [{
search: /%base64:(.+?)%/g.toString(),
replace: 'base64replacement'
}, {
search: i18nPattern.toString(),
replace: 'i18nReplacement',
i18n: true
}, {
search: faPattern.toString(),
replace: 'faReplacement'
}, {
search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
replace: 'collapseSpacesReplacement'
}]
}
}]
}, {

View File

@ -1,22 +1,30 @@
const loaderUtils = require('loader-utils');
import { getOptions } from 'loader-utils';
function trim(text, g) {
return text.substring(1, text.length - (g ? 2 : 0));
}
module.exports = function(src) {
export default function(src) {
const fn = options => {
const search = options.search;
const g = search[search.length - 1] == 'g';
const file = this.resourcePath.replace(/\\/g, '/');
const replace = options.i18n ? global[options.replace].bind(null, {
src: file,
lang: options.lang
}) : global[options.replace];
if (typeof search != 'string' || search.length == 0) console.error('invalid search');
if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
};
this.cacheable();
const options = loaderUtils.getOptions(this);
const search = options.search;
const g = search[search.length - 1] == 'g';
const file = this.resourcePath.replace(/\\/g, '/');
const replace = options.i18n ? global[options.replace].bind(null, {
src: file,
lang: options.lang
}) : global[options.replace];
if (typeof search != 'string' || search.length == 0) console.error('invalid search');
if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
const options = getOptions(this);
if (options.qs) {
options.qs.forEach(q => fn(q));
} else {
fn(options);
}
this.callback(null, src);
return src;
};
}