This commit is contained in:
syuilo 2018-02-21 02:53:34 +09:00
parent b2a6257f93
commit a1e57841e7
22 changed files with 304 additions and 372 deletions

View file

@ -13,6 +13,7 @@
"vue/html-self-closing": false, "vue/html-self-closing": false,
"vue/no-unused-vars": false, "vue/no-unused-vars": false,
"no-console": 0, "no-console": 0,
"no-unused-vars": 0 "no-unused-vars": 0,
"no-empty": 0
} }
} }

View file

@ -16,6 +16,38 @@ declare const _API_URL_: string;
declare const _SW_PUBLICKEY_: string; declare const _SW_PUBLICKEY_: string;
//#endregion //#endregion
export type API = {
chooseDriveFile: (opts: {
title?: string;
currentFolder?: any;
multiple?: boolean;
}) => Promise<any>;
chooseDriveFolder: (opts: {
title?: string;
currentFolder?: any;
}) => Promise<any>;
dialog: (opts: {
title: string;
text: string;
actions: Array<{
text: string;
id?: string;
}>;
}) => Promise<string>;
input: (opts: {
title: string;
placeholder?: string;
default?: string;
}) => Promise<string>;
post: () => void;
notify: (message: string) => void;
};
/** /**
* Misskey Operating System * Misskey Operating System
*/ */
@ -49,6 +81,8 @@ export default class MiOS extends EventEmitter {
return localStorage.getItem('debug') == 'true'; return localStorage.getItem('debug') == 'true';
} }
public apis: API;
/** /**
* A connection manager of home stream * A connection manager of home stream
*/ */

View file

@ -0,0 +1,21 @@
require('fuckadblock');
declare const fuckAdBlock: any;
export default (os) => {
function adBlockDetected() {
os.apis.dialog({
title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください',
text: '<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。',
actins: [{
text: 'OK'
}]
});
}
if (fuckAdBlock === undefined) {
adBlockDetected();
} else {
fuckAdBlock.onDetected(adBlockDetected);
}
};

View file

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

View file

@ -0,0 +1,95 @@
import OS from '../../common/mios';
import { apiUrl } from '../../config';
import CropWindow from '../views/components/crop-window.vue';
import ProgressDialog from '../views/components/progress-dialog.vue';
export default (os: OS) => (cb, file = null) => {
const fileSelected = file => {
const w = new CropWindow({
propsData: {
file: file,
title: 'アバターとして表示する部分を選択',
aspectRatio: 1 / 1
}
}).$mount();
w.$once('cropped', blob => {
const data = new FormData();
data.append('i', os.i.token);
data.append('file', blob, file.name + '.cropped.png');
os.api('drive/folders/find', {
name: 'アイコン'
}).then(iconFolder => {
if (iconFolder.length === 0) {
os.api('drive/folders/create', {
name: 'アイコン'
}).then(iconFolder => {
upload(data, iconFolder);
});
} else {
upload(data, iconFolder[0]);
}
});
});
w.$once('skipped', () => {
set(file);
});
document.body.appendChild(w.$el);
};
const upload = (data, folder) => {
const dialog = new ProgressDialog({
propsData: {
title: '新しいアバターをアップロードしています'
}
}).$mount();
document.body.appendChild(dialog.$el);
if (folder) data.append('folder_id', folder.id);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = e => {
const file = JSON.parse((e.target as any).response);
(dialog as any).close();
set(file);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) (dialog as any).updateProgress(e.loaded, e.total);
};
xhr.send(data);
};
const set = file => {
os.api('i/update', {
avatar_id: file.id
}).then(i => {
os.apis.dialog({
title: '%fa:info-circle%アバターを更新しました',
text: '新しいアバターが反映されるまで時間がかかる場合があります。',
actions: [{
text: 'わかった'
}]
});
if (cb) cb(i);
});
};
if (file) {
fileSelected(file);
} else {
os.apis.chooseDriveFile({
multiple: false,
title: '%fa:image%アバターにする画像を選択'
}).then(file => {
fileSelected(file);
});
}
};

View file

@ -0,0 +1,95 @@
import OS from '../../common/mios';
import { apiUrl } from '../../config';
import CropWindow from '../views/components/crop-window.vue';
import ProgressDialog from '../views/components/progress-dialog.vue';
export default (os: OS) => (cb, file = null) => {
const fileSelected = file => {
const w = new CropWindow({
propsData: {
file: file,
title: 'バナーとして表示する部分を選択',
aspectRatio: 16 / 9
}
}).$mount();
w.$once('cropped', blob => {
const data = new FormData();
data.append('i', os.i.token);
data.append('file', blob, file.name + '.cropped.png');
os.api('drive/folders/find', {
name: 'バナー'
}).then(bannerFolder => {
if (bannerFolder.length === 0) {
os.api('drive/folders/create', {
name: 'バナー'
}).then(iconFolder => {
upload(data, iconFolder);
});
} else {
upload(data, bannerFolder[0]);
}
});
});
w.$once('skipped', () => {
set(file);
});
document.body.appendChild(w.$el);
};
const upload = (data, folder) => {
const dialog = new ProgressDialog({
propsData: {
title: '新しいバナーをアップロードしています'
}
}).$mount();
document.body.appendChild(dialog.$el);
if (folder) data.append('folder_id', folder.id);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = e => {
const file = JSON.parse((e.target as any).response);
(dialog as any).close();
set(file);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) (dialog as any).updateProgress(e.loaded, e.total);
};
xhr.send(data);
};
const set = file => {
os.api('i/update', {
avatar_id: file.id
}).then(i => {
os.apis.dialog({
title: '%fa:info-circle%バナーを更新しました',
text: '新しいバナーが反映されるまで時間がかかる場合があります。',
actions: [{
text: 'わかった'
}]
});
if (cb) cb(i);
});
};
if (file) {
fileSelected(file);
} else {
os.apis.chooseDriveFile({
multiple: false,
title: '%fa:image%バナーにする画像を選択'
}).then(file => {
fileSelected(file);
});
}
};

View file

@ -6,7 +6,7 @@
import './style.styl'; import './style.styl';
import init from '../init'; import init from '../init';
import fuckAdBlock from './scripts/fuck-ad-block'; import fuckAdBlock from '../common/scripts/fuck-ad-block';
import HomeStreamManager from '../common/scripts/streaming/home-stream-manager'; import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
import composeNotification from '../common/scripts/compose-notification'; import composeNotification from '../common/scripts/compose-notification';
@ -15,6 +15,9 @@ import chooseDriveFile from './api/choose-drive-file';
import dialog from './api/dialog'; import dialog from './api/dialog';
import input from './api/input'; import input from './api/input';
import post from './api/post'; import post from './api/post';
import notify from './api/notify';
import updateAvatar from './api/update-avatar';
import updateBanner from './api/update-banner';
import MkIndex from './views/pages/index.vue'; import MkIndex from './views/pages/index.vue';
import MkUser from './views/pages/user/user.vue'; import MkUser from './views/pages/user/user.vue';
@ -25,24 +28,27 @@ import MkDrive from './views/pages/drive.vue';
* init * init
*/ */
init(async (launch) => { init(async (launch) => {
/**
* Fuck AD Block
*/
fuckAdBlock();
// Register directives // Register directives
require('./views/directives'); require('./views/directives');
// Register components // Register components
require('./views/components'); require('./views/components');
const app = launch({ const [app, os] = launch(os => ({
chooseDriveFolder, chooseDriveFolder,
chooseDriveFile, chooseDriveFile,
dialog, dialog,
input, input,
post post,
}); notify,
updateAvatar: updateAvatar(os),
updateBanner: updateBanner(os)
}));
/**
* Fuck AD Block
*/
fuckAdBlock(os);
/** /**
* Init Notification * Init Notification

View file

@ -1,16 +0,0 @@
import * as riot from 'riot';
export default (title, text, buttons, canThrough?, onThrough?) => {
const dialog = document.body.appendChild(document.createElement('mk-dialog'));
const controller = riot.observable();
(riot as any).mount(dialog, {
controller: controller,
title: title,
text: text,
buttons: buttons,
canThrough: canThrough,
onThrough: onThrough
});
controller.trigger('open');
return controller;
};

View file

@ -1,20 +0,0 @@
require('fuckadblock');
import dialog from './dialog';
declare const fuckAdBlock: any;
export default () => {
if (fuckAdBlock === undefined) {
adBlockDetected();
} else {
fuckAdBlock.onDetected(adBlockDetected);
}
};
function adBlockDetected() {
dialog('%fa:exclamation-triangle%広告ブロッカーを無効にしてください',
'<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。',
[{
text: 'OK'
}]);
}

View file

@ -1,12 +0,0 @@
import * as riot from 'riot';
export default (title, placeholder, defaultValue, onOk, onCancel) => {
const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
return (riot as any).mount(dialog, {
title: title,
placeholder: placeholder,
'default': defaultValue,
onOk: onOk,
onCancel: onCancel
});
};

View file

@ -1,8 +0,0 @@
import dialog from './dialog';
export default () => {
dialog('%fa:exclamation-triangle%Not implemented yet',
'要求された操作は実装されていません。<br>→<a href="https://github.com/syuilo/misskey" target="_blank">Misskeyの開発に参加する</a>', [{
text: 'OK'
}]);
};

View file

@ -1,8 +0,0 @@
import * as riot from 'riot';
export default message => {
const notification = document.body.appendChild(document.createElement('mk-ui-notification'));
(riot as any).mount(notification, {
message: message
});
};

View file

@ -1,61 +0,0 @@
/**
*
*/
export default class ScrollFollower {
private follower: Element;
private containerTop: number;
private topPadding: number;
constructor(follower: Element, topPadding: number) {
//#region
this.follow = this.follow.bind(this);
//#endregion
this.follower = follower;
this.containerTop = follower.getBoundingClientRect().top;
this.topPadding = topPadding;
window.addEventListener('scroll', this.follow);
window.addEventListener('resize', this.follow);
}
/**
*
*/
public dispose() {
window.removeEventListener('scroll', this.follow);
window.removeEventListener('resize', this.follow);
}
private follow() {
const windowBottom = window.scrollY + window.innerHeight;
const windowTop = window.scrollY + this.topPadding;
const rect = this.follower.getBoundingClientRect();
const followerBottom = (rect.top + window.scrollY) + rect.height;
const screenHeight = window.innerHeight - this.topPadding;
// スクロールの上部(+余白)がフォロワーコンテナの上部よりも上方にある
if (window.scrollY + this.topPadding < this.containerTop) {
// フォロワーをコンテナの最上部に合わせる
(this.follower.parentNode as any).style.marginTop = '0px';
return;
}
// スクロールの下部がフォロワーの下部よりも下方にある かつ 表示領域の縦幅がフォロワーの縦幅よりも狭い
if (windowBottom > followerBottom && rect.height > screenHeight) {
// フォロワーの下部をスクロール下部に合わせる
const top = (windowBottom - rect.height) - this.containerTop;
(this.follower.parentNode as any).style.marginTop = `${top}px`;
return;
}
// スクロールの上部(+余白)がフォロワーの上部よりも上方にある または 表示領域の縦幅がフォロワーの縦幅よりも広い
if (windowTop < rect.top + window.scrollY || rect.height < screenHeight) {
// フォロワーの上部をスクロール上部(+余白)に合わせる
const top = windowTop - this.containerTop;
(this.follower.parentNode as any).style.marginTop = `${top}px`;
return;
}
}
}

View file

@ -1,88 +0,0 @@
declare const _API_URL_: string;
import * as riot from 'riot';
import dialog from './dialog';
import api from '../../common/scripts/api';
export default (I, cb, file = null) => {
const fileSelected = file => {
const cropper = (riot as any).mount(document.body.appendChild(document.createElement('mk-crop-window')), {
file: file,
title: 'アバターとして表示する部分を選択',
aspectRatio: 1 / 1
})[0];
cropper.on('cropped', blob => {
const data = new FormData();
data.append('i', I.token);
data.append('file', blob, file.name + '.cropped.png');
api(I, 'drive/folders/find', {
name: 'アイコン'
}).then(iconFolder => {
if (iconFolder.length === 0) {
api(I, 'drive/folders/create', {
name: 'アイコン'
}).then(iconFolder => {
upload(data, iconFolder);
});
} else {
upload(data, iconFolder[0]);
}
});
});
cropper.on('skipped', () => {
set(file);
});
};
const upload = (data, folder) => {
const progress = (riot as any).mount(document.body.appendChild(document.createElement('mk-progress-dialog')), {
title: '新しいアバターをアップロードしています'
})[0];
if (folder) data.append('folder_id', folder.id);
const xhr = new XMLHttpRequest();
xhr.open('POST', _API_URL_ + '/drive/files/create', true);
xhr.onload = e => {
const file = JSON.parse((e.target as any).response);
progress.close();
set(file);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) progress.updateProgress(e.loaded, e.total);
};
xhr.send(data);
};
const set = file => {
api(I, 'i/update', {
avatar_id: file.id
}).then(i => {
dialog('%fa:info-circle%アバターを更新しました',
'新しいアバターが反映されるまで時間がかかる場合があります。',
[{
text: 'わかった'
}]);
if (cb) cb(i);
});
};
if (file) {
fileSelected(file);
} else {
const browser = (riot as any).mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
multiple: false,
title: '%fa:image%アバターにする画像を選択'
})[0];
browser.one('selected', file => {
fileSelected(file);
});
}
};

View file

@ -1,88 +0,0 @@
declare const _API_URL_: string;
import * as riot from 'riot';
import dialog from './dialog';
import api from '../../common/scripts/api';
export default (I, cb, file = null) => {
const fileSelected = file => {
const cropper = (riot as any).mount(document.body.appendChild(document.createElement('mk-crop-window')), {
file: file,
title: 'バナーとして表示する部分を選択',
aspectRatio: 16 / 9
})[0];
cropper.on('cropped', blob => {
const data = new FormData();
data.append('i', I.token);
data.append('file', blob, file.name + '.cropped.png');
api(I, 'drive/folders/find', {
name: 'バナー'
}).then(iconFolder => {
if (iconFolder.length === 0) {
api(I, 'drive/folders/create', {
name: 'バナー'
}).then(iconFolder => {
upload(data, iconFolder);
});
} else {
upload(data, iconFolder[0]);
}
});
});
cropper.on('skipped', () => {
set(file);
});
};
const upload = (data, folder) => {
const progress = (riot as any).mount(document.body.appendChild(document.createElement('mk-progress-dialog')), {
title: '新しいバナーをアップロードしています'
})[0];
if (folder) data.append('folder_id', folder.id);
const xhr = new XMLHttpRequest();
xhr.open('POST', _API_URL_ + '/drive/files/create', true);
xhr.onload = e => {
const file = JSON.parse((e.target as any).response);
progress.close();
set(file);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) progress.updateProgress(e.loaded, e.total);
};
xhr.send(data);
};
const set = file => {
api(I, 'i/update', {
banner_id: file.id
}).then(i => {
dialog('%fa:info-circle%バナーを更新しました',
'新しいバナーが反映されるまで時間がかかる場合があります。',
[{
text: 'わかりました。'
}]);
if (cb) cb(i);
});
};
if (file) {
fileSelected(file);
} else {
const browser = (riot as any).mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
multiple: false,
title: '%fa:image%バナーにする画像を選択'
})[0];
browser.one('selected', file => {
fileSelected(file);
});
}
};

View file

@ -5,7 +5,7 @@
<header v-html="title"></header> <header v-html="title"></header>
<div class="body" v-html="text"></div> <div class="body" v-html="text"></div>
<div class="buttons"> <div class="buttons">
<button v-for="button in buttons" @click="click(button)" :key="button.id">{{ button.text }}</button> <button v-for="button in buttons" @click="click(button)">{{ button.text }}</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -40,7 +40,6 @@ import Vue from 'vue';
import * as Sortable from 'sortablejs'; import * as Sortable from 'sortablejs';
import Autocomplete from '../../scripts/autocomplete'; import Autocomplete from '../../scripts/autocomplete';
import getKao from '../../../common/scripts/get-kao'; import getKao from '../../../common/scripts/get-kao';
import notify from '../../scripts/notify';
export default Vue.extend({ export default Vue.extend({
props: ['reply', 'repost'], props: ['reply', 'repost'],
@ -200,13 +199,13 @@ export default Vue.extend({
this.clear(); this.clear();
this.deleteDraft(); this.deleteDraft();
this.$emit('posted'); this.$emit('posted');
notify(this.repost (this as any).apis.notify(this.repost
? '%i18n:desktop.tags.mk-post-form.reposted%' ? '%i18n:desktop.tags.mk-post-form.reposted%'
: this.reply : this.reply
? '%i18n:desktop.tags.mk-post-form.replied%' ? '%i18n:desktop.tags.mk-post-form.replied%'
: '%i18n:desktop.tags.mk-post-form.posted%'); : '%i18n:desktop.tags.mk-post-form.posted%');
}).catch(err => { }).catch(err => {
notify(this.repost (this as any).apis.notify(this.repost
? '%i18n:desktop.tags.mk-post-form.repost-failed%' ? '%i18n:desktop.tags.mk-post-form.repost-failed%'
: this.reply : this.reply
? '%i18n:desktop.tags.mk-post-form.reply-failed%' ? '%i18n:desktop.tags.mk-post-form.reply-failed%'

View file

@ -16,7 +16,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import notify from '../../scripts/notify';
export default Vue.extend({ export default Vue.extend({
props: ['post'], props: ['post'],
@ -33,9 +32,9 @@ export default Vue.extend({
repost_id: this.post.id repost_id: this.post.id
}).then(data => { }).then(data => {
this.$emit('posted'); this.$emit('posted');
notify('%i18n:desktop.tags.mk-repost-form.success%'); (this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%');
}).catch(err => { }).catch(err => {
notify('%i18n:desktop.tags.mk-repost-form.failure%'); (this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.failure%');
}).then(() => { }).then(() => {
this.wait = false; this.wait = false;
}); });

View file

@ -27,7 +27,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import notify from '../../scripts/notify';
export default Vue.extend({ export default Vue.extend({
data() { data() {
@ -59,7 +58,7 @@ export default Vue.extend({
description: this.description || null, description: this.description || null,
birthday: this.birthday || null birthday: this.birthday || null
}).then(() => { }).then(() => {
notify('プロフィールを更新しました'); (this as any).apis.notify('プロフィールを更新しました');
}); });
} }
} }

View file

@ -11,24 +11,26 @@ import * as anime from 'animejs';
export default Vue.extend({ export default Vue.extend({
props: ['message'], props: ['message'],
mounted() { mounted() {
anime({ this.$nextTick(() => {
targets: this.$el,
opacity: 1,
translateY: [-64, 0],
easing: 'easeOutElastic',
duration: 500
});
setTimeout(() => {
anime({ anime({
targets: this.$el, targets: this.$el,
opacity: 0, opacity: 1,
translateY: -64, translateY: [-64, 0],
duration: 500, easing: 'easeOutElastic',
easing: 'easeInElastic', duration: 500
complete: () => this.$destroy()
}); });
}, 6000);
setTimeout(() => {
anime({
targets: this.$el,
opacity: 0,
translateY: -64,
duration: 500,
easing: 'easeInElastic',
complete: () => this.$destroy()
});
}, 6000);
});
} }
}); });
</script> </script>

View file

@ -22,7 +22,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import updateBanner from '../../../scripts/update-banner';
export default Vue.extend({ export default Vue.extend({
props: ['user'], props: ['user'],
@ -53,7 +52,7 @@ export default Vue.extend({
onBannerClick() { onBannerClick() {
if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return; if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
updateBanner((this as any).os.i, i => { (this as any).apis.updateBanner((this as any).os.i, i => {
this.user.banner_url = i.banner_url; this.user.banner_url = i.banner_url;
}); });
} }

View file

@ -34,7 +34,7 @@ Vue.mixin({
import App from './app.vue'; import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update'; import checkForUpdate from './common/scripts/check-for-update';
import MiOS from './common/mios'; import MiOS, { API } from './common/mios';
/** /**
* APP ENTRY POINT! * APP ENTRY POINT!
@ -79,59 +79,32 @@ if (localStorage.getItem('should-refresh') == 'true') {
location.reload(true); location.reload(true);
} }
type API = {
chooseDriveFile: (opts: {
title?: string;
currentFolder?: any;
multiple?: boolean;
}) => Promise<any>;
chooseDriveFolder: (opts: {
title?: string;
currentFolder?: any;
}) => Promise<any>;
dialog: (opts: {
title: string;
text: string;
actions: Array<{
text: string;
id: string;
}>;
}) => Promise<string>;
input: (opts: {
title: string;
placeholder?: string;
default?: string;
}) => Promise<string>;
post: () => void;
};
// MiOSを初期化してコールバックする // MiOSを初期化してコールバックする
export default (callback: (launch: (api: API) => Vue) => void, sw = false) => { export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
const os = new MiOS(sw); const os = new MiOS(sw);
os.init(() => { os.init(() => {
// アプリ基底要素マウント // アプリ基底要素マウント
document.body.innerHTML = '<div id="app"></div>'; document.body.innerHTML = '<div id="app"></div>';
const launch = (api: API) => { const launch = (api: (os: MiOS) => API) => {
os.apis = api(os);
Vue.mixin({ Vue.mixin({
created() { created() {
(this as any).os = os; (this as any).os = os;
(this as any).api = os.api; (this as any).api = os.api;
(this as any).apis = api; (this as any).apis = os.apis;
} }
}); });
return new Vue({ const app = new Vue({
router: new VueRouter({ router: new VueRouter({
mode: 'history' mode: 'history'
}), }),
render: createEl => createEl(App) render: createEl => createEl(App)
}).$mount('#app'); }).$mount('#app');
return [app, os] as [Vue, MiOS];
}; };
try { try {