diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2018-03-29 20:32:18 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2018-03-29 20:32:18 +0900 |
| commit | cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f (patch) | |
| tree | 318279530d3392ee40d91968477fc0e78d5cf0f7 /src/server | |
| parent | Update .travis.yml (diff) | |
| download | sharkey-cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f.tar.gz sharkey-cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f.tar.bz2 sharkey-cf33e483f7e6f40e8cbbbc0118a7df70bdaf651f.zip | |
整理した
Diffstat (limited to 'src/server')
541 files changed, 270 insertions, 40491 deletions
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 7b3983a838..8566744831 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -1,7 +1,7 @@ import * as express from 'express'; -import App from './models/app'; -import { default as User, IUser } from './models/user'; -import AccessToken from './models/access-token'; +import App from '../../models/app'; +import { default as User, IUser } from '../../models/user'; +import AccessToken from '../../models/access-token'; import isNativeToken from './common/is-native-token'; export interface IAuthContext { diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts index ec7c935f9e..f84f1f5dca 100644 --- a/src/server/api/bot/core.ts +++ b/src/server/api/bot/core.ts @@ -1,12 +1,12 @@ import * as EventEmitter from 'events'; import * as bcrypt from 'bcryptjs'; -import User, { ILocalAccount, IUser, init as initUser } from '../models/user'; +import User, { ILocalAccount, IUser, init as initUser } from '../../../models/user'; -import getPostSummary from '../../common/get-post-summary'; -import getUserSummary from '../../common/user/get-summary'; -import parseAcct from '../../common/user/parse-acct'; -import getNotificationSummary from '../../common/get-notification-summary'; +import getPostSummary from '../../../common/get-post-summary'; +import getUserSummary from '../../../common/user/get-summary'; +import parseAcct from '../../../common/user/parse-acct'; +import getNotificationSummary from '../../../common/get-notification-summary'; const hmm = [ '?', diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index 1340ac9923..58fbb217bc 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -2,14 +2,14 @@ import * as EventEmitter from 'events'; import * as express from 'express'; import * as request from 'request'; import * as crypto from 'crypto'; -import User from '../../models/user'; +import User from '../../../../models/user'; import config from '../../../../conf'; import BotCore from '../core'; import _redis from '../../../../db/redis'; import prominence = require('prominence'); -import getAcct from '../../../common/user/get-acct'; -import parseAcct from '../../../common/user/parse-acct'; -import getPostSummary from '../../../common/get-post-summary'; +import getAcct from '../../../../common/user/get-acct'; +import parseAcct from '../../../../common/user/parse-acct'; +import getPostSummary from '../../../../common/get-post-summary'; const redis = prominence(_redis); diff --git a/src/server/api/common/drive/add-file.ts b/src/server/api/common/drive/add-file.ts index 21ddd1aae3..4551f55748 100644 --- a/src/server/api/common/drive/add-file.ts +++ b/src/server/api/common/drive/add-file.ts @@ -10,11 +10,11 @@ import * as debug from 'debug'; import fileType = require('file-type'); import prominence = require('prominence'); -import DriveFile, { getGridFSBucket } from '../../models/drive-file'; -import DriveFolder from '../../models/drive-folder'; -import { pack } from '../../models/drive-file'; +import DriveFile, { getGridFSBucket } from '../../../../models/drive-file'; +import DriveFolder from '../../../../models/drive-folder'; +import { pack } from '../../../../models/drive-file'; import event, { publishDriveStream } from '../../event'; -import getAcct from '../../../common/user/get-acct'; +import getAcct from '../../../../common/user/get-acct'; import config from '../../../../conf'; const gm = _gm.subClass({ diff --git a/src/server/api/common/drive/upload_from_url.ts b/src/server/api/common/drive/upload_from_url.ts index 5dd9695936..b825e4c531 100644 --- a/src/server/api/common/drive/upload_from_url.ts +++ b/src/server/api/common/drive/upload_from_url.ts @@ -1,5 +1,5 @@ import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../../models/drive-file'; +import { IDriveFile, validateFileName } from '../../../../models/drive-file'; import create from './add-file'; import * as debug from 'debug'; import * as tmp from 'tmp'; diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts index 7f548b3bbf..e0942e0292 100644 --- a/src/server/api/common/get-friends.ts +++ b/src/server/api/common/get-friends.ts @@ -1,5 +1,5 @@ import * as mongodb from 'mongodb'; -import Following from '../models/following'; +import Following from '../../../models/following'; export default async (me: mongodb.ObjectID, includeMe: boolean = true) => { // Fetch relation to other users who the I follows diff --git a/src/server/api/common/notify.ts b/src/server/api/common/notify.ts index c4df17f880..f90506cf3c 100644 --- a/src/server/api/common/notify.ts +++ b/src/server/api/common/notify.ts @@ -1,8 +1,8 @@ import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import Mute from '../models/mute'; +import Notification from '../../../models/notification'; +import Mute from '../../../models/mute'; import event from '../event'; -import { pack } from '../models/notification'; +import { pack } from '../../../models/notification'; export default ( notifiee: mongo.ObjectID, diff --git a/src/server/api/common/push-sw.ts b/src/server/api/common/push-sw.ts index e5fbec10ec..13227af8d5 100644 --- a/src/server/api/common/push-sw.ts +++ b/src/server/api/common/push-sw.ts @@ -1,6 +1,6 @@ const push = require('web-push'); import * as mongo from 'mongodb'; -import Subscription from '../models/sw-subscription'; +import Subscription from '../../../models/sw-subscription'; import config from '../../../conf'; if (config.sw) { diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 9047edec8c..f728130bb3 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,6 +1,6 @@ import * as mongo from 'mongodb'; -import Message from '../models/messaging-message'; -import { IMessagingMessage as IMessage } from '../models/messaging-message'; +import Message from '../../../models/messaging-message'; +import { IMessagingMessage as IMessage } from '../../../models/messaging-message'; import publishUserStream from '../event'; import { publishMessagingStream } from '../event'; import { publishMessagingIndexStream } from '../event'; diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index 5bbf136323..27632c7ecd 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -1,5 +1,5 @@ import * as mongo from 'mongodb'; -import { default as Notification, INotification } from '../models/notification'; +import { default as Notification, INotification } from '../../../models/notification'; import publishUserStream from '../event'; /** diff --git a/src/server/api/common/text/core/syntax-highlighter.ts b/src/server/api/common/text/core/syntax-highlighter.ts deleted file mode 100644 index c0396b1fc6..0000000000 --- a/src/server/api/common/text/core/syntax-highlighter.ts +++ /dev/null @@ -1,334 +0,0 @@ -function escape(text) { - return text - .replace(/>/g, '>') - .replace(/</g, '<'); -} - -// 文字数が多い順にソートします -// そうしないと、「function」という文字列が与えられたときに「func」が先にマッチしてしまう可能性があるためです -const _keywords = [ - 'true', - 'false', - 'null', - 'nil', - 'undefined', - 'void', - 'var', - 'const', - 'let', - 'mut', - 'dim', - 'if', - 'then', - 'else', - 'switch', - 'match', - 'case', - 'default', - 'for', - 'each', - 'in', - 'while', - 'loop', - 'continue', - 'break', - 'do', - 'goto', - 'next', - 'end', - 'sub', - 'throw', - 'try', - 'catch', - 'finally', - 'enum', - 'delegate', - 'function', - 'func', - 'fun', - 'fn', - 'return', - 'yield', - 'async', - 'await', - 'require', - 'include', - 'import', - 'imports', - 'export', - 'exports', - 'from', - 'as', - 'using', - 'use', - 'internal', - 'module', - 'namespace', - 'where', - 'select', - 'struct', - 'union', - 'new', - 'delete', - 'this', - 'super', - 'base', - 'class', - 'interface', - 'abstract', - 'static', - 'public', - 'private', - 'protected', - 'virtual', - 'partial', - 'override', - 'extends', - 'implements', - 'constructor' -]; - -const keywords = _keywords - .concat(_keywords.map(k => k[0].toUpperCase() + k.substr(1))) - .concat(_keywords.map(k => k.toUpperCase())) - .sort((a, b) => b.length - a.length); - -const symbols = [ - '=', - '+', - '-', - '*', - '/', - '%', - '~', - '^', - '&', - '|', - '>', - '<', - '!', - '?' -]; - -const elements = [ - // comment - code => { - if (code.substr(0, 2) != '//') return null; - const match = code.match(/^\/\/(.+?)(\n|$)/); - if (!match) return null; - const comment = match[0]; - return { - html: `<span class="comment">${escape(comment)}</span>`, - next: comment.length - }; - }, - - // block comment - code => { - const match = code.match(/^\/\*([\s\S]+?)\*\//); - if (!match) return null; - return { - html: `<span class="comment">${escape(match[0])}</span>`, - next: match[0].length - }; - }, - - // string - code => { - if (!/^['"`]/.test(code)) return null; - const begin = code[0]; - let str = begin; - let thisIsNotAString = false; - for (let i = 1; i < code.length; i++) { - const char = code[i]; - if (char == '\\') { - str += char; - str += code[i + 1] || ''; - i++; - continue; - } else if (char == begin) { - str += char; - break; - } else if (char == '\n' || i == (code.length - 1)) { - thisIsNotAString = true; - break; - } else { - str += char; - } - } - if (thisIsNotAString) { - return null; - } else { - return { - html: `<span class="string">${escape(str)}</span>`, - next: str.length - }; - } - }, - - // regexp - code => { - if (code[0] != '/') return null; - let regexp = ''; - let thisIsNotARegexp = false; - for (let i = 1; i < code.length; i++) { - const char = code[i]; - if (char == '\\') { - regexp += char; - regexp += code[i + 1] || ''; - i++; - continue; - } else if (char == '/') { - break; - } else if (char == '\n' || i == (code.length - 1)) { - thisIsNotARegexp = true; - break; - } else { - regexp += char; - } - } - - if (thisIsNotARegexp) return null; - if (regexp == '') return null; - if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null; - - return { - html: `<span class="regexp">/${escape(regexp)}/</span>`, - next: regexp.length + 2 - }; - }, - - // label - code => { - if (code[0] != '@') return null; - const match = code.match(/^@([a-zA-Z_-]+?)\n/); - if (!match) return null; - const label = match[0]; - return { - html: `<span class="label">${label}</span>`, - next: label.length - }; - }, - - // number - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - if (!/^[\-\+]?[0-9\.]+/.test(code)) return null; - const match = code.match(/^[\-\+]?[0-9\.]+/)[0]; - if (match) { - return { - html: `<span class="number">${match}</span>`, - next: match.length - }; - } else { - return null; - } - }, - - // nan - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - if (code.substr(0, 3) == 'NaN') { - return { - html: `<span class="nan">NaN</span>`, - next: 3 - }; - } else { - return null; - } - }, - - // method - code => { - const match = code.match(/^([a-zA-Z_-]+?)\(/); - if (!match) return null; - - if (match[1] == '-') return null; - - return { - html: `<span class="method">${match[1]}</span>`, - next: match[1].length - }; - }, - - // property - (code, i, source) => { - const prev = source[i - 1]; - if (prev != '.') return null; - - const match = code.match(/^[a-zA-Z0-9_-]+/); - if (!match) return null; - - return { - html: `<span class="property">${match[0]}</span>`, - next: match[0].length - }; - }, - - // keyword - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - - const match = keywords.filter(k => code.substr(0, k.length) == k)[0]; - if (match) { - if (/^[a-zA-Z]/.test(code.substr(match.length))) return null; - return { - html: `<span class="keyword ${match}">${match}</span>`, - next: match.length - }; - } else { - return null; - } - }, - - // symbol - code => { - const match = symbols.filter(s => code[0] == s)[0]; - if (match) { - return { - html: `<span class="symbol">${match}</span>`, - next: 1 - }; - } else { - return null; - } - } -]; - -// specify lang is todo -export default (source: string, lang?: string) => { - let code = source; - let html = ''; - - let i = 0; - - function push(token) { - html += token.html; - code = code.substr(token.next); - i += token.next; - } - - while (code != '') { - const parsed = elements.some(el => { - const e = el(code, i, source); - if (e) { - push(e); - return true; - } else { - return false; - } - }); - - if (!parsed) { - push({ - html: escape(code[0]), - next: 1 - }); - } - } - - return html; -}; diff --git a/src/server/api/common/text/elements/bold.ts b/src/server/api/common/text/elements/bold.ts deleted file mode 100644 index ce25764457..0000000000 --- a/src/server/api/common/text/elements/bold.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Bold - */ - -module.exports = text => { - const match = text.match(/^\*\*(.+?)\*\*/); - if (!match) return null; - const bold = match[0]; - return { - type: 'bold', - content: bold, - bold: bold.substr(2, bold.length - 4) - }; -}; diff --git a/src/server/api/common/text/elements/code.ts b/src/server/api/common/text/elements/code.ts deleted file mode 100644 index 4821e95fe2..0000000000 --- a/src/server/api/common/text/elements/code.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Code (block) - */ - -import genHtml from '../core/syntax-highlighter'; - -module.exports = text => { - const match = text.match(/^```([\s\S]+?)```/); - if (!match) return null; - const code = match[0]; - return { - type: 'code', - content: code, - code: code.substr(3, code.length - 6).trim(), - html: genHtml(code.substr(3, code.length - 6).trim()) - }; -}; diff --git a/src/server/api/common/text/elements/emoji.ts b/src/server/api/common/text/elements/emoji.ts deleted file mode 100644 index e24231a223..0000000000 --- a/src/server/api/common/text/elements/emoji.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Emoji - */ - -module.exports = text => { - const match = text.match(/^:[a-zA-Z0-9+-_]+:/); - if (!match) return null; - const emoji = match[0]; - return { - type: 'emoji', - content: emoji, - emoji: emoji.substr(1, emoji.length - 2) - }; -}; diff --git a/src/server/api/common/text/elements/hashtag.ts b/src/server/api/common/text/elements/hashtag.ts deleted file mode 100644 index ee57b140b8..0000000000 --- a/src/server/api/common/text/elements/hashtag.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Hashtag - */ - -module.exports = (text, i) => { - if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; - const isHead = text[0] == '#'; - const hashtag = text.match(/^\s?#[^\s]+/)[0]; - const res: any[] = !isHead ? [{ - type: 'text', - content: text[0] - }] : []; - res.push({ - type: 'hashtag', - content: isHead ? hashtag : hashtag.substr(1), - hashtag: isHead ? hashtag.substr(1) : hashtag.substr(2) - }); - return res; -}; diff --git a/src/server/api/common/text/elements/inline-code.ts b/src/server/api/common/text/elements/inline-code.ts deleted file mode 100644 index 9f9ef51a2b..0000000000 --- a/src/server/api/common/text/elements/inline-code.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Code (inline) - */ - -import genHtml from '../core/syntax-highlighter'; - -module.exports = text => { - const match = text.match(/^`(.+?)`/); - if (!match) return null; - const code = match[0]; - return { - type: 'inline-code', - content: code, - code: code.substr(1, code.length - 2).trim(), - html: genHtml(code.substr(1, code.length - 2).trim()) - }; -}; diff --git a/src/server/api/common/text/elements/link.ts b/src/server/api/common/text/elements/link.ts deleted file mode 100644 index 35563ddc3d..0000000000 --- a/src/server/api/common/text/elements/link.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Link - */ - -module.exports = text => { - const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); - if (!match) return null; - const silent = text[0] == '?'; - const link = match[0]; - const title = match[1]; - const url = match[2]; - return { - type: 'link', - content: link, - title: title, - url: url, - silent: silent - }; -}; diff --git a/src/server/api/common/text/elements/mention.ts b/src/server/api/common/text/elements/mention.ts deleted file mode 100644 index 2025dfdaad..0000000000 --- a/src/server/api/common/text/elements/mention.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Mention - */ -import parseAcct from '../../../../common/user/parse-acct'; - -module.exports = text => { - const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/); - if (!match) return null; - const mention = match[0]; - const { username, host } = parseAcct(mention.substr(1)); - return { - type: 'mention', - content: mention, - username, - host - }; -}; diff --git a/src/server/api/common/text/elements/quote.ts b/src/server/api/common/text/elements/quote.ts deleted file mode 100644 index cc8cfffdc4..0000000000 --- a/src/server/api/common/text/elements/quote.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Quoted text - */ - -module.exports = text => { - const match = text.match(/^"([\s\S]+?)\n"/); - if (!match) return null; - const quote = match[0]; - return { - type: 'quote', - content: quote, - quote: quote.substr(1, quote.length - 2).trim(), - }; -}; diff --git a/src/server/api/common/text/elements/url.ts b/src/server/api/common/text/elements/url.ts deleted file mode 100644 index 1003aff9c3..0000000000 --- a/src/server/api/common/text/elements/url.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * URL - */ - -module.exports = text => { - const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+/); - if (!match) return null; - const url = match[0]; - return { - type: 'url', - content: url, - url: url - }; -}; diff --git a/src/server/api/common/text/index.ts b/src/server/api/common/text/index.ts deleted file mode 100644 index 1e2398dc38..0000000000 --- a/src/server/api/common/text/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Misskey Text Analyzer - */ - -const elements = [ - require('./elements/bold'), - require('./elements/url'), - require('./elements/link'), - require('./elements/mention'), - require('./elements/hashtag'), - require('./elements/code'), - require('./elements/inline-code'), - require('./elements/quote'), - require('./elements/emoji') -]; - -export default (source: string) => { - - if (source == '') { - return null; - } - - const tokens = []; - - function push(token) { - if (token != null) { - tokens.push(token); - source = source.substr(token.content.length); - } - } - - let i = 0; - - // パース - while (source != '') { - const parsed = elements.some(el => { - let _tokens = el(source, i); - if (_tokens) { - if (!Array.isArray(_tokens)) { - _tokens = [_tokens]; - } - _tokens.forEach(push); - return true; - } else { - return false; - } - }); - - if (!parsed) { - push({ - type: 'text', - content: source[0] - }); - } - - i++; - } - - // テキストを纏める - tokens[0] = [tokens[0]]; - return tokens.reduce((a, b) => { - if (a[a.length - 1].type == 'text' && b.type == 'text') { - const tail = a.pop(); - return a.concat({ - type: 'text', - content: tail.content + b.content - }); - } else { - return a.concat(b); - } - }); -}; diff --git a/src/server/api/common/watch-post.ts b/src/server/api/common/watch-post.ts index 61ea444430..83c9b94f3e 100644 --- a/src/server/api/common/watch-post.ts +++ b/src/server/api/common/watch-post.ts @@ -1,5 +1,5 @@ import * as mongodb from 'mongodb'; -import Watching from '../models/post-watching'; +import Watching from '../../../models/post-watching'; export default async (me: mongodb.ObjectID, post: object) => { // 自分の投稿はwatchできない diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts index 67d2619640..f4d401eda0 100644 --- a/src/server/api/endpoints/aggregation/posts.ts +++ b/src/server/api/endpoints/aggregation/posts.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; +import Post from '../../../../models/post'; /** * Aggregate posts diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts index 9f9a4f37ee..e622745337 100644 --- a/src/server/api/endpoints/aggregation/posts/reaction.ts +++ b/src/server/api/endpoints/aggregation/posts/reaction.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../../models/post'; -import Reaction from '../../../models/post-reaction'; +import Post from '../../../../../models/post'; +import Reaction from '../../../../../models/post-reaction'; /** * Aggregate reaction of a post diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts index 2dc989281c..5f23e296fd 100644 --- a/src/server/api/endpoints/aggregation/posts/reactions.ts +++ b/src/server/api/endpoints/aggregation/posts/reactions.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../../models/post'; -import Reaction from '../../../models/post-reaction'; +import Post from '../../../../../models/post'; +import Reaction from '../../../../../models/post-reaction'; /** * Aggregate reactions of a post diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts index 3b050582a9..c76191e86b 100644 --- a/src/server/api/endpoints/aggregation/posts/reply.ts +++ b/src/server/api/endpoints/aggregation/posts/reply.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../../models/post'; +import Post from '../../../../../models/post'; /** * Aggregate reply of a post diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts index d9f3e36a07..a203605ebf 100644 --- a/src/server/api/endpoints/aggregation/posts/repost.ts +++ b/src/server/api/endpoints/aggregation/posts/repost.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../../models/post'; +import Post from '../../../../../models/post'; /** * Aggregate repost of a post diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts index a4e91a2282..19776ed297 100644 --- a/src/server/api/endpoints/aggregation/users.ts +++ b/src/server/api/endpoints/aggregation/users.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; +import User from '../../../../models/user'; /** * Aggregate users diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts index d477616578..cef0072296 100644 --- a/src/server/api/endpoints/aggregation/users/activity.ts +++ b/src/server/api/endpoints/aggregation/users/activity.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Post from '../../../models/post'; +import User from '../../../../../models/user'; +import Post from '../../../../../models/post'; // TODO: likeやfollowも集計 diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts index 73a30281b3..dda34ed7be 100644 --- a/src/server/api/endpoints/aggregation/users/followers.ts +++ b/src/server/api/endpoints/aggregation/users/followers.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Following from '../../../models/following'; +import User from '../../../../../models/user'; +import Following from '../../../../../models/following'; /** * Aggregate followers of a user @@ -39,11 +39,12 @@ module.exports = (params) => new Promise(async (res, rej) => { { deletedAt: { $gt: startTime } } ] }, { - _id: false, - followerId: false, - followeeId: false - }, { - sort: { createdAt: -1 } + sort: { createdAt: -1 }, + fields: { + _id: false, + followerId: false, + followeeId: false + } }); const graph = []; diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts index 16fc568d59..cd08d89e49 100644 --- a/src/server/api/endpoints/aggregation/users/following.ts +++ b/src/server/api/endpoints/aggregation/users/following.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Following from '../../../models/following'; +import User from '../../../../../models/user'; +import Following from '../../../../../models/following'; /** * Aggregate following of a user @@ -39,11 +39,12 @@ module.exports = (params) => new Promise(async (res, rej) => { { deletedAt: { $gt: startTime } } ] }, { - _id: false, - followerId: false, - followeeId: false - }, { - sort: { createdAt: -1 } + sort: { createdAt: -1 }, + fields: { + _id: false, + followerId: false, + followeeId: false + } }); const graph = []; diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts index c988748593..13617cf639 100644 --- a/src/server/api/endpoints/aggregation/users/post.ts +++ b/src/server/api/endpoints/aggregation/users/post.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Post from '../../../models/post'; +import User from '../../../../../models/user'; +import Post from '../../../../../models/post'; /** * Aggregate post of a user diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts index 60b33e9d1c..0c42ba3360 100644 --- a/src/server/api/endpoints/aggregation/users/reaction.ts +++ b/src/server/api/endpoints/aggregation/users/reaction.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../../models/user'; -import Reaction from '../../../models/post-reaction'; +import User from '../../../../../models/user'; +import Reaction from '../../../../../models/post-reaction'; /** * Aggregate reaction of a user diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts index 713078463d..f2033d33f6 100644 --- a/src/server/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -3,7 +3,7 @@ */ import rndstr from 'rndstr'; import $ from 'cafy'; -import App, { isValidNameId, pack } from '../../models/app'; +import App, { isValidNameId, pack } from '../../../../models/app'; /** * @swagger diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts index 6d02b26d2b..93b33cfa20 100644 --- a/src/server/api/endpoints/app/name_id/available.ts +++ b/src/server/api/endpoints/app/name_id/available.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../../models/app'; -import { isValidNameId } from '../../../models/app'; +import App from '../../../../../models/app'; +import { isValidNameId } from '../../../../../models/app'; /** * @swagger diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index 34bb958eee..7c8d2881d5 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import App, { pack } from '../../models/app'; +import App, { pack } from '../../../../models/app'; /** * @swagger diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts index 5a1925144d..aeabac2db8 100644 --- a/src/server/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -4,9 +4,9 @@ import rndstr from 'rndstr'; const crypto = require('crypto'); import $ from 'cafy'; -import App from '../../models/app'; -import AuthSess from '../../models/auth-session'; -import AccessToken from '../../models/access-token'; +import App from '../../../../models/app'; +import AuthSess from '../../../../models/auth-session'; +import AccessToken from '../../../../models/access-token'; /** * @swagger diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts index 180ad83ccd..ad03e538ce 100644 --- a/src/server/api/endpoints/auth/session/generate.ts +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -3,8 +3,8 @@ */ import * as uuid from 'uuid'; import $ from 'cafy'; -import App from '../../../models/app'; -import AuthSess from '../../../models/auth-session'; +import App from '../../../../../models/app'; +import AuthSess from '../../../../../models/auth-session'; import config from '../../../../../conf'; /** diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts index 869b714e71..f473d73973 100644 --- a/src/server/api/endpoints/auth/session/show.ts +++ b/src/server/api/endpoints/auth/session/show.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import AuthSess, { pack } from '../../../models/auth-session'; +import AuthSess, { pack } from '../../../../../models/auth-session'; /** * @swagger diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts index 5d9983af67..7dbb5ea6e8 100644 --- a/src/server/api/endpoints/auth/session/userkey.ts +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -2,10 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import App from '../../../models/app'; -import AuthSess from '../../../models/auth-session'; -import AccessToken from '../../../models/access-token'; -import { pack } from '../../../models/user'; +import App from '../../../../../models/app'; +import AuthSess from '../../../../../models/auth-session'; +import AccessToken from '../../../../../models/access-token'; +import { pack } from '../../../../../models/user'; /** * @swagger diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts index a4acc06605..582e6ba43b 100644 --- a/src/server/api/endpoints/channels.ts +++ b/src/server/api/endpoints/channels.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Channel, { pack } from '../models/channel'; +import Channel, { pack } from '../../../models/channel'; /** * Get all channels diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts index 1dc453c4a5..0f0f558c8a 100644 --- a/src/server/api/endpoints/channels/create.ts +++ b/src/server/api/endpoints/channels/create.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Channel from '../../models/channel'; -import Watching from '../../models/channel-watching'; -import { pack } from '../../models/channel'; +import Channel from '../../../../models/channel'; +import Watching from '../../../../models/channel-watching'; +import { pack } from '../../../../models/channel'; /** * Create a channel diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts index 348dbb108b..e48f96da7e 100644 --- a/src/server/api/endpoints/channels/posts.ts +++ b/src/server/api/endpoints/channels/posts.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import { default as Channel, IChannel } from '../../models/channel'; -import Post, { pack } from '../../models/post'; +import { default as Channel, IChannel } from '../../../../models/channel'; +import Post, { pack } from '../../../../models/post'; /** * Show a posts of a channel diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts index 5874ed18a6..3ce9ce4745 100644 --- a/src/server/api/endpoints/channels/show.ts +++ b/src/server/api/endpoints/channels/show.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Channel, { IChannel, pack } from '../../models/channel'; +import Channel, { IChannel, pack } from '../../../../models/channel'; /** * Show a channel diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts index 709313bc6e..8220b90b68 100644 --- a/src/server/api/endpoints/channels/unwatch.ts +++ b/src/server/api/endpoints/channels/unwatch.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Channel from '../../models/channel'; -import Watching from '../../models/channel-watching'; +import Channel from '../../../../models/channel'; +import Watching from '../../../../models/channel-watching'; /** * Unwatch a channel diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts index df9e70d5c2..6906282a54 100644 --- a/src/server/api/endpoints/channels/watch.ts +++ b/src/server/api/endpoints/channels/watch.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Channel from '../../models/channel'; -import Watching from '../../models/channel-watching'; +import Channel from '../../../../models/channel'; +import Watching from '../../../../models/channel-watching'; /** * Watch a channel diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts index eb21853916..d77ab2bbb0 100644 --- a/src/server/api/endpoints/drive.ts +++ b/src/server/api/endpoints/drive.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import DriveFile from '../models/drive-file'; +import DriveFile from '../../../models/drive-file'; /** * Get drive information diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index f982ef62e0..63d69d145a 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile, { pack } from '../../models/drive-file'; +import DriveFile, { pack } from '../../../../models/drive-file'; /** * Get drive files diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index 2cd89a8fac..53c8c70676 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import { validateFileName, pack } from '../../../models/drive-file'; +import { validateFileName, pack } from '../../../../../models/drive-file'; import create from '../../../common/drive/add-file'; /** diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts index 47ce891305..0ab6e5d3e3 100644 --- a/src/server/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile, { pack } from '../../../models/drive-file'; +import DriveFile, { pack } from '../../../../../models/drive-file'; /** * Find a file(s) diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 63920db7fc..3398f24541 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile, { pack } from '../../../models/drive-file'; +import DriveFile, { pack } from '../../../../../models/drive-file'; /** * Show a file diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index bfad45b0a2..836b4cfcd3 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import DriveFile, { validateFileName, pack } from '../../../models/drive-file'; +import DriveFolder from '../../../../../models/drive-folder'; +import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file'; import { publishDriveStream } from '../../../event'; /** diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts index 1a4ce0bf08..7262f09bbc 100644 --- a/src/server/api/endpoints/drive/files/upload_from_url.ts +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import { pack } from '../../../models/drive-file'; +import { pack } from '../../../../../models/drive-file'; import uploadFromUrl from '../../../common/drive/upload_from_url'; /** diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts index c314837f72..489e47912e 100644 --- a/src/server/api/endpoints/drive/folders.ts +++ b/src/server/api/endpoints/drive/folders.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder, { pack } from '../../models/drive-folder'; +import DriveFolder, { pack } from '../../../../models/drive-folder'; /** * Get drive folders diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts index 564558606d..24e0359307 100644 --- a/src/server/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder'; +import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; import { publishDriveStream } from '../../../event'; /** diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts index f46aaedd32..04dc38f87f 100644 --- a/src/server/api/endpoints/drive/folders/find.ts +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder, { pack } from '../../../models/drive-folder'; +import DriveFolder, { pack } from '../../../../../models/drive-folder'; /** * Find a folder(s) diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts index a6d7e86df1..b432f5a50a 100644 --- a/src/server/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder, { pack } from '../../../models/drive-folder'; +import DriveFolder, { pack } from '../../../../../models/drive-folder'; /** * Show a folder diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts index fcfd241241..6c5a5c3761 100644 --- a/src/server/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder'; +import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; import { publishDriveStream } from '../../../event'; /** diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index 71db38f3b3..02313aa37b 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import DriveFile, { pack } from '../../models/drive-file'; +import DriveFile, { pack } from '../../../../models/drive-file'; /** * Get drive stream diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 983d8040f9..1e24388a7a 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack as packUser } from '../../models/user'; -import Following from '../../models/following'; +import User, { pack as packUser } from '../../../../models/user'; +import Following from '../../../../models/following'; import notify from '../../common/notify'; import event from '../../event'; diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index 25eba8b262..7fc5f477f7 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack as packUser } from '../../models/user'; -import Following from '../../models/following'; +import User, { pack as packUser } from '../../../../models/user'; +import Following from '../../../../models/following'; import event from '../../event'; /** diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index f5e92b4ded..44de71d162 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import User, { pack } from '../models/user'; +import User, { pack } from '../../../models/user'; /** * Show myself diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts index d61ebbe6f9..0b2e32c13f 100644 --- a/src/server/api/endpoints/i/2fa/done.ts +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import * as speakeasy from 'speakeasy'; -import User from '../../../models/user'; +import User from '../../../../../models/user'; module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'token' parameter diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts index 0b49ad8821..d2683fb617 100644 --- a/src/server/api/endpoints/i/2fa/register.ts +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; -import User from '../../../models/user'; +import User from '../../../../../models/user'; import config from '../../../../../conf'; module.exports = async (params, user) => new Promise(async (res, rej) => { diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts index 0221ecb96d..ff2a435fee 100644 --- a/src/server/api/endpoints/i/2fa/unregister.ts +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../models/user'; +import User from '../../../../../models/user'; module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'password' parameter diff --git a/src/server/api/endpoints/i/appdata/get.ts b/src/server/api/endpoints/i/appdata/get.ts deleted file mode 100644 index 0b34643f75..0000000000 --- a/src/server/api/endpoints/i/appdata/get.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Appdata from '../../../models/appdata'; - -/** - * Get app data - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise<any>} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); - - // Get 'key' parameter - const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; - if (keyError) return rej('invalid key param'); - - const select = {}; - if (key !== null) { - select[`data.${key}`] = true; - } - const appdata = await Appdata.findOne({ - appId: app._id, - userId: user._id - }, { - fields: select - }); - - if (appdata) { - res(appdata.data); - } else { - res(); - } -}); diff --git a/src/server/api/endpoints/i/appdata/set.ts b/src/server/api/endpoints/i/appdata/set.ts deleted file mode 100644 index 1e3232ce3d..0000000000 --- a/src/server/api/endpoints/i/appdata/set.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Appdata from '../../../models/appdata'; - -/** - * Set app data - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise<any>} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); - - // Get 'data' parameter - const [data, dataError] = $(params.data).optional.object() - .pipe(obj => { - const hasInvalidData = Object.entries(obj).some(([k, v]) => - $(k).string().match(/^[a-z_]+$/).nok() && $(v).string().nok()); - return !hasInvalidData; - }).$; - if (dataError) return rej('invalid data param'); - - // Get 'key' parameter - const [key, keyError] = $(params.key).optional.string().match(/[a-z_]+/).$; - if (keyError) return rej('invalid key param'); - - // Get 'value' parameter - const [value, valueError] = $(params.value).optional.string().$; - if (valueError) return rej('invalid value param'); - - const set = {}; - if (data) { - Object.entries(data).forEach(([k, v]) => { - set[`data.${k}`] = v; - }); - } else { - set[`data.${key}`] = value; - } - - await Appdata.update({ - appId: app._id, - userId: user._id - }, Object.assign({ - appId: app._id, - userId: user._id - }, { - $set: set - }), { - upsert: true - }); - - res(204); -}); diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts index 5a38d7c18f..82fd2d2516 100644 --- a/src/server/api/endpoints/i/authorized_apps.ts +++ b/src/server/api/endpoints/i/authorized_apps.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import AccessToken from '../../models/access-token'; -import { pack } from '../../models/app'; +import AccessToken from '../../../../models/access-token'; +import { pack } from '../../../../models/app'; /** * Get authorized apps of my account diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts index e3b0127e7d..a38b56a216 100644 --- a/src/server/api/endpoints/i/change_password.ts +++ b/src/server/api/endpoints/i/change_password.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../models/user'; +import User from '../../../../models/user'; /** * Change password diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 9f8becf218..0b594e3180 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Favorite from '../../models/favorite'; -import { pack } from '../../models/post'; +import Favorite from '../../../../models/favorite'; +import { pack } from '../../../../models/post'; /** * Get followers of a user diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 7119bf6ea3..5de087a9b3 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Notification from '../../models/notification'; -import Mute from '../../models/mute'; -import { pack } from '../../models/notification'; +import Notification from '../../../../models/notification'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/notification'; import getFriends from '../../common/get-friends'; import read from '../../common/read-notification'; diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index 886a3edeb5..2a57579772 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Post from '../../models/post'; -import { pack } from '../../models/user'; +import User from '../../../../models/user'; +import Post from '../../../../models/post'; +import { pack } from '../../../../models/user'; /** * Pin post diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts index 9ac7b55071..c35778ac0b 100644 --- a/src/server/api/endpoints/i/regenerate_token.ts +++ b/src/server/api/endpoints/i/regenerate_token.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../models/user'; +import User from '../../../../models/user'; import event from '../../event'; import generateUserToken from '../../common/generate-native-user-token'; diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts index a4ba22790c..931b9e2252 100644 --- a/src/server/api/endpoints/i/signin_history.ts +++ b/src/server/api/endpoints/i/signin_history.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Signin, { pack } from '../../models/signin'; +import Signin, { pack } from '../../../../models/signin'; /** * Get signin history of my account diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 8147b1bba4..8e198f3ad0 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user'; +import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; import event from '../../event'; import config from '../../../../conf'; diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts index a0bef5e595..03867b4017 100644 --- a/src/server/api/endpoints/i/update_client_setting.ts +++ b/src/server/api/endpoints/i/update_client_setting.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack } from '../../models/user'; +import User, { pack } from '../../../../models/user'; import event from '../../event'; /** diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts index 151c3e205f..713cf9fcc8 100644 --- a/src/server/api/endpoints/i/update_home.ts +++ b/src/server/api/endpoints/i/update_home.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; +import User from '../../../../models/user'; import event from '../../event'; module.exports = async (params, user) => new Promise(async (res, rej) => { diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts index a8436b940f..b06ca108aa 100644 --- a/src/server/api/endpoints/i/update_mobile_home.ts +++ b/src/server/api/endpoints/i/update_mobile_home.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; +import User from '../../../../models/user'; import event from '../../event'; module.exports = async (params, user) => new Promise(async (res, rej) => { diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 2bf3ed996d..e42d34f21a 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import History from '../../models/messaging-history'; -import Mute from '../../models/mute'; -import { pack } from '../../models/messaging-message'; +import History from '../../../../models/messaging-history'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/messaging-message'; /** * Show messaging history diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index dd80e41d03..092eab0562 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Message from '../../models/messaging-message'; -import User from '../../models/user'; -import { pack } from '../../models/messaging-message'; +import Message from '../../../../models/messaging-message'; +import User from '../../../../models/user'; +import { pack } from '../../../../models/messaging-message'; import read from '../../common/read-messaging-message'; /** diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index 4edd726552..d8ffa9fdec 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -2,13 +2,13 @@ * Module dependencies */ import $ from 'cafy'; -import Message from '../../../models/messaging-message'; -import { isValidText } from '../../../models/messaging-message'; -import History from '../../../models/messaging-history'; -import User from '../../../models/user'; -import Mute from '../../../models/mute'; -import DriveFile from '../../../models/drive-file'; -import { pack } from '../../../models/messaging-message'; +import Message from '../../../../../models/messaging-message'; +import { isValidText } from '../../../../../models/messaging-message'; +import History from '../../../../../models/messaging-history'; +import User from '../../../../../models/user'; +import Mute from '../../../../../models/mute'; +import DriveFile from '../../../../../models/drive-file'; +import { pack } from '../../../../../models/messaging-message'; import publishUserStream from '../../../event'; import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event'; import config from '../../../../../conf'; diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts index f7f4047b67..30d59dd8bd 100644 --- a/src/server/api/endpoints/messaging/unread.ts +++ b/src/server/api/endpoints/messaging/unread.ts @@ -1,8 +1,8 @@ /** * Module dependencies */ -import Message from '../../models/messaging-message'; -import Mute from '../../models/mute'; +import Message from '../../../../models/messaging-message'; +import Mute from '../../../../models/mute'; /** * Get count of unread messages diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index cb47ede57b..4f0ae2a60a 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -4,7 +4,7 @@ import * as os from 'os'; import version from '../../../version'; import config from '../../../conf'; -import Meta from '../models/meta'; +import Meta from '../../../models/meta'; /** * @swagger @@ -40,7 +40,7 @@ import Meta from '../models/meta'; * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { - const meta = (await Meta.findOne()) || {}; + const meta: any = (await Meta.findOne()) || {}; res({ maintainer: config.maintainer, diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index e860235086..a7fa5f7b4b 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Mute from '../../models/mute'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; /** * Mute a user diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 7e361b4792..687f010336 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Mute from '../../models/mute'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; /** * Unmute a user diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index 3401fba64d..bd80401445 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Mute from '../../models/mute'; -import { pack } from '../../models/user'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts index bc1290cac6..2a3f8bcd7a 100644 --- a/src/server/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import App, { pack } from '../../models/app'; +import App, { pack } from '../../../../models/app'; /** * Get my apps diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts index 8f9719fff6..283ecd63b1 100644 --- a/src/server/api/endpoints/notifications/get_unread_count.ts +++ b/src/server/api/endpoints/notifications/get_unread_count.ts @@ -1,8 +1,8 @@ /** * Module dependencies */ -import Notification from '../../models/notification'; -import Mute from '../../models/mute'; +import Notification from '../../../../models/notification'; +import Mute from '../../../../models/mute'; /** * Get count of unread notifications diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts index 693de3d0ef..3693ba87bc 100644 --- a/src/server/api/endpoints/notifications/mark_as_read_all.ts +++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import Notification from '../../models/notification'; +import Notification from '../../../../models/notification'; import event from '../../event'; /** diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts index 37fa384189..d05c1c2585 100644 --- a/src/server/api/endpoints/othello/games.ts +++ b/src/server/api/endpoints/othello/games.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import OthelloGame, { pack } from '../../models/othello-game'; +import OthelloGame, { pack } from '../../../../models/othello-game'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'my' parameter diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts index f9084682fa..0d3b539652 100644 --- a/src/server/api/endpoints/othello/games/show.ts +++ b/src/server/api/endpoints/othello/games/show.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import OthelloGame, { pack } from '../../../models/othello-game'; -import Othello from '../../../../common/othello/core'; +import OthelloGame, { pack } from '../../../../../models/othello-game'; +import Othello from '../../../../../common/othello/core'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'gameId' parameter diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts index f6e0071a6c..4761537614 100644 --- a/src/server/api/endpoints/othello/invitations.ts +++ b/src/server/api/endpoints/othello/invitations.ts @@ -1,4 +1,4 @@ -import Matching, { pack as packMatching } from '../../models/othello-matching'; +import Matching, { pack as packMatching } from '../../../../models/othello-matching'; module.exports = (params, user) => new Promise(async (res, rej) => { // Find session diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts index f503c5834c..03168095dc 100644 --- a/src/server/api/endpoints/othello/match.ts +++ b/src/server/api/endpoints/othello/match.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import Matching, { pack as packMatching } from '../../models/othello-matching'; -import OthelloGame, { pack as packGame } from '../../models/othello-game'; -import User from '../../models/user'; +import Matching, { pack as packMatching } from '../../../../models/othello-matching'; +import OthelloGame, { pack as packGame } from '../../../../models/othello-game'; +import User from '../../../../models/user'; import publishUserStream, { publishOthelloStream } from '../../event'; -import { eighteight } from '../../../common/othello/maps'; +import { eighteight } from '../../../../common/othello/maps'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'userId' parameter diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts index ee0f82a611..562e691061 100644 --- a/src/server/api/endpoints/othello/match/cancel.ts +++ b/src/server/api/endpoints/othello/match/cancel.ts @@ -1,4 +1,4 @@ -import Matching from '../../../models/othello-matching'; +import Matching from '../../../../../models/othello-matching'; module.exports = (params, user) => new Promise(async (res, rej) => { await Matching.remove({ diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts index bee1de02d4..7af8cff671 100644 --- a/src/server/api/endpoints/posts.ts +++ b/src/server/api/endpoints/posts.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post, { pack } from '../models/post'; +import Post, { pack } from '../../../models/post'; /** * Lists all posts diff --git a/src/server/api/endpoints/posts/categorize.ts b/src/server/api/endpoints/posts/categorize.ts deleted file mode 100644 index 0436c8e697..0000000000 --- a/src/server/api/endpoints/posts/categorize.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; - -/** - * Categorize a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - if (!user.account.isPro) { - return rej('This endpoint is available only from a Pro account'); - } - - // Get 'postId' parameter - const [postId, postIdErr] = $(params.postId).id().$; - if (postIdErr) return rej('invalid postId param'); - - // Get categorizee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - if (post.is_category_verified) { - return rej('This post already has the verified category'); - } - - // Get 'category' parameter - const [category, categoryErr] = $(params.category).string().or([ - 'music', 'game', 'anime', 'it', 'gadgets', 'photography' - ]).$; - if (categoryErr) return rej('invalid category param'); - - // Set category - Post.update({ _id: post._id }, { - $set: { - category: category, - is_category_verified: true - } - }); - - // Send response - res(); -}); diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts index 44a77d102e..7abb045a49 100644 --- a/src/server/api/endpoints/posts/context.ts +++ b/src/server/api/endpoints/posts/context.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post, { pack } from '../../models/post'; +import Post, { pack } from '../../../../models/post'; /** * Show a context of a post diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index 33042a51a2..6b2957ae67 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -3,21 +3,21 @@ */ import $ from 'cafy'; import deepEqual = require('deep-equal'); -import parse from '../../common/text'; -import { default as Post, IPost, isValidText } from '../../models/post'; -import { default as User, ILocalAccount, IUser } from '../../models/user'; -import { default as Channel, IChannel } from '../../models/channel'; -import Following from '../../models/following'; -import Mute from '../../models/mute'; -import DriveFile from '../../models/drive-file'; -import Watching from '../../models/post-watching'; -import ChannelWatching from '../../models/channel-watching'; -import { pack } from '../../models/post'; +import parse from '../../../../common/text'; +import { default as Post, IPost, isValidText } from '../../../../models/post'; +import { default as User, ILocalAccount, IUser } from '../../../../models/user'; +import { default as Channel, IChannel } from '../../../../models/channel'; +import Following from '../../../../models/following'; +import Mute from '../../../../models/mute'; +import DriveFile from '../../../../models/drive-file'; +import Watching from '../../../../models/post-watching'; +import ChannelWatching from '../../../../models/channel-watching'; +import { pack } from '../../../../models/post'; import notify from '../../common/notify'; import watch from '../../common/watch-post'; import event, { pushSw, publishChannelStream } from '../../event'; -import getAcct from '../../../common/user/get-acct'; -import parseAcct from '../../../common/user/parse-acct'; +import getAcct from '../../../../common/user/get-acct'; +import parseAcct from '../../../../common/user/parse-acct'; import config from '../../../../conf'; /** diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts index 6100e10b23..f537fb7ddf 100644 --- a/src/server/api/endpoints/posts/favorites/create.ts +++ b/src/server/api/endpoints/posts/favorites/create.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Favorite from '../../../models/favorite'; -import Post from '../../../models/post'; +import Favorite from '../../../../../models/favorite'; +import Post from '../../../../../models/post'; /** * Favorite a post diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts index db52036ec5..28930337a3 100644 --- a/src/server/api/endpoints/posts/favorites/delete.ts +++ b/src/server/api/endpoints/posts/favorites/delete.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Favorite from '../../../models/favorite'; -import Post from '../../../models/post'; +import Favorite from '../../../../../models/favorite'; +import Post from '../../../../../models/post'; /** * Unfavorite a post diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts index 1b342e8de9..d7302c0620 100644 --- a/src/server/api/endpoints/posts/mentions.ts +++ b/src/server/api/endpoints/posts/mentions.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; +import Post from '../../../../models/post'; import getFriends from '../../common/get-friends'; -import { pack } from '../../models/post'; +import { pack } from '../../../../models/post'; /** * Get mentions of myself diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts index 19ef0975fa..d706742618 100644 --- a/src/server/api/endpoints/posts/polls/recommendation.ts +++ b/src/server/api/endpoints/posts/polls/recommendation.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Vote from '../../../models/poll-vote'; -import Post, { pack } from '../../../models/post'; +import Vote from '../../../../../models/poll-vote'; +import Post, { pack } from '../../../../../models/post'; /** * Get recommended polls diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts index 734a3a3c45..b970c05e8d 100644 --- a/src/server/api/endpoints/posts/polls/vote.ts +++ b/src/server/api/endpoints/posts/polls/vote.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import Vote from '../../../models/poll-vote'; -import Post from '../../../models/post'; -import Watching from '../../../models/post-watching'; +import Vote from '../../../../../models/poll-vote'; +import Post from '../../../../../models/post'; +import Watching from '../../../../../models/post-watching'; import notify from '../../../common/notify'; import watch from '../../../common/watch-post'; import { publishPostStream } from '../../../event'; diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts index f753ba7c29..da733f5337 100644 --- a/src/server/api/endpoints/posts/reactions.ts +++ b/src/server/api/endpoints/posts/reactions.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import Reaction, { pack } from '../../models/post-reaction'; +import Post from '../../../../models/post'; +import Reaction, { pack } from '../../../../models/post-reaction'; /** * Show reactions of a post diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts index a1e6779805..5d2b5a7ed3 100644 --- a/src/server/api/endpoints/posts/reactions/create.ts +++ b/src/server/api/endpoints/posts/reactions/create.ts @@ -2,10 +2,10 @@ * Module dependencies */ import $ from 'cafy'; -import Reaction from '../../../models/post-reaction'; -import Post, { pack as packPost } from '../../../models/post'; -import { pack as packUser } from '../../../models/user'; -import Watching from '../../../models/post-watching'; +import Reaction from '../../../../../models/post-reaction'; +import Post, { pack as packPost } from '../../../../../models/post'; +import { pack as packUser } from '../../../../../models/user'; +import Watching from '../../../../../models/post-watching'; import notify from '../../../common/notify'; import watch from '../../../common/watch-post'; import { publishPostStream, pushSw } from '../../../event'; diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts index b09bcbb4b7..11f5c7dafa 100644 --- a/src/server/api/endpoints/posts/reactions/delete.ts +++ b/src/server/api/endpoints/posts/reactions/delete.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Reaction from '../../../models/post-reaction'; -import Post from '../../../models/post'; +import Reaction from '../../../../../models/post-reaction'; +import Post from '../../../../../models/post'; // import event from '../../../event'; /** diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts index db021505fc..dd5a95c173 100644 --- a/src/server/api/endpoints/posts/replies.ts +++ b/src/server/api/endpoints/posts/replies.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post, { pack } from '../../models/post'; +import Post, { pack } from '../../../../models/post'; /** * Show a replies of a post diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts index 51af41f523..ec6218ca38 100644 --- a/src/server/api/endpoints/posts/reposts.ts +++ b/src/server/api/endpoints/posts/reposts.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post, { pack } from '../../models/post'; +import Post, { pack } from '../../../../models/post'; /** * Show a reposts of a post diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts index bb5c438926..21c4e77fdd 100644 --- a/src/server/api/endpoints/posts/search.ts +++ b/src/server/api/endpoints/posts/search.ts @@ -3,11 +3,11 @@ */ import $ from 'cafy'; const escapeRegexp = require('escape-regexp'); -import Post from '../../models/post'; -import User from '../../models/user'; -import Mute from '../../models/mute'; +import Post from '../../../../models/post'; +import User from '../../../../models/user'; +import Mute from '../../../../models/mute'; import getFriends from '../../common/get-friends'; -import { pack } from '../../models/post'; +import { pack } from '../../../../models/post'; /** * Search a post diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts index bb4bcdb790..e1781b545c 100644 --- a/src/server/api/endpoints/posts/show.ts +++ b/src/server/api/endpoints/posts/show.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Post, { pack } from '../../models/post'; +import Post, { pack } from '../../../../models/post'; /** * Show a post diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts index a3e915f16a..b58d25fa80 100644 --- a/src/server/api/endpoints/posts/timeline.ts +++ b/src/server/api/endpoints/posts/timeline.ts @@ -3,11 +3,11 @@ */ import $ from 'cafy'; import rap from '@prezzemolo/rap'; -import Post from '../../models/post'; -import Mute from '../../models/mute'; -import ChannelWatching from '../../models/channel-watching'; +import Post from '../../../../models/post'; +import Mute from '../../../../models/mute'; +import ChannelWatching from '../../../../models/channel-watching'; import getFriends from '../../common/get-friends'; -import { pack } from '../../models/post'; +import { pack } from '../../../../models/post'; /** * Get timeline of myself diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts index bc0c47fbc6..dbee169138 100644 --- a/src/server/api/endpoints/posts/trend.ts +++ b/src/server/api/endpoints/posts/trend.ts @@ -3,7 +3,7 @@ */ const ms = require('ms'); import $ from 'cafy'; -import Post, { pack } from '../../models/post'; +import Post, { pack } from '../../../../models/post'; /** * Get trend posts diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts index 719792d40d..0fb0c44b0f 100644 --- a/src/server/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -1,8 +1,8 @@ /** * Module dependencies */ -import Post from '../models/post'; -import User from '../models/user'; +import Post from '../../../models/post'; +import User from '../../../models/user'; /** * @swagger diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 1542e1dbeb..ef3428057d 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import Subscription from '../../models/sw-subscription'; +import Subscription from '../../../../models/sw-subscription'; /** * subscribe service worker diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts index f23cdbd85a..bd27c37de0 100644 --- a/src/server/api/endpoints/username/available.ts +++ b/src/server/api/endpoints/username/available.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import { validateUsername } from '../../models/user'; +import User from '../../../../models/user'; +import { validateUsername } from '../../../../models/user'; /** * Check available username diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index 393c3479c5..e82d72748c 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack } from '../models/user'; +import User, { pack } from '../../../models/user'; /** * Lists all users diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index fc09cfa2c6..39b69a6aa9 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import { pack } from '../../models/user'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 3387dab366..aa6628dde2 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -2,9 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import { pack } from '../../models/user'; +import User from '../../../../models/user'; +import Following from '../../../../models/following'; +import { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts index 991c5555b6..3a116c8e26 100644 --- a/src/server/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Post from '../../models/post'; -import User, { pack } from '../../models/user'; +import Post from '../../../../models/post'; +import User, { pack } from '../../../../models/user'; module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'userId' parameter diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts index 9346907492..b6c533fb5b 100644 --- a/src/server/api/endpoints/users/posts.ts +++ b/src/server/api/endpoints/users/posts.ts @@ -3,8 +3,8 @@ */ import $ from 'cafy'; import getHostLower from '../../common/get-host-lower'; -import Post, { pack } from '../../models/post'; -import User from '../../models/user'; +import Post, { pack } from '../../../../models/post'; +import User from '../../../../models/user'; /** * Get posts of a user diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index c5297cdc50..c815339697 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -3,7 +3,7 @@ */ const ms = require('ms'); import $ from 'cafy'; -import User, { pack } from '../../models/user'; +import User, { pack } from '../../../../models/user'; import getFriends from '../../common/get-friends'; /** diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index b03ed2f2fd..335043b02e 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -3,7 +3,7 @@ */ import * as mongo from 'mongodb'; import $ from 'cafy'; -import User, { pack } from '../../models/user'; +import User, { pack } from '../../../../models/user'; import config from '../../../../conf'; const escapeRegexp = require('escape-regexp'); diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts index 24e9c98e78..5f6ececff9 100644 --- a/src/server/api/endpoints/users/search_by_username.ts +++ b/src/server/api/endpoints/users/search_by_username.ts @@ -2,7 +2,7 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack } from '../../models/user'; +import User, { pack } from '../../../../models/user'; /** * Search a user by username diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 16411dddc6..0b7646f81e 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import { JSDOM } from 'jsdom'; import { toUnicode, toASCII } from 'punycode'; import uploadFromUrl from '../../common/drive/upload_from_url'; -import User, { pack, validateUsername, isValidName, isValidDescription } from '../../models/user'; +import User, { pack, validateUsername, isValidName, isValidDescription } from '../../../../models/user'; const request = require('request-promise-native'); const WebFinger = require('webfinger.js'); diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts index 33337fbb1b..88ea6c3679 100644 --- a/src/server/api/limitter.ts +++ b/src/server/api/limitter.ts @@ -3,7 +3,7 @@ import * as debug from 'debug'; import limiterDB from '../../db/redis'; import { Endpoint } from './endpoints'; import { IAuthContext } from './authenticate'; -import getAcct from '../common/user/get-acct'; +import getAcct from '../../common/user/get-acct'; const log = debug('misskey:limitter'); diff --git a/src/server/api/models/access-token.ts b/src/server/api/models/access-token.ts deleted file mode 100644 index 59bb094269..0000000000 --- a/src/server/api/models/access-token.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const AccessToken = db.get<IAccessTokens>('accessTokens'); -AccessToken.createIndex('token'); -AccessToken.createIndex('hash'); -export default AccessToken; - -export type IAccessTokens = { - _id: mongo.ObjectID; - createdAt: Date; - appId: mongo.ObjectID; - userId: mongo.ObjectID; - token: string; - hash: string; -}; diff --git a/src/server/api/models/app.ts b/src/server/api/models/app.ts deleted file mode 100644 index 3c17c50fdf..0000000000 --- a/src/server/api/models/app.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import AccessToken from './access-token'; -import db from '../../../db/mongodb'; -import config from '../../../conf'; - -const App = db.get<IApp>('apps'); -App.createIndex('nameId'); -App.createIndex('nameIdLower'); -App.createIndex('secret'); -export default App; - -export type IApp = { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - secret: string; - name: string; - nameId: string; - nameIdLower: string; - description: string; - permission: string; - callbackUrl: string; -}; - -export function isValidNameId(nameId: string): boolean { - return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId); -} - -/** - * Pack an app for API response - * - * @param {any} app - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -export const pack = ( - app: any, - me?: any, - options?: { - includeSecret?: boolean, - includeProfileImageIds?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = options || { - includeSecret: false, - includeProfileImageIds: false - }; - - let _app: any; - - // Populate the app if 'app' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(app)) { - _app = await App.findOne({ - _id: app - }); - } else if (typeof app === 'string') { - _app = await App.findOne({ - _id: new mongo.ObjectID(app) - }); - } else { - _app = deepcopy(app); - } - - // Me - if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { - if (typeof me === 'string') { - me = new mongo.ObjectID(me); - } else { - me = me._id; - } - } - - // Rename _id to id - _app.id = _app._id; - delete _app._id; - - delete _app.nameIdLower; - - // Visible by only owner - if (!opts.includeSecret) { - delete _app.secret; - } - - _app.iconUrl = _app.icon != null - ? `${config.drive_url}/${_app.icon}` - : `${config.drive_url}/app-default.jpg`; - - if (me) { - // 既に連携しているか - const exist = await AccessToken.count({ - appId: _app.id, - userId: me, - }, { - limit: 1 - }); - - _app.isAuthorized = exist === 1; - } - - resolve(_app); -}); diff --git a/src/server/api/models/auth-session.ts b/src/server/api/models/auth-session.ts deleted file mode 100644 index 2da40b1ea9..0000000000 --- a/src/server/api/models/auth-session.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; -import { pack as packApp } from './app'; - -const AuthSession = db.get<IAuthSession>('authSessions'); -export default AuthSession; - -export interface IAuthSession { - _id: mongo.ObjectID; - createdAt: Date; - appId: mongo.ObjectID; - userId: mongo.ObjectID; - token: string; -} - -/** - * Pack an auth session for API response - * - * @param {any} session - * @param {any} me? - * @return {Promise<any>} - */ -export const pack = ( - session: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _session: any; - - // TODO: Populate session if it ID - _session = deepcopy(session); - - // Me - if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { - if (typeof me === 'string') { - me = new mongo.ObjectID(me); - } else { - me = me._id; - } - } - - delete _session._id; - - // Populate app - _session.app = await packApp(_session.appId, me); - - resolve(_session); -}); diff --git a/src/server/api/models/channel-watching.ts b/src/server/api/models/channel-watching.ts deleted file mode 100644 index a26b7edb91..0000000000 --- a/src/server/api/models/channel-watching.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const ChannelWatching = db.get<IChannelWatching>('channelWatching'); -export default ChannelWatching; - -export interface IChannelWatching { - _id: mongo.ObjectID; - createdAt: Date; - deletedAt: Date; - channelId: mongo.ObjectID; - userId: mongo.ObjectID; -} diff --git a/src/server/api/models/channel.ts b/src/server/api/models/channel.ts deleted file mode 100644 index 9f94c5a8d1..0000000000 --- a/src/server/api/models/channel.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import { IUser } from './user'; -import Watching from './channel-watching'; -import db from '../../../db/mongodb'; - -const Channel = db.get<IChannel>('channels'); -export default Channel; - -export type IChannel = { - _id: mongo.ObjectID; - createdAt: Date; - title: string; - userId: mongo.ObjectID; - index: number; - watchingCount: number; -}; - -/** - * Pack a channel for API response - * - * @param channel target - * @param me? serializee - * @return response - */ -export const pack = ( - channel: string | mongo.ObjectID | IChannel, - me?: string | mongo.ObjectID | IUser -) => new Promise<any>(async (resolve, reject) => { - - let _channel: any; - - // Populate the channel if 'channel' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(channel)) { - _channel = await Channel.findOne({ - _id: channel - }); - } else if (typeof channel === 'string') { - _channel = await Channel.findOne({ - _id: new mongo.ObjectID(channel) - }); - } else { - _channel = deepcopy(channel); - } - - // Rename _id to id - _channel.id = _channel._id; - delete _channel._id; - - // Remove needless properties - delete _channel.userId; - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - if (me) { - //#region Watchしているかどうか - const watch = await Watching.findOne({ - userId: meId, - channelId: _channel.id, - deletedAt: { $exists: false } - }); - - _channel.isWatching = watch !== null; - //#endregion - } - - resolve(_channel); -}); diff --git a/src/server/api/models/drive-file.ts b/src/server/api/models/drive-file.ts deleted file mode 100644 index 04c9c54bd4..0000000000 --- a/src/server/api/models/drive-file.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as mongodb from 'mongodb'; -import deepcopy = require('deepcopy'); -import { pack as packFolder } from './drive-folder'; -import config from '../../../conf'; -import monkDb, { nativeDbConn } from '../../../db/mongodb'; - -const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); - -export default DriveFile; - -const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => { - const db = await nativeDbConn(); - const bucket = new mongodb.GridFSBucket(db, { - bucketName: 'driveFiles' - }); - return bucket; -}; - -export { getGridFSBucket }; - -export type IDriveFile = { - _id: mongodb.ObjectID; - uploadDate: Date; - md5: string; - filename: string; - contentType: string; - metadata: { - properties: any; - userId: mongodb.ObjectID; - folderId: mongodb.ObjectID; - } -}; - -export function validateFileName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) && - (name.indexOf('\\') === -1) && - (name.indexOf('/') === -1) && - (name.indexOf('..') === -1) - ); -} - -/** - * Pack a drive file for API response - * - * @param {any} file - * @param {any} options? - * @return {Promise<any>} - */ -export const pack = ( - file: any, - options?: { - detail: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false - }, options); - - let _file: any; - - // Populate the file if 'file' is ID - if (mongodb.ObjectID.prototype.isPrototypeOf(file)) { - _file = await DriveFile.findOne({ - _id: file - }); - } else if (typeof file === 'string') { - _file = await DriveFile.findOne({ - _id: new mongodb.ObjectID(file) - }); - } else { - _file = deepcopy(file); - } - - if (!_file) return reject('invalid file arg.'); - - // rendered target - let _target: any = {}; - - _target.id = _file._id; - _target.createdAt = _file.uploadDate; - _target.name = _file.filename; - _target.type = _file.contentType; - _target.datasize = _file.length; - _target.md5 = _file.md5; - - _target = Object.assign(_target, _file.metadata); - - _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; - - if (_target.properties == null) _target.properties = {}; - - if (opts.detail) { - if (_target.folderId) { - // Populate folder - _target.folder = await packFolder(_target.folderId, { - detail: true - }); - } - - /* - if (_target.tags) { - // Populate tags - _target.tags = await _target.tags.map(async (tag: any) => - await serializeDriveTag(tag) - ); - } - */ - } - - resolve(_target); -}); diff --git a/src/server/api/models/drive-folder.ts b/src/server/api/models/drive-folder.ts deleted file mode 100644 index 4ecafaa154..0000000000 --- a/src/server/api/models/drive-folder.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; -import DriveFile from './drive-file'; - -const DriveFolder = db.get<IDriveFolder>('drive_folders'); -export default DriveFolder; - -export type IDriveFolder = { - _id: mongo.ObjectID; - createdAt: Date; - name: string; - userId: mongo.ObjectID; - parentId: mongo.ObjectID; -}; - -export function isValidFolderName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) - ); -} - -/** - * Pack a drive folder for API response - * - * @param {any} folder - * @param {any} options? - * @return {Promise<any>} - */ -export const pack = ( - folder: any, - options?: { - detail: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false - }, options); - - let _folder: any; - - // Populate the folder if 'folder' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(folder)) { - _folder = await DriveFolder.findOne({ _id: folder }); - } else if (typeof folder === 'string') { - _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) }); - } else { - _folder = deepcopy(folder); - } - - // Rename _id to id - _folder.id = _folder._id; - delete _folder._id; - - if (opts.detail) { - const childFoldersCount = await DriveFolder.count({ - parentId: _folder.id - }); - - const childFilesCount = await DriveFile.count({ - 'metadata.folderId': _folder.id - }); - - _folder.foldersCount = childFoldersCount; - _folder.filesCount = childFilesCount; - } - - if (opts.detail && _folder.parentId) { - // Populate parent folder - _folder.parent = await pack(_folder.parentId, { - detail: true - }); - } - - resolve(_folder); -}); diff --git a/src/server/api/models/favorite.ts b/src/server/api/models/favorite.ts deleted file mode 100644 index 5fb4db95a9..0000000000 --- a/src/server/api/models/favorite.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const Favorites = db.get<IFavorite>('favorites'); -export default Favorites; - -export type IFavorite = { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - postId: mongo.ObjectID; -}; diff --git a/src/server/api/models/following.ts b/src/server/api/models/following.ts deleted file mode 100644 index 552e946049..0000000000 --- a/src/server/api/models/following.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const Following = db.get<IFollowing>('following'); -export default Following; - -export type IFollowing = { - _id: mongo.ObjectID; - createdAt: Date; - deletedAt: Date; - followeeId: mongo.ObjectID; - followerId: mongo.ObjectID; -}; diff --git a/src/server/api/models/messaging-history.ts b/src/server/api/models/messaging-history.ts deleted file mode 100644 index 44a2adc315..0000000000 --- a/src/server/api/models/messaging-history.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const MessagingHistory = db.get<IMessagingHistory>('messagingHistories'); -export default MessagingHistory; - -export type IMessagingHistory = { - _id: mongo.ObjectID; - updatedAt: Date; - userId: mongo.ObjectID; - partnerId: mongo.ObjectID; - messageId: mongo.ObjectID; -}; diff --git a/src/server/api/models/messaging-message.ts b/src/server/api/models/messaging-message.ts deleted file mode 100644 index d3a418c9a6..0000000000 --- a/src/server/api/models/messaging-message.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import { pack as packUser } from './user'; -import { pack as packFile } from './drive-file'; -import db from '../../../db/mongodb'; -import parse from '../common/text'; - -const MessagingMessage = db.get<IMessagingMessage>('messagingMessages'); -export default MessagingMessage; - -export interface IMessagingMessage { - _id: mongo.ObjectID; - createdAt: Date; - text: string; - userId: mongo.ObjectID; - recipientId: mongo.ObjectID; - isRead: boolean; - fileId: mongo.ObjectID; -} - -export function isValidText(text: string): boolean { - return text.length <= 1000 && text.trim() != ''; -} - -/** - * Pack a messaging message for API response - * - * @param {any} message - * @param {any} me? - * @param {any} options? - * @return {Promise<any>} - */ -export const pack = ( - message: any, - me?: any, - options?: { - populateRecipient: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = options || { - populateRecipient: true - }; - - let _message: any; - - // Populate the message if 'message' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(message)) { - _message = await MessagingMessage.findOne({ - _id: message - }); - } else if (typeof message === 'string') { - _message = await MessagingMessage.findOne({ - _id: new mongo.ObjectID(message) - }); - } else { - _message = deepcopy(message); - } - - // Rename _id to id - _message.id = _message._id; - delete _message._id; - - // Parse text - if (_message.text) { - _message.ast = parse(_message.text); - } - - // Populate user - _message.user = await packUser(_message.userId, me); - - if (_message.fileId) { - // Populate file - _message.file = await packFile(_message.fileId); - } - - if (opts.populateRecipient) { - // Populate recipient - _message.recipient = await packUser(_message.recipientId, me); - } - - resolve(_message); -}); diff --git a/src/server/api/models/meta.ts b/src/server/api/models/meta.ts deleted file mode 100644 index cad7f5096e..0000000000 --- a/src/server/api/models/meta.ts +++ /dev/null @@ -1,8 +0,0 @@ -import db from '../../../db/mongodb'; - -const Meta = db.get<IMeta>('meta'); -export default Meta; - -export type IMeta = { - broadcasts: any[]; -}; diff --git a/src/server/api/models/mute.ts b/src/server/api/models/mute.ts deleted file mode 100644 index e5385ade39..0000000000 --- a/src/server/api/models/mute.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const Mute = db.get<IMute>('mute'); -export default Mute; - -export interface IMute { - _id: mongo.ObjectID; - createdAt: Date; - deletedAt: Date; - muterId: mongo.ObjectID; - muteeId: mongo.ObjectID; -} diff --git a/src/server/api/models/notification.ts b/src/server/api/models/notification.ts deleted file mode 100644 index 237e2663fc..0000000000 --- a/src/server/api/models/notification.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; -import { IUser, pack as packUser } from './user'; -import { pack as packPost } from './post'; - -const Notification = db.get<INotification>('notifications'); -export default Notification; - -export interface INotification { - _id: mongo.ObjectID; - createdAt: Date; - - /** - * 通知の受信者 - */ - notifiee?: IUser; - - /** - * 通知の受信者 - */ - notifieeId: mongo.ObjectID; - - /** - * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー - */ - notifier?: IUser; - - /** - * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー - */ - notifierId: mongo.ObjectID; - - /** - * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - (自分または自分がWatchしている)投稿が返信された - * repost - (自分または自分がWatchしている)投稿がRepostされた - * quote - (自分または自分がWatchしている)投稿が引用Repostされた - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された - */ - type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote'; - - /** - * 通知が読まれたかどうか - */ - isRead: Boolean; -} - -/** - * Pack a notification for API response - * - * @param {any} notification - * @return {Promise<any>} - */ -export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => { - let _notification: any; - - // Populate the notification if 'notification' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(notification)) { - _notification = await Notification.findOne({ - _id: notification - }); - } else if (typeof notification === 'string') { - _notification = await Notification.findOne({ - _id: new mongo.ObjectID(notification) - }); - } else { - _notification = deepcopy(notification); - } - - // Rename _id to id - _notification.id = _notification._id; - delete _notification._id; - - // Rename notifierId to userId - _notification.userId = _notification.notifierId; - delete _notification.notifierId; - - const me = _notification.notifieeId; - delete _notification.notifieeId; - - // Populate notifier - _notification.user = await packUser(_notification.userId, me); - - switch (_notification.type) { - case 'follow': - // nope - break; - case 'mention': - case 'reply': - case 'repost': - case 'quote': - case 'reaction': - case 'poll_vote': - // Populate post - _notification.post = await packPost(_notification.postId, me); - break; - default: - console.error(`Unknown type: ${_notification.type}`); - break; - } - - resolve(_notification); -}); diff --git a/src/server/api/models/othello-game.ts b/src/server/api/models/othello-game.ts deleted file mode 100644 index ebe7388150..0000000000 --- a/src/server/api/models/othello-game.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; -import { IUser, pack as packUser } from './user'; - -const OthelloGame = db.get<IOthelloGame>('othelloGames'); -export default OthelloGame; - -export interface IOthelloGame { - _id: mongo.ObjectID; - createdAt: Date; - startedAt: Date; - user1Id: mongo.ObjectID; - user2Id: mongo.ObjectID; - user1Accepted: boolean; - user2Accepted: boolean; - - /** - * どちらのプレイヤーが先行(黒)か - * 1 ... user1 - * 2 ... user2 - */ - black: number; - - isStarted: boolean; - isEnded: boolean; - winnerId: mongo.ObjectID; - logs: Array<{ - at: Date; - color: boolean; - pos: number; - }>; - settings: { - map: string[]; - bw: string | number; - isLlotheo: boolean; - canPutEverywhere: boolean; - loopedBoard: boolean; - }; - form1: any; - form2: any; - - // ログのposを文字列としてすべて連結したもののCRC32値 - crc32: string; -} - -/** - * Pack an othello game for API response - */ -export const pack = ( - game: any, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: true - }, options); - - let _game: any; - - // Populate the game if 'game' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(game)) { - _game = await OthelloGame.findOne({ - _id: game - }); - } else if (typeof game === 'string') { - _game = await OthelloGame.findOne({ - _id: new mongo.ObjectID(game) - }); - } else { - _game = deepcopy(game); - } - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - // Rename _id to id - _game.id = _game._id; - delete _game._id; - - if (opts.detail === false) { - delete _game.logs; - delete _game.settings.map; - } else { - // 互換性のため - if (_game.settings.map.hasOwnProperty('size')) { - _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); - } - } - - // Populate user - _game.user1 = await packUser(_game.user1Id, meId); - _game.user2 = await packUser(_game.user2Id, meId); - if (_game.winnerId) { - _game.winner = await packUser(_game.winnerId, meId); - } else { - _game.winner = null; - } - - resolve(_game); -}); diff --git a/src/server/api/models/othello-matching.ts b/src/server/api/models/othello-matching.ts deleted file mode 100644 index a294bd1ef9..0000000000 --- a/src/server/api/models/othello-matching.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; -import { IUser, pack as packUser } from './user'; - -const Matching = db.get<IMatching>('othelloMatchings'); -export default Matching; - -export interface IMatching { - _id: mongo.ObjectID; - createdAt: Date; - parentId: mongo.ObjectID; - childId: mongo.ObjectID; -} - -/** - * Pack an othello matching for API response - */ -export const pack = ( - matching: any, - me?: string | mongo.ObjectID | IUser -) => new Promise<any>(async (resolve, reject) => { - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - const _matching = deepcopy(matching); - - // Rename _id to id - _matching.id = _matching._id; - delete _matching._id; - - // Populate user - _matching.parent = await packUser(_matching.parentId, meId); - _matching.child = await packUser(_matching.childId, meId); - - resolve(_matching); -}); diff --git a/src/server/api/models/poll-vote.ts b/src/server/api/models/poll-vote.ts deleted file mode 100644 index 1cad95e5de..0000000000 --- a/src/server/api/models/poll-vote.ts +++ /dev/null @@ -1,17 +0,0 @@ -<<<<<<< HEAD:src/server/api/models/poll-vote.ts -import db from '../../../db/mongodb'; -======= -import * as mongo from 'mongodb'; -import db from '../../db/mongodb'; ->>>>>>> refs/remotes/origin/master:src/api/models/poll-vote.ts - -const PollVote = db.get<IPollVote>('pollVotes'); -export default PollVote; - -export interface IPollVote { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - postId: mongo.ObjectID; - choice: number; -} diff --git a/src/server/api/models/post-reaction.ts b/src/server/api/models/post-reaction.ts deleted file mode 100644 index f9a3f91c21..0000000000 --- a/src/server/api/models/post-reaction.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; -import Reaction from './post-reaction'; -import { pack as packUser } from './user'; - -const PostReaction = db.get<IPostReaction>('postReactions'); -export default PostReaction; - -export interface IPostReaction { - _id: mongo.ObjectID; - createdAt: Date; - deletedAt: Date; - postId: mongo.ObjectID; - userId: mongo.ObjectID; - reaction: string; -} - -/** - * Pack a reaction for API response - * - * @param {any} reaction - * @param {any} me? - * @return {Promise<any>} - */ -export const pack = ( - reaction: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _reaction: any; - - // Populate the reaction if 'reaction' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) { - _reaction = await Reaction.findOne({ - _id: reaction - }); - } else if (typeof reaction === 'string') { - _reaction = await Reaction.findOne({ - _id: new mongo.ObjectID(reaction) - }); - } else { - _reaction = deepcopy(reaction); - } - - // Rename _id to id - _reaction.id = _reaction._id; - delete _reaction._id; - - // Populate user - _reaction.user = await packUser(_reaction.userId, me); - - resolve(_reaction); -}); diff --git a/src/server/api/models/post-watching.ts b/src/server/api/models/post-watching.ts deleted file mode 100644 index abd6322495..0000000000 --- a/src/server/api/models/post-watching.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const PostWatching = db.get<IPostWatching>('postWatching'); -export default PostWatching; - -export interface IPostWatching { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - postId: mongo.ObjectID; -} diff --git a/src/server/api/models/post.ts b/src/server/api/models/post.ts deleted file mode 100644 index 1bf4e09051..0000000000 --- a/src/server/api/models/post.ts +++ /dev/null @@ -1,221 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import rap from '@prezzemolo/rap'; -import db from '../../../db/mongodb'; -import { IUser, pack as packUser } from './user'; -import { pack as packApp } from './app'; -import { pack as packChannel } from './channel'; -import Vote from './poll-vote'; -import Reaction from './post-reaction'; -import { pack as packFile } from './drive-file'; -import parse from '../common/text'; - -const Post = db.get<IPost>('posts'); - -export default Post; - -export function isValidText(text: string): boolean { - return text.length <= 1000 && text.trim() != ''; -} - -export type IPost = { - _id: mongo.ObjectID; - channelId: mongo.ObjectID; - createdAt: Date; - mediaIds: mongo.ObjectID[]; - replyId: mongo.ObjectID; - repostId: mongo.ObjectID; - poll: any; // todo - text: string; - userId: mongo.ObjectID; - appId: mongo.ObjectID; - viaMobile: boolean; - repostCount: number; - repliesCount: number; - reactionCounts: any; - mentions: mongo.ObjectID[]; - geo: { - coordinates: number[]; - altitude: number; - accuracy: number; - altitudeAccuracy: number; - heading: number; - speed: number; - }; -}; - -/** - * Pack a post for API response - * - * @param post target - * @param me? serializee - * @param options? serialize options - * @return response - */ -export const pack = async ( - post: string | mongo.ObjectID | IPost, - me?: string | mongo.ObjectID | IUser, - options?: { - detail: boolean - } -) => { - const opts = options || { - detail: true, - }; - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - let _post: any; - - // Populate the post if 'post' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(post)) { - _post = await Post.findOne({ - _id: post - }); - } else if (typeof post === 'string') { - _post = await Post.findOne({ - _id: new mongo.ObjectID(post) - }); - } else { - _post = deepcopy(post); - } - - if (!_post) throw 'invalid post arg.'; - - const id = _post._id; - - // Rename _id to id - _post.id = _post._id; - delete _post._id; - - delete _post.mentions; - if (_post.geo) delete _post.geo.type; - - // Parse text - if (_post.text) { - _post.ast = parse(_post.text); - } - - // Populate user - _post.user = packUser(_post.userId, meId); - - // Populate app - if (_post.appId) { - _post.app = packApp(_post.appId); - } - - // Populate channel - if (_post.channelId) { - _post.channel = packChannel(_post.channelId); - } - - // Populate media - if (_post.mediaIds) { - _post.media = Promise.all(_post.mediaIds.map(fileId => - packFile(fileId) - )); - } - - // When requested a detailed post data - if (opts.detail) { - // Get previous post info - _post.prev = (async () => { - const prev = await Post.findOne({ - userId: _post.userId, - _id: { - $lt: id - } - }, { - fields: { - _id: true - }, - sort: { - _id: -1 - } - }); - return prev ? prev._id : null; - })(); - - // Get next post info - _post.next = (async () => { - const next = await Post.findOne({ - userId: _post.userId, - _id: { - $gt: id - } - }, { - fields: { - _id: true - }, - sort: { - _id: 1 - } - }); - return next ? next._id : null; - })(); - - if (_post.replyId) { - // Populate reply to post - _post.reply = pack(_post.replyId, meId, { - detail: false - }); - } - - if (_post.repostId) { - // Populate repost - _post.repost = pack(_post.repostId, meId, { - detail: _post.text == null - }); - } - - // Poll - if (meId && _post.poll) { - _post.poll = (async (poll) => { - const vote = await Vote - .findOne({ - userId: meId, - postId: id - }); - - if (vote != null) { - const myChoice = poll.choices - .filter(c => c.id == vote.choice)[0]; - - myChoice.isVoted = true; - } - - return poll; - })(_post.poll); - } - - // Fetch my reaction - if (meId) { - _post.myReaction = (async () => { - const reaction = await Reaction - .findOne({ - userId: meId, - postId: id, - deletedAt: { $exists: false } - }); - - if (reaction) { - return reaction.reaction; - } - - return null; - })(); - } - } - - // resolve promises in _post object - _post = await rap(_post); - - return _post; -}; diff --git a/src/server/api/models/signin.ts b/src/server/api/models/signin.ts deleted file mode 100644 index bec6359477..0000000000 --- a/src/server/api/models/signin.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import db from '../../../db/mongodb'; - -const Signin = db.get<ISignin>('signin'); -export default Signin; - -export interface ISignin { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - ip: string; - headers: any; - success: boolean; -} - -/** - * Pack a signin record for API response - * - * @param {any} record - * @return {Promise<any>} - */ -export const pack = ( - record: any -) => new Promise<any>(async (resolve, reject) => { - - const _record = deepcopy(record); - - // Rename _id to id - _record.id = _record._id; - delete _record._id; - - resolve(_record); -}); diff --git a/src/server/api/models/sw-subscription.ts b/src/server/api/models/sw-subscription.ts deleted file mode 100644 index d3bbd75a6d..0000000000 --- a/src/server/api/models/sw-subscription.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../../db/mongodb'; - -const SwSubscription = db.get<ISwSubscription>('swSubscriptions'); -export default SwSubscription; - -export interface ISwSubscription { - _id: mongo.ObjectID; - userId: mongo.ObjectID; - endpoint: string; - auth: string; - publickey: string; -} diff --git a/src/server/api/models/user.ts b/src/server/api/models/user.ts deleted file mode 100644 index 419ad53970..0000000000 --- a/src/server/api/models/user.ts +++ /dev/null @@ -1,341 +0,0 @@ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import rap from '@prezzemolo/rap'; -import db from '../../../db/mongodb'; -import { IPost, pack as packPost } from './post'; -import Following from './following'; -import Mute from './mute'; -import getFriends from '../common/get-friends'; -import config from '../../../conf'; - -const User = db.get<IUser>('users'); - -User.createIndex('username'); -User.createIndex('account.token'); - -export default User; - -export function validateUsername(username: string): boolean { - return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username); -} - -export function validatePassword(password: string): boolean { - return typeof password == 'string' && password != ''; -} - -export function isValidName(name: string): boolean { - return typeof name == 'string' && name.length < 30 && name.trim() != ''; -} - -export function isValidDescription(description: string): boolean { - return typeof description == 'string' && description.length < 500 && description.trim() != ''; -} - -export function isValidLocation(location: string): boolean { - return typeof location == 'string' && location.length < 50 && location.trim() != ''; -} - -export function isValidBirthday(birthday: string): boolean { - return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); -} - -export type ILocalAccount = { - keypair: string; - email: string; - links: string[]; - password: string; - token: string; - twitter: { - accessToken: string; - accessTokenSecret: string; - userId: string; - screenName: string; - }; - line: { - userId: string; - }; - profile: { - location: string; - birthday: string; // 'YYYY-MM-DD' - tags: string[]; - }; - lastUsedAt: Date; - isBot: boolean; - isPro: boolean; - twoFactorSecret: string; - twoFactorEnabled: boolean; - twoFactorTempSecret: string; - clientSettings: any; - settings: any; -}; - -export type IRemoteAccount = { - uri: string; -}; - -export type IUser = { - _id: mongo.ObjectID; - createdAt: Date; - deletedAt: Date; - followersCount: number; - followingCount: number; - name: string; - postsCount: number; - driveCapacity: number; - username: string; - usernameLower: string; - avatarId: mongo.ObjectID; - bannerId: mongo.ObjectID; - data: any; - description: string; - latestPost: IPost; - pinnedPostId: mongo.ObjectID; - isSuspended: boolean; - keywords: string[]; - host: string; - hostLower: string; - account: ILocalAccount | IRemoteAccount; -}; - -export function init(user): IUser { - user._id = new mongo.ObjectID(user._id); - user.avatarId = new mongo.ObjectID(user.avatarId); - user.bannerId = new mongo.ObjectID(user.bannerId); - user.pinnedPostId = new mongo.ObjectID(user.pinnedPostId); - return user; -} - -/** - * Pack a user for API response - * - * @param user target - * @param me? serializee - * @param options? serialize options - * @return Packed user - */ -export const pack = ( - user: string | mongo.ObjectID | IUser, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean, - includeSecrets?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - - const opts = Object.assign({ - detail: false, - includeSecrets: false - }, options); - - let _user: any; - - const fields = opts.detail ? { - } : { - 'account.settings': false, - 'account.clientSettings': false, - 'account.profile': false, - 'account.keywords': false, - 'account.domains': false - }; - - // Populate the user if 'user' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(user)) { - _user = await User.findOne({ - _id: user - }, { fields }); - } else if (typeof user === 'string') { - _user = await User.findOne({ - _id: new mongo.ObjectID(user) - }, { fields }); - } else { - _user = deepcopy(user); - } - - if (!_user) return reject('invalid user arg.'); - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - // Rename _id to id - _user.id = _user._id; - delete _user._id; - - // Remove needless properties - delete _user.latestPost; - - if (!_user.host) { - // Remove private properties - delete _user.account.keypair; - delete _user.account.password; - delete _user.account.token; - delete _user.account.twoFactorTempSecret; - delete _user.account.twoFactorSecret; - delete _user.usernameLower; - if (_user.account.twitter) { - delete _user.account.twitter.accessToken; - delete _user.account.twitter.accessTokenSecret; - } - delete _user.account.line; - - // Visible via only the official client - if (!opts.includeSecrets) { - delete _user.account.email; - delete _user.account.settings; - delete _user.account.clientSettings; - } - - if (!opts.detail) { - delete _user.account.twoFactorEnabled; - } - } - - _user.avatarUrl = _user.avatarId != null - ? `${config.drive_url}/${_user.avatarId}` - : `${config.drive_url}/default-avatar.jpg`; - - _user.bannerUrl = _user.bannerId != null - ? `${config.drive_url}/${_user.bannerId}` - : null; - - if (!meId || !meId.equals(_user.id) || !opts.detail) { - delete _user.avatarId; - delete _user.bannerId; - - delete _user.driveCapacity; - } - - if (meId && !meId.equals(_user.id)) { - // Whether the user is following - _user.isFollowing = (async () => { - const follow = await Following.findOne({ - followerId: meId, - followeeId: _user.id, - deletedAt: { $exists: false } - }); - return follow !== null; - })(); - - // Whether the user is followed - _user.isFollowed = (async () => { - const follow2 = await Following.findOne({ - followerId: _user.id, - followeeId: meId, - deletedAt: { $exists: false } - }); - return follow2 !== null; - })(); - - // Whether the user is muted - _user.isMuted = (async () => { - const mute = await Mute.findOne({ - muterId: meId, - muteeId: _user.id, - deletedAt: { $exists: false } - }); - return mute !== null; - })(); - } - - if (opts.detail) { - if (_user.pinnedPostId) { - // Populate pinned post - _user.pinnedPost = packPost(_user.pinnedPostId, meId, { - detail: true - }); - } - - if (meId && !meId.equals(_user.id)) { - const myFollowingIds = await getFriends(meId); - - // Get following you know count - _user.followingYouKnowCount = Following.count({ - followeeId: { $in: myFollowingIds }, - followerId: _user.id, - deletedAt: { $exists: false } - }); - - // Get followers you know count - _user.followersYouKnowCount = Following.count({ - followeeId: _user.id, - followerId: { $in: myFollowingIds }, - deletedAt: { $exists: false } - }); - } - } - - // resolve promises in _user object - _user = await rap(_user); - - resolve(_user); -}); - -/** - * Pack a user for ActivityPub - * - * @param user target - * @return Packed user - */ -export const packForAp = ( - user: string | mongo.ObjectID | IUser -) => new Promise<any>(async (resolve, reject) => { - - let _user: any; - - const fields = { - // something - }; - - // Populate the user if 'user' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(user)) { - _user = await User.findOne({ - _id: user - }, { fields }); - } else if (typeof user === 'string') { - _user = await User.findOne({ - _id: new mongo.ObjectID(user) - }, { fields }); - } else { - _user = deepcopy(user); - } - - if (!_user) return reject('invalid user arg.'); - - const userUrl = `${config.url}/@${_user.username}`; - - resolve({ - "@context": ["https://www.w3.org/ns/activitystreams", { - "@language": "ja" - }], - "type": "Person", - "id": userUrl, - "following": `${userUrl}/following.json`, - "followers": `${userUrl}/followers.json`, - "liked": `${userUrl}/liked.json`, - "inbox": `${userUrl}/inbox.json`, - "outbox": `${userUrl}/outbox.json`, - "preferredUsername": _user.username, - "name": _user.name, - "summary": _user.description, - "icon": [ - `${config.drive_url}/${_user.avatarId}` - ] - }); -}); - -/* -function img(url) { - return { - thumbnail: { - large: `${url}`, - medium: '', - small: '' - } - }; -} -*/ diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 4b60f4c752..d78fa11b80 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -1,8 +1,8 @@ import * as express from 'express'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; -import { default as User, ILocalAccount, IUser } from '../models/user'; -import Signin, { pack } from '../models/signin'; +import { default as User, ILocalAccount, IUser } from '../../../models/user'; +import Signin, { pack } from '../../../models/signin'; import event from '../event'; import signin from '../common/signin'; import config from '../../../conf'; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index cad9752c45..fd47b53037 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -3,7 +3,7 @@ import * as express from 'express'; import * as bcrypt from 'bcryptjs'; import { generate as generateKeypair } from '../../../crypto_key'; import recaptcha = require('recaptcha-promise'); -import User, { IUser, validateUsername, validatePassword, pack } from '../models/user'; +import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user'; import generateUserToken from '../common/generate-native-user-token'; import config from '../../../conf'; diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index 98732e6b85..6eacdb7472 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -1,7 +1,7 @@ import * as EventEmitter from 'events'; import * as express from 'express'; -const crypto = require('crypto'); -import User from '../models/user'; +//const crypto = require('crypto'); +import User from '../../../models/user'; import config from '../../../conf'; import queue from '../../../queue'; diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index bdbedc8646..d77341db2b 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -5,7 +5,7 @@ import * as uuid from 'uuid'; // const Twitter = require('twitter'); import autwh from 'autwh'; import redis from '../../../db/redis'; -import User, { pack } from '../models/user'; +import User, { pack } from '../../../models/user'; import event from '../event'; import config from '../../../conf'; import signin from '../common/signin'; diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts index d67d77cbf4..cb04278237 100644 --- a/src/server/api/stream/channel.ts +++ b/src/server/api/stream/channel.ts @@ -1,8 +1,10 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; +import { ParsedUrlQuery } from 'querystring'; export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void { - const channel = request.resourceURL.query.channel; + const q = request.resourceURL.query as ParsedUrlQuery; + const channel = q.channel; // Subscribe channel stream subscriber.subscribe(`misskey:channel-stream:${channel}`); diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts index 291be0824d..648bd7c3c2 100644 --- a/src/server/api/stream/home.ts +++ b/src/server/api/stream/home.ts @@ -2,9 +2,9 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; import * as debug from 'debug'; -import User from '../models/user'; -import Mute from '../models/mute'; -import { pack as packPost } from '../models/post'; +import User from '../../../models/user'; +import Mute from '../../../models/mute'; +import { pack as packPost } from '../../../models/post'; import readNotification from '../common/read-notification'; const log = debug('misskey'); diff --git a/src/server/api/stream/messaging.ts b/src/server/api/stream/messaging.ts index a4a12426a3..3e6c2cd509 100644 --- a/src/server/api/stream/messaging.ts +++ b/src/server/api/stream/messaging.ts @@ -1,9 +1,11 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; import read from '../common/read-messaging-message'; +import { ParsedUrlQuery } from 'querystring'; export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { - const otherparty = request.resourceURL.query.otherparty; + const q = request.resourceURL.query as ParsedUrlQuery; + const otherparty = q.otherparty as string; // Subscribe messaging stream subscriber.subscribe(`misskey:messaging-stream:${user._id}-${otherparty}`); diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts index e48d93cddc..b6a251c4c8 100644 --- a/src/server/api/stream/othello-game.ts +++ b/src/server/api/stream/othello-game.ts @@ -1,13 +1,15 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; import * as CRC32 from 'crc-32'; -import OthelloGame, { pack } from '../models/othello-game'; +import OthelloGame, { pack } from '../../../models/othello-game'; import { publishOthelloGameStream } from '../event'; -import Othello from '../../common/othello/core'; -import * as maps from '../../common/othello/maps'; +import Othello from '../../../common/othello/core'; +import * as maps from '../../../common/othello/maps'; +import { ParsedUrlQuery } from 'querystring'; export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void { - const gameId = request.resourceURL.query.game; + const q = request.resourceURL.query as ParsedUrlQuery; + const gameId = q.game; // Subscribe game stream subscriber.subscribe(`misskey:othello-game-stream:${gameId}`); diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts index 55c993ec85..4205afae7c 100644 --- a/src/server/api/stream/othello.ts +++ b/src/server/api/stream/othello.ts @@ -1,7 +1,7 @@ import * as mongo from 'mongodb'; import * as websocket from 'websocket'; import * as redis from 'redis'; -import Matching, { pack } from '../models/othello-matching'; +import Matching, { pack } from '../../../models/othello-matching'; import publishUserStream from '../event'; export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index 73f099bd80..c86c6a8b4a 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -2,8 +2,8 @@ import * as http from 'http'; import * as websocket from 'websocket'; import * as redis from 'redis'; import config from '../../conf'; -import { default as User, IUser } from './models/user'; -import AccessToken from './models/access-token'; +import { default as User, IUser } from '../../models/user'; +import AccessToken from '../../models/access-token'; import isNativeToken from './common/is-native-token'; import homeStream from './stream/home'; @@ -15,6 +15,7 @@ import othelloStream from './stream/othello'; import serverStream from './stream/server'; import requestsStream from './stream/requests'; import channelStream from './stream/channel'; +import { ParsedUrlQuery } from 'querystring'; module.exports = (server: http.Server) => { /** @@ -51,7 +52,8 @@ module.exports = (server: http.Server) => { return; } - const user = await authenticate(request.resourceURL.query.i); + const q = request.resourceURL.query as ParsedUrlQuery; + const user = await authenticate(q.i as string); if (request.resourceURL.pathname === '/othello-game') { othelloGameStream(request, connection, subscriber, user); diff --git a/src/server/common/get-notification-summary.ts b/src/server/common/get-notification-summary.ts deleted file mode 100644 index 03db722c84..0000000000 --- a/src/server/common/get-notification-summary.ts +++ /dev/null @@ -1,27 +0,0 @@ -import getPostSummary from './get-post-summary'; -import getReactionEmoji from './get-reaction-emoji'; - -/** - * 通知を表す文字列を取得します。 - * @param notification 通知 - */ -export default function(notification: any): string { - switch (notification.type) { - case 'follow': - return `${notification.user.name}にフォローされました`; - case 'mention': - return `言及されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; - case 'reply': - return `返信されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; - case 'repost': - return `Repostされました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; - case 'quote': - return `引用されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; - case 'reaction': - return `リアクションされました:\n${notification.user.name} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`; - case 'poll_vote': - return `投票されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; - default: - return `<不明な通知タイプ: ${notification.type}>`; - } -} diff --git a/src/server/common/get-post-summary.ts b/src/server/common/get-post-summary.ts deleted file mode 100644 index 8d0033064f..0000000000 --- a/src/server/common/get-post-summary.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 投稿を表す文字列を取得します。 - * @param {*} post 投稿 - */ -const summarize = (post: any): string => { - let summary = ''; - - // チャンネル - summary += post.channel ? `${post.channel.title}:` : ''; - - // 本文 - summary += post.text ? post.text : ''; - - // メディアが添付されているとき - if (post.media) { - summary += ` (${post.media.length}つのメディア)`; - } - - // 投票が添付されているとき - if (post.poll) { - summary += ' (投票)'; - } - - // 返信のとき - if (post.replyId) { - if (post.reply) { - summary += ` RE: ${summarize(post.reply)}`; - } else { - summary += ' RE: ...'; - } - } - - // Repostのとき - if (post.repostId) { - if (post.repost) { - summary += ` RP: ${summarize(post.repost)}`; - } else { - summary += ' RP: ...'; - } - } - - return summary.trim(); -}; - -export default summarize; diff --git a/src/server/common/get-reaction-emoji.ts b/src/server/common/get-reaction-emoji.ts deleted file mode 100644 index c661205379..0000000000 --- a/src/server/common/get-reaction-emoji.ts +++ /dev/null @@ -1,14 +0,0 @@ -export default function(reaction: string): string { - switch (reaction) { - case 'like': return '👍'; - case 'love': return '❤️'; - case 'laugh': return '😆'; - case 'hmm': return '🤔'; - case 'surprise': return '😮'; - case 'congrats': return '🎉'; - case 'angry': return '💢'; - case 'confused': return '😥'; - case 'pudding': return '🍮'; - default: return ''; - } -} diff --git a/src/server/common/othello/ai/back.ts b/src/server/common/othello/ai/back.ts deleted file mode 100644 index 629e57113b..0000000000 --- a/src/server/common/othello/ai/back.ts +++ /dev/null @@ -1,376 +0,0 @@ -/** - * -AI- - * Botのバックエンド(思考を担当) - * - * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから - * 切断されてしまうので、別々のプロセスで行うようにします - */ - -import * as request from 'request-promise-native'; -import Othello, { Color } from '../core'; -import conf from '../../../../conf'; - -let game; -let form; - -/** - * BotアカウントのユーザーID - */ -const id = conf.othello_ai.id; - -/** - * BotアカウントのAPIキー - */ -const i = conf.othello_ai.i; - -let post; - -process.on('message', async msg => { - // 親プロセスからデータをもらう - if (msg.type == '_init_') { - game = msg.game; - form = msg.form; - } - - // フォームが更新されたとき - if (msg.type == 'update-form') { - form.find(i => i.id == msg.body.id).value = msg.body.value; - } - - // ゲームが始まったとき - if (msg.type == 'started') { - onGameStarted(msg.body); - - //#region TLに投稿する - const game = msg.body; - const url = `${conf.url}/othello/${game.id}`; - const user = game.user1Id == id ? game.user2 : game.user1; - const isSettai = form[0].value === 0; - const text = isSettai - ? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!` - : `対局を?[${user.name}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`; - - const res = await request.post(`${conf.api_url}/posts/create`, { - json: { i, - text: `${text}\n→[観戦する](${url})` - } - }); - - post = res.createdPost; - //#endregion - } - - // ゲームが終了したとき - if (msg.type == 'ended') { - // ストリームから切断 - process.send({ - type: 'close' - }); - - //#region TLに投稿する - const user = game.user1Id == id ? game.user2 : game.user1; - const isSettai = form[0].value === 0; - const text = isSettai - ? msg.body.winnerId === null - ? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...` - : msg.body.winnerId == id - ? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` - : `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` - : msg.body.winnerId === null - ? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~` - : msg.body.winnerId == id - ? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪` - : `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`; - - await request.post(`${conf.api_url}/posts/create`, { - json: { i, - repostId: post.id, - text: text - } - }); - //#endregion - - process.exit(); - } - - // 打たれたとき - if (msg.type == 'set') { - onSet(msg.body); - } -}); - -let o: Othello; -let botColor: Color; - -// 各マスの強さ -let cellWeights; - -/** - * ゲーム開始時 - * @param g ゲーム情報 - */ -function onGameStarted(g) { - game = g; - - // オセロエンジン初期化 - o = new Othello(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard - }); - - // 各マスの価値を計算しておく - cellWeights = o.map.map((pix, i) => { - if (pix == 'null') return 0; - const [x, y] = o.transformPosToXy(i); - let count = 0; - const get = (x, y) => { - if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null'; - return o.mapDataGet(o.transformXyToPos(x, y)); - }; - - if (get(x , y - 1) == 'null') count++; - if (get(x + 1, y - 1) == 'null') count++; - if (get(x + 1, y ) == 'null') count++; - if (get(x + 1, y + 1) == 'null') count++; - if (get(x , y + 1) == 'null') count++; - if (get(x - 1, y + 1) == 'null') count++; - if (get(x - 1, y ) == 'null') count++; - if (get(x - 1, y - 1) == 'null') count++; - //return Math.pow(count, 3); - return count >= 4 ? 1 : 0; - }); - - botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2; - - if (botColor) { - think(); - } -} - -function onSet(x) { - o.put(x.color, x.pos); - - if (x.next === botColor) { - think(); - } -} - -const db = {}; - -function think() { - console.log('Thinking...'); - console.time('think'); - - const isSettai = form[0].value === 0; - - // 接待モードのときは、全力(5手先読みくらい)で負けるようにする - const maxDepth = isSettai ? 5 : form[0].value; - - /** - * Botにとってある局面がどれだけ有利か取得する - */ - function staticEval() { - let score = o.canPutSomewhere(botColor).length; - - cellWeights.forEach((weight, i) => { - // 係数 - const coefficient = 30; - weight = weight * coefficient; - - const stone = o.board[i]; - if (stone === botColor) { - // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する - score += weight; - } else if (stone !== null) { - score -= weight; - } - }); - - // ロセオならスコアを反転 - if (game.settings.isLlotheo) score = -score; - - // 接待ならスコアを反転 - if (isSettai) score = -score; - - return score; - } - - /** - * αβ法での探索 - */ - const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { - // 試し打ち - o.put(o.turn, pos); - - const key = o.board.toString(); - let cache = db[key]; - if (cache) { - if (alpha >= cache.upper) { - o.undo(); - return cache.upper; - } - if (beta <= cache.lower) { - o.undo(); - return cache.lower; - } - alpha = Math.max(alpha, cache.lower); - beta = Math.min(beta, cache.upper); - } else { - cache = { - upper: Infinity, - lower: -Infinity - }; - } - - const isBotTurn = o.turn === botColor; - - // 勝った - if (o.turn === null) { - const winner = o.winner; - - // 勝つことによる基本スコア - const base = 10000; - - let score; - - if (game.settings.isLlotheo) { - // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); - } else { - // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); - } - - // 巻き戻し - o.undo(); - - // 接待なら自分が負けた方が高スコア - return isSettai - ? winner !== botColor ? score : -score - : winner === botColor ? score : -score; - } - - if (depth === maxDepth) { - // 静的に評価 - const score = staticEval(); - - // 巻き戻し - o.undo(); - - return score; - } else { - const cans = o.canPutSomewhere(o.turn); - - let value = isBotTurn ? -Infinity : Infinity; - let a = alpha; - let b = beta; - - // 次のターンのプレイヤーにとって最も良い手を取得 - for (const p of cans) { - if (isBotTurn) { - const score = dive(p, a, beta, depth + 1); - value = Math.max(value, score); - a = Math.max(a, value); - if (value >= beta) break; - } else { - const score = dive(p, alpha, b, depth + 1); - value = Math.min(value, score); - b = Math.min(b, value); - if (value <= alpha) break; - } - } - - // 巻き戻し - o.undo(); - - if (value <= alpha) { - cache.upper = value; - } else if (value >= beta) { - cache.lower = value; - } else { - cache.upper = value; - cache.lower = value; - } - - db[key] = cache; - - return value; - } - }; - - /** - * αβ法での探索(キャッシュ無し)(デバッグ用) - */ - const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { - // 試し打ち - o.put(o.turn, pos); - - const isBotTurn = o.turn === botColor; - - // 勝った - if (o.turn === null) { - const winner = o.winner; - - // 勝つことによる基本スコア - const base = 10000; - - let score; - - if (game.settings.isLlotheo) { - // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); - } else { - // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); - } - - // 巻き戻し - o.undo(); - - // 接待なら自分が負けた方が高スコア - return isSettai - ? winner !== botColor ? score : -score - : winner === botColor ? score : -score; - } - - if (depth === maxDepth) { - // 静的に評価 - const score = staticEval(); - - // 巻き戻し - o.undo(); - - return score; - } else { - const cans = o.canPutSomewhere(o.turn); - - // 次のターンのプレイヤーにとって最も良い手を取得 - for (const p of cans) { - if (isBotTurn) { - alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1)); - } else { - beta = Math.min(beta, dive2(p, alpha, beta, depth + 1)); - } - if (alpha >= beta) break; - } - - // 巻き戻し - o.undo(); - - return isBotTurn ? alpha : beta; - } - }; - - const cans = o.canPutSomewhere(botColor); - const scores = cans.map(p => dive(p)); - const pos = cans[scores.indexOf(Math.max(...scores))]; - - console.log('Thinked:', pos); - console.timeEnd('think'); - - process.send({ - type: 'put', - pos - }); -} diff --git a/src/server/common/othello/ai/front.ts b/src/server/common/othello/ai/front.ts deleted file mode 100644 index fb7a9be131..0000000000 --- a/src/server/common/othello/ai/front.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * -AI- - * Botのフロントエンド(ストリームとの対話を担当) - * - * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから - * 切断されてしまうので、別々のプロセスで行うようにします - */ - -import * as childProcess from 'child_process'; -const WebSocket = require('ws'); -import * as ReconnectingWebSocket from 'reconnecting-websocket'; -import * as request from 'request-promise-native'; -import conf from '../../../../conf'; - -// 設定 //////////////////////////////////////////////////////// - -/** - * BotアカウントのAPIキー - */ -const i = conf.othello_ai.i; - -/** - * BotアカウントのユーザーID - */ -const id = conf.othello_ai.id; - -//////////////////////////////////////////////////////////////// - -/** - * ホームストリーム - */ -const homeStream = new ReconnectingWebSocket(`${conf.ws_url}/?i=${i}`, undefined, { - constructor: WebSocket -}); - -homeStream.on('open', () => { - console.log('home stream opened'); -}); - -homeStream.on('close', () => { - console.log('home stream closed'); -}); - -homeStream.on('message', message => { - const msg = JSON.parse(message.toString()); - - // タイムライン上でなんか言われたまたは返信されたとき - if (msg.type == 'mention' || msg.type == 'reply') { - const post = msg.body; - - if (post.userId == id) return; - - // リアクションする - request.post(`${conf.api_url}/posts/reactions/create`, { - json: { i, - postId: post.id, - reaction: 'love' - } - }); - - if (post.text) { - if (post.text.indexOf('オセロ') > -1) { - request.post(`${conf.api_url}/posts/create`, { - json: { i, - replyId: post.id, - text: '良いですよ~' - } - }); - - invite(post.userId); - } - } - } - - // メッセージでなんか言われたとき - if (msg.type == 'messaging_message') { - const message = msg.body; - if (message.text) { - if (message.text.indexOf('オセロ') > -1) { - request.post(`${conf.api_url}/messaging/messages/create`, { - json: { i, - userId: message.userId, - text: '良いですよ~' - } - }); - - invite(message.userId); - } - } - } -}); - -// ユーザーを対局に誘う -function invite(userId) { - request.post(`${conf.api_url}/othello/match`, { - json: { i, - userId: userId - } - }); -} - -/** - * オセロストリーム - */ -const othelloStream = new ReconnectingWebSocket(`${conf.ws_url}/othello?i=${i}`, undefined, { - constructor: WebSocket -}); - -othelloStream.on('open', () => { - console.log('othello stream opened'); -}); - -othelloStream.on('close', () => { - console.log('othello stream closed'); -}); - -othelloStream.on('message', message => { - const msg = JSON.parse(message.toString()); - - // 招待されたとき - if (msg.type == 'invited') { - onInviteMe(msg.body.parent); - } - - // マッチしたとき - if (msg.type == 'matched') { - gameStart(msg.body); - } -}); - -/** - * ゲーム開始 - * @param game ゲーム情報 - */ -function gameStart(game) { - // ゲームストリームに接続 - const gw = new ReconnectingWebSocket(`${conf.ws_url}/othello-game?i=${i}&game=${game.id}`, undefined, { - constructor: WebSocket - }); - - gw.on('open', () => { - console.log('othello game stream opened'); - - // フォーム - const form = [{ - id: 'strength', - type: 'radio', - label: '強さ', - value: 2, - items: [{ - label: '接待', - value: 0 - }, { - label: '弱', - value: 1 - }, { - label: '中', - value: 2 - }, { - label: '強', - value: 3 - }, { - label: '最強', - value: 5 - }] - }]; - - //#region バックエンドプロセス開始 - const ai = childProcess.fork(__dirname + '/back.js'); - - // バックエンドプロセスに情報を渡す - ai.send({ - type: '_init_', - game, - form - }); - - ai.on('message', msg => { - if (msg.type == 'put') { - gw.send(JSON.stringify({ - type: 'set', - pos: msg.pos - })); - } else if (msg.type == 'close') { - gw.close(); - } - }); - - // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える - gw.on('message', message => { - const msg = JSON.parse(message.toString()); - ai.send(msg); - }); - //#endregion - - // フォーム初期化 - setTimeout(() => { - gw.send(JSON.stringify({ - type: 'init-form', - body: form - })); - }, 1000); - - // どんな設定内容の対局でも受け入れる - setTimeout(() => { - gw.send(JSON.stringify({ - type: 'accept' - })); - }, 2000); - }); - - gw.on('close', () => { - console.log('othello game stream closed'); - }); -} - -/** - * オセロの対局に招待されたとき - * @param inviter 誘ってきたユーザー - */ -async function onInviteMe(inviter) { - console.log(`Someone invited me: @${inviter.username}`); - - // 承認 - const game = await request.post(`${conf.api_url}/othello/match`, { - json: { - i, - userId: inviter.id - } - }); - - gameStart(game); -} diff --git a/src/server/common/othello/ai/index.ts b/src/server/common/othello/ai/index.ts deleted file mode 100644 index 5cd1db82da..0000000000 --- a/src/server/common/othello/ai/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./front'); diff --git a/src/server/common/othello/core.ts b/src/server/common/othello/core.ts deleted file mode 100644 index 217066d375..0000000000 --- a/src/server/common/othello/core.ts +++ /dev/null @@ -1,340 +0,0 @@ -/** - * true ... 黒 - * false ... 白 - */ -export type Color = boolean; -const BLACK = true; -const WHITE = false; - -export type MapPixel = 'null' | 'empty'; - -export type Options = { - isLlotheo: boolean; - canPutEverywhere: boolean; - loopedBoard: boolean; -}; - -export type Undo = { - /** - * 色 - */ - color: Color, - - /** - * どこに打ったか - */ - pos: number; - - /** - * 反転した石の位置の配列 - */ - effects: number[]; - - /** - * ターン - */ - turn: Color; -}; - -/** - * オセロエンジン - */ -export default class Othello { - public map: MapPixel[]; - public mapWidth: number; - public mapHeight: number; - public board: Color[]; - public turn: Color = BLACK; - public opts: Options; - - public prevPos = -1; - public prevColor: Color = null; - - private logs: Undo[] = []; - - /** - * ゲームを初期化します - */ - constructor(map: string[], opts: Options) { - //#region binds - this.put = this.put.bind(this); - //#endregion - - //#region Options - this.opts = opts; - if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; - if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; - if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; - //#endregion - - //#region Parse map data - this.mapWidth = map[0].length; - this.mapHeight = map.length; - const mapData = map.join(''); - - this.board = mapData.split('').map(d => { - if (d == '-') return null; - if (d == 'b') return BLACK; - if (d == 'w') return WHITE; - return undefined; - }); - - this.map = mapData.split('').map(d => { - if (d == '-' || d == 'b' || d == 'w') return 'empty'; - return 'null'; - }); - //#endregion - - // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある - if (this.canPutSomewhere(BLACK).length == 0) { - if (this.canPutSomewhere(WHITE).length == 0) { - this.turn = null; - } else { - this.turn = WHITE; - } - } - } - - /** - * 黒石の数 - */ - public get blackCount() { - return this.board.filter(x => x === BLACK).length; - } - - /** - * 白石の数 - */ - public get whiteCount() { - return this.board.filter(x => x === WHITE).length; - } - - /** - * 黒石の比率 - */ - public get blackP() { - if (this.blackCount == 0 && this.whiteCount == 0) return 0; - return this.blackCount / (this.blackCount + this.whiteCount); - } - - /** - * 白石の比率 - */ - public get whiteP() { - if (this.blackCount == 0 && this.whiteCount == 0) return 0; - return this.whiteCount / (this.blackCount + this.whiteCount); - } - - public transformPosToXy(pos: number): number[] { - const x = pos % this.mapWidth; - const y = Math.floor(pos / this.mapWidth); - return [x, y]; - } - - public transformXyToPos(x: number, y: number): number { - return x + (y * this.mapWidth); - } - - /** - * 指定のマスに石を打ちます - * @param color 石の色 - * @param pos 位置 - */ - public put(color: Color, pos: number) { - this.prevPos = pos; - this.prevColor = color; - - this.board[pos] = color; - - // 反転させられる石を取得 - const effects = this.effects(color, pos); - - // 反転させる - for (const pos of effects) { - this.board[pos] = color; - } - - const turn = this.turn; - - this.logs.push({ - color, - pos, - effects, - turn - }); - - this.calcTurn(); - } - - private calcTurn() { - // ターン計算 - if (this.canPutSomewhere(!this.prevColor).length > 0) { - this.turn = !this.prevColor; - } else if (this.canPutSomewhere(this.prevColor).length > 0) { - this.turn = this.prevColor; - } else { - this.turn = null; - } - } - - public undo() { - const undo = this.logs.pop(); - this.prevColor = undo.color; - this.prevPos = undo.pos; - this.board[undo.pos] = null; - for (const pos of undo.effects) { - const color = this.board[pos]; - this.board[pos] = !color; - } - this.turn = undo.turn; - } - - /** - * 指定した位置のマップデータのマスを取得します - * @param pos 位置 - */ - public mapDataGet(pos: number): MapPixel { - const [x, y] = this.transformPosToXy(pos); - if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null'; - return this.map[pos]; - } - - /** - * 打つことができる場所を取得します - */ - public canPutSomewhere(color: Color): number[] { - const result = []; - - this.board.forEach((x, i) => { - if (this.canPut(color, i)) result.push(i); - }); - - return result; - } - - /** - * 指定のマスに石を打つことができるかどうかを取得します - * @param color 自分の色 - * @param pos 位置 - */ - public canPut(color: Color, pos: number): boolean { - // 既に石が置いてある場所には打てない - if (this.board[pos] !== null) return false; - - if (this.opts.canPutEverywhere) { - // 挟んでなくても置けるモード - return this.mapDataGet(pos) == 'empty'; - } else { - // 相手の石を1つでも反転させられるか - return this.effects(color, pos).length !== 0; - } - } - - /** - * 指定のマスに石を置いた時の、反転させられる石を取得します - * @param color 自分の色 - * @param pos 位置 - */ - public effects(color: Color, pos: number): number[] { - const enemyColor = !color; - - // ひっくり返せる石(の位置)リスト - let stones = []; - - const initPos = pos; - - // 走査 - const iterate = (fn: (i: number) => number[]) => { - let i = 1; - const found = []; - - while (true) { - let [x, y] = fn(i); - - // 座標が指し示す位置がボード外に出たとき - if (this.opts.loopedBoard) { - if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth); - if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight); - if (x >= this.mapWidth ) x = x % this.mapWidth; - if (y >= this.mapHeight) y = y % this.mapHeight; - - // for debug - //if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) { - // console.log(x, y); - //} - - // 一周して自分に帰ってきたら - if (this.transformXyToPos(x, y) == initPos) { - // ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、 - // そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります) - // このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます - // (あと無効な方がゲームとしておもしろそうだった) - stones = stones.concat(found); - break; - } - } else { - if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break; - } - - const pos = this.transformXyToPos(x, y); - - //#region 「配置不能」マスに当たった場合走査終了 - const pixel = this.mapDataGet(pos); - if (pixel == 'null') break; - //#endregion - - // 石取得 - const stone = this.board[pos]; - - // 石が置かれていないマスなら走査終了 - if (stone === null) break; - - // 相手の石なら「ひっくり返せるかもリスト」に入れておく - if (stone === enemyColor) found.push(pos); - - // 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了 - if (stone === color) { - stones = stones.concat(found); - break; - } - - i++; - } - }; - - const [x, y] = this.transformPosToXy(pos); - - iterate(i => [x , y - i]); // 上 - iterate(i => [x + i, y - i]); // 右上 - iterate(i => [x + i, y ]); // 右 - iterate(i => [x + i, y + i]); // 右下 - iterate(i => [x , y + i]); // 下 - iterate(i => [x - i, y + i]); // 左下 - iterate(i => [x - i, y ]); // 左 - iterate(i => [x - i, y - i]); // 左上 - - return stones; - } - - /** - * ゲームが終了したか否か - */ - public get isEnded(): boolean { - return this.turn === null; - } - - /** - * ゲームの勝者 (null = 引き分け) - */ - public get winner(): Color { - if (!this.isEnded) return undefined; - - if (this.blackCount == this.whiteCount) return null; - - if (this.opts.isLlotheo) { - return this.blackCount > this.whiteCount ? WHITE : BLACK; - } else { - return this.blackCount > this.whiteCount ? BLACK : WHITE; - } - } -} diff --git a/src/server/common/othello/maps.ts b/src/server/common/othello/maps.ts deleted file mode 100644 index 68e5a446f1..0000000000 --- a/src/server/common/othello/maps.ts +++ /dev/null @@ -1,911 +0,0 @@ -/** - * 組み込みマップ定義 - * - * データ値: - * (スペース) ... マス無し - * - ... マス - * b ... 初期配置される黒石 - * w ... 初期配置される白石 - */ - -export type Map = { - name?: string; - category?: string; - author?: string; - data: string[]; -}; - -export const fourfour: Map = { - name: '4x4', - category: '4x4', - data: [ - '----', - '-wb-', - '-bw-', - '----' - ] -}; - -export const sixsix: Map = { - name: '6x6', - category: '6x6', - data: [ - '------', - '------', - '--wb--', - '--bw--', - '------', - '------' - ] -}; - -export const roundedSixsix: Map = { - name: '6x6 rounded', - category: '6x6', - author: 'syuilo', - data: [ - ' ---- ', - '------', - '--wb--', - '--bw--', - '------', - ' ---- ' - ] -}; - -export const roundedSixsix2: Map = { - name: '6x6 rounded 2', - category: '6x6', - author: 'syuilo', - data: [ - ' -- ', - ' ---- ', - '--wb--', - '--bw--', - ' ---- ', - ' -- ' - ] -}; - -export const eighteight: Map = { - name: '8x8', - category: '8x8', - data: [ - '--------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '--------' - ] -}; - -export const eighteightH1: Map = { - name: '8x8 handicap 1', - category: '8x8', - data: [ - 'b-------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '--------' - ] -}; - -export const eighteightH2: Map = { - name: '8x8 handicap 2', - category: '8x8', - data: [ - 'b-------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '-------b' - ] -}; - -export const eighteightH3: Map = { - name: '8x8 handicap 3', - category: '8x8', - data: [ - 'b------b', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '-------b' - ] -}; - -export const eighteightH4: Map = { - name: '8x8 handicap 4', - category: '8x8', - data: [ - 'b------b', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - 'b------b' - ] -}; - -export const eighteightH12: Map = { - name: '8x8 handicap 12', - category: '8x8', - data: [ - 'bb----bb', - 'b------b', - '--------', - '---wb---', - '---bw---', - '--------', - 'b------b', - 'bb----bb' - ] -}; - -export const eighteightH16: Map = { - name: '8x8 handicap 16', - category: '8x8', - data: [ - 'bbb---bb', - 'b------b', - '-------b', - '---wb---', - '---bw---', - 'b-------', - 'b------b', - 'bb---bbb' - ] -}; - -export const eighteightH20: Map = { - name: '8x8 handicap 20', - category: '8x8', - data: [ - 'bbb--bbb', - 'b------b', - 'b------b', - '---wb---', - '---bw---', - 'b------b', - 'b------b', - 'bbb---bb' - ] -}; - -export const eighteightH28: Map = { - name: '8x8 handicap 28', - category: '8x8', - data: [ - 'bbbbbbbb', - 'b------b', - 'b------b', - 'b--wb--b', - 'b--bw--b', - 'b------b', - 'b------b', - 'bbbbbbbb' - ] -}; - -export const roundedEighteight: Map = { - name: '8x8 rounded', - category: '8x8', - author: 'syuilo', - data: [ - ' ------ ', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - ' ------ ' - ] -}; - -export const roundedEighteight2: Map = { - name: '8x8 rounded 2', - category: '8x8', - author: 'syuilo', - data: [ - ' ---- ', - ' ------ ', - '--------', - '---wb---', - '---bw---', - '--------', - ' ------ ', - ' ---- ' - ] -}; - -export const roundedEighteight3: Map = { - name: '8x8 rounded 3', - category: '8x8', - author: 'syuilo', - data: [ - ' -- ', - ' ---- ', - ' ------ ', - '---wb---', - '---bw---', - ' ------ ', - ' ---- ', - ' -- ' - ] -}; - -export const eighteightWithNotch: Map = { - name: '8x8 with notch', - category: '8x8', - author: 'syuilo', - data: [ - '--- ---', - '--------', - '--------', - ' --wb-- ', - ' --bw-- ', - '--------', - '--------', - '--- ---' - ] -}; - -export const eighteightWithSomeHoles: Map = { - name: '8x8 with some holes', - category: '8x8', - author: 'syuilo', - data: [ - '--- ----', - '----- --', - '-- -----', - '---wb---', - '---bw- -', - ' -------', - '--- ----', - '--------' - ] -}; - -export const circle: Map = { - name: 'Circle', - category: '8x8', - author: 'syuilo', - data: [ - ' -- ', - ' ------ ', - ' ------ ', - '---wb---', - '---bw---', - ' ------ ', - ' ------ ', - ' -- ' - ] -}; - -export const smile: Map = { - name: 'Smile', - category: '8x8', - author: 'syuilo', - data: [ - ' ------ ', - '--------', - '-- -- --', - '---wb---', - '-- bw --', - '--- ---', - '--------', - ' ------ ' - ] -}; - -export const window: Map = { - name: 'Window', - category: '8x8', - author: 'syuilo', - data: [ - '--------', - '- -- -', - '- -- -', - '---wb---', - '---bw---', - '- -- -', - '- -- -', - '--------' - ] -}; - -export const reserved: Map = { - name: 'Reserved', - category: '8x8', - author: 'Aya', - data: [ - 'w------b', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - 'b------w' - ] -}; - -export const x: Map = { - name: 'X', - category: '8x8', - author: 'Aya', - data: [ - 'w------b', - '-w----b-', - '--w--b--', - '---wb---', - '---bw---', - '--b--w--', - '-b----w-', - 'b------w' - ] -}; - -export const parallel: Map = { - name: 'Parallel', - category: '8x8', - author: 'Aya', - data: [ - '--------', - '--------', - '--------', - '---bb---', - '---ww---', - '--------', - '--------', - '--------' - ] -}; - -export const lackOfBlack: Map = { - name: 'Lack of Black', - category: '8x8', - data: [ - '--------', - '--------', - '--------', - '---w----', - '---bw---', - '--------', - '--------', - '--------' - ] -}; - -export const squareParty: Map = { - name: 'Square Party', - category: '8x8', - author: 'syuilo', - data: [ - '--------', - '-wwwbbb-', - '-w-wb-b-', - '-wwwbbb-', - '-bbbwww-', - '-b-bw-w-', - '-bbbwww-', - '--------' - ] -}; - -export const minesweeper: Map = { - name: 'Minesweeper', - category: '8x8', - author: 'syuilo', - data: [ - 'b-b--w-w', - '-w-wb-b-', - 'w-b--w-b', - '-b-wb-w-', - '-w-bw-b-', - 'b-w--b-w', - '-b-bw-w-', - 'w-w--b-b' - ] -}; - -export const tenthtenth: Map = { - name: '10x10', - category: '10x10', - data: [ - '----------', - '----------', - '----------', - '----------', - '----wb----', - '----bw----', - '----------', - '----------', - '----------', - '----------' - ] -}; - -export const hole: Map = { - name: 'The Hole', - category: '10x10', - author: 'syuilo', - data: [ - '----------', - '----------', - '--wb--wb--', - '--bw--bw--', - '---- ----', - '---- ----', - '--wb--wb--', - '--bw--bw--', - '----------', - '----------' - ] -}; - -export const grid: Map = { - name: 'Grid', - category: '10x10', - author: 'syuilo', - data: [ - '----------', - '- - -- - -', - '----------', - '- - -- - -', - '----wb----', - '----bw----', - '- - -- - -', - '----------', - '- - -- - -', - '----------' - ] -}; - -export const cross: Map = { - name: 'Cross', - category: '10x10', - author: 'Aya', - data: [ - ' ---- ', - ' ---- ', - ' ---- ', - '----------', - '----wb----', - '----bw----', - '----------', - ' ---- ', - ' ---- ', - ' ---- ' - ] -}; - -export const charX: Map = { - name: 'Char X', - category: '10x10', - author: 'syuilo', - data: [ - '--- ---', - '---- ----', - '----------', - ' -------- ', - ' --wb-- ', - ' --bw-- ', - ' -------- ', - '----------', - '---- ----', - '--- ---' - ] -}; - -export const charY: Map = { - name: 'Char Y', - category: '10x10', - author: 'syuilo', - data: [ - '--- ---', - '---- ----', - '----------', - ' -------- ', - ' --wb-- ', - ' --bw-- ', - ' ------ ', - ' ------ ', - ' ------ ', - ' ------ ' - ] -}; - -export const walls: Map = { - name: 'Walls', - category: '10x10', - author: 'Aya', - data: [ - ' bbbbbbbb ', - 'w--------w', - 'w--------w', - 'w--------w', - 'w---wb---w', - 'w---bw---w', - 'w--------w', - 'w--------w', - 'w--------w', - ' bbbbbbbb ' - ] -}; - -export const cpu: Map = { - name: 'CPU', - category: '10x10', - author: 'syuilo', - data: [ - ' b b b b ', - 'w--------w', - ' -------- ', - 'w--------w', - ' ---wb--- ', - ' ---bw--- ', - 'w--------w', - ' -------- ', - 'w--------w', - ' b b b b ' - ] -}; - -export const checker: Map = { - name: 'Checker', - category: '10x10', - author: 'Aya', - data: [ - '----------', - '----------', - '----------', - '---wbwb---', - '---bwbw---', - '---wbwb---', - '---bwbw---', - '----------', - '----------', - '----------' - ] -}; - -export const japaneseCurry: Map = { - name: 'Japanese curry', - category: '10x10', - author: 'syuilo', - data: [ - 'w-b-b-b-b-', - '-w-b-b-b-b', - 'w-w-b-b-b-', - '-w-w-b-b-b', - 'w-w-wwb-b-', - '-w-wbb-b-b', - 'w-w-w-b-b-', - '-w-w-w-b-b', - 'w-w-w-w-b-', - '-w-w-w-w-b' - ] -}; - -export const mosaic: Map = { - name: 'Mosaic', - category: '10x10', - author: 'syuilo', - data: [ - '- - - - - ', - ' - - - - -', - '- - - - - ', - ' - w w - -', - '- - b b - ', - ' - w w - -', - '- - b b - ', - ' - - - - -', - '- - - - - ', - ' - - - - -', - ] -}; - -export const arena: Map = { - name: 'Arena', - category: '10x10', - author: 'syuilo', - data: [ - '- - -- - -', - ' - - - - ', - '- ------ -', - ' -------- ', - '- --wb-- -', - '- --bw-- -', - ' -------- ', - '- ------ -', - ' - - - - ', - '- - -- - -' - ] -}; - -export const reactor: Map = { - name: 'Reactor', - category: '10x10', - author: 'syuilo', - data: [ - '-w------b-', - 'b- - - -w', - '- --wb-- -', - '---b w---', - '- b wb w -', - '- w bw b -', - '---w b---', - '- --bw-- -', - 'w- - - -b', - '-b------w-' - ] -}; - -export const sixeight: Map = { - name: '6x8', - category: 'Special', - data: [ - '------', - '------', - '------', - '--wb--', - '--bw--', - '------', - '------', - '------' - ] -}; - -export const spark: Map = { - name: 'Spark', - category: 'Special', - author: 'syuilo', - data: [ - ' - - ', - '----------', - ' -------- ', - ' -------- ', - ' ---wb--- ', - ' ---bw--- ', - ' -------- ', - ' -------- ', - '----------', - ' - - ' - ] -}; - -export const islands: Map = { - name: 'Islands', - category: 'Special', - author: 'syuilo', - data: [ - '-------- ', - '---wb--- ', - '---bw--- ', - '-------- ', - ' - - ', - ' - - ', - ' --------', - ' --------', - ' --------', - ' --------' - ] -}; - -export const galaxy: Map = { - name: 'Galaxy', - category: 'Special', - author: 'syuilo', - data: [ - ' ------ ', - ' --www--- ', - ' ------w--- ', - '---bbb--w---', - '--b---b-w-b-', - '-b--wwb-w-b-', - '-b-w-bww--b-', - '-b-w-b---b--', - '---w--bbb---', - ' ---w------ ', - ' ---www-- ', - ' ------ ' - ] -}; - -export const triangle: Map = { - name: 'Triangle', - category: 'Special', - author: 'syuilo', - data: [ - ' -- ', - ' -- ', - ' ---- ', - ' ---- ', - ' --wb-- ', - ' --bw-- ', - ' -------- ', - ' -------- ', - '----------', - '----------' - ] -}; - -export const iphonex: Map = { - name: 'iPhone X', - category: 'Special', - author: 'syuilo', - data: [ - ' -- -- ', - '--------', - '--------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '--------', - '--------', - ' ------ ' - ] -}; - -export const dealWithIt: Map = { - name: 'Deal with it!', - category: 'Special', - author: 'syuilo', - data: [ - '------------', - '--w-b-------', - ' --b-w------', - ' --w-b---- ', - ' ------- ' - ] -}; - -export const experiment: Map = { - name: 'Let\'s experiment', - category: 'Special', - author: 'syuilo', - data: [ - ' ------------ ', - '------wb------', - '------bw------', - '--------------', - ' - - ', - '------ ------', - 'bbbbbb wwwwww', - 'bbbbbb wwwwww', - 'bbbbbb wwwwww', - 'bbbbbb wwwwww', - 'wwwwww bbbbbb' - ] -}; - -export const bigBoard: Map = { - name: 'Big board', - category: 'Special', - data: [ - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '-------wb-------', - '-------bw-------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------' - ] -}; - -export const twoBoard: Map = { - name: 'Two board', - category: 'Special', - author: 'Aya', - data: [ - '-------- --------', - '-------- --------', - '-------- --------', - '---wb--- ---wb---', - '---bw--- ---bw---', - '-------- --------', - '-------- --------', - '-------- --------' - ] -}; - -export const test1: Map = { - name: 'Test1', - category: 'Test', - data: [ - '--------', - '---wb---', - '---bw---', - '--------' - ] -}; - -export const test2: Map = { - name: 'Test2', - category: 'Test', - data: [ - '------', - '------', - '-b--w-', - '-w--b-', - '-w--b-' - ] -}; - -export const test3: Map = { - name: 'Test3', - category: 'Test', - data: [ - '-w-', - '--w', - 'w--', - '-w-', - '--w', - 'w--', - '-w-', - '--w', - 'w--', - '-w-', - '---', - 'b--', - ] -}; - -export const test4: Map = { - name: 'Test4', - category: 'Test', - data: [ - '-w--b-', - '-w--b-', - '------', - '-w--b-', - '-w--b-' - ] -}; - -// https://misskey.xyz/othello/5aaabf7fe126e10b5216ea09 64 -export const test5: Map = { - name: 'Test5', - category: 'Test', - data: [ - '--wwwwww--', - '--wwwbwwww', - '-bwwbwbwww', - '-bwwwbwbww', - '-bwwbwbwbw', - '-bwbwbwb-w', - 'bwbwwbbb-w', - 'w-wbbbbb--', - '--w-b-w---', - '----------' - ] -}; diff --git a/src/server/common/user/get-acct.ts b/src/server/common/user/get-acct.ts deleted file mode 100644 index 9afb03d88b..0000000000 --- a/src/server/common/user/get-acct.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default user => { - return user.host === null ? user.username : `${user.username}@${user.host}`; -}; diff --git a/src/server/common/user/get-summary.ts b/src/server/common/user/get-summary.ts deleted file mode 100644 index b314a5cefb..0000000000 --- a/src/server/common/user/get-summary.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ILocalAccount, IUser } from '../../api/models/user'; -import getAcct from './get-acct'; - -/** - * ユーザーを表す文字列を取得します。 - * @param user ユーザー - */ -export default function(user: IUser): string { - let string = `${user.name} (@${getAcct(user)})\n` + - `${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`; - - if (user.host === null) { - const account = user.account as ILocalAccount; - string += `場所: ${account.profile.location}、誕生日: ${account.profile.birthday}\n`; - } - - return string + `「${user.description}」`; -} diff --git a/src/server/common/user/parse-acct.ts b/src/server/common/user/parse-acct.ts deleted file mode 100644 index ef1f55405d..0000000000 --- a/src/server/common/user/parse-acct.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default acct => { - const splitted = acct.split('@', 2); - return { username: splitted[0], host: splitted[1] || null }; -}; diff --git a/src/server/file/server.ts b/src/server/file/server.ts index 3bda5b14fe..062d260cb4 100644 --- a/src/server/file/server.ts +++ b/src/server/file/server.ts @@ -10,7 +10,7 @@ import * as mongodb from 'mongodb'; import * as _gm from 'gm'; import * as stream from 'stream'; -import DriveFile, { getGridFSBucket } from '../api/models/drive-file'; +import DriveFile, { getGridFSBucket } from '../../models/drive-file'; const gm = _gm.subClass({ imageMagick: true diff --git a/src/server/web/app/animation.styl b/src/server/web/app/animation.styl deleted file mode 100644 index 8f121b313b..0000000000 --- a/src/server/web/app/animation.styl +++ /dev/null @@ -1,12 +0,0 @@ -.zoom-in-top-enter-active, -.zoom-in-top-leave-active { - opacity: 1; - transform: scaleY(1); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); - transform-origin: center top; -} -.zoom-in-top-enter, -.zoom-in-top-leave-active { - opacity: 0; - transform: scaleY(0); -} diff --git a/src/server/web/app/app.styl b/src/server/web/app/app.styl deleted file mode 100644 index 431b9daa65..0000000000 --- a/src/server/web/app/app.styl +++ /dev/null @@ -1,128 +0,0 @@ -@import "../style" -@import "../animation" - -html - &.progress - &, * - cursor progress !important - -body - overflow-wrap break-word - -#error - padding 32px - color #fff - - hr - border solid 1px #fff - -#nprogress - pointer-events none - - position absolute - z-index 65536 - - .bar - background $theme-color - - position fixed - z-index 65537 - top 0 - left 0 - - width 100% - height 2px - - /* Fancy blur effect */ - .peg - display block - position absolute - right 0px - width 100px - height 100% - box-shadow 0 0 10px $theme-color, 0 0 5px $theme-color - opacity 1 - - transform rotate(3deg) translate(0px, -4px) - -#wait - display block - position fixed - z-index 65537 - top 15px - right 15px - - &:before - content "" - display block - width 18px - height 18px - box-sizing border-box - - border solid 2px transparent - border-top-color $theme-color - border-left-color $theme-color - border-radius 50% - - animation progress-spinner 400ms linear infinite - - @keyframes progress-spinner - 0% - transform rotate(0deg) - 100% - transform rotate(360deg) - -code - font-family Consolas, 'Courier New', Courier, Monaco, monospace - - .comment - opacity 0.5 - - .string - color #e96900 - - .regexp - color #e9003f - - .keyword - color #2973b7 - - &.true - &.false - &.null - &.nil - &.undefined - color #ae81ff - - .symbol - color #42b983 - - .number - .nan - color #ae81ff - - .var:not(.keyword) - font-weight bold - font-style italic - //text-decoration underline - - .method - font-style italic - color #8964c1 - - .property - color #a71d5d - - .label - color #e9003f - -pre - display block - - > code - display block - overflow auto - tab-size 2 - -[data-fa] - display inline-block diff --git a/src/server/web/app/app.vue b/src/server/web/app/app.vue deleted file mode 100644 index 7a46e7dea0..0000000000 --- a/src/server/web/app/app.vue +++ /dev/null @@ -1,3 +0,0 @@ -<template> -<router-view id="app"></router-view> -</template> diff --git a/src/server/web/app/auth/assets/logo.svg b/src/server/web/app/auth/assets/logo.svg deleted file mode 100644 index 19b8a2737e..0000000000 --- a/src/server/web/app/auth/assets/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="1024px" height="512px" viewBox="0 256 1024 512" enable-background="new 0 256 1024 512" xml:space="preserve">
-<polyline opacity="0.5" fill="none" stroke="#000000" stroke-width="34" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
- 896.5,608.5 800.5,416.5 704.5,608.5 608.5,416.5 512.5,608.5 416.5,416.5 320.5,608.5 224.5,416.5 128.5,608.5 "/>
-</svg>
diff --git a/src/server/web/app/auth/script.ts b/src/server/web/app/auth/script.ts deleted file mode 100644 index 31c758ebc2..0000000000 --- a/src/server/web/app/auth/script.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Authorize Form - */ - -// Style -import './style.styl'; - -import init from '../init'; - -import Index from './views/index.vue'; - -/** - * init - */ -init(async (launch) => { - document.title = 'Misskey | アプリの連携'; - - // Launch the app - const [app] = launch(); - - // Routing - app.$router.addRoutes([ - { path: '/:token', component: Index }, - ]); -}); diff --git a/src/server/web/app/auth/style.styl b/src/server/web/app/auth/style.styl deleted file mode 100644 index bd25e1b572..0000000000 --- a/src/server/web/app/auth/style.styl +++ /dev/null @@ -1,15 +0,0 @@ -@import "../app" -@import "../reset" - -html - background #eee - - @media (max-width 600px) - background #fff - -body - margin 0 - padding 32px 0 - - @media (max-width 600px) - padding 0 diff --git a/src/server/web/app/auth/views/form.vue b/src/server/web/app/auth/views/form.vue deleted file mode 100644 index 9d9e8cdb1b..0000000000 --- a/src/server/web/app/auth/views/form.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<div class="form"> - <header> - <h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1> - <img :src="`${app.icon_url}?thumbnail&size=64`"/> - </header> - <div class="app"> - <section> - <h2>{{ app.name }}</h2> - <p class="nid">{{ app.nameId }}</p> - <p class="description">{{ app.description }}</p> - </section> - <section> - <h2>このアプリは次の権限を要求しています:</h2> - <ul> - <template v-for="p in app.permission"> - <li v-if="p == 'account-read'">アカウントの情報を見る。</li> - <li v-if="p == 'account-write'">アカウントの情報を操作する。</li> - <li v-if="p == 'post-write'">投稿する。</li> - <li v-if="p == 'like-write'">いいねしたりいいね解除する。</li> - <li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li> - <li v-if="p == 'drive-read'">ドライブを見る。</li> - <li v-if="p == 'drive-write'">ドライブを操作する。</li> - <li v-if="p == 'notification-read'">通知を見る。</li> - <li v-if="p == 'notification-write'">通知を操作する。</li> - </template> - </ul> - </section> - </div> - <div class="action"> - <button @click="cancel">キャンセル</button> - <button @click="accept">アクセスを許可</button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['session'], - computed: { - app(): any { - return this.session.app; - } - }, - methods: { - cancel() { - (this as any).api('auth/deny', { - token: this.session.token - }).then(() => { - this.$emit('denied'); - }); - }, - - accept() { - (this as any).api('auth/accept', { - token: this.session.token - }).then(() => { - this.$emit('accepted'); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.form - - > header - > h1 - margin 0 - padding 32px 32px 20px 32px - font-size 24px - font-weight normal - color #777 - - i - color #77aeca - - &:before - content '「' - - &:after - content '」' - - b - color #666 - - > img - display block - z-index 1 - width 84px - height 84px - margin 0 auto -38px auto - border solid 5px #fff - border-radius 100% - box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) - - > .app - padding 44px 16px 0 16px - color #555 - background #eee - box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset - - &:after - content '' - display block - clear both - - > section - float left - width 50% - padding 8px - text-align left - - > h2 - margin 0 - font-size 16px - color #777 - - > .action - padding 16px - - > button - margin 0 8px - padding 0 - - @media (max-width 600px) - > header - > img - box-shadow none - - > .app - box-shadow none - - @media (max-width 500px) - > header - > h1 - font-size 16px - -</style> diff --git a/src/server/web/app/auth/views/index.vue b/src/server/web/app/auth/views/index.vue deleted file mode 100644 index e1e1b265e1..0000000000 --- a/src/server/web/app/auth/views/index.vue +++ /dev/null @@ -1,149 +0,0 @@ -<template> -<div class="index"> - <main v-if="os.isSignedIn"> - <p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p> - <x-form - ref="form" - v-if="state == 'waiting'" - :session="session" - @denied="state = 'denied'" - @accepted="accepted" - /> - <div class="denied" v-if="state == 'denied'"> - <h1>アプリケーションの連携をキャンセルしました。</h1> - <p>このアプリがあなたのアカウントにアクセスすることはありません。</p> - </div> - <div class="accepted" v-if="state == 'accepted'"> - <h1>{{ session.app.isAuthorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました' }}</h1> - <p v-if="session.app.callbackUrl">アプリケーションに戻っています<mk-ellipsis/></p> - <p v-if="!session.app.callbackUrl">アプリケーションに戻って、やっていってください。</p> - </div> - <div class="error" v-if="state == 'fetch-session-error'"> - <p>セッションが存在しません。</p> - </div> - </main> - <main class="signin" v-if="!os.isSignedIn"> - <h1>サインインしてください</h1> - <mk-signin/> - </main> - <footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XForm from './form.vue'; - -export default Vue.extend({ - components: { - XForm - }, - data() { - return { - state: null, - session: null, - fetching: true - }; - }, - computed: { - token(): string { - return this.$route.params.token; - } - }, - mounted() { - if (!this.$root.$data.os.isSignedIn) return; - - // Fetch session - (this as any).api('auth/session/show', { - token: this.token - }).then(session => { - this.session = session; - this.fetching = false; - - // 既に連携していた場合 - if (this.session.app.isAuthorized) { - this.$root.$data.os.api('auth/accept', { - token: this.session.token - }).then(() => { - this.accepted(); - }); - } else { - this.state = 'waiting'; - } - }).catch(error => { - this.state = 'fetch-session-error'; - }); - }, - methods: { - accepted() { - this.state = 'accepted'; - if (this.session.app.callbackUrl) { - location.href = this.session.app.callbackUrl + '?token=' + this.session.token; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.index - - > main - width 100% - max-width 500px - margin 0 auto - text-align center - background #fff - box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2) - - > .fetching - margin 0 - padding 32px - color #555 - - > div - padding 64px - - > h1 - margin 0 0 8px 0 - padding 0 - font-size 20px - font-weight normal - - > p - margin 0 - color #555 - - &.denied > h1 - color #e65050 - - &.accepted > h1 - color #54af7c - - &.signin - padding 32px 32px 16px 32px - - > h1 - margin 0 0 22px 0 - padding 0 - font-size 20px - font-weight normal - color #555 - - @media (max-width 600px) - max-width none - box-shadow none - - @media (max-width 500px) - > div - > h1 - font-size 16px - - > footer - > img - display block - width 64px - height 64px - margin 0 auto - -</style> diff --git a/src/server/web/app/base.pug b/src/server/web/app/base.pug deleted file mode 100644 index 60eb1539ec..0000000000 --- a/src/server/web/app/base.pug +++ /dev/null @@ -1,38 +0,0 @@ -doctype html - -!= '\n<!-- Thank you for using Misskey! @syuilo -->\n' - -html - - head - meta(charset='utf-8') - meta(name='application-name' content='Misskey') - meta(name='theme-color' content=themeColor) - meta(name='referrer' content='origin') - link(rel='manifest' href='/manifest.json') - - title Misskey - - style - include ./../../../../built/server/web/assets/init.css - script - include ./../../../../built/server/web/assets/boot.js - - script - include ./../../../../built/server/web/assets/safe.js - - //- FontAwesome style - style #{facss} - - //- highlight.js style - style #{hljscss} - - body - noscript: p - | JavaScriptを有効にしてください - br - | Please turn on your JavaScript - div#ini: p - span . - span . - span . diff --git a/src/server/web/app/boot.js b/src/server/web/app/boot.js deleted file mode 100644 index 0846e4bd55..0000000000 --- a/src/server/web/app/boot.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * MISSKEY BOOT LOADER - * (ENTRY POINT) - */ - -/** - * ドメインに基づいて適切なスクリプトを読み込みます。 - * ユーザーの言語およびモバイル端末か否かも考慮します。 - * webpackは介さないためrequireやimportは使えません。 - */ - -'use strict'; - -// Chromeで確認したことなのですが、constやletを用いたとしても -// グローバルなスコープで定数/変数を定義するとwindowのプロパティ -// としてそれがアクセスできるようになる訳ではありませんが、普通に -// コンソールから定数/変数名を入力するとアクセスできてしまいます。 -// ブロック内に入れてスコープを非グローバル化するとそれが防げます -// (Chrome以外のブラウザでは検証していません) -{ - // Get the current url information - const url = new URL(location.href); - - //#region Detect app name - let app = null; - - if (url.pathname == '/docs') app = 'docs'; - if (url.pathname == '/dev') app = 'dev'; - if (url.pathname == '/auth') app = 'auth'; - //#endregion - - // Detect the user language - // Note: The default language is English - let lang = navigator.language.split('-')[0]; - if (!/^(en|ja)$/.test(lang)) lang = 'en'; - if (localStorage.getItem('lang')) lang = localStorage.getItem('lang'); - if (ENV != 'production') lang = 'ja'; - - // Detect the user agent - const ua = navigator.userAgent.toLowerCase(); - const isMobile = /mobile|iphone|ipad|android/.test(ua); - - // Get the <head> element - const head = document.getElementsByTagName('head')[0]; - - // If mobile, insert the viewport meta tag - if (isMobile) { - const meta = document.createElement('meta'); - meta.setAttribute('name', 'viewport'); - meta.setAttribute('content', - 'width=device-width,' + - 'initial-scale=1,' + - 'minimum-scale=1,' + - 'maximum-scale=1,' + - 'user-scalable=no'); - head.appendChild(meta); - } - - // Switch desktop or mobile version - if (app == null) { - app = isMobile ? 'mobile' : 'desktop'; - } - - // Script version - const ver = localStorage.getItem('v') || VERSION; - - // Whether in debug mode - const isDebug = localStorage.getItem('debug') == 'true'; - - // Whether use raw version script - const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug) - || ENV != 'production'; - - // Load an app script - // Note: 'async' make it possible to load the script asyncly. - // 'defer' make it possible to run the script when the dom loaded. - const script = document.createElement('script'); - script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js`); - script.setAttribute('async', 'true'); - script.setAttribute('defer', 'true'); - head.appendChild(script); - - // 1秒経ってもスクリプトがロードされない場合はバージョンが古くて - // 404になっているせいかもしれないので、バージョンを確認して古ければ更新する - // - // 読み込まれたスクリプトからこのタイマーを解除できるように、 - // グローバルにタイマーIDを代入しておく - window.mkBootTimer = window.setTimeout(async () => { - // Fetch meta - const res = await fetch(API + '/meta', { - method: 'POST', - cache: 'no-cache' - }); - - // Parse - const meta = await res.json(); - - // Compare versions - if (meta.version != ver) { - alert( - 'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' + - '\n\n' + - 'New version of Misskey available. The page will be reloaded.'); - - // Clear cache (serive worker) - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => registration.unregister()); - }); - } catch (e) { - console.error(e); - } - - // Force reload - location.reload(true); - } - }, 1000); -} diff --git a/src/server/web/app/ch/script.ts b/src/server/web/app/ch/script.ts deleted file mode 100644 index 4c6b6dfd1b..0000000000 --- a/src/server/web/app/ch/script.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Channels - */ - -// Style -import './style.styl'; - -require('./tags'); -import init from '../init'; - -/** - * init - */ -init(() => { -}); diff --git a/src/server/web/app/ch/style.styl b/src/server/web/app/ch/style.styl deleted file mode 100644 index 21ca648cbe..0000000000 --- a/src/server/web/app/ch/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" - -html - padding 8px - background #efefef - -#wait - top auto - bottom 15px - left 15px diff --git a/src/server/web/app/ch/tags/channel.tag b/src/server/web/app/ch/tags/channel.tag deleted file mode 100644 index 2abfb106a5..0000000000 --- a/src/server/web/app/ch/tags/channel.tag +++ /dev/null @@ -1,403 +0,0 @@ -<mk-channel> - <mk-header/> - <hr> - <main v-if="!fetching"> - <h1>{ channel.title }</h1> - - <div v-if="$root.$data.os.isSignedIn"> - <p v-if="channel.isWatching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p> - <p v-if="!channel.isWatching"><a @click="watch">このチャンネルをウォッチする</a></p> - </div> - - <div class="share"> - <mk-twitter-button/> - <mk-line-button/> - </div> - - <div class="body"> - <p v-if="postsFetching">読み込み中<mk-ellipsis/></p> - <div v-if="!postsFetching"> - <p v-if="posts == null || posts.length == 0">まだ投稿がありません</p> - <template v-if="posts != null"> - <mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/> - </template> - </div> - </div> - <hr> - <mk-channel-form v-if="$root.$data.os.isSignedIn" channel={ channel } ref="form"/> - <div v-if="!$root.$data.os.isSignedIn"> - <p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p> - </div> - <hr> - <footer> - <small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small> - </footer> - </main> - <style lang="stylus" scoped> - :scope - display block - - > main - > h1 - font-size 1.5em - color #f00 - - > .share - > * - margin-right 4px - - > .body - margin 8px 0 0 0 - - > mk-channel-form - max-width 500px - - </style> - <script lang="typescript"> - import Progress from '../../common/scripts/loading'; - import ChannelStream from '../../common/scripts/streaming/channel-stream'; - - this.mixin('i'); - this.mixin('api'); - - this.id = this.opts.id; - this.fetching = true; - this.postsFetching = true; - this.channel = null; - this.posts = null; - this.connection = new ChannelStream(this.id); - this.unreadCount = 0; - - this.on('mount', () => { - document.documentElement.style.background = '#efefef'; - - Progress.start(); - - let fetched = false; - - // チャンネル概要読み込み - this.$root.$data.os.api('channels/show', { - channelId: this.id - }).then(channel => { - if (fetched) { - Progress.done(); - } else { - Progress.set(0.5); - fetched = true; - } - - this.update({ - fetching: false, - channel: channel - }); - - document.title = channel.title + ' | Misskey' - }); - - // 投稿読み込み - this.$root.$data.os.api('channels/posts', { - channelId: this.id - }).then(posts => { - if (fetched) { - Progress.done(); - } else { - Progress.set(0.5); - fetched = true; - } - - this.update({ - postsFetching: false, - posts: posts - }); - }); - - this.connection.on('post', this.onPost); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - }); - - this.on('unmount', () => { - this.connection.off('post', this.onPost); - this.connection.close(); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }); - - this.onPost = post => { - this.posts.unshift(post); - this.update(); - - if (document.hidden && this.$root.$data.os.isSignedIn && post.userId !== this.$root.$data.os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`; - } - }; - - this.onVisibilitychange = () => { - if (!document.hidden) { - this.unreadCount = 0; - document.title = this.channel.title + ' | Misskey' - } - }; - - this.watch = () => { - this.$root.$data.os.api('channels/watch', { - channelId: this.id - }).then(() => { - this.channel.isWatching = true; - this.update(); - }, e => { - alert('error'); - }); - }; - - this.unwatch = () => { - this.$root.$data.os.api('channels/unwatch', { - channelId: this.id - }).then(() => { - this.channel.isWatching = false; - this.update(); - }, e => { - alert('error'); - }); - }; - </script> -</mk-channel> - -<mk-channel-post> - <header> - <a class="index" @click="reply">{ post.index }:</a> - <a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a> - <mk-time time={ post.createdAt }/> - <mk-time time={ post.createdAt } mode="detail"/> - <span>ID:<i>{ acct }</i></span> - </header> - <div> - <a v-if="post.reply">>>{ post.reply.index }</a> - { post.text } - <div class="media" v-if="post.media"> - <template each={ file in post.media }> - <a href={ file.url } target="_blank"> - <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/> - </a> - </template> - </div> - </div> - <style lang="stylus" scoped> - :scope - display block - margin 0 - padding 0 - - > header - position -webkit-sticky - position sticky - z-index 1 - top 0 - background rgba(239, 239, 239, 0.9) - - > .index - margin-right 0.25em - color #000 - - > .name - margin-right 0.5em - color #008000 - - > mk-time - margin-right 0.5em - - &:first-of-type - display none - - @media (max-width 600px) - > mk-time - &:first-of-type - display initial - - &:last-of-type - display none - - > div - padding 0 0 1em 2em - - > .media - > a - display inline-block - - > img - max-width 100% - vertical-align bottom - - </style> - <script lang="typescript"> - import getAcct from '../../../../common/user/get-acct'; - - this.post = this.opts.post; - this.form = this.opts.form; - this.acct = getAcct(this.post.user); - - this.reply = () => { - this.form.update({ - reply: this.post - }); - }; - </script> -</mk-channel-post> - -<mk-channel-form> - <p v-if="reply"><b>>>{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p> - <textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea> - <div class="actions"> - <button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button> - <button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button> - <button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post"> - <template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis v-if="wait"/> - </button> - </div> - <mk-uploader ref="uploader"/> - <ol v-if="files"> - <li each={ files }>{ name }</li> - </ol> - <input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/> - <style lang="stylus" scoped> - :scope - display block - - > textarea - width 100% - max-width 100% - min-width 100% - min-height 5em - - > .actions - display flex - - > button - > [data-fa] - margin-right 0.25em - - &:last-child - margin-left auto - - &.wait - cursor wait - - > input[type='file'] - display none - - </style> - <script lang="typescript"> - this.mixin('api'); - - this.channel = this.opts.channel; - this.files = null; - - this.on('mount', () => { - this.$refs.uploader.on('uploaded', file => { - this.update({ - files: [file] - }); - }); - }); - - this.upload = file => { - this.$refs.uploader.upload(file); - }; - - this.clearReply = () => { - this.update({ - reply: null - }); - }; - - this.clear = () => { - this.clearReply(); - this.update({ - files: null - }); - this.$refs.text.value = ''; - }; - - this.post = () => { - this.update({ - wait: true - }); - - const files = this.files && this.files.length > 0 - ? this.files.map(f => f.id) - : undefined; - - this.$root.$data.os.api('posts/create', { - text: this.$refs.text.value == '' ? undefined : this.$refs.text.value, - mediaIds: files, - replyId: this.reply ? this.reply.id : undefined, - channelId: this.channel.id - }).then(data => { - this.clear(); - }).catch(err => { - alert('失敗した'); - }).then(() => { - this.update({ - wait: false - }); - }); - }; - - this.changeFile = () => { - Array.from(this.$refs.file.files).forEach(this.upload); - }; - - this.selectFile = () => { - this.$refs.file.click(); - }; - - this.drive = () => { - window['cb'] = files => { - this.update({ - files: files - }); - }; - - window.open(_URL_ + '/selectdrive?multiple=true', - 'drive_window', - 'height=500,width=800'); - }; - - this.onkeydown = e => { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); - }; - - this.onpaste = e => { - Array.from(e.clipboardData.items).forEach(item => { - if (item.kind == 'file') { - this.upload(item.getAsFile()); - } - }); - }; - </script> -</mk-channel-form> - -<mk-twitter-button> - <a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a> - <script lang="typescript"> - this.on('mount', () => { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); - script.setAttribute('async', 'async'); - head.appendChild(script); - }); - </script> -</mk-twitter-button> - -<mk-line-button> - <div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div> - <script lang="typescript"> - this.on('mount', () => { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js'); - script.setAttribute('async', 'async'); - head.appendChild(script); - }); - </script> -</mk-line-button> diff --git a/src/server/web/app/ch/tags/header.tag b/src/server/web/app/ch/tags/header.tag deleted file mode 100644 index 901123d63b..0000000000 --- a/src/server/web/app/ch/tags/header.tag +++ /dev/null @@ -1,20 +0,0 @@ -<mk-header> - <div> - <a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a> - </div> - <div> - <a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a> - <a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/@' + I.username }>{ I.username }</a> - </div> - <style lang="stylus" scoped> - :scope - display flex - - > div:last-child - margin-left auto - - </style> - <script lang="typescript"> - this.mixin('i'); - </script> -</mk-header> diff --git a/src/server/web/app/ch/tags/index.tag b/src/server/web/app/ch/tags/index.tag deleted file mode 100644 index 88df2ec45d..0000000000 --- a/src/server/web/app/ch/tags/index.tag +++ /dev/null @@ -1,37 +0,0 @@ -<mk-index> - <mk-header/> - <hr> - <button @click="n">%i18n:ch.tags.mk-index.new%</button> - <hr> - <ul v-if="channels"> - <li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li> - </ul> - <style lang="stylus" scoped> - :scope - display block - - </style> - <script lang="typescript"> - this.mixin('api'); - - this.on('mount', () => { - this.$root.$data.os.api('channels', { - limit: 100 - }).then(channels => { - this.update({ - channels: channels - }); - }); - }); - - this.n = () => { - const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%'); - - this.$root.$data.os.api('channels/create', { - title: title - }).then(channel => { - location.href = '/' + channel.id; - }); - }; - </script> -</mk-index> diff --git a/src/server/web/app/ch/tags/index.ts b/src/server/web/app/ch/tags/index.ts deleted file mode 100644 index 12ffdaeb84..0000000000 --- a/src/server/web/app/ch/tags/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -require('./index.tag'); -require('./channel.tag'); -require('./header.tag'); diff --git a/src/server/web/app/common/define-widget.ts b/src/server/web/app/common/define-widget.ts deleted file mode 100644 index 27db59b5ee..0000000000 --- a/src/server/web/app/common/define-widget.ts +++ /dev/null @@ -1,79 +0,0 @@ -import Vue from 'vue'; - -export default function<T extends object>(data: { - name: string; - props?: () => T; -}) { - return Vue.extend({ - props: { - widget: { - type: Object - }, - isMobile: { - type: Boolean, - default: false - }, - isCustomizeMode: { - type: Boolean, - default: false - } - }, - computed: { - id(): string { - return this.widget.id; - } - }, - data() { - return { - props: data.props ? data.props() : {} as T, - bakedOldProps: null, - preventSave: false - }; - }, - created() { - if (this.props) { - Object.keys(this.props).forEach(prop => { - if (this.widget.data.hasOwnProperty(prop)) { - this.props[prop] = this.widget.data[prop]; - } - }); - } - - this.bakeProps(); - - this.$watch('props', newProps => { - if (this.preventSave) { - this.preventSave = false; - this.bakeProps(); - return; - } - if (this.bakedOldProps == JSON.stringify(newProps)) return; - - this.bakeProps(); - - if (this.isMobile) { - (this as any).api('i/update_mobile_home', { - id: this.id, - data: newProps - }).then(() => { - (this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == this.id).data = newProps; - }); - } else { - (this as any).api('i/update_home', { - id: this.id, - data: newProps - }).then(() => { - (this as any).os.i.account.clientSettings.home.find(w => w.id == this.id).data = newProps; - }); - } - }, { - deep: true - }); - }, - methods: { - bakeProps() { - this.bakedOldProps = JSON.stringify(this.props); - } - } - }); -} diff --git a/src/server/web/app/common/mios.ts b/src/server/web/app/common/mios.ts deleted file mode 100644 index bcb8b60678..0000000000 --- a/src/server/web/app/common/mios.ts +++ /dev/null @@ -1,578 +0,0 @@ -import Vue from 'vue'; -import { EventEmitter } from 'eventemitter3'; -import * as merge from 'object-assign-deep'; -import * as uuid from 'uuid'; - -import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config'; -import Progress from './scripts/loading'; -import Connection from './scripts/streaming/stream'; -import { HomeStreamManager } from './scripts/streaming/home'; -import { DriveStreamManager } from './scripts/streaming/drive'; -import { ServerStreamManager } from './scripts/streaming/server'; -import { RequestsStreamManager } from './scripts/streaming/requests'; -import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index'; -import { OthelloStreamManager } from './scripts/streaming/othello'; - -import Err from '../common/views/components/connect-failed.vue'; - -//#region api requests -let spinner = null; -let pending = 0; -//#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: (opts?: { - reply?: any; - repost?: any; - }) => void; - - notify: (message: string) => void; -}; - -/** - * Misskey Operating System - */ -export default class MiOS extends EventEmitter { - /** - * Misskeyの /meta で取得できるメタ情報 - */ - private meta: { - data: { [x: string]: any }; - chachedAt: Date; - }; - - private isMetaFetching = false; - - public app: Vue; - - public new(vm, props) { - const w = new vm({ - parent: this.app, - propsData: props - }).$mount(); - document.body.appendChild(w.$el); - } - - /** - * A signing user - */ - public i: { [x: string]: any }; - - /** - * Whether signed in - */ - public get isSignedIn() { - return this.i != null; - } - - /** - * Whether is debug mode - */ - public get debug() { - return localStorage.getItem('debug') == 'true'; - } - - /** - * Whether enable sounds - */ - public get isEnableSounds() { - return localStorage.getItem('enableSounds') == 'true'; - } - - public apis: API; - - /** - * A connection manager of home stream - */ - public stream: HomeStreamManager; - - /** - * Connection managers - */ - public streams: { - driveStream: DriveStreamManager; - serverStream: ServerStreamManager; - requestsStream: RequestsStreamManager; - messagingIndexStream: MessagingIndexStreamManager; - othelloStream: OthelloStreamManager; - } = { - driveStream: null, - serverStream: null, - requestsStream: null, - messagingIndexStream: null, - othelloStream: null - }; - - /** - * A registration of service worker - */ - private swRegistration: ServiceWorkerRegistration = null; - - /** - * Whether should register ServiceWorker - */ - private shouldRegisterSw: boolean; - - /** - * ウィンドウシステム - */ - public windows = new WindowSystem(); - - /** - * MiOSインスタンスを作成します - * @param shouldRegisterSw ServiceWorkerを登録するかどうか - */ - constructor(shouldRegisterSw = false) { - super(); - - this.shouldRegisterSw = shouldRegisterSw; - - //#region BIND - this.log = this.log.bind(this); - this.logInfo = this.logInfo.bind(this); - this.logWarn = this.logWarn.bind(this); - this.logError = this.logError.bind(this); - this.init = this.init.bind(this); - this.api = this.api.bind(this); - this.getMeta = this.getMeta.bind(this); - this.registerSw = this.registerSw.bind(this); - //#endregion - - if (this.debug) { - (window as any).os = this; - } - } - - private googleMapsIniting = false; - - public getGoogleMaps() { - return new Promise((res, rej) => { - if ((window as any).google && (window as any).google.maps) { - res((window as any).google.maps); - } else { - this.once('init-google-maps', () => { - res((window as any).google.maps); - }); - - //#region load google maps api - if (!this.googleMapsIniting) { - this.googleMapsIniting = true; - (window as any).initGoogleMaps = () => { - this.emit('init-google-maps'); - }; - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`); - script.setAttribute('async', 'true'); - script.setAttribute('defer', 'true'); - head.appendChild(script); - } - //#endregion - } - }); - } - - public log(...args) { - if (!this.debug) return; - console.log.apply(null, args); - } - - public logInfo(...args) { - if (!this.debug) return; - console.info.apply(null, args); - } - - public logWarn(...args) { - if (!this.debug) return; - console.warn.apply(null, args); - } - - public logError(...args) { - if (!this.debug) return; - console.error.apply(null, args); - } - - public signout() { - localStorage.removeItem('me'); - document.cookie = `i=; domain=.${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; - location.href = '/'; - } - - /** - * Initialize MiOS (boot) - * @param callback A function that call when initialized - */ - public async init(callback) { - //#region Init stream managers - this.streams.serverStream = new ServerStreamManager(this); - this.streams.requestsStream = new RequestsStreamManager(this); - - this.once('signedin', () => { - // Init home stream manager - this.stream = new HomeStreamManager(this, this.i); - - // Init other stream manager - this.streams.driveStream = new DriveStreamManager(this, this.i); - this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i); - this.streams.othelloStream = new OthelloStreamManager(this, this.i); - }); - //#endregion - - // ユーザーをフェッチしてコールバックする - const fetchme = (token, cb) => { - let me = null; - - // Return when not signed in - if (token == null) { - return done(); - } - - // Fetch user - fetch(`${apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token - }) - }) - // When success - .then(res => { - // When failed to authenticate user - if (res.status !== 200) { - return this.signout(); - } - - // Parse response - res.json().then(i => { - me = i; - me.account.token = token; - done(); - }); - }) - // When failure - .catch(() => { - // Render the error screen - document.body.innerHTML = '<div id="err"></div>'; - new Vue({ - render: createEl => createEl(Err) - }).$mount('#err'); - - Progress.done(); - }); - - function done() { - if (cb) cb(me); - } - }; - - // フェッチが完了したとき - const fetched = me => { - if (me) { - // デフォルトの設定をマージ - me.account.clientSettings = Object.assign({ - fetchOnScroll: true, - showMaps: true, - showPostFormOnTopOfTl: false, - gradientWindowHeader: false - }, me.account.clientSettings); - - // ローカルストレージにキャッシュ - localStorage.setItem('me', JSON.stringify(me)); - } - - this.i = me; - - this.emit('signedin'); - - // Finish init - callback(); - - //#region Post - - // Init service worker - if (this.shouldRegisterSw) this.registerSw(); - - //#endregion - }; - - // Get cached account data - const cachedMe = JSON.parse(localStorage.getItem('me')); - - // キャッシュがあったとき - if (cachedMe) { - // とりあえずキャッシュされたデータでお茶を濁して(?)おいて、 - fetched(cachedMe); - - // 後から新鮮なデータをフェッチ - fetchme(cachedMe.account.token, freshData => { - merge(cachedMe, freshData); - }); - } else { - // Get token from cookie - const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; - - fetchme(i, fetched); - } - } - - /** - * Register service worker - */ - private registerSw() { - // Check whether service worker and push manager supported - const isSwSupported = - ('serviceWorker' in navigator) && ('PushManager' in window); - - // Reject when browser not service worker supported - if (!isSwSupported) return; - - // Reject when not signed in to Misskey - if (!this.isSignedIn) return; - - // When service worker activated - navigator.serviceWorker.ready.then(registration => { - this.log('[sw] ready: ', registration); - - this.swRegistration = registration; - - // Options of pushManager.subscribe - // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters - const opts = { - // A boolean indicating that the returned push subscription - // will only be used for messages whose effect is made visible to the user. - userVisibleOnly: true, - - // A public key your push server will use to send - // messages to client apps via a push server. - applicationServerKey: urlBase64ToUint8Array(swPublickey) - }; - - // Subscribe push notification - this.swRegistration.pushManager.subscribe(opts).then(subscription => { - this.log('[sw] Subscribe OK:', subscription); - - function encode(buffer: ArrayBuffer) { - return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); - } - - // Register - this.api('sw/register', { - endpoint: subscription.endpoint, - auth: encode(subscription.getKey('auth')), - publickey: encode(subscription.getKey('p256dh')) - }); - }) - // When subscribe failed - .catch(async (err: Error) => { - this.logError('[sw] Subscribe Error:', err); - - // 通知が許可されていなかったとき - if (err.name == 'NotAllowedError') { - this.logError('[sw] Subscribe failed due to notification not allowed'); - return; - } - - // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが - // 既に存在していることが原因でエラーになった可能性があるので、 - // そのサブスクリプションを解除しておく - const subscription = await this.swRegistration.pushManager.getSubscription(); - if (subscription) subscription.unsubscribe(); - }); - }); - - // Whether use raw version script - const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug) - || process.env.NODE_ENV != 'production'; - - // The path of service worker script - const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`; - - // Register service worker - navigator.serviceWorker.register(sw).then(registration => { - // 登録成功 - this.logInfo('[sw] Registration successful with scope: ', registration.scope); - }).catch(err => { - // 登録失敗 :( - this.logError('[sw] Registration failed: ', err); - }); - } - - public requests = []; - - /** - * Misskey APIにリクエストします - * @param endpoint エンドポイント名 - * @param data パラメータ - */ - public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> { - if (++pending === 1) { - spinner = document.createElement('div'); - spinner.setAttribute('id', 'wait'); - document.body.appendChild(spinner); - } - - // Append a credential - if (this.isSignedIn) (data as any).i = this.i.account.token; - - // TODO - //const viaStream = localStorage.getItem('enableExperimental') == 'true'; - - return new Promise((resolve, reject) => { - /*if (viaStream) { - const stream = this.stream.borrow(); - const id = Math.random().toString(); - stream.once(`api-res:${id}`, res => { - resolve(res); - }); - stream.send({ - type: 'api', - id, - endpoint, - data - }); - } else {*/ - const req = { - id: uuid(), - date: new Date(), - name: endpoint, - data, - res: null, - status: null - }; - - if (this.debug) { - this.requests.push(req); - } - - // Send request - fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: endpoint === 'signin' ? 'include' : 'omit', - cache: 'no-cache' - }).then(async (res) => { - if (--pending === 0) spinner.parentNode.removeChild(spinner); - - const body = res.status === 204 ? null : await res.json(); - - if (this.debug) { - req.status = res.status; - req.res = body; - } - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - /*}*/ - }); - } - - /** - * Misskeyのメタ情報を取得します - * @param force キャッシュを無視するか否か - */ - public getMeta(force = false) { - return new Promise<{ [x: string]: any }>(async (res, rej) => { - if (this.isMetaFetching) { - this.once('_meta_fetched_', () => { - res(this.meta.data); - }); - return; - } - - const expire = 1000 * 60; // 1min - - // forceが有効, meta情報を保持していない or 期限切れ - if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) { - this.isMetaFetching = true; - const meta = await this.api('meta'); - this.meta = { - data: meta, - chachedAt: new Date() - }; - this.isMetaFetching = false; - this.emit('_meta_fetched_'); - res(meta); - } else { - res(this.meta.data); - } - }); - } - - public connections: Connection[] = []; - - public registerStreamConnection(connection: Connection) { - this.connections.push(connection); - } - - public unregisterStreamConnection(connection: Connection) { - this.connections = this.connections.filter(c => c != connection); - } -} - -class WindowSystem extends EventEmitter { - public windows = new Set(); - - public add(window) { - this.windows.add(window); - this.emit('added', window); - } - - public remove(window) { - this.windows.delete(window); - this.emit('removed', window); - } - - public getAll() { - return this.windows; - } -} - -/** - * Convert the URL safe base64 string to a Uint8Array - * @param base64String base64 string - */ -function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/\-/g, '+') - .replace(/_/g, '/'); - - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -} diff --git a/src/server/web/app/common/scripts/check-for-update.ts b/src/server/web/app/common/scripts/check-for-update.ts deleted file mode 100644 index 81c1eb9812..0000000000 --- a/src/server/web/app/common/scripts/check-for-update.ts +++ /dev/null @@ -1,33 +0,0 @@ -import MiOS from '../mios'; -import { version as current } from '../../config'; - -export default async function(mios: MiOS, force = false, silent = false) { - const meta = await mios.getMeta(force); - const newer = meta.version; - - if (newer != current) { - localStorage.setItem('should-refresh', 'true'); - localStorage.setItem('v', newer); - - // Clear cache (serive worker) - try { - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - } - - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => registration.unregister()); - }); - } catch (e) { - console.error(e); - } - - if (!silent) { - alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)); - } - - return newer; - } else { - return null; - } -} diff --git a/src/server/web/app/common/scripts/compose-notification.ts b/src/server/web/app/common/scripts/compose-notification.ts deleted file mode 100644 index 273579cbc6..0000000000 --- a/src/server/web/app/common/scripts/compose-notification.ts +++ /dev/null @@ -1,67 +0,0 @@ -import getPostSummary from '../../../../common/get-post-summary'; -import getReactionEmoji from '../../../../common/get-reaction-emoji'; - -type Notification = { - title: string; - body: string; - icon: string; - onclick?: any; -}; - -// TODO: i18n - -export default function(type, data): Notification { - switch (type) { - case 'drive_file_created': - return { - title: 'ファイルがアップロードされました', - body: data.name, - icon: data.url + '?thumbnail&size=64' - }; - - case 'mention': - return { - title: `${data.user.name}さんから:`, - body: getPostSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'reply': - return { - title: `${data.user.name}さんから返信:`, - body: getPostSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'quote': - return { - title: `${data.user.name}さんが引用:`, - body: getPostSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'reaction': - return { - title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`, - body: getPostSummary(data.post), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'unread_messaging_message': - return { - title: `${data.user.name}さんからメッセージ:`, - body: data.text, // TODO: getMessagingMessageSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' - }; - - case 'othello_invited': - return { - title: '対局への招待があります', - body: `${data.parent.name}さんから`, - icon: data.parent.avatarUrl + '?thumbnail&size=64' - }; - - default: - return null; - } -} diff --git a/src/server/web/app/common/scripts/contains.ts b/src/server/web/app/common/scripts/contains.ts deleted file mode 100644 index a5071b3f25..0000000000 --- a/src/server/web/app/common/scripts/contains.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default (parent, child) => { - let node = child.parentNode; - while (node) { - if (node == parent) return true; - node = node.parentNode; - } - return false; -}; diff --git a/src/server/web/app/common/scripts/copy-to-clipboard.ts b/src/server/web/app/common/scripts/copy-to-clipboard.ts deleted file mode 100644 index 3d2741f8d7..0000000000 --- a/src/server/web/app/common/scripts/copy-to-clipboard.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Clipboardに値をコピー(TODO: 文字列以外も対応) - */ -export default val => { - const form = document.createElement('textarea'); - form.textContent = val; - document.body.appendChild(form); - form.select(); - const result = document.execCommand('copy'); - document.body.removeChild(form); - - return result; -}; diff --git a/src/server/web/app/common/scripts/date-stringify.ts b/src/server/web/app/common/scripts/date-stringify.ts deleted file mode 100644 index e51de8833d..0000000000 --- a/src/server/web/app/common/scripts/date-stringify.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default date => { - if (typeof date == 'string') date = new Date(date); - return ( - date.getFullYear() + '年' + - (date.getMonth() + 1) + '月' + - date.getDate() + '日' + - ' ' + - date.getHours() + '時' + - date.getMinutes() + '分' + - ' ' + - `(${['日', '月', '火', '水', '木', '金', '土'][date.getDay()]})` - ); -}; diff --git a/src/server/web/app/common/scripts/fuck-ad-block.ts b/src/server/web/app/common/scripts/fuck-ad-block.ts deleted file mode 100644 index 9bcf7deeff..0000000000 --- a/src/server/web/app/common/scripts/fuck-ad-block.ts +++ /dev/null @@ -1,21 +0,0 @@ -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); - } -}; diff --git a/src/server/web/app/common/scripts/gcd.ts b/src/server/web/app/common/scripts/gcd.ts deleted file mode 100644 index 9a19f9da66..0000000000 --- a/src/server/web/app/common/scripts/gcd.ts +++ /dev/null @@ -1,2 +0,0 @@ -const gcd = (a, b) => !b ? a : gcd(b, a % b); -export default gcd; diff --git a/src/server/web/app/common/scripts/get-kao.ts b/src/server/web/app/common/scripts/get-kao.ts deleted file mode 100644 index 2168c5be88..0000000000 --- a/src/server/web/app/common/scripts/get-kao.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default () => [ - '(=^・・^=)', - 'v(‘ω’)v', - '🐡( \'-\' 🐡 )フグパンチ!!!!' -][Math.floor(Math.random() * 3)]; diff --git a/src/server/web/app/common/scripts/get-median.ts b/src/server/web/app/common/scripts/get-median.ts deleted file mode 100644 index 91a415d5b2..0000000000 --- a/src/server/web/app/common/scripts/get-median.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 中央値を求めます - * @param samples サンプル - */ -export default function(samples) { - if (!samples.length) return 0; - const numbers = samples.slice(0).sort((a, b) => a - b); - const middle = Math.floor(numbers.length / 2); - const isEven = numbers.length % 2 === 0; - return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle]; -} diff --git a/src/server/web/app/common/scripts/loading.ts b/src/server/web/app/common/scripts/loading.ts deleted file mode 100644 index c48e626648..0000000000 --- a/src/server/web/app/common/scripts/loading.ts +++ /dev/null @@ -1,21 +0,0 @@ -const NProgress = require('nprogress'); -NProgress.configure({ - trickleSpeed: 500, - showSpinner: false -}); - -const root = document.getElementsByTagName('html')[0]; - -export default { - start: () => { - root.classList.add('progress'); - NProgress.start(); - }, - done: () => { - root.classList.remove('progress'); - NProgress.done(); - }, - set: val => { - NProgress.set(val); - } -}; diff --git a/src/server/web/app/common/scripts/parse-search-query.ts b/src/server/web/app/common/scripts/parse-search-query.ts deleted file mode 100644 index 4f09d2b93f..0000000000 --- a/src/server/web/app/common/scripts/parse-search-query.ts +++ /dev/null @@ -1,53 +0,0 @@ -export default function(qs: string) { - const q = { - text: '' - }; - - qs.split(' ').forEach(x => { - if (/^([a-z_]+?):(.+?)$/.test(x)) { - const [key, value] = x.split(':'); - switch (key) { - case 'user': - q['includeUserUsernames'] = value.split(','); - break; - case 'exclude_user': - q['excludeUserUsernames'] = value.split(','); - break; - case 'follow': - q['following'] = value == 'null' ? null : value == 'true'; - break; - case 'reply': - q['reply'] = value == 'null' ? null : value == 'true'; - break; - case 'repost': - q['repost'] = value == 'null' ? null : value == 'true'; - break; - case 'media': - q['media'] = value == 'null' ? null : value == 'true'; - break; - case 'poll': - q['poll'] = value == 'null' ? null : value == 'true'; - break; - case 'until': - case 'since': - // YYYY-MM-DD - if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { - const [yyyy, mm, dd] = value.split('-'); - q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); - } - break; - default: - q[key] = value; - break; - } - } else { - q.text += x + ' '; - } - }); - - if (q.text) { - q.text = q.text.trim(); - } - - return q; -} diff --git a/src/server/web/app/common/scripts/streaming/channel.ts b/src/server/web/app/common/scripts/streaming/channel.ts deleted file mode 100644 index cab5f4edb4..0000000000 --- a/src/server/web/app/common/scripts/streaming/channel.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Stream from './stream'; -import MiOS from '../../mios'; - -/** - * Channel stream connection - */ -export default class Connection extends Stream { - constructor(os: MiOS, channelId) { - super(os, 'channel', { - channel: channelId - }); - } -} diff --git a/src/server/web/app/common/scripts/streaming/drive.ts b/src/server/web/app/common/scripts/streaming/drive.ts deleted file mode 100644 index f11573685e..0000000000 --- a/src/server/web/app/common/scripts/streaming/drive.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../mios'; - -/** - * Drive stream connection - */ -export class DriveStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'drive', { - i: me.account.token - }); - } -} - -export class DriveStreamManager extends StreamManager<DriveStream> { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new DriveStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/server/web/app/common/scripts/streaming/home.ts b/src/server/web/app/common/scripts/streaming/home.ts deleted file mode 100644 index c198619400..0000000000 --- a/src/server/web/app/common/scripts/streaming/home.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as merge from 'object-assign-deep'; - -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../mios'; - -/** - * Home stream connection - */ -export class HomeStream extends Stream { - constructor(os: MiOS, me) { - super(os, '', { - i: me.account.token - }); - - // 最終利用日時を更新するため定期的にaliveメッセージを送信 - setInterval(() => { - this.send({ type: 'alive' }); - me.account.lastUsedAt = new Date(); - }, 1000 * 60); - - // 自分の情報が更新されたとき - this.on('i_updated', i => { - if (os.debug) { - console.log('I updated:', i); - } - merge(me, i); - }); - - // トークンが再生成されたとき - // このままではAPIが利用できないので強制的にサインアウトさせる - this.on('my_token_regenerated', () => { - alert('%i18n:common.my-token-regenerated%'); - os.signout(); - }); - } -} - -export class HomeStreamManager extends StreamManager<HomeStream> { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new HomeStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/server/web/app/common/scripts/streaming/messaging-index.ts b/src/server/web/app/common/scripts/streaming/messaging-index.ts deleted file mode 100644 index 24f0ce0c9f..0000000000 --- a/src/server/web/app/common/scripts/streaming/messaging-index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../mios'; - -/** - * Messaging index stream connection - */ -export class MessagingIndexStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'messaging-index', { - i: me.account.token - }); - } -} - -export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new MessagingIndexStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/server/web/app/common/scripts/streaming/messaging.ts b/src/server/web/app/common/scripts/streaming/messaging.ts deleted file mode 100644 index 4c593deb31..0000000000 --- a/src/server/web/app/common/scripts/streaming/messaging.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Stream from './stream'; -import MiOS from '../../mios'; - -/** - * Messaging stream connection - */ -export class MessagingStream extends Stream { - constructor(os: MiOS, me, otherparty) { - super(os, 'messaging', { - i: me.account.token, - otherparty - }); - - (this as any).on('_connected_', () => { - this.send({ - i: me.account.token - }); - }); - } -} diff --git a/src/server/web/app/common/scripts/streaming/othello-game.ts b/src/server/web/app/common/scripts/streaming/othello-game.ts deleted file mode 100644 index f34ef35147..0000000000 --- a/src/server/web/app/common/scripts/streaming/othello-game.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Stream from './stream'; -import MiOS from '../../mios'; - -export class OthelloGameStream extends Stream { - constructor(os: MiOS, me, game) { - super(os, 'othello-game', { - i: me ? me.account.token : null, - game: game.id - }); - } -} diff --git a/src/server/web/app/common/scripts/streaming/othello.ts b/src/server/web/app/common/scripts/streaming/othello.ts deleted file mode 100644 index 8c6f4b9c3c..0000000000 --- a/src/server/web/app/common/scripts/streaming/othello.ts +++ /dev/null @@ -1,31 +0,0 @@ -import StreamManager from './stream-manager'; -import Stream from './stream'; -import MiOS from '../../mios'; - -export class OthelloStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'othello', { - i: me.account.token - }); - } -} - -export class OthelloStreamManager extends StreamManager<OthelloStream> { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new OthelloStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/server/web/app/common/scripts/streaming/requests.ts b/src/server/web/app/common/scripts/streaming/requests.ts deleted file mode 100644 index 5bec30143f..0000000000 --- a/src/server/web/app/common/scripts/streaming/requests.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../mios'; - -/** - * Requests stream connection - */ -export class RequestsStream extends Stream { - constructor(os: MiOS) { - super(os, 'requests'); - } -} - -export class RequestsStreamManager extends StreamManager<RequestsStream> { - private os: MiOS; - - constructor(os: MiOS) { - super(); - - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new RequestsStream(this.os); - } - - return this.connection; - } -} diff --git a/src/server/web/app/common/scripts/streaming/server.ts b/src/server/web/app/common/scripts/streaming/server.ts deleted file mode 100644 index 3d35ef4d9d..0000000000 --- a/src/server/web/app/common/scripts/streaming/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../mios'; - -/** - * Server stream connection - */ -export class ServerStream extends Stream { - constructor(os: MiOS) { - super(os, 'server'); - } -} - -export class ServerStreamManager extends StreamManager<ServerStream> { - private os: MiOS; - - constructor(os: MiOS) { - super(); - - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new ServerStream(this.os); - } - - return this.connection; - } -} diff --git a/src/server/web/app/common/scripts/streaming/stream-manager.ts b/src/server/web/app/common/scripts/streaming/stream-manager.ts deleted file mode 100644 index 568b8b0372..0000000000 --- a/src/server/web/app/common/scripts/streaming/stream-manager.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { EventEmitter } from 'eventemitter3'; -import * as uuid from 'uuid'; -import Connection from './stream'; - -/** - * ストリーム接続を管理するクラス - * 複数の場所から同じストリームを利用する際、接続をまとめたりする - */ -export default abstract class StreamManager<T extends Connection> extends EventEmitter { - private _connection: T = null; - - private disposeTimerId: any; - - /** - * コネクションを必要としているユーザー - */ - private users = []; - - protected set connection(connection: T) { - this._connection = connection; - - if (this._connection == null) { - this.emit('disconnected'); - } else { - this.emit('connected', this._connection); - - this._connection.on('_connected_', () => { - this.emit('_connected_'); - }); - - this._connection.on('_disconnected_', () => { - this.emit('_disconnected_'); - }); - - this._connection.user = 'Managed'; - } - } - - protected get connection() { - return this._connection; - } - - /** - * コネクションを持っているか否か - */ - public get hasConnection() { - return this._connection != null; - } - - public get state(): string { - if (!this.hasConnection) return 'no-connection'; - return this._connection.state; - } - - /** - * コネクションを要求します - */ - public abstract getConnection(): T; - - /** - * 現在接続しているコネクションを取得します - */ - public borrow() { - return this._connection; - } - - /** - * コネクションを要求するためのユーザーIDを発行します - */ - public use() { - // タイマー解除 - if (this.disposeTimerId) { - clearTimeout(this.disposeTimerId); - this.disposeTimerId = null; - } - - // ユーザーID生成 - const userId = uuid(); - - this.users.push(userId); - - this._connection.user = `Managed (${ this.users.length })`; - - return userId; - } - - /** - * コネクションを利用し終わってもう必要ないことを通知します - * @param userId use で発行したユーザーID - */ - public dispose(userId) { - this.users = this.users.filter(id => id != userId); - - this._connection.user = `Managed (${ this.users.length })`; - - // 誰もコネクションの利用者がいなくなったら - if (this.users.length == 0) { - // また直ぐに再利用される可能性があるので、一定時間待ち、 - // 新たな利用者が現れなければコネクションを切断する - this.disposeTimerId = setTimeout(() => { - this.disposeTimerId = null; - - this.connection.close(); - this.connection = null; - }, 3000); - } - } -} diff --git a/src/server/web/app/common/scripts/streaming/stream.ts b/src/server/web/app/common/scripts/streaming/stream.ts deleted file mode 100644 index 3912186ad3..0000000000 --- a/src/server/web/app/common/scripts/streaming/stream.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { EventEmitter } from 'eventemitter3'; -import * as uuid from 'uuid'; -import * as ReconnectingWebsocket from 'reconnecting-websocket'; -import { wsUrl } from '../../../config'; -import MiOS from '../../mios'; - -/** - * Misskey stream connection - */ -export default class Connection extends EventEmitter { - public state: string; - private buffer: any[]; - public socket: ReconnectingWebsocket; - public name: string; - public connectedAt: Date; - public user: string = null; - public in: number = 0; - public out: number = 0; - public inout: Array<{ - type: 'in' | 'out', - at: Date, - data: string - }> = []; - public id: string; - public isSuspended = false; - private os: MiOS; - - constructor(os: MiOS, endpoint, params?) { - super(); - - //#region BIND - this.onOpen = this.onOpen.bind(this); - this.onClose = this.onClose.bind(this); - this.onMessage = this.onMessage.bind(this); - this.send = this.send.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.id = uuid(); - this.os = os; - this.name = endpoint; - this.state = 'initializing'; - this.buffer = []; - - const query = params - ? Object.keys(params) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) - .join('&') - : null; - - this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); - this.socket.addEventListener('open', this.onOpen); - this.socket.addEventListener('close', this.onClose); - this.socket.addEventListener('message', this.onMessage); - - // Register this connection for debugging - this.os.registerStreamConnection(this); - } - - /** - * Callback of when open connection - */ - private onOpen() { - this.state = 'connected'; - this.emit('_connected_'); - - this.connectedAt = new Date(); - - // バッファーを処理 - const _buffer = [].concat(this.buffer); // Shallow copy - this.buffer = []; // Clear buffer - _buffer.forEach(data => { - this.send(data); // Resend each buffered messages - - if (this.os.debug) { - this.out++; - this.inout.push({ type: 'out', at: new Date(), data }); - } - }); - } - - /** - * Callback of when close connection - */ - private onClose() { - this.state = 'reconnecting'; - this.emit('_disconnected_'); - } - - /** - * Callback of when received a message from connection - */ - private onMessage(message) { - if (this.isSuspended) return; - - if (this.os.debug) { - this.in++; - this.inout.push({ type: 'in', at: new Date(), data: message.data }); - } - - try { - const msg = JSON.parse(message.data); - if (msg.type) this.emit(msg.type, msg.body); - } catch (e) { - // noop - } - } - - /** - * Send a message to connection - */ - public send(data) { - if (this.isSuspended) return; - - // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する - if (this.state != 'connected') { - this.buffer.push(data); - return; - } - - if (this.os.debug) { - this.out++; - this.inout.push({ type: 'out', at: new Date(), data }); - } - - this.socket.send(JSON.stringify(data)); - } - - /** - * Close this connection - */ - public close() { - this.os.unregisterStreamConnection(this); - this.socket.removeEventListener('open', this.onOpen); - this.socket.removeEventListener('message', this.onMessage); - } -} diff --git a/src/server/web/app/common/views/components/autocomplete.vue b/src/server/web/app/common/views/components/autocomplete.vue deleted file mode 100644 index 79bd2ba023..0000000000 --- a/src/server/web/app/common/views/components/autocomplete.vue +++ /dev/null @@ -1,306 +0,0 @@ -<template> -<div class="mk-autocomplete" @contextmenu.prevent="() => {}"> - <ol class="users" ref="suggests" v-if="users.length > 0"> - <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> - <span class="name">{{ user.name }}</span> - <span class="username">@{{ getAcct(user) }}</span> - </li> - </ol> - <ol class="emojis" ref="suggests" v-if="emojis.length > 0"> - <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> - <span class="emoji">{{ emoji.emoji }}</span> - <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> - <span class="alias" v-if="emoji.alias">({{ emoji.alias }})</span> - </li> - </ol> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as emojilib from 'emojilib'; -import contains from '../../../common/scripts/contains'; -import getAcct from '../../../../../common/user/get-acct'; - -const lib = Object.entries(emojilib.lib).filter((x: any) => { - return x[1].category != 'flags'; -}); -const emjdb = lib.map((x: any) => ({ - emoji: x[1].char, - name: x[0], - alias: null -})); -lib.forEach((x: any) => { - if (x[1].keywords) { - x[1].keywords.forEach(k => { - emjdb.push({ - emoji: x[1].char, - name: k, - alias: x[0] - }); - }); - } -}); -emjdb.sort((a, b) => a.name.length - b.name.length); - -export default Vue.extend({ - props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'], - data() { - return { - fetching: true, - users: [], - emojis: [], - select: -1, - emojilib - } - }, - computed: { - items(): HTMLCollection { - return (this.$refs.suggests as Element).children; - } - }, - updated() { - //#region 位置調整 - const margin = 32; - - if (this.x + this.$el.offsetWidth > window.innerWidth - margin) { - this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px'; - this.$el.style.marginLeft = '-16px'; - } else { - this.$el.style.left = this.x + 'px'; - this.$el.style.marginLeft = '0'; - } - - if (this.y + this.$el.offsetHeight > window.innerHeight - margin) { - this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px'; - this.$el.style.marginTop = '0'; - } else { - this.$el.style.top = this.y + 'px'; - this.$el.style.marginTop = 'calc(1em + 8px)'; - } - //#endregion - }, - mounted() { - this.textarea.addEventListener('keydown', this.onKeydown); - - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.addEventListener('mousedown', this.onMousedown); - }); - - this.$nextTick(() => { - this.exec(); - - this.$watch('q', () => { - this.$nextTick(() => { - this.exec(); - }); - }); - }); - }, - beforeDestroy() { - this.textarea.removeEventListener('keydown', this.onKeydown); - - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.removeEventListener('mousedown', this.onMousedown); - }); - }, - methods: { - getAcct, - exec() { - this.select = -1; - if (this.$refs.suggests) { - Array.from(this.items).forEach(el => { - el.removeAttribute('data-selected'); - }); - } - - if (this.type == 'user') { - const cache = sessionStorage.getItem(this.q); - if (cache) { - const users = JSON.parse(cache); - this.users = users; - this.fetching = false; - } else { - (this as any).api('users/search_by_username', { - query: this.q, - limit: 30 - }).then(users => { - this.users = users; - this.fetching = false; - - // キャッシュ - sessionStorage.setItem(this.q, JSON.stringify(users)); - }); - } - } else if (this.type == 'emoji') { - const matched = []; - emjdb.some(x => { - if (x.name.indexOf(this.q) == 0 && !x.alias && !matched.some(y => y.emoji == x.emoji)) matched.push(x); - return matched.length == 30; - }); - if (matched.length < 30) { - emjdb.some(x => { - if (x.name.indexOf(this.q) == 0 && !matched.some(y => y.emoji == x.emoji)) matched.push(x); - return matched.length == 30; - }); - } - if (matched.length < 30) { - emjdb.some(x => { - if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x); - return matched.length == 30; - }); - } - this.emojis = matched; - } - }, - - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - }, - - onKeydown(e) { - const cancel = () => { - e.preventDefault(); - e.stopPropagation(); - }; - - switch (e.which) { - case 10: // [ENTER] - case 13: // [ENTER] - if (this.select !== -1) { - cancel(); - (this.items[this.select] as any).click(); - } else { - this.close(); - } - break; - - case 27: // [ESC] - cancel(); - this.close(); - break; - - case 38: // [↑] - if (this.select !== -1) { - cancel(); - this.selectPrev(); - } else { - this.close(); - } - break; - - case 9: // [TAB] - case 40: // [↓] - cancel(); - this.selectNext(); - break; - - default: - e.stopPropagation(); - this.textarea.focus(); - } - }, - - selectNext() { - if (++this.select >= this.items.length) this.select = 0; - this.applySelect(); - }, - - selectPrev() { - if (--this.select < 0) this.select = this.items.length - 1; - this.applySelect(); - }, - - applySelect() { - Array.from(this.items).forEach(el => { - el.removeAttribute('data-selected'); - }); - - this.items[this.select].setAttribute('data-selected', 'true'); - (this.items[this.select] as any).focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-autocomplete - position fixed - z-index 65535 - margin-top calc(1em + 8px) - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - transition top 0.1s ease, left 0.1s ease - - > ol - display block - margin 0 - padding 4px 0 - max-height 190px - max-width 500px - overflow auto - list-style none - - > li - display block - padding 4px 12px - white-space nowrap - overflow hidden - font-size 0.9em - color rgba(0, 0, 0, 0.8) - cursor default - - &, * - user-select none - - &:hover - &[data-selected='true'] - background $theme-color - - &, * - color #fff !important - - &:active - background darken($theme-color, 10%) - - &, * - color #fff !important - - > .users > li - - .avatar - vertical-align middle - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 100% - - .name - margin 0 8px 0 0 - color rgba(0, 0, 0, 0.8) - - .username - color rgba(0, 0, 0, 0.3) - - > .emojis > li - - .emoji - display inline-block - margin 0 4px 0 0 - width 24px - - .name - color rgba(0, 0, 0, 0.8) - - .alias - margin 0 0 0 8px - color rgba(0, 0, 0, 0.3) - -</style> diff --git a/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue b/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue deleted file mode 100644 index cadbd36ba4..0000000000 --- a/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue +++ /dev/null @@ -1,137 +0,0 @@ -<template> -<div class="troubleshooter"> - <h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1> - <div> - <p :data-wip="network == null"> - <template v-if="network != null"> - <template v-if="network">%fa:check%</template> - <template v-if="!network">%fa:times%</template> - </template> - {{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }}<mk-ellipsis v-if="network == null"/> - </p> - <p v-if="network == true" :data-wip="internet == null"> - <template v-if="internet != null"> - <template v-if="internet">%fa:check%</template> - <template v-if="!internet">%fa:times%</template> - </template> - {{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }}<mk-ellipsis v-if="internet == null"/> - </p> - <p v-if="internet == true" :data-wip="server == null"> - <template v-if="server != null"> - <template v-if="server">%fa:check%</template> - <template v-if="!server">%fa:times%</template> - </template> - {{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }}<mk-ellipsis v-if="server == null"/> - </p> - </div> - <p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p> - <p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p> - <p v-if="internet === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p> - <p v-if="server === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p> - <p v-if="server === true" class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { apiUrl } from '../../../config'; - -export default Vue.extend({ - data() { - return { - network: navigator.onLine, - end: false, - internet: null, - server: null - }; - }, - mounted() { - if (!this.network) { - this.end = true; - return; - } - - // Check internet connection - fetch('https://google.com?rand=' + Math.random(), { - mode: 'no-cors' - }).then(() => { - this.internet = true; - - // Check misskey server is available - fetch(`${apiUrl}/meta`).then(() => { - this.end = true; - this.server = true; - }) - .catch(() => { - this.end = true; - this.server = false; - }); - }) - .catch(() => { - this.end = true; - this.internet = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.troubleshooter - width 100% - max-width 500px - text-align left - background #fff - border-radius 8px - border solid 1px #ddd - - > h1 - margin 0 - padding 0.6em 1.2em - font-size 1em - color #444 - border-bottom solid 1px #eee - - > [data-fa] - margin-right 0.25em - - > div - overflow hidden - padding 0.6em 1.2em - - > p - margin 0.5em 0 - font-size 0.9em - color #444 - - &[data-wip] - color #888 - - > [data-fa] - margin-right 0.25em - - &.times - color #e03524 - - &.check - color #84c32f - - > p - margin 0 - padding 0.7em 1.2em - font-size 1em - color #444 - border-top solid 1px #eee - - > b - > [data-fa] - margin-right 0.25em - - &.success - > b - color #39adad - - &:not(.success) - > b - color #ad4339 - -</style> diff --git a/src/server/web/app/common/views/components/connect-failed.vue b/src/server/web/app/common/views/components/connect-failed.vue deleted file mode 100644 index 185250dbd8..0000000000 --- a/src/server/web/app/common/views/components/connect-failed.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="mk-connect-failed"> - <img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/> - <h1>%i18n:common.tags.mk-error.title%</h1> - <p class="text"> - {{ '%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{')) }} - <a @click="reload">{{ '%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1] }}</a> - {{ '%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1) }} - </p> - <button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:common.tags.mk-error.troubleshoot%</button> - <x-troubleshooter v-if="troubleshooting"/> - <p class="thanks">%i18n:common.tags.mk-error.thanks%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XTroubleshooter from './connect-failed.troubleshooter.vue'; - -export default Vue.extend({ - components: { - XTroubleshooter - }, - data() { - return { - troubleshooting: false - }; - }, - mounted() { - document.title = 'Oops!'; - document.documentElement.style.background = '#f8f8f8'; - }, - methods: { - reload() { - location.reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-connect-failed - width 100% - padding 32px 18px - text-align center - - > img - display block - height 200px - margin 0 auto - pointer-events none - user-select none - - > h1 - display block - margin 1.25em auto 0.65em auto - font-size 1.5em - color #555 - - > .text - display block - margin 0 auto - max-width 600px - font-size 1em - color #666 - - > button - display block - margin 1em auto 0 auto - padding 8px 10px - color $theme-color-foreground - background $theme-color - - &:focus - outline solid 3px rgba($theme-color, 0.3) - - &:hover - background lighten($theme-color, 10%) - - &:active - background darken($theme-color, 10%) - - > .troubleshooter - margin 1em auto 0 auto - - > .thanks - display block - margin 2em auto 0 auto - padding 2em 0 0 0 - max-width 600px - font-size 0.9em - font-style oblique - color #aaa - border-top solid 1px #eee - - @media (max-width 500px) - padding 24px 18px - font-size 80% - - > img - height 150px - -</style> - diff --git a/src/server/web/app/common/views/components/ellipsis.vue b/src/server/web/app/common/views/components/ellipsis.vue deleted file mode 100644 index 07349902de..0000000000 --- a/src/server/web/app/common/views/components/ellipsis.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> - <span class="mk-ellipsis"> - <span>.</span><span>.</span><span>.</span> - </span> -</template> - -<style lang="stylus" scoped> -.mk-ellipsis - > span - animation ellipsis 1.4s infinite ease-in-out both - - &:nth-child(1) - animation-delay 0s - - &:nth-child(2) - animation-delay 0.16s - - &:nth-child(3) - animation-delay 0.32s - - @keyframes ellipsis - 0%, 80%, 100% - opacity 1 - 40% - opacity 0 -</style> diff --git a/src/server/web/app/common/views/components/file-type-icon.vue b/src/server/web/app/common/views/components/file-type-icon.vue deleted file mode 100644 index b7e868d1f7..0000000000 --- a/src/server/web/app/common/views/components/file-type-icon.vue +++ /dev/null @@ -1,17 +0,0 @@ -<template> -<span class="mk-file-type-icon"> - <template v-if="kind == 'image'">%fa:file-image%</template> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['type'], - computed: { - kind(): string { - return this.type.split('/')[0]; - } - } -}); -</script> diff --git a/src/server/web/app/common/views/components/forkit.vue b/src/server/web/app/common/views/components/forkit.vue deleted file mode 100644 index 6f334b965a..0000000000 --- a/src/server/web/app/common/views/components/forkit.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<a class="a" href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%"> - <svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden"> - <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> - <path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path> - <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path> - </svg> -</a> -</template> - -<style lang="stylus" scoped> -@import '~const.styl' - -.a - display block - position absolute - top 0 - right 0 - - > svg - display block - //fill #151513 - //color #fff - fill $theme-color - color $theme-color-foreground - - .octo-arm - transform-origin 130px 106px - - &:hover - .octo-arm - animation octocat-wave 560ms ease-in-out - - @keyframes octocat-wave - 0%, 100% - transform rotate(0) - 20%, 60% - transform rotate(-25deg) - 40%, 80% - transform rotate(10deg) - -</style> diff --git a/src/server/web/app/common/views/components/index.ts b/src/server/web/app/common/views/components/index.ts deleted file mode 100644 index b58ba37ecb..0000000000 --- a/src/server/web/app/common/views/components/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Vue from 'vue'; - -import signin from './signin.vue'; -import signup from './signup.vue'; -import forkit from './forkit.vue'; -import nav from './nav.vue'; -import postHtml from './post-html'; -import poll from './poll.vue'; -import pollEditor from './poll-editor.vue'; -import reactionIcon from './reaction-icon.vue'; -import reactionsViewer from './reactions-viewer.vue'; -import time from './time.vue'; -import timer from './timer.vue'; -import mediaList from './media-list.vue'; -import uploader from './uploader.vue'; -import specialMessage from './special-message.vue'; -import streamIndicator from './stream-indicator.vue'; -import ellipsis from './ellipsis.vue'; -import messaging from './messaging.vue'; -import messagingRoom from './messaging-room.vue'; -import urlPreview from './url-preview.vue'; -import twitterSetting from './twitter-setting.vue'; -import fileTypeIcon from './file-type-icon.vue'; -import Switch from './switch.vue'; -import Othello from './othello.vue'; -import welcomeTimeline from './welcome-timeline.vue'; - -Vue.component('mk-signin', signin); -Vue.component('mk-signup', signup); -Vue.component('mk-forkit', forkit); -Vue.component('mk-nav', nav); -Vue.component('mk-post-html', postHtml); -Vue.component('mk-poll', poll); -Vue.component('mk-poll-editor', pollEditor); -Vue.component('mk-reaction-icon', reactionIcon); -Vue.component('mk-reactions-viewer', reactionsViewer); -Vue.component('mk-time', time); -Vue.component('mk-timer', timer); -Vue.component('mk-media-list', mediaList); -Vue.component('mk-uploader', uploader); -Vue.component('mk-special-message', specialMessage); -Vue.component('mk-stream-indicator', streamIndicator); -Vue.component('mk-ellipsis', ellipsis); -Vue.component('mk-messaging', messaging); -Vue.component('mk-messaging-room', messagingRoom); -Vue.component('mk-url-preview', urlPreview); -Vue.component('mk-twitter-setting', twitterSetting); -Vue.component('mk-file-type-icon', fileTypeIcon); -Vue.component('mk-switch', Switch); -Vue.component('mk-othello', Othello); -Vue.component('mk-welcome-timeline', welcomeTimeline); diff --git a/src/server/web/app/common/views/components/media-list.vue b/src/server/web/app/common/views/components/media-list.vue deleted file mode 100644 index 64172ad0b4..0000000000 --- a/src/server/web/app/common/views/components/media-list.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="mk-media-list" :data-count="mediaList.length"> - <template v-for="media in mediaList"> - <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/> - <mk-media-image :image="media" :key="media.id" v-else /> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['mediaList'], -}); -</script> - -<style lang="stylus" scoped> -.mk-media-list - display grid - grid-gap 4px - height 256px - - @media (max-width 500px) - height 192px - - &[data-count="1"] - grid-template-rows 1fr - &[data-count="2"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr - &[data-count="3"] - grid-template-columns 1fr 0.5fr - grid-template-rows 1fr 1fr - :nth-child(1) - grid-row 1 / 3 - :nth-child(3) - grid-column 2 / 3 - grid-row 2/3 - &[data-count="4"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr 1fr - - :nth-child(1) - grid-column 1 / 2 - grid-row 1 / 2 - :nth-child(2) - grid-column 2 / 3 - grid-row 1 / 2 - :nth-child(3) - grid-column 1 / 2 - grid-row 2 / 3 - :nth-child(4) - grid-column 2 / 3 - grid-row 2 / 3 - -</style> diff --git a/src/server/web/app/common/views/components/messaging-room.form.vue b/src/server/web/app/common/views/components/messaging-room.form.vue deleted file mode 100644 index 704f2016d8..0000000000 --- a/src/server/web/app/common/views/components/messaging-room.form.vue +++ /dev/null @@ -1,305 +0,0 @@ -<template> -<div class="mk-messaging-form" - @dragover.stop="onDragover" - @drop.stop="onDrop" -> - <textarea - v-model="text" - ref="textarea" - @keypress="onKeypress" - @paste="onPaste" - placeholder="%i18n:common.input-message-here%" - v-autocomplete="'text'" - ></textarea> - <div class="file" @click="file = null" v-if="file">{{ file.name }}</div> - <mk-uploader ref="uploader" @uploaded="onUploaded"/> - <button class="send" @click="send" :disabled="!canSend || sending" title="%i18n:common.send%"> - <template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template> - </button> - <button class="attach-from-local" @click="chooseFile" title="%i18n:common.tags.mk-messaging-form.attach-from-local%"> - %fa:upload% - </button> - <button class="attach-from-drive" @click="chooseFileFromDrive" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%"> - %fa:R folder-open% - </button> - <input ref="file" type="file" @change="onChangeFile"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as autosize from 'autosize'; - -export default Vue.extend({ - props: ['user'], - data() { - return { - text: null, - file: null, - sending: false - }; - }, - computed: { - draftId(): string { - return this.user.id; - }, - canSend(): boolean { - return (this.text != null && this.text != '') || this.file != null; - }, - room(): any { - return this.$parent; - } - }, - watch: { - text() { - this.saveDraft(); - }, - file() { - this.saveDraft(); - - if (this.room.isBottom()) { - this.room.scrollToBottom(); - } - } - }, - mounted() { - autosize(this.$refs.textarea); - - // 書きかけの投稿を復元 - const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId]; - if (draft) { - this.text = draft.data.text; - this.file = draft.data.file; - } - }, - methods: { - onPaste(e) { - const data = e.clipboardData; - const items = data.items; - - if (items.length == 1) { - if (items[0].kind == 'file') { - this.upload(items[0].getAsFile()); - } - } else { - if (items[0].kind == 'file') { - alert('メッセージに添付できるのはひとつのファイルのみです'); - } - } - }, - - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - if (isFile || isDriveFile) { - e.preventDefault(); - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - - onDrop(e): void { - // ファイルだったら - if (e.dataTransfer.files.length == 1) { - e.preventDefault(); - this.upload(e.dataTransfer.files[0]); - return; - } else if (e.dataTransfer.files.length > 1) { - e.preventDefault(); - alert('メッセージに添付できるのはひとつのファイルのみです'); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - this.file = JSON.parse(driveFile); - e.preventDefault(); - } - //#endregion - }, - - onKeypress(e) { - if ((e.which == 10 || e.which == 13) && e.ctrlKey) { - this.send(); - } - }, - - chooseFile() { - (this.$refs.file as any).click(); - }, - - chooseFileFromDrive() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.file = file; - }); - }, - - onChangeFile() { - this.upload((this.$refs.file as any).files[0]); - }, - - upload(file) { - (this.$refs.uploader as any).upload(file); - }, - - onUploaded(file) { - this.file = file; - }, - - send() { - this.sending = true; - (this as any).api('messaging/messages/create', { - userId: this.user.id, - text: this.text ? this.text : undefined, - fileId: this.file ? this.file.id : undefined - }).then(message => { - this.clear(); - }).catch(err => { - console.error(err); - }).then(() => { - this.sending = false; - }); - }, - - clear() { - this.text = ''; - this.file = null; - this.deleteDraft(); - }, - - saveDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - - data[this.draftId] = { - updatedAt: new Date(), - data: { - text: this.text, - file: this.file - } - } - - localStorage.setItem('message_drafts', JSON.stringify(data)); - }, - - deleteDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - - delete data[this.draftId]; - - localStorage.setItem('message_drafts', JSON.stringify(data)); - }, - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-messaging-form - > textarea - cursor auto - display block - width 100% - min-width 100% - max-width 100% - height 64px - margin 0 - padding 8px - resize none - font-size 1em - color #000 - outline none - border none - border-top solid 1px #eee - border-radius 0 - box-shadow none - background transparent - - > .file - padding 8px - color #444 - background #eee - cursor pointer - - > .send - position absolute - bottom 0 - right 0 - margin 0 - padding 10px 14px - font-size 1em - color #aaa - transition color 0.1s ease - - &:hover - color $theme-color - - &:active - color darken($theme-color, 10%) - transition color 0s ease - - .files - display block - margin 0 - padding 0 8px - list-style none - - &:after - content '' - display block - clear both - - > li - display block - float left - margin 4px - padding 0 - width 64px - height 64px - background-color #eee - background-repeat no-repeat - background-position center center - background-size cover - cursor move - - &:hover - > .remove - display block - - > .remove - display none - position absolute - right -6px - top -6px - margin 0 - padding 0 - background transparent - outline none - border none - border-radius 0 - box-shadow none - cursor pointer - - .attach-from-local - .attach-from-drive - margin 0 - padding 10px 14px - font-size 1em - font-weight normal - text-decoration none - color #aaa - transition color 0.1s ease - - &:hover - color $theme-color - - &:active - color darken($theme-color, 10%) - transition color 0s ease - - input[type=file] - display none - -</style> diff --git a/src/server/web/app/common/views/components/messaging-room.message.vue b/src/server/web/app/common/views/components/messaging-room.message.vue deleted file mode 100644 index 94f87fd709..0000000000 --- a/src/server/web/app/common/views/components/messaging-room.message.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<div class="message" :data-is-me="isMe"> - <router-link class="avatar-anchor" :to="`/@${acct}`" :title="acct" target="_blank"> - <img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/> - </router-link> - <div class="content"> - <div class="balloon" :data-no-text="message.text == null"> - <p class="read" v-if="isMe && message.isRead">%i18n:common.tags.mk-messaging-message.is-read%</p> - <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> - <img src="/assets/desktop/messaging/delete.png" alt="Delete"/> - </button> - <div class="content" v-if="!message.isDeleted"> - <mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/> - <div class="file" v-if="message.file"> - <a :href="message.file.url" target="_blank" :title="message.file.name"> - <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> - <p v-else>{{ message.file.name }}</p> - </a> - </div> - </div> - <div class="content" v-if="message.isDeleted"> - <p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p> - </div> - </div> - <div></div> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <footer> - <mk-time :time="message.createdAt"/> - <template v-if="message.is_edited">%fa:pencil-alt%</template> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['message'], - computed: { - acct() { - return getAcct(this.message.user); - }, - isMe(): boolean { - return this.message.userId == (this as any).os.i.id; - }, - urls(): string[] { - if (this.message.ast) { - return this.message.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.message - $me-balloon-color = #23A7B6 - - padding 10px 12px 10px 12px - background-color transparent - - > .avatar-anchor - display block - position absolute - top 10px - - > .avatar - display block - min-width 54px - min-height 54px - max-width 54px - max-height 54px - margin 0 - border-radius 8px - transition all 0.1s ease - - > .content - - > .balloon - display block - padding 0 - max-width calc(100% - 16px) - min-height 38px - border-radius 16px - - &:before - content "" - pointer-events none - display block - position absolute - top 12px - - & + * - clear both - - &:hover - > .delete-button - display block - - > .delete-button - display none - position absolute - z-index 1 - top -4px - right -4px - margin 0 - padding 0 - cursor pointer - outline none - border none - border-radius 0 - box-shadow none - background transparent - - > img - vertical-align bottom - width 16px - height 16px - cursor pointer - - > .read - user-select none - display block - position absolute - z-index 1 - bottom -4px - left -12px - margin 0 - color rgba(0, 0, 0, 0.5) - font-size 11px - - > .content - - > .is-deleted - display block - margin 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1em - color rgba(0, 0, 0, 0.5) - - > .text - display block - margin 0 - padding 8px 16px - overflow hidden - overflow-wrap break-word - font-size 1em - color rgba(0, 0, 0, 0.8) - - & + .file - > a - border-radius 0 0 16px 16px - - > .file - > a - display block - max-width 100% - max-height 512px - border-radius 16px - overflow hidden - text-decoration none - - &:hover - text-decoration none - - > p - background #ccc - - > * - display block - margin 0 - width 100% - height 100% - - > p - padding 30px - text-align center - color #555 - background #ddd - - > .mk-url-preview - margin 8px 0 - - > footer - display block - margin 2px 0 0 0 - font-size 10px - color rgba(0, 0, 0, 0.4) - - > [data-fa] - margin-left 4px - - &:not([data-is-me]) - > .avatar-anchor - left 12px - - > .content - padding-left 66px - - > .balloon - float left - background #eee - - &[data-no-text] - background transparent - - &:not([data-no-text]):before - left -14px - border-top solid 8px transparent - border-right solid 8px #eee - border-bottom solid 8px transparent - border-left solid 8px transparent - - > footer - text-align left - - &[data-is-me] - > .avatar-anchor - right 12px - - > .content - padding-right 66px - - > .balloon - float right - background $me-balloon-color - - &[data-no-text] - background transparent - - &:not([data-no-text]):before - right -14px - left auto - border-top solid 8px transparent - border-right solid 8px transparent - border-bottom solid 8px transparent - border-left solid 8px $me-balloon-color - - > .content - - > p.is-deleted - color rgba(255, 255, 255, 0.5) - - > .text >>> - &, * - color #fff !important - - > footer - text-align right - - &[data-is-deleted] - > .baloon - opacity 0.5 - -</style> diff --git a/src/server/web/app/common/views/components/messaging-room.vue b/src/server/web/app/common/views/components/messaging-room.vue deleted file mode 100644 index d30c64d74a..0000000000 --- a/src/server/web/app/common/views/components/messaging-room.vue +++ /dev/null @@ -1,377 +0,0 @@ -<template> -<div class="mk-messaging-room" - @dragover.prevent.stop="onDragover" - @drop.prevent.stop="onDrop" -> - <div class="stream"> - <p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p> - <p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p> - <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p> - <button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> - <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }} - </button> - <template v-for="(message, i) in _messages"> - <x-message :message="message" :key="message.id"/> - <p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"> - <span>{{ _messages[i + 1]._datetext }}</span> - </p> - </template> - </div> - <footer> - <div ref="notifications" class="notifications"></div> - <x-form :user="user" ref="form"/> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { MessagingStream } from '../../scripts/streaming/messaging'; -import XMessage from './messaging-room.message.vue'; -import XForm from './messaging-room.form.vue'; -import { url } from '../../../config'; - -export default Vue.extend({ - components: { - XMessage, - XForm - }, - - props: ['user', 'isNaked'], - - data() { - return { - init: true, - fetchingMoreMessages: false, - messages: [], - existMoreMessages: false, - connection: null - }; - }, - - computed: { - _messages(): any[] { - return (this.messages as any).map(message => { - const date = new Date(message.createdAt).getDate(); - const month = new Date(message.createdAt).getMonth() + 1; - message._date = date; - message._datetext = `${month}月 ${date}日`; - return message; - }); - }, - - form(): any { - return this.$refs.form; - } - }, - - mounted() { - this.connection = new MessagingStream((this as any).os, (this as any).os.i, this.user.id); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - - document.addEventListener('visibilitychange', this.onVisibilitychange); - - this.fetchMessages().then(() => { - this.init = false; - this.scrollToBottom(); - }); - }, - - beforeDestroy() { - this.connection.off('message', this.onMessage); - this.connection.off('read', this.onRead); - this.connection.close(); - - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, - - methods: { - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - - if (isFile || isDriveFile) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - }, - - onDrop(e): void { - // ファイルだったら - if (e.dataTransfer.files.length == 1) { - this.form.upload(e.dataTransfer.files[0]); - return; - } else if (e.dataTransfer.files.length > 1) { - alert('メッセージに添付できるのはひとつのファイルのみです'); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.form.file = file; - } - //#endregion - }, - - fetchMessages() { - return new Promise((resolve, reject) => { - const max = this.existMoreMessages ? 20 : 10; - - (this as any).api('messaging/messages', { - userId: this.user.id, - limit: max + 1, - untilId: this.existMoreMessages ? this.messages[0].id : undefined - }).then(messages => { - if (messages.length == max + 1) { - this.existMoreMessages = true; - messages.pop(); - } else { - this.existMoreMessages = false; - } - - this.messages.unshift.apply(this.messages, messages.reverse()); - resolve(); - }); - }); - }, - - fetchMoreMessages() { - this.fetchingMoreMessages = true; - this.fetchMessages().then(() => { - this.fetchingMoreMessages = false; - }); - }, - - onMessage(message) { - // サウンドを再生する - if ((this as any).os.isEnableSounds) { - const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; - sound.play(); - } - - const isBottom = this.isBottom(); - - this.messages.push(message); - if (message.userId != (this as any).os.i.id && !document.hidden) { - this.connection.send({ - type: 'read', - id: message.id - }); - } - - if (isBottom) { - // Scroll to bottom - this.$nextTick(() => { - this.scrollToBottom(); - }); - } else if (message.userId != (this as any).os.i.id) { - // Notify - this.notify('%i18n:common.tags.mk-messaging-room.new-message%'); - } - }, - - onRead(ids) { - if (!Array.isArray(ids)) ids = [ids]; - ids.forEach(id => { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist].isRead = true; - } - }); - }, - - isBottom() { - const asobi = 64; - const current = this.isNaked - ? window.scrollY + window.innerHeight - : this.$el.scrollTop + this.$el.offsetHeight; - const max = this.isNaked - ? document.body.offsetHeight - : this.$el.scrollHeight; - return current > (max - asobi); - }, - - scrollToBottom() { - if (this.isNaked) { - window.scroll(0, document.body.offsetHeight); - } else { - this.$el.scrollTop = this.$el.scrollHeight; - } - }, - - notify(message) { - const n = document.createElement('p') as any; - n.innerHTML = '%fa:arrow-circle-down%' + message; - n.onclick = () => { - this.scrollToBottom(); - n.parentNode.removeChild(n); - }; - (this.$refs.notifications as any).appendChild(n); - - setTimeout(() => { - n.style.opacity = 0; - setTimeout(() => n.parentNode.removeChild(n), 1000); - }, 4000); - }, - - onVisibilitychange() { - if (document.hidden) return; - this.messages.forEach(message => { - if (message.userId !== (this as any).os.i.id && !message.isRead) { - this.connection.send({ - type: 'read', - id: message.id - }); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-messaging-room - display flex - flex 1 - flex-direction column - height 100% - - > .stream - width 100% - max-width 600px - margin 0 auto - flex 1 - - > .init - width 100% - margin 0 - padding 16px 8px 8px 8px - text-align center - font-size 0.8em - color rgba(0, 0, 0, 0.4) - - [data-fa] - margin-right 4px - - > .empty - width 100% - margin 0 - padding 16px 8px 8px 8px - text-align center - font-size 0.8em - color rgba(0, 0, 0, 0.4) - - [data-fa] - margin-right 4px - - > .no-history - display block - margin 0 - padding 16px - text-align center - font-size 0.8em - color rgba(0, 0, 0, 0.4) - - [data-fa] - margin-right 4px - - > .more - display block - margin 16px auto - padding 0 12px - line-height 24px - color #fff - background rgba(0, 0, 0, 0.3) - border-radius 12px - - &:hover - background rgba(0, 0, 0, 0.4) - - &:active - background rgba(0, 0, 0, 0.5) - - &.fetching - cursor wait - - > [data-fa] - margin-right 4px - - > .message - // something - - > .date - display block - margin 8px 0 - text-align center - - &:before - content '' - display block - position absolute - height 1px - width 90% - top 16px - left 0 - right 0 - margin 0 auto - background rgba(0, 0, 0, 0.1) - - > span - display inline-block - margin 0 - padding 0 16px - //font-weight bold - line-height 32px - color rgba(0, 0, 0, 0.3) - background #fff - - > footer - position -webkit-sticky - position sticky - z-index 2 - bottom 0 - width 100% - max-width 600px - margin 0 auto - padding 0 - background rgba(255, 255, 255, 0.95) - background-clip content-box - - > .notifications - position absolute - top -48px - width 100% - padding 8px 0 - text-align center - - &:empty - display none - - > p - display inline-block - margin 0 - padding 0 12px 0 28px - cursor pointer - line-height 32px - font-size 12px - color $theme-color-foreground - background $theme-color - border-radius 16px - transition opacity 1s ease - - > [data-fa] - position absolute - top 0 - left 10px - line-height 32px - font-size 16px - -</style> diff --git a/src/server/web/app/common/views/components/messaging.vue b/src/server/web/app/common/views/components/messaging.vue deleted file mode 100644 index 8317c3738a..0000000000 --- a/src/server/web/app/common/views/components/messaging.vue +++ /dev/null @@ -1,463 +0,0 @@ -<template> -<div class="mk-messaging" :data-compact="compact"> - <div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }"> - <div class="form"> - <label for="search-input">%fa:search%</label> - <input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:common.tags.mk-messaging.search-user%"/> - </div> - <div class="result"> - <ol class="users" v-if="result.length > 0" ref="searchResult"> - <li v-for="(user, i) in result" - @keydown.enter="navigate(user)" - @keydown="onSearchResultKeydown(i)" - @click="navigate(user)" - tabindex="-1" - > - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> - <span class="name">{{ user.name }}</span> - <span class="username">@{{ getAcct(user) }}</span> - </li> - </ol> - </div> - </div> - <div class="history" v-if="messages.length > 0"> - <template> - <a v-for="message in messages" - class="user" - :href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" - :data-is-me="isMe(message)" - :data-is-read="message.isRead" - @click.prevent="navigate(isMe(message) ? message.recipient : message.user)" - :key="message.id" - > - <div> - <img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/> - <header> - <span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span> - <span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span> - <mk-time :time="message.createdAt"/> - </header> - <div class="body"> - <p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ message.text }}</p> - </div> - </div> - </a> - </template> - </div> - <p class="no-history" v-if="!fetching && messages.length == 0">%i18n:common.tags.mk-messaging.no-history%</p> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: { - compact: { - type: Boolean, - default: false - }, - headerTop: { - type: Number, - default: 0 - } - }, - data() { - return { - fetching: true, - moreFetching: false, - messages: [], - q: null, - result: [], - connection: null, - connectionId: null - }; - }, - mounted() { - this.connection = (this as any).os.streams.messagingIndexStream.getConnection(); - this.connectionId = (this as any).os.streams.messagingIndexStream.use(); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - - (this as any).api('messaging/history').then(messages => { - this.messages = messages; - this.fetching = false; - }); - }, - beforeDestroy() { - this.connection.off('message', this.onMessage); - this.connection.off('read', this.onRead); - (this as any).os.streams.messagingIndexStream.dispose(this.connectionId); - }, - methods: { - getAcct, - isMe(message) { - return message.userId == (this as any).os.i.id; - }, - onMessage(message) { - this.messages = this.messages.filter(m => !( - (m.recipientId == message.recipientId && m.userId == message.userId) || - (m.recipientId == message.userId && m.userId == message.recipientId))); - - this.messages.unshift(message); - }, - onRead(ids) { - ids.forEach(id => { - const found = this.messages.find(m => m.id == id); - if (found) found.isRead = true; - }); - }, - search() { - if (this.q == '') { - this.result = []; - return; - } - (this as any).api('users/search', { - query: this.q, - max: 5 - }).then(users => { - this.result = users; - }); - }, - navigate(user) { - this.$emit('navigate', user); - }, - onSearchKeydown(e) { - switch (e.which) { - case 9: // [TAB] - case 40: // [↓] - e.preventDefault(); - e.stopPropagation(); - (this.$refs.searchResult as any).childNodes[0].focus(); - break; - } - }, - onSearchResultKeydown(i, e) { - const list = this.$refs.searchResult as any; - - const cancel = () => { - e.preventDefault(); - e.stopPropagation(); - }; - - switch (true) { - case e.which == 27: // [ESC] - cancel(); - (this.$refs.search as any).focus(); - break; - - case e.which == 9 && e.shiftKey: // [TAB] + [Shift] - case e.which == 38: // [↑] - cancel(); - (list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus(); - break; - - case e.which == 9: // [TAB] - case e.which == 40: // [↓] - cancel(); - (list.childNodes[i].nextElementSibling || list.childNodes[0]).focus(); - break; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-messaging - - &[data-compact] - font-size 0.8em - - > .history - > a - &:last-child - border-bottom none - - &:not([data-is-me]):not([data-is-read]) - > div - background-image none - border-left solid 4px #3aa2dc - - > div - padding 16px - - > header - > .mk-time - font-size 1em - - > .avatar - width 42px - height 42px - margin 0 12px 0 0 - - > .search - display block - position -webkit-sticky - position sticky - top 0 - left 0 - z-index 1 - width 100% - background #fff - box-shadow 0 0px 2px rgba(0, 0, 0, 0.2) - - > .form - padding 8px - background #f7f7f7 - - > label - display block - position absolute - top 0 - left 8px - z-index 1 - height 100% - width 38px - pointer-events none - - > [data-fa] - display block - position absolute - top 0 - right 0 - bottom 0 - left 0 - width 1em - line-height 56px - margin auto - color #555 - - > input - margin 0 - padding 0 0 0 32px - width 100% - font-size 1em - line-height 38px - color #000 - outline none - border solid 1px #eee - border-radius 5px - box-shadow none - transition color 0.5s ease, border 0.5s ease - - &:hover - border solid 1px #ddd - transition border 0.2s ease - - &:focus - color darken($theme-color, 20%) - border solid 1px $theme-color - transition color 0, border 0 - - > .result - display block - top 0 - left 0 - z-index 2 - width 100% - margin 0 - padding 0 - background #fff - - > .users - margin 0 - padding 0 - list-style none - - > li - display inline-block - z-index 1 - width 100% - padding 8px 32px - vertical-align top - white-space nowrap - overflow hidden - color rgba(0, 0, 0, 0.8) - text-decoration none - transition none - cursor pointer - - &:hover - &:focus - color #fff - background $theme-color - - .name - color #fff - - .username - color #fff - - &:active - color #fff - background darken($theme-color, 10%) - - .name - color #fff - - .username - color #fff - - .avatar - vertical-align middle - min-width 32px - min-height 32px - max-width 32px - max-height 32px - margin 0 8px 0 0 - border-radius 6px - - .name - margin 0 8px 0 0 - /*font-weight bold*/ - font-weight normal - color rgba(0, 0, 0, 0.8) - - .username - font-weight normal - color rgba(0, 0, 0, 0.3) - - > .history - - > a - display block - text-decoration none - background #fff - border-bottom solid 1px #eee - - * - pointer-events none - user-select none - - &:hover - background #fafafa - - > .avatar - filter saturate(200%) - - &:active - background #eee - - &[data-is-read] - &[data-is-me] - opacity 0.8 - - &:not([data-is-me]):not([data-is-read]) - > div - background-image url("/assets/unread.svg") - background-repeat no-repeat - background-position 0 center - - &:after - content "" - display block - clear both - - > div - max-width 500px - margin 0 auto - padding 20px 30px - - &:after - content "" - display block - clear both - - > header - display flex - align-items center - margin-bottom 2px - white-space nowrap - overflow hidden - - > .name - margin 0 - padding 0 - overflow hidden - text-overflow ellipsis - font-size 1em - color rgba(0, 0, 0, 0.9) - font-weight bold - transition all 0.1s ease - - > .username - margin 0 8px - color rgba(0, 0, 0, 0.5) - - > .mk-time - margin 0 0 0 auto - color rgba(0, 0, 0, 0.5) - font-size 80% - - > .avatar - float left - width 54px - height 54px - margin 0 16px 0 0 - border-radius 8px - transition all 0.1s ease - - > .body - - > .text - display block - margin 0 0 0 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1.1em - color rgba(0, 0, 0, 0.8) - - .me - color rgba(0, 0, 0, 0.4) - - > .image - display block - max-width 100% - max-height 512px - - > .no-history - margin 0 - padding 2em 1em - text-align center - color #999 - font-weight 500 - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - - // TODO: element base media query - @media (max-width 400px) - > .search - > .result - > .users - > li - padding 8px 16px - - > .history - > a - &:not([data-is-me]):not([data-is-read]) - > div - background-image none - border-left solid 4px #3aa2dc - - > div - padding 16px - font-size 14px - - > .avatar - margin 0 12px 0 0 - -</style> diff --git a/src/server/web/app/common/views/components/nav.vue b/src/server/web/app/common/views/components/nav.vue deleted file mode 100644 index 8ce75d3529..0000000000 --- a/src/server/web/app/common/views/components/nav.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<span class="mk-nav"> - <a :href="aboutUrl">%i18n:common.tags.mk-nav-links.about%</a> - <i>・</i> - <a :href="statsUrl">%i18n:common.tags.mk-nav-links.stats%</a> - <i>・</i> - <a :href="statusUrl">%i18n:common.tags.mk-nav-links.status%</a> - <i>・</i> - <a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a> - <i>・</i> - <a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a> - <i>・</i> - <a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a> - <i>・</i> - <a :href="devUrl">%i18n:common.tags.mk-nav-links.develop%</a> - <i>・</i> - <a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { docsUrl, statsUrl, statusUrl, devUrl, lang } from '../../../config'; - -export default Vue.extend({ - data() { - return { - aboutUrl: `${docsUrl}/${lang}/about`, - statsUrl, - statusUrl, - devUrl - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-nav - a - color inherit -</style> diff --git a/src/server/web/app/common/views/components/othello.game.vue b/src/server/web/app/common/views/components/othello.game.vue deleted file mode 100644 index f08742ad10..0000000000 --- a/src/server/web/app/common/views/components/othello.game.vue +++ /dev/null @@ -1,324 +0,0 @@ -<template> -<div class="root"> - <header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header> - - <div style="overflow: hidden"> - <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ turnUser.name }}のターンです<mk-ellipsis/></p> - <p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p> - <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">相手のターンです<mk-ellipsis/></p> - <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p> - <p class="result" v-if="game.isEnded && logPos == logs.length"> - <template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> - <template v-else>引き分け</template> - </p> - </div> - - <div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> - <div v-for="(stone, i) in o.board" - :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" - @click="set(i)" - :title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'" - > - <img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt=""> - <img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> - </div> - </div> - - <p class="status"><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p> - - <div class="player" v-if="game.isEnded"> - <el-button-group> - <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button> - <el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button> - </el-button-group> - <span>{{ logPos }} / {{ logs.length }}</span> - <el-button-group> - <el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button> - <el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button> - </el-button-group> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as CRC32 from 'crc-32'; -import Othello, { Color } from '../../../../../common/othello/core'; -import { url } from '../../../config'; - -export default Vue.extend({ - props: ['initGame', 'connection'], - - data() { - return { - game: null, - o: null as Othello, - logs: [], - logPos: 0, - pollingClock: null - }; - }, - - computed: { - iAmPlayer(): boolean { - if (!(this as any).os.isSignedIn) return false; - return this.game.user1Id == (this as any).os.i.id || this.game.user2Id == (this as any).os.i.id; - }, - myColor(): Color { - if (!this.iAmPlayer) return null; - if (this.game.user1Id == (this as any).os.i.id && this.game.black == 1) return true; - if (this.game.user2Id == (this as any).os.i.id && this.game.black == 2) return true; - return false; - }, - opColor(): Color { - if (!this.iAmPlayer) return null; - return this.myColor === true ? false : true; - }, - blackUser(): any { - return this.game.black == 1 ? this.game.user1 : this.game.user2; - }, - whiteUser(): any { - return this.game.black == 1 ? this.game.user2 : this.game.user1; - }, - turnUser(): any { - if (this.o.turn === true) { - return this.game.black == 1 ? this.game.user1 : this.game.user2; - } else if (this.o.turn === false) { - return this.game.black == 1 ? this.game.user2 : this.game.user1; - } else { - return null; - } - }, - isMyTurn(): boolean { - if (this.turnUser == null) return null; - return this.turnUser.id == (this as any).os.i.id; - } - }, - - watch: { - logPos(v) { - if (!this.game.isEnded) return; - this.o = new Othello(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard - }); - this.logs.forEach((log, i) => { - if (i < v) { - this.o.put(log.color, log.pos); - } - }); - this.$forceUpdate(); - } - }, - - created() { - this.game = this.initGame; - - this.o = new Othello(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard - }); - - this.game.logs.forEach(log => { - this.o.put(log.color, log.pos); - }); - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - // 通信を取りこぼしてもいいように定期的にポーリングさせる - if (this.game.isStarted && !this.game.isEnded) { - this.pollingClock = setInterval(() => { - const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join('')); - this.connection.send({ - type: 'check', - crc32 - }); - }, 3000); - } - }, - - mounted() { - this.connection.on('set', this.onSet); - this.connection.on('rescue', this.onRescue); - }, - - beforeDestroy() { - this.connection.off('set', this.onSet); - this.connection.off('rescue', this.onRescue); - - clearInterval(this.pollingClock); - }, - - methods: { - set(pos) { - if (this.game.isEnded) return; - if (!this.iAmPlayer) return; - if (!this.isMyTurn) return; - if (!this.o.canPut(this.myColor, pos)) return; - - this.o.put(this.myColor, pos); - - // サウンドを再生する - if ((this as any).os.isEnableSounds) { - const sound = new Audio(`${url}/assets/othello-put-me.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; - sound.play(); - } - - this.connection.send({ - type: 'set', - pos - }); - - this.checkEnd(); - - this.$forceUpdate(); - }, - - onSet(x) { - this.logs.push(x); - this.logPos++; - this.o.put(x.color, x.pos); - this.checkEnd(); - this.$forceUpdate(); - - // サウンドを再生する - if ((this as any).os.isEnableSounds && x.color != this.myColor) { - const sound = new Audio(`${url}/assets/othello-put-you.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; - sound.play(); - } - }, - - checkEnd() { - this.game.isEnded = this.o.isEnded; - if (this.game.isEnded) { - if (this.o.winner === true) { - this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id; - this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2; - } else if (this.o.winner === false) { - this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id; - this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1; - } else { - this.game.winnerId = null; - this.game.winner = null; - } - } - }, - - // 正しいゲーム情報が送られてきたとき - onRescue(game) { - this.game = game; - - this.o = new Othello(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard - }); - - this.game.logs.forEach(log => { - this.o.put(log.color, log.pos, true); - }); - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - this.checkEnd(); - this.$forceUpdate(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.root - text-align center - - > header - padding 8px - border-bottom dashed 1px #c4cdd4 - - > .board - display grid - grid-gap 4px - width 350px - height 350px - margin 0 auto - - > div - background transparent - border-radius 6px - overflow hidden - - * - pointer-events none - user-select none - - &.empty - border solid 2px #eee - - &.empty.can - background #eee - - &.empty.myTurn - border-color #ddd - - &.can - background #eee - cursor pointer - - &:hover - border-color darken($theme-color, 10%) - background $theme-color - - &:active - background darken($theme-color, 10%) - - &.prev - box-shadow 0 0 0 4px rgba($theme-color, 0.7) - - &.isEnded - border-color #ddd - - &.none - border-color transparent !important - - > img - display block - width 100% - height 100% - - > .graph - display grid - grid-template-columns repeat(61, 1fr) - width 300px - height 38px - margin 0 auto 16px auto - - > div - &:not(:empty) - background #ccc - - > div:first-child - background #333 - - > div:last-child - background #ccc - - > .status - margin 0 - padding 16px 0 - - > .player - padding-bottom 32px - - > span - display inline-block - margin 0 8px - min-width 70px -</style> diff --git a/src/server/web/app/common/views/components/othello.gameroom.vue b/src/server/web/app/common/views/components/othello.gameroom.vue deleted file mode 100644 index dba9ccd16d..0000000000 --- a/src/server/web/app/common/views/components/othello.gameroom.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<div> - <x-room v-if="!g.isStarted" :game="g" :connection="connection"/> - <x-game v-else :init-game="g" :connection="connection"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XGame from './othello.game.vue'; -import XRoom from './othello.room.vue'; -import { OthelloGameStream } from '../../scripts/streaming/othello-game'; - -export default Vue.extend({ - components: { - XGame, - XRoom - }, - props: ['game'], - data() { - return { - connection: null, - g: null - }; - }, - created() { - this.g = this.game; - this.connection = new OthelloGameStream((this as any).os, (this as any).os.i, this.game); - this.connection.on('started', this.onStarted); - }, - beforeDestroy() { - this.connection.off('started', this.onStarted); - this.connection.close(); - }, - methods: { - onStarted(game) { - Object.assign(this.g, game); - this.$forceUpdate(); - } - } -}); -</script> diff --git a/src/server/web/app/common/views/components/othello.room.vue b/src/server/web/app/common/views/components/othello.room.vue deleted file mode 100644 index a32be6b74f..0000000000 --- a/src/server/web/app/common/views/components/othello.room.vue +++ /dev/null @@ -1,297 +0,0 @@ -<template> -<div class="root"> - <header><b>{{ game.user1.name }}</b> vs <b>{{ game.user2.name }}</b></header> - - <div> - <p>ゲームの設定</p> - - <el-card class="map"> - <div slot="header"> - <el-select :class="$style.mapSelect" v-model="mapName" placeholder="マップを選択" @change="onMapChange"> - <el-option label="ランダム" :value="null"/> - <el-option-group v-for="c in mapCategories" :key="c" :label="c"> - <el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"> - <span style="float: left">{{ m.name }}</span> - <span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span> - </el-option> - </el-option-group> - </el-select> - </div> - <div :class="$style.board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.settings.map.join('')" - :data-none="x == ' '" - @click="onPixelClick(i, x)" - > - <template v-if="x == 'b'">%fa:circle%</template> - <template v-if="x == 'w'">%fa:circle R%</template> - </div> - </div> - </el-card> - - <el-card class="bw"> - <div slot="header"> - <span>先手/後手</span> - </div> - <el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio> - <el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ game.user1.name }}が黒</el-radio> - <el-radio v-model="game.settings.bw" :label="2" @change="updateSettings">{{ game.user2.name }}が黒</el-radio> - </el-card> - - <el-card class="rules"> - <div slot="header"> - <span>ルール</span> - </div> - <mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/> - <mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="ループマップ"/> - <mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="どこでも置けるモード"/> - </el-card> - - <el-card class="bot-form" v-if="form"> - <div slot="header"> - <span>Botの設定</span> - </div> - <el-alert v-for="message in messages" - :title="message.text" - :type="message.type" - :key="message.id" - /> - <template v-for="item in form"> - <mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch> - - <el-card v-if="item.type == 'radio'" :key="item.id"> - <div slot="header"> - <span>{{ item.label }}</span> - </div> - <el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio> - </el-card> - - <el-card v-if="item.type == 'textbox'" :key="item.id"> - <div slot="header"> - <span>{{ item.label }}</span> - </div> - <el-input v-model="item.value" @change="onChangeForm($event, item)"/> - </el-card> - </template> - </el-card> - </div> - - <footer> - <p class="status"> - <template v-if="isAccepted && isOpAccepted">ゲームは数秒後に開始されます<mk-ellipsis/></template> - <template v-if="isAccepted && !isOpAccepted">相手の準備が完了するのを待っています<mk-ellipsis/></template> - <template v-if="!isAccepted && isOpAccepted">あなたの準備が完了するのを待っています</template> - <template v-if="!isAccepted && !isOpAccepted">準備中<mk-ellipsis/></template> - </p> - - <div class="actions"> - <el-button @click="exit">キャンセル</el-button> - <el-button type="primary" @click="accept" v-if="!isAccepted">準備完了</el-button> - <el-button type="primary" @click="cancel" v-if="isAccepted">準備続行</el-button> - </div> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as maps from '../../../../../common/othello/maps'; - -export default Vue.extend({ - props: ['game', 'connection'], - - data() { - return { - o: null, - isLlotheo: false, - mapName: maps.eighteight.name, - maps: maps, - form: null, - messages: [] - }; - }, - - computed: { - mapCategories(): string[] { - const categories = Object.entries(maps).map(x => x[1].category); - return categories.filter((item, pos) => categories.indexOf(item) == pos); - }, - isAccepted(): boolean { - if (this.game.user1Id == (this as any).os.i.id && this.game.user1Accepted) return true; - if (this.game.user2Id == (this as any).os.i.id && this.game.user2Accepted) return true; - return false; - }, - isOpAccepted(): boolean { - if (this.game.user1Id != (this as any).os.i.id && this.game.user1Accepted) return true; - if (this.game.user2Id != (this as any).os.i.id && this.game.user2Accepted) return true; - return false; - } - }, - - created() { - this.connection.on('change-accepts', this.onChangeAccepts); - this.connection.on('update-settings', this.onUpdateSettings); - this.connection.on('init-form', this.onInitForm); - this.connection.on('message', this.onMessage); - - if (this.game.user1Id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1; - if (this.game.user2Id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2; - }, - - beforeDestroy() { - this.connection.off('change-accepts', this.onChangeAccepts); - this.connection.off('update-settings', this.onUpdateSettings); - this.connection.off('init-form', this.onInitForm); - this.connection.off('message', this.onMessage); - }, - - methods: { - exit() { - - }, - - accept() { - this.connection.send({ - type: 'accept' - }); - }, - - cancel() { - this.connection.send({ - type: 'cancel-accept' - }); - }, - - onChangeAccepts(accepts) { - this.game.user1Accepted = accepts.user1; - this.game.user2Accepted = accepts.user2; - this.$forceUpdate(); - }, - - updateSettings() { - this.connection.send({ - type: 'update-settings', - settings: this.game.settings - }); - }, - - onUpdateSettings(settings) { - this.game.settings = settings; - if (this.game.settings.map == null) { - this.mapName = null; - } else { - const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join('')); - this.mapName = foundMap ? foundMap[1].name : '-Custom-'; - } - }, - - onInitForm(x) { - if (x.userId == (this as any).os.i.id) return; - this.form = x.form; - }, - - onMessage(x) { - if (x.userId == (this as any).os.i.id) return; - this.messages.unshift(x.message); - }, - - onChangeForm(v, item) { - this.connection.send({ - type: 'update-form', - id: item.id, - value: v - }); - }, - - onMapChange(v) { - if (v == null) { - this.game.settings.map = null; - } else { - this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data; - } - this.$forceUpdate(); - this.updateSettings(); - }, - - onPixelClick(pos, pixel) { - const x = pos % this.game.settings.map[0].length; - const y = Math.floor(pos / this.game.settings.map[0].length); - const newPixel = - pixel == ' ' ? '-' : - pixel == '-' ? 'b' : - pixel == 'b' ? 'w' : - ' '; - const line = this.game.settings.map[y].split(''); - line[x] = newPixel; - this.$set(this.game.settings.map, y, line.join('')); - this.$forceUpdate(); - this.updateSettings(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.root - text-align center - background #f9f9f9 - - > header - padding 8px - border-bottom dashed 1px #c4cdd4 - - > div - padding 0 16px - - > .map - > .bw - > .rules - > .bot-form - max-width 400px - margin 0 auto 16px auto - - > footer - position sticky - bottom 0 - padding 16px - background rgba(255, 255, 255, 0.9) - border-top solid 1px #c4cdd4 - - > .status - margin 0 0 16px 0 -</style> - -<style lang="stylus" module> -.mapSelect - width 100% - -.board - display grid - grid-gap 4px - width 300px - height 300px - margin 0 auto - - > div - background transparent - border solid 2px #ddd - border-radius 6px - overflow hidden - cursor pointer - - * - pointer-events none - user-select none - width 100% - height 100% - - &[data-none] - border-color transparent - -</style> - -<style lang="stylus"> -.el-alert__content - position initial !important -</style> diff --git a/src/server/web/app/common/views/components/othello.vue b/src/server/web/app/common/views/components/othello.vue deleted file mode 100644 index 8f7d9dfd6a..0000000000 --- a/src/server/web/app/common/views/components/othello.vue +++ /dev/null @@ -1,311 +0,0 @@ -<template> -<div class="mk-othello"> - <div v-if="game"> - <x-gameroom :game="game"/> - </div> - <div class="matching" v-else-if="matching"> - <h1><b>{{ matching.name }}</b>を待っています<mk-ellipsis/></h1> - <div class="cancel"> - <el-button round @click="cancel">キャンセル</el-button> - </div> - </div> - <div class="index" v-else> - <h1>Misskey %fa:circle%thell%fa:circle R%</h1> - <p>他のMisskeyユーザーとオセロで対戦しよう</p> - <div class="play"> - <el-button round>フリーマッチ(準備中)</el-button> - <el-button type="primary" round @click="match">指名</el-button> - <details> - <summary>遊び方</summary> - <div> - <p>オセロは、相手と交互に石をボードに置いてゆき、相手の石を挟んでひっくり返しながら、最終的に残った石が多い方が勝ちというボードゲームです。</p> - <dl> - <dt><b>フリーマッチ</b></dt> - <dd>ランダムなユーザーと対戦するモードです。</dd> - <dt><b>指名</b></dt> - <dd>指定したユーザーと対戦するモードです。</dd> - </dl> - </div> - </details> - </div> - <section v-if="invitations.length > 0"> - <h2>対局の招待があります!:</h2> - <div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)"> - <img :src="`${i.parent.avatarUrl}?thumbnail&size=32`" alt=""> - <span class="name"><b>{{ i.parent.name }}</b></span> - <span class="username">@{{ i.parent.username }}</span> - <mk-time :time="i.createdAt"/> - </div> - </section> - <section v-if="myGames.length > 0"> - <h2>自分の対局</h2> - <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`"> - <img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt=""> - <img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt=""> - <span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span> - <span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span> - </a> - </section> - <section v-if="games.length > 0"> - <h2>みんなの対局</h2> - <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`"> - <img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt=""> - <img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt=""> - <span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span> - <span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span> - </a> - </section> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XGameroom from './othello.gameroom.vue'; - -export default Vue.extend({ - components: { - XGameroom - }, - props: ['initGame'], - data() { - return { - game: null, - games: [], - gamesFetching: true, - gamesMoreFetching: false, - myGames: [], - matching: null, - invitations: [], - connection: null, - connectionId: null, - pingClock: null - }; - }, - watch: { - game(g) { - this.$emit('gamed', g); - } - }, - created() { - if (this.initGame) { - this.game = this.initGame; - } - }, - mounted() { - this.connection = (this as any).os.streams.othelloStream.getConnection(); - this.connectionId = (this as any).os.streams.othelloStream.use(); - - this.connection.on('matched', this.onMatched); - this.connection.on('invited', this.onInvited); - - (this as any).api('othello/games', { - my: true - }).then(games => { - this.myGames = games; - }); - - (this as any).api('othello/games').then(games => { - this.games = games; - this.gamesFetching = false; - }); - - (this as any).api('othello/invitations').then(invitations => { - this.invitations = this.invitations.concat(invitations); - }); - - this.pingClock = setInterval(() => { - if (this.matching) { - this.connection.send({ - type: 'ping', - id: this.matching.id - }); - } - }, 3000); - }, - beforeDestroy() { - this.connection.off('matched', this.onMatched); - this.connection.off('invited', this.onInvited); - (this as any).os.streams.othelloStream.dispose(this.connectionId); - - clearInterval(this.pingClock); - }, - methods: { - go(game) { - (this as any).api('othello/games/show', { - gameId: game.id - }).then(game => { - this.matching = null; - this.game = game; - }); - }, - match() { - (this as any).apis.input({ - title: 'ユーザー名を入力してください' - }).then(username => { - (this as any).api('users/show', { - username - }).then(user => { - (this as any).api('othello/match', { - userId: user.id - }).then(res => { - if (res == null) { - this.matching = user; - } else { - this.game = res; - } - }); - }); - }); - }, - cancel() { - this.matching = null; - (this as any).api('othello/match/cancel'); - }, - accept(invitation) { - (this as any).api('othello/match', { - userId: invitation.parent.id - }).then(game => { - if (game) { - this.matching = null; - this.game = game; - } - }); - }, - onMatched(game) { - this.matching = null; - this.game = game; - }, - onInvited(invite) { - this.invitations.unshift(invite); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-othello - color #677f84 - background #fff - - > .matching - > h1 - margin 0 - padding 24px - font-size 20px - text-align center - font-weight normal - - > .cancel - margin 0 auto - padding 24px 0 0 0 - max-width 200px - text-align center - border-top dashed 1px #c4cdd4 - - > .index - > h1 - margin 0 - padding 24px - font-size 24px - text-align center - font-weight normal - color #fff - background linear-gradient(to bottom, #8bca3e, #d6cf31) - - & + p - margin 0 - padding 12px - margin-bottom 12px - text-align center - font-size 14px - border-bottom solid 1px #d3d9dc - - > .play - margin 0 auto - padding 0 16px - max-width 500px - text-align center - - > details - margin 8px 0 - - > div - padding 16px - font-size 14px - text-align left - background #f5f5f5 - border-radius 8px - - > section - margin 0 auto - padding 0 16px 16px 16px - max-width 500px - border-top solid 1px #d3d9dc - - > h2 - margin 0 - padding 16px 0 8px 0 - font-size 16px - font-weight bold - - .invitation - margin 8px 0 - padding 8px - border solid 1px #e1e5e8 - border-radius 6px - cursor pointer - - * - pointer-events none - user-select none - - &:focus - border-color $theme-color - - &:hover - background #f5f5f5 - - &:active - background #eee - - > img - vertical-align bottom - border-radius 100% - - > span - margin 0 8px - line-height 32px - - .game - display block - margin 8px 0 - padding 8px - color #677f84 - border solid 1px #e1e5e8 - border-radius 6px - cursor pointer - - * - pointer-events none - user-select none - - &:focus - border-color $theme-color - - &:hover - background #f5f5f5 - - &:active - background #eee - - > img - vertical-align bottom - border-radius 100% - - > span - margin 0 8px - line-height 32px -</style> diff --git a/src/server/web/app/common/views/components/poll-editor.vue b/src/server/web/app/common/views/components/poll-editor.vue deleted file mode 100644 index 47d901d7b1..0000000000 --- a/src/server/web/app/common/views/components/poll-editor.vue +++ /dev/null @@ -1,142 +0,0 @@ -<template> -<div class="mk-poll-editor"> - <p class="caution" v-if="choices.length < 2"> - %fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice% - </p> - <ul ref="choices"> - <li v-for="(choice, i) in choices"> - <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1)"> - <button @click="remove(i)" title="%i18n:common.tags.mk-poll-editor.remove%"> - %fa:times% - </button> - </li> - </ul> - <button class="add" v-if="choices.length < 10" @click="add">%i18n:common.tags.mk-poll-editor.add%</button> - <button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%"> - %fa:times% - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - choices: ['', ''] - }; - }, - watch: { - choices() { - this.$emit('updated'); - } - }, - methods: { - onInput(i, e) { - Vue.set(this.choices, i, e.target.value); - }, - - add() { - this.choices.push(''); - this.$nextTick(() => { - (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); - }); - }, - - remove(i) { - this.choices = this.choices.filter((_, _i) => _i != i); - }, - - destroy() { - this.$emit('destroyed'); - }, - - get() { - return { - choices: this.choices.filter(choice => choice != '') - } - }, - - set(data) { - if (data.choices.length == 0) return; - this.choices = data.choices; - if (data.choices.length == 1) this.choices = this.choices.concat(''); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-poll-editor - padding 8px - - > .caution - margin 0 0 8px 0 - font-size 0.8em - color #f00 - - > [data-fa] - margin-right 4px - - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 8px 0 - padding 0 - width 100% - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > input - padding 6px 8px - width 300px - font-size 14px - border solid 1px rgba($theme-color, 0.1) - border-radius 4px - - &:hover - border-color rgba($theme-color, 0.2) - - &:focus - border-color rgba($theme-color, 0.5) - - > button - padding 4px 8px - color rgba($theme-color, 0.4) - - &:hover - color rgba($theme-color, 0.6) - - &:active - color darken($theme-color, 30%) - - > .add - margin 8px 0 0 0 - vertical-align top - color $theme-color - - > .destroy - position absolute - top 0 - right 0 - padding 4px 8px - color rgba($theme-color, 0.4) - - &:hover - color rgba($theme-color, 0.6) - - &:active - color darken($theme-color, 30%) - -</style> diff --git a/src/server/web/app/common/views/components/poll.vue b/src/server/web/app/common/views/components/poll.vue deleted file mode 100644 index 711d89720e..0000000000 --- a/src/server/web/app/common/views/components/poll.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<div class="mk-poll" :data-is-voted="isVoted"> - <ul> - <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''"> - <div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div> - <span> - <template v-if="choice.isVoted">%fa:check%</template> - <span>{{ choice.text }}</span> - <span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span> - </span> - </li> - </ul> - <p v-if="total > 0"> - <span>{{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }}</span> - <span>・</span> - <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }}</a> - <span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['post'], - data() { - return { - showResult: false - }; - }, - computed: { - poll(): any { - return this.post.poll; - }, - total(): number { - return this.poll.choices.reduce((a, b) => a + b.votes, 0); - }, - isVoted(): boolean { - return this.poll.choices.some(c => c.isVoted); - } - }, - created() { - this.showResult = this.isVoted; - }, - methods: { - toggleShowResult() { - this.showResult = !this.showResult; - }, - vote(id) { - if (this.poll.choices.some(c => c.isVoted)) return; - (this as any).api('posts/polls/vote', { - postId: this.post.id, - choice: id - }).then(() => { - this.poll.choices.forEach(c => { - if (c.id == id) { - c.votes++; - Vue.set(c, 'isVoted', true); - } - }); - this.showResult = true; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-poll - - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 4px 0 - padding 4px 8px - width 100% - border solid 1px #eee - border-radius 4px - overflow hidden - cursor pointer - - &:hover - background rgba(0, 0, 0, 0.05) - - &:active - background rgba(0, 0, 0, 0.1) - - > .backdrop - position absolute - top 0 - left 0 - height 100% - background $theme-color - transition width 1s ease - - > span - > [data-fa] - margin-right 4px - - > .votes - margin-left 4px - - > p - a - color inherit - - &[data-is-voted] - > ul > li - cursor default - - &:hover - background transparent - - &:active - background transparent - -</style> diff --git a/src/server/web/app/common/views/components/post-html.ts b/src/server/web/app/common/views/components/post-html.ts deleted file mode 100644 index 98da86617d..0000000000 --- a/src/server/web/app/common/views/components/post-html.ts +++ /dev/null @@ -1,137 +0,0 @@ -import Vue from 'vue'; -import * as emojilib from 'emojilib'; -import getAcct from '../../../../../common/user/get-acct'; -import { url } from '../../../config'; -import MkUrl from './url.vue'; - -const flatten = list => list.reduce( - (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] -); - -export default Vue.component('mk-post-html', { - props: { - ast: { - type: Array, - required: true - }, - shouldBreak: { - type: Boolean, - default: true - }, - i: { - type: Object, - default: null - } - }, - render(createElement) { - const els = flatten((this as any).ast.map(token => { - switch (token.type) { - case 'text': - const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); - - if ((this as any).shouldBreak) { - const x = text.split('\n') - .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); - x[x.length - 1].pop(); - return x; - } else { - return createElement('span', text.replace(/\n/g, ' ')); - } - - case 'bold': - return createElement('strong', token.bold); - - case 'url': - return createElement(MkUrl, { - props: { - url: token.content, - target: '_blank' - } - }); - - case 'link': - return createElement('a', { - attrs: { - class: 'link', - href: token.url, - target: '_blank', - title: token.url - } - }, token.title); - - case 'mention': - return (createElement as any)('a', { - attrs: { - href: `${url}/@${getAcct(token)}`, - target: '_blank', - dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token) - }, - directives: [{ - name: 'user-preview', - value: token.content - }] - }, token.content); - - case 'hashtag': - return createElement('a', { - attrs: { - href: `${url}/search?q=${token.content}`, - target: '_blank' - } - }, token.content); - - case 'code': - return createElement('pre', [ - createElement('code', { - domProps: { - innerHTML: token.html - } - }) - ]); - - case 'inline-code': - return createElement('code', token.html); - - case 'quote': - const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); - - if ((this as any).shouldBreak) { - const x = text2.split('\n') - .map(t => [createElement('span', t), createElement('br')]); - x[x.length - 1].pop(); - return createElement('div', { - attrs: { - class: 'quote' - } - }, x); - } else { - return createElement('span', { - attrs: { - class: 'quote' - } - }, text2.replace(/\n/g, ' ')); - } - - case 'emoji': - const emoji = emojilib.lib[token.emoji]; - return createElement('span', emoji ? emoji.char : token.content); - - default: - console.log('unknown ast type:', token.type); - } - })); - - const _els = []; - els.forEach((el, i) => { - if (el.tag == 'br') { - if (els[i - 1].tag != 'div') { - _els.push(el); - } - } else { - _els.push(el); - } - }); - - return createElement('span', _els); - } -}); diff --git a/src/server/web/app/common/views/components/post-menu.vue b/src/server/web/app/common/views/components/post-menu.vue deleted file mode 100644 index 35116db7e2..0000000000 --- a/src/server/web/app/common/views/components/post-menu.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<div class="mk-post-menu"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ compact }" ref="popover"> - <button v-if="post.userId == os.i.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: ['post', 'source', 'compact'], - mounted() { - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - if (this.compact) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - popover.style.left = (x - (width / 2)) + 'px'; - popover.style.top = (y - (height / 2)) + 'px'; - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - popover.style.left = (x - (width / 2)) + 'px'; - popover.style.top = y + 'px'; - } - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - }, - methods: { - pin() { - (this as any).api('i/pin', { - postId: this.post.id - }).then(() => { - this.$destroy(); - }); - }, - - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: 200, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: 200, - easing: 'easeInBack', - complete: () => this.$destroy() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -$border-color = rgba(27, 31, 35, 0.15) - -.mk-post-menu - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background rgba(0, 0, 0, 0.1) - opacity 0 - - > .popover - position absolute - z-index 10001 - background #fff - border 1px solid $border-color - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - $balloon-size = 16px - - &:not(.compact) - margin-top $balloon-size - transform-origin center -($balloon-size) - - &:before - content "" - display block - position absolute - top -($balloon-size * 2) - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size $border-color - - &:after - content "" - display block - position absolute - top -($balloon-size * 2) + 1.5px - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size #fff - - > button - display block - padding 16px - -</style> diff --git a/src/server/web/app/common/views/components/reaction-icon.vue b/src/server/web/app/common/views/components/reaction-icon.vue deleted file mode 100644 index 7d24f4f9e9..0000000000 --- a/src/server/web/app/common/views/components/reaction-icon.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<span class="mk-reaction-icon"> - <img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"> - <img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"> - <img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"> - <img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"> - <img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"> - <img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"> - <img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"> - <img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"> - <img v-if="reaction == 'pudding'" src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['reaction'] -}); -</script> - -<style lang="stylus" scoped> -.mk-reaction-icon - img - vertical-align middle - width 1em - height 1em -</style> diff --git a/src/server/web/app/common/views/components/reaction-picker.vue b/src/server/web/app/common/views/components/reaction-picker.vue deleted file mode 100644 index bcb6b2b965..0000000000 --- a/src/server/web/app/common/views/components/reaction-picker.vue +++ /dev/null @@ -1,191 +0,0 @@ -<template> -<div class="mk-reaction-picker"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ compact }" ref="popover"> - <p v-if="!compact">{{ title }}</p> - <div> - <button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button> - <button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button> - <button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button> - <button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button> - <button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button> - <button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button> - <button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button> - <button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button> - <button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%'; - -export default Vue.extend({ - props: ['post', 'source', 'compact', 'cb'], - data() { - return { - title: placeholder - }; - }, - mounted() { - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - if (this.compact) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - popover.style.left = (x - (width / 2)) + 'px'; - popover.style.top = (y - (height / 2)) + 'px'; - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - popover.style.left = (x - (width / 2)) + 'px'; - popover.style.top = y + 'px'; - } - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - }, - methods: { - react(reaction) { - (this as any).api('posts/reactions/create', { - postId: this.post.id, - reaction: reaction - }).then(() => { - if (this.cb) this.cb(); - this.$destroy(); - }); - }, - onMouseover(e) { - this.title = e.target.title; - }, - onMouseout(e) { - this.title = placeholder; - }, - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: 200, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: 200, - easing: 'easeInBack', - complete: () => this.$destroy() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -$border-color = rgba(27, 31, 35, 0.15) - -.mk-reaction-picker - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background rgba(0, 0, 0, 0.1) - opacity 0 - - > .popover - position absolute - z-index 10001 - background #fff - border 1px solid $border-color - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - $balloon-size = 16px - - &:not(.compact) - margin-top $balloon-size - transform-origin center -($balloon-size) - - &:before - content "" - display block - position absolute - top -($balloon-size * 2) - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size $border-color - - &:after - content "" - display block - position absolute - top -($balloon-size * 2) + 1.5px - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size #fff - - > p - display block - margin 0 - padding 8px 10px - font-size 14px - color #586069 - border-bottom solid 1px #e1e4e8 - - > div - padding 4px - width 240px - text-align center - - > button - padding 0 - width 40px - height 40px - font-size 24px - border-radius 2px - - &:hover - background #eee - - &:active - background $theme-color - box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15) - -</style> diff --git a/src/server/web/app/common/views/components/reactions-viewer.vue b/src/server/web/app/common/views/components/reactions-viewer.vue deleted file mode 100644 index 246451008f..0000000000 --- a/src/server/web/app/common/views/components/reactions-viewer.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<div class="mk-reactions-viewer"> - <template v-if="reactions"> - <span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{{ reactions.like }}</span></span> - <span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{{ reactions.love }}</span></span> - <span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{{ reactions.laugh }}</span></span> - <span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{{ reactions.hmm }}</span></span> - <span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{{ reactions.surprise }}</span></span> - <span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{{ reactions.congrats }}</span></span> - <span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{{ reactions.angry }}</span></span> - <span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{{ reactions.confused }}</span></span> - <span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{{ reactions.pudding }}</span></span> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['post'], - computed: { - reactions(): number { - return this.post.reactionCounts; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-reactions-viewer - border-top dashed 1px #eee - border-bottom dashed 1px #eee - margin 4px 0 - - &:empty - display none - - > span - margin-right 8px - - > .mk-reaction-icon - font-size 1.4em - - > span - margin-left 4px - font-size 1.2em - color #444 - -</style> diff --git a/src/server/web/app/common/views/components/signin.vue b/src/server/web/app/common/views/components/signin.vue deleted file mode 100644 index 17154e6b31..0000000000 --- a/src/server/web/app/common/views/components/signin.vue +++ /dev/null @@ -1,142 +0,0 @@ -<template> -<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit"> - <label class="user-name"> - <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at% - </label> - <label class="password"> - <input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required/>%fa:lock% - </label> - <label class="token" v-if="user && user.account.twoFactorEnabled"> - <input v-model="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required/>%fa:lock% - </label> - <button type="submit" :disabled="signing">{{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }}</button> - もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { apiUrl } from '../../../config'; - -export default Vue.extend({ - data() { - return { - signing: false, - user: null, - username: '', - password: '', - token: '', - apiUrl, - }; - }, - methods: { - onUsernameChange() { - (this as any).api('users/show', { - username: this.username - }).then(user => { - this.user = user; - }); - }, - onSubmit() { - this.signing = true; - - (this as any).api('signin', { - username: this.username, - password: this.password, - token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined - }).then(() => { - location.reload(); - }).catch(() => { - alert('something happened'); - this.signing = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-signin - &.signing - &, * - cursor wait !important - - label - display block - margin 12px 0 - - [data-fa] - display block - pointer-events none - position absolute - bottom 0 - top 0 - left 0 - z-index 1 - margin auto - padding 0 16px - height 1em - color #898786 - - input[type=text] - input[type=password] - input[type=number] - user-select text - display inline-block - cursor auto - padding 0 0 0 38px - margin 0 - width 100% - line-height 44px - font-size 1em - color rgba(0, 0, 0, 0.7) - background #fff - outline none - border solid 1px #eee - border-radius 4px - - &:hover - background rgba(255, 255, 255, 0.7) - border-color #ddd - - & + i - color #797776 - - &:focus - background #fff - border-color #ccc - - & + i - color #797776 - - [type=submit] - cursor pointer - padding 16px - margin -6px 0 0 0 - width 100% - font-size 1.2em - color rgba(0, 0, 0, 0.5) - outline none - border none - border-radius 0 - background transparent - transition all .5s ease - - &:hover - color $theme-color - transition all .2s ease - - &:focus - color $theme-color - transition all .2s ease - - &:active - color darken($theme-color, 30%) - transition all .2s ease - - &:disabled - opacity 0.7 - -</style> diff --git a/src/server/web/app/common/views/components/signup.vue b/src/server/web/app/common/views/components/signup.vue deleted file mode 100644 index e77d849ade..0000000000 --- a/src/server/web/app/common/views/components/signup.vue +++ /dev/null @@ -1,287 +0,0 @@ -<template> -<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off"> - <label class="username"> - <p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p> - <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/> - <p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p> - <p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p> - <p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p> - <p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p> - <p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p> - <p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p> - <p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p> - <p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p> - </label> - <label class="password"> - <p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p> - <input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required @input="onChangePassword"/> - <div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> - <div class="value" ref="passwordMetar"></div> - </div> - <p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p> - <p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p> - <p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p> - </label> - <label class="retype-password"> - <p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p> - <input v-model="retypedPassword" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/> - <p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p> - <p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p> - </label> - <label class="recaptcha"> - <p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p> - <div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div> - </label> - <label class="agree-tou"> - <input name="agree-tou" type="checkbox" autocomplete="off" required/> - <p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> - </label> - <button type="submit">%i18n:common.tags.mk-signup.create%</button> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; -const getPasswordStrength = require('syuilo-password-strength'); -import { url, docsUrl, lang, recaptchaSitekey } from '../../../config'; - -export default Vue.extend({ - data() { - return { - username: '', - password: '', - retypedPassword: '', - url, - touUrl: `${docsUrl}/${lang}/tou`, - recaptchaSitekey, - recaptchaed: false, - usernameState: null, - passwordStrength: '', - passwordRetypeState: null - } - }, - computed: { - shouldShowProfileUrl(): boolean { - return (this.username != '' && - this.usernameState != 'invalid-format' && - this.usernameState != 'min-range' && - this.usernameState != 'max-range'); - } - }, - methods: { - onChangeUsername() { - if (this.username == '') { - this.usernameState = null; - return; - } - - const err = - !this.username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' : - this.username.length < 3 ? 'min-range' : - this.username.length > 20 ? 'max-range' : - null; - - if (err) { - this.usernameState = err; - return; - } - - this.usernameState = 'wait'; - - (this as any).api('username/available', { - username: this.username - }).then(result => { - this.usernameState = result.available ? 'ok' : 'unavailable'; - }).catch(err => { - this.usernameState = 'error'; - }); - }, - onChangePassword() { - if (this.password == '') { - this.passwordStrength = ''; - return; - } - - const strength = getPasswordStrength(this.password); - this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; - (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; - }, - onChangePasswordRetype() { - if (this.retypedPassword == '') { - this.passwordRetypeState = null; - return; - } - - this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match'; - }, - onSubmit() { - (this as any).api('signup', { - username: this.username, - password: this.password, - 'g-recaptcha-response': (window as any).grecaptcha.getResponse() - }).then(() => { - (this as any).api('signin', { - username: this.username, - password: this.password - }).then(() => { - location.href = '/'; - }); - }).catch(() => { - alert('%i18n:common.tags.mk-signup.some-error%'); - - (window as any).grecaptcha.reset(); - this.recaptchaed = false; - }); - } - }, - created() { - (window as any).onRecaptchaed = () => { - this.recaptchaed = true; - }; - - (window as any).onRecaptchaExpired = () => { - this.recaptchaed = false; - }; - }, - mounted() { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); - head.appendChild(script); - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-signup - min-width 302px - - label - display block - margin 0 0 16px 0 - - > .caption - margin 0 0 4px 0 - color #828888 - font-size 0.95em - - > [data-fa] - margin-right 0.25em - color #96adac - - > .info - display block - margin 4px 0 - font-size 0.8em - - > [data-fa] - margin-right 0.3em - - &.username - .profile-page-url-preview - display block - margin 4px 8px 0 4px - font-size 0.8em - color #888 - - &:empty - display none - - &:not(:empty) + .info - margin-top 0 - - &.password - .meter - display block - margin-top 8px - width 100% - height 8px - - &[data-strength=''] - display none - - &[data-strength='low'] - > .value - background #d73612 - - &[data-strength='medium'] - > .value - background #d7ca12 - - &[data-strength='high'] - > .value - background #61bb22 - - > .value - display block - width 0% - height 100% - background transparent - border-radius 4px - transition all 0.1s ease - - [type=text], [type=password] - user-select text - display inline-block - cursor auto - padding 0 12px - margin 0 - width 100% - line-height 44px - font-size 1em - color #333 !important - background #fff !important - outline none - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - box-shadow 0 0 0 114514px #fff inset - transition all .3s ease - - &:hover - border-color rgba(0, 0, 0, 0.2) - transition all .1s ease - - &:focus - color $theme-color !important - border-color $theme-color - box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%) - transition all 0s ease - - &:disabled - opacity 0.5 - - .agree-tou - padding 4px - border-radius 4px - - &:hover - background #f4f4f4 - - &:active - background #eee - - &, * - cursor pointer - - p - display inline - color #555 - - button - margin 0 - padding 16px - width 100% - font-size 1em - color #fff - background $theme-color - border-radius 3px - - &:hover - background lighten($theme-color, 5%) - - &:active - background darken($theme-color, 5%) - -</style> diff --git a/src/server/web/app/common/views/components/special-message.vue b/src/server/web/app/common/views/components/special-message.vue deleted file mode 100644 index 2fd4d6515e..0000000000 --- a/src/server/web/app/common/views/components/special-message.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<div class="mk-special-message"> - <p v-if="m == 1 && d == 1">%i18n:common.tags.mk-special-message.new-year%</p> - <p v-if="m == 12 && d == 25">%i18n:common.tags.mk-special-message.christmas%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - now: new Date() - }; - }, - computed: { - d(): number { - return this.now.getDate(); - }, - m(): number { - return this.now.getMonth() + 1; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-special-message - &:empty - display none - - > p - margin 0 - padding 4px - text-align center - font-size 14px - font-weight bold - text-transform uppercase - color #fff - background #ff1036 - -</style> diff --git a/src/server/web/app/common/views/components/stream-indicator.vue b/src/server/web/app/common/views/components/stream-indicator.vue deleted file mode 100644 index 1f18fa76ed..0000000000 --- a/src/server/web/app/common/views/components/stream-indicator.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="mk-stream-indicator"> - <p v-if=" stream.state == 'initializing' "> - %fa:spinner .pulse% - <span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span> - </p> - <p v-if=" stream.state == 'reconnecting' "> - %fa:spinner .pulse% - <span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span> - </p> - <p v-if=" stream.state == 'connected' "> - %fa:check% - <span>%i18n:common.tags.mk-stream-indicator.connected%</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - computed: { - stream() { - return (this as any).os.stream; - } - }, - created() { - (this as any).os.stream.on('_connected_', this.onConnected); - (this as any).os.stream.on('_disconnected_', this.onDisconnected); - - this.$nextTick(() => { - if (this.stream.state == 'connected') { - this.$el.style.opacity = '0'; - } - }); - }, - beforeDestroy() { - (this as any).os.stream.off('_connected_', this.onConnected); - (this as any).os.stream.off('_disconnected_', this.onDisconnected); - }, - methods: { - onConnected() { - setTimeout(() => { - anime({ - targets: this.$el, - opacity: 0, - easing: 'linear', - duration: 200 - }); - }, 1000); - }, - onDisconnected() { - anime({ - targets: this.$el, - opacity: 1, - easing: 'linear', - duration: 100 - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-stream-indicator - pointer-events none - position fixed - z-index 16384 - bottom 8px - right 8px - margin 0 - padding 6px 12px - font-size 0.9em - color #fff - background rgba(0, 0, 0, 0.8) - border-radius 4px - - > p - display block - margin 0 - - > [data-fa] - margin-right 0.25em - -</style> diff --git a/src/server/web/app/common/views/components/switch.vue b/src/server/web/app/common/views/components/switch.vue deleted file mode 100644 index 19a4adc3de..0000000000 --- a/src/server/web/app/common/views/components/switch.vue +++ /dev/null @@ -1,190 +0,0 @@ -<template> -<div - class="mk-switch" - :class="{ disabled, checked }" - role="switch" - :aria-checked="checked" - :aria-disabled="disabled" - @click="switchValue" - @mouseover="mouseenter" -> - <input - type="checkbox" - @change="handleChange" - ref="input" - :disabled="disabled" - @keydown.enter="switchValue" - > - <span class="button"> - <span :style="{ transform }"></span> - </span> - <span class="label"> - <span :aria-hidden="!checked">{{ text }}</span> - <p :aria-hidden="!checked"> - <slot></slot> - </p> - </span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - value: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - }, - text: String - },/* - created() { - if (!~[true, false].indexOf(this.value)) { - this.$emit('input', false); - } - },*/ - computed: { - checked(): boolean { - return this.value; - }, - transform(): string { - return this.checked ? 'translate3d(20px, 0, 0)' : ''; - } - }, - watch: { - value() { - (this.$el).style.transition = 'all 0.3s'; - (this.$refs.input as any).checked = this.checked; - } - }, - mounted() { - (this.$refs.input as any).checked = this.checked; - }, - methods: { - mouseenter() { - (this.$el).style.transition = 'all 0s'; - }, - handleChange() { - (this.$el).style.transition = 'all 0.3s'; - this.$emit('input', !this.checked); - this.$emit('change', !this.checked); - this.$nextTick(() => { - // set input's checked property - // in case parent refuses to change component's value - (this.$refs.input as any).checked = this.checked; - }); - }, - switchValue() { - !this.disabled && this.handleChange(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-switch - display flex - margin 12px 0 - cursor pointer - transition all 0.3s - - > * - user-select none - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - background-color $theme-color - border-color $theme-color - - > .label - > span - color $theme-color - - &:hover - > .label - > span - color darken($theme-color, 10%) - - > .button - background darken($theme-color, 10%) - border-color darken($theme-color, 10%) - - &:hover - > .label - > span - color #2e3338 - - > .button - background #ced2da - border-color #ced2da - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - &:focus + .button - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 14px - - > .button - display inline-block - margin 0 - width 40px - min-width 40px - height 20px - min-height 20px - background #dcdfe6 - border 1px solid #dcdfe6 - outline none - border-radius 10px - transition inherit - - > * - position absolute - top 1px - left 1px - border-radius 100% - transition transform 0.3s - width 16px - height 16px - background-color #fff - - > .label - margin-left 8px - display block - font-size 15px - cursor pointer - transition inherit - - > span - display block - line-height 20px - color #4a535a - transition inherit - - > p - margin 0 - //font-size 90% - color #9daab3 - -</style> diff --git a/src/server/web/app/common/views/components/time.vue b/src/server/web/app/common/views/components/time.vue deleted file mode 100644 index 6e0d2b0dcb..0000000000 --- a/src/server/web/app/common/views/components/time.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<time class="mk-time"> - <span v-if=" mode == 'relative' ">{{ relative }}</span> - <span v-if=" mode == 'absolute' ">{{ absolute }}</span> - <span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span> -</time> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - time: { - type: [Date, String], - required: true - }, - mode: { - type: String, - default: 'relative' - } - }, - data() { - return { - tickId: null, - now: new Date() - }; - }, - computed: { - _time(): Date { - return typeof this.time == 'string' ? new Date(this.time) : this.time; - }, - absolute(): string { - const time = this._time; - return ( - time.getFullYear() + '年' + - (time.getMonth() + 1) + '月' + - time.getDate() + '日' + - ' ' + - time.getHours() + '時' + - time.getMinutes() + '分'); - }, - relative(): string { - const time = this._time; - const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/; - return ( - ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) : - ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) : - ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) : - ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) : - ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) : - ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) : - ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) : - ago >= 0 ? '%i18n:common.time.just_now%' : - ago < 0 ? '%i18n:common.time.future%' : - '%i18n:common.time.unknown%'); - } - }, - created() { - if (this.mode == 'relative' || this.mode == 'detail') { - this.tick(); - this.tickId = setInterval(this.tick, 1000); - } - }, - destroyed() { - if (this.mode === 'relative' || this.mode === 'detail') { - clearInterval(this.tickId); - } - }, - methods: { - tick() { - this.now = new Date(); - } - } -}); -</script> diff --git a/src/server/web/app/common/views/components/timer.vue b/src/server/web/app/common/views/components/timer.vue deleted file mode 100644 index a3c4f01b77..0000000000 --- a/src/server/web/app/common/views/components/timer.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<time class="mk-time"> - {{ hh }}:{{ mm }}:{{ ss }} -</time> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - time: { - type: [Date, String], - required: true - } - }, - data() { - return { - tickId: null, - hh: null, - mm: null, - ss: null - }; - }, - computed: { - _time(): Date { - return typeof this.time == 'string' ? new Date(this.time) : this.time; - } - }, - created() { - this.tick(); - this.tickId = setInterval(this.tick, 1000); - }, - destroyed() { - clearInterval(this.tickId); - }, - methods: { - tick() { - const now = new Date().getTime(); - const start = this._time.getTime(); - const ago = Math.floor((now - start) / 1000); - - this.hh = Math.floor(ago / (60 * 60)).toString().padStart(2, '0'); - this.mm = Math.floor(ago / 60).toString().padStart(2, '0'); - this.ss = (ago % 60).toString().padStart(2, '0'); - } - } -}); -</script> diff --git a/src/server/web/app/common/views/components/twitter-setting.vue b/src/server/web/app/common/views/components/twitter-setting.vue deleted file mode 100644 index 082d2b435d..0000000000 --- a/src/server/web/app/common/views/components/twitter-setting.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<div class="mk-twitter-setting"> - <p>%i18n:common.tags.mk-twitter-setting.description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p> - <p class="account" v-if="os.i.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.userId}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screenName}`" target="_blank">@{{ os.i.account.twitter.screenName }}</a></p> - <p> - <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.account.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a> - <span v-if="os.i.account.twitter"> or </span> - <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.account.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a> - </p> - <p class="id" v-if="os.i.account.twitter">Twitter ID: {{ os.i.account.twitter.userId }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { apiUrl, docsUrl } from '../../../config'; - -export default Vue.extend({ - data() { - return { - form: null, - apiUrl, - docsUrl - }; - }, - mounted() { - this.$watch('os.i', () => { - if ((this as any).os.i.account.twitter) { - if (this.form) this.form.close(); - } - }, { - deep: true - }); - }, - methods: { - connect() { - this.form = window.open(apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570, width=520'); - }, - - disconnect() { - window.open(apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570, width=520'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-twitter-setting - color #4a535a - - .account - border solid 1px #e1e8ed - border-radius 4px - padding 16px - - a - font-weight bold - color inherit - - .id - color #8899a6 -</style> diff --git a/src/server/web/app/common/views/components/uploader.vue b/src/server/web/app/common/views/components/uploader.vue deleted file mode 100644 index c74a1edb41..0000000000 --- a/src/server/web/app/common/views/components/uploader.vue +++ /dev/null @@ -1,212 +0,0 @@ -<template> -<div class="mk-uploader"> - <ol v-if="uploads.length > 0"> - <li v-for="ctx in uploads" :key="ctx.id"> - <div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div> - <p class="name">%fa:spinner .pulse%{{ ctx.name }}</p> - <p class="status"> - <span class="initing" v-if="ctx.progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span> - <span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span> - <span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span> - </p> - <progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress> - <div class="progress initing" v-if="ctx.progress == undefined"></div> - <div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div> - </li> - </ol> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { apiUrl } from '../../../config'; - -export default Vue.extend({ - data() { - return { - uploads: [] - }; - }, - methods: { - upload(file, folder) { - if (folder && typeof folder == 'object') folder = folder.id; - - const id = Math.random(); - - const ctx = { - id: id, - name: file.name || 'untitled', - progress: undefined, - img: undefined - }; - - this.uploads.push(ctx); - this.$emit('change', this.uploads); - - const reader = new FileReader(); - reader.onload = (e: any) => { - ctx.img = e.target.result; - }; - reader.readAsDataURL(file); - - const data = new FormData(); - data.append('i', (this as any).os.i.account.token); - data.append('file', file); - - if (folder) data.append('folderId', folder); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = (e: any) => { - const driveFile = JSON.parse(e.target.response); - - this.$emit('uploaded', driveFile); - - this.uploads = this.uploads.filter(x => x.id != id); - this.$emit('change', this.uploads); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) { - if (ctx.progress == undefined) ctx.progress = {}; - ctx.progress.max = e.total; - ctx.progress.value = e.loaded; - } - }; - - xhr.send(data); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-uploader - overflow auto - - &:empty - display none - - > ol - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 8px 0 0 0 - padding 0 - height 36px - box-shadow 0 -1px 0 rgba($theme-color, 0.1) - border-top solid 8px transparent - - &:first-child - margin 0 - box-shadow none - border-top none - - > .img - display block - position absolute - top 0 - left 0 - width 36px - height 36px - background-size cover - background-position center center - - > .name - display block - position absolute - top 0 - left 44px - margin 0 - padding 0 - max-width 256px - font-size 0.8em - color rgba($theme-color, 0.7) - white-space nowrap - text-overflow ellipsis - overflow hidden - - > [data-fa] - margin-right 4px - - > .status - display block - position absolute - top 0 - right 0 - margin 0 - padding 0 - font-size 0.8em - - > .initing - color rgba($theme-color, 0.5) - - > .kb - color rgba($theme-color, 0.5) - - > .percentage - display inline-block - width 48px - text-align right - - color rgba($theme-color, 0.7) - - &:after - content '%' - - > progress - display block - position absolute - bottom 0 - right 0 - margin 0 - width calc(100% - 44px) - height 8px - background transparent - border none - border-radius 4px - overflow hidden - - &::-webkit-progress-value - background $theme-color - - &::-webkit-progress-bar - background rgba($theme-color, 0.1) - - > .progress - display block - position absolute - bottom 0 - right 0 - margin 0 - width calc(100% - 44px) - height 8px - border none - border-radius 4px - background linear-gradient( - 45deg, - lighten($theme-color, 30%) 25%, - $theme-color 25%, - $theme-color 50%, - lighten($theme-color, 30%) 50%, - lighten($theme-color, 30%) 75%, - $theme-color 75%, - $theme-color - ) - background-size 32px 32px - animation bg 1.5s linear infinite - - &.initing - opacity 0.3 - - @keyframes bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - -</style> diff --git a/src/server/web/app/common/views/components/url-preview.vue b/src/server/web/app/common/views/components/url-preview.vue deleted file mode 100644 index e91e510550..0000000000 --- a/src/server/web/app/common/views/components/url-preview.vue +++ /dev/null @@ -1,142 +0,0 @@ -<template> -<iframe v-if="youtubeId" type="text/html" height="250" - :src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`" - frameborder="0"/> -<div v-else> - <a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching"> - <div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div> - <article> - <header> - <h1>{{ title }}</h1> - </header> - <p>{{ description }}</p> - <footer> - <img class="icon" v-if="icon" :src="icon"/> - <p>{{ sitename }}</p> - </footer> - </article> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url as misskeyUrl } from '../../../config'; - -export default Vue.extend({ - props: ['url'], - data() { - return { - fetching: true, - title: null, - description: null, - thumbnail: null, - icon: null, - sitename: null, - youtubeId: null, - misskeyUrl - }; - }, - created() { - const url = new URL(this.url); - - if (url.hostname == 'www.youtube.com') { - this.youtubeId = url.searchParams.get('v'); - } else if (url.hostname == 'youtu.be') { - this.youtubeId = url.pathname; - } else { - fetch('/api:url?url=' + this.url).then(res => { - res.json().then(info => { - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - - this.fetching = false; - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -iframe - width 100% - -.mk-url-preview - display block - font-size 16px - border solid 1px #eee - border-radius 4px - overflow hidden - - &:hover - text-decoration none - border-color #ddd - - > article > header > h1 - text-decoration underline - - > .thumbnail - position absolute - width 100px - height 100% - background-position center - background-size cover - - & + article - left 100px - width calc(100% - 100px) - - > article - padding 16px - - > header - margin-bottom 8px - - > h1 - margin 0 - font-size 1em - color #555 - - > p - margin 0 - color #777 - font-size 0.8em - - > footer - margin-top 8px - height 16px - - > img - display inline-block - width 16px - height 16px - margin-right 4px - vertical-align top - - > p - display inline-block - margin 0 - color #666 - font-size 0.8em - line-height 16px - vertical-align top - - @media (max-width 500px) - font-size 8px - border none - - > .thumbnail - width 70px - - & + article - left 70px - width calc(100% - 70px) - - > article - padding 8px - -</style> diff --git a/src/server/web/app/common/views/components/url.vue b/src/server/web/app/common/views/components/url.vue deleted file mode 100644 index 14d4fc82f3..0000000000 --- a/src/server/web/app/common/views/components/url.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<a class="mk-url" :href="url" :target="target"> - <span class="schema">{{ schema }}//</span> - <span class="hostname">{{ hostname }}</span> - <span class="port" v-if="port != ''">:{{ port }}</span> - <span class="pathname" v-if="pathname != ''">{{ pathname }}</span> - <span class="query">{{ query }}</span> - <span class="hash">{{ hash }}</span> - %fa:external-link-square-alt% -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['url', 'target'], - data() { - return { - schema: null, - hostname: null, - port: null, - pathname: null, - query: null, - hash: null - }; - }, - created() { - const url = new URL(this.url); - - this.schema = url.protocol; - this.hostname = url.hostname; - this.port = url.port; - this.pathname = url.pathname; - this.query = url.search; - this.hash = url.hash; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-url - word-break break-all - - > [data-fa] - padding-left 2px - font-size .9em - font-weight 400 - font-style normal - - > .schema - opacity 0.5 - - > .hostname - font-weight bold - - > .pathname - opacity 0.8 - - > .query - opacity 0.5 - - > .hash - font-style italic - -</style> diff --git a/src/server/web/app/common/views/components/welcome-timeline.vue b/src/server/web/app/common/views/components/welcome-timeline.vue deleted file mode 100644 index 8f6199732a..0000000000 --- a/src/server/web/app/common/views/components/welcome-timeline.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<div class="mk-welcome-timeline"> - <div v-for="post in posts"> - <router-link class="avatar-anchor" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> - </router-link> - <div class="body"> - <header> - <router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link> - <span class="username">@{{ getAcct(post.user) }}</span> - <div class="info"> - <router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </div> - </header> - <div class="text"> - <mk-post-html :ast="post.ast"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - data() { - return { - fetching: true, - posts: [] - }; - }, - mounted() { - this.fetch(); - }, - methods: { - getAcct, - fetch(cb?) { - this.fetching = true; - (this as any).api('posts', { - reply: false, - repost: false, - media: false, - poll: false, - bot: false - }).then(posts => { - this.posts = posts; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-welcome-timeline - background #fff - - > div - padding 16px - overflow-wrap break-word - font-size .9em - color #4C4C4C - border-bottom 1px solid rgba(0, 0, 0, 0.05) - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - position -webkit-sticky - position sticky - top 16px - - > img - display block - width 42px - height 42px - border-radius 6px - - > .body - float right - width calc(100% - 42px) - padding-left 12px - - > header - display flex - align-items center - margin-bottom 4px - white-space nowrap - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - font-weight bold - text-overflow ellipsis - color #627079 - - > .username - margin 0 .5em 0 0 - color #ccc - - > .info - margin-left auto - font-size 0.9em - - > .created-at - color #c0c0c0 - -</style> diff --git a/src/server/web/app/common/views/directives/autocomplete.ts b/src/server/web/app/common/views/directives/autocomplete.ts deleted file mode 100644 index 94635d301a..0000000000 --- a/src/server/web/app/common/views/directives/autocomplete.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as getCaretCoordinates from 'textarea-caret'; -import MkAutocomplete from '../components/autocomplete.vue'; - -export default { - bind(el, binding, vn) { - const self = el._autoCompleteDirective_ = {} as any; - self.x = new Autocomplete(el, vn.context, binding.value); - self.x.attach(); - }, - - unbind(el, binding, vn) { - const self = el._autoCompleteDirective_; - self.x.detach(); - } -}; - -/** - * オートコンプリートを管理するクラス。 - */ -class Autocomplete { - private suggestion: any; - private textarea: any; - private vm: any; - private model: any; - private currentType: string; - - private get text(): string { - return this.vm[this.model]; - } - - private set text(text: string) { - this.vm[this.model] = text; - } - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea, vm, model) { - //#region BIND - this.onInput = this.onInput.bind(this); - this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.suggestion = null; - this.textarea = textarea; - this.vm = vm; - this.model = model; - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - */ - public attach() { - this.textarea.addEventListener('input', this.onInput); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - */ - public detach() { - this.textarea.removeEventListener('input', this.onInput); - this.close(); - } - - /** - * テキスト入力時 - */ - private onInput() { - const caret = this.textarea.selectionStart; - const text = this.text.substr(0, caret); - - const mentionIndex = text.lastIndexOf('@'); - const emojiIndex = text.lastIndexOf(':'); - - let opened = false; - - if (mentionIndex != -1 && mentionIndex > emojiIndex) { - const username = text.substr(mentionIndex + 1); - if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { - this.open('user', username); - opened = true; - } - } - - if (emojiIndex != -1 && emojiIndex > mentionIndex) { - const emoji = text.substr(emojiIndex + 1); - if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) { - this.open('emoji', emoji); - opened = true; - } - } - - if (!opened) { - this.close(); - } - } - - /** - * サジェストを提示します。 - */ - private open(type, q) { - if (type != this.currentType) { - this.close(); - } - this.currentType = type; - - //#region サジェストを表示すべき位置を計算 - const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); - - const rect = this.textarea.getBoundingClientRect(); - - const x = rect.left + caretPosition.left - this.textarea.scrollLeft; - const y = rect.top + caretPosition.top - this.textarea.scrollTop; - //#endregion - - if (this.suggestion) { - this.suggestion.x = x; - this.suggestion.y = y; - this.suggestion.q = q; - } else { - // サジェスト要素作成 - this.suggestion = new MkAutocomplete({ - propsData: { - textarea: this.textarea, - complete: this.complete, - close: this.close, - type: type, - q: q, - x, - y - } - }).$mount(); - - // 要素追加 - document.body.appendChild(this.suggestion.$el); - } - } - - /** - * サジェストを閉じます。 - */ - private close() { - if (this.suggestion == null) return; - - this.suggestion.$destroy(); - this.suggestion = null; - - this.textarea.focus(); - } - - /** - * オートコンプリートする - */ - private complete(type, value) { - this.close(); - - const caret = this.textarea.selectionStart; - - if (type == 'user') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); - - // 挿入 - this.text = trimmedBefore + '@' + value.username + ' ' + after; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.username.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type == 'emoji') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf(':')); - const after = source.substr(caret); - - // 挿入 - this.text = trimmedBefore + value + after; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + 1; - this.textarea.setSelectionRange(pos, pos); - }); - } - } -} diff --git a/src/server/web/app/common/views/directives/index.ts b/src/server/web/app/common/views/directives/index.ts deleted file mode 100644 index 268f07a950..0000000000 --- a/src/server/web/app/common/views/directives/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Vue from 'vue'; - -import autocomplete from './autocomplete'; - -Vue.directive('autocomplete', autocomplete); diff --git a/src/server/web/app/common/views/filters/bytes.ts b/src/server/web/app/common/views/filters/bytes.ts deleted file mode 100644 index 3afb11e9ae..0000000000 --- a/src/server/web/app/common/views/filters/bytes.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Vue from 'vue'; - -Vue.filter('bytes', (v, digits = 0) => { - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (v == 0) return '0Byte'; - const i = Math.floor(Math.log(v) / Math.log(1024)); - return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; -}); diff --git a/src/server/web/app/common/views/filters/index.ts b/src/server/web/app/common/views/filters/index.ts deleted file mode 100644 index 3a1d1ac235..0000000000 --- a/src/server/web/app/common/views/filters/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -require('./bytes'); -require('./number'); diff --git a/src/server/web/app/common/views/filters/number.ts b/src/server/web/app/common/views/filters/number.ts deleted file mode 100644 index d9f48229dd..0000000000 --- a/src/server/web/app/common/views/filters/number.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Vue from 'vue'; - -Vue.filter('number', (n) => { - return n.toLocaleString(); -}); diff --git a/src/server/web/app/common/views/widgets/access-log.vue b/src/server/web/app/common/views/widgets/access-log.vue deleted file mode 100644 index f7bb17d833..0000000000 --- a/src/server/web/app/common/views/widgets/access-log.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<div class="mkw-access-log"> - <mk-widget-container :show-header="props.design == 0"> - <template slot="header">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</template> - - <div :class="$style.logs" ref="log"> - <p v-for="req in requests"> - <span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span> - <b>{{ req.method }}</b> - <span>{{ req.path }}</span> - </p> - </div> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import * as seedrandom from 'seedrandom'; - -export default define({ - name: 'broadcast', - props: () => ({ - design: 0 - }) -}).extend({ - data() { - return { - requests: [], - connection: null, - connectionId: null - }; - }, - mounted() { - this.connection = (this as any).os.streams.requestsStream.getConnection(); - this.connectionId = (this as any).os.streams.requestsStream.use(); - this.connection.on('request', this.onRequest); - }, - beforeDestroy() { - this.connection.off('request', this.onRequest); - (this as any).os.streams.requestsStream.dispose(this.connectionId); - }, - methods: { - onRequest(request) { - const random = seedrandom(request.ip); - const r = Math.floor(random() * 255); - const g = Math.floor(random() * 255); - const b = Math.floor(random() * 255); - const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings - request.bg = `rgb(${r}, ${g}, ${b})`; - request.fg = luma >= 165 ? '#000' : '#fff'; - - this.requests.push(request); - if (this.requests.length > 30) this.requests.shift(); - - (this.$refs.log as any).scrollTop = (this.$refs.log as any).scrollHeight; - }, - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> - -<style lang="stylus" module> -.logs - max-height 250px - overflow auto - - > p - margin 0 - padding 8px - font-size 0.8em - color #555 - - &:nth-child(odd) - background rgba(0, 0, 0, 0.025) - - > b - margin-right 4px - -.ip - margin-right 4px - padding 0 4px - -</style> diff --git a/src/server/web/app/common/views/widgets/broadcast.vue b/src/server/web/app/common/views/widgets/broadcast.vue deleted file mode 100644 index bf41a5fc67..0000000000 --- a/src/server/web/app/common/views/widgets/broadcast.vue +++ /dev/null @@ -1,161 +0,0 @@ -<template> -<div class="mkw-broadcast" - :data-found="broadcasts.length != 0" - :data-melt="props.design == 1" - :data-mobile="isMobile" -> - <div class="icon"> - <svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> - <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path> - <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path> - <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path> - <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path> - <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path> - </svg> - </div> - <p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p> - <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title }}</h1> - <p v-if="!fetching"> - <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span> - <template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template> - </p> - <a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% >></a> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import { lang } from '../../../config'; - -export default define({ - name: 'broadcast', - props: () => ({ - design: 0 - }) -}).extend({ - data() { - return { - i: 0, - fetching: true, - broadcasts: [] - }; - }, - mounted() { - (this as any).os.getMeta().then(meta => { - let broadcasts = []; - if (meta.broadcasts) { - meta.broadcasts.forEach(broadcast => { - if (broadcast[lang]) { - broadcasts.push(broadcast[lang]); - } - }); - } - this.broadcasts = broadcasts; - this.fetching = false; - }); - }, - methods: { - next() { - if (this.i == this.broadcasts.length - 1) { - this.i = 0; - } else { - this.i++; - } - }, - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-broadcast - padding 10px - border solid 1px #4078c0 - border-radius 6px - - &[data-melt] - border none - - &[data-found] - padding-left 50px - - > .icon - display block - - &:after - content "" - display block - clear both - - > .icon - display none - float left - margin-left -40px - - > svg - fill currentColor - color #4078c0 - - > .wave - opacity 1 - - &.a - animation wave 20s ease-in-out 2.1s infinite - &.b - animation wave 20s ease-in-out 2s infinite - &.c - animation wave 20s ease-in-out 2s infinite - &.d - animation wave 20s ease-in-out 2.1s infinite - - @keyframes wave - 0% - opacity 1 - 1.5% - opacity 0 - 3.5% - opacity 0 - 5% - opacity 1 - 6.5% - opacity 0 - 8.5% - opacity 0 - 10% - opacity 1 - - > h1 - margin 0 - font-size 0.95em - font-weight normal - color #4078c0 - - > p - display block - z-index 1 - margin 0 - font-size 0.7em - color #555 - - &.fetching - text-align center - - a - color #555 - text-decoration underline - - > a - display block - font-size 0.7em - - &[data-mobile] - > p - color #fff - -</style> diff --git a/src/server/web/app/common/views/widgets/calendar.vue b/src/server/web/app/common/views/widgets/calendar.vue deleted file mode 100644 index 03f69a7597..0000000000 --- a/src/server/web/app/common/views/widgets/calendar.vue +++ /dev/null @@ -1,201 +0,0 @@ -<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> - </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> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'calendar', - props: () => ({ - design: 0 - }) -}).extend({ - data() { - return { - now: new Date(), - year: null, - month: null, - day: null, - weekDay: null, - yearP: null, - dayP: null, - monthP: null, - isHoliday: null, - special: null, - clock: null - }; - }, - created() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - func() { - if (this.isMobile) return; - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - }, - tick() { - const now = new Date(); - const nd = now.getDate(); - const nm = now.getMonth(); - const ny = now.getFullYear(); - - this.year = ny; - this.month = nm + 1; - this.day = nd; - this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()]; - - const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime(); - const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/; - const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime(); - const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime(); - const yearNumer = now.getTime() - new Date(ny, 0, 1).getTime(); - const yearDenom = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime(); - - this.dayP = dayNumer / dayDenom * 100; - this.monthP = monthNumer / monthDenom * 100; - this.yearP = yearNumer / yearDenom * 100; - - this.isHoliday = now.getDay() == 0 || now.getDay() == 6; - - this.special = - nm == 0 && nd == 1 ? 'on-new-years-day' : - false; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mkw-calendar - padding 16px 0 - color #777 - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &[data-special='on-new-years-day'] - border-color #ef95a0 - - &[data-melt] - background transparent - border none - - &[data-mobile] - border none - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - &:after - content "" - display block - clear both - - > .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 - - > p - margin 0 0 2px 0 - font-size 12px - line-height 18px - color #888 - - > b - margin-left 2px - - > .meter - width 100% - overflow hidden - background #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 - -</style> diff --git a/src/server/web/app/common/views/widgets/donation.vue b/src/server/web/app/common/views/widgets/donation.vue deleted file mode 100644 index e218df06e1..0000000000 --- a/src/server/web/app/common/views/widgets/donation.vue +++ /dev/null @@ -1,58 +0,0 @@ -<template> -<div class="mkw-donation" :data-mobile="isMobile"> - <article> - <h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1> - <p> - {{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }} - <a href="https://syuilo.com">@syuilo</a> - {{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }} - </p> - </article> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'donation' -}); -</script> - -<style lang="stylus" scoped> -.mkw-donation - background #fff - border solid 1px #ead8bb - border-radius 6px - - > article - padding 20px - - > h1 - margin 0 0 5px 0 - font-size 1em - color #888 - - > [data-fa] - margin-right 0.25em - - > p - display block - z-index 1 - margin 0 - font-size 0.8em - color #999 - - &[data-mobile] - border none - background #ead8bb - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - > article - > h1 - color #7b8871 - - > p - color #777d71 - -</style> diff --git a/src/server/web/app/common/views/widgets/index.ts b/src/server/web/app/common/views/widgets/index.ts deleted file mode 100644 index e41030e85a..0000000000 --- a/src/server/web/app/common/views/widgets/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Vue from 'vue'; - -import wAccessLog from './access-log.vue'; -import wVersion from './version.vue'; -import wRss from './rss.vue'; -import wServer from './server.vue'; -import wBroadcast from './broadcast.vue'; -import wCalendar from './calendar.vue'; -import wPhotoStream from './photo-stream.vue'; -import wSlideshow from './slideshow.vue'; -import wTips from './tips.vue'; -import wDonation from './donation.vue'; -import wNav from './nav.vue'; - -Vue.component('mkw-nav', wNav); -Vue.component('mkw-calendar', wCalendar); -Vue.component('mkw-photo-stream', wPhotoStream); -Vue.component('mkw-slideshow', wSlideshow); -Vue.component('mkw-tips', wTips); -Vue.component('mkw-donation', wDonation); -Vue.component('mkw-broadcast', wBroadcast); -Vue.component('mkw-server', wServer); -Vue.component('mkw-rss', wRss); -Vue.component('mkw-version', wVersion); -Vue.component('mkw-access-log', wAccessLog); diff --git a/src/server/web/app/common/views/widgets/nav.vue b/src/server/web/app/common/views/widgets/nav.vue deleted file mode 100644 index 7bd5a7832f..0000000000 --- a/src/server/web/app/common/views/widgets/nav.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<div class="mkw-nav"> - <mk-widget-container> - <div :class="$style.body"> - <mk-nav/> - </div> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'nav' -}); -</script> - -<style lang="stylus" module> -.body - padding 16px - font-size 12px - color #aaa - background #fff - - a - color #999 - - i - color #ccc - -</style> diff --git a/src/server/web/app/common/views/widgets/photo-stream.vue b/src/server/web/app/common/views/widgets/photo-stream.vue deleted file mode 100644 index baafd40662..0000000000 --- a/src/server/web/app/common/views/widgets/photo-stream.vue +++ /dev/null @@ -1,104 +0,0 @@ -<template> -<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2"> - <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> - <template slot="header">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</template> - - <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <div :class="$style.stream" v-if="!fetching && images.length > 0"> - <div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div> - </div> - <p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'photo-stream', - props: () => ({ - design: 0 - }) -}).extend({ - data() { - return { - images: [], - fetching: true, - connection: null, - connectionId: null - }; - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('drive_file_created', this.onDriveFileCreated); - - (this as any).api('drive/stream', { - type: 'image/*', - limit: 9 - }).then(images => { - this.images = images; - this.fetching = false; - }); - }, - beforeDestroy() { - this.connection.off('drive_file_created', this.onDriveFileCreated); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - onDriveFileCreated(file) { - if (/^image\/.+$/.test(file.type)) { - this.images.unshift(file); - if (this.images.length > 9) this.images.pop(); - } - }, - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> - -<style lang="stylus" module> -.root[data-melt] - .stream - padding 0 - - .img - border solid 4px transparent - border-radius 8px - -.stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - .img - flex 1 1 33% - width 33% - height 80px - background-position center center - background-size cover - border solid 2px transparent - border-radius 4px - -.fetching -.empty - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/common/views/widgets/rss.vue b/src/server/web/app/common/views/widgets/rss.vue deleted file mode 100644 index 4d74b2f7a4..0000000000 --- a/src/server/web/app/common/views/widgets/rss.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<div class="mkw-rss" :data-mobile="isMobile"> - <mk-widget-container :show-header="!props.compact"> - <template slot="header">%fa:rss-square%RSS</template> - <button slot="func" title="設定" @click="setting">%fa:cog%</button> - - <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <div :class="$style.feed" v-else> - <a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a> - </div> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'rss', - props: () => ({ - compact: false - }) -}).extend({ - data() { - return { - url: 'http://news.yahoo.co.jp/pickup/rss.xml', - items: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 60000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - }, - fetch() { - fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, { - cache: 'no-cache' - }).then(res => { - res.json().then(feed => { - this.items = feed.items; - this.fetching = false; - }); - }); - }, - setting() { - alert('not implemented yet'); - } - } -}); -</script> - -<style lang="stylus" module> -.feed - padding 12px 16px - font-size 0.9em - - > a - display block - padding 4px 0 - color #666 - border-bottom dashed 1px #eee - - &:last-child - border-bottom none - -.fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -&[data-mobile] - .feed - padding 0 - font-size 1em - - > a - padding 8px 16px - - &:nth-child(even) - background rgba(0, 0, 0, 0.05) - -</style> diff --git a/src/server/web/app/common/views/widgets/server.cpu-memory.vue b/src/server/web/app/common/views/widgets/server.cpu-memory.vue deleted file mode 100644 index d75a142568..0000000000 --- a/src/server/web/app/common/views/widgets/server.cpu-memory.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div class="cpu-memory"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none"> - <defs> - <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="cpuPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="cpuPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - </mask> - </defs> - <rect - x="-1" y="-1" - :width="viewBoxX + 2" :height="viewBoxY + 2" - :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> - <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> - </svg> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none"> - <defs> - <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> - </linearGradient> - <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="memPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="memPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - </mask> - </defs> - <rect - x="-1" y="-1" - :width="viewBoxX + 2" :height="viewBoxY + 2" - :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> - <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> - </svg> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as uuid from 'uuid'; - -export default Vue.extend({ - props: ['connection'], - data() { - return { - viewBoxX: 50, - viewBoxY: 30, - stats: [], - cpuGradientId: uuid(), - cpuMaskId: uuid(), - memGradientId: uuid(), - memMaskId: uuid(), - cpuPolylinePoints: '', - memPolylinePoints: '', - cpuPolygonPoints: '', - memPolygonPoints: '', - cpuP: '', - memP: '' - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - stats.mem.used = stats.mem.total - stats.mem.free; - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - - this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' '); - this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' '); - - this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - this.cpuP = (stats.cpu_usage * 100).toFixed(0); - this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cpu-memory - > svg - display block - padding 10px - width 50% - float left - - &:first-child - padding-right 5px - - &:last-child - padding-left 5px - - > text - font-size 5px - fill rgba(0, 0, 0, 0.55) - - > tspan - opacity 0.5 - - &:after - content "" - display block - clear both -</style> diff --git a/src/server/web/app/common/views/widgets/server.cpu.vue b/src/server/web/app/common/views/widgets/server.cpu.vue deleted file mode 100644 index 596c856da8..0000000000 --- a/src/server/web/app/common/views/widgets/server.cpu.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="cpu"> - <x-pie class="pie" :value="usage"/> - <div> - <p>%fa:microchip%CPU</p> - <p>{{ meta.cpu.cores }} Cores</p> - <p>{{ meta.cpu.model }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPie from './server.pie.vue'; - -export default Vue.extend({ - components: { - XPie - }, - props: ['connection', 'meta'], - data() { - return { - usage: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.usage = stats.cpu_usage; - } - } -}); -</script> - -<style lang="stylus" scoped> -.cpu - > .pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - > [data-fa] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/server/web/app/common/views/widgets/server.disk.vue b/src/server/web/app/common/views/widgets/server.disk.vue deleted file mode 100644 index 2af1982a96..0000000000 --- a/src/server/web/app/common/views/widgets/server.disk.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="disk"> - <x-pie class="pie" :value="usage"/> - <div> - <p>%fa:R hdd%Storage</p> - <p>Total: {{ total | bytes(1) }}</p> - <p>Available: {{ available | bytes(1) }}</p> - <p>Used: {{ used | bytes(1) }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPie from './server.pie.vue'; - -export default Vue.extend({ - components: { - XPie - }, - props: ['connection'], - data() { - return { - usage: 0, - total: 0, - used: 0, - available: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - stats.disk.used = stats.disk.total - stats.disk.free; - this.usage = stats.disk.used / stats.disk.total; - this.total = stats.disk.total; - this.used = stats.disk.used; - this.available = stats.disk.available; - } - } -}); -</script> - -<style lang="stylus" scoped> -.disk - > .pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - > [data-fa] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/server/web/app/common/views/widgets/server.info.vue b/src/server/web/app/common/views/widgets/server.info.vue deleted file mode 100644 index d243629506..0000000000 --- a/src/server/web/app/common/views/widgets/server.info.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> -<div class="info"> - <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> - <p>Machine: {{ meta.machine }}</p> - <p>Node: {{ meta.node }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['meta'] -}); -</script> - -<style lang="stylus" scoped> -.info - padding 10px 14px - - > p - margin 0 - font-size 12px - color #505050 -</style> diff --git a/src/server/web/app/common/views/widgets/server.memory.vue b/src/server/web/app/common/views/widgets/server.memory.vue deleted file mode 100644 index 834a62671d..0000000000 --- a/src/server/web/app/common/views/widgets/server.memory.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="memory"> - <x-pie class="pie" :value="usage"/> - <div> - <p>%fa:flask%Memory</p> - <p>Total: {{ total | bytes(1) }}</p> - <p>Used: {{ used | bytes(1) }}</p> - <p>Free: {{ free | bytes(1) }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPie from './server.pie.vue'; - -export default Vue.extend({ - components: { - XPie - }, - props: ['connection'], - data() { - return { - usage: 0, - total: 0, - used: 0, - free: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - stats.mem.used = stats.mem.total - stats.mem.free; - this.usage = stats.mem.used / stats.mem.total; - this.total = stats.mem.total; - this.used = stats.mem.used; - this.free = stats.mem.free; - } - } -}); -</script> - -<style lang="stylus" scoped> -.memory - > .pie - padding 10px - height 100px - float left - - > div - float left - width calc(100% - 100px) - padding 10px 10px 10px 0 - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold - - > [data-fa] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/server/web/app/common/views/widgets/server.pie.vue b/src/server/web/app/common/views/widgets/server.pie.vue deleted file mode 100644 index ce2cff1d00..0000000000 --- a/src/server/web/app/common/views/widgets/server.pie.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<svg viewBox="0 0 1 1" preserveAspectRatio="none"> - <circle - :r="r" - cx="50%" cy="50%" - fill="none" - stroke-width="0.1" - stroke="rgba(0, 0, 0, 0.05)"/> - <circle - :r="r" - cx="50%" cy="50%" - :stroke-dasharray="Math.PI * (r * 2)" - :stroke-dashoffset="strokeDashoffset" - fill="none" - stroke-width="0.1" - :stroke="color"/> - <text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - type: Number, - required: true - } - }, - data() { - return { - r: 0.4 - }; - }, - computed: { - color(): string { - return `hsl(${180 - (this.value * 180)}, 80%, 70%)`; - }, - strokeDashoffset(): number { - return (1 - this.value) * (Math.PI * (this.r * 2)); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - height 100% - - > circle - transform-origin center - transform rotate(-90deg) - transition stroke-dashoffset 0.5s ease - - > text - font-size 0.15px - fill rgba(0, 0, 0, 0.6) - -</style> diff --git a/src/server/web/app/common/views/widgets/server.uptimes.vue b/src/server/web/app/common/views/widgets/server.uptimes.vue deleted file mode 100644 index 06713d83ce..0000000000 --- a/src/server/web/app/common/views/widgets/server.uptimes.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> -<div class="uptimes"> - <p>Uptimes</p> - <p>Process: {{ process ? process.toFixed(0) : '---' }}s</p> - <p>OS: {{ os ? os.toFixed(0) : '---' }}s</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['connection'], - data() { - return { - process: 0, - os: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.process = stats.process_uptime; - this.os = stats.os_uptime; - } - } -}); -</script> - -<style lang="stylus" scoped> -.uptimes - padding 10px 14px - - > p - margin 0 - font-size 12px - color #505050 - - &:first-child - font-weight bold -</style> diff --git a/src/server/web/app/common/views/widgets/server.vue b/src/server/web/app/common/views/widgets/server.vue deleted file mode 100644 index 3d5248998f..0000000000 --- a/src/server/web/app/common/views/widgets/server.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<div class="mkw-server"> - <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> - <template slot="header">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</template> - <button slot="func" @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button> - - <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <template v-if="!fetching"> - <x-cpu-memory v-show="props.view == 0" :connection="connection"/> - <x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/> - <x-memory v-show="props.view == 2" :connection="connection"/> - <x-disk v-show="props.view == 3" :connection="connection"/> - <x-uptimes v-show="props.view == 4" :connection="connection"/> - <x-info v-show="props.view == 5" :connection="connection" :meta="meta"/> - </template> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import XCpuMemory from './server.cpu-memory.vue'; -import XCpu from './server.cpu.vue'; -import XMemory from './server.memory.vue'; -import XDisk from './server.disk.vue'; -import XUptimes from './server.uptimes.vue'; -import XInfo from './server.info.vue'; - -export default define({ - name: 'server', - props: () => ({ - design: 0, - view: 0 - }) -}).extend({ - components: { - XCpuMemory, - XCpu, - XMemory, - XDisk, - XUptimes, - XInfo - }, - data() { - return { - fetching: true, - meta: null, - connection: null, - connectionId: null - }; - }, - mounted() { - (this as any).os.getMeta().then(meta => { - this.meta = meta; - this.fetching = false; - }); - - this.connection = (this as any).os.streams.serverStream.getConnection(); - this.connectionId = (this as any).os.streams.serverStream.use(); - }, - beforeDestroy() { - (this as any).os.streams.serverStream.dispose(this.connectionId); - }, - methods: { - toggle() { - if (this.props.view == 5) { - this.props.view = 0; - } else { - this.props.view++; - } - }, - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> - -<style lang="stylus" module> -.fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/common/views/widgets/slideshow.vue b/src/server/web/app/common/views/widgets/slideshow.vue deleted file mode 100644 index ad32299f37..0000000000 --- a/src/server/web/app/common/views/widgets/slideshow.vue +++ /dev/null @@ -1,159 +0,0 @@ -<template> -<div class="mkw-slideshow" :data-mobile="isMobile"> - <div @click="choose"> - <p v-if="props.folder === undefined"> - <template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template> - <template v-else>クリックしてフォルダを指定してください</template> - </p> - <p v-if="props.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p> - <div ref="slideA" class="slide a"></div> - <div ref="slideB" class="slide b"></div> - </div> -</div> -</template> - -<script lang="ts"> -import * as anime from 'animejs'; -import define from '../../../common/define-widget'; -export default define({ - name: 'slideshow', - props: () => ({ - folder: undefined, - size: 0 - }) -}).extend({ - data() { - return { - images: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.$nextTick(() => { - this.applySize(); - }); - - if (this.props.folder !== undefined) { - this.fetch(); - } - - this.clock = setInterval(this.change, 10000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - func() { - this.resize(); - }, - applySize() { - let h; - - if (this.props.size == 1) { - h = 250; - } else { - h = 170; - } - - this.$el.style.height = `${h}px`; - }, - resize() { - if (this.props.size == 1) { - this.props.size = 0; - } else { - this.props.size++; - } - - this.applySize(); - }, - change() { - if (this.images.length == 0) return; - - const index = Math.floor(Math.random() * this.images.length); - const img = `url(${ this.images[index].url }?thumbnail&size=1024)`; - - (this.$refs.slideB as any).style.backgroundImage = img; - - anime({ - targets: this.$refs.slideB, - opacity: 1, - duration: 1000, - easing: 'linear', - complete: () => { - // 既にこのウィジェットがunmountされていたら要素がない - if ((this.$refs.slideA as any) == null) return; - - (this.$refs.slideA as any).style.backgroundImage = img; - anime({ - targets: this.$refs.slideB, - opacity: 0, - duration: 0 - }); - } - }); - }, - fetch() { - this.fetching = true; - - (this as any).api('drive/files', { - folderId: this.props.folder, - type: 'image/*', - limit: 100 - }).then(images => { - this.images = images; - this.fetching = false; - (this.$refs.slideA as any).style.backgroundImage = ''; - (this.$refs.slideB as any).style.backgroundImage = ''; - this.change(); - }); - }, - choose() { - (this as any).apis.chooseDriveFolder().then(folder => { - this.props.folder = folder ? folder.id : null; - this.fetch(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-slideshow - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &[data-mobile] - border none - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - > div - width 100% - height 100% - cursor pointer - - > p - display block - margin 1em - text-align center - color #888 - - > * - pointer-events none - - > .slide - position absolute - top 0 - left 0 - width 100% - height 100% - background-size cover - background-position center - - &.b - opacity 0 - -</style> diff --git a/src/server/web/app/common/views/widgets/tips.vue b/src/server/web/app/common/views/widgets/tips.vue deleted file mode 100644 index bdecc068e1..0000000000 --- a/src/server/web/app/common/views/widgets/tips.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<div class="mkw-tips"> - <p ref="tip">%fa:R lightbulb%<span v-html="tip"></span></p> -</div> -</template> - -<script lang="ts"> -import * as anime from 'animejs'; -import define from '../../../common/define-widget'; - -const tips = [ - '<kbd>t</kbd>でタイムラインにフォーカスできます', - '<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます', - '投稿フォームにはファイルをドラッグ&ドロップできます', - '投稿フォームにクリップボードにある画像データをペーストできます', - 'ドライブにファイルをドラッグ&ドロップしてアップロードできます', - 'ドライブでファイルをドラッグしてフォルダ移動できます', - 'ドライブでフォルダをドラッグしてフォルダ移動できます', - 'ホームは設定からカスタマイズできます', - 'MisskeyはMIT Licenseです', - 'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます', - '投稿の ... をクリックして、投稿をユーザーページにピン留めできます', - 'ドライブの容量は(デフォルトで)1GBです', - '投稿に添付したファイルは全てドライブに保存されます', - 'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます', - 'タイムライン上部にもウィジェットを設置できます', - '投稿をダブルクリックすると詳細が見れます', - '「**」でテキストを囲むと**強調表示**されます', - 'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます', - 'いくつかのウィンドウはブラウザの外に切り離すことができます', - 'カレンダーウィジェットのパーセンテージは、経過の割合を示しています', - 'APIを利用してbotの開発なども行えます', - 'MisskeyはLINEを通じてでも利用できます', - 'まゆかわいいよまゆ', - 'Misskeyは2014年にサービスを開始しました', - '対応ブラウザではMisskeyを開いていなくても通知を受け取れます' -] - -export default define({ - name: 'tips' -}).extend({ - data() { - return { - tip: null, - clock: null - }; - }, - mounted() { - this.$nextTick(() => { - this.set(); - }); - - this.clock = setInterval(this.change, 20000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - set() { - this.tip = tips[Math.floor(Math.random() * tips.length)]; - }, - change() { - anime({ - targets: this.$refs.tip, - opacity: 0, - duration: 500, - easing: 'linear', - complete: this.set - }); - - setTimeout(() => { - anime({ - targets: this.$refs.tip, - opacity: 1, - duration: 500, - easing: 'linear' - }); - }, 500); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-tips - overflow visible !important - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color #999 - - > [data-fa] - margin-right 4px - - kbd - display inline - padding 0 6px - margin 0 2px - font-size 1em - font-family inherit - border solid 1px #999 - border-radius 2px - -</style> diff --git a/src/server/web/app/common/views/widgets/version.vue b/src/server/web/app/common/views/widgets/version.vue deleted file mode 100644 index 30b632b396..0000000000 --- a/src/server/web/app/common/views/widgets/version.vue +++ /dev/null @@ -1,29 +0,0 @@ -<template> -<p>ver {{ version }} ({{ codename }})</p> -</template> - -<script lang="ts"> -import { version, codename } from '../../../config'; -import define from '../../../common/define-widget'; -export default define({ - name: 'version' -}).extend({ - data() { - return { - version, - codename - }; - } -}); -</script> - -<style lang="stylus" scoped> -p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color #aaa - -</style> diff --git a/src/server/web/app/config.ts b/src/server/web/app/config.ts deleted file mode 100644 index 522d7ff056..0000000000 --- a/src/server/web/app/config.ts +++ /dev/null @@ -1,37 +0,0 @@ -declare const _HOST_: string; -declare const _HOSTNAME_: string; -declare const _URL_: string; -declare const _API_URL_: string; -declare const _WS_URL_: string; -declare const _DOCS_URL_: string; -declare const _STATS_URL_: string; -declare const _STATUS_URL_: string; -declare const _DEV_URL_: string; -declare const _LANG_: string; -declare const _RECAPTCHA_SITEKEY_: string; -declare const _SW_PUBLICKEY_: string; -declare const _THEME_COLOR_: string; -declare const _COPYRIGHT_: string; -declare const _VERSION_: string; -declare const _CODENAME_: string; -declare const _LICENSE_: string; -declare const _GOOGLE_MAPS_API_KEY_: string; - -export const host = _HOST_; -export const hostname = _HOSTNAME_; -export const url = _URL_; -export const apiUrl = _API_URL_; -export const wsUrl = _WS_URL_; -export const docsUrl = _DOCS_URL_; -export const statsUrl = _STATS_URL_; -export const statusUrl = _STATUS_URL_; -export const devUrl = _DEV_URL_; -export const lang = _LANG_; -export const recaptchaSitekey = _RECAPTCHA_SITEKEY_; -export const swPublickey = _SW_PUBLICKEY_; -export const themeColor = _THEME_COLOR_; -export const copyright = _COPYRIGHT_; -export const version = _VERSION_; -export const codename = _CODENAME_; -export const license = _LICENSE_; -export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_; diff --git a/src/server/web/app/desktop/api/choose-drive-file.ts b/src/server/web/app/desktop/api/choose-drive-file.ts deleted file mode 100644 index fbda600e6e..0000000000 --- a/src/server/web/app/desktop/api/choose-drive-file.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { url } from '../../config'; -import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue'; - -export default function(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - - if (document.body.clientWidth > 800) { - const w = new MkChooseFileFromDriveWindow({ - propsData: { - title: o.title, - multiple: o.multiple, - initFolder: o.currentFolder - } - }).$mount(); - w.$once('selected', file => { - res(file); - }); - document.body.appendChild(w.$el); - } else { - window['cb'] = file => { - res(file); - }; - - window.open(url + '/selectdrive', - 'choose_drive_window', - 'height=500, width=800'); - } - }); -} diff --git a/src/server/web/app/desktop/api/choose-drive-folder.ts b/src/server/web/app/desktop/api/choose-drive-folder.ts deleted file mode 100644 index 9b33a20d9a..0000000000 --- a/src/server/web/app/desktop/api/choose-drive-folder.ts +++ /dev/null @@ -1,17 +0,0 @@ -import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue'; - -export default function(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const w = new MkChooseFolderFromDriveWindow({ - propsData: { - title: o.title, - initFolder: o.currentFolder - } - }).$mount(); - w.$once('selected', folder => { - res(folder); - }); - document.body.appendChild(w.$el); - }); -} diff --git a/src/server/web/app/desktop/api/contextmenu.ts b/src/server/web/app/desktop/api/contextmenu.ts deleted file mode 100644 index b70d7122d3..0000000000 --- a/src/server/web/app/desktop/api/contextmenu.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Ctx from '../views/components/context-menu.vue'; - -export default function(e, menu, opts?) { - const o = opts || {}; - const vm = new Ctx({ - propsData: { - menu, - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset, - } - }).$mount(); - vm.$once('closed', () => { - if (o.closed) o.closed(); - }); - document.body.appendChild(vm.$el); -} diff --git a/src/server/web/app/desktop/api/dialog.ts b/src/server/web/app/desktop/api/dialog.ts deleted file mode 100644 index 07935485b0..0000000000 --- a/src/server/web/app/desktop/api/dialog.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Dialog from '../views/components/dialog.vue'; - -export default function(opts) { - return new Promise<string>((res, rej) => { - const o = opts || {}; - const d = new Dialog({ - propsData: { - title: o.title, - text: o.text, - modal: o.modal, - buttons: o.actions - } - }).$mount(); - d.$once('clicked', id => { - res(id); - }); - document.body.appendChild(d.$el); - }); -} diff --git a/src/server/web/app/desktop/api/input.ts b/src/server/web/app/desktop/api/input.ts deleted file mode 100644 index ce26a8112f..0000000000 --- a/src/server/web/app/desktop/api/input.ts +++ /dev/null @@ -1,20 +0,0 @@ -import InputDialog from '../views/components/input-dialog.vue'; - -export default function(opts) { - return new Promise<string>((res, rej) => { - const o = opts || {}; - const d = new InputDialog({ - propsData: { - title: o.title, - placeholder: o.placeholder, - default: o.default, - type: o.type || 'text', - allowEmpty: o.allowEmpty - } - }).$mount(); - d.$once('done', text => { - res(text); - }); - document.body.appendChild(d.$el); - }); -} diff --git a/src/server/web/app/desktop/api/notify.ts b/src/server/web/app/desktop/api/notify.ts deleted file mode 100644 index 1f89f40ce6..0000000000 --- a/src/server/web/app/desktop/api/notify.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Notification from '../views/components/ui-notification.vue'; - -export default function(message) { - const vm = new Notification({ - propsData: { - message - } - }).$mount(); - document.body.appendChild(vm.$el); -} diff --git a/src/server/web/app/desktop/api/post.ts b/src/server/web/app/desktop/api/post.ts deleted file mode 100644 index cf49615df3..0000000000 --- a/src/server/web/app/desktop/api/post.ts +++ /dev/null @@ -1,21 +0,0 @@ -import PostFormWindow from '../views/components/post-form-window.vue'; -import RepostFormWindow from '../views/components/repost-form-window.vue'; - -export default function(opts) { - const o = opts || {}; - if (o.repost) { - const vm = new RepostFormWindow({ - propsData: { - repost: o.repost - } - }).$mount(); - document.body.appendChild(vm.$el); - } else { - const vm = new PostFormWindow({ - propsData: { - reply: o.reply - } - }).$mount(); - document.body.appendChild(vm.$el); - } -} diff --git a/src/server/web/app/desktop/api/update-avatar.ts b/src/server/web/app/desktop/api/update-avatar.ts deleted file mode 100644 index 36a2ffe914..0000000000 --- a/src/server/web/app/desktop/api/update-avatar.ts +++ /dev/null @@ -1,98 +0,0 @@ -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: { - image: file, - title: 'アバターとして表示する部分を選択', - aspectRatio: 1 / 1 - } - }).$mount(); - - w.$once('cropped', blob => { - const data = new FormData(); - data.append('i', os.i.account.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('folderId', 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).update(e.loaded, e.total); - }; - - xhr.send(data); - }; - - const set = file => { - os.api('i/update', { - avatarId: file.id - }).then(i => { - os.i.avatarId = i.avatarId; - os.i.avatarUrl = i.avatarUrl; - - 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); - }); - } -}; diff --git a/src/server/web/app/desktop/api/update-banner.ts b/src/server/web/app/desktop/api/update-banner.ts deleted file mode 100644 index e66dbf016b..0000000000 --- a/src/server/web/app/desktop/api/update-banner.ts +++ /dev/null @@ -1,98 +0,0 @@ -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: { - image: file, - title: 'バナーとして表示する部分を選択', - aspectRatio: 16 / 9 - } - }).$mount(); - - w.$once('cropped', blob => { - const data = new FormData(); - data.append('i', os.i.account.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('folderId', 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).update(e.loaded, e.total); - }; - - xhr.send(data); - }; - - const set = file => { - os.api('i/update', { - bannerId: file.id - }).then(i => { - os.i.bannerId = i.bannerId; - os.i.bannerUrl = i.bannerUrl; - - 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); - }); - } -}; diff --git a/src/server/web/app/desktop/assets/grid.svg b/src/server/web/app/desktop/assets/grid.svg deleted file mode 100644 index d1d72cd8ce..0000000000 --- a/src/server/web/app/desktop/assets/grid.svg +++ /dev/null @@ -1,150 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="32" - height="32" - viewBox="0 0 8.4666665 8.4666669" - version="1.1" - id="svg8" - inkscape:version="0.92.1 r15371" - sodipodi:docname="grid.svg"> - <defs - id="defs2" /> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="22.4" - inkscape:cx="14.687499" - inkscape:cy="14.558219" - inkscape:document-units="px" - inkscape:current-layer="layer1" - showgrid="true" - units="px" - showguides="true" - inkscape:window-width="1920" - inkscape:window-height="1017" - inkscape:window-x="-8" - inkscape:window-y="1072" - inkscape:window-maximized="1"> - <inkscape:grid - type="xygrid" - id="grid3680" - empspacing="8" - empcolor="#ff3fff" - empopacity="0.41176471" /> - </sodipodi:namedview> - <metadata - id="metadata5"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="レイヤー 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(0,-288.53331)"> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 0,296.99998 v -8.46667 h 8.4666666 l 10e-8,0.26458 H 0.26458333 l 0,8.20209 z" - id="path3684" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,292.23748 h 0.2645833 v 0.52916 h 0.5291667 l 0,0.26459 H 4.4979167 v 0.52917 H 4.2333334 v -0.52917 H 3.7041667 l 0,-0.26459 h 0.5291667 z" - id="path4491" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 3.4395833,292.76664 0,0.26459 H 2.38125 l 0,-0.26459 z" - id="path4493" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 6.3499999,292.76664 10e-8,0.26459 H 5.2916667 l -1e-7,-0.26459 z" - id="path4493-2" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 7.6729167,292.76664 v 0.26459 H 6.6145834 v -0.26459 z" - id="path4493-6" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 2.1166666,292.76664 1e-7,0.26459 H 1.0583334 l -1e-7,-0.26459 z" - id="path4493-1" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333333,291.97289 0.2645834,0 v -1.05833 l -0.2645834,0 z" - id="path4522" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,290.64997 0.2645833,1e-5 v -1.05833 l -0.2645833,-1e-5 z" - id="path4522-7" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,294.88331 h 0.2645833 v -1.05833 H 4.2333334 Z" - id="path4522-5" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333333,296.20622 h 0.2645833 v -1.05833 H 4.2333333 Z" - id="path4522-74" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333334,289.32706 0.2645834,10e-6 -10e-8,-0.52918 -0.2645834,-10e-6 z" - id="path4522-7-4" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 4.2333332,296.99998 h 0.2645835 l 0,-0.52917 H 4.2333333 Z" - id="path4522-7-4-4" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 0.79375,292.76664 -3e-8,0.26459 -0.52916667,0 3e-8,-0.26459 z" - id="path4493-1-7" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - <path - style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 8.4666667,292.76664 v 0.26459 l -0.5291667,0 v -0.26459 z" - id="path4493-1-7-2" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccc" /> - </g> -</svg> diff --git a/src/server/web/app/desktop/assets/header-logo-white.svg b/src/server/web/app/desktop/assets/header-logo-white.svg deleted file mode 100644 index 8082edb30d..0000000000 --- a/src/server/web/app/desktop/assets/header-logo-white.svg +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle fill="#FFFFFF" cx="128" cy="153.6" r="19.201"/>
-<circle fill="#FFFFFF" cx="51.2" cy="153.6" r="19.2"/>
-<circle fill="#FFFFFF" cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#FFFFFF" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle fill="#FFFFFF" cx="89.6" cy="102.4" r="19.2"/>
-<circle fill="#FFFFFF" cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/server/web/app/desktop/assets/header-logo.svg b/src/server/web/app/desktop/assets/header-logo.svg deleted file mode 100644 index 3a2207954a..0000000000 --- a/src/server/web/app/desktop/assets/header-logo.svg +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle cx="128" cy="153.6" r="19.201"/>
-<circle cx="51.2" cy="153.6" r="19.2"/>
-<circle cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#000000" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle cx="89.6" cy="102.4" r="19.2"/>
-<circle cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/server/web/app/desktop/assets/index.jpg b/src/server/web/app/desktop/assets/index.jpg Binary files differdeleted file mode 100644 index 10c412efe2..0000000000 --- a/src/server/web/app/desktop/assets/index.jpg +++ /dev/null diff --git a/src/server/web/app/desktop/assets/remove.png b/src/server/web/app/desktop/assets/remove.png Binary files differdeleted file mode 100644 index 8b1f4c06c9..0000000000 --- a/src/server/web/app/desktop/assets/remove.png +++ /dev/null diff --git a/src/server/web/app/desktop/script.ts b/src/server/web/app/desktop/script.ts deleted file mode 100644 index b95e168544..0000000000 --- a/src/server/web/app/desktop/script.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Desktop Client - */ - -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; -import '../../element.scss'; - -import init from '../init'; -import fuckAdBlock from '../common/scripts/fuck-ad-block'; -import { HomeStreamManager } from '../common/scripts/streaming/home'; -import composeNotification from '../common/scripts/compose-notification'; - -import chooseDriveFolder from './api/choose-drive-folder'; -import chooseDriveFile from './api/choose-drive-file'; -import dialog from './api/dialog'; -import input from './api/input'; -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 MkUser from './views/pages/user/user.vue'; -import MkSelectDrive from './views/pages/selectdrive.vue'; -import MkDrive from './views/pages/drive.vue'; -import MkHomeCustomize from './views/pages/home-customize.vue'; -import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkPost from './views/pages/post.vue'; -import MkSearch from './views/pages/search.vue'; -import MkOthello from './views/pages/othello.vue'; - -/** - * init - */ -init(async (launch) => { - // Register directives - require('./views/directives'); - - // Register components - require('./views/components'); - require('./views/widgets'); - - // Init router - const router = new VueRouter({ - mode: 'history', - routes: [ - { path: '/', name: 'index', component: MkIndex }, - { path: '/i/customize-home', component: MkHomeCustomize }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/search', component: MkSearch }, - { path: '/othello', component: MkOthello }, - { path: '/othello/:game', component: MkOthello }, - { path: '/@:user', component: MkUser }, - { path: '/@:user/:post', component: MkPost } - ] - }); - - // Launch the app - const [, os] = launch(router, os => ({ - chooseDriveFolder, - chooseDriveFile, - dialog, - input, - post, - notify, - updateAvatar: updateAvatar(os), - updateBanner: updateBanner(os) - })); - - /** - * Fuck AD Block - */ - fuckAdBlock(os); - - /** - * Init Notification - */ - if ('Notification' in window) { - // 許可を得ていなかったらリクエスト - if ((Notification as any).permission == 'default') { - await Notification.requestPermission(); - } - - if ((Notification as any).permission == 'granted') { - registerNotifications(os.stream); - } - } -}, true); - -function registerNotifications(stream: HomeStreamManager) { - if (stream == null) return; - - if (stream.hasConnection) { - attach(stream.borrow()); - } - - stream.on('connected', connection => { - attach(connection); - }); - - function attach(connection) { - connection.on('drive_file_created', file => { - const _n = composeNotification('drive_file_created', file); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 5000); - }); - - connection.on('mention', post => { - const _n = composeNotification('mention', post); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('reply', post => { - const _n = composeNotification('reply', post); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('quote', post => { - const _n = composeNotification('quote', post); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('unread_messaging_message', message => { - const _n = composeNotification('unread_messaging_message', message); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - n.onclick = () => { - n.close(); - /*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), { - user: message.user - });*/ - }; - setTimeout(n.close.bind(n), 7000); - }); - - connection.on('othello_invited', matching => { - const _n = composeNotification('othello_invited', matching); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - }); - } -} diff --git a/src/server/web/app/desktop/style.styl b/src/server/web/app/desktop/style.styl deleted file mode 100644 index 49f71fbde7..0000000000 --- a/src/server/web/app/desktop/style.styl +++ /dev/null @@ -1,50 +0,0 @@ -@import "../app" -@import "../reset" - -@import "./ui" - -*::input-placeholder - color #D8CBC5 - -* - &:focus - outline none - - &::scrollbar - width 5px - background transparent - - &:horizontal - height 5px - - &::scrollbar-button - width 0 - height 0 - background rgba(0, 0, 0, 0.2) - - &::scrollbar-piece - background transparent - - &:start - background transparent - - &::scrollbar-thumb - background rgba(0, 0, 0, 0.2) - - &:hover - background rgba(0, 0, 0, 0.4) - - &:active - background $theme-color - - &::scrollbar-corner - background rgba(0, 0, 0, 0.2) - -html - height 100% - background #f7f7f7 - -body - display flex - flex-direction column - min-height 100% diff --git a/src/server/web/app/desktop/ui.styl b/src/server/web/app/desktop/ui.styl deleted file mode 100644 index 5a8d1718e2..0000000000 --- a/src/server/web/app/desktop/ui.styl +++ /dev/null @@ -1,125 +0,0 @@ -@import "../../const" - -button - font-family sans-serif - - * - pointer-events none - -button.ui -.button.ui - display inline-block - cursor pointer - padding 0 14px - margin 0 - min-width 100px - line-height 38px - font-size 14px - color #888 - text-decoration none - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - border-radius 4px - outline none - - &.block - display block - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - &.primary - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - -input:not([type]).ui -input[type='text'].ui -input[type='password'].ui -input[type='email'].ui -input[type='date'].ui -input[type='number'].ui -textarea.ui - display block - padding 10px - width 100% - height 40px - font-family sans-serif - font-size 16px - color #55595c - border solid 1px #dadada - border-radius 4px - - &:hover - border-color #b0b0b0 - - &:focus - border-color $theme-color - -textarea.ui - min-width 100% - max-width 100% - min-height 64px - -.ui.info - display block - margin 1em 0 - padding 0 1em - font-size 90% - color rgba(#000, 0.87) - background #f8f8f9 - border solid 1px rgba(34, 36, 38, 0.22) - border-radius 4px - - > p - opacity 0.8 - - > [data-fa]:first-child - margin-right 0.25em - - &.warn - color #573a08 - background #FFFAF3 - border-color #C9BA9B - -.ui.from.group - display block - margin 16px 0 - - > p:first-child - margin 0 0 6px 0 - font-size 90% - font-weight bold - color rgba(#373a3c, 0.9) diff --git a/src/server/web/app/desktop/views/components/activity.calendar.vue b/src/server/web/app/desktop/views/components/activity.calendar.vue deleted file mode 100644 index 72233e9aca..0000000000 --- a/src/server/web/app/desktop/views/components/activity.calendar.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<svg viewBox="0 0 21 7" preserveAspectRatio="none"> - <rect v-for="record in data" class="day" - width="1" height="1" - :x="record.x" :y="record.date.weekday" - rx="1" ry="1" - fill="transparent"> - <title>{{ record.date.year }}/{{ record.date.month }}/{{ record.date.day }}</title> - </rect> - <rect v-for="record in data" class="day" - :width="record.v" :height="record.v" - :x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)" - rx="1" ry="1" - :fill="record.color" - style="pointer-events: none;"/> - <rect class="today" - width="1" height="1" - :x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday" - rx="1" ry="1" - fill="none" - stroke-width="0.1" - stroke="#f73520"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['data'], - created() { - this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - let x = 0; - this.data.reverse().forEach(d => { - d.x = x; - d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); - - d.v = peak == 0 ? 0 : d.total / (peak / 2); - if (d.v > 1) d.v = 1; - const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170; - const cs = d.v * 100; - const cl = 15 + ((1 - d.v) * 80); - d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; - - if (d.date.weekday == 6) x++; - }); - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - - > rect - transform-origin center - - &.day - &:hover - fill rgba(0, 0, 0, 0.05) - -</style> diff --git a/src/server/web/app/desktop/views/components/activity.chart.vue b/src/server/web/app/desktop/views/components/activity.chart.vue deleted file mode 100644 index 5057786ed4..0000000000 --- a/src/server/web/app/desktop/views/components/activity.chart.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown"> - <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title> - <polyline - :points="pointsPost" - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - :points="pointsReply" - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - :points="pointsRepost" - 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'; - -function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); -} - -function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); -} - -export default Vue.extend({ - props: ['data'], - data() { - return { - viewBoxX: 140, - viewBoxY: 60, - zoom: 1, - pos: 0, - pointsPost: null, - pointsReply: null, - pointsRepost: null, - pointsTotal: null - }; - }, - created() { - this.data.reverse(); - this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); - this.render(); - }, - methods: { - render() { - const peak = Math.max.apply(null, this.data.map(d => d.total)); - if (peak != 0) { - this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '); - this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); - } - }, - onMousedown(e) { - const clickX = e.clientX; - const clickY = e.clientY; - const baseZoom = this.zoom; - const basePos = this.pos; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; - - this.zoom = baseZoom + (-moveTop / 20); - this.pos = basePos + moveLeft; - if (this.zoom < 1) this.zoom = 1; - if (this.pos > 0) this.pos = 0; - if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); - - this.render(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - cursor all-scroll - -</style> diff --git a/src/server/web/app/desktop/views/components/activity.vue b/src/server/web/app/desktop/views/components/activity.vue deleted file mode 100644 index 480b956ecc..0000000000 --- a/src/server/web/app/desktop/views/components/activity.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> -<div class="mk-activity" :data-melt="design == 2"> - <template v-if="design == 0"> - <p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p> - <button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button> - </template> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <template v-else> - <x-calendar v-show="view == 0" :data="[].concat(activity)"/> - <x-chart v-show="view == 1" :data="[].concat(activity)"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XCalendar from './activity.calendar.vue'; -import XChart from './activity.chart.vue'; - -export default Vue.extend({ - components: { - XCalendar, - XChart - }, - props: { - design: { - default: 0 - }, - initView: { - default: 0 - }, - user: { - type: Object, - required: true - } - }, - data() { - return { - fetching: true, - activity: null, - view: this.initView - }; - }, - mounted() { - (this as any).api('aggregation/users/activity', { - userId: this.user.id, - limit: 20 * 7 - }).then(activity => { - this.activity = activity; - this.fetching = false; - }); - }, - methods: { - toggle() { - if (this.view == 1) { - this.view = 0; - this.$emit('viewChanged', this.view); - } else { - this.view++; - this.$emit('viewChanged', this.view); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-activity - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &[data-melt] - background transparent !important - border none !important - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/analog-clock.vue b/src/server/web/app/desktop/views/components/analog-clock.vue deleted file mode 100644 index 81eec81598..0000000000 --- a/src/server/web/app/desktop/views/components/analog-clock.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<canvas class="mk-analog-clock" ref="canvas" width="256" height="256"></canvas> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { themeColor } from '../../../config'; - -const Vec2 = function(this: any, x, y) { - this.x = x; - this.y = y; -}; - -export default Vue.extend({ - data() { - return { - clock: null - }; - }, - mounted() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - tick() { - const canv = this.$refs.canvas as any; - - const now = new Date(); - const s = now.getSeconds(); - const m = now.getMinutes(); - const h = now.getHours(); - - const ctx = canv.getContext('2d'); - const canvW = canv.width; - const canvH = canv.height; - ctx.clearRect(0, 0, canvW, canvH); - - { // 背景 - const center = Math.min((canvW / 2), (canvH / 2)); - const lineStart = center * 0.90; - const shortLineEnd = center * 0.87; - const longLineEnd = center * 0.84; - for (let i = 0; i < 60; i++) { - const angle = Math.PI * i / 30; - const uv = new Vec2(Math.sin(angle), -Math.cos(angle)); - ctx.beginPath(); - ctx.lineWidth = 1; - ctx.moveTo((canvW / 2) + uv.x * lineStart, (canvH / 2) + uv.y * lineStart); - if (i % 5 == 0) { - ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; - ctx.lineTo((canvW / 2) + uv.x * longLineEnd, (canvH / 2) + uv.y * longLineEnd); - } else { - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.lineTo((canvW / 2) + uv.x * shortLineEnd, (canvH / 2) + uv.y * shortLineEnd); - } - ctx.stroke(); - } - } - - { // 分 - const angle = Math.PI * (m + s / 60) / 30; - const length = Math.min(canvW, canvH) / 2.6; - const uv = new Vec2(Math.sin(angle), -Math.cos(angle)); - ctx.beginPath(); - ctx.strokeStyle = '#ffffff'; - ctx.lineWidth = 2; - ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5); - ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length); - ctx.stroke(); - } - - { // 時 - const angle = Math.PI * (h % 12 + m / 60) / 6; - const length = Math.min(canvW, canvH) / 4; - const uv = new Vec2(Math.sin(angle), -Math.cos(angle)); - ctx.beginPath(); - ctx.strokeStyle = themeColor; - ctx.lineWidth = 2; - ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5); - ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length); - ctx.stroke(); - } - - { // 秒 - const angle = Math.PI * s / 30; - const length = Math.min(canvW, canvH) / 2.6; - const uv = new Vec2(Math.sin(angle), -Math.cos(angle)); - ctx.beginPath(); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.lineWidth = 1; - ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5); - ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length); - ctx.stroke(); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-analog-clock - display block - width 256px - height 256px -</style> diff --git a/src/server/web/app/desktop/views/components/calendar.vue b/src/server/web/app/desktop/views/components/calendar.vue deleted file mode 100644 index 71aab2e8a5..0000000000 --- a/src/server/web/app/desktop/views/components/calendar.vue +++ /dev/null @@ -1,252 +0,0 @@ -<template> -<div class="mk-calendar" :data-melt="design == 4 || design == 5"> - <template v-if="design == 0 || design == 1"> - <button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button> - <p class="title">{{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }}</p> - <button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button> - </template> - - <div class="calendar"> - <template v-if="design == 0 || design == 2 || design == 4"> - <div class="weekday" - v-for="(day, i) in Array(7).fill(0)" - :data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i" - :data-is-donichi="i == 0 || i == 6" - >{{ weekdayText[i] }}</div> - </template> - <div v-for="n in paddingDays"></div> - <div class="day" v-for="(day, i) in days" - :data-today="isToday(i + 1)" - :data-selected="isSelected(i + 1)" - :data-is-out-of-range="isOutOfRange(i + 1)" - :data-is-donichi="isDonichi(i + 1)" - @click="go(i + 1)" - :title="isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%'" - > - <div>{{ i + 1 }}</div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - -function isLeapYear(year) { - return (year % 400 == 0) ? true : - (year % 100 == 0) ? false : - (year % 4 == 0) ? true : - false; -} - -export default Vue.extend({ - props: { - design: { - default: 0 - }, - start: { - type: Date, - required: false - } - }, - data() { - return { - today: new Date(), - year: new Date().getFullYear(), - month: new Date().getMonth() + 1, - selected: new Date(), - weekdayText: [ - '%i18n:common.weekday-short.sunday%', - '%i18n:common.weekday-short.monday%', - '%i18n:common.weekday-short.tuesday%', - '%i18n:common.weekday-short.wednesday%', - '%i18n:common.weekday-short.thursday%', - '%i18n:common.weekday-short.friday%', - '%i18n:common.weekday-short.satruday%' - ] - }; - }, - computed: { - paddingDays(): number { - const date = new Date(this.year, this.month - 1, 1); - return date.getDay(); - }, - days(): number { - let days = eachMonthDays[this.month - 1]; - - // うるう年なら+1日 - if (this.month == 2 && isLeapYear(this.year)) days++; - - return days; - } - }, - methods: { - isToday(day) { - return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate(); - }, - - isSelected(day) { - return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate(); - }, - - isOutOfRange(day) { - const test = (new Date(this.year, this.month - 1, day)).getTime(); - return test > this.today.getTime() || - (this.start ? test < (this.start as any).getTime() : false); - }, - - isDonichi(day) { - const weekday = (new Date(this.year, this.month - 1, day)).getDay(); - return weekday == 0 || weekday == 6; - }, - - prev() { - if (this.month == 1) { - this.year = this.year - 1; - this.month = 12; - } else { - this.month--; - } - }, - - next() { - if (this.month == 12) { - this.year = this.year + 1; - this.month = 1; - } else { - this.month++; - } - }, - - go(day) { - if (this.isOutOfRange(day)) return; - const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999); - this.selected = date; - this.$emit('chosen', this.selected); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-calendar - color #777 - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &[data-melt] - background transparent !important - border none !important - - > .title - z-index 1 - margin 0 - padding 0 16px - text-align center - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - &:first-of-type - left 0 - - &:last-of-type - right 0 - - > .calendar - display flex - flex-wrap wrap - padding 16px - - * - user-select none - - > div - width calc(100% * (1/7)) - text-align center - line-height 32px - font-size 14px - - &.weekday - color #19a2a9 - - &[data-is-donichi] - color #ef95a0 - - &[data-today] - box-shadow 0 0 0 1px #19a2a9 inset - border-radius 6px - - &[data-is-donichi] - box-shadow 0 0 0 1px #ef95a0 inset - - &.day - cursor pointer - color #777 - - > div - border-radius 6px - - &:hover > div - background rgba(0, 0, 0, 0.025) - - &:active > div - background rgba(0, 0, 0, 0.05) - - &[data-is-donichi] - color #ef95a0 - - &[data-is-out-of-range] - cursor default - color rgba(#777, 0.5) - - &[data-is-donichi] - color rgba(#ef95a0, 0.5) - - &[data-selected] - font-weight bold - - > div - background rgba(0, 0, 0, 0.025) - - &:active > div - background rgba(0, 0, 0, 0.05) - - &[data-today] - > div - color $theme-color-foreground - background $theme-color - - &:hover > div - background lighten($theme-color, 10%) - - &:active > div - background darken($theme-color, 10%) - -</style> diff --git a/src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue deleted file mode 100644 index 9a1e9c958a..0000000000 --- a/src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue +++ /dev/null @@ -1,180 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> - <span slot="header"> - <span v-html="title" :class="$style.title"></span> - <span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}ファイル選択中)</span> - </span> - - <mk-drive - ref="browser" - :class="$style.browser" - :multiple="multiple" - @selected="onSelected" - @change-selection="onChangeSelection" - /> - <div :class="$style.footer"> - <button :class="$style.upload" title="PCからドライブにファイルをアップロード" @click="upload">%fa:upload%</button> - <button :class="$style.cancel" @click="cancel">キャンセル</button> - <button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">決定</button> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - multiple: { - default: false - }, - title: { - default: '%fa:R file%ファイルを選択' - } - }, - data() { - return { - files: [] - }; - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - ok() { - this.$emit('selected', this.multiple ? this.files : this.files[0]); - (this.$refs.window as any).close(); - }, - cancel() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> -@import '~const.styl' - -.title - > [data-fa] - margin-right 4px - -.count - margin-left 8px - opacity 0.7 - -.browser - height calc(100% - 72px) - -.footer - height 72px - background lighten($theme-color, 95%) - -.upload - display inline-block - position absolute - top 8px - left 16px - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color rgba($theme-color, 0.5) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color rgba($theme-color, 0.3) - - &:active - color rgba($theme-color, 0.6) - background transparent - border-color rgba($theme-color, 0.5) - box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - -.ok -.cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - -.ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - -.cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -</style> - diff --git a/src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue deleted file mode 100644 index f99533176d..0000000000 --- a/src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue +++ /dev/null @@ -1,114 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> - <span slot="header"> - <span v-html="title" :class="$style.title"></span> - </span> - - <mk-drive - ref="browser" - :class="$style.browser" - :multiple="false" - /> - <div :class="$style.footer"> - <button :class="$style.cancel" @click="cancel">キャンセル</button> - <button :class="$style.ok" @click="ok">決定</button> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - title: { - default: '%fa:R folder%フォルダを選択' - } - }, - methods: { - ok() { - this.$emit('selected', (this.$refs.browser as any).folder); - (this.$refs.window as any).close(); - }, - cancel() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> -@import '~const.styl' - -.title - > [data-fa] - margin-right 4px - -.browser - height calc(100% - 72px) - -.footer - height 72px - background lighten($theme-color, 95%) - -.ok -.cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - -.ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - -.cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -</style> diff --git a/src/server/web/app/desktop/views/components/context-menu.menu.vue b/src/server/web/app/desktop/views/components/context-menu.menu.vue deleted file mode 100644 index 6359dbf1b4..0000000000 --- a/src/server/web/app/desktop/views/components/context-menu.menu.vue +++ /dev/null @@ -1,121 +0,0 @@ -<template> -<ul class="menu"> - <li v-for="(item, i) in menu" :class="item.type"> - <template v-if="item.type == 'item'"> - <p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p> - </template> - <template v-if="item.type == 'link'"> - <a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a> - </template> - <template v-else-if="item.type == 'nest'"> - <p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p> - <me-nu :menu="item.menu" @x="click"/> - </template> - </li> -</ul> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - name: 'me-nu', - props: ['menu'], - methods: { - click(item) { - this.$emit('x', item); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.menu - $width = 240px - $item-height = 38px - $padding = 10px - - margin 0 - padding $padding 0 - list-style none - - li - display block - - &.divider - margin-top $padding - padding-top $padding - border-top solid 1px #eee - - &.nest - > p - cursor default - - > .caret - position absolute - top 0 - right 8px - - > * - line-height $item-height - width 28px - text-align center - - &:hover > ul - visibility visible - - &:active - > p, a - background $theme-color - - > p, a - display block - z-index 1 - margin 0 - padding 0 32px 0 38px - line-height $item-height - color #868C8C - text-decoration none - cursor pointer - - &:hover - text-decoration none - - * - pointer-events none - - &:hover - > p, a - text-decoration none - background $theme-color - color $theme-color-foreground - - &:active - > p, a - text-decoration none - background darken($theme-color, 10%) - color $theme-color-foreground - - li > ul - visibility hidden - position absolute - top 0 - left $width - margin-top -($padding) - width $width - background #fff - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) - transition visibility 0s linear 0.2s - -</style> - -<style lang="stylus" module> -.icon - > * - width 28px - margin-left -28px - text-align center -</style> - diff --git a/src/server/web/app/desktop/views/components/context-menu.vue b/src/server/web/app/desktop/views/components/context-menu.vue deleted file mode 100644 index 8bd9945840..0000000000 --- a/src/server/web/app/desktop/views/components/context-menu.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}"> - <x-menu :menu="menu" @x="click"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; -import contains from '../../../common/scripts/contains'; -import XMenu from './context-menu.menu.vue'; - -export default Vue.extend({ - components: { - XMenu - }, - props: ['x', 'y', 'menu'], - mounted() { - this.$nextTick(() => { - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.addEventListener('mousedown', this.onMousedown); - }); - - this.$el.style.display = 'block'; - - anime({ - targets: this.$el, - opacity: [0, 1], - duration: 100, - easing: 'linear' - }); - }); - }, - methods: { - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - return false; - }, - click(item) { - if (item.onClick) item.onClick(); - this.close(); - }, - close() { - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.removeEventListener('mousedown', this.onMousedown); - }); - - this.$emit('closed'); - this.$destroy(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.context-menu - $width = 240px - $item-height = 38px - $padding = 10px - - display none - position fixed - top 0 - left 0 - z-index 4096 - width $width - font-size 0.8em - background #fff - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) - opacity 0 - -</style> diff --git a/src/server/web/app/desktop/views/components/crop-window.vue b/src/server/web/app/desktop/views/components/crop-window.vue deleted file mode 100644 index eb6a55d959..0000000000 --- a/src/server/web/app/desktop/views/components/crop-window.vue +++ /dev/null @@ -1,178 +0,0 @@ -<template> - <mk-window ref="window" is-modal width="800px" :can-close="false"> - <span slot="header">%fa:crop%{{ title }}</span> - <div class="body"> - <vue-cropper ref="cropper" - :src="image.url" - :view-mode="1" - :aspect-ratio="aspectRatio" - :container-style="{ width: '100%', 'max-height': '400px' }" - /> - </div> - <div :class="$style.actions"> - <button :class="$style.skip" @click="skip">クロップをスキップ</button> - <button :class="$style.cancel" @click="cancel">キャンセル</button> - <button :class="$style.ok" @click="ok">決定</button> - </div> - </mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import VueCropper from 'vue-cropperjs'; - -export default Vue.extend({ - components: { - VueCropper - }, - props: { - image: { - type: Object, - required: true - }, - title: { - type: String, - required: true - }, - aspectRatio: { - type: Number, - required: true - } - }, - methods: { - ok() { - (this.$refs.cropper as any).getCroppedCanvas().toBlob(blob => { - this.$emit('cropped', blob); - (this.$refs.window as any).close(); - }); - }, - - skip() { - this.$emit('skipped'); - (this.$refs.window as any).close(); - }, - - cancel() { - this.$emit('canceled'); - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> -@import '~const.styl' - -.header - > [data-fa] - margin-right 4px - -.img - width 100% - max-height 400px - -.actions - height 72px - background lighten($theme-color, 95%) - -.ok -.cancel -.skip - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - -.ok -.cancel - width 120px - -.ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - -.cancel -.skip - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -.cancel - right 148px - -.skip - left 16px - width 150px - -</style> - -<style lang="stylus"> -.cropper-modal { - opacity: 0.8; -} - -.cropper-view-box { - outline-color: $theme-color; -} - -.cropper-line, .cropper-point { - background-color: $theme-color; -} - -.cropper-bg { - animation: cropper-bg 0.5s linear infinite; -} - -@keyframes cropper-bg { - 0% { - background-position: 0 0; - } - - 100% { - background-position: -8px -8px; - } -} -</style> diff --git a/src/server/web/app/desktop/views/components/dialog.vue b/src/server/web/app/desktop/views/components/dialog.vue deleted file mode 100644 index fa17e4a9d2..0000000000 --- a/src/server/web/app/desktop/views/components/dialog.vue +++ /dev/null @@ -1,170 +0,0 @@ -<template> -<div class="mk-dialog"> - <div class="bg" ref="bg" @click="onBgClick"></div> - <div class="main" ref="main"> - <header v-html="title" :class="$style.header"></header> - <div class="body" v-html="text"></div> - <div class="buttons"> - <button v-for="button in buttons" @click="click(button)">{{ button.text }}</button> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: { - title: { - type: String, - required: false - }, - text: { - type: String, - required: true - }, - buttons: { - type: Array, - default: () => { - return [{ - text: 'OK' - }]; - } - }, - modal: { - type: Boolean, - default: false - } - }, - mounted() { - this.$nextTick(() => { - (this.$refs.bg as any).style.pointerEvents = 'auto'; - anime({ - targets: this.$refs.bg, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.main, - opacity: 1, - scale: [1.2, 1], - duration: 300, - easing: [0, 0.5, 0.5, 1] - }); - }); - }, - methods: { - click(button) { - this.$emit('clicked', button.id); - this.close(); - }, - close() { - (this.$refs.bg as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.bg, - opacity: 0, - duration: 300, - easing: 'linear' - }); - - (this.$refs.main as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.main, - opacity: 0, - scale: 0.8, - duration: 300, - easing: [ 0.5, -0.5, 1, 0.5 ], - complete: () => this.$destroy() - }); - }, - onBgClick() { - if (!this.modal) { - this.close(); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-dialog - > .bg - display block - position fixed - z-index 8192 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - opacity 0 - pointer-events none - - > .main - display block - position fixed - z-index 8192 - top 20% - left 0 - right 0 - margin 0 auto 0 auto - padding 32px 42px - width 480px - background #fff - opacity 0 - - > .body - margin 1em 0 - color #888 - - > .buttons - > button - display inline-block - float right - margin 0 - padding 10px 10px - font-size 1.1em - font-weight normal - text-decoration none - color #888 - background transparent - outline none - border none - border-radius 0 - cursor pointer - transition color 0.1s ease - - i - margin 0 0.375em - - &:hover - color $theme-color - - &:active - color darken($theme-color, 10%) - transition color 0s ease - -</style> - -<style lang="stylus" module> -@import '~const.styl' - -.header - margin 1em 0 - color $theme-color - // color #43A4EC - font-weight bold - - &:empty - display none - - > i - margin-right 0.5em - -</style> diff --git a/src/server/web/app/desktop/views/components/drive-window.vue b/src/server/web/app/desktop/views/components/drive-window.vue deleted file mode 100644 index 3a072f4794..0000000000 --- a/src/server/web/app/desktop/views/components/drive-window.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout"> - <template slot="header"> - <p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p> - <span :class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span> - </template> - <mk-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url } from '../../../config'; - -export default Vue.extend({ - props: ['folder'], - data() { - return { - usage: null - }; - }, - mounted() { - (this as any).api('drive').then(info => { - this.usage = info.usage / info.capacity * 100; - }); - }, - methods: { - popout() { - const folder = (this.$refs.browser as any) ? (this.$refs.browser as any).folder : null; - if (folder) { - return `${url}/i/drive/folder/${folder.id}`; - } else { - return `${url}/i/drive`; - } - } - } -}); -</script> - -<style lang="stylus" module> -.title - > [data-fa] - margin-right 4px - -.info - position absolute - top 0 - left 16px - margin 0 - font-size 80% - -.browser - height 100% - -</style> - diff --git a/src/server/web/app/desktop/views/components/drive.file.vue b/src/server/web/app/desktop/views/components/drive.file.vue deleted file mode 100644 index 85f8361c9f..0000000000 --- a/src/server/web/app/desktop/views/components/drive.file.vue +++ /dev/null @@ -1,317 +0,0 @@ -<template> -<div class="root file" - :data-is-selected="isSelected" - :data-is-contextmenu-showing="isContextmenuShowing" - @click="onClick" - draggable="true" - @dragstart="onDragstart" - @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu" - :title="title" -> - <div class="label" v-if="os.i.avatarId == file.id"><img src="/assets/label.svg"/> - <p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p> - </div> - <div class="label" v-if="os.i.bannerId == file.id"><img src="/assets/label.svg"/> - <p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p> - </div> - <div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`"> - <img :src="`${file.url}?thumbnail&size=128`" alt="" @load="onThumbnailLoaded"/> - </div> - <p class="name"> - <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> - <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; -import contextmenu from '../../api/contextmenu'; -import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; - -export default Vue.extend({ - props: ['file'], - data() { - return { - isContextmenuShowing: false, - isDragging: false - }; - }, - computed: { - browser(): any { - return this.$parent; - }, - isSelected(): boolean { - return this.browser.selectedFiles.some(f => f.id == this.file.id); - }, - title(): string { - return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`; - }, - background(): string { - return this.file.properties.avgColor - ? `rgb(${this.file.properties.avgColor.join(',')})` - : 'transparent'; - } - }, - methods: { - onClick() { - this.browser.chooseFile(this.file); - }, - - onContextmenu(e) { - this.isContextmenuShowing = true; - contextmenu(e, [{ - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename%', - icon: '%fa:i-cursor%', - onClick: this.rename - }, { - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copy-url%', - icon: '%fa:link%', - onClick: this.copyUrl - }, { - type: 'link', - href: `${this.file.url}?download`, - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%', - icon: '%fa:download%', - }, { - type: 'divider', - }, { - type: 'item', - text: '%i18n:common.delete%', - icon: '%fa:R trash-alt%', - onClick: this.deleteFile - }, { - type: 'divider', - }, { - type: 'nest', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.else-files%', - menu: [{ - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-avatar%', - onClick: this.setAsAvatar - }, { - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-banner%', - onClick: this.setAsBanner - }] - }, { - type: 'nest', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.open-in-app%', - menu: [{ - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.add-app%...', - onClick: this.addApp - }] - }], { - closed: () => { - this.isContextmenuShowing = false; - } - }); - }, - - onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file)); - this.isDragging = true; - - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }, - - onDragend(e) { - this.isDragging = false; - this.browser.isDragSource = false; - }, - - onThumbnailLoaded() { - if (this.file.properties.avgColor) { - anime({ - targets: this.$refs.thumbnail, - backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`, - duration: 100, - easing: 'linear' - }); - } - }, - - rename() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%', - placeholder: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%', - default: this.file.name, - allowEmpty: false - }).then(name => { - (this as any).api('drive/files/update', { - fileId: this.file.id, - name: name - }) - }); - }, - - copyUrl() { - copyToClipboard(this.file.url); - (this as any).apis.dialog({ - title: '%fa:check%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied%', - text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied-url-to-clipboard%', - actions: [{ - text: '%i18n:common.ok%' - }] - }); - }, - - setAsAvatar() { - (this as any).apis.updateAvatar(this.file); - }, - - setAsBanner() { - (this as any).apis.updateBanner(this.file); - }, - - addApp() { - alert('not implemented yet'); - }, - - deleteFile() { - alert('not implemented yet'); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.root.file - padding 8px 0 0 0 - height 180px - border-radius 4px - - &, * - cursor pointer - - &:hover - background rgba(0, 0, 0, 0.05) - - > .label - &:before - &:after - background #0b65a5 - - &:active - background rgba(0, 0, 0, 0.1) - - > .label - &:before - &:after - background #0b588c - - &[data-is-selected] - background $theme-color - - &:hover - background lighten($theme-color, 10%) - - &:active - background darken($theme-color, 10%) - - > .label - &:before - &:after - display none - - > .name - color $theme-color-foreground - - &[data-is-contextmenu-showing] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed rgba($theme-color, 0.3) - border-radius 4px - - > .label - position absolute - top 0 - left 0 - pointer-events none - - &:before - content "" - display block - position absolute - z-index 1 - top 0 - left 57px - width 28px - height 8px - background #0c7ac9 - - &:after - content "" - display block - position absolute - z-index 1 - top 57px - left 0 - width 8px - height 28px - background #0c7ac9 - - > img - position absolute - z-index 2 - top 0 - left 0 - - > p - position absolute - z-index 3 - top 19px - left -28px - width 120px - margin 0 - text-align center - line-height 28px - color #fff - transform rotate(-45deg) - - > .thumbnail - width 128px - height 128px - margin auto - - > img - display block - position absolute - top 0 - left 0 - right 0 - bottom 0 - margin auto - max-width 128px - max-height 128px - pointer-events none - - > .name - display block - margin 4px 0 0 0 - font-size 0.8em - text-align center - word-break break-all - color #444 - overflow hidden - - > .ext - opacity 0.5 - -</style> diff --git a/src/server/web/app/desktop/views/components/drive.folder.vue b/src/server/web/app/desktop/views/components/drive.folder.vue deleted file mode 100644 index a926bf47b2..0000000000 --- a/src/server/web/app/desktop/views/components/drive.folder.vue +++ /dev/null @@ -1,267 +0,0 @@ -<template> -<div class="root folder" - :data-is-contextmenu-showing="isContextmenuShowing" - :data-draghover="draghover" - @click="onClick" - @mouseover="onMouseover" - @mouseout="onMouseout" - @dragover.prevent.stop="onDragover" - @dragenter.prevent="onDragenter" - @dragleave="onDragleave" - @drop.prevent.stop="onDrop" - draggable="true" - @dragstart="onDragstart" - @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu" - :title="title" -> - <p class="name"> - <template v-if="hover">%fa:R folder-open .fw%</template> - <template v-if="!hover">%fa:R folder .fw%</template> - {{ folder.name }} - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import contextmenu from '../../api/contextmenu'; - -export default Vue.extend({ - props: ['folder'], - data() { - return { - hover: false, - draghover: false, - isDragging: false, - isContextmenuShowing: false - }; - }, - computed: { - browser(): any { - return this.$parent; - }, - title(): string { - return this.folder.name; - } - }, - methods: { - onClick() { - this.browser.move(this.folder); - }, - - onContextmenu(e) { - this.isContextmenuShowing = true; - contextmenu(e, [{ - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.move-to-this-folder%', - icon: '%fa:arrow-right%', - onClick: this.go - }, { - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.show-in-new-window%', - icon: '%fa:R window-restore%', - onClick: this.newWindow - }, { - type: 'divider', - }, { - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename%', - icon: '%fa:i-cursor%', - onClick: this.rename - }, { - type: 'divider', - }, { - type: 'item', - text: '%i18n:common.delete%', - icon: '%fa:R trash-alt%', - onClick: this.deleteFolder - }], { - closed: () => { - this.isContextmenuShowing = false; - } - }); - }, - - onMouseover() { - this.hover = true; - }, - - onMouseout() { - this.hover = false - }, - - onDragover(e) { - // 自分自身がドラッグされている場合 - if (this.isDragging) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } - - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; - - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - }, - - onDragenter() { - if (!this.isDragging) this.draghover = true; - }, - - onDragleave() { - this.draghover = false; - }, - - onDrop(e) { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - Array.from(e.dataTransfer.files).forEach(file => { - this.browser.upload(file, this.folder); - }); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.browser.removeFile(file.id); - (this as any).api('drive/files/update', { - fileId: file.id, - folderId: this.folder.id - }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData('mk_drive_folder'); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - - // 移動先が自分自身ならreject - if (folder.id == this.folder.id) return; - - this.browser.removeFolder(folder.id); - (this as any).api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder.id - }).then(() => { - // noop - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - (this as any).apis.dialog({ - title: '%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%', - text: '%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%', - actions: [{ - text: '%i18n:common.ok%' - }] - }); - break; - default: - alert('%i18n:desktop.tags.mk-drive-browser-folder.unhandled-error% ' + err); - } - }); - } - //#endregion - }, - - onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder)); - this.isDragging = true; - - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }, - - onDragend() { - this.isDragging = false; - this.browser.isDragSource = false; - }, - - go() { - this.browser.move(this.folder.id); - }, - - newWindow() { - this.browser.newWindow(this.folder); - }, - - rename() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%', - placeholder: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%', - default: this.folder.name - }).then(name => { - (this as any).api('drive/folders/update', { - folderId: this.folder.id, - name: name - }); - }); - }, - - deleteFolder() { - alert('not implemented yet'); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.root.folder - padding 8px - height 64px - background lighten($theme-color, 95%) - border-radius 4px - - &, * - cursor pointer - - * - pointer-events none - - &:hover - background lighten($theme-color, 90%) - - &:active - background lighten($theme-color, 85%) - - &[data-is-contextmenu-showing] - &[data-draghover] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed rgba($theme-color, 0.3) - border-radius 4px - - &[data-draghover] - background lighten($theme-color, 90%) - - > .name - margin 0 - font-size 0.9em - color darken($theme-color, 30%) - - > [data-fa] - margin-right 4px - margin-left 2px - text-align left - -</style> diff --git a/src/server/web/app/desktop/views/components/drive.nav-folder.vue b/src/server/web/app/desktop/views/components/drive.nav-folder.vue deleted file mode 100644 index d885a72f7f..0000000000 --- a/src/server/web/app/desktop/views/components/drive.nav-folder.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div class="root nav-folder" - :data-draghover="draghover" - @click="onClick" - @dragover.prevent.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" -> - <template v-if="folder == null">%fa:cloud%</template> - <span>{{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }}</span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['folder'], - data() { - return { - hover: false, - draghover: false - }; - }, - computed: { - browser(): any { - return this.$parent; - } - }, - methods: { - onClick() { - this.browser.move(this.folder); - }, - onMouseover() { - this.hover = true; - }, - onMouseout() { - this.hover = false; - }, - onDragover(e) { - // このフォルダがルートかつカレントディレクトリならドロップ禁止 - if (this.folder == null && this.browser.folder == null) { - e.dataTransfer.dropEffect = 'none'; - } - - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; - - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - - return false; - }, - onDragenter() { - if (this.folder || this.browser.folder) this.draghover = true; - }, - onDragleave() { - if (this.folder || this.browser.folder) this.draghover = false; - }, - onDrop(e) { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - Array.from(e.dataTransfer.files).forEach(file => { - this.browser.upload(file, this.folder); - }); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.browser.removeFile(file.id); - (this as any).api('drive/files/update', { - fileId: file.id, - folderId: this.folder ? this.folder.id : null - }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData('mk_drive_folder'); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - // 移動先が自分自身ならreject - if (this.folder && folder.id == this.folder.id) return; - this.browser.removeFolder(folder.id); - (this as any).api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder ? this.folder.id : null - }); - } - //#endregion - } - } -}); -</script> - -<style lang="stylus" scoped> -.root.nav-folder - > * - pointer-events none - - &[data-draghover] - background #eee - -</style> diff --git a/src/server/web/app/desktop/views/components/drive.vue b/src/server/web/app/desktop/views/components/drive.vue deleted file mode 100644 index c766dfec12..0000000000 --- a/src/server/web/app/desktop/views/components/drive.vue +++ /dev/null @@ -1,773 +0,0 @@ -<template> -<div class="mk-drive"> - <nav> - <div class="path" @contextmenu.prevent.stop="() => {}"> - <x-nav-folder :class="{ current: folder == null }"/> - <template v-for="folder in hierarchyFolders"> - <span class="separator">%fa:angle-right%</span> - <x-nav-folder :folder="folder" :key="folder.id"/> - </template> - <span class="separator" v-if="folder != null">%fa:angle-right%</span> - <span class="folder current" v-if="folder != null">{{ folder.name }}</span> - </div> - <input class="search" type="search" placeholder=" %i18n:desktop.tags.mk-drive-browser.search%"/> - </nav> - <div class="main" :class="{ uploading: uploadings.length > 0, fetching }" - ref="main" - @mousedown="onMousedown" - @dragover.prevent.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.prevent.stop="onDrop" - @contextmenu.prevent.stop="onContextmenu" - > - <div class="selection" ref="selection"></div> - <div class="contents" ref="contents"> - <div class="folders" ref="foldersContainer" v-if="folders.length > 0"> - <x-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/> - <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> - <div class="padding" v-for="n in 16"></div> - <button v-if="moreFolders">%i18n:desktop.tags.mk-drive-browser.load-more%</button> - </div> - <div class="files" ref="filesContainer" v-if="files.length > 0"> - <x-file v-for="file in files" :key="file.id" class="file" :file="file"/> - <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> - <div class="padding" v-for="n in 16"></div> - <button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button> - </div> - <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> - <p v-if="draghover">%i18n:desktop.tags.mk-drive-browser.empty-draghover%</p> - <p v-if="!draghover && folder == null"><strong>%i18n:desktop.tags.mk-drive-browser.empty-drive%</strong><br/>%i18n:desktop.tags.mk-drive-browser.empty-drive-description%</p> - <p v-if="!draghover && folder != null">%i18n:desktop.tags.mk-drive-browser.empty-folder%</p> - </div> - </div> - <div class="fetching" v-if="fetching"> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - </div> - <div class="dropzone" v-if="draghover"></div> - <mk-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/> - <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkDriveWindow from './drive-window.vue'; -import XNavFolder from './drive.nav-folder.vue'; -import XFolder from './drive.folder.vue'; -import XFile from './drive.file.vue'; -import contains from '../../../common/scripts/contains'; -import contextmenu from '../../api/contextmenu'; -import { url } from '../../../config'; - -export default Vue.extend({ - components: { - XNavFolder, - XFolder, - XFile - }, - props: { - initFolder: { - type: Object, - required: false - }, - multiple: { - type: Boolean, - default: false - } - }, - data() { - return { - /** - * 現在の階層(フォルダ) - * * null でルートを表す - */ - folder: null, - - files: [], - folders: [], - moreFiles: false, - moreFolders: false, - hierarchyFolders: [], - selectedFiles: [], - uploadings: [], - connection: null, - connectionId: null, - - /** - * ドロップされようとしているか - */ - draghover: false, - - /** - * 自信の所有するアイテムがドラッグをスタートさせたか - * (自分自身の階層にドロップできないようにするためのフラグ) - */ - isDragSource: false, - - fetching: true - }; - }, - mounted() { - this.connection = (this as any).os.streams.driveStream.getConnection(); - this.connectionId = (this as any).os.streams.driveStream.use(); - - this.connection.on('file_created', this.onStreamDriveFileCreated); - this.connection.on('file_updated', this.onStreamDriveFileUpdated); - this.connection.on('folder_created', this.onStreamDriveFolderCreated); - this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); - - if (this.initFolder) { - this.move(this.initFolder); - } else { - this.fetch(); - } - }, - beforeDestroy() { - this.connection.off('file_created', this.onStreamDriveFileCreated); - this.connection.off('file_updated', this.onStreamDriveFileUpdated); - this.connection.off('folder_created', this.onStreamDriveFolderCreated); - this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); - (this as any).os.streams.driveStream.dispose(this.connectionId); - }, - methods: { - onContextmenu(e) { - contextmenu(e, [{ - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%', - icon: '%fa:R folder%', - onClick: this.createFolder - }, { - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%', - icon: '%fa:upload%', - onClick: this.selectLocalFile - }, { - type: 'item', - text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%', - icon: '%fa:cloud-upload-alt%', - onClick: this.urlUpload - }]); - }, - - onStreamDriveFileCreated(file) { - this.addFile(file, true); - }, - - onStreamDriveFileUpdated(file) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }, - - onStreamDriveFolderCreated(folder) { - this.addFolder(folder, true); - }, - - onStreamDriveFolderUpdated(folder) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }, - - onChangeUploaderUploads(uploads) { - this.uploadings = uploads; - }, - - onUploaderUploaded(file) { - this.addFile(file, true); - }, - - onMousedown(e): any { - if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true; - - const main = this.$refs.main as any; - const selection = this.$refs.selection as any; - - const rect = main.getBoundingClientRect(); - - const left = e.pageX + main.scrollLeft - rect.left - window.pageXOffset - const top = e.pageY + main.scrollTop - rect.top - window.pageYOffset - - const move = e => { - selection.style.display = 'block'; - - const cursorX = e.pageX + main.scrollLeft - rect.left - window.pageXOffset; - const cursorY = e.pageY + main.scrollTop - rect.top - window.pageYOffset; - const w = cursorX - left; - const h = cursorY - top; - - if (w > 0) { - selection.style.width = w + 'px'; - selection.style.left = left + 'px'; - } else { - selection.style.width = -w + 'px'; - selection.style.left = cursorX + 'px'; - } - - if (h > 0) { - selection.style.height = h + 'px'; - selection.style.top = top + 'px'; - } else { - selection.style.height = -h + 'px'; - selection.style.top = cursorY + 'px'; - } - }; - - const up = e => { - document.documentElement.removeEventListener('mousemove', move); - document.documentElement.removeEventListener('mouseup', up); - - selection.style.display = 'none'; - }; - - document.documentElement.addEventListener('mousemove', move); - document.documentElement.addEventListener('mouseup', up); - }, - - onDragover(e): any { - // ドラッグ元が自分自身の所有するアイテムだったら - if (this.isDragSource) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } - - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; - - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - - return false; - }, - - onDragenter(e) { - if (!this.isDragSource) this.draghover = true; - }, - - onDragleave(e) { - this.draghover = false; - }, - - onDrop(e): any { - this.draghover = false; - - // ドロップされてきたものがファイルだったら - if (e.dataTransfer.files.length > 0) { - Array.from(e.dataTransfer.files).forEach(file => { - this.upload(file, this.folder); - }); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - if (this.files.some(f => f.id == file.id)) return; - this.removeFile(file.id); - (this as any).api('drive/files/update', { - fileId: file.id, - folderId: this.folder ? this.folder.id : null - }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData('mk_drive_folder'); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - - // 移動先が自分自身ならreject - if (this.folder && folder.id == this.folder.id) return false; - if (this.folders.some(f => f.id == folder.id)) return false; - this.removeFolder(folder.id); - (this as any).api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder ? this.folder.id : null - }).then(() => { - // noop - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - (this as any).apis.dialog({ - title: '%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser.unable-to-process%', - text: '%i18n:desktop.tags.mk-drive-browser.circular-reference-detected%', - actions: [{ - text: '%i18n:common.ok%' - }] - }); - break; - default: - alert('%i18n:desktop.tags.mk-drive-browser.unhandled-error% ' + err); - } - }); - } - //#endregion - }, - - selectLocalFile() { - (this.$refs.fileInput as any).click(); - }, - - urlUpload() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-drive-browser.url-upload%', - placeholder: '%i18n:desktop.tags.mk-drive-browser.url-of-file%' - }).then(url => { - (this as any).api('drive/files/upload_from_url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); - - (this as any).apis.dialog({ - title: '%fa:check%%i18n:desktop.tags.mk-drive-browser.url-upload-requested%', - text: '%i18n:desktop.tags.mk-drive-browser.may-take-time%', - actions: [{ - text: '%i18n:common.ok%' - }] - }); - }); - }, - - createFolder() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-drive-browser.create-folder%', - placeholder: '%i18n:desktop.tags.mk-drive-browser.folder-name%' - }).then(name => { - (this as any).api('drive/folders/create', { - name: name, - folderId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - }); - }); - }, - - onChangeFileInput() { - Array.from((this.$refs.fileInput as any).files).forEach(file => { - this.upload(file, this.folder); - }); - }, - - upload(file, folder) { - if (folder && typeof folder == 'object') folder = folder.id; - (this.$refs.uploader as any).upload(file, folder); - }, - - chooseFile(file) { - const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); - if (this.multiple) { - if (isAlreadySelected) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.$emit('change-selection', this.selectedFiles); - } else { - if (isAlreadySelected) { - this.$emit('selected', file); - } else { - this.selectedFiles = [file]; - this.$emit('change-selection', [file]); - } - } - }, - - newWindow(folder) { - if (document.body.clientWidth > 800) { - (this as any).os.new(MkDriveWindow, { - folder: folder - }); - } else { - window.open(url + '/i/drive/folder/' + folder.id, - 'drive_window', - 'height=500, width=800'); - } - }, - - move(target) { - if (target == null) { - this.goRoot(); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.fetching = true; - - (this as any).api('drive/folders/show', { - folderId: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - const dive = folder => { - this.hierarchyFolders.unshift(folder); - if (folder.parent) dive(folder.parent); - }; - - if (folder.parent) dive(folder.parent); - - this.$emit('open-folder', folder); - this.fetch(); - }); - }, - - addFolder(folder, unshift = false) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) return; - - if (this.folders.some(f => f.id == folder.id)) { - const exist = this.folders.map(f => f.id).indexOf(folder.id); - Vue.set(this.folders, exist, folder); - return; - } - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - }, - - addFile(file, unshift = false) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - Vue.set(this.files, exist, file); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - }, - - removeFolder(folder) { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - }, - - removeFile(file) { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - }, - - appendFile(file) { - this.addFile(file); - }, - - appendFolder(folder) { - this.addFolder(folder); - }, - - prependFile(file) { - this.addFile(file, true); - }, - - prependFolder(folder) { - this.addFolder(folder, true); - }, - - goRoot() { - // 既にrootにいるなら何もしない - if (this.folder == null) return; - - this.folder = null; - this.hierarchyFolders = []; - this.$emit('move-root'); - this.fetch(); - }, - - fetch() { - this.folders = []; - this.files = []; - this.moreFolders = false; - this.moreFiles = false; - this.fetching = true; - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 30; - const filesMax = 30; - - // フォルダ一覧取得 - (this as any).api('drive/folders', { - folderId: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - (this as any).api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - fetchedFolders.forEach(this.appendFolder); - fetchedFiles.forEach(this.appendFile); - this.fetching = false; - } else { - flag = true; - } - }; - }, - - fetchMoreFiles() { - this.fetching = true; - - const max = 30; - - // ファイル一覧取得 - (this as any).api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: max + 1 - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - files.forEach(this.appendFile); - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-drive - - > nav - display block - z-index 2 - width 100% - overflow auto - font-size 0.9em - color #555 - background #fff - //border-bottom 1px solid #dfdfdf - box-shadow 0 1px 0 rgba(0, 0, 0, 0.05) - - &, * - user-select none - - > .path - display inline-block - vertical-align bottom - margin 0 - padding 0 8px - width calc(100% - 200px) - line-height 38px - white-space nowrap - - > * - display inline-block - margin 0 - padding 0 8px - line-height 38px - cursor pointer - - i - margin-right 4px - - * - pointer-events none - - &:hover - text-decoration underline - - &.current - font-weight bold - cursor default - - &:hover - text-decoration none - - &.separator - margin 0 - padding 0 - opacity 0.5 - cursor default - - > [data-fa] - margin 0 - - > .search - display inline-block - vertical-align bottom - user-select text - cursor auto - margin 0 - padding 0 18px - width 200px - font-size 1em - line-height 38px - background transparent - outline none - //border solid 1px #ddd - border none - border-radius 0 - box-shadow none - transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif - - &[data-active='true'] - background #fff - - &::-webkit-input-placeholder, - &:-ms-input-placeholder, - &:-moz-placeholder - color $ui-control-foreground-color - - > .main - padding 8px - height calc(100% - 38px) - overflow auto - - &, * - user-select none - - &.fetching - cursor wait !important - - * - pointer-events none - - > .contents - opacity 0.5 - - &.uploading - height calc(100% - 38px - 100px) - - > .selection - display none - position absolute - z-index 128 - top 0 - left 0 - border solid 1px $theme-color - background rgba($theme-color, 0.5) - pointer-events none - - > .contents - - > .folders - > .files - display flex - flex-wrap wrap - - > .folder - > .file - flex-grow 1 - width 144px - margin 4px - - > .padding - flex-grow 1 - pointer-events none - width 144px + 8px // 8px is margin - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background-color rgba(0, 0, 0, 0.3) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { 100% { transform: rotate(360deg); }} - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } 50% { - transform: scale(1.0); - } - } - - > .dropzone - position absolute - left 0 - top 38px - width 100% - height calc(100% - 38px) - border dashed 2px rgba($theme-color, 0.5) - pointer-events none - - > .mk-uploader - height 100px - padding 16px - background #fff - - > input - display none - -</style> diff --git a/src/server/web/app/desktop/views/components/ellipsis-icon.vue b/src/server/web/app/desktop/views/components/ellipsis-icon.vue deleted file mode 100644 index c54a7db29d..0000000000 --- a/src/server/web/app/desktop/views/components/ellipsis-icon.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<div class="mk-ellipsis-icon"> - <div></div><div></div><div></div> -</div> -</template> - -<style lang="stylus" scoped> -.mk-ellipsis-icon - width 70px - margin 0 auto - text-align center - - > div - display inline-block - width 18px - height 18px - background-color rgba(0, 0, 0, 0.3) - border-radius 100% - animation bounce 1.4s infinite ease-in-out both - - &:nth-child(1) - animation-delay 0s - - &:nth-child(2) - margin 0 6px - animation-delay 0.16s - - &:nth-child(3) - animation-delay 0.32s - - @keyframes bounce - 0%, 80%, 100% - transform scale(0) - 40% - transform scale(1) - -</style> diff --git a/src/server/web/app/desktop/views/components/follow-button.vue b/src/server/web/app/desktop/views/components/follow-button.vue deleted file mode 100644 index 9eb22b0fb8..0000000000 --- a/src/server/web/app/desktop/views/components/follow-button.vue +++ /dev/null @@ -1,164 +0,0 @@ -<template> -<button class="mk-follow-button" - :class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }" - @click="onClick" - :disabled="wait" - :title="user.isFollowing ? 'フォロー解除' : 'フォローする'" -> - <template v-if="!wait && user.isFollowing"> - <template v-if="size == 'compact'">%fa:minus%</template> - <template v-if="size == 'big'">%fa:minus%フォロー解除</template> - </template> - <template v-if="!wait && !user.isFollowing"> - <template v-if="size == 'compact'">%fa:plus%</template> - <template v-if="size == 'big'">%fa:plus%フォロー</template> - </template> - <template v-if="wait">%fa:spinner .pulse .fw%</template> -</button> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - user: { - type: Object, - required: true - }, - size: { - type: String, - default: 'compact' - } - }, - data() { - return { - wait: false, - connection: null, - connectionId: null - }; - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('follow', this.onFollow); - this.connection.on('unfollow', this.onUnfollow); - }, - beforeDestroy() { - this.connection.off('follow', this.onFollow); - this.connection.off('unfollow', this.onUnfollow); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - - onFollow(user) { - if (user.id == this.user.id) { - this.user.isFollowing = user.isFollowing; - } - }, - - onUnfollow(user) { - if (user.id == this.user.id) { - this.user.isFollowing = user.isFollowing; - } - }, - - onClick() { - this.wait = true; - if (this.user.isFollowing) { - (this as any).api('following/delete', { - userId: this.user.id - }).then(() => { - this.user.isFollowing = false; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - }); - } else { - (this as any).api('following/create', { - userId: this.user.id - }).then(() => { - this.user.isFollowing = true; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-follow-button - display block - cursor pointer - padding 0 - margin 0 - width 32px - height 32px - font-size 1em - outline none - border-radius 4px - - * - pointer-events none - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &.follow - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - &.unfollow - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - &.wait - cursor wait !important - opacity 0.7 - - &.big - width 100% - height 38px - line-height 38px - - i - margin-right 8px - -</style> diff --git a/src/server/web/app/desktop/views/components/followers-window.vue b/src/server/web/app/desktop/views/components/followers-window.vue deleted file mode 100644 index 623971fa33..0000000000 --- a/src/server/web/app/desktop/views/components/followers-window.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<mk-window width="400px" height="550px" @closed="$destroy"> - <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー - </span> - <mk-followers :user="user"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'] -}); -</script> - -<style lang="stylus" module> -.header - > img - display inline-block - vertical-align bottom - height calc(100% - 10px) - margin 5px - border-radius 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/followers.vue b/src/server/web/app/desktop/views/components/followers.vue deleted file mode 100644 index a1b98995d8..0000000000 --- a/src/server/web/app/desktop/views/components/followers.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<mk-users-list - :fetch="fetch" - :count="user.followersCount" - :you-know-count="user.followersYouKnowCount" -> - フォロワーはいないようです。 -</mk-users-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - methods: { - fetch(iknow, limit, cursor, cb) { - (this as any).api('users/followers', { - userId: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/components/following-window.vue b/src/server/web/app/desktop/views/components/following-window.vue deleted file mode 100644 index 612847b386..0000000000 --- a/src/server/web/app/desktop/views/components/following-window.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<mk-window width="400px" height="550px" @closed="$destroy"> - <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー - </span> - <mk-following :user="user"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'] -}); -</script> - -<style lang="stylus" module> -.header - > img - display inline-block - vertical-align bottom - height calc(100% - 10px) - margin 5px - border-radius 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/following.vue b/src/server/web/app/desktop/views/components/following.vue deleted file mode 100644 index b7aedda84f..0000000000 --- a/src/server/web/app/desktop/views/components/following.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<mk-users-list - :fetch="fetch" - :count="user.followingCount" - :you-know-count="user.followingYouKnowCount" -> - フォロー中のユーザーはいないようです。 -</mk-users-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - methods: { - fetch(iknow, limit, cursor, cb) { - (this as any).api('users/following', { - userId: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/components/friends-maker.vue b/src/server/web/app/desktop/views/components/friends-maker.vue deleted file mode 100644 index fd9914b152..0000000000 --- a/src/server/web/app/desktop/views/components/friends-maker.vue +++ /dev/null @@ -1,171 +0,0 @@ -<template> -<div class="mk-friends-maker"> - <p class="title">気になるユーザーをフォロー:</p> - <div class="users" v-if="!fetching && users.length > 0"> - <div class="user" v-for="user in users" :key="user.id"> - <router-link class="avatar-anchor" :to="`/@${getAcct(user)}`"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/> - </router-link> - <div class="body"> - <router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link> - <p class="username">@{{ getAcct(user) }}</p> - </div> - <mk-follow-button :user="user"/> - </div> - </div> - <p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p> - <a class="refresh" @click="refresh">もっと見る</a> - <button class="close" @click="$destroy()" title="閉じる">%fa:times%</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - data() { - return { - users: [], - fetching: true, - limit: 6, - page: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - getAcct, - fetch() { - this.fetching = true; - this.users = []; - - (this as any).api('users/recommendation', { - limit: this.limit, - offset: this.limit * this.page - }).then(users => { - this.users = users; - this.fetching = false; - }); - }, - refresh() { - if (this.users.length < this.limit) { - this.page = 0; - } else { - this.page++; - } - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-friends-maker - padding 24px - - > .title - margin 0 0 12px 0 - font-size 1em - font-weight bold - color #888 - - > .users - &:after - content "" - display block - clear both - - > .user - padding 16px - width 238px - float left - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - margin 0 - font-size 15px - line-height 16px - color #ccc - - > .mk-follow-button - position absolute - top 16px - right 16px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - - > .refresh - display block - margin 0 8px 0 0 - text-align right - font-size 0.9em - color #999 - - > .close - cursor pointer - display block - position absolute - top 6px - right 6px - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - background transparent - - &:hover - color #555 - - &:active - color #222 - - > [data-fa] - padding 14px - -</style> diff --git a/src/server/web/app/desktop/views/components/game-window.vue b/src/server/web/app/desktop/views/components/game-window.vue deleted file mode 100644 index 3c8bf40e12..0000000000 --- a/src/server/web/app/desktop/views/components/game-window.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:gamepad%オセロ</span> - <mk-othello :class="$style.content" @gamed="g => game = g"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url } from '../../../config'; - -export default Vue.extend({ - data() { - return { - game: null - }; - }, - computed: { - popout(): string { - return this.game - ? `${url}/othello/${this.game.id}` - : `${url}/othello`; - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -.content - height 100% - overflow auto - -</style> diff --git a/src/server/web/app/desktop/views/components/home.vue b/src/server/web/app/desktop/views/components/home.vue deleted file mode 100644 index 7145ddce03..0000000000 --- a/src/server/web/app/desktop/views/components/home.vue +++ /dev/null @@ -1,357 +0,0 @@ -<template> -<div class="mk-home" :data-customize="customize"> - <div class="customize" v-if="customize"> - <router-link to="/">%fa:check%完了</router-link> - <div> - <div class="adder"> - <p>ウィジェットを追加:</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> - </select> - <button @click="addWidget">追加</button> - </div> - <div class="trash"> - <x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> - <p>ゴミ箱</p> - </div> - </div> - </div> - <div class="main"> - <template v-if="customize"> - <x-draggable v-for="place in ['left', 'right']" - :list="widgets[place]" - :class="place" - :data-place="place" - :options="{ group: 'x', animation: 150 }" - @sort="onWidgetSort" - :key="place" - > - <div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/> - </div> - </x-draggable> - <div class="main"> - <a @click="hint">カスタマイズのヒント</a> - <div> - <mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/> - <mk-timeline ref="tl" @loaded="onTlLoaded"/> - </div> - </div> - </template> - <template v-else> - <div v-for="place in ['left', 'right']" :class="place"> - <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/> - </div> - <div class="main"> - <mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/> - <mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> - <mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/> - </div> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import * as uuid from 'uuid'; - -export default Vue.extend({ - components: { - XDraggable - }, - props: { - customize: Boolean, - mode: { - type: String, - default: 'timeline' - } - }, - data() { - return { - connection: null, - connectionId: null, - widgetAdderSelected: null, - trash: [], - widgets: { - left: [], - right: [] - } - }; - }, - computed: { - home: { - get(): any[] { - //#region 互換性のため - (this as any).os.i.account.clientSettings.home.forEach(w => { - if (w.name == 'rss-reader') w.name = 'rss'; - if (w.name == 'user-recommendation') w.name = 'users'; - if (w.name == 'recommended-polls') w.name = 'polls'; - }); - //#endregion - return (this as any).os.i.account.clientSettings.home; - }, - set(value) { - (this as any).os.i.account.clientSettings.home = value; - } - }, - left(): any[] { - return this.home.filter(w => w.place == 'left'); - }, - right(): any[] { - return this.home.filter(w => w.place == 'right'); - } - }, - created() { - this.widgets.left = this.left; - this.widgets.right = this.right; - this.$watch('os.i.account.clientSettings', i => { - this.widgets.left = this.left; - this.widgets.right = this.right; - }, { - deep: true - }); - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('home_updated', this.onHomeUpdated); - }, - beforeDestroy() { - this.connection.off('home_updated', this.onHomeUpdated); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - hint() { - (this as any).apis.dialog({ - title: '%fa:info-circle%カスタマイズのヒント', - text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' + - '<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' + - '<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' + - '<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>', - actions: [{ - text: 'Got it!' - }] - }); - }, - onTlLoaded() { - this.$emit('loaded'); - }, - onHomeUpdated(data) { - if (data.home) { - (this as any).os.i.account.clientSettings.home = data.home; - this.widgets.left = data.home.filter(w => w.place == 'left'); - this.widgets.right = data.home.filter(w => w.place == 'right'); - } else { - const w = (this as any).os.i.account.clientSettings.home.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.left = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'left'); - this.widgets.right = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'right'); - } - } - }, - onWidgetContextmenu(widgetId) { - const w = (this.$refs[widgetId] as any)[0]; - if (w.func) w.func(); - }, - onWidgetSort() { - this.saveHome(); - }, - onTrash(evt) { - this.saveHome(); - }, - addWidget() { - const widget = { - name: this.widgetAdderSelected, - id: uuid(), - place: 'left', - data: {} - }; - - this.widgets.left.unshift(widget); - this.saveHome(); - }, - saveHome() { - const left = this.widgets.left; - const right = this.widgets.right; - this.home = left.concat(right); - left.forEach(w => w.place = 'left'); - right.forEach(w => w.place = 'right'); - (this as any).api('i/update_home', { - home: this.home - }); - }, - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-home - display block - - &[data-customize] - padding-top 48px - background-image url('/assets/desktop/grid.svg') - - > .main > .main - > a - display block - margin-bottom 8px - text-align center - - > div - cursor not-allowed !important - - > * - pointer-events none - - &:not([data-customize]) - > .main > *:empty - display none - - > .customize - position fixed - z-index 1000 - top 0 - left 0 - width 100% - height 48px - background #f7f7f7 - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) - - > a - display block - position absolute - z-index 1001 - top 0 - right 0 - padding 0 16px - line-height 48px - text-decoration none - color $theme-color-foreground - background $theme-color - transition background 0.1s ease - - &:hover - background lighten($theme-color, 10%) - - &:active - background darken($theme-color, 10%) - transition background 0s ease - - > [data-fa] - margin-right 8px - - > div - display flex - margin 0 auto - max-width 1200px - 32px - - > div - width 50% - - &.adder - > p - display inline - line-height 48px - - &.trash - border-left solid 1px #ddd - - > div - width 100% - height 100% - - > p - position absolute - top 0 - left 0 - width 100% - line-height 48px - margin 0 - text-align center - pointer-events none - - > .main - display flex - justify-content center - margin 0 auto - max-width 1200px - - > * - .customize-container - cursor move - border-radius 6px - - &:hover - box-shadow 0 0 8px rgba(64, 120, 200, 0.3) - - > * - pointer-events none - - > .main - padding 16px - width calc(100% - 275px * 2) - order 2 - - .mk-post-form - margin-bottom 16px - border solid 1px #e5e5e5 - border-radius 4px - - > *:not(.main) - width 275px - padding 16px 0 16px 0 - - > *:not(:last-child) - margin-bottom 16px - - > .left - padding-left 16px - order 1 - - > .right - padding-right 16px - order 3 - - @media (max-width 1100px) - > *:not(.main) - display none - - > .main - float none - width 100% - max-width 700px - margin 0 auto - -</style> diff --git a/src/server/web/app/desktop/views/components/index.ts b/src/server/web/app/desktop/views/components/index.ts deleted file mode 100644 index 3798bf6d2d..0000000000 --- a/src/server/web/app/desktop/views/components/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import uiNotification from './ui-notification.vue'; -import home from './home.vue'; -import timeline from './timeline.vue'; -import posts from './posts.vue'; -import subPostContent from './sub-post-content.vue'; -import window from './window.vue'; -import postFormWindow from './post-form-window.vue'; -import repostFormWindow from './repost-form-window.vue'; -import analogClock from './analog-clock.vue'; -import ellipsisIcon from './ellipsis-icon.vue'; -import mediaImage from './media-image.vue'; -import mediaImageDialog from './media-image-dialog.vue'; -import mediaVideo from './media-video.vue'; -import notifications from './notifications.vue'; -import postForm from './post-form.vue'; -import repostForm from './repost-form.vue'; -import followButton from './follow-button.vue'; -import postPreview from './post-preview.vue'; -import drive from './drive.vue'; -import postDetail from './post-detail.vue'; -import settings from './settings.vue'; -import calendar from './calendar.vue'; -import activity from './activity.vue'; -import friendsMaker from './friends-maker.vue'; -import followers from './followers.vue'; -import following from './following.vue'; -import usersList from './users-list.vue'; -import widgetContainer from './widget-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-ui-notification', uiNotification); -Vue.component('mk-home', home); -Vue.component('mk-timeline', timeline); -Vue.component('mk-posts', posts); -Vue.component('mk-sub-post-content', subPostContent); -Vue.component('mk-window', window); -Vue.component('mk-post-form-window', postFormWindow); -Vue.component('mk-repost-form-window', repostFormWindow); -Vue.component('mk-analog-clock', analogClock); -Vue.component('mk-ellipsis-icon', ellipsisIcon); -Vue.component('mk-media-image', mediaImage); -Vue.component('mk-media-image-dialog', mediaImageDialog); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-notifications', notifications); -Vue.component('mk-post-form', postForm); -Vue.component('mk-repost-form', repostForm); -Vue.component('mk-follow-button', followButton); -Vue.component('mk-post-preview', postPreview); -Vue.component('mk-drive', drive); -Vue.component('mk-post-detail', postDetail); -Vue.component('mk-settings', settings); -Vue.component('mk-calendar', calendar); -Vue.component('mk-activity', activity); -Vue.component('mk-friends-maker', friendsMaker); -Vue.component('mk-followers', followers); -Vue.component('mk-following', following); -Vue.component('mk-users-list', usersList); -Vue.component('mk-widget-container', widgetContainer); diff --git a/src/server/web/app/desktop/views/components/input-dialog.vue b/src/server/web/app/desktop/views/components/input-dialog.vue deleted file mode 100644 index e939fc1903..0000000000 --- a/src/server/web/app/desktop/views/components/input-dialog.vue +++ /dev/null @@ -1,180 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy"> - <span slot="header" :class="$style.header"> - %fa:i-cursor%{{ title }} - </span> - - <div :class="$style.body"> - <input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/> - </div> - <div :class="$style.actions"> - <button :class="$style.cancel" @click="cancel">キャンセル</button> - <button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">決定</button> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - title: { - type: String - }, - placeholder: { - type: String - }, - default: { - type: String - }, - allowEmpty: { - default: true - }, - type: { - default: 'text' - } - }, - data() { - return { - done: false, - text: '' - }; - }, - mounted() { - if (this.default) this.text = this.default; - this.$nextTick(() => { - (this.$refs.text as any).focus(); - }); - }, - methods: { - ok() { - if (!this.allowEmpty && this.text == '') return; - this.done = true; - (this.$refs.window as any).close(); - }, - cancel() { - this.done = false; - (this.$refs.window as any).close(); - }, - beforeClose() { - if (this.done) { - this.$emit('done', this.text); - } else { - this.$emit('canceled'); - } - }, - onKeydown(e) { - if (e.which == 13) { // Enter - e.preventDefault(); - e.stopPropagation(); - this.ok(); - } - } - } -}); -</script> - - -<style lang="stylus" module> -@import '~const.styl' - -.header - > [data-fa] - margin-right 4px - -.body - padding 16px - - > input - display block - padding 8px - margin 0 - width 100% - max-width 100% - min-width 100% - font-size 1em - color #333 - background #fff - outline none - border solid 1px rgba($theme-color, 0.1) - border-radius 4px - transition border-color .3s ease - - &:hover - border-color rgba($theme-color, 0.2) - transition border-color .1s ease - - &:focus - color $theme-color - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - - &::-webkit-input-placeholder - color rgba($theme-color, 0.3) - -.actions - height 72px - background lighten($theme-color, 95%) - -.ok -.cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - -.ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - -.cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -</style> diff --git a/src/server/web/app/desktop/views/components/media-image-dialog.vue b/src/server/web/app/desktop/views/components/media-image-dialog.vue deleted file mode 100644 index dec140d1c9..0000000000 --- a/src/server/web/app/desktop/views/components/media-image-dialog.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<div class="mk-media-image-dialog"> - <div class="bg" @click="close"></div> - <img :src="image.url" :alt="image.name" :title="image.name" @click="close"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: ['image'], - mounted() { - anime({ - targets: this.$el, - opacity: 1, - duration: 100, - easing: 'linear' - }); - }, - methods: { - close() { - anime({ - targets: this.$el, - opacity: 0, - duration: 100, - easing: 'linear', - complete: () => this.$destroy() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-media-image-dialog - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - opacity 0 - - > .bg - display block - position fixed - z-index 1 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - - > img - position fixed - z-index 2 - top 0 - right 0 - bottom 0 - left 0 - max-width 100% - max-height 100% - margin auto - cursor zoom-out - -</style> diff --git a/src/server/web/app/desktop/views/components/media-image.vue b/src/server/web/app/desktop/views/components/media-image.vue deleted file mode 100644 index 51309a0578..0000000000 --- a/src/server/web/app/desktop/views/components/media-image.vue +++ /dev/null @@ -1,63 +0,0 @@ -<template> -<a class="mk-media-image" - :href="image.url" - @mousemove="onMousemove" - @mouseleave="onMouseleave" - @click.prevent="onClick" - :style="style" - :title="image.name" -></a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkMediaImageDialog from './media-image-dialog.vue'; - -export default Vue.extend({ - props: ['image'], - computed: { - style(): any { - return { - 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${this.image.url}?thumbnail&size=512)` - }; - } - }, - methods: { - onMousemove(e) { - const rect = this.$el.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - const xp = mouseX / this.$el.offsetWidth * 100; - const yp = mouseY / this.$el.offsetHeight * 100; - this.$el.style.backgroundPosition = xp + '% ' + yp + '%'; - this.$el.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")'; - }, - - onMouseleave() { - this.$el.style.backgroundPosition = ''; - }, - - onClick() { - (this as any).os.new(MkMediaImageDialog, { - image: this.image - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-media-image - display block - cursor zoom-in - overflow hidden - width 100% - height 100% - background-position center - border-radius 4px - - &:not(:hover) - background-size cover - -</style> diff --git a/src/server/web/app/desktop/views/components/media-video-dialog.vue b/src/server/web/app/desktop/views/components/media-video-dialog.vue deleted file mode 100644 index cbf862cd1c..0000000000 --- a/src/server/web/app/desktop/views/components/media-video-dialog.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<div class="mk-media-video-dialog"> - <div class="bg" @click="close"></div> - <video :src="video.url" :title="video.name" controls autoplay ref="video"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: ['video', 'start'], - mounted() { - anime({ - targets: this.$el, - opacity: 1, - duration: 100, - easing: 'linear' - }); - const videoTag = this.$refs.video as HTMLVideoElement - if (this.start) videoTag.currentTime = this.start - }, - methods: { - close() { - anime({ - targets: this.$el, - opacity: 0, - duration: 100, - easing: 'linear', - complete: () => this.$destroy() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-media-video-dialog - display block - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - opacity 0 - - > .bg - display block - position fixed - z-index 1 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - - > video - position fixed - z-index 2 - top 0 - right 0 - bottom 0 - left 0 - max-width 80vw - max-height 80vh - margin auto - -</style> diff --git a/src/server/web/app/desktop/views/components/media-video.vue b/src/server/web/app/desktop/views/components/media-video.vue deleted file mode 100644 index 4fd955a821..0000000000 --- a/src/server/web/app/desktop/views/components/media-video.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> - <video class="mk-media-video" - :src="video.url" - :title="video.name" - controls - @dblclick.prevent="onClick" - ref="video" - v-if="inlinePlayable" /> - <a class="mk-media-video-thumbnail" - :href="video.url" - :style="imageStyle" - @click.prevent="onClick" - :title="video.name" - v-else> - %fa:R play-circle% - </a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkMediaVideoDialog from './media-video-dialog.vue'; - -export default Vue.extend({ - props: ['video', 'inlinePlayable'], - computed: { - imageStyle(): any { - return { - 'background-image': `url(${this.video.url}?thumbnail&size=512)` - }; - } - }, - methods: { - onClick() { - const videoTag = this.$refs.video as (HTMLVideoElement | null) - var start = 0 - if (videoTag) { - start = videoTag.currentTime - videoTag.pause() - } - (this as any).os.new(MkMediaVideoDialog, { - video: this.video, - start, - }) - } - } -}) -</script> - -<style lang="stylus" scoped> -.mk-media-video - display block - width 100% - height 100% - border-radius 4px -.mk-media-video-thumbnail - display flex - justify-content center - align-items center - font-size 3.5em - - cursor zoom-in - overflow hidden - background-position center - background-size cover - width 100% - height 100% -</style> diff --git a/src/server/web/app/desktop/views/components/mentions.vue b/src/server/web/app/desktop/views/components/mentions.vue deleted file mode 100644 index 90a92495b7..0000000000 --- a/src/server/web/app/desktop/views/components/mentions.vue +++ /dev/null @@ -1,125 +0,0 @@ -<template> -<div class="mk-mentions"> - <header> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて</span> - <span :data-is-active="mode == 'following'" @click="mode = 'following'">フォロー中</span> - </header> - <div class="fetching" v-if="fetching"> - <mk-ellipsis-icon/> - </div> - <p class="empty" v-if="posts.length == 0 && !fetching"> - %fa:R comments% - <span v-if="mode == 'all'">あなた宛ての投稿はありません。</span> - <span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span> - </p> - <mk-posts :posts="posts" ref="timeline"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - moreFetching: false, - mode: 'all', - posts: [] - }; - }, - watch: { - mode() { - this.fetch(); - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.fetch(() => this.$emit('loaded')); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.timeline as any).focus(); - } - } - }, - onScroll() { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); - }, - fetch(cb?) { - this.fetching = true; - this.posts = []; - (this as any).api('posts/mentions', { - following: this.mode == 'following' - }).then(posts => { - this.posts = posts; - this.fetching = false; - if (cb) cb(); - }); - }, - more() { - if (this.moreFetching || this.fetching || this.posts.length == 0) return; - this.moreFetching = true; - (this as any).api('posts/mentions', { - following: this.mode == 'following', - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { - this.posts = this.posts.concat(posts); - this.moreFetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-mentions - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > header - padding 8px 16px - border-bottom solid 1px #eee - - > span - margin-right 16px - line-height 27px - font-size 18px - color #555 - - &:not([data-is-active]) - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - > .fetching - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc - -</style> diff --git a/src/server/web/app/desktop/views/components/messaging-room-window.vue b/src/server/web/app/desktop/views/components/messaging-room-window.vue deleted file mode 100644 index 3735267811..0000000000 --- a/src/server/web/app/desktop/views/components/messaging-room-window.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user.name }}</span> - <mk-messaging-room :user="user" :class="$style.content"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url } from '../../../config'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - computed: { - popout(): string { - return `${url}/i/messaging/${getAcct(this.user)}`; - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -.content - height 100% - overflow auto - -</style> diff --git a/src/server/web/app/desktop/views/components/messaging-window.vue b/src/server/web/app/desktop/views/components/messaging-window.vue deleted file mode 100644 index ac27465987..0000000000 --- a/src/server/web/app/desktop/views/components/messaging-window.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:comments%メッセージ</span> - <mk-messaging :class="$style.content" @navigate="navigate"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkMessagingRoomWindow from './messaging-room-window.vue'; - -export default Vue.extend({ - methods: { - navigate(user) { - (this as any).os.new(MkMessagingRoomWindow, { - user: user - }); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -.content - height 100% - overflow auto - -</style> diff --git a/src/server/web/app/desktop/views/components/notifications.vue b/src/server/web/app/desktop/views/components/notifications.vue deleted file mode 100644 index 5e6db08c12..0000000000 --- a/src/server/web/app/desktop/views/components/notifications.vue +++ /dev/null @@ -1,317 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="notifications" v-if="notifications.length != 0"> - <template v-for="(notification, i) in _notifications"> - <div class="notification" :class="notification.type" :key="notification.id"> - <mk-time :time="notification.createdAt"/> - <template v-if="notification.type == 'reaction'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p> - <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link> - </p> - <router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`"> - %fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right% - </router-link> - </div> - </template> - <template v-if="notification.type == 'repost'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:retweet% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> - </p> - <router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`"> - %fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right% - </router-link> - </div> - </template> - <template v-if="notification.type == 'quote'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:quote-left% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> - </p> - <router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link> - </div> - </template> - <template v-if="notification.type == 'follow'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:user-plus% - <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link> - </p> - </div> - </template> - <template v-if="notification.type == 'reply'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:reply% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> - </p> - <router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link> - </div> - </template> - <template v-if="notification.type == 'mention'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:at% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> - </p> - <a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a> - </div> - </template> - <template v-if="notification.type == 'poll_vote'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p> - <router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`"> - %fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right% - </router-link> - </div> - </template> - </div> - <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> - <span>%fa:angle-up%{{ notification._datetext }}</span> - <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </div> - <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> - <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }} - </button> - <p class="empty" v-if="notifications.length == 0 && !fetching">ありません!</p> - <p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; -import getPostSummary from '../../../../../common/get-post-summary'; - -export default Vue.extend({ - data() { - return { - fetching: true, - fetchingMoreNotifications: false, - notifications: [], - moreNotifications: false, - connection: null, - connectionId: null, - getPostSummary - }; - }, - computed: { - _notifications(): any[] { - return (this.notifications as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = `${month}月 ${date}日`; - return notification; - }); - } - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('notification', this.onNotification); - - const max = 10; - - (this as any).api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.notifications = notifications; - this.fetching = false; - }); - }, - beforeDestroy() { - this.connection.off('notification', this.onNotification); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - getAcct, - fetchMoreNotifications() { - this.fetchingMoreNotifications = true; - - const max = 30; - - (this as any).api('i/notifications', { - limit: max + 1, - untilId: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.notifications = this.notifications.concat(notifications); - this.fetchingMoreNotifications = false; - }); - }, - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.connection.send({ - type: 'read_notification', - id: notification.id - }); - - this.notifications.unshift(notification); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - > .notifications - > .notification - margin 0 - padding 16px - overflow-wrap break-word - font-size 0.9em - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - &:last-child - border-bottom none - - > .mk-time - display inline - position absolute - top 16px - right 12px - vertical-align top - color rgba(0, 0, 0, 0.6) - font-size small - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - position -webkit-sticky - position sticky - top 16px - - > img - display block - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - i, .mk-reaction-icon - margin-right 4px - - .post-preview - color rgba(0, 0, 0, 0.7) - - .post-ref - color rgba(0, 0, 0, 0.7) - - [data-fa] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.repost, &.quote - .text p i - color #77B255 - - &.follow - .text p i - color #53c7ce - - &.reply, &.mention - .text p i - color #555 - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color #aaa - background #fdfdfd - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - span - margin 0 16px - - [data-fa] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color #555 - border-top solid 1px rgba(0, 0, 0, 0.05) - - &:hover - background rgba(0, 0, 0, 0.025) - - &:active - background rgba(0, 0, 0, 0.05) - - &.fetching - cursor wait - - > [data-fa] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .loading - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/post-detail.sub.vue b/src/server/web/app/desktop/views/components/post-detail.sub.vue deleted file mode 100644 index 35377e7c24..0000000000 --- a/src/server/web/app/desktop/views/components/post-detail.sub.vue +++ /dev/null @@ -1,126 +0,0 @@ -<template> -<div class="sub" :title="title"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/> - </router-link> - <div class="main"> - <header> - <div class="left"> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - </div> - <div class="right"> - <router-link class="time" :to="`/@${acct}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </div> - </header> - <div class="body"> - <mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i" :class="$style.text"/> - <div class="media" v-if="post.media"> - <mk-media-list :media-list="post.media"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - }, - title(): string { - return dateStringify(this.post.createdAt); - } - } -}); -</script> - -<style lang="stylus" scoped> -.sub - margin 0 - padding 20px 32px - background #fdfdfd - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 16px 0 0 - - > .avatar - display block - width 44px - height 44px - margin 0 - border-radius 4px - vertical-align bottom - - > .main - float left - width calc(100% - 60px) - - > header - margin-bottom 4px - white-space nowrap - - &:after - content "" - display block - clear both - - > .left - float left - - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 0 0 8px - color #ccc - - > .right - float right - - > .time - font-size 0.9em - color #c0c0c0 - -</style> - -<style lang="stylus" module> -.text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1em - color #717171 -</style> diff --git a/src/server/web/app/desktop/views/components/post-detail.vue b/src/server/web/app/desktop/views/components/post-detail.vue deleted file mode 100644 index 5c7a7dfdbe..0000000000 --- a/src/server/web/app/desktop/views/components/post-detail.vue +++ /dev/null @@ -1,433 +0,0 @@ -<template> -<div class="mk-post-detail" :title="title"> - <button - class="read-more" - v-if="p.reply && p.reply.replyId && context == null" - title="会話をもっと読み込む" - @click="fetchContext" - :disabled="contextFetching" - > - <template v-if="!contextFetching">%fa:ellipsis-v%</template> - <template v-if="contextFetching">%fa:spinner .pulse%</template> - </button> - <div class="context"> - <x-sub v-for="post in context" :key="post.id" :post="post"/> - </div> - <div class="reply-to" v-if="p.reply"> - <x-sub :post="p.reply"/> - </div> - <div class="repost" v-if="isRepost"> - <p> - <router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> - </router-link> - %fa:retweet% - <router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link> - がRepost - </p> - </div> - <article> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> - </router-link> - <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${p.id}`"> - <mk-time :time="p.createdAt"/> - </router-link> - </header> - <div class="body"> - <mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/> - <div class="media" v-if="p.media"> - <mk-media-list :media-list="p.media"/> - </div> - <mk-poll v-if="p.poll" :post="p"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> - </div> - <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> - <div class="map" v-if="p.geo" ref="map"></div> - <div class="repost" v-if="p.repost"> - <mk-post-preview :post="p.repost"/> - </div> - </div> - <footer> - <mk-reactions-viewer :post="p"/> - <button @click="reply" title="返信"> - %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> - </button> - <button @click="repost" title="Repost"> - %fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p> - </button> - <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション"> - %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> - </button> - <button @click="menu" ref="menuButton"> - %fa:ellipsis-h% - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="post in replies" :key="post.id" :post="post"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../common/user/get-acct'; - -import MkPostFormWindow from './post-form-window.vue'; -import MkRepostFormWindow from './repost-form-window.vue'; -import MkPostMenu from '../../../common/views/components/post-menu.vue'; -import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; -import XSub from './post-detail.sub.vue'; - -export default Vue.extend({ - components: { - XSub - }, - props: { - post: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - computed: { - acct() { - return getAcct(this.post.user); - } - }, - data() { - return { - context: [], - contextFetching: false, - replies: [], - }; - }, - computed: { - isRepost(): boolean { - return (this.post.repost && - this.post.text == null && - this.post.mediaIds == null && - this.post.poll == null); - }, - p(): any { - return this.isRepost ? this.post.repost : this.post; - }, - reactionsCount(): number { - return this.p.reactionCounts - ? Object.keys(this.p.reactionCounts) - .map(key => this.p.reactionCounts[key]) - .reduce((a, b) => a + b) - : 0; - }, - title(): string { - return dateStringify(this.p.createdAt); - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } - } - }, - mounted() { - // Get replies - if (!this.compact) { - (this as any).api('posts/replies', { - postId: this.p.id, - limit: 8 - }).then(replies => { - this.replies = replies; - }); - } - - // Draw map - if (this.p.geo) { - const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true; - if (shouldShowMap) { - (this as any).os.getGoogleMaps().then(maps => { - const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); - const map = new maps.Map(this.$refs.map, { - center: uluru, - zoom: 15 - }); - new maps.Marker({ - position: uluru, - map: map - }); - }); - } - } - }, - methods: { - fetchContext() { - this.contextFetching = true; - - // Fetch context - (this as any).api('posts/context', { - postId: this.p.replyId - }).then(context => { - this.contextFetching = false; - this.context = context.reverse(); - }); - }, - reply() { - (this as any).os.new(MkPostFormWindow, { - reply: this.p - }); - }, - repost() { - (this as any).os.new(MkRepostFormWindow, { - post: this.p - }); - }, - react() { - (this as any).os.new(MkReactionPicker, { - source: this.$refs.reactButton, - post: this.p - }); - }, - menu() { - (this as any).os.new(MkPostMenu, { - source: this.$refs.menuButton, - post: this.p - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-post-detail - margin 0 - padding 0 - overflow hidden - text-align left - background #fff - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 8px - - > .read-more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color #999 - cursor pointer - background #fafafa - outline none - border none - border-bottom solid 1px #eef0f2 - border-radius 6px 6px 0 0 - - &:hover - background #f6f6f6 - - &:active - background #f0f0f0 - - &:disabled - color #ccc - - > .context - > * - border-bottom 1px solid #eef0f2 - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 16px 32px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 6px - - [data-fa] - margin-right 4px - - .name - font-weight bold - - & + article - padding-top 8px - - > .reply-to - border-bottom 1px solid #eef0f2 - - > article - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - width 60px - height 60px - - > .avatar - display block - width 60px - height 60px - margin 0 - border-radius 8px - vertical-align bottom - - > header - position absolute - top 28px - left 108px - width calc(100% - 108px) - - > .name - display inline-block - margin 0 - line-height 24px - color #777 - font-size 18px - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color #ccc - - > .time - position absolute - top 0 - right 32px - font-size 1em - color #c0c0c0 - - > .body - padding 8px 0 - - > .repost - margin 8px 0 - - > .mk-post-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 300px - - &:empty - display none - - > .mk-url-preview - margin-top 8px - - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% - - &:hover - text-decoration none - background #e2e7ec - - > footer - font-size 1.2em - - > button - margin 0 28px 0 0 - padding 8px - background transparent - border none - font-size 1em - color #ddd - cursor pointer - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - > .replies - > * - border-top 1px solid #eef0f2 - -</style> - -<style lang="stylus" module> -.text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.5em - color #717171 -</style> diff --git a/src/server/web/app/desktop/views/components/post-form-window.vue b/src/server/web/app/desktop/views/components/post-form-window.vue deleted file mode 100644 index d0b115e852..0000000000 --- a/src/server/web/app/desktop/views/components/post-form-window.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<mk-window ref="window" is-modal @closed="$destroy"> - <span slot="header"> - <span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span> - <span v-if="!reply">%i18n:desktop.tags.mk-post-form-window.post%</span> - <span v-if="reply">%i18n:desktop.tags.mk-post-form-window.reply%</span> - <span :class="$style.count" v-if="media.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', media.length) }}</span> - <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span> - </span> - - <mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/> - <mk-post-form ref="form" - :reply="reply" - @posted="onPosted" - @change-uploadings="onChangeUploadings" - @change-attached-media="onChangeMedia" - @geo-attached="onGeoAttached" - @geo-dettached="onGeoDettached"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['reply'], - data() { - return { - uploadings: [], - media: [], - geo: null - }; - }, - mounted() { - this.$nextTick(() => { - (this.$refs.form as any).focus(); - }); - }, - methods: { - onChangeUploadings(files) { - this.uploadings = files; - }, - onChangeMedia(media) { - this.media = media; - }, - onGeoAttached(geo) { - this.geo = geo; - }, - onGeoDettached() { - this.geo = null; - }, - onPosted() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> -.icon - margin-right 8px - -.count - margin-left 8px - opacity 0.8 - - &:before - content '(' - - &:after - content ')' - -.postPreview - margin 16px 22px - -</style> diff --git a/src/server/web/app/desktop/views/components/post-form.vue b/src/server/web/app/desktop/views/components/post-form.vue deleted file mode 100644 index 1c83a38b64..0000000000 --- a/src/server/web/app/desktop/views/components/post-form.vue +++ /dev/null @@ -1,536 +0,0 @@ -<template> -<div class="mk-post-form" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" -> - <div class="content"> - <textarea :class="{ with: (files.length != 0 || poll) }" - ref="text" v-model="text" :disabled="posting" - @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" - v-autocomplete="'text'" - ></textarea> - <div class="medias" :class="{ with: poll }" v-show="files.length != 0"> - <x-draggable :list="files" :options="{ animation: 150 }"> - <div v-for="file in files" :key="file.id"> - <div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div> - <img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/> - </div> - </x-draggable> - <p class="remain">{{ 4 - files.length }}/4</p> - </div> - <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/> - </div> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <button class="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="chooseFile">%fa:upload%</button> - <button class="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> - <button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button> - <button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="poll = true">%fa:chart-pie%</button> - <button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> - <p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - text.length) }}</p> - <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> - {{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/> - </button> - <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/> - <div class="dropzone" v-if="draghover"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import getKao from '../../../common/scripts/get-kao'; - -export default Vue.extend({ - components: { - XDraggable - }, - props: ['reply', 'repost'], - data() { - return { - posting: false, - text: '', - files: [], - uploadings: [], - poll: false, - geo: null, - autocomplete: null, - draghover: false - }; - }, - computed: { - draftId(): string { - return this.repost - ? 'repost:' + this.repost.id - : this.reply - ? 'reply:' + this.reply.id - : 'post'; - }, - placeholder(): string { - return this.repost - ? '%i18n:desktop.tags.mk-post-form.quote-placeholder%' - : this.reply - ? '%i18n:desktop.tags.mk-post-form.reply-placeholder%' - : '%i18n:desktop.tags.mk-post-form.post-placeholder%'; - }, - submitText(): string { - return this.repost - ? '%i18n:desktop.tags.mk-post-form.repost%' - : this.reply - ? '%i18n:desktop.tags.mk-post-form.reply%' - : '%i18n:desktop.tags.mk-post-form.post%'; - }, - canPost(): boolean { - return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.repost); - } - }, - watch: { - text() { - this.saveDraft(); - }, - poll() { - this.saveDraft(); - }, - files() { - this.saveDraft(); - } - }, - mounted() { - this.$nextTick(() => { - // 書きかけの投稿を復元 - const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId]; - if (draft) { - this.text = draft.data.text; - this.files = draft.data.files; - if (draft.data.poll) { - this.poll = true; - this.$nextTick(() => { - (this.$refs.poll as any).set(draft.data.poll); - }); - } - this.$emit('change-attached-media', this.files); - } - }); - }, - methods: { - focus() { - (this.$refs.text as any).focus(); - }, - chooseFile() { - (this.$refs.file as any).click(); - }, - chooseFileFromDrive() { - (this as any).apis.chooseDriveFile({ - multiple: true - }).then(files => { - files.forEach(this.attachMedia); - }); - }, - attachMedia(driveFile) { - this.files.push(driveFile); - this.$emit('change-attached-media', this.files); - }, - detachMedia(id) { - this.files = this.files.filter(x => x.id != id); - this.$emit('change-attached-media', this.files); - }, - onChangeFile() { - Array.from((this.$refs.file as any).files).forEach(this.upload); - }, - upload(file) { - (this.$refs.uploader as any).upload(file); - }, - onChangeUploadings(uploads) { - this.$emit('change-uploadings', uploads); - }, - clear() { - this.text = ''; - this.files = []; - this.poll = false; - this.$emit('change-attached-media', this.files); - }, - onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); - }, - onPaste(e) { - Array.from(e.clipboardData.items).forEach((item: any) => { - if (item.kind == 'file') { - this.upload(item.getAsFile()); - } - }); - }, - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; - if (isFile || isDriveFile) { - e.preventDefault(); - this.draghover = true; - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - onDragenter(e) { - this.draghover = true; - }, - onDragleave(e) { - this.draghover = false; - }, - onDrop(e): void { - this.draghover = false; - - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.preventDefault(); - Array.from(e.dataTransfer.files).forEach(this.upload); - return; - } - - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.files.push(file); - this.$emit('change-attached-media', this.files); - e.preventDefault(); - } - //#endregion - }, - setGeo() { - if (navigator.geolocation == null) { - alert('お使いの端末は位置情報に対応していません'); - return; - } - - navigator.geolocation.getCurrentPosition(pos => { - this.geo = pos.coords; - this.$emit('geo-attached', this.geo); - }, err => { - alert('エラー: ' + err.message); - }, { - enableHighAccuracy: true - }); - }, - removeGeo() { - this.geo = null; - this.$emit('geo-dettached'); - }, - post() { - this.posting = true; - - (this as any).api('posts/create', { - 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, - repostId: this.repost ? this.repost.id : undefined, - poll: this.poll ? (this.$refs.poll as any).get() : undefined, - geo: this.geo ? { - coordinates: [this.geo.longitude, this.geo.latitude], - altitude: this.geo.altitude, - accuracy: this.geo.accuracy, - altitudeAccuracy: this.geo.altitudeAccuracy, - heading: isNaN(this.geo.heading) ? null : this.geo.heading, - speed: this.geo.speed, - } : null - }).then(data => { - this.clear(); - this.deleteDraft(); - this.$emit('posted'); - (this as any).apis.notify(this.repost - ? '%i18n:desktop.tags.mk-post-form.reposted%' - : this.reply - ? '%i18n:desktop.tags.mk-post-form.replied%' - : '%i18n:desktop.tags.mk-post-form.posted%'); - }).catch(err => { - (this as any).apis.notify(this.repost - ? '%i18n:desktop.tags.mk-post-form.repost-failed%' - : this.reply - ? '%i18n:desktop.tags.mk-post-form.reply-failed%' - : '%i18n:desktop.tags.mk-post-form.post-failed%'); - }).then(() => { - this.posting = false; - }); - }, - saveDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - data[this.draftId] = { - updatedAt: new Date(), - data: { - text: this.text, - files: this.files, - poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined - } - } - - localStorage.setItem('drafts', JSON.stringify(data)); - }, - deleteDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - - delete data[this.draftId]; - - localStorage.setItem('drafts', JSON.stringify(data)); - }, - kao() { - this.text += getKao(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-post-form - display block - padding 16px - background lighten($theme-color, 95%) - - &:after - content "" - display block - clear both - - > .content - - textarea - display block - padding 12px - margin 0 - width 100% - max-width 100% - min-width 100% - min-height calc(16px + 12px + 12px) - font-size 16px - color #333 - background #fff - outline none - border solid 1px rgba($theme-color, 0.1) - border-radius 4px - transition border-color .3s ease - - &:hover - border-color rgba($theme-color, 0.2) - transition border-color .1s ease - - & + * - & + * + * - border-color rgba($theme-color, 0.2) - transition border-color .1s ease - - &:focus - color $theme-color - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - - & + * - & + * + * - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - - &:disabled - opacity 0.5 - - &::-webkit-input-placeholder - color rgba($theme-color, 0.3) - - &.with - border-bottom solid 1px rgba($theme-color, 0.1) !important - border-radius 4px 4px 0 0 - - > .medias - margin 0 - padding 0 - background lighten($theme-color, 98%) - border solid 1px rgba($theme-color, 0.1) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - &.with - border-bottom solid 1px rgba($theme-color, 0.1) !important - border-radius 0 - - > .remain - display block - position absolute - top 8px - right 8px - margin 0 - padding 0 - color rgba($theme-color, 0.4) - - > div - padding 4px - - &:after - content "" - display block - clear both - - > div - float left - border solid 4px transparent - cursor move - - &:hover > .remove - display block - - > .img - width 64px - height 64px - background-size cover - background-position center center - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - - > .mk-poll-editor - background lighten($theme-color, 98%) - border solid 1px rgba($theme-color, 0.1) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - border solid 1px rgba($theme-color, 0.2) - border-radius 4px - - input[type='file'] - display none - - .text-count - pointer-events none - display block - position absolute - bottom 16px - right 138px - margin 0 - line-height 40px - color rgba($theme-color, 0.5) - - &.over - color #ec3828 - - .submit - display block - position absolute - bottom 16px - right 16px - cursor pointer - padding 0 - margin 0 - width 110px - height 40px - font-size 1em - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - outline none - border solid 1px lighten($theme-color, 15%) - border-radius 4px - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - &.wait - background linear-gradient( - 45deg, - darken($theme-color, 10%) 25%, - $theme-color 25%, - $theme-color 50%, - darken($theme-color, 10%) 50%, - darken($theme-color, 10%) 75%, - $theme-color 75%, - $theme-color - ) - background-size 32px 32px - animation stripe-bg 1.5s linear infinite - opacity 0.7 - cursor wait - - @keyframes stripe-bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - - > .upload - > .drive - > .kao - > .poll - > .geo - display inline-block - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color rgba($theme-color, 0.5) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color rgba($theme-color, 0.3) - - &:active - color rgba($theme-color, 0.6) - background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%) - border-color rgba($theme-color, 0.5) - box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - > .dropzone - position absolute - left 0 - top 0 - width 100% - height 100% - border dashed 2px rgba($theme-color, 0.5) - pointer-events none - -</style> diff --git a/src/server/web/app/desktop/views/components/post-preview.vue b/src/server/web/app/desktop/views/components/post-preview.vue deleted file mode 100644 index 0ac3223be2..0000000000 --- a/src/server/web/app/desktop/views/components/post-preview.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<div class="mk-post-preview" :title="title"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </header> - <div class="body"> - <mk-sub-post-content class="text" :post="post"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - }, - title(): string { - return dateStringify(this.post.createdAt); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-post-preview - font-size 0.9em - background #fff - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 16px 0 0 - - > .avatar - display block - width 52px - height 52px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 68px) - - > header - display flex - white-space nowrap - - > .name - margin 0 .5em 0 0 - padding 0 - color #607073 - font-size 1em - font-weight bold - text-decoration none - white-space normal - - &:hover - text-decoration underline - - > .username - margin 0 .5em 0 0 - color #d1d8da - - > .time - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - -</style> diff --git a/src/server/web/app/desktop/views/components/posts.post.sub.vue b/src/server/web/app/desktop/views/components/posts.post.sub.vue deleted file mode 100644 index 65d3017d3d..0000000000 --- a/src/server/web/app/desktop/views/components/posts.post.sub.vue +++ /dev/null @@ -1,112 +0,0 @@ -<template> -<div class="sub" :title="title"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="created-at" :to="`/@${acct}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </header> - <div class="body"> - <mk-sub-post-content class="text" :post="post"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - }, - title(): string { - return dateStringify(this.post.createdAt); - } - } -}); -</script> - -<style lang="stylus" scoped> -.sub - margin 0 - padding 16px - font-size 0.9em - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 14px 0 0 - - > .avatar - display block - width 52px - height 52px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 66px) - - > header - display flex - margin-bottom 2px - white-space nowrap - line-height 21px - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight bold - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - margin 0 .5em 0 0 - color #d1d8da - - > .created-at - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/server/web/app/desktop/views/components/posts.post.vue b/src/server/web/app/desktop/views/components/posts.post.vue deleted file mode 100644 index 37c6e63043..0000000000 --- a/src/server/web/app/desktop/views/components/posts.post.vue +++ /dev/null @@ -1,582 +0,0 @@ -<template> -<div class="post" tabindex="-1" :title="title" @keydown="onKeydown"> - <div class="reply-to" v-if="p.reply"> - <x-sub :post="p.reply"/> - </div> - <div class="repost" v-if="isRepost"> - <p> - <router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> - </router-link> - %fa:retweet% - <span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span> - <a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</a> - <span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span> - </p> - <mk-time :time="post.createdAt"/> - </div> - <article> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link> - <span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span> - <span class="username">@{{ acct }}</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> - <router-link class="created-at" :to="url"> - <mk-time :time="p.createdAt"/> - </router-link> - </div> - </header> - <div class="body"> - <p class="channel" v-if="p.channel"> - <a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>: - </p> - <div class="text"> - <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> - <a class="rp" v-if="p.repost">RP:</a> - </div> - <div class="media" v-if="p.media"> - <mk-media-list :media-list="p.media"/> - </div> - <mk-poll v-if="p.poll" :post="p" ref="pollViewer"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> - </div> - <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> - <div class="map" v-if="p.geo" ref="map"></div> - <div class="repost" v-if="p.repost"> - <mk-post-preview :post="p.repost"/> - </div> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - </div> - <footer> - <mk-reactions-viewer :post="p" ref="reactionsViewer"/> - <button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%"> - %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> - </button> - <button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%"> - %fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p> - </button> - <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%"> - %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> - </button> - <button @click="menu" ref="menuButton"> - %fa:ellipsis-h% - </button> - <button title="%i18n:desktop.tags.mk-timeline-post.detail"> - <template v-if="!isDetailOpened">%fa:caret-down%</template> - <template v-if="isDetailOpened">%fa:caret-up%</template> - </button> - </footer> - </div> - </article> - <div class="detail" v-if="isDetailOpened"> - <mk-post-status-graph width="462" height="130" :post="p"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../common/user/get-acct'; -import MkPostFormWindow from './post-form-window.vue'; -import MkRepostFormWindow from './repost-form-window.vue'; -import MkPostMenu from '../../../common/views/components/post-menu.vue'; -import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; -import XSub from './posts.post.sub.vue'; - -function focus(el, fn) { - const target = fn(el); - if (target) { - if (target.hasAttribute('tabindex')) { - target.focus(); - } else { - focus(target, fn); - } - } -} - -export default Vue.extend({ - components: { - XSub - }, - props: ['post'], - data() { - return { - isDetailOpened: false, - connection: null, - connectionId: null - }; - }, - computed: { - acct() { - return getAcct(this.p.user); - }, - isRepost(): boolean { - return (this.post.repost && - this.post.text == null && - this.post.mediaIds == null && - this.post.poll == null); - }, - p(): any { - return this.isRepost ? this.post.repost : this.post; - }, - reactionsCount(): number { - return this.p.reactionCounts - ? Object.keys(this.p.reactionCounts) - .map(key => this.p.reactionCounts[key]) - .reduce((a, b) => a + b) - : 0; - }, - title(): string { - return dateStringify(this.p.createdAt); - }, - url(): string { - return `/@${this.acct}/${this.p.id}`; - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } - } - }, - created() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - } - }, - mounted() { - this.capture(true); - - if ((this as any).os.isSignedIn) { - this.connection.on('_connected_', this.onStreamConnected); - } - - // Draw map - if (this.p.geo) { - const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true; - if (shouldShowMap) { - (this as any).os.getGoogleMaps().then(maps => { - const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); - const map = new maps.Map(this.$refs.map, { - center: uluru, - zoom: 15 - }); - new maps.Marker({ - position: uluru, - map: map - }); - }); - } - } - }, - beforeDestroy() { - this.decapture(true); - - if ((this as any).os.isSignedIn) { - this.connection.off('_connected_', this.onStreamConnected); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - capture(withHandler = false) { - if ((this as any).os.isSignedIn) { - this.connection.send({ - type: 'capture', - id: this.p.id - }); - if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated); - } - }, - decapture(withHandler = false) { - if ((this as any).os.isSignedIn) { - this.connection.send({ - type: 'decapture', - id: this.p.id - }); - if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated); - } - }, - onStreamConnected() { - this.capture(); - }, - onStreamPostUpdated(data) { - const post = data.post; - if (post.id == this.post.id) { - this.$emit('update:post', post); - } else if (post.id == this.post.repostId) { - this.post.repost = post; - } - }, - reply() { - (this as any).os.new(MkPostFormWindow, { - reply: this.p - }); - }, - repost() { - (this as any).os.new(MkRepostFormWindow, { - post: this.p - }); - }, - react() { - (this as any).os.new(MkReactionPicker, { - source: this.$refs.reactButton, - post: this.p - }); - }, - menu() { - (this as any).os.new(MkPostMenu, { - source: this.$refs.menuButton, - post: this.p - }); - }, - onKeydown(e) { - let shouldBeCancel = true; - - switch (true) { - case e.which == 38: // [↑] - case e.which == 74: // [j] - case e.which == 9 && e.shiftKey: // [Shift] + [Tab] - focus(this.$el, e => e.previousElementSibling); - break; - - case e.which == 40: // [↓] - case e.which == 75: // [k] - case e.which == 9: // [Tab] - focus(this.$el, e => e.nextElementSibling); - break; - - case e.which == 81: // [q] - case e.which == 69: // [e] - this.repost(); - break; - - case e.which == 70: // [f] - case e.which == 76: // [l] - //this.like(); - break; - - case e.which == 82: // [r] - this.reply(); - break; - - default: - shouldBeCancel = false; - } - - if (shouldBeCancel) e.preventDefault(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.post - margin 0 - padding 0 - background #fff - border-bottom solid 1px #eaeaea - - &:first-child - border-top-left-radius 6px - border-top-right-radius 6px - - > .repost - border-top-left-radius 6px - border-top-right-radius 6px - - &:last-of-type - border-bottom none - - &:focus - z-index 1 - - &:after - content "" - pointer-events none - position absolute - top 2px - right 2px - bottom 2px - left 2px - border 2px solid rgba($theme-color, 0.3) - border-radius 4px - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 16px 32px - line-height 28px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px - - [data-fa] - margin-right 4px - - .name - font-weight bold - - > .mk-time - position absolute - top 16px - right 32px - font-size 0.9em - line-height 28px - - & + article - padding-top 8px - - > .reply-to - padding 0 16px - background rgba(0, 0, 0, 0.0125) - - > .mk-post-preview - background transparent - - > article - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 16px 10px 0 - //position -webkit-sticky - //position sticky - //top 74px - - > .avatar - display block - width 58px - height 58px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 74px) - - > header - display flex - align-items center - margin-bottom 4px - white-space nowrap - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #627079 - font-size 1em - font-weight bold - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .is-bot - margin 0 .5em 0 0 - padding 1px 6px - font-size 12px - color #aaa - border solid 1px #ddd - border-radius 3px - - > .username - margin 0 .5em 0 0 - color #ccc - - > .info - margin-left auto - font-size 0.9em - - > .mobile - margin-right 8px - color #ccc - - > .app - margin-right 8px - padding-right 8px - color #ccc - border-right solid 1px #eaeaea - - > .created-at - color #c0c0c0 - - > .body - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - >>> .quote - margin 8px - padding 6px 12px - color #aaa - border-left solid 3px #eee - - > .reply - margin-right 8px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color #a0bf46 - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 300px - - &:empty - display none - - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% - - &:hover - text-decoration none - background #e2e7ec - - .mk-url-preview - margin-top 8px - - > .channel - margin 0 - - > .mk-poll - font-size 80% - - > .repost - margin 8px 0 - - > .mk-post-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - - > footer - > button - margin 0 28px 0 0 - padding 0 8px - line-height 32px - font-size 1em - color #ddd - background transparent - border none - cursor pointer - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - &:last-child - position absolute - right 0 - margin 0 - - > .detail - padding-top 4px - background rgba(0, 0, 0, 0.0125) - -</style> - -<style lang="stylus" module> -.text - - code - padding 4px 8px - margin 0 0.5em - font-size 80% - color #525252 - background #f8f8f8 - border-radius 2px - - pre > code - padding 16px - margin 0 - - [data-is-me]:after - content "you" - padding 0 4px - margin-left 4px - font-size 80% - color $theme-color-foreground - background $theme-color - border-radius 4px -</style> diff --git a/src/server/web/app/desktop/views/components/posts.vue b/src/server/web/app/desktop/views/components/posts.vue deleted file mode 100644 index 5031667c7c..0000000000 --- a/src/server/web/app/desktop/views/components/posts.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> -<div class="mk-posts"> - <template v-for="(post, i) in _posts"> - <x-post :post="post" :key="post.id" @update:post="onPostUpdated(i, $event)"/> - <p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date"> - <span>%fa:angle-up%{{ post._datetext }}</span> - <span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span> - </p> - </template> - <footer> - <slot name="footer"></slot> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPost from './posts.post.vue'; - -export default Vue.extend({ - components: { - XPost - }, - props: { - posts: { - type: Array, - default: () => [] - } - }, - computed: { - _posts(): any[] { - return (this.posts as any).map(post => { - const date = new Date(post.createdAt).getDate(); - const month = new Date(post.createdAt).getMonth() + 1; - post._date = date; - post._datetext = `${month}月 ${date}日`; - return post; - }); - } - }, - methods: { - focus() { - (this.$el as any).children[0].focus(); - }, - onPostUpdated(i, post) { - Vue.set((this as any).posts, i, post); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-posts - - > .date - display block - margin 0 - line-height 32px - font-size 14px - text-align center - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea - - span - margin 0 16px - - [data-fa] - margin-right 8px - - > footer - > * - display block - margin 0 - padding 16px - width 100% - text-align center - color #ccc - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px - - > button - &:hover - background #f5f5f5 - - &:active - background #eee -</style> diff --git a/src/server/web/app/desktop/views/components/progress-dialog.vue b/src/server/web/app/desktop/views/components/progress-dialog.vue deleted file mode 100644 index a4292e1aec..0000000000 --- a/src/server/web/app/desktop/views/components/progress-dialog.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy"> - <span slot="header">{{ title }}<mk-ellipsis/></span> - <div :class="$style.body"> - <p :class="$style.init" v-if="isNaN(value)">待機中<mk-ellipsis/></p> - <p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p> - <progress :class="$style.progress" - v-if="!isNaN(value) && value < max" - :value="isNaN(value) ? 0 : value" - :max="max" - ></progress> - <div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['title', 'initValue', 'initMax'], - data() { - return { - value: this.initValue, - max: this.initMax - }; - }, - methods: { - update(value, max) { - this.value = parseInt(value, 10); - this.max = parseInt(max, 10); - }, - close() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> -@import '~const.styl' - -.body - padding 18px 24px 24px 24px - -.init - display block - margin 0 - text-align center - color rgba(#000, 0.7) - -.percentage - display block - margin 0 0 4px 0 - text-align center - line-height 16px - color rgba($theme-color, 0.7) - - &:after - content '%' - -.progress - display block - margin 0 - width 100% - height 10px - background transparent - border none - border-radius 4px - overflow hidden - - &::-webkit-progress-value - background $theme-color - - &::-webkit-progress-bar - background rgba($theme-color, 0.1) - -.waiting - background linear-gradient( - 45deg, - lighten($theme-color, 30%) 25%, - $theme-color 25%, - $theme-color 50%, - lighten($theme-color, 30%) 50%, - lighten($theme-color, 30%) 75%, - $theme-color 75%, - $theme-color - ) - background-size 32px 32px - animation progress-dialog-tag-progress-waiting 1.5s linear infinite - - @keyframes progress-dialog-tag-progress-waiting - from {background-position: 0 0;} - to {background-position: -64px 32px;} - -</style> diff --git a/src/server/web/app/desktop/views/components/repost-form-window.vue b/src/server/web/app/desktop/views/components/repost-form-window.vue deleted file mode 100644 index 7db5adbff3..0000000000 --- a/src/server/web/app/desktop/views/components/repost-form-window.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<mk-window ref="window" is-modal @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-repost-form-window.title%</span> - <mk-repost-form ref="form" :post="post" @posted="onPosted" @canceled="onCanceled"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['post'], - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 27) { // Esc - (this.$refs.window as any).close(); - } - } - }, - onPosted() { - (this.$refs.window as any).close(); - }, - onCanceled() { - (this.$refs.window as any).close(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/repost-form.vue b/src/server/web/app/desktop/views/components/repost-form.vue deleted file mode 100644 index 3a5e3a7c56..0000000000 --- a/src/server/web/app/desktop/views/components/repost-form.vue +++ /dev/null @@ -1,131 +0,0 @@ -<template> -<div class="mk-repost-form"> - <mk-post-preview :post="post"/> - <template v-if="!quote"> - <footer> - <a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-repost-form.quote%</a> - <button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button> - <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }}</button> - </footer> - </template> - <template v-if="quote"> - <mk-post-form ref="form" :repost="post" @posted="onChildFormPosted"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['post'], - data() { - return { - wait: false, - quote: false - }; - }, - methods: { - ok() { - this.wait = true; - (this as any).api('posts/create', { - repostId: this.post.id - }).then(data => { - this.$emit('posted'); - (this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%'); - }).catch(err => { - (this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.failure%'); - }).then(() => { - this.wait = false; - }); - }, - cancel() { - this.$emit('canceled'); - }, - onQuote() { - this.quote = true; - - this.$nextTick(() => { - (this.$refs.form as any).focus(); - }); - }, - onChildFormPosted() { - this.$emit('posted'); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-repost-form - - > .mk-post-preview - margin 16px 22px - - > footer - height 72px - background lighten($theme-color, 95%) - - > .quote - position absolute - bottom 16px - left 28px - line-height 40px - - button - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - > .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - > .ok - right 16px - font-weight bold - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:hover - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active - background $theme-color - border-color $theme-color - -</style> diff --git a/src/server/web/app/desktop/views/components/settings-window.vue b/src/server/web/app/desktop/views/components/settings-window.vue deleted file mode 100644 index d5be177dcc..0000000000 --- a/src/server/web/app/desktop/views/components/settings-window.vue +++ /dev/null @@ -1,24 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:cog%設定</span> - <mk-settings @done="close"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - methods: { - close() { - (this as any).$refs.window.close(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/settings.2fa.vue b/src/server/web/app/desktop/views/components/settings.2fa.vue deleted file mode 100644 index b8dd1dfd9b..0000000000 --- a/src/server/web/app/desktop/views/components/settings.2fa.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<div class="2fa"> - <p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p> - <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div> - <p v-if="!data && !os.i.account.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p> - <template v-if="os.i.account.twoFactorEnabled"> - <p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p> - <button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button> - </template> - <div v-if="data"> - <ol> - <li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li> - <li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img :src="data.qr"></li> - <li>%i18n:desktop.tags.mk-2fa-setting.done%<br> - <input type="number" v-model="token" class="ui"> - <button @click="submit" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button> - </li> - </ol> - <div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - data: null, - token: null - }; - }, - methods: { - register() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-2fa-setting.enter-password%', - type: 'password' - }).then(password => { - (this as any).api('i/2fa/register', { - password: password - }).then(data => { - this.data = data; - }); - }); - }, - - unregister() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-2fa-setting.enter-password%', - type: 'password' - }).then(password => { - (this as any).api('i/2fa/unregister', { - password: password - }).then(() => { - (this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%'); - (this as any).os.i.account.twoFactorEnabled = false; - }); - }); - }, - - submit() { - (this as any).api('i/2fa/done', { - token: this.token - }).then(() => { - (this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%'); - (this as any).os.i.account.twoFactorEnabled = true; - }).catch(() => { - (this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%'); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.2fa - color #4a535a - -</style> diff --git a/src/server/web/app/desktop/views/components/settings.api.vue b/src/server/web/app/desktop/views/components/settings.api.vue deleted file mode 100644 index 0d5921ab7f..0000000000 --- a/src/server/web/app/desktop/views/components/settings.api.vue +++ /dev/null @@ -1,40 +0,0 @@ -<template> -<div class="root api"> - <p>Token: <code>{{ os.i.account.token }}</code></p> - <p>%i18n:desktop.tags.mk-api-info.intro%</p> - <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div> - <p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p> - <button class="ui" @click="regenerateToken">%i18n:desktop.tags.mk-api-info.regenerate-token%</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - methods: { - regenerateToken() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-api-info.enter-password%', - type: 'password' - }).then(password => { - (this as any).api('i/regenerate_token', { - password: password - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.root.api - color #4a535a - - code - display inline-block - padding 4px 6px - color #555 - background #eee - border-radius 2px -</style> diff --git a/src/server/web/app/desktop/views/components/settings.apps.vue b/src/server/web/app/desktop/views/components/settings.apps.vue deleted file mode 100644 index 0503b03abd..0000000000 --- a/src/server/web/app/desktop/views/components/settings.apps.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<div class="root"> - <div class="none ui info" v-if="!fetching && apps.length == 0"> - <p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p> - </div> - <div class="apps" v-if="apps.length != 0"> - <div v-for="app in apps"> - <p><b>{{ app.name }}</b></p> - <p>{{ app.description }}</p> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - apps: [] - }; - }, - mounted() { - (this as any).api('i/authorized_apps').then(apps => { - this.apps = apps; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root - > .apps - > div - padding 16px 0 0 0 - border-bottom solid 1px #eee -</style> diff --git a/src/server/web/app/desktop/views/components/settings.drive.vue b/src/server/web/app/desktop/views/components/settings.drive.vue deleted file mode 100644 index 8bb0c760a7..0000000000 --- a/src/server/web/app/desktop/views/components/settings.drive.vue +++ /dev/null @@ -1,35 +0,0 @@ -<template> -<div class="root"> - <template v-if="!fetching"> - <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/> - <p><b>{{ capacity | bytes }}</b>中<b>{{ usage | bytes }}</b>使用中</p> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - usage: null, - capacity: null - }; - }, - mounted() { - (this as any).api('drive').then(info => { - this.capacity = info.capacity; - this.usage = info.usage; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root - > p - > b - margin 0 8px -</style> diff --git a/src/server/web/app/desktop/views/components/settings.mute.vue b/src/server/web/app/desktop/views/components/settings.mute.vue deleted file mode 100644 index a8dfe10604..0000000000 --- a/src/server/web/app/desktop/views/components/settings.mute.vue +++ /dev/null @@ -1,35 +0,0 @@ -<template> -<div> - <div class="none ui info" v-if="!fetching && users.length == 0"> - <p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p> - </div> - <div class="users" v-if="users.length != 0"> - <div v-for="user in users" :key="user.id"> - <p><b>{{ user.name }}</b> @{{ getAcct(user) }}</p> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - data() { - return { - fetching: true, - users: [] - }; - }, - methods: { - getAcct - }, - mounted() { - (this as any).api('mute/list').then(x => { - this.users = x.users; - this.fetching = false; - }); - } -}); -</script> diff --git a/src/server/web/app/desktop/views/components/settings.password.vue b/src/server/web/app/desktop/views/components/settings.password.vue deleted file mode 100644 index f883b54065..0000000000 --- a/src/server/web/app/desktop/views/components/settings.password.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div> - <button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - methods: { - reset() { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-password-setting.enter-current-password%', - type: 'password' - }).then(currentPassword => { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-password-setting.enter-new-password%', - type: 'password' - }).then(newPassword => { - (this as any).apis.input({ - title: '%i18n:desktop.tags.mk-password-setting.enter-new-password-again%', - type: 'password' - }).then(newPassword2 => { - if (newPassword !== newPassword2) { - (this as any).apis.dialog({ - title: null, - text: '%i18n:desktop.tags.mk-password-setting.not-match%', - actions: [{ - text: 'OK' - }] - }); - return; - } - (this as any).api('i/change_password', { - currentPasword: currentPassword, - newPassword: newPassword - }).then(() => { - (this as any).apis.notify('%i18n:desktop.tags.mk-password-setting.changed%'); - }); - }); - }); - }); - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/components/settings.profile.vue b/src/server/web/app/desktop/views/components/settings.profile.vue deleted file mode 100644 index ba86286f87..0000000000 --- a/src/server/web/app/desktop/views/components/settings.profile.vue +++ /dev/null @@ -1,87 +0,0 @@ -<template> -<div class="profile"> - <label class="avatar ui from group"> - <p>%i18n:desktop.tags.mk-profile-setting.avatar%</p> - <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <button class="ui" @click="updateAvatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button> - </label> - <label class="ui from group"> - <p>%i18n:desktop.tags.mk-profile-setting.name%</p> - <input v-model="name" type="text" class="ui"/> - </label> - <label class="ui from group"> - <p>%i18n:desktop.tags.mk-profile-setting.location%</p> - <input v-model="location" type="text" class="ui"/> - </label> - <label class="ui from group"> - <p>%i18n:desktop.tags.mk-profile-setting.description%</p> - <textarea v-model="description" class="ui"></textarea> - </label> - <label class="ui from group"> - <p>%i18n:desktop.tags.mk-profile-setting.birthday%</p> - <el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/> - </label> - <button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button> - <section> - <h2>その他</h2> - <mk-switch v-model="os.i.account.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/> - </section> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - name: null, - location: null, - description: null, - birthday: null, - }; - }, - created() { - this.name = (this as any).os.i.name; - this.location = (this as any).os.i.account.profile.location; - this.description = (this as any).os.i.description; - this.birthday = (this as any).os.i.account.profile.birthday; - }, - methods: { - updateAvatar() { - (this as any).apis.updateAvatar(); - }, - save() { - (this as any).api('i/update', { - name: this.name, - location: this.location || null, - description: this.description || null, - birthday: this.birthday || null - }).then(() => { - (this as any).apis.notify('プロフィールを更新しました'); - }); - }, - onChangeIsBot() { - (this as any).api('i/update', { - isBot: (this as any).os.i.account.isBot - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.profile - > .avatar - > img - display inline-block - vertical-align top - width 64px - height 64px - border-radius 4px - - > button - margin-left 8px - -</style> - diff --git a/src/server/web/app/desktop/views/components/settings.signins.vue b/src/server/web/app/desktop/views/components/settings.signins.vue deleted file mode 100644 index a414c95c27..0000000000 --- a/src/server/web/app/desktop/views/components/settings.signins.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<div class="root"> -<div class="signins" v-if="signins.length != 0"> - <div v-for="signin in signins"> - <header @click="signin._show = !signin._show"> - <template v-if="signin.success">%fa:check%</template> - <template v-else>%fa:times%</template> - <span class="ip">{{ signin.ip }}</span> - <mk-time :time="signin.createdAt"/> - </header> - <div class="headers" v-show="signin._show"> - <tree-view :data="signin.headers"/> - </div> - </div> -</div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - signins: [], - connection: null, - connectionId: null - }; - }, - mounted() { - (this as any).api('i/signin_history').then(signins => { - this.signins = signins; - this.fetching = false; - }); - - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('signin', this.onSignin); - }, - beforeDestroy() { - this.connection.off('signin', this.onSignin); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - onSignin(signin) { - this.signins.unshift(signin); - } - } -}); -</script> - -<style lang="stylus" scoped> -.root - > .signins - > div - border-bottom solid 1px #eee - - > header - display flex - padding 8px 0 - line-height 32px - cursor pointer - - > [data-fa] - margin-right 8px - text-align left - - &.check - color #0fda82 - - &.times - color #ff3100 - - > .ip - display inline-block - text-align left - padding 8px - line-height 16px - font-family monospace - font-size 14px - color #444 - background #f8f8f8 - border-radius 4px - - > .mk-time - margin-left auto - text-align right - color #777 - - > .headers - overflow auto - margin 0 0 16px 0 - max-height 100px - white-space pre-wrap - word-break break-all - -</style> diff --git a/src/server/web/app/desktop/views/components/settings.vue b/src/server/web/app/desktop/views/components/settings.vue deleted file mode 100644 index fd82c171c1..0000000000 --- a/src/server/web/app/desktop/views/components/settings.vue +++ /dev/null @@ -1,419 +0,0 @@ -<template> -<div class="mk-settings"> - <div class="nav"> - <p :class="{ active: page == 'profile' }" @mousedown="page = 'profile'">%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p> - <p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> - <p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%通知</p> - <p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p> - <p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p> - <p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%アプリ</p> - <p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> - <p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p> - <p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p> - <p :class="{ active: page == 'other' }" @mousedown="page = 'other'">%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p> - </div> - <div class="pages"> - <section class="profile" v-show="page == 'profile'"> - <h1>%i18n:desktop.tags.mk-settings.profile%</h1> - <x-profile/> - </section> - - <section class="web" v-show="page == 'web'"> - <h1>動作</h1> - <mk-switch v-model="os.i.account.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み"> - <span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span> - </mk-switch> - <mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト"> - <span>ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。</span> - </mk-switch> - </section> - - <section class="web" v-show="page == 'web'"> - <h1>デザインと表示</h1> - <div class="div"> - <button class="ui button" @click="customizeHome">ホームをカスタマイズ</button> - </div> - <mk-switch v-model="os.i.account.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/> - <mk-switch v-model="os.i.account.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開"> - <span>位置情報が添付された投稿のマップを自動的に展開します。</span> - </mk-switch> - <mk-switch v-model="os.i.account.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/> - </section> - - <section class="web" v-show="page == 'web'"> - <h1>サウンド</h1> - <mk-switch v-model="enableSounds" text="サウンドを有効にする"> - <span>投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。</span> - </mk-switch> - <label>ボリューム</label> - <el-slider - v-model="soundVolume" - :show-input="true" - :format-tooltip="v => `${v}%`" - :disabled="!enableSounds" - /> - <button class="ui button" @click="soundTest">%fa:volume-up% テスト</button> - </section> - - <section class="web" v-show="page == 'web'"> - <h1>モバイル</h1> - <mk-switch v-model="os.i.account.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/> - </section> - - <section class="web" v-show="page == 'web'"> - <h1>言語</h1> - <el-select v-model="lang" placeholder="言語を選択"> - <el-option-group label="推奨"> - <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-group> - </el-select> - <div class="none ui info"> - <p>%fa:info-circle%変更はページの再度読み込み後に反映されます。</p> - </div> - </section> - - <section class="web" v-show="page == 'web'"> - <h1>キャッシュ</h1> - <button class="ui button" @click="clean">クリーンアップ</button> - <div class="none ui info warn"> - <p>%fa:exclamation-triangle%クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。</p> - </div> - </section> - - <section class="notification" v-show="page == 'notification'"> - <h1>通知</h1> - <mk-switch v-model="os.i.account.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ"> - <span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span> - </mk-switch> - </section> - - <section class="drive" v-show="page == 'drive'"> - <h1>%i18n:desktop.tags.mk-settings.drive%</h1> - <x-drive/> - </section> - - <section class="mute" v-show="page == 'mute'"> - <h1>%i18n:desktop.tags.mk-settings.mute%</h1> - <x-mute/> - </section> - - <section class="apps" v-show="page == 'apps'"> - <h1>アプリケーション</h1> - <x-apps/> - </section> - - <section class="twitter" v-show="page == 'twitter'"> - <h1>Twitter</h1> - <mk-twitter-setting/> - </section> - - <section class="password" v-show="page == 'security'"> - <h1>%i18n:desktop.tags.mk-settings.password%</h1> - <x-password/> - </section> - - <section class="2fa" v-show="page == 'security'"> - <h1>%i18n:desktop.tags.mk-settings.2fa%</h1> - <x-2fa/> - </section> - - <section class="signin" v-show="page == 'security'"> - <h1>サインイン履歴</h1> - <x-signins/> - </section> - - <section class="api" v-show="page == 'api'"> - <h1>API</h1> - <x-api/> - </section> - - <section class="other" v-show="page == 'other'"> - <h1>Misskeyについて</h1> - <p v-if="meta">このサーバーの運営者: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p> - </section> - - <section class="other" v-show="page == 'other'"> - <h1>Misskey Update</h1> - <p> - <span>バージョン: <i>{{ version }}</i></span> - <template v-if="latestVersion !== undefined"> - <br> - <span>最新のバージョン: <i>{{ latestVersion ? latestVersion : version }}</i></span> - </template> - </p> - <button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate"> - <template v-if="checkingForUpdate">アップデートを確認中<mk-ellipsis/></template> - <template v-else>アップデートを確認</template> - </button> - <details> - <summary>詳細設定</summary> - <mk-switch v-model="preventUpdate" text="アップデートを延期する(非推奨)"> - <span>この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。</span> - </mk-switch> - </details> - </section> - - <section class="other" v-show="page == 'other'"> - <h1>高度な設定</h1> - <mk-switch v-model="debug" text="デバッグモードを有効にする"> - <span>この設定はブラウザに記憶されます。</span> - </mk-switch> - <template v-if="debug"> - <mk-switch v-model="useRawScript" text="生のスクリプトを読み込む"> - <span>圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。</span> - </mk-switch> - <div class="none ui info"> - <p>%fa:info-circle%Misskeyはソースマップも提供しています。</p> - </div> - </template> - <mk-switch v-model="enableExperimental" text="実験的機能を有効にする"> - <span>実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。</span> - </mk-switch> - <details v-if="debug"> - <summary>ツール</summary> - <button class="ui button block" @click="taskmngr">タスクマネージャ</button> - </details> - </section> - - <section class="other" v-show="page == 'other'"> - <h1>%i18n:desktop.tags.mk-settings.license%</h1> - <div v-html="license"></div> - <a :href="licenseUrl" target="_blank">サードパーティ</a> - </section> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XProfile from './settings.profile.vue'; -import XMute from './settings.mute.vue'; -import XPassword from './settings.password.vue'; -import X2fa from './settings.2fa.vue'; -import XApi from './settings.api.vue'; -import XApps from './settings.apps.vue'; -import XSignins from './settings.signins.vue'; -import XDrive from './settings.drive.vue'; -import { url, docsUrl, license, lang, version } from '../../../config'; -import checkForUpdate from '../../../common/scripts/check-for-update'; -import MkTaskManager from './taskmanager.vue'; - -export default Vue.extend({ - components: { - XProfile, - XMute, - XPassword, - X2fa, - XApi, - XApps, - XSignins, - XDrive - }, - data() { - return { - page: 'profile', - meta: null, - license, - version, - latestVersion: undefined, - checkingForUpdate: false, - enableSounds: localStorage.getItem('enableSounds') == 'true', - autoPopout: localStorage.getItem('autoPopout') == 'true', - soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100, - lang: localStorage.getItem('lang') || '', - preventUpdate: localStorage.getItem('preventUpdate') == 'true', - debug: localStorage.getItem('debug') == 'true', - useRawScript: localStorage.getItem('useRawScript') == 'true', - enableExperimental: localStorage.getItem('enableExperimental') == 'true' - }; - }, - computed: { - licenseUrl(): string { - return `${docsUrl}/${lang}/license`; - } - }, - watch: { - autoPopout() { - localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false'); - }, - enableSounds() { - localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false'); - }, - soundVolume() { - localStorage.setItem('soundVolume', this.soundVolume.toString()); - }, - lang() { - localStorage.setItem('lang', this.lang); - }, - preventUpdate() { - localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false'); - }, - debug() { - localStorage.setItem('debug', this.debug ? 'true' : 'false'); - }, - useRawScript() { - localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false'); - }, - enableExperimental() { - localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false'); - } - }, - created() { - (this as any).os.getMeta().then(meta => { - this.meta = meta; - }); - }, - methods: { - taskmngr() { - (this as any).os.new(MkTaskManager); - }, - customizeHome() { - this.$router.push('/i/customize-home'); - this.$emit('done'); - }, - onChangeFetchOnScroll(v) { - (this as any).api('i/update_client_setting', { - name: 'fetchOnScroll', - value: v - }); - }, - onChangeAutoWatch(v) { - (this as any).api('i/update', { - autoWatch: v - }); - }, - onChangeShowPostFormOnTopOfTl(v) { - (this as any).api('i/update_client_setting', { - name: 'showPostFormOnTopOfTl', - value: v - }); - }, - onChangeShowMaps(v) { - (this as any).api('i/update_client_setting', { - name: 'showMaps', - value: v - }); - }, - onChangeGradientWindowHeader(v) { - (this as any).api('i/update_client_setting', { - name: 'gradientWindowHeader', - value: v - }); - }, - onChangeDisableViaMobile(v) { - (this as any).api('i/update_client_setting', { - name: 'disableViaMobile', - value: v - }); - }, - checkForUpdate() { - this.checkingForUpdate = true; - checkForUpdate((this as any).os, true, true).then(newer => { - this.checkingForUpdate = false; - this.latestVersion = newer; - if (newer == null) { - (this as any).apis.dialog({ - title: '利用可能な更新はありません', - text: 'お使いのMisskeyは最新です。' - }); - } else { - (this as any).apis.dialog({ - title: '新しいバージョンが利用可能です', - text: 'ページを再度読み込みすると更新が適用されます。' - }); - } - }); - }, - clean() { - localStorage.clear(); - (this as any).apis.dialog({ - title: 'キャッシュを削除しました', - text: 'ページを再度読み込みしてください。' - }); - }, - soundTest() { - const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; - sound.play(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-settings - display flex - width 100% - height 100% - - > .nav - flex 0 0 200px - width 100% - height 100% - padding 16px 0 0 0 - overflow auto - border-right solid 1px #ddd - - > p - display block - padding 10px 16px - margin 0 - color #666 - cursor pointer - user-select none - transition margin-left 0.2s ease - - > [data-fa] - margin-right 4px - - &:hover - color #555 - - &.active - margin-left 8px - color $theme-color !important - - > .pages - width 100% - height 100% - flex auto - overflow auto - - > section - margin 32px - color #4a535a - - > h1 - margin 0 0 1em 0 - padding 0 0 8px 0 - font-size 1em - color #555 - border-bottom solid 1px #eee - - &, >>> * - .ui.button.block - margin 16px 0 - - > section - margin 32px 0 - - > h2 - margin 0 0 1em 0 - padding 0 0 8px 0 - font-size 1em - color #555 - border-bottom solid 1px #eee - - > .web - > .div - border-bottom solid 1px #eee - padding 0 0 16px 0 - margin 0 0 16px 0 - -</style> diff --git a/src/server/web/app/desktop/views/components/sub-post-content.vue b/src/server/web/app/desktop/views/components/sub-post-content.vue deleted file mode 100644 index f13822331b..0000000000 --- a/src/server/web/app/desktop/views/components/sub-post-content.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div class="mk-sub-post-content"> - <div class="body"> - <a class="reply" v-if="post.replyId">%fa:reply%</a> - <mk-post-html :ast="post.ast" :i="os.i"/> - <a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - </div> - <details v-if="post.media"> - <summary>({{ post.media.length }}つのメディア)</summary> - <mk-media-list :media-list="post.media"/> - </details> - <details v-if="post.poll"> - <summary>投票</summary> - <mk-poll :post="post"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['post'], - computed: { - urls(): string[] { - if (this.post.ast) { - return this.post.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-post-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color #a0bf46 - - mk-poll - font-size 80% - -</style> diff --git a/src/server/web/app/desktop/views/components/taskmanager.vue b/src/server/web/app/desktop/views/components/taskmanager.vue deleted file mode 100644 index a00fabb047..0000000000 --- a/src/server/web/app/desktop/views/components/taskmanager.vue +++ /dev/null @@ -1,219 +0,0 @@ -<template> -<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager"> - <span slot="header" :class="$style.header">%fa:stethoscope%タスクマネージャ</span> - <el-tabs :class="$style.content"> - <el-tab-pane label="Requests"> - <el-table - :data="os.requests" - style="width: 100%" - :default-sort="{prop: 'date', order: 'descending'}" - > - <el-table-column type="expand"> - <template slot-scope="props"> - <pre>{{ props.row.data }}</pre> - <pre>{{ props.row.res }}</pre> - </template> - </el-table-column> - - <el-table-column - label="Requested at" - prop="date" - sortable - > - <template slot-scope="scope"> - <b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b> - <span>(<mk-time :time="scope.row.date"/>)</span> - </template> - </el-table-column> - - <el-table-column - label="Name" - > - <template slot-scope="scope"> - <b>{{ scope.row.name }}</b> - </template> - </el-table-column> - - <el-table-column - label="Status" - > - <template slot-scope="scope"> - <span>{{ scope.row.status || '(pending)' }}</span> - </template> - </el-table-column> - </el-table> - </el-tab-pane> - - <el-tab-pane label="Streams"> - <el-table - :data="os.connections" - style="width: 100%" - > - <el-table-column - label="Uptime" - > - <template slot-scope="scope"> - <mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/> - <span v-else>-</span> - </template> - </el-table-column> - - <el-table-column - label="Name" - > - <template slot-scope="scope"> - <b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b> - </template> - </el-table-column> - - <el-table-column - label="User" - > - <template slot-scope="scope"> - <span>{{ scope.row.user || '(anonymous)' }}</span> - </template> - </el-table-column> - - <el-table-column - prop="state" - label="State" - /> - - <el-table-column - prop="in" - label="In" - /> - - <el-table-column - prop="out" - label="Out" - /> - </el-table> - </el-tab-pane> - - <el-tab-pane label="Streams (Inspect)"> - <el-tabs type="card" style="height:50%"> - <el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab"> - <div style="padding: 12px 0 0 12px"> - <el-button size="mini" @click="send(c)">Send</el-button> - <el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button> - <el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button> - <el-button size="mini" type="danger" @click="c.close">Disconnect</el-button> - </div> - - <el-table - :data="c.inout" - style="width: 100%" - :default-sort="{prop: 'at', order: 'descending'}" - > - <el-table-column type="expand"> - <template slot-scope="props"> - <pre>{{ props.row.data }}</pre> - </template> - </el-table-column> - - <el-table-column - label="Date" - prop="at" - sortable - > - <template slot-scope="scope"> - <b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b> - <span>(<mk-time :time="scope.row.at"/>)</span> - </template> - </el-table-column> - - <el-table-column - label="Type" - > - <template slot-scope="scope"> - <span>{{ getMessageType(scope.row.data) }}</span> - </template> - </el-table-column> - - <el-table-column - label="Incoming / Outgoing" - prop="type" - /> - </el-table> - </el-tab-pane> - </el-tabs> - </el-tab-pane> - - <el-tab-pane label="Windows"> - <el-table - :data="Array.from(os.windows.windows)" - style="width: 100%" - > - <el-table-column - label="Name" - > - <template slot-scope="scope"> - <b>{{ scope.row.name || '(unknown)' }}</b> - </template> - </el-table-column> - - <el-table-column - label="Operations" - > - <template slot-scope="scope"> - <el-button size="mini" type="danger" @click="scope.row.close">Close</el-button> - </template> - </el-table-column> - </el-table> - </el-tab-pane> - </el-tabs> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - mounted() { - (this as any).os.windows.on('added', this.onWindowsChanged); - (this as any).os.windows.on('removed', this.onWindowsChanged); - }, - beforeDestroy() { - (this as any).os.windows.off('added', this.onWindowsChanged); - (this as any).os.windows.off('removed', this.onWindowsChanged); - }, - methods: { - getMessageType(data): string { - return data.type ? data.type : '-'; - }, - onWindowsChanged() { - this.$forceUpdate(); - }, - send(c) { - (this as any).apis.input({ - title: 'Send a JSON message', - allowEmpty: false - }).then(json => { - c.send(JSON.parse(json)); - }); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -.content - height 100% - overflow auto - -</style> - -<style> -.el-tabs__header { - margin-bottom: 0 !important; -} - -.el-tabs__item { - padding: 0 20px !important; -} -</style> diff --git a/src/server/web/app/desktop/views/components/timeline.vue b/src/server/web/app/desktop/views/components/timeline.vue deleted file mode 100644 index 65b4bd1c7a..0000000000 --- a/src/server/web/app/desktop/views/components/timeline.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div class="mk-timeline"> - <mk-friends-maker v-if="alone"/> - <div class="fetching" v-if="fetching"> - <mk-ellipsis-icon/> - </div> - <p class="empty" v-if="posts.length == 0 && !fetching"> - %fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。 - </p> - <mk-posts :posts="posts" ref="timeline"> - <button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">もっと見る</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </button> - </mk-posts> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url } from '../../../config'; - -export default Vue.extend({ - data() { - return { - fetching: true, - moreFetching: false, - existMore: false, - posts: [], - connection: null, - connectionId: null, - date: null - }; - }, - computed: { - alone(): boolean { - return (this as any).os.i.followingCount == 0; - } - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('post', this.onPost); - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - - document.addEventListener('keydown', this.onKeydown); - window.addEventListener('scroll', this.onScroll); - - this.fetch(); - }, - beforeDestroy() { - this.connection.off('post', this.onPost); - this.connection.off('follow', this.onChangeFollowing); - this.connection.off('unfollow', this.onChangeFollowing); - (this as any).os.stream.dispose(this.connectionId); - - document.removeEventListener('keydown', this.onKeydown); - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - fetch(cb?) { - this.fetching = true; - - (this as any).api('posts/timeline', { - limit: 11, - untilDate: this.date ? this.date.getTime() : undefined - }).then(posts => { - if (posts.length == 11) { - posts.pop(); - this.existMore = true; - } - this.posts = posts; - this.fetching = false; - this.$emit('loaded'); - if (cb) cb(); - }); - }, - more() { - if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return; - this.moreFetching = true; - (this as any).api('posts/timeline', { - limit: 11, - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { - if (posts.length == 11) { - posts.pop(); - } else { - this.existMore = false; - } - this.posts = this.posts.concat(posts); - this.moreFetching = false; - }); - }, - onPost(post) { - // サウンドを再生する - if ((this as any).os.isEnableSounds) { - const sound = new Audio(`${url}/assets/post.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; - sound.play(); - } - - this.posts.unshift(post); - }, - onChangeFollowing() { - this.fetch(); - }, - onScroll() { - if ((this as any).os.i.account.clientSettings.fetchOnScroll !== false) { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); - } - }, - onKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.timeline as any).focus(); - } - } - }, - warp(date) { - this.date = date; - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-timeline - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .mk-friends-maker - border-bottom solid 1px #eee - - > .fetching - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc - -</style> diff --git a/src/server/web/app/desktop/views/components/ui-notification.vue b/src/server/web/app/desktop/views/components/ui-notification.vue deleted file mode 100644 index 9983f02c5e..0000000000 --- a/src/server/web/app/desktop/views/components/ui-notification.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<div class="mk-ui-notification"> - <p>{{ message }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: ['message'], - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$el, - opacity: 1, - translateY: [-64, 0], - easing: 'easeOutElastic', - duration: 500 - }); - - setTimeout(() => { - anime({ - targets: this.$el, - opacity: 0, - translateY: -64, - duration: 500, - easing: 'easeInElastic', - complete: () => this.$destroy() - }); - }, 6000); - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui-notification - display block - position fixed - z-index 10000 - top -128px - left 0 - right 0 - margin 0 auto - padding 128px 0 0 0 - width 500px - color rgba(#000, 0.6) - background rgba(#fff, 0.9) - border-radius 0 0 8px 8px - box-shadow 0 2px 4px rgba(#000, 0.2) - transform translateY(-64px) - opacity 0 - - > p - margin 0 - line-height 64px - text-align center - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.account.vue b/src/server/web/app/desktop/views/components/ui.header.account.vue deleted file mode 100644 index ec4635f338..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.account.vue +++ /dev/null @@ -1,225 +0,0 @@ -<template> -<div class="account"> - <button class="header" :data-active="isOpen" @click="toggle"> - <span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> - <img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/> - </button> - <transition name="zoom-in-top"> - <div class="menu" v-if="isOpen"> - <ul> - <li> - <router-link :to="`/@${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</router-link> - </li> - <li @click="drive"> - <p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p> - </li> - <li> - <a href="/i/mentions">%fa:at%%i18n:desktop.tags.mk-ui-header-account.mentions%%fa:angle-right%</a> - </li> - </ul> - <ul> - <li @click="settings"> - <p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p> - </li> - </ul> - <ul> - <li @click="signout"> - <p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p> - </li> - </ul> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkSettingsWindow from './settings-window.vue'; -import MkDriveWindow from './drive-window.vue'; -import contains from '../../../common/scripts/contains'; - -export default Vue.extend({ - data() { - return { - isOpen: false - }; - }, - beforeDestroy() { - this.close(); - }, - methods: { - toggle() { - this.isOpen ? this.close() : this.open(); - }, - open() { - this.isOpen = true; - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.addEventListener('mousedown', this.onMousedown); - }); - }, - close() { - this.isOpen = false; - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.removeEventListener('mousedown', this.onMousedown); - }); - }, - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); - return false; - }, - drive() { - this.close(); - (this as any).os.new(MkDriveWindow); - }, - settings() { - this.close(); - (this as any).os.new(MkSettingsWindow); - }, - signout() { - (this as any).os.signout(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.account - > .header - display block - margin 0 - padding 0 - color #9eaba8 - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color darken(#9eaba8, 20%) - - > .avatar - filter saturate(150%) - - &:active - color darken(#9eaba8, 30%) - - > .username - display block - float left - margin 0 12px 0 16px - max-width 16em - line-height 48px - font-weight bold - font-family Meiryo, sans-serif - text-decoration none - - [data-fa] - margin-left 8px - - > .avatar - display block - float left - min-width 32px - max-width 32px - min-height 32px - max-height 32px - margin 8px 8px 8px 0 - border-radius 4px - transition filter 100ms ease - - > .menu - display block - position absolute - top 56px - right -2px - width 230px - font-size 0.8em - background #fff - border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px #fff - border-left solid 14px transparent - - ul - display block - margin 10px 0 - padding 0 - list-style none - - & + ul - padding-top 10px - border-top solid 1px #eee - - > li - display block - margin 0 - padding 0 - - > a - > p - display block - z-index 1 - padding 0 28px - margin 0 - line-height 40px - color #868C8C - cursor pointer - - * - pointer-events none - - > [data-fa]:first-of-type - margin-right 6px - - > [data-fa]:last-of-type - display block - position absolute - top 0 - right 8px - z-index 1 - padding 0 20px - font-size 1.2em - line-height 40px - - &:hover, &:active - text-decoration none - background $theme-color - color $theme-color-foreground - - &:active - background darken($theme-color, 10%) - -.zoom-in-top-enter-active, -.zoom-in-top-leave-active { - transform-origin: center -16px; -} - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.clock.vue b/src/server/web/app/desktop/views/components/ui.header.clock.vue deleted file mode 100644 index cd23a67506..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.clock.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="clock"> - <div class="header"> - <time ref="time"> - <span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span> - <br> - <span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span> - </time> - </div> - <div class="content"> - <mk-analog-clock/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - now: new Date(), - clock: null - }; - }, - computed: { - yyyy(): number { - return this.now.getFullYear(); - }, - mm(): string { - return ('0' + (this.now.getMonth() + 1)).slice(-2); - }, - dd(): string { - return ('0' + this.now.getDate()).slice(-2); - }, - hh(): string { - return ('0' + this.now.getHours()).slice(-2); - }, - nn(): string { - return ('0' + this.now.getMinutes()).slice(-2); - } - }, - mounted() { - this.tick(); - this.clock = setInterval(this.tick, 1000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - tick() { - this.now = new Date(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.clock - display inline-block - overflow visible - - > .header - padding 0 12px - text-align center - font-size 10px - - &, * - cursor: default - - &:hover - background #899492 - - & + .content - visibility visible - - > time - color #fff !important - - * - color #fff !important - - &:after - content "" - display block - clear both - - > time - display table-cell - vertical-align middle - height 48px - color #9eaba8 - - > .yyyymmdd - opacity 0.7 - - > .content - visibility hidden - display block - position absolute - top auto - right 0 - z-index 3 - margin 0 - padding 0 - width 256px - background #899492 - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.nav.vue b/src/server/web/app/desktop/views/components/ui.header.nav.vue deleted file mode 100644 index 7582e8afce..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.nav.vue +++ /dev/null @@ -1,175 +0,0 @@ -<template> -<div class="nav"> - <ul> - <template v-if="os.isSignedIn"> - <li class="home" :class="{ active: $route.name == 'index' }"> - <router-link to="/"> - %fa:home% - <p>%i18n:desktop.tags.mk-ui-header-nav.home%</p> - </router-link> - </li> - <li class="messaging"> - <a @click="messaging"> - %fa:comments% - <p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p> - <template v-if="hasUnreadMessagingMessages">%fa:circle%</template> - </a> - </li> - <li class="game"> - <a @click="game"> - %fa:gamepad% - <p>ゲーム</p> - <template v-if="hasGameInvitations">%fa:circle%</template> - </a> - </li> - </template> - <li class="ch"> - <a :href="chUrl" target="_blank"> - %fa:tv% - <p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p> - </a> - </li> - </ul> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { chUrl } from '../../../config'; -import MkMessagingWindow from './messaging-window.vue'; -import MkGameWindow from './game-window.vue'; - -export default Vue.extend({ - data() { - return { - hasUnreadMessagingMessages: false, - hasGameInvitations: false, - connection: null, - connectionId: null, - chUrl - }; - }, - mounted() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); - this.connection.on('othello_invited', this.onOthelloInvited); - this.connection.on('othello_no_invites', this.onOthelloNoInvites); - - // Fetch count of unread messaging messages - (this as any).api('messaging/unread').then(res => { - if (res.count > 0) { - this.hasUnreadMessagingMessages = true; - } - }); - } - }, - beforeDestroy() { - if ((this as any).os.isSignedIn) { - this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); - this.connection.off('othello_invited', this.onOthelloInvited); - this.connection.off('othello_no_invites', this.onOthelloNoInvites); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - onUnreadMessagingMessage() { - this.hasUnreadMessagingMessages = true; - }, - - onReadAllMessagingMessages() { - this.hasUnreadMessagingMessages = false; - }, - - onOthelloInvited() { - this.hasGameInvitations = true; - }, - - onOthelloNoInvites() { - this.hasGameInvitations = false; - }, - - messaging() { - (this as any).os.new(MkMessagingWindow); - }, - - game() { - (this as any).os.new(MkGameWindow); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.nav - display inline-block - margin 0 - padding 0 - line-height 3rem - vertical-align top - - > ul - display inline-block - margin 0 - padding 0 - vertical-align top - line-height 3rem - list-style none - - > li - display inline-block - vertical-align top - height 48px - line-height 48px - - &.active - > a - border-bottom solid 3px $theme-color - - > a - display inline-block - z-index 1 - height 100% - padding 0 24px - font-size 13px - font-variant small-caps - color #9eaba8 - text-decoration none - transition none - cursor pointer - - * - pointer-events none - - &:hover - color darken(#9eaba8, 20%) - text-decoration none - - > [data-fa]:first-child - margin-right 8px - - > [data-fa]:last-child - margin-left 5px - font-size 10px - color $theme-color - - @media (max-width 1100px) - margin-left -5px - - > p - display inline - margin 0 - - @media (max-width 1100px) - display none - - @media (max-width 700px) - padding 0 12px - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.notifications.vue b/src/server/web/app/desktop/views/components/ui.header.notifications.vue deleted file mode 100644 index e829418d18..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.notifications.vue +++ /dev/null @@ -1,158 +0,0 @@ -<template> -<div class="notifications"> - <button :data-active="isOpen" @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%"> - %fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template> - </button> - <div class="pop" v-if="isOpen"> - <mk-notifications/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import contains from '../../../common/scripts/contains'; - -export default Vue.extend({ - data() { - return { - isOpen: false, - hasUnreadNotifications: false, - connection: null, - connectionId: null - }; - }, - mounted() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('read_all_notifications', this.onReadAllNotifications); - this.connection.on('unread_notification', this.onUnreadNotification); - - // Fetch count of unread notifications - (this as any).api('notifications/get_unread_count').then(res => { - if (res.count > 0) { - this.hasUnreadNotifications = true; - } - }); - } - }, - beforeDestroy() { - if ((this as any).os.isSignedIn) { - this.connection.off('read_all_notifications', this.onReadAllNotifications); - this.connection.off('unread_notification', this.onUnreadNotification); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - onReadAllNotifications() { - this.hasUnreadNotifications = false; - }, - - onUnreadNotification() { - this.hasUnreadNotifications = true; - }, - - toggle() { - this.isOpen ? this.close() : this.open(); - }, - - open() { - this.isOpen = true; - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.addEventListener('mousedown', this.onMousedown); - }); - }, - - close() { - this.isOpen = false; - Array.from(document.querySelectorAll('body *')).forEach(el => { - el.removeEventListener('mousedown', this.onMousedown); - }); - }, - - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); - return false; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.notifications - - > button - display block - margin 0 - padding 0 - width 32px - color #9eaba8 - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color darken(#9eaba8, 20%) - - &:active - color darken(#9eaba8, 30%) - - > [data-fa].bell - font-size 1.2em - line-height 48px - - > [data-fa].circle - margin-left -5px - vertical-align super - font-size 10px - color $theme-color - - > .pop - display block - position absolute - top 56px - right -72px - width 300px - background #fff - border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px #fff - border-left solid 14px transparent - - > .mk-notifications - max-height 350px - font-size 1rem - overflow auto - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.post.vue b/src/server/web/app/desktop/views/components/ui.header.post.vue deleted file mode 100644 index c2f0e07dd3..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.post.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="post"> - <button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - methods: { - post() { - (this as any).apis.post(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.post - display inline-block - padding 8px - height 100% - vertical-align top - - > button - display inline-block - margin 0 - padding 0 10px - height 100% - font-size 1.2em - font-weight normal - text-decoration none - color $theme-color-foreground - background $theme-color !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background lighten($theme-color, 10%) !important - - &:active - background darken($theme-color, 10%) !important - transition background 0s ease - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.search.vue b/src/server/web/app/desktop/views/components/ui.header.search.vue deleted file mode 100644 index 86215556ad..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.search.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<form class="search" @submit.prevent="onSubmit"> - %fa:search% - <input v-model="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/> - <div class="result"></div> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - q: '' - }; - }, - methods: { - onSubmit() { - location.href = `/search?q=${encodeURIComponent(this.q)}`; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.search - - > [data-fa] - display block - position absolute - top 0 - left 0 - width 48px - text-align center - line-height 48px - color #9eaba8 - pointer-events none - - > * - vertical-align middle - - > input - user-select text - cursor auto - margin 8px 0 0 0 - padding 6px 18px 6px 36px - width 14em - height 32px - font-size 1em - background rgba(0, 0, 0, 0.05) - outline none - //border solid 1px #ddd - border none - border-radius 16px - transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif - - &::placeholder - color #9eaba8 - - &:hover - background rgba(0, 0, 0, 0.08) - - &:focus - box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.header.vue b/src/server/web/app/desktop/views/components/ui.header.vue deleted file mode 100644 index 7e337d2ae5..0000000000 --- a/src/server/web/app/desktop/views/components/ui.header.vue +++ /dev/null @@ -1,172 +0,0 @@ -<template> -<div class="header"> - <mk-special-message/> - <div class="main" ref="main"> - <div class="backdrop"></div> - <div class="main"> - <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p> - <div class="container" ref="mainContainer"> - <div class="left"> - <x-nav/> - </div> - <div class="right"> - <x-search/> - <x-account v-if="os.isSignedIn"/> - <x-notifications v-if="os.isSignedIn"/> - <x-post v-if="os.isSignedIn"/> - <x-clock/> - </div> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -import XNav from './ui.header.nav.vue'; -import XSearch from './ui.header.search.vue'; -import XAccount from './ui.header.account.vue'; -import XNotifications from './ui.header.notifications.vue'; -import XPost from './ui.header.post.vue'; -import XClock from './ui.header.clock.vue'; - -export default Vue.extend({ - components: { - XNav, - XSearch, - XAccount, - XNotifications, - XPost, - XClock, - }, - mounted() { - if ((this as any).os.isSignedIn) { - const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000 - const isHisasiburi = ago >= 3600; - (this as any).os.i.account.lastUsedAt = new Date(); - if (isHisasiburi) { - (this.$refs.welcomeback as any).style.display = 'block'; - (this.$refs.main as any).style.overflow = 'hidden'; - - anime({ - targets: this.$refs.welcomeback, - top: '0', - opacity: 1, - delay: 1000, - duration: 500, - easing: 'easeOutQuad' - }); - - anime({ - targets: this.$refs.mainContainer, - opacity: 0, - delay: 1000, - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.$refs.welcomeback, - top: '-48px', - opacity: 0, - duration: 500, - complete: () => { - (this.$refs.welcomeback as any).style.display = 'none'; - (this.$refs.main as any).style.overflow = 'initial'; - }, - easing: 'easeInQuad' - }); - - anime({ - targets: this.$refs.mainContainer, - opacity: 1, - duration: 500, - easing: 'easeInQuad' - }); - }, 2500); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.header - position -webkit-sticky - position sticky - top 0 - z-index 1000 - width 100% - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) - - > .main - height 48px - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height 48px - background #f7f7f7 - - > .main - z-index 1001 - margin 0 - padding 0 - background-clip content-box - font-size 0.9rem - user-select none - - > p - display none - position absolute - top 48px - width 100% - line-height 48px - margin 0 - text-align center - color #888 - opacity 0 - - > .container - display flex - width 100% - max-width 1300px - margin 0 auto - - &:before - content "" - position absolute - top 0 - left 0 - display block - width 100% - height 48px - background-image url(/assets/desktop/header-logo.svg) - background-size 46px - background-position center - background-repeat no-repeat - opacity 0.3 - - > .left - margin 0 auto 0 0 - height 48px - - > .right - margin 0 0 0 auto - height 48px - - > * - display inline-block - vertical-align top - - @media (max-width 1100px) - > .mk-ui-header-search - display none - -</style> diff --git a/src/server/web/app/desktop/views/components/ui.vue b/src/server/web/app/desktop/views/components/ui.vue deleted file mode 100644 index 87f932ff14..0000000000 --- a/src/server/web/app/desktop/views/components/ui.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<div> - <x-header/> - <div class="content"> - <slot></slot> - </div> - <mk-stream-indicator v-if="os.isSignedIn"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XHeader from './ui.header.vue'; - -export default Vue.extend({ - components: { - XHeader - }, - mounted() { - document.addEventListener('keydown', this.onKeydown); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onKeydown); - }, - methods: { - onKeydown(e) { - if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return; - - if (e.which == 80 || e.which == 78) { // p or n - e.preventDefault(); - (this as any).apis.post(); - } - } - } -}); -</script> - diff --git a/src/server/web/app/desktop/views/components/user-preview.vue b/src/server/web/app/desktop/views/components/user-preview.vue deleted file mode 100644 index 8c86b2efe8..0000000000 --- a/src/server/web/app/desktop/views/components/user-preview.vue +++ /dev/null @@ -1,173 +0,0 @@ -<template> -<div class="mk-user-preview"> - <template v-if="u != null"> - <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div> - <router-link class="avatar" :to="`/@${acct}`"> - <img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="title"> - <router-link class="name" :to="`/@${acct}`">{{ u.name }}</router-link> - <p class="username">@{{ acct }}</p> - </div> - <div class="description">{{ u.description }}</div> - <div class="status"> - <div> - <p>投稿</p><a>{{ u.postsCount }}</a> - </div> - <div> - <p>フォロー</p><a>{{ u.followingCount }}</a> - </div> - <div> - <p>フォロワー</p><a>{{ u.followersCount }}</a> - </div> - </div> - <mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; -import getAcct from '../../../../../common/user/get-acct'; -import parseAcct from '../../../../../common/user/parse-acct'; - -export default Vue.extend({ - props: { - user: { - type: [Object, String], - required: true - } - }, - computed: { - acct() { - return getAcct(this.u); - } - }, - data() { - return { - u: null - }; - }, - mounted() { - if (typeof this.user == 'object') { - this.u = this.user; - this.$nextTick(() => { - this.open(); - }); - } else { - const query = this.user[0] == '@' ? - parseAcct(this.user[0].substr(1)) : - { userId: this.user[0] }; - - (this as any).api('users/show', query).then(user => { - this.u = user; - this.open(); - }); - } - }, - methods: { - open() { - anime({ - targets: this.$el, - opacity: 1, - 'margin-top': 0, - duration: 200, - easing: 'easeOutQuad' - }); - }, - close() { - anime({ - targets: this.$el, - opacity: 0, - 'margin-top': '-8px', - duration: 200, - easing: 'easeOutQuad', - complete: () => this.$destroy() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-user-preview - position absolute - z-index 2048 - margin-top -8px - width 250px - background #fff - background-clip content-box - border solid 1px rgba(0, 0, 0, 0.1) - border-radius 4px - overflow hidden - opacity 0 - - > .banner - height 84px - background-color #f5f5f5 - background-size cover - background-position center - - > .avatar - display block - position absolute - top 62px - left 13px - z-index 2 - - > img - display block - width 58px - height 58px - margin 0 - border solid 3px #fff - border-radius 8px - - > .title - display block - padding 8px 0 8px 82px - - > .name - display inline-block - margin 0 - font-weight bold - line-height 16px - color #656565 - - > .username - display block - margin 0 - line-height 16px - font-size 0.8em - color #999 - - > .description - padding 0 16px - font-size 0.7em - color #555 - - > .status - padding 8px 16px - - > div - display inline-block - width 33% - - > p - margin 0 - font-size 0.7em - color #aaa - - > a - font-size 1em - color $theme-color - - > .mk-follow-button - position absolute - top 92px - right 8px - -</style> diff --git a/src/server/web/app/desktop/views/components/users-list.item.vue b/src/server/web/app/desktop/views/components/users-list.item.vue deleted file mode 100644 index d2bfc117da..0000000000 --- a/src/server/web/app/desktop/views/components/users-list.item.vue +++ /dev/null @@ -1,107 +0,0 @@ -<template> -<div class="root item"> - <router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ user.name }}</router-link> - <span class="username">@{{ acct }}</span> - </header> - <div class="body"> - <p class="followed" v-if="user.isFollowed">フォローされています</p> - <div class="description">{{ user.description }}</div> - </div> - </div> - <mk-follow-button :user="user"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - } - } -}); -</script> - -<style lang="stylus" scoped> -.root.item - padding 16px - font-size 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 16px 0 0 - - > .avatar - display block - width 58px - height 58px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 74px) - - > header - margin-bottom 2px - - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 0 0 8px - color #ccc - - > .body - > .followed - display inline-block - margin 0 0 4px 0 - padding 2px 8px - vertical-align top - font-size 10px - color #71afc7 - background #eefaff - border-radius 4px - - > .description - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - > .mk-follow-button - position absolute - top 16px - right 16px - -</style> diff --git a/src/server/web/app/desktop/views/components/users-list.vue b/src/server/web/app/desktop/views/components/users-list.vue deleted file mode 100644 index a08e76f573..0000000000 --- a/src/server/web/app/desktop/views/components/users-list.vue +++ /dev/null @@ -1,143 +0,0 @@ -<template> -<div class="mk-users-list"> - <nav> - <div> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span> - </div> - </nav> - <div class="users" v-if="!fetching && users.length != 0"> - <div v-for="u in users" :key="u.id"> - <x-item :user="u"/> - </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> - </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> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XItem from './users-list.item.vue'; - -export default Vue.extend({ - components: { - XItem - }, - props: ['fetch', 'count', 'youKnowCount'], - data() { - return { - limit: 30, - mode: 'all', - fetching: true, - moreFetching: false, - users: [], - next: null - }; - }, - mounted() { - this._fetch(() => { - this.$emit('loaded'); - }); - }, - methods: { - _fetch(cb) { - this.fetching = true; - this.fetch(this.mode == 'iknow', this.limit, null, obj => { - this.users = obj.users; - this.next = obj.next; - this.fetching = false; - if (cb) cb(); - }); - }, - more() { - this.moreFetching = true; - this.fetch(this.mode == 'iknow', this.limit, this.next, obj => { - this.moreFetching = false; - this.users = this.users.concat(obj.users); - this.next = obj.next; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-users-list - height 100% - background #fff - - > nav - z-index 1 - box-shadow 0 1px 0 rgba(#000, 0.1) - - > div - display flex - justify-content center - margin 0 auto - max-width 600px - - > span - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - color #657786 - border-bottom solid 2px transparent - cursor pointer - - * - pointer-events none - - &[data-is-active] - font-weight bold - color $theme-color - border-color $theme-color - cursor default - - > span - display inline-block - margin-left 4px - padding 2px 5px - font-size 12px - line-height 1 - color #888 - background #eee - border-radius 20px - - > .users - height calc(100% - 54px) - overflow auto - - > * - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - > * - max-width 600px - margin 0 auto - - > .no - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/components/widget-container.vue b/src/server/web/app/desktop/views/components/widget-container.vue deleted file mode 100644 index 68c5bcb8dc..0000000000 --- a/src/server/web/app/desktop/views/components/widget-container.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<div class="mk-widget-container" :class="{ naked }"> - <header :class="{ withGradient }" v-if="showHeader"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - </header> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - } - }, - computed: { - withGradient(): boolean { - return (this as any).os.isSignedIn - ? (this as any).os.i.account.clientSettings.gradientWindowHeader != null - ? (this as any).os.i.account.clientSettings.gradientWindowHeader - : false - : false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-widget-container - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - overflow hidden - - &.naked - background transparent !important - border none !important - - > header - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - &:empty - display none - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - &.withGradient - > .title - background linear-gradient(to bottom, #fff, #ececec) - box-shadow 0 1px rgba(#000, 0.11) -</style> diff --git a/src/server/web/app/desktop/views/components/window.vue b/src/server/web/app/desktop/views/components/window.vue deleted file mode 100644 index 48dc46febd..0000000000 --- a/src/server/web/app/desktop/views/components/window.vue +++ /dev/null @@ -1,635 +0,0 @@ -<template> -<div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover"> - <div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div> - <div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }"> - <div class="body"> - <header ref="header" - :class="{ withGradient }" - @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown" - > - <h1><slot name="header"></slot></h1> - <div> - <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button> - <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button> - </div> - </header> - <div class="content"> - <slot></slot> - </div> - </div> - <div class="handle top" v-if="canResize" @mousedown.prevent="onTopHandleMousedown"></div> - <div class="handle right" v-if="canResize" @mousedown.prevent="onRightHandleMousedown"></div> - <div class="handle bottom" v-if="canResize" @mousedown.prevent="onBottomHandleMousedown"></div> - <div class="handle left" v-if="canResize" @mousedown.prevent="onLeftHandleMousedown"></div> - <div class="handle top-left" v-if="canResize" @mousedown.prevent="onTopLeftHandleMousedown"></div> - <div class="handle top-right" v-if="canResize" @mousedown.prevent="onTopRightHandleMousedown"></div> - <div class="handle bottom-right" v-if="canResize" @mousedown.prevent="onBottomRightHandleMousedown"></div> - <div class="handle bottom-left" v-if="canResize" @mousedown.prevent="onBottomLeftHandleMousedown"></div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; -import contains from '../../../common/scripts/contains'; - -const minHeight = 40; -const minWidth = 200; - -function dragListen(fn) { - window.addEventListener('mousemove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); -} - -function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); -} - -export default Vue.extend({ - props: { - isModal: { - type: Boolean, - default: false - }, - canClose: { - type: Boolean, - default: true - }, - width: { - type: String, - default: '530px' - }, - height: { - type: String, - default: 'auto' - }, - popoutUrl: { - type: [String, Function], - default: null - }, - name: { - type: String, - default: null - } - }, - - data() { - return { - preventMount: false - }; - }, - - computed: { - isFlexible(): boolean { - return this.height == null; - }, - canResize(): boolean { - return !this.isFlexible; - }, - withGradient(): boolean { - return (this as any).os.isSignedIn - ? (this as any).os.i.account.clientSettings.gradientWindowHeader != null - ? (this as any).os.i.account.clientSettings.gradientWindowHeader - : false - : false; - } - }, - - created() { - if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) { - this.popout(); - this.preventMount = true; - } else { - // ウィンドウをウィンドウシステムに登録 - (this as any).os.windows.add(this); - } - }, - - mounted() { - if (this.preventMount) { - this.$destroy(); - return; - } - - this.$nextTick(() => { - const main = this.$refs.main as any; - main.style.top = '15%'; - main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px'; - - window.addEventListener('resize', this.onBrowserResize); - - this.open(); - }); - }, - - destroyed() { - // ウィンドウをウィンドウシステムから削除 - (this as any).os.windows.remove(this); - - window.removeEventListener('resize', this.onBrowserResize); - }, - - methods: { - open() { - this.$emit('opening'); - - this.top(); - - const bg = this.$refs.bg as any; - const main = this.$refs.main as any; - - if (this.isModal) { - bg.style.pointerEvents = 'auto'; - anime({ - targets: bg, - opacity: 1, - duration: 100, - easing: 'linear' - }); - } - - main.style.pointerEvents = 'auto'; - anime({ - targets: main, - opacity: 1, - scale: [1.1, 1], - duration: 200, - easing: 'easeOutQuad' - }); - - if (focus) main.focus(); - - setTimeout(() => { - this.$emit('opened'); - }, 300); - }, - - close() { - this.$emit('before-close'); - - const bg = this.$refs.bg as any; - const main = this.$refs.main as any; - - if (this.isModal) { - bg.style.pointerEvents = 'none'; - anime({ - targets: bg, - opacity: 0, - duration: 300, - easing: 'linear' - }); - } - - main.style.pointerEvents = 'none'; - - anime({ - targets: main, - opacity: 0, - scale: 0.8, - duration: 300, - easing: [0.5, -0.5, 1, 0.5] - }); - - setTimeout(() => { - this.$destroy(); - this.$emit('closed'); - }, 300); - }, - - popout() { - const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl; - - const main = this.$refs.main as any; - - if (main) { - const position = main.getBoundingClientRect(); - - const width = parseInt(getComputedStyle(main, '').width, 10); - const height = parseInt(getComputedStyle(main, '').height, 10); - const x = window.screenX + position.left; - const y = window.screenY + position.top; - - window.open(url, url, - `width=${width}, height=${height}, top=${y}, left=${x}`); - - this.close(); - } else { - const x = window.top.outerHeight / 2 + window.top.screenY - (parseInt(this.height, 10) / 2); - const y = window.top.outerWidth / 2 + window.top.screenX - (parseInt(this.width, 10) / 2); - window.open(url, url, - `width=${this.width}, height=${this.height}, top=${x}, left=${y}`); - } - }, - - // 最前面へ移動 - top() { - let z = 0; - - (this as any).os.windows.getAll().forEach(w => { - if (w == this) return; - const m = w.$refs.main; - const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex); - if (mz > z) z = mz; - }); - - if (z > 0) { - (this.$refs.main as any).style.zIndex = z + 1; - if (this.isModal) (this.$refs.bg as any).style.zIndex = z + 1; - } - }, - - onBgClick() { - if (this.canClose) this.close(); - }, - - onBodyMousedown() { - this.top(); - }, - - onHeaderMousedown(e) { - const main = this.$refs.main as any; - - if (!contains(main, document.activeElement)) main.focus(); - - const position = main.getBoundingClientRect(); - - const clickX = e.clientX; - const clickY = e.clientY; - const moveBaseX = clickX - position.left; - const moveBaseY = clickY - position.top; - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - moveBaseX; - let moveTop = me.clientY - moveBaseY; - - // 上はみ出し - if (moveTop < 0) moveTop = 0; - - // 左はみ出し - if (moveLeft < 0) moveLeft = 0; - - // 下はみ出し - if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; - - // 右はみ出し - if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; - - main.style.left = moveLeft + 'px'; - main.style.top = moveTop + 'px'; - }); - }, - - // 上ハンドル掴み時 - onTopHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + move > 0) { - if (height + -move > minHeight) { - this.applyTransformHeight(height + -move); - this.applyTransformTop(top + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - this.applyTransformTop(top + (height - minHeight)); - } - } else { // 上のはみ出し時 - this.applyTransformHeight(top + height); - this.applyTransformTop(0); - } - }); - }, - - // 右ハンドル掴み時 - onRightHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - const browserWidth = window.innerWidth; - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + width + move < browserWidth) { - if (width + move > minWidth) { - this.applyTransformWidth(width + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - } - } else { // 右のはみ出し時 - this.applyTransformWidth(browserWidth - left); - } - }); - }, - - // 下ハンドル掴み時 - onBottomHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - const browserHeight = window.innerHeight; - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + height + move < browserHeight) { - if (height + move > minHeight) { - this.applyTransformHeight(height + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - } - } else { // 下のはみ出し時 - this.applyTransformHeight(browserHeight - top); - } - }); - }, - - // 左ハンドル掴み時 - onLeftHandleMousedown(e) { - const main = this.$refs.main as any; - - const base = e.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + move > 0) { - if (width + -move > minWidth) { - this.applyTransformWidth(width + -move); - this.applyTransformLeft(left + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - this.applyTransformLeft(left + (width - minWidth)); - } - } else { // 左のはみ出し時 - this.applyTransformWidth(left + width); - this.applyTransformLeft(0); - } - }); - }, - - // 左上ハンドル掴み時 - onTopLeftHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onLeftHandleMousedown(e); - }, - - // 右上ハンドル掴み時 - onTopRightHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onRightHandleMousedown(e); - }, - - // 右下ハンドル掴み時 - onBottomRightHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onRightHandleMousedown(e); - }, - - // 左下ハンドル掴み時 - onBottomLeftHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onLeftHandleMousedown(e); - }, - - // 高さを適用 - applyTransformHeight(height) { - (this.$refs.main as any).style.height = height + 'px'; - }, - - // 幅を適用 - applyTransformWidth(width) { - (this.$refs.main as any).style.width = width + 'px'; - }, - - // Y座標を適用 - applyTransformTop(top) { - (this.$refs.main as any).style.top = top + 'px'; - }, - - // X座標を適用 - applyTransformLeft(left) { - (this.$refs.main as any).style.left = left + 'px'; - }, - - onDragover(e) { - e.dataTransfer.dropEffect = 'none'; - }, - - onKeydown(e) { - if (e.which == 27) { // Esc - if (this.canClose) { - e.preventDefault(); - e.stopPropagation(); - this.close(); - } - } - }, - - onBrowserResize() { - const main = this.$refs.main as any; - const position = main.getBoundingClientRect(); - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - if (position.left < 0) main.style.left = 0; - if (position.top < 0) main.style.top = 0; - if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; - if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-window - display block - - > .bg - display block - position fixed - z-index 2000 - top 0 - left 0 - width 100% - height 100% - background rgba(0, 0, 0, 0.7) - opacity 0 - pointer-events none - - > .main - display block - position fixed - z-index 2000 - top 15% - left 0 - margin 0 - opacity 0 - pointer-events none - - &:focus - &:not([data-is-modal]) - > .body - box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(0, 0, 0, 0.2) - - > .handle - $size = 8px - - position absolute - - &.top - top -($size) - left 0 - width 100% - height $size - cursor ns-resize - - &.right - top 0 - right -($size) - width $size - height 100% - cursor ew-resize - - &.bottom - bottom -($size) - left 0 - width 100% - height $size - cursor ns-resize - - &.left - top 0 - left -($size) - width $size - height 100% - cursor ew-resize - - &.top-left - top -($size) - left -($size) - width $size * 2 - height $size * 2 - cursor nwse-resize - - &.top-right - top -($size) - right -($size) - width $size * 2 - height $size * 2 - cursor nesw-resize - - &.bottom-right - bottom -($size) - right -($size) - width $size * 2 - height $size * 2 - cursor nwse-resize - - &.bottom-left - bottom -($size) - left -($size) - width $size * 2 - height $size * 2 - cursor nesw-resize - - > .body - height 100% - overflow hidden - background #fff - border-radius 6px - box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2) - - > header - $header-height = 40px - - z-index 1001 - height $header-height - overflow hidden - white-space nowrap - cursor move - background #fff - border-radius 6px 6px 0 0 - box-shadow 0 1px 0 rgba(#000, 0.1) - - &.withGradient - background linear-gradient(to bottom, #fff, #ececec) - box-shadow 0 1px 0 rgba(#000, 0.15) - - &, * - user-select none - - > h1 - pointer-events none - display block - margin 0 auto - overflow hidden - height $header-height - text-overflow ellipsis - text-align center - font-size 1em - line-height $header-height - font-weight normal - color #666 - - > div:last-child - position absolute - top 0 - right 0 - display block - z-index 1 - - > * - display inline-block - margin 0 - padding 0 - cursor pointer - font-size 1em - color rgba(#000, 0.4) - border none - outline none - background transparent - - &:hover - color rgba(#000, 0.6) - - &:active - color darken(#000, 30%) - - > [data-fa] - padding 0 - width $header-height - line-height $header-height - text-align center - - > .content - height 100% - - &:not([flexible]) - > .main > .body > .content - height calc(100% - 40px) - -</style> diff --git a/src/server/web/app/desktop/views/directives/index.ts b/src/server/web/app/desktop/views/directives/index.ts deleted file mode 100644 index 324e07596d..0000000000 --- a/src/server/web/app/desktop/views/directives/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue'; - -import userPreview from './user-preview'; - -Vue.directive('userPreview', userPreview); -Vue.directive('user-preview', userPreview); diff --git a/src/server/web/app/desktop/views/directives/user-preview.ts b/src/server/web/app/desktop/views/directives/user-preview.ts deleted file mode 100644 index 8a4035881a..0000000000 --- a/src/server/web/app/desktop/views/directives/user-preview.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * マウスオーバーするとユーザーがプレビューされる要素を設定します - */ - -import MkUserPreview from '../components/user-preview.vue'; - -export default { - bind(el, binding, vn) { - const self = el._userPreviewDirective_ = {} as any; - - self.user = binding.value; - self.tag = null; - self.showTimer = null; - self.hideTimer = null; - - self.close = () => { - if (self.tag) { - self.tag.close(); - self.tag = null; - } - }; - - const show = () => { - if (self.tag) return; - - self.tag = new MkUserPreview({ - parent: vn.context, - propsData: { - user: self.user - } - }).$mount(); - - const preview = self.tag.$el; - const rect = el.getBoundingClientRect(); - const x = rect.left + el.offsetWidth + window.pageXOffset; - const y = rect.top + window.pageYOffset; - - preview.style.top = y + 'px'; - preview.style.left = x + 'px'; - - preview.addEventListener('mouseover', () => { - clearTimeout(self.hideTimer); - }); - - preview.addEventListener('mouseleave', () => { - clearTimeout(self.showTimer); - self.hideTimer = setTimeout(self.close, 500); - }); - - document.body.appendChild(preview); - }; - - el.addEventListener('mouseover', () => { - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.showTimer = setTimeout(show, 500); - }); - - el.addEventListener('mouseleave', () => { - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.hideTimer = setTimeout(self.close, 500); - }); - }, - - unbind(el, binding, vn) { - const self = el._userPreviewDirective_; - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.close(); - } -}; diff --git a/src/server/web/app/desktop/views/pages/drive.vue b/src/server/web/app/desktop/views/pages/drive.vue deleted file mode 100644 index 353f59b703..0000000000 --- a/src/server/web/app/desktop/views/pages/drive.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<div class="mk-drive-page"> - <mk-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - folder: null - }; - }, - created() { - this.folder = this.$route.params.folder; - }, - mounted() { - document.title = 'Misskey Drive'; - }, - methods: { - onMoveRoot() { - const title = 'Misskey Drive'; - - // Rewrite URL - history.pushState(null, title, '/i/drive'); - - document.title = title; - }, - onOpenFolder(folder) { - const title = folder.name + ' | Misskey Drive'; - - // Rewrite URL - history.pushState(null, title, '/i/drive/folder/' + folder.id); - - document.title = title; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive-page - position fixed - width 100% - height 100% - background #fff - - > .mk-drive - height 100% -</style> - diff --git a/src/server/web/app/desktop/views/pages/home-customize.vue b/src/server/web/app/desktop/views/pages/home-customize.vue deleted file mode 100644 index 8aa06be57f..0000000000 --- a/src/server/web/app/desktop/views/pages/home-customize.vue +++ /dev/null @@ -1,12 +0,0 @@ -<template> -<mk-home customize/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - mounted() { - document.title = 'Misskey - ホームのカスタマイズ'; - } -}); -</script> diff --git a/src/server/web/app/desktop/views/pages/home.vue b/src/server/web/app/desktop/views/pages/home.vue deleted file mode 100644 index 69e134f79f..0000000000 --- a/src/server/web/app/desktop/views/pages/home.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<mk-ui> - <mk-home :mode="mode" @loaded="loaded"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; -import getPostSummary from '../../../../../common/get-post-summary'; - -export default Vue.extend({ - props: { - mode: { - type: String, - default: 'timeline' - } - }, - data() { - return { - connection: null, - connectionId: null, - unreadCount: 0 - }; - }, - mounted() { - document.title = 'Misskey'; - - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('post', this.onStreamPost); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - - Progress.start(); - }, - beforeDestroy() { - this.connection.off('post', this.onStreamPost); - (this as any).os.stream.dispose(this.connectionId); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, - methods: { - loaded() { - Progress.done(); - }, - - onStreamPost(post) { - if (document.hidden && post.userId != (this as any).os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getPostSummary(post)}`; - } - }, - - onVisibilitychange() { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/pages/index.vue b/src/server/web/app/desktop/views/pages/index.vue deleted file mode 100644 index 0ea47d913b..0000000000 --- a/src/server/web/app/desktop/views/pages/index.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<component :is="os.isSignedIn ? 'home' : 'welcome'"></component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Home from './home.vue'; -import Welcome from './welcome.vue'; - -export default Vue.extend({ - components: { - Home, - Welcome - } -}); -</script> diff --git a/src/server/web/app/desktop/views/pages/messaging-room.vue b/src/server/web/app/desktop/views/pages/messaging-room.vue deleted file mode 100644 index 0cab1e0d10..0000000000 --- a/src/server/web/app/desktop/views/pages/messaging-room.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="mk-messaging-room-page"> - <mk-messaging-room v-if="user" :user="user" :is-naked="true"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../common/user/parse-acct'; - -export default Vue.extend({ - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.documentElement.style.background = '#fff'; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = 'メッセージ: ' + this.user.name; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-messaging-room-page - display flex - flex 1 - flex-direction column - min-height 100% - background #fff - -</style> diff --git a/src/server/web/app/desktop/views/pages/othello.vue b/src/server/web/app/desktop/views/pages/othello.vue deleted file mode 100644 index 0d8e987dd9..0000000000 --- a/src/server/web/app/desktop/views/pages/othello.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> -<component :is="ui ? 'mk-ui' : 'div'"> - <mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/> -</component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - props: { - ui: { - default: false - } - }, - data() { - return { - fetching: false, - game: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - if (this.$route.params.game == null) return; - - Progress.start(); - this.fetching = true; - - (this as any).api('othello/games/show', { - gameId: this.$route.params.game - }).then(game => { - this.game = game; - this.fetching = false; - - Progress.done(); - }); - }, - onGamed(game) { - history.pushState(null, null, '/othello/' + game.id); - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/pages/post.vue b/src/server/web/app/desktop/views/pages/post.vue deleted file mode 100644 index dbd707e049..0000000000 --- a/src/server/web/app/desktop/views/pages/post.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<mk-ui> - <main v-if="!fetching"> - <a v-if="post.next" :href="post.next">%fa:angle-up%%i18n:desktop.tags.mk-post-page.next%</a> - <mk-post-detail :post="post"/> - <a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - data() { - return { - fetching: true, - post: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - (this as any).api('posts/show', { - postId: this.$route.params.post - }).then(post => { - this.post = post; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - padding 16px - text-align center - - > a - display inline-block - - &:first-child - margin-bottom 4px - - &:last-child - margin-top 4px - - > [data-fa] - margin-right 4px - - > .mk-post-detail - margin 0 auto - width 640px - -</style> diff --git a/src/server/web/app/desktop/views/pages/search.vue b/src/server/web/app/desktop/views/pages/search.vue deleted file mode 100644 index afd37c8cee..0000000000 --- a/src/server/web/app/desktop/views/pages/search.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<mk-ui> - <header :class="$style.header"> - <h1>{{ q }}</h1> - </header> - <div :class="$style.loading" v-if="fetching"> - <mk-ellipsis-icon/> - </div> - <p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> - <mk-posts ref="timeline" :class="$style.posts" :posts="posts"> - <div slot="footer"> - <template v-if="!moreFetching">%fa:search%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </div> - </mk-posts> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; -import parse from '../../../common/scripts/parse-search-query'; - -const limit = 20; - -export default Vue.extend({ - data() { - return { - fetching: true, - moreFetching: false, - existMore: false, - offset: 0, - posts: [] - }; - }, - watch: { - $route: 'fetch' - }, - computed: { - empty(): boolean { - return this.posts.length == 0; - }, - q(): string { - return this.$route.query.q; - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.fetch(); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.timeline as any).focus(); - } - } - }, - fetch() { - this.fetching = true; - Progress.start(); - - (this as any).api('posts/search', Object.assign({ - limit: limit + 1, - offset: this.offset - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - this.existMore = true; - } - this.posts = posts; - this.fetching = false; - Progress.done(); - }); - }, - more() { - if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return; - this.offset += limit; - this.moreFetching = true; - return (this as any).api('posts/search', Object.assign({ - limit: limit + 1, - offset: this.offset - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - } else { - this.existMore = false; - } - this.posts = this.posts.concat(posts); - this.moreFetching = false; - }); - }, - onScroll() { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16) this.more(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - width 100% - max-width 600px - margin 0 auto - color #555 - -.posts - max-width 600px - margin 0 auto - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - overflow hidden - -.loading - padding 64px 0 - -.empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc - -</style> diff --git a/src/server/web/app/desktop/views/pages/selectdrive.vue b/src/server/web/app/desktop/views/pages/selectdrive.vue deleted file mode 100644 index 4f0b86014b..0000000000 --- a/src/server/web/app/desktop/views/pages/selectdrive.vue +++ /dev/null @@ -1,177 +0,0 @@ -<template> -<div class="mkp-selectdrive"> - <mk-drive ref="browser" - :multiple="multiple" - @selected="onSelected" - @change-selection="onChangeSelection" - /> - <footer> - <button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button> - <button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button> - <button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%'; - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - close() { - window.close(); - }, - ok() { - window.opener.cb(this.multiple ? this.files : this.files[0]); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mkp-selectdrive - display block - position fixed - width 100% - height 100% - background #fff - - > .mk-drive - height calc(100% - 72px) - - > footer - position fixed - bottom 0 - left 0 - width 100% - height 72px - background lighten($theme-color, 95%) - - .upload - display inline-block - position absolute - top 8px - left 16px - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color rgba($theme-color, 0.5) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color rgba($theme-color, 0.3) - - &:active - color rgba($theme-color, 0.6) - background transparent - border-color rgba($theme-color, 0.5) - box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - .ok - .cancel - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - .ok - right 16px - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active:not(:disabled) - background $theme-color - border-color $theme-color - - .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue b/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue deleted file mode 100644 index d0dab6c3df..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> -<div class="followers-you-know"> - <p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p> - <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p> - <div v-if="!fetching && users.length > 0"> - <router-link v-for="user in users" :to="`/@${getAcct(user)}`" :key="user.id"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/> - </router-link> - </div> - <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - data() { - return { - users: [], - fetching: true - }; - }, - method() { - getAcct - }, - mounted() { - (this as any).api('users/followers', { - userId: this.user.id, - iknow: true, - limit: 16 - }).then(x => { - this.users = x.users; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.followers-you-know - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > div - padding 8px - - > a - display inline-block - margin 4px - - > img - width 48px - height 48px - vertical-align bottom - border-radius 100% - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.friends.vue b/src/server/web/app/desktop/views/pages/user/user.friends.vue deleted file mode 100644 index 3ec30fb438..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.friends.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<div class="friends"> - <p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p> - <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p> - <template v-if="!fetching && users.length != 0"> - <div class="user" v-for="friend in users"> - <router-link class="avatar-anchor" :to="`/@${getAcct(friend)}`"> - <img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/> - </router-link> - <div class="body"> - <router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link> - <p class="username">@{{ getAcct(friend) }}</p> - </div> - <mk-follow-button :user="friend"/> - </div> - </template> - <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - data() { - return { - users: [], - fetching: true - }; - }, - method() { - getAcct - }, - mounted() { - (this as any).api('users/get_frequently_replied_users', { - userId: this.user.id, - limit: 4 - }).then(docs => { - this.users = docs.map(doc => doc.user); - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.friends - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - - > .user - padding 16px - border-bottom solid 1px #eee - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color #ccc - - > .mk-follow-button - position absolute - top 16px - right 16px - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.header.vue b/src/server/web/app/desktop/views/pages/user/user.header.vue deleted file mode 100644 index 54f431fd2e..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.header.vue +++ /dev/null @@ -1,196 +0,0 @@ -<template> -<div class="header" :data-is-dark-background="user.bannerUrl != null"> - <div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''"> - <div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div> - </div> - <div class="fade"></div> - <div class="container"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/> - <div class="title"> - <p class="name">{{ user.name }}</p> - <p class="username">@{{ acct }}</p> - <p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p> - </div> - <footer> - <router-link :to="`/@${acct}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link> - <router-link :to="`/@${acct}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link> - <router-link :to="`/@${acct}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - } - }, - mounted() { - window.addEventListener('load', this.onScroll); - window.addEventListener('scroll', this.onScroll); - window.addEventListener('resize', this.onScroll); - }, - beforeDestroy() { - window.removeEventListener('load', this.onScroll); - window.removeEventListener('scroll', this.onScroll); - window.removeEventListener('resize', this.onScroll); - }, - methods: { - onScroll() { - const banner = this.$refs.banner as any; - - const top = window.scrollY; - - const z = 1.25; // 奥行き(小さいほど奥) - const pos = -(top / z); - banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; - - const blur = top / 32 - if (blur <= 10) banner.style.filter = `blur(${blur}px)`; - }, - - onBannerClick() { - if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return; - - (this as any).apis.updateBanner((this as any).os.i, i => { - this.user.bannerUrl = i.bannerUrl; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.header - $banner-height = 320px - $footer-height = 58px - - overflow hidden - background #f7f7f7 - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) - - &[data-is-dark-background] - > .banner-container - > .banner - background-color #383838 - - > .fade - background linear-gradient(transparent, rgba(0, 0, 0, 0.7)) - - > .container - > .title - color #fff - - > .name - text-shadow 0 0 8px #000 - - > .banner-container - height $banner-height - overflow hidden - background-size cover - background-position center - - > .banner - height 100% - background-color #f5f5f5 - background-size cover - background-position center - - > .fade - $fade-hight = 78px - - position absolute - top ($banner-height - $fade-hight) - left 0 - width 100% - height $fade-hight - - > .container - max-width 1200px - margin 0 auto - - > .avatar - display block - position absolute - bottom 16px - left 16px - z-index 2 - width 160px - height 160px - margin 0 - border solid 3px #fff - border-radius 8px - box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2) - - > .title - position absolute - bottom $footer-height - left 0 - width 100% - padding 0 0 8px 195px - color #656565 - font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif - - > .name - display block - margin 0 - line-height 40px - font-weight bold - font-size 2em - - > .username - > .location - display inline-block - margin 0 16px 0 0 - line-height 20px - opacity 0.8 - - > i - margin-right 4px - - > footer - z-index 1 - height $footer-height - padding-left 195px - - > a - display inline-block - margin 0 - padding 0 16px - height $footer-height - line-height $footer-height - color #555 - - &[data-active] - border-bottom solid 4px $theme-color - - > i - margin-right 6px - - > button - display block - position absolute - top 0 - right 0 - margin 8px - padding 0 - width $footer-height - 16px - line-height $footer-height - 16px - 2px - font-size 1.2em - color #777 - border solid 1px #eee - border-radius 4px - - &:hover - color #555 - border solid 1px #ddd - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.home.vue b/src/server/web/app/desktop/views/pages/user/user.home.vue deleted file mode 100644 index 071c9bb61c..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.home.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<div class="home"> - <div> - <div ref="left"> - <x-profile :user="user"/> - <x-photos :user="user"/> - <x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> - <p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p> - </div> - </div> - <main> - <mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/> - <x-timeline class="timeline" ref="tl" :user="user"/> - </main> - <div> - <div ref="right"> - <mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/> - <mk-activity :user="user"/> - <x-friends :user="user"/> - <div class="nav"><mk-nav/></div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XTimeline from './user.timeline.vue'; -import XProfile from './user.profile.vue'; -import XPhotos from './user.photos.vue'; -import XFollowersYouKnow from './user.followers-you-know.vue'; -import XFriends from './user.friends.vue'; - -export default Vue.extend({ - components: { - XTimeline, - XProfile, - XPhotos, - XFollowersYouKnow, - XFriends - }, - props: ['user'], - methods: { - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -.home - display flex - justify-content center - margin 0 auto - max-width 1200px - - > main - > div > div - > *:not(:last-child) - margin-bottom 16px - - > main - padding 16px - width calc(100% - 275px * 2) - - > .timeline - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > div - width 275px - margin 0 - - &:first-child > div - padding 16px 0 16px 16px - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.8em - color #aaa - - &:last-child > div - padding 16px 16px 16px 0 - - > .nav - padding 16px - font-size 12px - color #aaa - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - a - color #999 - - i - color #ccc - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.photos.vue b/src/server/web/app/desktop/views/pages/user/user.photos.vue deleted file mode 100644 index 1ff79b4aee..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.photos.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<div class="photos"> - <p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p> - <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p> - <div class="stream" v-if="!fetching && images.length > 0"> - <div v-for="image in images" class="img" - :style="`background-image: url(${image.url}?thumbnail&size=256)`" - ></div> - </div> - <p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - data() { - return { - images: [], - fetching: true - }; - }, - mounted() { - (this as any).api('users/posts', { - userId: this.user.id, - withMedia: true, - limit: 9 - }).then(posts => { - posts.forEach(post => { - post.media.forEach(media => { - if (this.images.length < 9) this.images.push(media); - }); - }); - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.photos - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > i - margin-right 4px - - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - > .img - flex 1 1 33% - width 33% - height 80px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.profile.vue b/src/server/web/app/desktop/views/pages/user/user.profile.vue deleted file mode 100644 index f5562d0915..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.profile.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<div class="profile"> - <div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id"> - <mk-follow-button :user="user" size="big"/> - <p class="followed" v-if="user.isFollowed">%i18n:desktop.tags.mk-user.follows-you%</p> - <p v-if="user.isMuted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p> - <p v-if="!user.isMuted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p> - </div> - <div class="description" v-if="user.description">{{ user.description }}</div> - <div class="birthday" v-if="user.host === null && user.account.profile.birthday"> - <p>%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p> - </div> - <div class="twitter" v-if="user.host === null && user.account.twitter"> - <p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screenName}`" target="_blank">@{{ user.account.twitter.screenName }}</a></p> - </div> - <div class="status"> - <p class="posts-count">%fa:angle-right%<a>{{ user.postsCount }}</a><b>投稿</b></p> - <p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p> - <p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as age from 's-age'; -import MkFollowingWindow from '../../components/following-window.vue'; -import MkFollowersWindow from '../../components/followers-window.vue'; - -export default Vue.extend({ - props: ['user'], - computed: { - age(): number { - return age(this.user.account.profile.birthday); - } - }, - methods: { - showFollowing() { - (this as any).os.new(MkFollowingWindow, { - user: this.user - }); - }, - - showFollowers() { - (this as any).os.new(MkFollowersWindow, { - user: this.user - }); - }, - - mute() { - (this as any).api('mute/create', { - userId: this.user.id - }).then(() => { - this.user.isMuted = true; - }, () => { - alert('error'); - }); - }, - - unmute() { - (this as any).api('mute/delete', { - userId: this.user.id - }).then(() => { - this.user.isMuted = false; - }, () => { - alert('error'); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.profile - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > *:first-child - border-top none !important - - > .friend-form - padding 16px - border-top solid 1px #eee - - > .mk-big-follow-button - width 100% - - > .followed - margin 12px 0 0 0 - padding 0 - text-align center - line-height 24px - font-size 0.8em - color #71afc7 - background #eefaff - border-radius 4px - - > .description - padding 16px - color #555 - border-top solid 1px #eee - - > .birthday - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .twitter - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 0 - - > i - margin-right 8px - - > .status - padding 16px - color #555 - border-top solid 1px #eee - - > p - margin 8px 0 - - > i - margin-left 8px - margin-right 8px - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.timeline.vue b/src/server/web/app/desktop/views/pages/user/user.timeline.vue deleted file mode 100644 index 134ad423ce..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.timeline.vue +++ /dev/null @@ -1,139 +0,0 @@ -<template> -<div class="timeline"> - <header> - <span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span> - <span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span> - </header> - <div class="loading" v-if="fetching"> - <mk-ellipsis-icon/> - </div> - <p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p> - <mk-posts ref="timeline" :posts="posts"> - <div slot="footer"> - <template v-if="!moreFetching">%fa:moon%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </div> - </mk-posts> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - data() { - return { - fetching: true, - moreFetching: false, - mode: 'default', - unreadCount: 0, - posts: [], - date: null - }; - }, - watch: { - mode() { - this.fetch(); - } - }, - computed: { - empty(): boolean { - return this.posts.length == 0; - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); - - this.fetch(() => this.$emit('loaded')); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { - if (e.which == 84) { // [t] - (this.$refs.timeline as any).focus(); - } - } - }, - fetch(cb?) { - (this as any).api('users/posts', { - userId: this.user.id, - untilDate: this.date ? this.date.getTime() : undefined, - with_replies: this.mode == 'with-replies' - }).then(posts => { - this.posts = posts; - this.fetching = false; - if (cb) cb(); - }); - }, - more() { - if (this.moreFetching || this.fetching || this.posts.length == 0) return; - this.moreFetching = true; - (this as any).api('users/posts', { - userId: this.user.id, - with_replies: this.mode == 'with-replies', - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { - this.moreFetching = false; - this.posts = this.posts.concat(posts); - }); - }, - onScroll() { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16/*遊び*/) { - this.more(); - } - }, - warp(date) { - this.date = date; - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.timeline - background #fff - - > header - padding 8px 16px - border-bottom solid 1px #eee - - > span - margin-right 16px - line-height 27px - font-size 18px - color #555 - - &:not([data-is-active]) - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - > .loading - padding 64px 0 - - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc - -</style> diff --git a/src/server/web/app/desktop/views/pages/user/user.vue b/src/server/web/app/desktop/views/pages/user/user.vue deleted file mode 100644 index 67cef93269..0000000000 --- a/src/server/web/app/desktop/views/pages/user/user.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<mk-ui> - <div class="user" v-if="!fetching"> - <x-header :user="user"/> - <x-home v-if="page == 'home'" :user="user"/> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import parseAcct from '../../../../../../common/user/parse-acct'; -import Progress from '../../../../common/scripts/loading'; -import XHeader from './user.header.vue'; -import XHome from './user.home.vue'; - -export default Vue.extend({ - components: { - XHeader, - XHome - }, - props: { - page: { - default: 'home' - } - }, - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - Progress.start(); - (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - Progress.done(); - document.title = user.name + ' | Misskey'; - }); - } - } -}); -</script> - diff --git a/src/server/web/app/desktop/views/pages/welcome.vue b/src/server/web/app/desktop/views/pages/welcome.vue deleted file mode 100644 index 34c28854b1..0000000000 --- a/src/server/web/app/desktop/views/pages/welcome.vue +++ /dev/null @@ -1,319 +0,0 @@ -<template> -<div class="mk-welcome"> - <main> - <div class="top"> - <div> - <div> - <h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1> - <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p> - <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p> - <div class="users"> - <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - </div> - </div> - <div> - <div> - <header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header> - <mk-welcome-timeline/> - </div> - </div> - </div> - </div> - </main> - <mk-forkit/> - <footer> - <div> - <mk-nav :class="$style.nav"/> - <p class="c">{{ copyright }}</p> - </div> - </footer> - <modal name="signup" width="500px" height="auto" scrollable> - <header :class="$style.signupFormHeader">新規登録</header> - <mk-signup :class="$style.signupForm"/> - </modal> - <modal name="signin" width="500px" height="auto" scrollable> - <header :class="$style.signinFormHeader">ログイン</header> - <mk-signin :class="$style.signinForm"/> - </modal> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { docsUrl, copyright, lang } from '../../../config'; -import getAcct from '../../../../../common/user/get-acct'; - -const shares = [ - 'Everything!', - 'Webpages', - 'Photos', - 'Interests', - 'Favorites' -]; - -export default Vue.extend({ - data() { - return { - aboutUrl: `${docsUrl}/${lang}/about`, - copyright, - users: [], - clock: null, - i: 0 - }; - }, - mounted() { - (this as any).api('users', { - sort: '+follower', - limit: 20 - }).then(users => { - this.users = users; - }); - - this.clock = setInterval(() => { - if (++this.i == shares.length) this.i = 0; - const speed = 70; - const text = (this.$refs.share as any).innerText; - for (let i = 0; i < text.length; i++) { - setTimeout(() => { - if (this.$refs.share) { - (this.$refs.share as any).innerText = text.substr(0, text.length - i); - } - }, i * speed) - } - setTimeout(() => { - const newText = shares[this.i]; - for (let i = 0; i <= newText.length; i++) { - setTimeout(() => { - if (this.$refs.share) { - (this.$refs.share as any).innerText = newText.substr(0, i); - } - }, i * speed) - } - }, text.length * speed); - }, 4000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - getAcct, - signup() { - this.$modal.show('signup'); - }, - signin() { - this.$modal.show('signin'); - } - } -}); -</script> - -<style> -#wait { - right: auto; - left: 15px; -} -</style> - -<style lang="stylus" scoped> -@import '~const.styl' - -@import url('https://fonts.googleapis.com/css?family=Sarpanch:700') - -.mk-welcome - display flex - flex-direction column - flex 1 - $width = 1000px - - background-image url('/assets/welcome-bg.svg') - background-size cover - background-position top center - - &:before - content "" - display block - position fixed - bottom 0 - left 0 - width 100% - height 100% - background-image url('/assets/welcome-fg.svg') - background-size cover - background-position bottom center - - > main - display flex - flex 1 - - > .top - display flex - width 100% - - > div - display flex - max-width $width + 64px - margin 0 auto - padding 80px 32px 0 32px - - > * - margin-bottom 48px - - > div:first-child - margin-right 48px - color #fff - text-shadow 0 0 12px #172062 - - > h1 - margin 0 - font-weight bold - //font-variant small-caps - letter-spacing 12px - font-family 'Sarpanch', sans-serif - font-size 42px - line-height 48px - - > .cursor - animation cursor 1s infinite linear both - - @keyframes cursor - 0% - opacity 1 - 50% - opacity 0 - - > p - margin 1em 0 - line-height 2em - - button - padding 8px 16px - font-size inherit - - .signup - color $theme-color - border solid 2px $theme-color - border-radius 4px - - &:focus - box-shadow 0 0 0 3px rgba($theme-color, 0.2) - - &:hover - color $theme-color-foreground - background $theme-color - - &:active - color $theme-color-foreground - background darken($theme-color, 10%) - border-color darken($theme-color, 10%) - - .signin - &:hover - color #fff - - > .users - margin 16px 0 0 0 - - > * - display inline-block - margin 4px - - > * - display inline-block - width 38px - height 38px - vertical-align top - border-radius 6px - - > div:last-child - - > div - width 410px - background #fff - border-radius 8px - box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1) - overflow hidden - - > header - z-index 1 - padding 12px 16px - color #888d94 - box-shadow 0 1px 0px rgba(0, 0, 0, 0.1) - - > div - position absolute - top 0 - right 0 - padding inherit - - > span - display inline-block - height 11px - width 11px - margin-left 6px - background #ccc - border-radius 100% - vertical-align middle - - &:nth-child(1) - background #5BCC8B - - &:nth-child(2) - background #E6BB46 - - &:nth-child(3) - background #DF7065 - - > .mk-welcome-timeline - max-height 350px - overflow auto - - > footer - font-size 12px - color #949ea5 - - > div - max-width $width - margin 0 auto - padding 0 0 42px 0 - text-align center - - > .c - margin 16px 0 0 0 - font-size 10px - opacity 0.7 - -</style> - -<style lang="stylus" module> -.signupForm - padding 24px 48px 48px 48px - -.signupFormHeader - padding 48px 0 12px 0 - margin: 0 48px - font-size 1.5em - color #777 - border-bottom solid 1px #eee - -.signinForm - padding 24px 48px 48px 48px - -.signinFormHeader - padding 48px 0 12px 0 - margin: 0 48px - font-size 1.5em - color #777 - border-bottom solid 1px #eee - -.nav - a - color #666 -</style> - -<style lang="stylus"> -html -body - background linear-gradient(to bottom, #1e1d65, #bd6659) -</style> diff --git a/src/server/web/app/desktop/views/widgets/activity.vue b/src/server/web/app/desktop/views/widgets/activity.vue deleted file mode 100644 index 0bdf4622af..0000000000 --- a/src/server/web/app/desktop/views/widgets/activity.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<mk-activity - :design="props.design" - :init-view="props.view" - :user="os.i" - @view-changed="viewChanged"/> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'activity', - props: () => ({ - design: 0, - view: 0 - }) -}).extend({ - methods: { - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - }, - viewChanged(view) { - this.props.view = view; - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.form.vue b/src/server/web/app/desktop/views/widgets/channel.channel.form.vue deleted file mode 100644 index aaf327f1ef..0000000000 --- a/src/server/web/app/desktop/views/widgets/channel.channel.form.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<div class="form"> - <input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて"> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - text: '', - wait: false - }; - }, - methods: { - onKeydown(e) { - if (e.which == 10 || e.which == 13) this.post(); - }, - post() { - this.wait = true; - - let reply = null; - - if (/^>>([0-9]+) /.test(this.text)) { - const index = this.text.match(/^>>([0-9]+) /)[1]; - reply = (this.$parent as any).posts.find(p => p.index.toString() == index); - this.text = this.text.replace(/^>>([0-9]+) /, ''); - } - - (this as any).api('posts/create', { - text: this.text, - replyId: reply ? reply.id : undefined, - channelId: (this.$parent as any).channel.id - }).then(data => { - this.text = ''; - }).catch(err => { - alert('失敗した'); - }).then(() => { - this.wait = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.form - width 100% - height 38px - padding 4px - border-top solid 1px #ddd - - > input - padding 0 8px - width 100% - height 100% - font-size 14px - color #55595c - border solid 1px #dadada - border-radius 4px - - &:hover - &:focus - border-color #aeaeae - -</style> diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.post.vue b/src/server/web/app/desktop/views/widgets/channel.channel.post.vue deleted file mode 100644 index 433f9a00aa..0000000000 --- a/src/server/web/app/desktop/views/widgets/channel.channel.post.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<div class="post"> - <header> - <a class="index" @click="reply">{{ post.index }}:</a> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link> - <span>ID:<i>{{ acct }}</i></span> - </header> - <div> - <a v-if="post.reply">>>{{ post.reply.index }}</a> - {{ post.text }} - <div class="media" v-if="post.media"> - <a v-for="file in post.media" :href="file.url" target="_blank"> - <img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/> - </a> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - } - }, - methods: { - reply() { - this.$emit('reply', this.post); - } - } -}); -</script> - -<style lang="stylus" scoped> -.post - margin 0 - padding 0 - color #444 - - > header - position -webkit-sticky - position sticky - z-index 1 - top 0 - padding 8px 4px 4px 16px - background rgba(255, 255, 255, 0.9) - - > .index - margin-right 0.25em - - > .name - margin-right 0.5em - color #008000 - - > div - padding 0 16px 16px 16px - - > .media - > a - display inline-block - - > img - max-width 100% - vertical-align bottom - -</style> diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.vue b/src/server/web/app/desktop/views/widgets/channel.channel.vue deleted file mode 100644 index e9fb9e3fd7..0000000000 --- a/src/server/web/app/desktop/views/widgets/channel.channel.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="channel"> - <p v-if="fetching">読み込み中<mk-ellipsis/></p> - <div v-if="!fetching" ref="posts" class="posts"> - <p v-if="posts.length == 0">まだ投稿がありません</p> - <x-post class="post" v-for="post in posts.slice().reverse()" :post="post" :key="post.id" @reply="reply"/> - </div> - <x-form class="form" ref="form"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import ChannelStream from '../../../common/scripts/streaming/channel'; -import XForm from './channel.channel.form.vue'; -import XPost from './channel.channel.post.vue'; - -export default Vue.extend({ - components: { - XForm, - XPost - }, - props: ['channel'], - data() { - return { - fetching: true, - posts: [], - connection: null - }; - }, - watch: { - channel() { - this.zap(); - } - }, - mounted() { - this.zap(); - }, - beforeDestroy() { - this.disconnect(); - }, - methods: { - zap() { - this.fetching = true; - - (this as any).api('channels/posts', { - channelId: this.channel.id - }).then(posts => { - this.posts = posts; - this.fetching = false; - - this.$nextTick(() => { - this.scrollToBottom(); - }); - - this.disconnect(); - this.connection = new ChannelStream((this as any).os, this.channel.id); - this.connection.on('post', this.onPost); - }); - }, - disconnect() { - if (this.connection) { - this.connection.off('post', this.onPost); - this.connection.close(); - } - }, - onPost(post) { - this.posts.unshift(post); - this.scrollToBottom(); - }, - scrollToBottom() { - (this.$refs.posts as any).scrollTop = (this.$refs.posts as any).scrollHeight; - }, - reply(post) { - (this.$refs.form as any).text = `>>${ post.index } `; - } - } -}); -</script> - -<style lang="stylus" scoped> -.channel - - > p - margin 0 - padding 16px - text-align center - color #aaa - - > .posts - height calc(100% - 38px) - overflow auto - font-size 0.9em - - > .post - border-bottom solid 1px #eee - - &:last-child - border-bottom none - - > .form - position absolute - left 0 - bottom 0 - -</style> diff --git a/src/server/web/app/desktop/views/widgets/channel.vue b/src/server/web/app/desktop/views/widgets/channel.vue deleted file mode 100644 index c9b62dfeab..0000000000 --- a/src/server/web/app/desktop/views/widgets/channel.vue +++ /dev/null @@ -1,107 +0,0 @@ -<template> -<div class="mkw-channel"> - <template v-if="!props.compact"> - <p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' }}</p> - <button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button> - </template> - <p class="get-started" v-if="props.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p> - <x-channel class="channel" :channel="channel" v-if="channel != null"/> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import XChannel from './channel.channel.vue'; - -export default define({ - name: 'server', - props: () => ({ - channel: null, - compact: false - }) -}).extend({ - components: { - XChannel - }, - data() { - return { - fetching: true, - channel: null - }; - }, - mounted() { - if (this.props.channel) { - this.zap(); - } - }, - methods: { - func() { - this.props.compact = !this.props.compact; - }, - settings() { - const id = window.prompt('チャンネルID'); - if (!id) return; - this.props.channel = id; - this.zap(); - }, - zap() { - this.fetching = true; - - (this as any).api('channels/show', { - channelId: this.props.channel - }).then(channel => { - this.channel = channel; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-channel - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - overflow hidden - - > .title - z-index 2 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .get-started - margin 0 - padding 16px - text-align center - color #aaa - - > .channel - height 200px - -</style> diff --git a/src/server/web/app/desktop/views/widgets/index.ts b/src/server/web/app/desktop/views/widgets/index.ts deleted file mode 100644 index 77d771d6b3..0000000000 --- a/src/server/web/app/desktop/views/widgets/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Vue from 'vue'; - -import wNotifications from './notifications.vue'; -import wTimemachine from './timemachine.vue'; -import wActivity from './activity.vue'; -import wTrends from './trends.vue'; -import wUsers from './users.vue'; -import wPolls from './polls.vue'; -import wPostForm from './post-form.vue'; -import wMessaging from './messaging.vue'; -import wChannel from './channel.vue'; -import wProfile from './profile.vue'; - -Vue.component('mkw-notifications', wNotifications); -Vue.component('mkw-timemachine', wTimemachine); -Vue.component('mkw-activity', wActivity); -Vue.component('mkw-trends', wTrends); -Vue.component('mkw-users', wUsers); -Vue.component('mkw-polls', wPolls); -Vue.component('mkw-post-form', wPostForm); -Vue.component('mkw-messaging', wMessaging); -Vue.component('mkw-channel', wChannel); -Vue.component('mkw-profile', wProfile); diff --git a/src/server/web/app/desktop/views/widgets/messaging.vue b/src/server/web/app/desktop/views/widgets/messaging.vue deleted file mode 100644 index 2c9f473bd1..0000000000 --- a/src/server/web/app/desktop/views/widgets/messaging.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<div class="mkw-messaging"> - <p class="title" v-if="props.design == 0">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p> - <mk-messaging ref="index" compact @navigate="navigate"/> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import MkMessagingRoomWindow from '../components/messaging-room-window.vue'; - -export default define({ - name: 'messaging', - props: () => ({ - design: 0 - }) -}).extend({ - methods: { - navigate(user) { - (this as any).os.new(MkMessagingRoomWindow, { - user: user - }); - }, - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-messaging - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 2 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > .mk-messaging - max-height 250px - overflow auto - -</style> diff --git a/src/server/web/app/desktop/views/widgets/notifications.vue b/src/server/web/app/desktop/views/widgets/notifications.vue deleted file mode 100644 index 1a2b3d3f89..0000000000 --- a/src/server/web/app/desktop/views/widgets/notifications.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<div class="mkw-notifications"> - <template v-if="!props.compact"> - <p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p> - <button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button> - </template> - <mk-notifications/> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'notifications', - props: () => ({ - compact: false - }) -}).extend({ - methods: { - settings() { - alert('not implemented yet'); - }, - func() { - this.props.compact = !this.props.compact; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-notifications - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .mk-notifications - max-height 300px - overflow auto - -</style> diff --git a/src/server/web/app/desktop/views/widgets/polls.vue b/src/server/web/app/desktop/views/widgets/polls.vue deleted file mode 100644 index e5db34fc7a..0000000000 --- a/src/server/web/app/desktop/views/widgets/polls.vue +++ /dev/null @@ -1,129 +0,0 @@ -<template> -<div class="mkw-polls"> - <template v-if="!props.compact"> - <p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p> - <button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button> - </template> - <div class="poll" v-if="!fetching && poll != null"> - <p v-if="poll.text"><router-link to="`/@${ acct }/${ poll.id }`">{{ poll.text }}</router-link></p> - <p v-if="!poll.text"><router-link to="`/@${ acct }/${ poll.id }`">%fa:link%</router-link></p> - <mk-poll :post="poll"/> - </div> - <p class="empty" v-if="!fetching && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import getAcct from '../../../../../common/user/get-acct'; - -export default define({ - name: 'polls', - props: () => ({ - compact: false - }) -}).extend({ - computed: { - acct() { - return getAcct(this.poll.user); - }, - }, - data() { - return { - poll: null, - fetching: true, - offset: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - }, - fetch() { - this.fetching = true; - this.poll = null; - - (this as any).api('posts/polls/recommendation', { - limit: 1, - offset: this.offset - }).then(posts => { - const poll = posts ? posts[0] : null; - if (poll == null) { - this.offset = 0; - } else { - this.offset++; - } - this.poll = poll; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-polls - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .poll - padding 16px - font-size 12px - color #555 - - > p - margin 0 0 8px 0 - - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/widgets/post-form.vue b/src/server/web/app/desktop/views/widgets/post-form.vue deleted file mode 100644 index cf7fd1f2b2..0000000000 --- a/src/server/web/app/desktop/views/widgets/post-form.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<div class="mkw-post-form"> - <template v-if="props.design == 0"> - <p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p> - </template> - <textarea :disabled="posting" v-model="text" @keydown="onKeydown" placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea> - <button @click="post" :disabled="posting">%i18n:desktop.tags.mk-post-form-home-widget.post%</button> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'post-form', - props: () => ({ - design: 0 - }) -}).extend({ - data() { - return { - posting: false, - text: '' - }; - }, - methods: { - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - }, - onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); - }, - post() { - this.posting = true; - - (this as any).api('posts/create', { - text: this.text - }).then(data => { - this.clear(); - }).catch(err => { - alert('失敗した'); - }).then(() => { - this.posting = false; - }); - }, - clear() { - this.text = ''; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mkw-post-form - background #fff - overflow hidden - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > textarea - display block - width 100% - max-width 100% - min-width 100% - padding 16px - margin-bottom 28px + 16px - border none - border-bottom solid 1px #eee - - > button - display block - position absolute - bottom 8px - right 8px - margin 0 - padding 0 10px - height 28px - color $theme-color-foreground - background $theme-color !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - &:hover - background lighten($theme-color, 10%) !important - - &:active - background darken($theme-color, 10%) !important - transition background 0s ease - -</style> diff --git a/src/server/web/app/desktop/views/widgets/profile.vue b/src/server/web/app/desktop/views/widgets/profile.vue deleted file mode 100644 index 83cd67b50c..0000000000 --- a/src/server/web/app/desktop/views/widgets/profile.vue +++ /dev/null @@ -1,125 +0,0 @@ -<template> -<div class="mkw-profile" - :data-compact="props.design == 1 || props.design == 2" - :data-melt="props.design == 2" -> - <div class="banner" - :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''" - title="クリックでバナー編集" - @click="os.apis.updateBanner" - ></div> - <img class="avatar" - :src="`${os.i.avatarUrl}?thumbnail&size=96`" - @click="os.apis.updateAvatar" - alt="avatar" - title="クリックでアバター編集" - v-user-preview="os.i.id" - /> - <router-link class="name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link> - <p class="username">@{{ os.i.username }}</p> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'profile', - props: () => ({ - design: 0 - }) -}).extend({ - methods: { - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-profile - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &[data-compact] - > .banner:before - content "" - display block - width 100% - height 100% - background rgba(0, 0, 0, 0.5) - - > .avatar - top ((100px - 58px) / 2) - left ((100px - 58px) / 2) - border none - border-radius 100% - box-shadow 0 0 16px rgba(0, 0, 0, 0.5) - - > .name - position absolute - top 0 - left 92px - margin 0 - line-height 100px - color #fff - text-shadow 0 0 8px rgba(0, 0, 0, 0.5) - - > .username - display none - - &[data-melt] - background transparent !important - border none !important - - > .banner - visibility hidden - - > .avatar - box-shadow none - - > .name - color #666 - text-shadow none - - > .banner - height 100px - background-color #f5f5f5 - background-size cover - background-position center - cursor pointer - - > .avatar - display block - position absolute - top 76px - left 16px - width 58px - height 58px - margin 0 - border solid 3px #fff - border-radius 8px - vertical-align bottom - cursor pointer - - > .name - display block - margin 10px 0 0 84px - line-height 16px - font-weight bold - color #555 - - > .username - display block - margin 4px 0 8px 84px - line-height 16px - font-size 0.9em - color #999 - -</style> diff --git a/src/server/web/app/desktop/views/widgets/timemachine.vue b/src/server/web/app/desktop/views/widgets/timemachine.vue deleted file mode 100644 index 6db3b14c62..0000000000 --- a/src/server/web/app/desktop/views/widgets/timemachine.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<div class="mkw-timemachine"> - <mk-calendar :design="props.design" @chosen="chosen"/> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'timemachine', - props: () => ({ - design: 0 - }) -}).extend({ - methods: { - chosen(date) { - this.$emit('chosen', date); - }, - func() { - if (this.props.design == 5) { - this.props.design = 0; - } else { - this.props.design++; - } - } - } -}); -</script> diff --git a/src/server/web/app/desktop/views/widgets/trends.vue b/src/server/web/app/desktop/views/widgets/trends.vue deleted file mode 100644 index 77779787ee..0000000000 --- a/src/server/web/app/desktop/views/widgets/trends.vue +++ /dev/null @@ -1,135 +0,0 @@ -<template> -<div class="mkw-trends"> - <template v-if="!props.compact"> - <p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p> - <button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button> - </template> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <div class="post" v-else-if="post != null"> - <p class="text"><router-link :to="`/@${ acct }/${ post.id }`">{{ post.text }}</router-link></p> - <p class="author">―<router-link :to="`/@${ acct }`">@{{ acct }}</router-link></p> - </div> - <p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import getAcct from '../../../../../common/user/get-acct'; - -export default define({ - name: 'trends', - props: () => ({ - compact: false - }) -}).extend({ - computed: { - acct() { - return getAcct(this.post.user); - }, - }, - data() { - return { - post: null, - fetching: true, - offset: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - }, - fetch() { - this.fetching = true; - this.post = null; - - (this as any).api('posts/trend', { - limit: 1, - offset: this.offset, - repost: false, - reply: false, - media: false, - poll: false - }).then(posts => { - const post = posts ? posts[0] : null; - if (post == null) { - this.offset = 0; - } else { - this.offset++; - } - this.post = post; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-trends - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .post - padding 16px - font-size 12px - font-style oblique - color #555 - - > p - margin 0 - - > .text, - > .author - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/desktop/views/widgets/users.vue b/src/server/web/app/desktop/views/widgets/users.vue deleted file mode 100644 index 7b89441126..0000000000 --- a/src/server/web/app/desktop/views/widgets/users.vue +++ /dev/null @@ -1,172 +0,0 @@ -<template> -<div class="mkw-users"> - <template v-if="!props.compact"> - <p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p> - <button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button> - </template> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <template v-else-if="users.length != 0"> - <div class="user" v-for="_user in users"> - <router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`"> - <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/> - </router-link> - <div class="body"> - <router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link> - <p class="username">@{{ getAcct(_user) }}</p> - </div> - <mk-follow-button :user="_user"/> - </div> - </template> - <p class="empty" v-else>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import getAcct from '../../../../../common/user/get-acct'; - -const limit = 3; - -export default define({ - name: 'users', - props: () => ({ - compact: false - }) -}).extend({ - data() { - return { - users: [], - fetching: true, - page: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - getAcct, - func() { - this.props.compact = !this.props.compact; - }, - fetch() { - this.fetching = true; - this.users = []; - - (this as any).api('users/recommendation', { - limit: limit, - offset: limit * this.page - }).then(users => { - this.users = users; - this.fetching = false; - }); - }, - refresh() { - if (this.users.length < limit) { - this.page = 0; - } else { - this.page++; - } - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-users - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .user - padding 16px - border-bottom solid 1px #eee - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color #555 - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color #ccc - - > .mk-follow-button - position absolute - top 16px - right 16px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/dev/script.ts b/src/server/web/app/dev/script.ts deleted file mode 100644 index c043813b40..0000000000 --- a/src/server/web/app/dev/script.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Developer Center - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import BootstrapVue from 'bootstrap-vue'; -import 'bootstrap/dist/css/bootstrap.css'; -import 'bootstrap-vue/dist/bootstrap-vue.css'; - -// Style -import './style.styl'; - -import init from '../init'; - -import Index from './views/index.vue'; -import Apps from './views/apps.vue'; -import AppNew from './views/new-app.vue'; -import App from './views/app.vue'; -import ui from './views/ui.vue'; - -Vue.use(BootstrapVue); - -Vue.component('mk-ui', ui); - -/** - * init - */ -init(launch => { - // Init router - const router = new VueRouter({ - mode: 'history', - base: '/dev/', - routes: [ - { path: '/', component: Index }, - { path: '/apps', component: Apps }, - { path: '/app/new', component: AppNew }, - { path: '/app/:id', component: App }, - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/server/web/app/dev/style.styl b/src/server/web/app/dev/style.styl deleted file mode 100644 index e635897b17..0000000000 --- a/src/server/web/app/dev/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -// Bootstrapのデザインを崩すので: -* - position initial - background-clip initial !important - -html - background-color #fff diff --git a/src/server/web/app/dev/views/app.vue b/src/server/web/app/dev/views/app.vue deleted file mode 100644 index a35b032b73..0000000000 --- a/src/server/web/app/dev/views/app.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<mk-ui> - <p v-if="fetching">読み込み中</p> - <b-card v-if="!fetching" :header="app.name"> - <b-form-group label="App Secret"> - <b-input :value="app.secret" readonly/> - </b-form-group> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - app: null - }; - }, - watch: { - $route: 'fetch' - }, - mounted() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - (this as any).api('app/show', { - appId: this.$route.params.id - }).then(app => { - this.app = app; - this.fetching = false; - }); - } - } -}); -</script> diff --git a/src/server/web/app/dev/views/apps.vue b/src/server/web/app/dev/views/apps.vue deleted file mode 100644 index 7e0b107a30..0000000000 --- a/src/server/web/app/dev/views/apps.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<mk-ui> - <b-card header="アプリを管理"> - <b-button to="/app/new" variant="primary">アプリ作成</b-button> - <hr> - <div class="apps"> - <p v-if="fetching">読み込み中</p> - <template v-if="!fetching"> - <b-alert v-if="apps.length == 0">アプリなし</b-alert> - <b-list-group v-else> - <b-list-group-item v-for="app in apps" :key="app.id" :to="`/app/${app.id}`"> - {{ app.name }} - </b-list-group-item> - </b-list-group> - </template> - </div> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - apps: [] - }; - }, - mounted() { - (this as any).api('my/apps').then(apps => { - this.apps = apps; - this.fetching = false; - }); - } -}); -</script> diff --git a/src/server/web/app/dev/views/index.vue b/src/server/web/app/dev/views/index.vue deleted file mode 100644 index 3f572b3907..0000000000 --- a/src/server/web/app/dev/views/index.vue +++ /dev/null @@ -1,10 +0,0 @@ -<template> -<mk-ui> - <b-button to="/apps" variant="primary">アプリの管理</b-button> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend(); -</script> diff --git a/src/server/web/app/dev/views/new-app.vue b/src/server/web/app/dev/views/new-app.vue deleted file mode 100644 index e407ca00d7..0000000000 --- a/src/server/web/app/dev/views/new-app.vue +++ /dev/null @@ -1,105 +0,0 @@ -<template> -<mk-ui> - <b-card header="アプリケーションの作成"> - <b-form @submit.prevent="onSubmit" autocomplete="off"> - <b-form-group label="アプリケーション名" description="あなたのアプリの名称。"> - <b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/> - </b-form-group> - <b-form-group label="ID" description="あなたのアプリのID。"> - <b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/> - <p class="info" v-if="nidState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%確認しています...</p> - <p class="info" v-if="nidState == 'ok'" style="color:#3CB7B5">%fa:fw check%利用できます</p> - <p class="info" v-if="nidState == 'unavailable'" style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p> - <p class="info" v-if="nidState == 'error'" style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p> - <p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、_が使えます</p> - <p class="info" v-if="nidState == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%3文字以上でお願いします!</p> - <p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p> - </b-form-group> - <b-form-group label="アプリの概要" description="あなたのアプリの簡単な説明や紹介。"> - <b-textarea v-model="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required></b-textarea> - </b-form-group> - <b-form-group label="コールバックURL (オプション)" description="ユーザーが認証フォームで認証した際にリダイレクトするURLを設定できます。"> - <b-input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/> - </b-form-group> - <b-card header="権限"> - <b-form-group description="ここで要求した機能だけがAPIからアクセスできます。"> - <b-alert show variant="warning">%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</b-alert> - <b-form-checkbox-group v-model="permission" stacked> - <b-form-checkbox value="account-read">アカウントの情報を見る。</b-form-checkbox> - <b-form-checkbox value="account-write">アカウントの情報を操作する。</b-form-checkbox> - <b-form-checkbox value="post-write">投稿する。</b-form-checkbox> - <b-form-checkbox value="reaction-write">リアクションしたりリアクションをキャンセルする。</b-form-checkbox> - <b-form-checkbox value="following-write">フォローしたりフォロー解除する。</b-form-checkbox> - <b-form-checkbox value="drive-read">ドライブを見る。</b-form-checkbox> - <b-form-checkbox value="drive-write">ドライブを操作する。</b-form-checkbox> - <b-form-checkbox value="notification-read">通知を見る。</b-form-checkbox> - <b-form-checkbox value="notification-write">通知を操作する。</b-form-checkbox> - </b-form-checkbox-group> - </b-form-group> - </b-card> - <hr> - <b-button type="submit" variant="primary">アプリ作成</b-button> - </b-form> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - name: '', - nid: '', - description: '', - cb: '', - nidState: null, - permission: [] - }; - }, - watch: { - nid() { - if (this.nid == null || this.nid == '') { - this.nidState = null; - return; - } - - const err = - !this.nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' : - this.nid.length < 3 ? 'min-range' : - this.nid.length > 30 ? 'max-range' : - null; - - if (err) { - this.nidState = err; - return; - } - - this.nidState = 'wait'; - - (this as any).api('app/nameId/available', { - nameId: this.nid - }).then(result => { - this.nidState = result.available ? 'ok' : 'unavailable'; - }).catch(err => { - this.nidState = 'error'; - }); - } - }, - methods: { - onSubmit() { - (this as any).api('app/create', { - name: this.name, - nameId: this.nid, - description: this.description, - callbackUrl: this.cb, - permission: this.permission - }).then(() => { - location.href = '/apps'; - }).catch(() => { - alert('アプリの作成に失敗しました。再度お試しください。'); - }); - } - } -}); -</script> diff --git a/src/server/web/app/dev/views/ui.vue b/src/server/web/app/dev/views/ui.vue deleted file mode 100644 index 4a0fcee635..0000000000 --- a/src/server/web/app/dev/views/ui.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> -<div> - <b-navbar toggleable="md" type="dark" variant="info"> - <b-navbar-brand>Misskey Developers</b-navbar-brand> - <b-navbar-nav> - <b-nav-item to="/">Home</b-nav-item> - <b-nav-item to="/apps">Apps</b-nav-item> - </b-navbar-nav> - </b-navbar> - <main> - <slot></slot> - </main> -</div> -</template> - -<style lang="stylus" scoped> -main - padding 32px - max-width 700px -</style> diff --git a/src/server/web/app/init.css b/src/server/web/app/init.css deleted file mode 100644 index 2587f63943..0000000000 --- a/src/server/web/app/init.css +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Boot screen style - */ - -@charset 'utf-8'; - -html { - font-family: sans-serif; -} - -body > noscript { - position: fixed; - z-index: 2; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - background: #fff; -} - body > noscript > p { - display: block; - margin: 32px; - font-size: 2em; - color: #555; - } - -#ini { - position: fixed; - z-index: 1; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - background: #fff; - cursor: wait; -} - #ini > p { - display: block; - user-select: none; - margin: 32px; - font-size: 4em; - color: #555; - } - #ini > p > span { - animation: ini 1.4s infinite ease-in-out both; - } - #ini > p > span:nth-child(1) { - animation-delay: 0s; - } - #ini > p > span:nth-child(2) { - animation-delay: 0.16s; - } - #ini > p > span:nth-child(3) { - animation-delay: 0.32s; - } - -@keyframes ini { - 0%, 80%, 100% { - opacity: 1; - } - 40% { - opacity: 0; - } -} diff --git a/src/server/web/app/init.ts b/src/server/web/app/init.ts deleted file mode 100644 index 3e5c38961f..0000000000 --- a/src/server/web/app/init.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * App initializer - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import VModal from 'vue-js-modal'; -import * as TreeView from 'vue-json-tree-view'; -import VAnimateCss from 'v-animate-css'; -import Element from 'element-ui'; -import ElementLocaleEn from 'element-ui/lib/locale/lang/en'; -import ElementLocaleJa from 'element-ui/lib/locale/lang/ja'; - -import App from './app.vue'; -import checkForUpdate from './common/scripts/check-for-update'; -import MiOS, { API } from './common/mios'; -import { version, codename, hostname, lang } from './config'; - -let elementLocale; -switch (lang) { - case 'ja': elementLocale = ElementLocaleJa; break; - case 'en': elementLocale = ElementLocaleEn; break; - default: elementLocale = ElementLocaleEn; break; -} - -Vue.use(VueRouter); -Vue.use(VModal); -Vue.use(TreeView); -Vue.use(VAnimateCss); -Vue.use(Element, { locale: elementLocale }); - -// Register global directives -require('./common/views/directives'); - -// Register global components -require('./common/views/components'); -require('./common/views/widgets'); - -// Register global filters -require('./common/views/filters'); - -Vue.mixin({ - destroyed(this: any) { - if (this.$el.parentNode) { - this.$el.parentNode.removeChild(this.$el); - } - } -}); - -/** - * APP ENTRY POINT! - */ - -console.info(`Misskey v${version} (${codename})`); -console.info( - '%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。', - 'color: red; background: yellow; font-size: 16px; font-weight: bold;'); - -// BootTimer解除 -window.clearTimeout((window as any).mkBootTimer); -delete (window as any).mkBootTimer; - -if (hostname != 'localhost') { - document.domain = hostname; -} - -//#region Set lang attr -const html = document.documentElement; -html.setAttribute('lang', lang); -//#endregion - -//#region Set description meta tag -const head = document.getElementsByTagName('head')[0]; -const meta = document.createElement('meta'); -meta.setAttribute('name', 'description'); -meta.setAttribute('content', '%i18n:common.misskey%'); -head.appendChild(meta); -//#endregion - -// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする -try { - localStorage.setItem('kyoppie', 'yuppie'); -} catch (e) { - Storage.prototype.setItem = () => { }; // noop -} - -// クライアントを更新すべきならする -if (localStorage.getItem('should-refresh') == 'true') { - localStorage.removeItem('should-refresh'); - location.reload(true); -} - -// MiOSを初期化してコールバックする -export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => { - const os = new MiOS(sw); - - os.init(() => { - // アプリ基底要素マウント - document.body.innerHTML = '<div id="app"></div>'; - - const launch = (router: VueRouter, api?: (os: MiOS) => API) => { - os.apis = api ? api(os) : null; - - Vue.mixin({ - data() { - return { - os, - api: os.api, - apis: os.apis - }; - } - }); - - const app = new Vue({ - router, - created() { - this.$watch('os.i', i => { - // キャッシュ更新 - localStorage.setItem('me', JSON.stringify(i)); - }, { - deep: true - }); - }, - render: createEl => createEl(App) - }); - - os.app = app; - - // マウント - app.$mount('#app'); - - return [app, os] as [Vue, MiOS]; - }; - - try { - callback(launch); - } catch (e) { - panic(e); - } - - //#region 更新チェック - const preventUpdate = localStorage.getItem('preventUpdate') == 'true'; - if (!preventUpdate) { - setTimeout(() => { - checkForUpdate(os); - }, 3000); - } - //#endregion - }); -}; - -// BSoD -function panic(e) { - console.error(e); - - // Display blue screen - document.documentElement.style.background = '#1269e2'; - document.body.innerHTML = - '<div id="error">' - + '<h1>:( 致命的な問題が発生しました。</h1>' - + '<p>お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。</p>' - + '<hr>' - + `<p>エラーコード: ${e.toString()}</p>` - + `<p>ブラウザ バージョン: ${navigator.userAgent}</p>` - + `<p>クライアント バージョン: ${version}</p>` - + '<hr>' - + '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>' - + '<p>Thank you for using Misskey.</p>' - + '</div>'; - - // TODO: Report the bug -} diff --git a/src/server/web/app/mobile/api/choose-drive-file.ts b/src/server/web/app/mobile/api/choose-drive-file.ts deleted file mode 100644 index b1a78f2364..0000000000 --- a/src/server/web/app/mobile/api/choose-drive-file.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Chooser from '../views/components/drive-file-chooser.vue'; - -export default function(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const w = new Chooser({ - propsData: { - title: o.title, - multiple: o.multiple, - initFolder: o.currentFolder - } - }).$mount(); - w.$once('selected', file => { - res(file); - }); - document.body.appendChild(w.$el); - }); -} diff --git a/src/server/web/app/mobile/api/choose-drive-folder.ts b/src/server/web/app/mobile/api/choose-drive-folder.ts deleted file mode 100644 index d1f97d1487..0000000000 --- a/src/server/web/app/mobile/api/choose-drive-folder.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Chooser from '../views/components/drive-folder-chooser.vue'; - -export default function(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const w = new Chooser({ - propsData: { - title: o.title, - initFolder: o.currentFolder - } - }).$mount(); - w.$once('selected', folder => { - res(folder); - }); - document.body.appendChild(w.$el); - }); -} diff --git a/src/server/web/app/mobile/api/dialog.ts b/src/server/web/app/mobile/api/dialog.ts deleted file mode 100644 index a2378767be..0000000000 --- a/src/server/web/app/mobile/api/dialog.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function(opts) { - return new Promise<string>((res, rej) => { - alert('dialog not implemented yet'); - }); -} diff --git a/src/server/web/app/mobile/api/input.ts b/src/server/web/app/mobile/api/input.ts deleted file mode 100644 index 38d0fb61eb..0000000000 --- a/src/server/web/app/mobile/api/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function(opts) { - return new Promise<string>((res, rej) => { - const x = window.prompt(opts.title); - if (x) { - res(x); - } - }); -} diff --git a/src/server/web/app/mobile/api/notify.ts b/src/server/web/app/mobile/api/notify.ts deleted file mode 100644 index 82780d196f..0000000000 --- a/src/server/web/app/mobile/api/notify.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function(message) { - alert(message); -} diff --git a/src/server/web/app/mobile/api/post.ts b/src/server/web/app/mobile/api/post.ts deleted file mode 100644 index 841103fee1..0000000000 --- a/src/server/web/app/mobile/api/post.ts +++ /dev/null @@ -1,43 +0,0 @@ -import PostForm from '../views/components/post-form.vue'; -//import RepostForm from '../views/components/repost-form.vue'; -import getPostSummary from '../../../../common/get-post-summary'; - -export default (os) => (opts) => { - const o = opts || {}; - - if (o.repost) { - /*const vm = new RepostForm({ - propsData: { - repost: o.repost - } - }).$mount(); - vm.$once('cancel', recover); - vm.$once('post', recover); - document.body.appendChild(vm.$el);*/ - - const text = window.prompt(`「${getPostSummary(o.repost)}」をRepost`); - if (text == null) return; - os.api('posts/create', { - repostId: o.repost.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('post', recover); - document.body.appendChild(vm.$el); - (vm as any).focus(); - } -}; diff --git a/src/server/web/app/mobile/script.ts b/src/server/web/app/mobile/script.ts deleted file mode 100644 index 4776fccddb..0000000000 --- a/src/server/web/app/mobile/script.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Mobile Client - */ - -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; -import '../../element.scss'; - -import init from '../init'; - -import chooseDriveFolder from './api/choose-drive-folder'; -import chooseDriveFile from './api/choose-drive-file'; -import dialog from './api/dialog'; -import input from './api/input'; -import post from './api/post'; -import notify from './api/notify'; - -import MkIndex from './views/pages/index.vue'; -import MkSignup from './views/pages/signup.vue'; -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 MkMessaging from './views/pages/messaging.vue'; -import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkPost from './views/pages/post.vue'; -import MkSearch from './views/pages/search.vue'; -import MkFollowers from './views/pages/followers.vue'; -import MkFollowing from './views/pages/following.vue'; -import MkSettings from './views/pages/settings.vue'; -import MkProfileSetting from './views/pages/profile-setting.vue'; -import MkOthello from './views/pages/othello.vue'; - -/** - * init - */ -init((launch) => { - // Register directives - require('./views/directives'); - - // Register components - require('./views/components'); - require('./views/widgets'); - - // http://qiita.com/junya/items/3ff380878f26ca447f85 - document.body.setAttribute('ontouchstart', ''); - - // Init router - const router = new VueRouter({ - mode: 'history', - routes: [ - { path: '/', name: 'index', component: MkIndex }, - { path: '/signup', name: 'signup', component: MkSignup }, - { path: '/i/settings', component: MkSettings }, - { path: '/i/settings/profile', component: MkProfileSetting }, - { path: '/i/notifications', component: MkNotifications }, - { path: '/i/messaging', component: MkMessaging }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/i/drive/file/:file', component: MkDrive }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/search', component: MkSearch }, - { path: '/othello', component: MkOthello }, - { path: '/othello/:game', component: MkOthello }, - { path: '/@:user', component: MkUser }, - { path: '/@:user/followers', component: MkFollowers }, - { path: '/@:user/following', component: MkFollowing }, - { path: '/@:user/:post', component: MkPost } - ] - }); - - // Launch the app - launch(router, os => ({ - chooseDriveFolder, - chooseDriveFile, - dialog, - input, - post: post(os), - notify - })); -}, true); diff --git a/src/server/web/app/mobile/style.styl b/src/server/web/app/mobile/style.styl deleted file mode 100644 index 81912a2483..0000000000 --- a/src/server/web/app/mobile/style.styl +++ /dev/null @@ -1,15 +0,0 @@ -@import "../app" -@import "../reset" - -#wait - top auto - bottom 15px - left 15px - -html - height 100% - -body - display flex - flex-direction column - min-height 100% diff --git a/src/server/web/app/mobile/views/components/activity.vue b/src/server/web/app/mobile/views/components/activity.vue deleted file mode 100644 index 2e44017e77..0000000000 --- a/src/server/web/app/mobile/views/components/activity.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="mk-activity"> - <svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none"> - <g v-for="(d, i) in data"> - <rect width="0.8" :height="d.postsH" - :x="i + 0.1" :y="1 - d.postsH - d.repliesH - d.repostsH" - fill="#41ddde"/> - <rect width="0.8" :height="d.repliesH" - :x="i + 0.1" :y="1 - d.repliesH - d.repostsH" - fill="#f7796c"/> - <rect width="0.8" :height="d.repostsH" - :x="i + 0.1" :y="1 - d.repostsH" - fill="#a1de41"/> - </g> - </svg> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - data() { - return { - fetching: true, - data: [], - peak: null - }; - }, - mounted() { - (this as any).api('aggregation/users/activity', { - userId: this.user.id, - limit: 30 - }).then(data => { - data.forEach(d => d.total = d.posts + d.replies + d.reposts); - this.peak = Math.max.apply(null, data.map(d => d.total)); - data.forEach(d => { - d.postsH = d.posts / this.peak; - d.repliesH = d.replies / this.peak; - d.repostsH = d.reposts / this.peak; - }); - data.reverse(); - this.data = data; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-activity - max-width 600px - margin 0 auto - - > svg - display block - width 100% - height 80px - - > rect - transform-origin center - -</style> diff --git a/src/server/web/app/mobile/views/components/drive-file-chooser.vue b/src/server/web/app/mobile/views/components/drive-file-chooser.vue deleted file mode 100644 index 6806af0f1e..0000000000 --- a/src/server/web/app/mobile/views/components/drive-file-chooser.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<div class="mk-drive-file-chooser"> - <div class="body"> - <header> - <h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="close" @click="cancel">%fa:times%</button> - <button v-if="multiple" class="ok" @click="ok">%fa:check%</button> - </header> - <mk-drive ref="browser" - :select-file="true" - :multiple="multiple" - @change-selection="onChangeSelection" - @selected="onSelected" - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['multiple'], - data() { - return { - files: [] - }; - }, - methods: { - onChangeSelection(files) { - this.files = files; - }, - onSelected(file) { - this.$emit('selected', file); - this.$destroy(); - }, - cancel() { - this.$emit('canceled'); - this.$destroy(); - }, - ok() { - this.$emit('selected', this.files); - this.$destroy(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive-file-chooser - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(0, 0, 0, 0.2) - - > .body - width 100% - height 100% - background #fff - - > header - border-bottom solid 1px #eee - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/server/web/app/mobile/views/components/drive-folder-chooser.vue b/src/server/web/app/mobile/views/components/drive-folder-chooser.vue deleted file mode 100644 index 853078664f..0000000000 --- a/src/server/web/app/mobile/views/components/drive-folder-chooser.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> -<div class="mk-drive-folder-chooser"> - <div class="body"> - <header> - <h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1> - <button class="close" @click="cancel">%fa:times%</button> - <button class="ok" @click="ok">%fa:check%</button> - </header> - <mk-drive ref="browser" - select-folder - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - methods: { - cancel() { - this.$emit('canceled'); - this.$destroy(); - }, - ok() { - this.$emit('selected', (this.$refs.browser as any).folder); - this.$destroy(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive-folder-chooser - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(0, 0, 0, 0.2) - - > .body - width 100% - height 100% - background #fff - - > header - border-bottom solid 1px #eee - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/server/web/app/mobile/views/components/drive.file-detail.vue b/src/server/web/app/mobile/views/components/drive.file-detail.vue deleted file mode 100644 index f3274f677f..0000000000 --- a/src/server/web/app/mobile/views/components/drive.file-detail.vue +++ /dev/null @@ -1,295 +0,0 @@ -<template> -<div class="file-detail"> - <div class="preview"> - <img v-if="kind == 'image'" ref="img" - :src="file.url" - :alt="file.name" - :title="file.name" - @load="onImageLoaded" - :style="style"> - <template v-if="kind != 'image'">%fa:file%</template> - <footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height"> - <span class="size"> - <span class="width">{{ file.properties.width }}</span> - <span class="time">×</span> - <span class="height">{{ file.properties.height }}</span> - <span class="px">px</span> - </span> - <span class="separator"></span> - <span class="aspect-ratio"> - <span class="width">{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}</span> - <span class="colon">:</span> - <span class="height">{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}</span> - </span> - </footer> - </div> - <div class="info"> - <div> - <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> - <span class="separator"></span> - <span class="data-size">{{ file.datasize | bytes }}</span> - <span class="separator"></span> - <span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time :time="file.createdAt"/></span> - </div> - </div> - <div class="menu"> - <div> - <a :href="`${file.url}?download`" :download="file.name"> - %fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download% - </a> - <button @click="rename"> - %fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename% - </button> - <button @click="move"> - %fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move% - </button> - </div> - </div> - <div class="exif" v-show="exif"> - <div> - <p> - %fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif% - </p> - <pre ref="exif" class="json">{{ exif ? JSON.stringify(exif, null, 2) : '' }}</pre> - </div> - </div> - <div class="hash"> - <div> - <p> - %fa:hashtag%%i18n:mobile.tags.mk-drive-file-viewer.hash% - </p> - <code>{{ file.md5 }}</code> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as EXIF from 'exif-js'; -import * as hljs from 'highlight.js'; -import gcd from '../../../common/scripts/gcd'; - -export default Vue.extend({ - props: ['file'], - data() { - return { - gcd, - exif: null - }; - }, - computed: { - browser(): any { - return this.$parent; - }, - kind(): string { - return this.file.type.split('/')[0]; - }, - style(): any { - return this.file.properties.avgColor ? { - 'background-color': `rgb(${ this.file.properties.avgColor.join(',') })` - } : {}; - } - }, - methods: { - rename() { - const name = window.prompt('名前を変更', this.file.name); - if (name == null || name == '' || name == this.file.name) return; - (this as any).api('drive/files/update', { - fileId: this.file.id, - name: name - }).then(() => { - this.browser.cf(this.file, true); - }); - }, - move() { - (this as any).apis.chooseDriveFolder().then(folder => { - (this as any).api('drive/files/update', { - fileId: this.file.id, - folderId: folder == null ? null : folder.id - }).then(() => { - this.browser.cf(this.file, true); - }); - }); - }, - showCreatedAt() { - alert(new Date(this.file.createdAt).toLocaleString()); - }, - onImageLoaded() { - const self = this; - EXIF.getData(this.$refs.img, function(this: any) { - const allMetaData = EXIF.getAllTags(this); - self.exif = allMetaData; - hljs.highlightBlock(self.$refs.exif); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.file-detail - - > .preview - padding 8px - background #f0f0f0 - - > img - display block - max-width 100% - max-height 300px - margin 0 auto - box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2) - - > footer - padding 8px 8px 0 8px - font-size 0.8em - color #888 - text-align center - - > .separator - display inline - padding 0 4px - - > .size - display inline - - .time - margin 0 2px - - .px - margin-left 4px - - > .aspect-ratio - display inline - opacity 0.7 - - &:before - content "(" - - &:after - content ")" - - > .info - padding 14px - font-size 0.8em - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > .separator - padding 0 4px - color #cdcdcd - - > .type - > .data-size - color #9d9d9d - - > mk-file-type-icon - margin-right 4px - - > .created-at - color #bdbdbd - - > [data-fa] - margin-right 2px - - > .menu - padding 14px - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > * - display block - width 100% - padding 10px 16px - margin 0 0 12px 0 - color #333 - font-size 0.9em - text-align center - text-decoration none - text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) - background-image linear-gradient(#fafafa, #eaeaea) - border 1px solid #ddd - border-bottom-color #cecece - border-radius 3px - - &:last-child - margin-bottom 0 - - &:active - background-color #767676 - background-image none - border-color #444 - box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) - - > [data-fa] - margin-right 4px - - > .hash - padding 14px - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > p - display block - margin 0 - padding 0 - color #555 - font-size 0.9em - - > [data-fa] - margin-right 4px - - > code - display block - width 100% - margin 6px 0 0 0 - padding 8px - white-space nowrap - overflow auto - font-size 0.8em - color #222 - border solid 1px #dfdfdf - border-radius 2px - background #f5f5f5 - - > .exif - padding 14px - border-top solid 1px #dfdfdf - - > div - max-width 500px - margin 0 auto - - > p - display block - margin 0 - padding 0 - color #555 - font-size 0.9em - - > [data-fa] - margin-right 4px - - > pre - display block - width 100% - margin 6px 0 0 0 - padding 8px - height 128px - overflow auto - font-size 0.9em - border solid 1px #dfdfdf - border-radius 2px - background #f5f5f5 - -</style> diff --git a/src/server/web/app/mobile/views/components/drive.file.vue b/src/server/web/app/mobile/views/components/drive.file.vue deleted file mode 100644 index 7d1957042b..0000000000 --- a/src/server/web/app/mobile/views/components/drive.file.vue +++ /dev/null @@ -1,171 +0,0 @@ -<template> -<a class="file" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> - <div class="container"> - <div class="thumbnail" :style="thumbnail"></div> - <div class="body"> - <p class="name"> - <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> - <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> - </p> - <!-- - if file.tags.length > 0 - ul.tags - each tag in file.tags - li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name - --> - <footer> - <p class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</p> - <p class="separator"></p> - <p class="data-size">{{ file.datasize | bytes }}</p> - <p class="separator"></p> - <p class="created-at"> - %fa:R clock%<mk-time :time="file.createdAt"/> - </p> - </footer> - </div> - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['file'], - data() { - return { - isSelected: false - }; - }, - computed: { - browser(): any { - return this.$parent; - }, - thumbnail(): any { - return { - 'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${this.file.url}?thumbnail&size=128)` - }; - } - }, - created() { - this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id) - - this.browser.$on('change-selection', this.onBrowserChangeSelection); - }, - beforeDestroy() { - this.browser.$off('change-selection', this.onBrowserChangeSelection); - }, - methods: { - onBrowserChangeSelection(selections) { - this.isSelected = selections.some(f => f.id == this.file.id); - }, - onClick() { - this.browser.chooseFile(this.file); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.file - display block - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - max-width 500px - margin 0 auto - padding 16px - - &:after - content "" - display block - clear both - - > .thumbnail - display block - float left - width 64px - height 64px - background-size cover - background-position center center - - > .body - display block - float left - width calc(100% - 74px) - margin-left 10px - - > .name - display block - margin 0 - padding 0 - font-size 0.9em - font-weight bold - color #555 - text-overflow ellipsis - overflow-wrap break-word - - > .ext - opacity 0.5 - - > .tags - display block - margin 4px 0 0 0 - padding 0 - list-style none - font-size 0.5em - - > .tag - display inline-block - margin 0 5px 0 0 - padding 1px 5px - border-radius 2px - - > footer - display block - margin 4px 0 0 0 - font-size 0.7em - - > .separator - display inline - margin 0 - padding 0 4px - color #CDCDCD - - > .type - display inline - margin 0 - padding 0 - color #9D9D9D - - > .mk-file-type-icon - margin-right 4px - - > .data-size - display inline - margin 0 - padding 0 - color #9D9D9D - - > .created-at - display inline - margin 0 - padding 0 - color #BDBDBD - - > [data-fa] - margin-right 2px - - &[data-is-selected] - background $theme-color - - &, * - color #fff !important - -</style> diff --git a/src/server/web/app/mobile/views/components/drive.folder.vue b/src/server/web/app/mobile/views/components/drive.folder.vue deleted file mode 100644 index 22ff38fecb..0000000000 --- a/src/server/web/app/mobile/views/components/drive.folder.vue +++ /dev/null @@ -1,58 +0,0 @@ -<template> -<a class="root folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> - <div class="container"> - <p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right% - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['folder'], - computed: { - browser(): any { - return this.$parent; - } - }, - methods: { - onClick() { - this.browser.cd(this.folder); - } - } -}); -</script> - -<style lang="stylus" scoped> -.root.folder - display block - color #777 - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - max-width 500px - margin 0 auto - padding 16px - - > .name - display block - margin 0 - padding 0 - - > [data-fa] - margin-right 6px - - > [data-fa] - position absolute - top 0 - bottom 0 - right 20px - - > * - height 100% - -</style> diff --git a/src/server/web/app/mobile/views/components/drive.vue b/src/server/web/app/mobile/views/components/drive.vue deleted file mode 100644 index ff5366a0ad..0000000000 --- a/src/server/web/app/mobile/views/components/drive.vue +++ /dev/null @@ -1,581 +0,0 @@ -<template> -<div class="mk-drive"> - <nav ref="nav"> - <a @click.prevent="goRoot()" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a> - <template v-for="folder in hierarchyFolders"> - <span :key="folder.id + '>'">%fa:angle-right%</span> - <a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a> - </template> - <template v-if="folder != null"> - <span>%fa:angle-right%</span> - <p>{{ folder.name }}</p> - </template> - <template v-if="file != null"> - <span>%fa:angle-right%</span> - <p>{{ file.name }}</p> - </template> - </nav> - <mk-uploader ref="uploader"/> - <div class="browser" :class="{ fetching }" v-if="file == null"> - <div class="info" v-if="info"> - <p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% %i18n:mobile.tags.mk-drive.used%</p> - <p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)"> - <template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} %i18n:mobile.tags.mk-drive.folder-count%</template> - <template v-if="folder.foldersCount > 0 && folder.filesCount > 0">%i18n:mobile.tags.mk-drive.count-separator%</template> - <template v-if="folder.filesCount > 0">{{ folder.filesCount }} %i18n:mobile.tags.mk-drive.file-count%</template> - </p> - </div> - <div class="folders" v-if="folders.length > 0"> - <x-folder v-for="folder in folders" :key="folder.id" :folder="folder"/> - <p v-if="moreFolders">%i18n:mobile.tags.mk-drive.load-more%</p> - </div> - <div class="files" v-if="files.length > 0"> - <x-file v-for="file in files" :key="file.id" :file="file"/> - <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> - {{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }} - </button> - </div> - <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> - <p v-if="folder == null">%i18n:mobile.tags.mk-drive.nothing-in-drive%</p> - <p v-if="folder != null">%i18n:mobile.tags.mk-drive.folder-is-empty%</p> - </div> - </div> - <div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0"> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeLocalFile"/> - <x-file-detail v-if="file != null" :file="file"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XFolder from './drive.folder.vue'; -import XFile from './drive.file.vue'; -import XFileDetail from './drive.file-detail.vue'; - -export default Vue.extend({ - components: { - XFolder, - XFile, - XFileDetail - }, - props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'], - data() { - return { - /** - * 現在の階層(フォルダ) - * * null でルートを表す - */ - folder: null, - - file: null, - - files: [], - folders: [], - moreFiles: false, - moreFolders: false, - hierarchyFolders: [], - selectedFiles: [], - info: null, - connection: null, - connectionId: null, - - fetching: true, - fetchingMoreFiles: false, - fetchingMoreFolders: false - }; - }, - computed: { - isFileSelectMode(): boolean { - return this.selectFile; - } - }, - mounted() { - this.connection = (this as any).os.streams.driveStream.getConnection(); - this.connectionId = (this as any).os.streams.driveStream.use(); - - this.connection.on('file_created', this.onStreamDriveFileCreated); - this.connection.on('file_updated', this.onStreamDriveFileUpdated); - this.connection.on('folder_created', this.onStreamDriveFolderCreated); - this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); - - if (this.initFolder) { - this.cd(this.initFolder, true); - } else if (this.initFile) { - this.cf(this.initFile, true); - } else { - this.fetch(); - } - - if (this.isNaked) { - (this.$refs.nav as any).style.top = `${this.top}px`; - } - }, - beforeDestroy() { - this.connection.off('file_created', this.onStreamDriveFileCreated); - this.connection.off('file_updated', this.onStreamDriveFileUpdated); - this.connection.off('folder_created', this.onStreamDriveFolderCreated); - this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); - (this as any).os.streams.driveStream.dispose(this.connectionId); - }, - methods: { - onStreamDriveFileCreated(file) { - this.addFile(file, true); - }, - - onStreamDriveFileUpdated(file) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }, - - onStreamDriveFolderCreated(folder) { - this.addFolder(folder, true); - }, - - onStreamDriveFolderUpdated(folder) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }, - - dive(folder) { - this.hierarchyFolders.unshift(folder); - if (folder.parent) this.dive(folder.parent); - }, - - cd(target, silent = false) { - this.file = null; - - if (target == null) { - this.goRoot(silent); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.fetching = true; - - (this as any).api('drive/folders/show', { - folderId: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - if (folder.parent) this.dive(folder.parent); - - this.$emit('open-folder', this.folder, silent); - this.fetch(); - }); - }, - - addFolder(folder, unshift = false) { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断 - if (current != folder.parentId) return; - - // 追加しようとしているフォルダを既に所有してたら中断 - if (this.folders.some(f => f.id == folder.id)) return; - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - }, - - addFile(file, unshift = false) { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断 - if (current != file.folderId) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - Vue.set(this.files, exist, file); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - }, - - removeFolder(folder) { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - }, - - removeFile(file) { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - }, - - appendFile(file) { - this.addFile(file); - }, - appendFolder(folder) { - this.addFolder(folder); - }, - prependFile(file) { - this.addFile(file, true); - }, - prependFolder(folder) { - this.addFolder(folder, true); - }, - - goRoot(silent = false) { - if (this.folder || this.file) { - this.file = null; - this.folder = null; - this.hierarchyFolders = []; - this.$emit('move-root', silent); - this.fetch(); - } - }, - - fetch() { - this.folders = []; - this.files = []; - this.moreFolders = false; - this.moreFiles = false; - this.fetching = true; - - this.$emit('begin-fetch'); - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 20; - const filesMax = 20; - - // フォルダ一覧取得 - (this as any).api('drive/folders', { - folderId: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - (this as any).api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - fetchedFolders.forEach(this.appendFolder); - fetchedFiles.forEach(this.appendFile); - this.fetching = false; - - // 一連の読み込みが完了したイベントを発行 - this.$emit('fetched'); - } else { - flag = true; - // 一連の読み込みが半分完了したイベントを発行 - this.$emit('fetch-mid'); - } - }; - - if (this.folder == null) { - // Fetch addtional drive info - (this as any).api('drive').then(info => { - this.info = info; - }); - } - }, - - fetchMoreFiles() { - this.fetching = true; - this.fetchingMoreFiles = true; - - const max = 30; - - // ファイル一覧取得 - (this as any).api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: max + 1, - untilId: this.files[this.files.length - 1].id - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - files.forEach(this.appendFile); - this.fetching = false; - this.fetchingMoreFiles = false; - }); - }, - - chooseFile(file) { - if (this.isFileSelectMode) { - if (this.multiple) { - if (this.selectedFiles.some(f => f.id == file.id)) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.$emit('change-selection', this.selectedFiles); - } else { - this.$emit('selected', file); - } - } else { - this.cf(file); - } - }, - - cf(file, silent = false) { - if (typeof file == 'object') file = file.id; - - this.fetching = true; - - (this as any).api('drive/files/show', { - fileId: file - }).then(file => { - this.file = file; - this.folder = null; - this.hierarchyFolders = []; - - if (file.folder) this.dive(file.folder); - - this.fetching = false; - - this.$emit('open-file', this.file, silent); - }); - }, - - openContextMenu() { - const fn = window.prompt('何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>'); - if (fn == null || fn == '') return; - switch (fn) { - case '1': - this.selectLocalFile(); - break; - case '2': - this.urlUpload(); - break; - case '3': - this.createFolder(); - break; - case '4': - this.renameFolder(); - break; - case '5': - this.moveFolder(); - break; - case '6': - alert('ごめんなさい!フォルダの削除は未実装です...。'); - break; - } - }, - - selectLocalFile() { - (this.$refs.file as any).click(); - }, - - createFolder() { - const name = window.prompt('フォルダー名'); - if (name == null || name == '') return; - (this as any).api('drive/folders/create', { - name: name, - parentId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - }); - }, - - renameFolder() { - if (this.folder == null) { - alert('現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。'); - return; - } - const name = window.prompt('フォルダー名', this.folder.name); - if (name == null || name == '') return; - (this as any).api('drive/folders/update', { - name: name, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }, - - moveFolder() { - if (this.folder == null) { - alert('現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。'); - return; - } - (this as any).apis.chooseDriveFolder().then(folder => { - (this as any).api('drive/folders/update', { - parentId: folder ? folder.id : null, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - urlUpload() { - const url = window.prompt('アップロードしたいファイルのURL'); - if (url == null || url == '') return; - (this as any).api('drive/files/upload_from_url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); - alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。'); - }, - - onChangeLocalFile() { - Array.from((this.$refs.file as any).files) - .forEach(f => (this.$refs.uploader as any).upload(f, this.folder)); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive - background #fff - - > nav - display block - position sticky - position -webkit-sticky - top 0 - z-index 1 - width 100% - padding 10px 12px - overflow auto - white-space nowrap - font-size 0.9em - color rgba(0, 0, 0, 0.67) - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color rgba(#fff, 0.75) - border-bottom solid 1px rgba(0, 0, 0, 0.13) - - > p - > a - display inline - margin 0 - padding 0 - text-decoration none !important - color inherit - - &:last-child - font-weight bold - - > [data-fa] - margin-right 4px - - > span - margin 0 8px - opacity 0.5 - - > .browser - &.fetching - opacity 0.5 - - > .info - border-bottom solid 1px #eee - - &:empty - display none - - > p - display block - max-width 500px - margin 0 auto - padding 4px 16px - font-size 10px - color #777 - - > .folders - > .folder - border-bottom solid 1px #eee - - > .files - > .file - border-bottom solid 1px #eee - - > .more - display block - width 100% - padding 16px - font-size 16px - color #555 - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background rgba(0, 0, 0, 0.2) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { 100% { transform: rotate(360deg); }} - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } 50% { - transform: scale(1.0); - } - } - - > .file - display none - -</style> diff --git a/src/server/web/app/mobile/views/components/follow-button.vue b/src/server/web/app/mobile/views/components/follow-button.vue deleted file mode 100644 index 43c69d4e02..0000000000 --- a/src/server/web/app/mobile/views/components/follow-button.vue +++ /dev/null @@ -1,123 +0,0 @@ -<template> -<button class="mk-follow-button" - :class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }" - @click="onClick" - :disabled="wait" -> - <template v-if="!wait && user.isFollowing">%fa:minus%</template> - <template v-if="!wait && !user.isFollowing">%fa:plus%</template> - <template v-if="wait">%fa:spinner .pulse .fw%</template> - {{ user.isFollowing ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }} -</button> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - user: { - type: Object, - required: true - } - }, - data() { - return { - wait: false, - connection: null, - connectionId: null - }; - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('follow', this.onFollow); - this.connection.on('unfollow', this.onUnfollow); - }, - beforeDestroy() { - this.connection.off('follow', this.onFollow); - this.connection.off('unfollow', this.onUnfollow); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - - onFollow(user) { - if (user.id == this.user.id) { - this.user.isFollowing = user.isFollowing; - } - }, - - onUnfollow(user) { - if (user.id == this.user.id) { - this.user.isFollowing = user.isFollowing; - } - }, - - onClick() { - this.wait = true; - if (this.user.isFollowing) { - (this as any).api('following/delete', { - userId: this.user.id - }).then(() => { - this.user.isFollowing = false; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - }); - } else { - (this as any).api('following/create', { - userId: this.user.id - }).then(() => { - this.user.isFollowing = true; - }).catch(err => { - console.error(err); - }).then(() => { - this.wait = false; - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-follow-button - display block - user-select none - cursor pointer - padding 0 16px - margin 0 - height inherit - font-size 16px - outline none - border solid 1px $theme-color - border-radius 4px - - * - pointer-events none - - &.follow - color $theme-color - background transparent - - &:hover - background rgba($theme-color, 0.1) - - &:active - background rgba($theme-color, 0.2) - - &.unfollow - color $theme-color-foreground - background $theme-color - - &.wait - cursor wait !important - opacity 0.7 - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/components/friends-maker.vue b/src/server/web/app/mobile/views/components/friends-maker.vue deleted file mode 100644 index 961a5f568a..0000000000 --- a/src/server/web/app/mobile/views/components/friends-maker.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div class="mk-friends-maker"> - <p class="title">気になるユーザーをフォロー:</p> - <div class="users" v-if="!fetching && users.length > 0"> - <mk-user-card v-for="user in users" :key="user.id" :user="user"/> - </div> - <p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p> - <a class="refresh" @click="refresh">もっと見る</a> - <button class="close" @click="close" title="閉じる">%fa:times%</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - users: [], - fetching: true, - limit: 6, - page: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - this.users = []; - - (this as any).api('users/recommendation', { - limit: this.limit, - offset: this.limit * this.page - }).then(users => { - this.users = users; - this.fetching = false; - }); - }, - refresh() { - if (this.users.length < this.limit) { - this.page = 0; - } else { - this.page++; - } - this.fetch(); - }, - close() { - this.$destroy(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-friends-maker - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - > .title - margin 0 - padding 8px 16px - font-size 1em - font-weight bold - color #888 - - > .users - overflow-x scroll - -webkit-overflow-scrolling touch - white-space nowrap - padding 16px - background #eee - - > .mk-user-card - &:not(:last-child) - margin-right 16px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - - > .refresh - display block - margin 0 - padding 8px 16px - text-align right - font-size 0.9em - color #999 - - > .close - cursor pointer - display block - position absolute - top 0 - right 0 - z-index 1 - margin 0 - padding 0 - font-size 1.2em - color #999 - border none - outline none - background transparent - - &:hover - color #555 - - &:active - color #222 - - > [data-fa] - padding 10px - -</style> diff --git a/src/server/web/app/mobile/views/components/index.ts b/src/server/web/app/mobile/views/components/index.ts deleted file mode 100644 index fb8f65f47d..0000000000 --- a/src/server/web/app/mobile/views/components/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import timeline from './timeline.vue'; -import post from './post.vue'; -import posts from './posts.vue'; -import mediaImage from './media-image.vue'; -import mediaVideo from './media-video.vue'; -import drive from './drive.vue'; -import postPreview from './post-preview.vue'; -import subPostContent from './sub-post-content.vue'; -import postCard from './post-card.vue'; -import userCard from './user-card.vue'; -import postDetail from './post-detail.vue'; -import followButton from './follow-button.vue'; -import friendsMaker from './friends-maker.vue'; -import notification from './notification.vue'; -import notifications from './notifications.vue'; -import notificationPreview from './notification-preview.vue'; -import usersList from './users-list.vue'; -import userPreview from './user-preview.vue'; -import userTimeline from './user-timeline.vue'; -import activity from './activity.vue'; -import widgetContainer from './widget-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-timeline', timeline); -Vue.component('mk-post', post); -Vue.component('mk-posts', posts); -Vue.component('mk-media-image', mediaImage); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-drive', drive); -Vue.component('mk-post-preview', postPreview); -Vue.component('mk-sub-post-content', subPostContent); -Vue.component('mk-post-card', postCard); -Vue.component('mk-user-card', userCard); -Vue.component('mk-post-detail', postDetail); -Vue.component('mk-follow-button', followButton); -Vue.component('mk-friends-maker', friendsMaker); -Vue.component('mk-notification', notification); -Vue.component('mk-notifications', notifications); -Vue.component('mk-notification-preview', notificationPreview); -Vue.component('mk-users-list', usersList); -Vue.component('mk-user-preview', userPreview); -Vue.component('mk-user-timeline', userTimeline); -Vue.component('mk-activity', activity); -Vue.component('mk-widget-container', widgetContainer); diff --git a/src/server/web/app/mobile/views/components/media-image.vue b/src/server/web/app/mobile/views/components/media-image.vue deleted file mode 100644 index cfc2134988..0000000000 --- a/src/server/web/app/mobile/views/components/media-image.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<a class="mk-media-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['image'], - computed: { - style(): any { - return { - 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${this.image.url}?thumbnail&size=512)` - }; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-media-image - display block - overflow hidden - width 100% - height 100% - background-position center - background-size cover - border-radius 4px - -</style> diff --git a/src/server/web/app/mobile/views/components/media-video.vue b/src/server/web/app/mobile/views/components/media-video.vue deleted file mode 100644 index 68cd48587a..0000000000 --- a/src/server/web/app/mobile/views/components/media-video.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> - <a class="mk-media-video" - :href="video.url" - target="_blank" - :style="imageStyle" - :title="video.name"> - %fa:R play-circle% - </a> -</template> - -<script lang="ts"> -import Vue from 'vue' -export default Vue.extend({ - props: ['video'], - computed: { - imageStyle(): any { - return { - 'background-image': `url(${this.video.url}?thumbnail&size=512)` - }; - } - },}) -</script> - -<style lang="stylus" scoped> -.mk-media-video - display flex - justify-content center - align-items center - - font-size 3.5em - overflow hidden - background-position center - background-size cover - width 100% - height 100% -</style> diff --git a/src/server/web/app/mobile/views/components/notification-preview.vue b/src/server/web/app/mobile/views/components/notification-preview.vue deleted file mode 100644 index fce9ed82f9..0000000000 --- a/src/server/web/app/mobile/views/components/notification-preview.vue +++ /dev/null @@ -1,128 +0,0 @@ -<template> -<div class="mk-notification-preview" :class="notification.type"> - <template v-if="notification.type == 'reaction'"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user.name }}</p> - <p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p> - </div> - </template> - - <template v-if="notification.type == 'repost'"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p>%fa:retweet%{{ notification.post.user.name }}</p> - <p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%</p> - </div> - </template> - - <template v-if="notification.type == 'quote'"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p>%fa:quote-left%{{ notification.post.user.name }}</p> - <p class="post-preview">{{ getPostSummary(notification.post) }}</p> - </div> - </template> - - <template v-if="notification.type == 'follow'"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p>%fa:user-plus%{{ notification.user.name }}</p> - </div> - </template> - - <template v-if="notification.type == 'reply'"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p>%fa:reply%{{ notification.post.user.name }}</p> - <p class="post-preview">{{ getPostSummary(notification.post) }}</p> - </div> - </template> - - <template v-if="notification.type == 'mention'"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p>%fa:at%{{ notification.post.user.name }}</p> - <p class="post-preview">{{ getPostSummary(notification.post) }}</p> - </div> - </template> - - <template v-if="notification.type == 'poll_vote'"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - <div class="text"> - <p>%fa:chart-pie%{{ notification.user.name }}</p> - <p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p> - </div> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getPostSummary from '../../../../../common/get-post-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getPostSummary - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notification-preview - margin 0 - padding 8px - color #fff - overflow-wrap break-word - - &:after - content "" - display block - clear both - - img - display block - float left - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px - - .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - i, mk-reaction-icon - margin-right 4px - - .post-ref - - [data-fa] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.repost, &.quote - .text p i - color #77B255 - - &.follow - .text p i - color #53c7ce - - &.reply, &.mention - .text p i - color #fff - -</style> - diff --git a/src/server/web/app/mobile/views/components/notification.vue b/src/server/web/app/mobile/views/components/notification.vue deleted file mode 100644 index e221fb3ac4..0000000000 --- a/src/server/web/app/mobile/views/components/notification.vue +++ /dev/null @@ -1,164 +0,0 @@ -<template> -<div class="mk-notification"> - <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> - <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link> - </p> - <router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`"> - %fa:quote-left%{{ getPostSummary(notification.post) }} - %fa:quote-right% - </router-link> - </div> - </div> - - <div class="notification repost" v-if="notification.type == 'repost'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> - %fa:retweet% - <router-link :to="`/@${acct}`">{{ notification.post.user.name }}</router-link> - </p> - <router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`"> - %fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right% - </router-link> - </div> - </div> - - <template v-if="notification.type == 'quote'"> - <mk-post :post="notification.post"/> - </template> - - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> - %fa:user-plus% - <router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link> - </p> - </div> - </div> - - <template v-if="notification.type == 'reply'"> - <mk-post :post="notification.post"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-post :post="notification.post"/> - </template> - - <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> - %fa:chart-pie% - <router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link> - </p> - <router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`"> - %fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right% - </router-link> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getPostSummary from '../../../../../common/get-post-summary'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['notification'], - computed: { - acct() { - return getAcct(this.notification.user); - } - }, - data() { - return { - getPostSummary - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notification - - > .notification - padding 16px - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .mk-time - display inline - position absolute - top 16px - right 12px - vertical-align top - color rgba(0, 0, 0, 0.6) - font-size 0.9em - - > .avatar-anchor - display block - float left - - img - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - i, .mk-reaction-icon - margin-right 4px - - > .post-preview - color rgba(0, 0, 0, 0.7) - - > .post-ref - color rgba(0, 0, 0, 0.7) - - [data-fa] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.repost - .text p i - color #77B255 - - &.follow - .text p i - color #53c7ce - -</style> - diff --git a/src/server/web/app/mobile/views/components/notifications.vue b/src/server/web/app/mobile/views/components/notifications.vue deleted file mode 100644 index d68b990dfa..0000000000 --- a/src/server/web/app/mobile/views/components/notifications.vue +++ /dev/null @@ -1,168 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="notifications" v-if="notifications.length != 0"> - <template v-for="(notification, i) in _notifications"> - <mk-notification :notification="notification" :key="notification.id"/> - <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> - <span>%fa:angle-up%{{ notification._datetext }}</span> - <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </div> - <button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> - <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template> - {{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }} - </button> - <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:mobile.tags.mk-notifications.empty%</p> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - fetchingMoreNotifications: false, - notifications: [], - moreNotifications: false, - connection: null, - connectionId: null - }; - }, - computed: { - _notifications(): any[] { - return (this.notifications as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = `${month}月 ${date}日`; - return notification; - }); - } - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('notification', this.onNotification); - - const max = 10; - - (this as any).api('i/notifications', { - limit: max + 1 - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } - - this.notifications = notifications; - this.fetching = false; - this.$emit('fetched'); - }); - }, - beforeDestroy() { - this.connection.off('notification', this.onNotification); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - fetchMoreNotifications() { - this.fetchingMoreNotifications = true; - - const max = 30; - - (this as any).api('i/notifications', { - limit: max + 1, - untilId: this.notifications[this.notifications.length - 1].id - }).then(notifications => { - if (notifications.length == max + 1) { - this.moreNotifications = true; - notifications.pop(); - } else { - this.moreNotifications = false; - } - this.notifications = this.notifications.concat(notifications); - this.fetchingMoreNotifications = false; - }); - }, - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.connection.send({ - type: 'read_notification', - id: notification.id - }); - - this.notifications.unshift(notification); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - margin 8px auto - padding 0 - max-width 500px - width calc(100% - 16px) - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > .notifications - - > .mk-notification - margin 0 auto - max-width 500px - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - &:last-child - border-bottom none - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color #aaa - background #fdfdfd - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - span - margin 0 16px - - i - margin-right 8px - - > .more - display block - width 100% - padding 16px - color #555 - border-top solid 1px rgba(0, 0, 0, 0.05) - - > [data-fa] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/components/notify.vue b/src/server/web/app/mobile/views/components/notify.vue deleted file mode 100644 index 6d4a481dbe..0000000000 --- a/src/server/web/app/mobile/views/components/notify.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<div class="mk-notify"> - <mk-notification-preview :notification="notification"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: ['notification'], - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$el, - bottom: '0px', - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.$el, - bottom: '-64px', - duration: 500, - easing: 'easeOutQuad', - complete: () => this.$destroy() - }); - }, 6000); - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notify - position fixed - z-index 1024 - bottom -64px - left 0 - width 100% - height 64px - pointer-events none - -webkit-backdrop-filter blur(2px) - backdrop-filter blur(2px) - background-color rgba(#000, 0.5) - -</style> diff --git a/src/server/web/app/mobile/views/components/post-card.vue b/src/server/web/app/mobile/views/components/post-card.vue deleted file mode 100644 index 10dfd92415..0000000000 --- a/src/server/web/app/mobile/views/components/post-card.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> -<div class="mk-post-card"> - <a :href="`/@${acct}/${post.id}`"> - <header> - <img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3> - </header> - <div> - {{ text }} - </div> - <mk-time :time="post.createdAt"/> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import summary from '../../../../../common/get-post-summary'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - }, - text(): string { - return summary(this.post); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-post-card - display inline-block - width 150px - //height 120px - font-size 12px - background #fff - border-radius 4px - - > a - display block - color #2c3940 - - &:hover - text-decoration none - - > header - > img - position absolute - top 8px - left 8px - width 28px - height 28px - border-radius 6px - - > h3 - display inline-block - overflow hidden - width calc(100% - 45px) - margin 8px 0 0 42px - line-height 28px - white-space nowrap - text-overflow ellipsis - font-size 12px - - > div - padding 2px 8px 8px 8px - height 60px - overflow hidden - white-space normal - - &:after - content "" - display block - position absolute - top 40px - left 0 - width 100% - height 20px - background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%) - - > .mk-time - display inline-block - padding 8px - color #aaa - -</style> diff --git a/src/server/web/app/mobile/views/components/post-detail.sub.vue b/src/server/web/app/mobile/views/components/post-detail.sub.vue deleted file mode 100644 index db7567834a..0000000000 --- a/src/server/web/app/mobile/views/components/post-detail.sub.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="root sub"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </header> - <div class="body"> - <mk-sub-post-content class="text" :post="post"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - } - } -}); -</script> - -<style lang="stylus" scoped> -.root.sub - padding 8px - font-size 0.9em - background #fdfdfd - - @media (min-width 500px) - padding 12px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 60px) - - > header - display flex - margin-bottom 4px - white-space nowrap - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 .5em 0 0 - color #d1d8da - - > .time - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - -</style> - diff --git a/src/server/web/app/mobile/views/components/post-detail.vue b/src/server/web/app/mobile/views/components/post-detail.vue deleted file mode 100644 index f0af1a61aa..0000000000 --- a/src/server/web/app/mobile/views/components/post-detail.vue +++ /dev/null @@ -1,447 +0,0 @@ -<template> -<div class="mk-post-detail"> - <button - class="more" - v-if="p.reply && p.reply.replyId && context == null" - @click="fetchContext" - :disabled="fetchingContext" - > - <template v-if="!contextFetching">%fa:ellipsis-v%</template> - <template v-if="contextFetching">%fa:spinner .pulse%</template> - </button> - <div class="context"> - <x-sub v-for="post in context" :key="post.id" :post="post"/> - </div> - <div class="reply-to" v-if="p.reply"> - <x-sub :post="p.reply"/> - </div> - <div class="repost" v-if="isRepost"> - <p> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> - </router-link> - %fa:retweet% - <router-link class="name" :to="`/@${acct}`"> - {{ post.user.name }} - </router-link> - がRepost - </p> - </div> - <article> - <header> - <router-link class="avatar-anchor" :to="`/@${pAcct}`"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div> - <router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link> - <span class="username">@{{ pAcct }}</span> - </div> - </header> - <div class="body"> - <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> - </div> - <div class="media" v-if="p.media"> - <mk-media-list :media-list="p.media"/> - </div> - <mk-poll v-if="p.poll" :post="p"/> - <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% 位置情報</a> - <div class="map" v-if="p.geo" ref="map"></div> - <div class="repost" v-if="p.repost"> - <mk-post-preview :post="p.repost"/> - </div> - </div> - <router-link class="time" :to="`/@${pAcct}/${p.id}`"> - <mk-time :time="p.createdAt" mode="detail"/> - </router-link> - <footer> - <mk-reactions-viewer :post="p"/> - <button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%"> - %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> - </button> - <button @click="repost" title="Repost"> - %fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p> - </button> - <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%"> - %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> - </button> - <button @click="menu" ref="menuButton"> - %fa:ellipsis-h% - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="post in replies" :key="post.id" :post="post"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; -import MkPostMenu from '../../../common/views/components/post-menu.vue'; -import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; -import XSub from './post-detail.sub.vue'; - -export default Vue.extend({ - components: { - XSub - }, - props: { - post: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - data() { - return { - context: [], - contextFetching: false, - replies: [], - }; - }, - computed: { - acct() { - return getAcct(this.post.user); - }, - pAcct() { - return getAcct(this.p.user); - }, - isRepost(): boolean { - return (this.post.repost && - this.post.text == null && - this.post.mediaIds == null && - this.post.poll == null); - }, - p(): any { - return this.isRepost ? this.post.repost : this.post; - }, - reactionsCount(): number { - return this.p.reactionCounts - ? Object.keys(this.p.reactionCounts) - .map(key => this.p.reactionCounts[key]) - .reduce((a, b) => a + b) - : 0; - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } - } - }, - mounted() { - // Get replies - if (!this.compact) { - (this as any).api('posts/replies', { - postId: this.p.id, - limit: 8 - }).then(replies => { - this.replies = replies; - }); - } - - // Draw map - if (this.p.geo) { - const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true; - if (shouldShowMap) { - (this as any).os.getGoogleMaps().then(maps => { - const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); - const map = new maps.Map(this.$refs.map, { - center: uluru, - zoom: 15 - }); - new maps.Marker({ - position: uluru, - map: map - }); - }); - } - } - }, - methods: { - fetchContext() { - this.contextFetching = true; - - // Fetch context - (this as any).api('posts/context', { - postId: this.p.replyId - }).then(context => { - this.contextFetching = false; - this.context = context.reverse(); - }); - }, - reply() { - (this as any).apis.post({ - reply: this.p - }); - }, - repost() { - (this as any).apis.post({ - repost: this.p - }); - }, - react() { - (this as any).os.new(MkReactionPicker, { - source: this.$refs.reactButton, - post: this.p, - compact: true - }); - }, - menu() { - (this as any).os.new(MkPostMenu, { - source: this.$refs.menuButton, - post: this.p, - compact: true - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-post-detail - overflow hidden - margin 0 auto - padding 0 - width 100% - text-align left - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - > .fetching - padding 64px 0 - - > .more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color #999 - cursor pointer - background #fafafa - outline none - border none - border-bottom solid 1px #eef0f2 - border-radius 6px 6px 0 0 - box-shadow none - - &:hover - background #f6f6f6 - - &:active - background #f0f0f0 - - &:disabled - color #ccc - - > .context - > * - border-bottom 1px solid #eef0f2 - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 16px 32px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 6px - - [data-fa] - margin-right 4px - - .name - font-weight bold - - & + article - padding-top 8px - - > .reply-to - border-bottom 1px solid #eef0f2 - - > article - padding 14px 16px 9px 16px - - @media (min-width 500px) - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > header - display flex - line-height 1.1 - - > .avatar-anchor - display block - padding 0 .5em 0 0 - - > .avatar - display block - width 54px - height 54px - margin 0 - border-radius 8px - vertical-align bottom - - @media (min-width 500px) - width 60px - height 60px - - > div - - > .name - display inline-block - margin .4em 0 - color #777 - font-size 16px - font-weight bold - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color #ccc - - > .body - padding 8px 0 - - > .repost - margin 8px 0 - - > .mk-post-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-url-preview - margin-top 8px - - > .media - > img - display block - max-width 100% - - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% - - > .time - font-size 16px - color #c0c0c0 - - > footer - font-size 1.2em - - > button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color #ddd - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - > .replies - > * - border-top 1px solid #eef0f2 - -</style> - -<style lang="stylus" module> -.text - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 16px - color #717171 - - @media (min-width 500px) - font-size 24px - -</style> diff --git a/src/server/web/app/mobile/views/components/post-form.vue b/src/server/web/app/mobile/views/components/post-form.vue deleted file mode 100644 index 5b78a25710..0000000000 --- a/src/server/web/app/mobile/views/components/post-form.vue +++ /dev/null @@ -1,275 +0,0 @@ -<template> -<div class="mk-post-form"> - <header> - <button class="cancel" @click="cancel">%fa:times%</button> - <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:mobile.tags.mk-post-form.submit%' }}</button> - </div> - </header> - <div class="form"> - <mk-post-preview v-if="reply" :post="reply"/> - <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-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"> - <div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="detachMedia(file)"></div> - </div> - </x-draggable> - </div> - <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <button class="upload" @click="chooseFile">%fa:upload%</button> - <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> - <button class="kao" @click="kao">%fa:R smile%</button> - <button class="poll" @click="poll = true">%fa:chart-pie%</button> - <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> - <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import getKao from '../../../common/scripts/get-kao'; - -export default Vue.extend({ - components: { - XDraggable - }, - props: ['reply'], - data() { - return { - posting: false, - text: '', - uploadings: [], - files: [], - poll: false, - geo: null - }; - }, - mounted() { - this.$nextTick(() => { - this.focus(); - }); - }, - methods: { - focus() { - (this.$refs.text as any).focus(); - }, - chooseFile() { - (this.$refs.file as any).click(); - }, - chooseFileFromDrive() { - (this as any).apis.chooseDriveFile({ - multiple: true - }).then(files => { - files.forEach(this.attachMedia); - }); - }, - attachMedia(driveFile) { - this.files.push(driveFile); - this.$emit('change-attached-media', this.files); - }, - detachMedia(file) { - this.files = this.files.filter(x => x.id != file.id); - this.$emit('change-attached-media', this.files); - }, - onChangeFile() { - Array.from((this.$refs.file as any).files).forEach(this.upload); - }, - upload(file) { - (this.$refs.uploader as any).upload(file); - }, - onChangeUploadings(uploads) { - this.$emit('change-uploadings', uploads); - }, - setGeo() { - if (navigator.geolocation == null) { - alert('お使いの端末は位置情報に対応していません'); - return; - } - - navigator.geolocation.getCurrentPosition(pos => { - this.geo = pos.coords; - }, err => { - alert('エラー: ' + err.message); - }, { - enableHighAccuracy: true - }); - }, - removeGeo() { - this.geo = null; - }, - clear() { - this.text = ''; - this.files = []; - this.poll = false; - this.$emit('change-attached-media'); - }, - post() { - this.posting = true; - const viaMobile = (this as any).os.i.account.clientSettings.disableViaMobile !== true; - (this as any).api('posts/create', { - 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, - poll: this.poll ? (this.$refs.poll as any).get() : undefined, - geo: this.geo ? { - coordinates: [this.geo.longitude, this.geo.latitude], - altitude: this.geo.altitude, - accuracy: this.geo.accuracy, - altitudeAccuracy: this.geo.altitudeAccuracy, - heading: isNaN(this.geo.heading) ? null : this.geo.heading, - speed: this.geo.speed, - } : null, - viaMobile: viaMobile - }).then(data => { - this.$emit('post'); - this.$destroy(); - }).catch(err => { - this.posting = false; - }); - }, - cancel() { - this.$emit('cancel'); - this.$destroy(); - }, - kao() { - this.text += getKao(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-post-form - max-width 500px - width calc(100% - 16px) - margin 8px auto - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > header - z-index 1 - height 50px - box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1) - - > .cancel - padding 0 - width 50px - line-height 50px - font-size 24px - color #555 - - > div - position absolute - top 0 - right 0 - color #657786 - - > .text-count - line-height 50px - - > .geo - margin 0 8px - line-height 50px - - > .submit - margin 8px - padding 0 16px - line-height 34px - vertical-align bottom - color $theme-color-foreground - background $theme-color - border-radius 4px - - &:disabled - opacity 0.7 - - > .form - max-width 500px - margin 0 auto - - > .mk-post-preview - padding 16px - - > .attaches - - > .files - display block - margin 0 - padding 4px - list-style none - - &:after - content "" - display block - clear both - - > .file - display block - float left - margin 0 - padding 0 - border solid 4px transparent - - > .img - width 64px - height 64px - background-size cover - background-position center center - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - - > .file - display none - - > textarea - display block - padding 12px - margin 0 - width 100% - max-width 100% - min-width 100% - min-height 80px - font-size 16px - color #333 - border none - border-bottom solid 1px #ddd - border-radius 0 - - &:disabled - opacity 0.5 - - > .upload - > .drive - > .kao - > .poll - > .geo - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color #657786 - background transparent - outline none - border none - border-radius 0 - box-shadow none - -</style> - diff --git a/src/server/web/app/mobile/views/components/post-preview.vue b/src/server/web/app/mobile/views/components/post-preview.vue deleted file mode 100644 index a6141dc8e3..0000000000 --- a/src/server/web/app/mobile/views/components/post-preview.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="mk-post-preview"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </header> - <div class="body"> - <mk-sub-post-content class="text" :post="post"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-post-preview - margin 0 - padding 0 - font-size 0.9em - background #fff - - &:after - content "" - display block - clear both - - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 8px - vertical-align bottom - - > .main - float left - width calc(100% - 60px) - - > header - display flex - margin-bottom 4px - white-space nowrap - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 .5em 0 0 - color #d1d8da - - > .time - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - -</style> diff --git a/src/server/web/app/mobile/views/components/post.sub.vue b/src/server/web/app/mobile/views/components/post.sub.vue deleted file mode 100644 index adf444a2d6..0000000000 --- a/src/server/web/app/mobile/views/components/post.sub.vue +++ /dev/null @@ -1,115 +0,0 @@ -<template> -<div class="sub"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="created-at" :to="`/@${acct}/${post.id}`"> - <mk-time :time="post.createdAt"/> - </router-link> - </header> - <div class="body"> - <mk-sub-post-content class="text" :post="post"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['post'], - computed: { - acct() { - return getAcct(this.post.user); - } - } -}); -</script> - -<style lang="stylus" scoped> -.sub - font-size 0.9em - padding 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 10px 0 0 - - @media (min-width 500px) - margin-right 16px - - > .avatar - display block - width 44px - height 44px - margin 0 - border-radius 8px - vertical-align bottom - - @media (min-width 500px) - width 52px - height 52px - - > .main - float left - width calc(100% - 54px) - - @media (min-width 500px) - width calc(100% - 68px) - - > header - display flex - margin-bottom 2px - white-space nowrap - - > .name - display block - margin 0 0.5em 0 0 - padding 0 - overflow hidden - color #607073 - font-size 1em - font-weight 700 - text-align left - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 - color #d1d8da - - > .created-at - margin-left auto - color #b2b8bb - - > .body - - > .text - cursor default - margin 0 - padding 0 - font-size 1.1em - color #717171 - - pre - max-height 120px - font-size 80% - -</style> - diff --git a/src/server/web/app/mobile/views/components/post.vue b/src/server/web/app/mobile/views/components/post.vue deleted file mode 100644 index a01eb7669e..0000000000 --- a/src/server/web/app/mobile/views/components/post.vue +++ /dev/null @@ -1,523 +0,0 @@ -<template> -<div class="post" :class="{ repost: isRepost }"> - <div class="reply-to" v-if="p.reply"> - <x-sub :post="p.reply"/> - </div> - <div class="repost" v-if="isRepost"> - <p> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - %fa:retweet% - <span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> - <span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span> - </p> - <mk-time :time="post.createdAt"/> - </div> - <article> - <router-link class="avatar-anchor" :to="`/@${pAcct}`"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link> - <span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span> - <span class="username">@{{ pAcct }}</span> - <div class="info"> - <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> - <router-link class="created-at" :to="url"> - <mk-time :time="p.createdAt"/> - </router-link> - </div> - </header> - <div class="body"> - <p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p> - <div class="text"> - <a class="reply" v-if="p.reply"> - %fa:reply% - </a> - <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> - <a class="rp" v-if="p.repost != null">RP:</a> - </div> - <div class="media" v-if="p.media"> - <mk-media-list :media-list="p.media"/> - </div> - <mk-poll v-if="p.poll" :post="p" ref="pollViewer"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> - </div> - <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% 位置情報</a> - <div class="map" v-if="p.geo" ref="map"></div> - <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> - <div class="repost" v-if="p.repost"> - <mk-post-preview :post="p.repost"/> - </div> - </div> - <footer> - <mk-reactions-viewer :post="p" ref="reactionsViewer"/> - <button @click="reply"> - %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> - </button> - <button @click="repost" title="Repost"> - %fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p> - </button> - <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton"> - %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> - </button> - <button class="menu" @click="menu" ref="menuButton"> - %fa:ellipsis-h% - </button> - </footer> - </div> - </article> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; -import MkPostMenu from '../../../common/views/components/post-menu.vue'; -import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; -import XSub from './post.sub.vue'; - -export default Vue.extend({ - components: { - XSub - }, - props: ['post'], - data() { - return { - connection: null, - connectionId: null - }; - }, - computed: { - acct() { - return getAcct(this.post.user); - }, - pAcct() { - return getAcct(this.p.user); - }, - isRepost(): boolean { - return (this.post.repost && - this.post.text == null && - this.post.mediaIds == null && - this.post.poll == null); - }, - p(): any { - return this.isRepost ? this.post.repost : this.post; - }, - reactionsCount(): number { - return this.p.reactionCounts - ? Object.keys(this.p.reactionCounts) - .map(key => this.p.reactionCounts[key]) - .reduce((a, b) => a + b) - : 0; - }, - url(): string { - return `/@${this.pAcct}/${this.p.id}`; - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } - } - }, - created() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - } - }, - mounted() { - this.capture(true); - - if ((this as any).os.isSignedIn) { - this.connection.on('_connected_', this.onStreamConnected); - } - - // Draw map - if (this.p.geo) { - const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true; - if (shouldShowMap) { - (this as any).os.getGoogleMaps().then(maps => { - const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); - const map = new maps.Map(this.$refs.map, { - center: uluru, - zoom: 15 - }); - new maps.Marker({ - position: uluru, - map: map - }); - }); - } - } - }, - beforeDestroy() { - this.decapture(true); - - if ((this as any).os.isSignedIn) { - this.connection.off('_connected_', this.onStreamConnected); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - capture(withHandler = false) { - if ((this as any).os.isSignedIn) { - this.connection.send({ - type: 'capture', - id: this.p.id - }); - if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated); - } - }, - decapture(withHandler = false) { - if ((this as any).os.isSignedIn) { - this.connection.send({ - type: 'decapture', - id: this.p.id - }); - if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated); - } - }, - onStreamConnected() { - this.capture(); - }, - onStreamPostUpdated(data) { - const post = data.post; - if (post.id == this.post.id) { - this.$emit('update:post', post); - } else if (post.id == this.post.repostId) { - this.post.repost = post; - } - }, - reply() { - (this as any).apis.post({ - reply: this.p - }); - }, - repost() { - (this as any).apis.post({ - repost: this.p - }); - }, - react() { - (this as any).os.new(MkReactionPicker, { - source: this.$refs.reactButton, - post: this.p, - compact: true - }); - }, - menu() { - (this as any).os.new(MkPostMenu, { - source: this.$refs.menuButton, - post: this.p, - compact: true - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.post - font-size 12px - border-bottom solid 1px #eaeaea - - &:first-child - border-radius 8px 8px 0 0 - - > .repost - border-radius 8px 8px 0 0 - - &:last-of-type - border-bottom none - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - > .repost - color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) - - > p - margin 0 - padding 8px 16px - line-height 28px - - @media (min-width 500px) - padding 16px - - .avatar-anchor - display inline-block - - .avatar - vertical-align bottom - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px - - [data-fa] - margin-right 4px - - .name - font-weight bold - - > .mk-time - position absolute - top 8px - right 16px - font-size 0.9em - line-height 28px - - @media (min-width 500px) - top 16px - - & + article - padding-top 8px - - > .reply-to - background rgba(0, 0, 0, 0.0125) - - > .mk-post-preview - background transparent - - > article - padding 14px 16px 9px 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 10px 8px 0 - position -webkit-sticky - position sticky - top 62px - - @media (min-width 500px) - margin-right 16px - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 6px - vertical-align bottom - - @media (min-width 500px) - width 58px - height 58px - border-radius 8px - - > .main - float left - width calc(100% - 58px) - - @media (min-width 500px) - width calc(100% - 74px) - - > header - display flex - align-items center - white-space nowrap - - @media (min-width 500px) - margin-bottom 2px - - > .name - display block - margin 0 0.5em 0 0 - padding 0 - overflow hidden - color #627079 - font-size 1em - font-weight bold - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .is-bot - margin 0 0.5em 0 0 - padding 1px 6px - font-size 12px - color #aaa - border solid 1px #ddd - border-radius 3px - - > .username - margin 0 0.5em 0 0 - color #ccc - - > .info - margin-left auto - font-size 0.9em - - > .mobile - margin-right 6px - color #c0c0c0 - - > .created-at - color #c0c0c0 - - > .body - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - - >>> .quote - margin 8px - padding 6px 12px - color #aaa - border-left solid 3px #eee - - > .reply - margin-right 8px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color #a0bf46 - - [data-is-me]:after - content "you" - padding 0 4px - margin-left 4px - font-size 80% - color $theme-color-foreground - background $theme-color - border-radius 4px - - .mk-url-preview - margin-top 8px - - > .channel - margin 0 - - > .tags - margin 4px 0 0 0 - - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px - - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% - - > .media - > img - display block - max-width 100% - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 200px - - &:empty - display none - - > .app - font-size 12px - color #ccc - - > .mk-poll - font-size 80% - - > .repost - margin 8px 0 - - > .mk-post-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - - > footer - > button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color #ddd - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color #666 - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color $theme-color - - &.menu - @media (max-width 350px) - display none - -</style> - -<style lang="stylus" module> -.text - code - padding 4px 8px - margin 0 0.5em - font-size 80% - color #525252 - background #f8f8f8 - border-radius 2px - - pre > code - padding 16px - margin 0 -</style> diff --git a/src/server/web/app/mobile/views/components/posts.vue b/src/server/web/app/mobile/views/components/posts.vue deleted file mode 100644 index 4695f1beaa..0000000000 --- a/src/server/web/app/mobile/views/components/posts.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<div class="mk-posts"> - <slot name="head"></slot> - <slot></slot> - <template v-for="(post, i) in _posts"> - <mk-post :post="post" :key="post.id" @update:post="onPostUpdated(i, $event)"/> - <p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date"> - <span>%fa:angle-up%{{ post._datetext }}</span> - <span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span> - </p> - </template> - <footer> - <slot name="tail"></slot> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - posts: { - type: Array, - default: () => [] - } - }, - computed: { - _posts(): any[] { - return (this.posts as any).map(post => { - const date = new Date(post.createdAt).getDate(); - const month = new Date(post.createdAt).getMonth() + 1; - post._date = date; - post._datetext = `${month}月 ${date}日`; - return post; - }); - } - }, - methods: { - onPostUpdated(i, post) { - Vue.set((this as any).posts, i, post); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-posts - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - > .init - padding 64px 0 - text-align center - color #999 - - > [data-fa] - margin-right 4px - - > .empty - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.9em - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea - - span - margin 0 16px - - [data-fa] - margin-right 8px - - > footer - text-align center - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px - - &:empty - display none - - > button - margin 0 - padding 16px - width 100% - color $theme-color - border-radius 0 0 8px 8px - - &:disabled - opacity 0.7 - -</style> diff --git a/src/server/web/app/mobile/views/components/sub-post-content.vue b/src/server/web/app/mobile/views/components/sub-post-content.vue deleted file mode 100644 index b95883de77..0000000000 --- a/src/server/web/app/mobile/views/components/sub-post-content.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<div class="mk-sub-post-content"> - <div class="body"> - <a class="reply" v-if="post.replyId">%fa:reply%</a> - <mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/> - <a class="rp" v-if="post.repostId">RP: ...</a> - </div> - <details v-if="post.media"> - <summary>({{ post.media.length }}個のメディア)</summary> - <mk-media-list :media-list="post.media"/> - </details> - <details v-if="post.poll"> - <summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary> - <mk-poll :post="post"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['post'] -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-post-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color #a0bf46 - - mk-poll - font-size 80% - -</style> diff --git a/src/server/web/app/mobile/views/components/timeline.vue b/src/server/web/app/mobile/views/components/timeline.vue deleted file mode 100644 index 7b5948faf1..0000000000 --- a/src/server/web/app/mobile/views/components/timeline.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="mk-timeline"> - <mk-friends-maker v-if="alone"/> - <mk-posts :posts="posts"> - <div class="init" v-if="fetching"> - %fa:spinner .pulse%%i18n:common.loading% - </div> - <div class="empty" v-if="!fetching && posts.length == 0"> - %fa:R comments% - %i18n:mobile.tags.mk-home-timeline.empty-timeline% - </div> - <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> - <span v-if="!moreFetching">%i18n:mobile.tags.mk-timeline.load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> - </mk-posts> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -const limit = 10; - -export default Vue.extend({ - props: { - date: { - type: Date, - required: false - } - }, - data() { - return { - fetching: true, - moreFetching: false, - posts: [], - existMore: false, - connection: null, - connectionId: null - }; - }, - computed: { - alone(): boolean { - return (this as any).os.i.followingCount == 0; - } - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('post', this.onPost); - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - - this.fetch(); - }, - beforeDestroy() { - this.connection.off('post', this.onPost); - this.connection.off('follow', this.onChangeFollowing); - this.connection.off('unfollow', this.onChangeFollowing); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - fetch(cb?) { - this.fetching = true; - (this as any).api('posts/timeline', { - limit: limit + 1, - untilDate: this.date ? (this.date as any).getTime() : undefined - }).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - this.existMore = true; - } - this.posts = posts; - this.fetching = false; - this.$emit('loaded'); - if (cb) cb(); - }); - }, - more() { - this.moreFetching = true; - (this as any).api('posts/timeline', { - limit: limit + 1, - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.posts = this.posts.concat(posts); - this.moreFetching = false; - }); - }, - onPost(post) { - this.posts.unshift(post); - }, - onChangeFollowing() { - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-friends-maker - margin-bottom 8px -</style> diff --git a/src/server/web/app/mobile/views/components/ui.header.vue b/src/server/web/app/mobile/views/components/ui.header.vue deleted file mode 100644 index 2bf47a90a9..0000000000 --- a/src/server/web/app/mobile/views/components/ui.header.vue +++ /dev/null @@ -1,242 +0,0 @@ -<template> -<div class="header"> - <mk-special-message/> - <div class="main" ref="main"> - <div class="backdrop"></div> - <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p> - <div class="content" ref="mainContainer"> - <button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button> - <template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template> - <h1> - <slot>Misskey</slot> - </h1> - <slot name="func"></slot> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as anime from 'animejs'; - -export default Vue.extend({ - props: ['func'], - data() { - return { - hasUnreadNotifications: false, - hasUnreadMessagingMessages: false, - hasGameInvitations: false, - connection: null, - connectionId: null - }; - }, - mounted() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('read_all_notifications', this.onReadAllNotifications); - this.connection.on('unread_notification', this.onUnreadNotification); - this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); - this.connection.on('othello_invited', this.onOthelloInvited); - this.connection.on('othello_no_invites', this.onOthelloNoInvites); - - // Fetch count of unread notifications - (this as any).api('notifications/get_unread_count').then(res => { - if (res.count > 0) { - this.hasUnreadNotifications = true; - } - }); - - // Fetch count of unread messaging messages - (this as any).api('messaging/unread').then(res => { - if (res.count > 0) { - this.hasUnreadMessagingMessages = true; - } - }); - - const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000 - const isHisasiburi = ago >= 3600; - (this as any).os.i.account.lastUsedAt = new Date(); - if (isHisasiburi) { - (this.$refs.welcomeback as any).style.display = 'block'; - (this.$refs.main as any).style.overflow = 'hidden'; - - anime({ - targets: this.$refs.welcomeback, - top: '0', - opacity: 1, - delay: 1000, - duration: 500, - easing: 'easeOutQuad' - }); - - anime({ - targets: this.$refs.mainContainer, - opacity: 0, - delay: 1000, - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.$refs.welcomeback, - top: '-48px', - opacity: 0, - duration: 500, - complete: () => { - (this.$refs.welcomeback as any).style.display = 'none'; - (this.$refs.main as any).style.overflow = 'initial'; - }, - easing: 'easeInQuad' - }); - - anime({ - targets: this.$refs.mainContainer, - opacity: 1, - duration: 500, - easing: 'easeInQuad' - }); - }, 2500); - } - } - }, - beforeDestroy() { - if ((this as any).os.isSignedIn) { - this.connection.off('read_all_notifications', this.onReadAllNotifications); - this.connection.off('unread_notification', this.onUnreadNotification); - this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); - this.connection.off('othello_invited', this.onOthelloInvited); - this.connection.off('othello_no_invites', this.onOthelloNoInvites); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - onReadAllNotifications() { - this.hasUnreadNotifications = false; - }, - onUnreadNotification() { - this.hasUnreadNotifications = true; - }, - onReadAllMessagingMessages() { - this.hasUnreadMessagingMessages = false; - }, - onUnreadMessagingMessage() { - this.hasUnreadMessagingMessages = true; - }, - onOthelloInvited() { - this.hasGameInvitations = true; - }, - onOthelloNoInvites() { - this.hasGameInvitations = false; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.header - $height = 48px - - position fixed - top 0 - z-index 1024 - width 100% - box-shadow 0 1px 0 rgba(#000, 0.075) - - > .main - color rgba(#fff, 0.9) - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height $height - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - //background-color rgba(#1b2023, 0.75) - background-color #1b2023 - - > p - display none - position absolute - z-index 1002 - top $height - width 100% - line-height $height - margin 0 - text-align center - color #fff - opacity 0 - - > .content - z-index 1001 - - > h1 - display block - margin 0 auto - padding 0 - width 100% - max-width calc(100% - 112px) - text-align center - font-size 1.1em - font-weight normal - line-height $height - white-space nowrap - overflow hidden - text-overflow ellipsis - - [data-fa], [data-icon] - margin-right 4px - - > img - display inline-block - vertical-align bottom - width ($height - 16px) - height ($height - 16px) - margin 8px - border-radius 6px - - > .nav - display block - position absolute - top 0 - left 0 - padding 0 - width $height - font-size 1.4em - line-height $height - border-right solid 1px rgba(#000, 0.1) - - > [data-fa] - transition all 0.2s ease - - > [data-fa].circle - position absolute - top 8px - left 8px - pointer-events none - font-size 10px - color $theme-color - - > button:last-child - display block - position absolute - top 0 - right 0 - padding 0 - width $height - text-align center - font-size 1.4em - color inherit - line-height $height - border-left solid 1px rgba(#000, 0.1) - -</style> diff --git a/src/server/web/app/mobile/views/components/ui.nav.vue b/src/server/web/app/mobile/views/components/ui.nav.vue deleted file mode 100644 index a923774a73..0000000000 --- a/src/server/web/app/mobile/views/components/ui.nav.vue +++ /dev/null @@ -1,244 +0,0 @@ -<template> -<div class="nav"> - <transition name="back"> - <div class="backdrop" - v-if="isOpen" - @click="$parent.isDrawerOpening = false" - @touchstart="$parent.isDrawerOpening = false" - ></div> - </transition> - <transition name="nav"> - <div class="body" v-if="isOpen"> - <router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`"> - <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/> - <p class="name">{{ os.i.name }}</p> - </router-link> - <div class="links"> - <ul> - <li><router-link to="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</router-link></li> - <li><router-link to="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li> - <li><router-link to="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li> - <li><router-link to="/othello">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li> - </ul> - <ul> - <li><a :href="chUrl" target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li> - <li><router-link to="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</router-link></li> - </ul> - <ul> - <li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li> - </ul> - <ul> - <li><router-link to="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</router-link></li> - </ul> - </div> - <a :href="aboutUrl"><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { docsUrl, chUrl, lang } from '../../../config'; - -export default Vue.extend({ - props: ['isOpen'], - data() { - return { - hasUnreadNotifications: false, - hasUnreadMessagingMessages: false, - hasGameInvitations: false, - connection: null, - connectionId: null, - aboutUrl: `${docsUrl}/${lang}/about`, - chUrl - }; - }, - mounted() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('read_all_notifications', this.onReadAllNotifications); - this.connection.on('unread_notification', this.onUnreadNotification); - this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); - this.connection.on('othello_invited', this.onOthelloInvited); - this.connection.on('othello_no_invites', this.onOthelloNoInvites); - - // Fetch count of unread notifications - (this as any).api('notifications/get_unread_count').then(res => { - if (res.count > 0) { - this.hasUnreadNotifications = true; - } - }); - - // Fetch count of unread messaging messages - (this as any).api('messaging/unread').then(res => { - if (res.count > 0) { - this.hasUnreadMessagingMessages = true; - } - }); - } - }, - beforeDestroy() { - if ((this as any).os.isSignedIn) { - this.connection.off('read_all_notifications', this.onReadAllNotifications); - this.connection.off('unread_notification', this.onUnreadNotification); - this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); - this.connection.off('othello_invited', this.onOthelloInvited); - this.connection.off('othello_no_invites', this.onOthelloNoInvites); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - search() { - const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%'); - if (query == null || query == '') return; - this.$router.push('/search?q=' + encodeURIComponent(query)); - }, - onReadAllNotifications() { - this.hasUnreadNotifications = false; - }, - onUnreadNotification() { - this.hasUnreadNotifications = true; - }, - onReadAllMessagingMessages() { - this.hasUnreadMessagingMessages = false; - }, - onUnreadMessagingMessage() { - this.hasUnreadMessagingMessages = true; - }, - onOthelloInvited() { - this.hasGameInvitations = true; - }, - onOthelloNoInvites() { - this.hasGameInvitations = false; - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.nav - .backdrop - position fixed - top 0 - left 0 - z-index 1025 - width 100% - height 100% - background rgba(0, 0, 0, 0.2) - - .body - position fixed - top 0 - left 0 - z-index 1026 - width 240px - height 100% - overflow auto - -webkit-overflow-scrolling touch - color #777 - background #fff - - .me - display block - margin 0 - padding 16px - - .avatar - display inline - max-width 64px - border-radius 32px - vertical-align middle - - .name - display block - margin 0 16px - position absolute - top 0 - left 80px - padding 0 - width calc(100% - 112px) - color #777 - line-height 96px - overflow hidden - text-overflow ellipsis - white-space nowrap - - ul - display block - margin 16px 0 - padding 0 - list-style none - - &:first-child - margin-top 0 - - li - display block - font-size 1em - line-height 1em - - a - display block - padding 0 20px - line-height 3rem - line-height calc(1rem + 30px) - color #777 - text-decoration none - - > [data-fa]:first-child - margin-right 0.5em - - > [data-fa].circle - margin-left 6px - font-size 10px - color $theme-color - - > [data-fa]:last-child - position absolute - top 0 - right 0 - padding 0 20px - font-size 1.2em - line-height calc(1rem + 30px) - color #ccc - - .about - margin 0 - padding 1em 0 - text-align center - font-size 0.8em - opacity 0.5 - - a - color #777 - -.nav-enter-active, -.nav-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.nav-enter, -.nav-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.back-enter-active, -.back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.back-enter, -.back-leave-active { - opacity: 0; -} - -</style> diff --git a/src/server/web/app/mobile/views/components/ui.vue b/src/server/web/app/mobile/views/components/ui.vue deleted file mode 100644 index 325ce9d40e..0000000000 --- a/src/server/web/app/mobile/views/components/ui.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> -<div class="mk-ui"> - <x-header> - <template slot="func"><slot name="func"></slot></template> - <slot name="header"></slot> - </x-header> - <x-nav :is-open="isDrawerOpening"/> - <div class="content"> - <slot></slot> - </div> - <mk-stream-indicator v-if="os.isSignedIn"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkNotify from './notify.vue'; -import XHeader from './ui.header.vue'; -import XNav from './ui.nav.vue'; - -export default Vue.extend({ - components: { - XHeader, - XNav - }, - props: ['title'], - data() { - return { - isDrawerOpening: false, - connection: null, - connectionId: null - }; - }, - mounted() { - if ((this as any).os.isSignedIn) { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('notification', this.onNotification); - } - }, - beforeDestroy() { - if ((this as any).os.isSignedIn) { - this.connection.off('notification', this.onNotification); - (this as any).os.stream.dispose(this.connectionId); - } - }, - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.connection.send({ - type: 'read_notification', - id: notification.id - }); - - (this as any).os.new(MkNotify, { - notification - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui - display flex - flex 1 - flex-direction column - padding-top 48px - - > .content - display flex - flex 1 - flex-direction column -</style> diff --git a/src/server/web/app/mobile/views/components/user-card.vue b/src/server/web/app/mobile/views/components/user-card.vue deleted file mode 100644 index ffa1100519..0000000000 --- a/src/server/web/app/mobile/views/components/user-card.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<div class="mk-user-card"> - <header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"> - <a :href="`/@${acct}`"> - <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> - </a> - </header> - <a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a> - <p class="username">@{{ acct }}</p> - <mk-follow-button :user="user"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-user-card - display inline-block - width 200px - text-align center - border-radius 8px - background #fff - - > header - display block - height 80px - background-color #ddd - background-size cover - background-position center - border-radius 8px 8px 0 0 - - > a - > img - position absolute - top 20px - left calc(50% - 40px) - width 80px - height 80px - border solid 2px #fff - border-radius 8px - - > .name - display block - margin 24px 0 0 0 - font-size 16px - color #555 - - > .username - margin 0 - font-size 15px - color #ccc - - > .mk-follow-button - display inline-block - margin 8px 0 16px 0 - -</style> diff --git a/src/server/web/app/mobile/views/components/user-preview.vue b/src/server/web/app/mobile/views/components/user-preview.vue deleted file mode 100644 index e51e4353d3..0000000000 --- a/src/server/web/app/mobile/views/components/user-preview.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> -<div class="mk-user-preview"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="main"> - <header> - <router-link class="name" :to="`/@${acct}`">{{ user.name }}</router-link> - <span class="username">@{{ acct }}</span> - </header> - <div class="body"> - <div class="description">{{ user.description }}</div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-user-preview - margin 0 - padding 16px - font-size 12px - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 10px 0 0 - - @media (min-width 500px) - margin-right 16px - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 6px - vertical-align bottom - - @media (min-width 500px) - width 58px - height 58px - border-radius 8px - - > .main - float left - width calc(100% - 58px) - - @media (min-width 500px) - width calc(100% - 74px) - - > header - @media (min-width 500px) - margin-bottom 2px - - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - text-align left - margin 0 0 0 8px - color #ccc - - > .body - - > .description - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 - -</style> diff --git a/src/server/web/app/mobile/views/components/user-timeline.vue b/src/server/web/app/mobile/views/components/user-timeline.vue deleted file mode 100644 index bd3e3d0c87..0000000000 --- a/src/server/web/app/mobile/views/components/user-timeline.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="mk-user-timeline"> - <mk-posts :posts="posts"> - <div class="init" v-if="fetching"> - %fa:spinner .pulse%%i18n:common.loading% - </div> - <div class="empty" v-if="!fetching && posts.length == 0"> - %fa:R comments% - {{ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-posts-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-posts%' }} - </div> - <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> - <span v-if="!moreFetching">%i18n:mobile.tags.mk-user-timeline.load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> - </mk-posts> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -const limit = 10; - -export default Vue.extend({ - props: ['user', 'withMedia'], - data() { - return { - fetching: true, - posts: [], - existMore: false, - moreFetching: false - }; - }, - mounted() { - (this as any).api('users/posts', { - userId: this.user.id, - withMedia: this.withMedia, - limit: limit + 1 - }).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - this.existMore = true; - } - this.posts = posts; - this.fetching = false; - this.$emit('loaded'); - }); - }, - methods: { - more() { - this.moreFetching = true; - (this as any).api('users/posts', { - userId: this.user.id, - withMedia: this.withMedia, - limit: limit + 1, - untilId: this.posts[this.posts.length - 1].id - }).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.posts = this.posts.concat(posts); - this.moreFetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-user-timeline - max-width 600px - margin 0 auto -</style> diff --git a/src/server/web/app/mobile/views/components/users-list.vue b/src/server/web/app/mobile/views/components/users-list.vue deleted file mode 100644 index b11e4549d6..0000000000 --- a/src/server/web/app/mobile/views/components/users-list.vue +++ /dev/null @@ -1,133 +0,0 @@ -<template> -<div class="mk-users-list"> - <nav> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:mobile.tags.mk-users-list.all%<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:mobile.tags.mk-users-list.known%<span>{{ youKnowCount }}</span></span> - </nav> - <div class="users" v-if="!fetching && users.length != 0"> - <mk-user-preview v-for="u in users" :user="u" :key="u.id"/> - </div> - <button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching"> - <span v-if="!moreFetching">%i18n:mobile.tags.mk-users-list.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%%i18n:common.loading%<mk-ellipsis/></p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['fetch', 'count', 'youKnowCount'], - data() { - return { - limit: 30, - mode: 'all', - fetching: true, - moreFetching: false, - users: [], - next: null - }; - }, - watch: { - mode() { - this._fetch(); - } - }, - mounted() { - this._fetch(() => { - this.$emit('loaded'); - }); - }, - methods: { - _fetch(cb?) { - this.fetching = true; - this.fetch(this.mode == 'iknow', this.limit, null, obj => { - this.users = obj.users; - this.next = obj.next; - this.fetching = false; - if (cb) cb(); - }); - }, - more() { - this.moreFetching = true; - this.fetch(this.mode == 'iknow', this.limit, this.next, obj => { - this.moreFetching = false; - this.users = this.users.concat(obj.users); - this.next = obj.next; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-users-list - - > nav - display flex - justify-content center - margin 0 auto - max-width 600px - border-bottom solid 1px rgba(0, 0, 0, 0.2) - - > span - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - color #657786 - border-bottom solid 2px transparent - - &[data-is-active] - font-weight bold - color $theme-color - border-color $theme-color - - > span - display inline-block - margin-left 4px - padding 2px 5px - font-size 12px - line-height 1 - color #fff - background rgba(0, 0, 0, 0.3) - border-radius 20px - - > .users - margin 8px auto - max-width 500px - width calc(100% - 16px) - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > * - border-bottom solid 1px rgba(0, 0, 0, 0.05) - - > .no - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/components/widget-container.vue b/src/server/web/app/mobile/views/components/widget-container.vue deleted file mode 100644 index 7319c90849..0000000000 --- a/src/server/web/app/mobile/views/components/widget-container.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="mk-widget-container" :class="{ naked, hideHeader: !showHeader }"> - <header v-if="showHeader"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - </header> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-widget-container - background #eee - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - overflow hidden - - &.hideHeader - background #fff - - &.naked - background transparent !important - box-shadow none !important - - > header - > .title - margin 0 - padding 8px 10px - font-size 15px - font-weight normal - color #465258 - background #fff - border-radius 8px 8px 0 0 - - > [data-fa] - margin-right 6px - - &:empty - display none - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - height 100% - font-size 15px - color #465258 - -</style> diff --git a/src/server/web/app/mobile/views/directives/index.ts b/src/server/web/app/mobile/views/directives/index.ts deleted file mode 100644 index 324e07596d..0000000000 --- a/src/server/web/app/mobile/views/directives/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue'; - -import userPreview from './user-preview'; - -Vue.directive('userPreview', userPreview); -Vue.directive('user-preview', userPreview); diff --git a/src/server/web/app/mobile/views/directives/user-preview.ts b/src/server/web/app/mobile/views/directives/user-preview.ts deleted file mode 100644 index 1a54abc20d..0000000000 --- a/src/server/web/app/mobile/views/directives/user-preview.ts +++ /dev/null @@ -1,2 +0,0 @@ -// nope -export default {}; diff --git a/src/server/web/app/mobile/views/pages/drive.vue b/src/server/web/app/mobile/views/pages/drive.vue deleted file mode 100644 index 200379f222..0000000000 --- a/src/server/web/app/mobile/views/pages/drive.vue +++ /dev/null @@ -1,107 +0,0 @@ -<template> -<mk-ui> - <span slot="header"> - <template v-if="folder">%fa:R folder-open%{{ folder.name }}</template> - <template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template> - <template v-if="!folder && !file">%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template> - </span> - <template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template> - <mk-drive - ref="browser" - :init-folder="initFolder" - :init-file="initFile" - :is-naked="true" - :top="48" - @begin-fetch="Progress.start()" - @fetched-mid="Progress.set(0.5)" - @fetched="Progress.done()" - @move-root="onMoveRoot" - @open-folder="onOpenFolder" - @open-file="onOpenFile" - /> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - data() { - return { - Progress, - folder: null, - file: null, - initFolder: null, - initFile: null - }; - }, - created() { - this.initFolder = this.$route.params.folder; - this.initFile = this.$route.params.file; - - window.addEventListener('popstate', this.onPopState); - }, - mounted() { - document.title = 'Misskey Drive'; - document.documentElement.style.background = '#fff'; - }, - beforeDestroy() { - window.removeEventListener('popstate', this.onPopState); - }, - methods: { - onPopState() { - if (this.$route.params.folder) { - (this.$refs as any).browser.cd(this.$route.params.folder, true); - } else if (this.$route.params.file) { - (this.$refs as any).browser.cf(this.$route.params.file, true); - } else { - (this.$refs as any).browser.goRoot(true); - } - }, - fn() { - (this.$refs as any).browser.openContextMenu(); - }, - onMoveRoot(silent) { - const title = 'Misskey Drive'; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive'); - } - - document.title = title; - - this.file = null; - this.folder = null; - }, - onOpenFolder(folder, silent) { - const title = folder.name + ' | Misskey Drive'; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive/folder/' + folder.id); - } - - document.title = title; - - this.file = null; - this.folder = folder; - }, - onOpenFile(file, silent) { - const title = file.name + ' | Misskey Drive'; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive/file/' + file.id); - } - - document.title = title; - - this.file = file; - this.folder = null; - } - } -}); -</script> - diff --git a/src/server/web/app/mobile/views/pages/followers.vue b/src/server/web/app/mobile/views/pages/followers.vue deleted file mode 100644 index 8c058eb4e6..0000000000 --- a/src/server/web/app/mobile/views/pages/followers.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<mk-ui> - <template slot="header" v-if="!fetching"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> - {{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }} - </template> - <mk-users-list - v-if="!fetching" - :fetch="fetchUsers" - :count="user.followersCount" - :you-know-count="user.followersYouKnowCount" - @loaded="onLoaded" - > - %i18n:mobile.tags.mk-user-followers.no-users% - </mk-users-list> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../common/user/parse-acct'; - -export default Vue.extend({ - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.documentElement.style.background = '#313a42'; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey'; - }); - }, - onLoaded() { - Progress.done(); - }, - fetchUsers(iknow, limit, cursor, cb) { - (this as any).api('users/followers', { - userId: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - } - } -}); -</script> diff --git a/src/server/web/app/mobile/views/pages/following.vue b/src/server/web/app/mobile/views/pages/following.vue deleted file mode 100644 index a73c9d1710..0000000000 --- a/src/server/web/app/mobile/views/pages/following.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<mk-ui> - <template slot="header" v-if="!fetching"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> - {{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) }} - </template> - <mk-users-list - v-if="!fetching" - :fetch="fetchUsers" - :count="user.followingCount" - :you-know-count="user.followingYouKnowCount" - @loaded="onLoaded" - > - %i18n:mobile.tags.mk-user-following.no-users% - </mk-users-list> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../common/user/parse-acct'; - -export default Vue.extend({ - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.documentElement.style.background = '#313a42'; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey'; - }); - }, - onLoaded() { - Progress.done(); - }, - fetchUsers(iknow, limit, cursor, cb) { - (this as any).api('users/following', { - userId: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - } - } -}); -</script> diff --git a/src/server/web/app/mobile/views/pages/home.vue b/src/server/web/app/mobile/views/pages/home.vue deleted file mode 100644 index be9101aa7f..0000000000 --- a/src/server/web/app/mobile/views/pages/home.vue +++ /dev/null @@ -1,259 +0,0 @@ -<template> -<mk-ui> - <span slot="header" @click="showTl = !showTl"> - <template v-if="showTl">%fa:home%タイムライン</template> - <template v-else>%fa:home%ウィジェット</template> - <span style="margin-left:8px"> - <template v-if="showTl">%fa:angle-down%</template> - <template v-else>%fa:angle-up%</template> - </span> - </span> - <template slot="func"> - <button @click="fn" v-if="showTl">%fa:pencil-alt%</button> - <button @click="customizing = !customizing" v-else>%fa:cog%</button> - </template> - <main> - <div class="tl"> - <mk-timeline @loaded="onLoaded" v-show="showTl"/> - </div> - <div class="widgets" v-show="!showTl"> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">プロフィール</option> - <option value="calendar">カレンダー</option> - <option value="activity">アクティビティ</option> - <option value="rss">RSSリーダー</option> - <option value="photo-stream">フォトストリーム</option> - <option value="slideshow">スライドショー</option> - <option value="version">バージョン</option> - <option value="access-log">アクセスログ</option> - <option value="server">サーバー情報</option> - <option value="donation">寄付のお願い</option> - <option value="nav">ナビゲーション</option> - <option value="tips">ヒント</option> - </select> - <button @click="addWidget">追加</button> - <p><a @click="hint">カスタマイズのヒント</a></p> - </header> - <x-draggable - :list="widgets" - :options="{ handle: '.handle', animation: 150 }" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> - </div> - </div> - </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"/> - </template> - </div> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import * as uuid from 'uuid'; -import Progress from '../../../common/scripts/loading'; -import getPostSummary from '../../../../../common/get-post-summary'; - -export default Vue.extend({ - components: { - XDraggable - }, - data() { - return { - connection: null, - connectionId: null, - unreadCount: 0, - showTl: true, - widgets: [], - customizing: false, - widgetAdderSelected: null - }; - }, - created() { - if ((this as any).os.i.account.clientSettings.mobile_home == null) { - Vue.set((this as any).os.i.account.clientSettings, 'mobile_home', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'donation', - id: 'e', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - this.widgets = (this as any).os.i.account.clientSettings.mobile_home; - this.saveHome(); - } else { - this.widgets = (this as any).os.i.account.clientSettings.mobile_home; - } - - this.$watch('os.i.account.clientSettings', i => { - this.widgets = (this as any).os.i.account.clientSettings.mobile_home; - }, { - deep: true - }); - }, - mounted() { - document.title = 'Misskey'; - document.documentElement.style.background = '#313a42'; - - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('post', this.onStreamPost); - this.connection.on('mobile_home_updated', this.onHomeUpdated); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - - Progress.start(); - }, - beforeDestroy() { - this.connection.off('post', this.onStreamPost); - this.connection.off('mobile_home_updated', this.onHomeUpdated); - (this as any).os.stream.dispose(this.connectionId); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, - methods: { - fn() { - (this as any).apis.post(); - }, - onLoaded() { - Progress.done(); - }, - onStreamPost(post) { - if (document.hidden && post.userId !== (this as any).os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getPostSummary(post)}`; - } - }, - onVisibilitychange() { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - }, - onHomeUpdated(data) { - if (data.home) { - (this as any).os.i.account.clientSettings.mobile_home = data.home; - this.widgets = data.home; - } else { - const w = (this as any).os.i.account.clientSettings.mobile_home.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).os.i.account.clientSettings.mobile_home; - } - } - }, - hint() { - alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); - }, - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - onWidgetSort() { - this.saveHome(); - }, - addWidget() { - const widget = { - 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(); - }, - saveHome() { - (this as any).os.i.account.clientSettings.mobile_home = this.widgets; - (this as any).api('i/update_mobile_home', { - home: this.widgets - }); - }, - warp() { - - } - } -}); -</script> - -<style lang="stylus" scoped> -main - - > .tl - > .mk-timeline - max-width 600px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - > .widgets - margin 0 auto - max-width 500px - - @media (min-width 500px) - padding 8px - - > header - padding 8px - background #fff - - .widget - margin 8px - - .customize-container - margin 8px - background #fff - - > header - line-height 32px - background #eee - - > .handle - padding 0 8px - - > .remove - position absolute - top 0 - right 0 - padding 0 8px - line-height 32px - - > div - padding 8px - - > * - pointer-events none - -</style> diff --git a/src/server/web/app/mobile/views/pages/index.vue b/src/server/web/app/mobile/views/pages/index.vue deleted file mode 100644 index 0ea47d913b..0000000000 --- a/src/server/web/app/mobile/views/pages/index.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<component :is="os.isSignedIn ? 'home' : 'welcome'"></component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Home from './home.vue'; -import Welcome from './welcome.vue'; - -export default Vue.extend({ - components: { - Home, - Welcome - } -}); -</script> diff --git a/src/server/web/app/mobile/views/pages/messaging-room.vue b/src/server/web/app/mobile/views/pages/messaging-room.vue deleted file mode 100644 index 193c41179c..0000000000 --- a/src/server/web/app/mobile/views/pages/messaging-room.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<mk-ui> - <span slot="header"> - <template v-if="user">%fa:R comments%{{ user.name }}</template> - <template v-else><mk-ellipsis/></template> - </span> - <mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import parseAcct from '../../../../../common/user/parse-acct'; - -export default Vue.extend({ - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - document.documentElement.style.background = '#fff'; - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`; - }); - } - } -}); -</script> - diff --git a/src/server/web/app/mobile/views/pages/messaging.vue b/src/server/web/app/mobile/views/pages/messaging.vue deleted file mode 100644 index e92068eda5..0000000000 --- a/src/server/web/app/mobile/views/pages/messaging.vue +++ /dev/null @@ -1,23 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:R comments%%i18n:mobile.tags.mk-messaging-page.message%</span> - <mk-messaging @navigate="navigate" :header-top="48"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; - -export default Vue.extend({ - mounted() { - document.title = 'Misskey %i18n:mobile.tags.mk-messaging-page.message%'; - document.documentElement.style.background = '#fff'; - }, - methods: { - navigate(user) { - (this as any).$router.push(`/i/messaging/${getAcct(user)}`); - } - } -}); -</script> diff --git a/src/server/web/app/mobile/views/pages/notifications.vue b/src/server/web/app/mobile/views/pages/notifications.vue deleted file mode 100644 index 6d45e22a9c..0000000000 --- a/src/server/web/app/mobile/views/pages/notifications.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span> - <template slot="func"><button @click="fn">%fa:check%</button></template> - <mk-notifications @fetched="onFetched"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - mounted() { - document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%'; - document.documentElement.style.background = '#313a42'; - - Progress.start(); - }, - methods: { - fn() { - const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%'); - if (!ok) return; - - (this as any).api('notifications/markAsRead_all'); - }, - onFetched() { - Progress.done(); - } - } -}); -</script> diff --git a/src/server/web/app/mobile/views/pages/othello.vue b/src/server/web/app/mobile/views/pages/othello.vue deleted file mode 100644 index e04e583c20..0000000000 --- a/src/server/web/app/mobile/views/pages/othello.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:gamepad%オセロ</span> - <mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - data() { - return { - fetching: false, - game: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.title = 'Misskey オセロ'; - document.documentElement.style.background = '#fff'; - }, - methods: { - fetch() { - if (this.$route.params.game == null) return; - - Progress.start(); - this.fetching = true; - - (this as any).api('othello/games/show', { - gameId: this.$route.params.game - }).then(game => { - this.game = game; - this.fetching = false; - - Progress.done(); - }); - }, - onGamed(game) { - history.pushState(null, null, '/othello/' + game.id); - } - } -}); -</script> diff --git a/src/server/web/app/mobile/views/pages/post.vue b/src/server/web/app/mobile/views/pages/post.vue deleted file mode 100644 index 49a4bfd9dc..0000000000 --- a/src/server/web/app/mobile/views/pages/post.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:R sticky-note%%i18n:mobile.tags.mk-post-page.title%</span> - <main v-if="!fetching"> - <a v-if="post.next" :href="post.next">%fa:angle-up%%i18n:mobile.tags.mk-post-page.next%</a> - <div> - <mk-post-detail :post="post"/> - </div> - <a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - data() { - return { - fetching: true, - post: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.title = 'Misskey'; - document.documentElement.style.background = '#313a42'; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - (this as any).api('posts/show', { - postId: this.$route.params.post - }).then(post => { - this.post = post; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - text-align center - - > div - margin 8px auto - padding 0 - max-width 500px - width calc(100% - 16px) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > a - display inline-block - - &:first-child - margin-top 8px - - @media (min-width 500px) - margin-top 16px - - &:last-child - margin-bottom 8px - - @media (min-width 500px) - margin-bottom 16px - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/pages/profile-setting.vue b/src/server/web/app/mobile/views/pages/profile-setting.vue deleted file mode 100644 index 15f9bc9b68..0000000000 --- a/src/server/web/app/mobile/views/pages/profile-setting.vue +++ /dev/null @@ -1,226 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:user%%i18n:mobile.tags.mk-profile-setting-page.title%</span> - <div :class="$style.content"> - <p>%fa:info-circle%%i18n:mobile.tags.mk-profile-setting.will-be-published%</p> - <div :class="$style.form"> - <div :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner"> - <img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/> - </div> - <label> - <p>%i18n:mobile.tags.mk-profile-setting.name%</p> - <input v-model="name" type="text"/> - </label> - <label> - <p>%i18n:mobile.tags.mk-profile-setting.location%</p> - <input v-model="location" type="text"/> - </label> - <label> - <p>%i18n:mobile.tags.mk-profile-setting.description%</p> - <textarea v-model="description"></textarea> - </label> - <label> - <p>%i18n:mobile.tags.mk-profile-setting.birthday%</p> - <input v-model="birthday" type="date"/> - </label> - <label> - <p>%i18n:mobile.tags.mk-profile-setting.avatar%</p> - <button @click="setAvatar" :disabled="avatarSaving">%i18n:mobile.tags.mk-profile-setting.set-avatar%</button> - </label> - <label> - <p>%i18n:mobile.tags.mk-profile-setting.banner%</p> - <button @click="setBanner" :disabled="bannerSaving">%i18n:mobile.tags.mk-profile-setting.set-banner%</button> - </label> - </div> - <button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - name: null, - location: null, - description: null, - birthday: null, - avatarSaving: false, - bannerSaving: false, - saving: false - }; - }, - created() { - this.name = (this as any).os.i.name; - this.location = (this as any).os.i.account.profile.location; - this.description = (this as any).os.i.description; - this.birthday = (this as any).os.i.account.profile.birthday; - }, - mounted() { - document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%'; - document.documentElement.style.background = '#313a42'; - }, - methods: { - setAvatar() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.avatarSaving = true; - - (this as any).api('i/update', { - avatarId: file.id - }).then(() => { - this.avatarSaving = false; - alert('%i18n:mobile.tags.mk-profile-setting.avatar-saved%'); - }); - }); - }, - setBanner() { - (this as any).apis.chooseDriveFile({ - multiple: false - }).then(file => { - this.bannerSaving = true; - - (this as any).api('i/update', { - bannerId: file.id - }).then(() => { - this.bannerSaving = false; - alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%'); - }); - }); - }, - save() { - this.saving = true; - - (this as any).api('i/update', { - name: this.name, - location: this.location || null, - description: this.description || null, - birthday: this.birthday || null - }).then(() => { - this.saving = false; - alert('%i18n:mobile.tags.mk-profile-setting.saved%'); - }); - } - } -}); -</script> - -<style lang="stylus" module> -@import '~const.styl' - -.content - margin 8px auto - max-width 500px - width calc(100% - 16px) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > p - display block - margin 0 0 8px 0 - padding 12px 16px - font-size 14px - color #79d4e6 - border solid 1px #71afbb - //color #276f86 - //background #f8ffff - //border solid 1px #a9d5de - border-radius 8px - - > [data-fa] - margin-right 6px - -.form - position relative - background #fff - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - border-radius 8px - - &:before - content "" - display block - position absolute - bottom -20px - left calc(50% - 10px) - border-top solid 10px rgba(0, 0, 0, 0.2) - border-right solid 10px transparent - border-bottom solid 10px transparent - border-left solid 10px transparent - - &:after - content "" - display block - position absolute - bottom -16px - left calc(50% - 8px) - border-top solid 8px #fff - border-right solid 8px transparent - border-bottom solid 8px transparent - border-left solid 8px transparent - - > div - height 128px - background-color #e4e4e4 - background-size cover - background-position center - border-radius 8px 8px 0 0 - - > img - position absolute - top 25px - left calc(50% - 40px) - width 80px - height 80px - border solid 2px #fff - border-radius 8px - - > label - display block - margin 0 - padding 16px - border-bottom solid 1px #eee - - &:last-of-type - border none - - > p:first-child - display block - margin 0 - padding 0 0 4px 0 - font-weight bold - color #2f3c42 - - > input[type="text"] - > textarea - display block - width 100% - padding 12px - font-size 16px - color #192427 - border solid 2px #ddd - border-radius 4px - - > textarea - min-height 80px - -.save - display block - margin 8px 0 0 0 - padding 16px - width 100% - font-size 16px - color $theme-color-foreground - background $theme-color - border-radius 8px - - &:disabled - opacity 0.7 - - > [data-fa] - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/pages/search.vue b/src/server/web/app/mobile/views/pages/search.vue deleted file mode 100644 index cbab504e3c..0000000000 --- a/src/server/web/app/mobile/views/pages/search.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:search% {{ q }}</span> - <main v-if="!fetching"> - <mk-posts :class="$style.posts" :posts="posts"> - <span v-if="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', q) }}</span> - <button v-if="existMore" @click="more" :disabled="fetching" slot="tail"> - <span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span> - <span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> - </mk-posts> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Progress from '../../../common/scripts/loading'; -import parse from '../../../common/scripts/parse-search-query'; - -const limit = 20; - -export default Vue.extend({ - data() { - return { - fetching: true, - existMore: false, - posts: [], - offset: 0 - }; - }, - watch: { - $route: 'fetch' - }, - computed: { - q(): string { - return this.$route.query.q; - } - }, - mounted() { - document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.q} | Misskey`; - document.documentElement.style.background = '#313a42'; - - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - Progress.start(); - - (this as any).api('posts/search', Object.assign({ - limit: limit + 1 - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - this.existMore = true; - } - this.posts = posts; - this.fetching = false; - Progress.done(); - }); - }, - more() { - this.offset += limit; - (this as any).api('posts/search', Object.assign({ - limit: limit + 1, - offset: this.offset - }, parse(this.q))).then(posts => { - if (posts.length == limit + 1) { - posts.pop(); - } else { - this.existMore = false; - } - this.posts = this.posts.concat(posts); - }); - } - } -}); -</script> - -<style lang="stylus" module> -.posts - margin 8px auto - max-width 500px - width calc(100% - 16px) - background #fff - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) -</style> diff --git a/src/server/web/app/mobile/views/pages/selectdrive.vue b/src/server/web/app/mobile/views/pages/selectdrive.vue deleted file mode 100644 index 3480a0d103..0000000000 --- a/src/server/web/app/mobile/views/pages/selectdrive.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> -<div class="mk-selectdrive"> - <header> - <h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="upload" @click="upload">%fa:upload%</button> - <button v-if="multiple" class="ok" @click="ok">%fa:check%</button> - </header> - <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="42"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%'; - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - close() { - window.close(); - }, - ok() { - window.opener.cb(this.multiple ? this.files : this.files[0]); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-selectdrive - width 100% - height 100% - background #fff - - > header - position fixed - top 0 - left 0 - width 100% - z-index 1000 - background #fff - box-shadow 0 1px rgba(0, 0, 0, 0.1) - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .upload - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - top 42px - -</style> diff --git a/src/server/web/app/mobile/views/pages/settings.vue b/src/server/web/app/mobile/views/pages/settings.vue deleted file mode 100644 index a945a21c5c..0000000000 --- a/src/server/web/app/mobile/views/pages/settings.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<mk-ui> - <span slot="header">%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%</span> - <div :class="$style.content"> - <p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + os.i.name + '</b>')"></p> - <ul> - <li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</router-link></li> - <li><router-link to="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</router-link></li> - <li><router-link to="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</router-link></li> - <li><router-link to="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</router-link></li> - </ul> - <ul> - <li><a @click="signout">%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li> - </ul> - <p><small>ver {{ version }} ({{ codename }})</small></p> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { version, codename } from '../../../config'; - -export default Vue.extend({ - data() { - return { - version, - codename - }; - }, - mounted() { - document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%'; - document.documentElement.style.background = '#313a42'; - }, - methods: { - signout() { - (this as any).os.signout(); - } - } -}); -</script> - -<style lang="stylus" module> -.content - - > p - display block - margin 24px - text-align center - color #cad2da - - > ul - $radius = 8px - - display block - margin 16px auto - padding 0 - max-width 500px - width calc(100% - 32px) - list-style none - background #fff - border solid 1px rgba(0, 0, 0, 0.2) - border-radius $radius - - > li - display block - border-bottom solid 1px #ddd - - &:hover - background rgba(0, 0, 0, 0.1) - - &:first-child - border-top-left-radius $radius - border-top-right-radius $radius - - &:last-child - border-bottom-left-radius $radius - border-bottom-right-radius $radius - border-bottom none - - > a - $height = 48px - - display block - position relative - padding 0 16px - line-height $height - color #4d635e - - > [data-fa]:nth-of-type(1) - margin-right 4px - - > [data-fa]:nth-of-type(2) - display block - position absolute - top 0 - right 8px - z-index 1 - padding 0 20px - font-size 1.2em - line-height $height - -</style> diff --git a/src/server/web/app/mobile/views/pages/signup.vue b/src/server/web/app/mobile/views/pages/signup.vue deleted file mode 100644 index 9dc07a4b86..0000000000 --- a/src/server/web/app/mobile/views/pages/signup.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="signup"> - <h1>Misskeyをはじめる</h1> - <p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p> - <div class="form"> - <p>新規登録</p> - <div> - <mk-signup/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - mounted() { - document.documentElement.style.background = '#293946'; - } -}); -</script> - -<style lang="stylus" scoped> -.signup - padding 16px - margin 0 auto - max-width 500px - - h1 - margin 0 - padding 8px - font-size 1.5em - font-weight normal - color #c3c6ca - - & + p - margin 0 0 16px 0 - padding 0 8px 0 8px - color #949fa9 - - .form - background #fff - border solid 1px rgba(0, 0, 0, 0.2) - border-radius 8px - overflow hidden - - > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd - - > div - padding 16px - -</style> diff --git a/src/server/web/app/mobile/views/pages/user.vue b/src/server/web/app/mobile/views/pages/user.vue deleted file mode 100644 index 114decb8e4..0000000000 --- a/src/server/web/app/mobile/views/pages/user.vue +++ /dev/null @@ -1,247 +0,0 @@ -<template> -<mk-ui> - <span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span> - <main v-if="!fetching"> - <header> - <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div> - <div class="body"> - <div class="top"> - <a class="avatar"> - <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> - </a> - <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> - </div> - <div class="title"> - <h1>{{ user.name }}</h1> - <span class="username">@{{ acct }}</span> - <span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span> - </div> - <div class="description">{{ user.description }}</div> - <div class="info"> - <p class="location" v-if="user.host === null && user.account.profile.location"> - %fa:map-marker%{{ user.account.profile.location }} - </p> - <p class="birthday" v-if="user.host === null && user.account.profile.birthday"> - %fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳) - </p> - </div> - <div class="status"> - <a> - <b>{{ user.postsCount | number }}</b> - <i>%i18n:mobile.tags.mk-user.posts%</i> - </a> - <a :href="`@${acct}/following`"> - <b>{{ user.followingCount | number }}</b> - <i>%i18n:mobile.tags.mk-user.following%</i> - </a> - <a :href="`@${acct}/followers`"> - <b>{{ user.followersCount | number }}</b> - <i>%i18n:mobile.tags.mk-user.followers%</i> - </a> - </div> - </div> - </header> - <nav> - <div class="nav-container"> - <a :data-is-active=" page == 'home' " @click="page = 'home'">%i18n:mobile.tags.mk-user.overview%</a> - <a :data-is-active=" page == 'posts' " @click="page = 'posts'">%i18n:mobile.tags.mk-user.timeline%</a> - <a :data-is-active=" page == 'media' " @click="page = 'media'">%i18n:mobile.tags.mk-user.media%</a> - </div> - </nav> - <div class="body"> - <x-home v-if="page == 'home'" :user="user"/> - <mk-user-timeline v-if="page == 'posts'" :user="user"/> - <mk-user-timeline v-if="page == 'media'" :user="user" with-media/> - </div> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as age from 's-age'; -import getAcct from '../../../../../common/user/get-acct'; -import getAcct from '../../../../../common/user/parse-acct'; -import Progress from '../../../common/scripts/loading'; -import XHome from './user/home.vue'; - -export default Vue.extend({ - components: { - XHome - }, - data() { - return { - fetching: true, - user: null, - page: 'home' - }; - }, - computed: { - acct() { - return this.getAcct(this.user); - }, - age(): number { - return age(this.user.account.profile.birthday); - } - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.documentElement.style.background = '#313a42'; - }, - methods: { - fetch() { - Progress.start(); - - (this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - Progress.done(); - document.title = user.name + ' | Misskey'; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -main - > header - - > .banner - padding-bottom 33.3% - background-color #1b1b1b - background-size cover - background-position center - - > .body - padding 12px - margin 0 auto - max-width 600px - - > .top - &:after - content '' - display block - clear both - - > .avatar - display block - float left - width 25% - height 40px - - > img - display block - position absolute - left -2px - bottom -2px - width 100% - border 3px solid #313a42 - border-radius 6px - - @media (min-width 500px) - left -4px - bottom -4px - border 4px solid #313a42 - border-radius 12px - - > .mk-follow-button - float right - height 40px - - > .title - margin 8px 0 - - > h1 - margin 0 - line-height 22px - font-size 20px - color #fff - - > .username - display inline-block - line-height 20px - font-size 16px - font-weight bold - color #657786 - - > .followed - margin-left 8px - padding 2px 4px - font-size 12px - color #657786 - background #f8f8f8 - border-radius 4px - - > .description - margin 8px 0 - color #fff - - > .info - margin 8px 0 - - > p - display inline - margin 0 16px 0 0 - color #a9b9c1 - - > i - margin-right 4px - - > .status - > a - color #657786 - - &:not(:last-child) - margin-right 16px - - > b - margin-right 4px - font-size 16px - color #fff - - > i - font-size 14px - - > nav - position sticky - top 48px - box-shadow 0 4px 4px rgba(0, 0, 0, 0.3) - background-color #313a42 - z-index 1 - > .nav-container - display flex - justify-content center - margin 0 auto - max-width 600px - - > a - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - text-decoration none - color #657786 - border-bottom solid 2px transparent - - &[data-is-active] - font-weight bold - color $theme-color - border-color $theme-color - - > .body - padding 8px - - @media (min-width 500px) - padding 16px - -</style> diff --git a/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue deleted file mode 100644 index 8c84d2dbba..0000000000 --- a/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<div class="root followers-you-know"> - <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> - <div v-if="!fetching && users.length > 0"> - <a v-for="user in users" :key="user.id" :href="`/@${getAcct(user)}`"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name"/> - </a> - </div> - <p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - data() { - return { - fetching: true, - users: [] - }; - }, - methods: { - getAcct - }, - mounted() { - (this as any).api('users/followers', { - userId: this.user.id, - iknow: true, - limit: 30 - }).then(res => { - this.fetching = false; - this.users = res.users; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.followers-you-know - - > div - padding 4px - - > a - display inline-block - margin 4px - - > img - width 48px - height 48px - vertical-align bottom - border-radius 100% - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/pages/user/home.friends.vue b/src/server/web/app/mobile/views/pages/user/home.friends.vue deleted file mode 100644 index 469781abb9..0000000000 --- a/src/server/web/app/mobile/views/pages/user/home.friends.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="root friends"> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p> - <div v-if="!fetching && users.length > 0"> - <mk-user-card v-for="user in users" :key="user.id" :user="user"/> - </div> - <p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - data() { - return { - fetching: true, - users: [] - }; - }, - mounted() { - (this as any).api('users/get_frequently_replied_users', { - userId: this.user.id - }).then(res => { - this.users = res.map(x => x.user); - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.friends - > div - overflow-x scroll - -webkit-overflow-scrolling touch - white-space nowrap - padding 8px - - > .mk-user-card - &:not(:last-child) - margin-right 8px - - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/pages/user/home.photos.vue b/src/server/web/app/mobile/views/pages/user/home.photos.vue deleted file mode 100644 index f703f8a740..0000000000 --- a/src/server/web/app/mobile/views/pages/user/home.photos.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div class="root photos"> - <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p> - <div class="stream" v-if="!fetching && images.length > 0"> - <a v-for="image in images" - class="img" - :style="`background-image: url(${image.media.url}?thumbnail&size=256)`" - :href="`/@${getAcct(image.post.user)}/${image.post.id}`" - ></a> - </div> - <p class="empty" v-if="!fetching && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../../common/user/get-acct'; - -export default Vue.extend({ - props: ['user'], - data() { - return { - fetching: true, - images: [] - }; - }, - methods: { - getAcct - }, - mounted() { - (this as any).api('users/posts', { - userId: this.user.id, - withMedia: true, - limit: 6 - }).then(posts => { - posts.forEach(post => { - post.media.forEach(media => { - if (this.images.length < 9) this.images.push({ - post, - media - }); - }); - }); - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.photos - - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - > .img - flex 1 1 33% - width 33% - height 80px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - border-radius 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - -</style> - diff --git a/src/server/web/app/mobile/views/pages/user/home.posts.vue b/src/server/web/app/mobile/views/pages/user/home.posts.vue deleted file mode 100644 index 654f7f63e0..0000000000 --- a/src/server/web/app/mobile/views/pages/user/home.posts.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="root posts"> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p> - <div v-if="!fetching && posts.length > 0"> - <mk-post-card v-for="post in posts" :key="post.id" :post="post"/> - </div> - <p class="empty" v-if="!fetching && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - data() { - return { - fetching: true, - posts: [] - }; - }, - mounted() { - (this as any).api('users/posts', { - userId: this.user.id - }).then(posts => { - this.posts = posts; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.posts - - > div - overflow-x scroll - -webkit-overflow-scrolling touch - white-space nowrap - padding 8px - - > * - vertical-align top - - &:not(:last-child) - margin-right 8px - - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > i - margin-right 4px - -</style> diff --git a/src/server/web/app/mobile/views/pages/user/home.vue b/src/server/web/app/mobile/views/pages/user/home.vue deleted file mode 100644 index 1afcd1f5ba..0000000000 --- a/src/server/web/app/mobile/views/pages/user/home.vue +++ /dev/null @@ -1,94 +0,0 @@ -<template> -<div class="root home"> - <mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/> - <section class="recent-posts"> - <h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2> - <div> - <x-posts :user="user"/> - </div> - </section> - <section class="images"> - <h2>%fa:image%%i18n:mobile.tags.mk-user-overview.images%</h2> - <div> - <x-photos :user="user"/> - </div> - </section> - <section class="activity"> - <h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2> - <div> - <mk-activity :user="user"/> - </div> - </section> - <section class="frequently-replied-users"> - <h2>%fa:users%%i18n:mobile.tags.mk-user-overview.frequently-replied-users%</h2> - <div> - <x-friends :user="user"/> - </div> - </section> - <section class="followers-you-know" v-if="os.isSignedIn && os.i.id !== user.id"> - <h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2> - <div> - <x-followers-you-know :user="user"/> - </div> - </section> - <p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XPosts from './home.posts.vue'; -import XPhotos from './home.photos.vue'; -import XFriends from './home.friends.vue'; -import XFollowersYouKnow from './home.followers-you-know.vue'; - -export default Vue.extend({ - components: { - XPosts, - XPhotos, - XFriends, - XFollowersYouKnow - }, - props: ['user'] -}); -</script> - -<style lang="stylus" scoped> -.root.home - max-width 600px - margin 0 auto - - > .mk-post-detail - margin 0 0 8px 0 - - > section - background #eee - border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) - - &:not(:last-child) - margin-bottom 8px - - > h2 - margin 0 - padding 8px 10px - font-size 15px - font-weight normal - color #465258 - background #fff - border-radius 8px 8px 0 0 - - > i - margin-right 6px - - > .activity - > div - padding 8px - - > p - display block - margin 16px - text-align center - color #cad2da - -</style> diff --git a/src/server/web/app/mobile/views/pages/welcome.vue b/src/server/web/app/mobile/views/pages/welcome.vue deleted file mode 100644 index 17cdf93065..0000000000 --- a/src/server/web/app/mobile/views/pages/welcome.vue +++ /dev/null @@ -1,206 +0,0 @@ -<template> -<div class="welcome"> - <h1><b>Misskey</b>へようこそ</h1> - <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p> - <div class="form"> - <p>%fa:lock% ログイン</p> - <div> - <form @submit.prevent="onSubmit"> - <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> - <input v-model="password" type="password" placeholder="パスワード" required/> - <input v-if="user && user.account.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> - <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> - </form> - <div> - <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> - </div> - </div> - </div> - <div class="tl"> - <p>%fa:comments R% タイムラインを見てみる</p> - <mk-welcome-timeline/> - </div> - <div class="users"> - <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - </div> - <footer> - <small>{{ copyright }}</small> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { apiUrl, copyright } from '../../../config'; - -export default Vue.extend({ - data() { - return { - signing: false, - user: null, - username: '', - password: '', - token: '', - apiUrl, - copyright, - users: [] - }; - }, - mounted() { - (this as any).api('users', { - sort: '+follower', - limit: 20 - }).then(users => { - this.users = users; - }); - }, - methods: { - onUsernameChange() { - (this as any).api('users/show', { - username: this.username - }).then(user => { - this.user = user; - }); - }, - onSubmit() { - this.signing = true; - - (this as any).api('signin', { - username: this.username, - password: this.password, - token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined - }).then(() => { - location.reload(); - }).catch(() => { - alert('something happened'); - this.signing = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.welcome - padding 16px - margin 0 auto - max-width 500px - - h1 - margin 0 - padding 8px - font-size 1.5em - font-weight normal - color #cacac3 - - & + p - margin 0 0 16px 0 - padding 0 8px 0 8px - color #949fa9 - - .form - margin-bottom 16px - background #fff - border solid 1px rgba(0, 0, 0, 0.2) - border-radius 8px - overflow hidden - - > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd - - > div - - > form - padding 16px - border-bottom solid 1px #ddd - - input - display block - padding 12px - margin 0 0 16px 0 - width 100% - font-size 1em - color rgba(0, 0, 0, 0.7) - background #fff - outline none - border solid 1px #ddd - border-radius 4px - - button - display block - width 100% - padding 10px - margin 0 - color #333 - font-size 1em - text-align center - text-decoration none - text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) - background-image linear-gradient(#fafafa, #eaeaea) - border 1px solid #ddd - border-bottom-color #cecece - border-radius 4px - - &:active - background-color #767676 - background-image none - border-color #444 - box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) - - > div - padding 16px - text-align center - - > .tl - background #fff - border solid 1px rgba(0, 0, 0, 0.2) - border-radius 8px - overflow hidden - - > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd - - > .mk-welcome-timeline - max-height 300px - overflow auto - - > .users - margin 12px 0 0 0 - - > * - display inline-block - margin 4px - - > * - display inline-block - width 38px - height 38px - vertical-align top - border-radius 6px - - > footer - text-align center - color #fff - - > small - display block - margin 16px 0 0 0 - opacity 0.7 - -</style> - -<style lang="stylus"> -html -body - background linear-gradient(to bottom, #1e1d65, #bd6659) -</style> diff --git a/src/server/web/app/mobile/views/widgets/activity.vue b/src/server/web/app/mobile/views/widgets/activity.vue deleted file mode 100644 index 48dcafb3ed..0000000000 --- a/src/server/web/app/mobile/views/widgets/activity.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<div class="mkw-activity"> - <mk-widget-container :show-header="!props.compact"> - <template slot="header">%fa:chart-bar%アクティビティ</template> - <div :class="$style.body"> - <mk-activity :user="os.i"/> - </div> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; - -export default define({ - name: 'activity', - props: () => ({ - compact: false - }) -}).extend({ - methods: { - func() { - this.props.compact = !this.props.compact; - } - } -}); -</script> - -<style lang="stylus" module> -.body - padding 8px -</style> diff --git a/src/server/web/app/mobile/views/widgets/index.ts b/src/server/web/app/mobile/views/widgets/index.ts deleted file mode 100644 index 4de912b64c..0000000000 --- a/src/server/web/app/mobile/views/widgets/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Vue from 'vue'; - -import wActivity from './activity.vue'; -import wProfile from './profile.vue'; - -Vue.component('mkw-activity', wActivity); -Vue.component('mkw-profile', wProfile); diff --git a/src/server/web/app/mobile/views/widgets/profile.vue b/src/server/web/app/mobile/views/widgets/profile.vue deleted file mode 100644 index f1d283e45a..0000000000 --- a/src/server/web/app/mobile/views/widgets/profile.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="mkw-profile"> - <mk-widget-container> - <div :class="$style.banner" - :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''" - ></div> - <img :class="$style.avatar" - :src="`${os.i.avatarUrl}?thumbnail&size=96`" - alt="avatar" - /> - <router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link> - </mk-widget-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'profile' -}); -</script> - -<style lang="stylus" module> -.banner - height 100px - background-color #f5f5f5 - background-size cover - background-position center - cursor pointer - -.banner:before - content "" - display block - width 100% - height 100% - background rgba(0, 0, 0, 0.5) - -.avatar - display block - position absolute - width 58px - height 58px - margin 0 - vertical-align bottom - top ((100px - 58px) / 2) - left ((100px - 58px) / 2) - border none - border-radius 100% - box-shadow 0 0 16px rgba(0, 0, 0, 0.5) - -.name - display block - position absolute - top 0 - left 92px - margin 0 - line-height 100px - color #fff - font-weight bold - text-shadow 0 0 8px rgba(0, 0, 0, 0.5) - -</style> diff --git a/src/server/web/app/reset.styl b/src/server/web/app/reset.styl deleted file mode 100644 index 10bd3113a2..0000000000 --- a/src/server/web/app/reset.styl +++ /dev/null @@ -1,32 +0,0 @@ -input:not([type]) -input[type='text'] -input[type='password'] -input[type='search'] -input[type='email'] -textarea -button -progress - -webkit-appearance none - -moz-appearance none - appearance none - box-shadow none - -textarea - font-family sans-serif - -button - margin 0 - background transparent - border none - cursor pointer - color inherit - - * - pointer-events none - - &[disabled] - cursor default - -pre - overflow auto - white-space pre diff --git a/src/server/web/app/safe.js b/src/server/web/app/safe.js deleted file mode 100644 index 2fd5361725..0000000000 --- a/src/server/web/app/safe.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * ブラウザの検証 - */ - -// Detect an old browser -if (!('fetch' in window)) { - alert( - 'お使いのブラウザが古いためMisskeyを動作させることができません。' + - 'バージョンを最新のものに更新するか、別のブラウザをお試しください。' + - '\n\n' + - 'Your browser seems outdated. ' + - 'To run Misskey, please update your browser to latest version or try other browsers.'); -} - -// Detect Edge -if (navigator.userAgent.toLowerCase().indexOf('edge') != -1) { - alert( - '現在、お使いのブラウザ(Microsoft Edge)ではMisskeyは正しく動作しません。' + - 'サポートしているブラウザ: Google Chrome, Mozilla Firefox, Apple Safari など' + - '\n\n' + - 'Currently, Misskey cannot run correctly on your browser (Microsoft Edge). ' + - 'Supported browsers: Google Chrome, Mozilla Firefox, Apple Safari, etc'); -} - -// Check whether cookie enabled -if (!navigator.cookieEnabled) { - alert( - 'Misskeyを利用するにはCookieを有効にしてください。' + - '\n\n' + - 'To use Misskey, please enable Cookie.'); -} diff --git a/src/server/web/app/stats/style.styl b/src/server/web/app/stats/style.styl deleted file mode 100644 index 5ae230ea56..0000000000 --- a/src/server/web/app/stats/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -html - color #456267 - background #fff - -body - margin 0 - padding 0 diff --git a/src/server/web/app/stats/tags/index.tag b/src/server/web/app/stats/tags/index.tag deleted file mode 100644 index 63fdd24044..0000000000 --- a/src/server/web/app/stats/tags/index.tag +++ /dev/null @@ -1,209 +0,0 @@ -<mk-index> - <h1>Misskey<i>Statistics</i></h1> - <main v-if="!initializing"> - <mk-users stats={ stats }/> - <mk-posts stats={ stats }/> - </main> - <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> - <style lang="stylus" scoped> - :scope - display block - margin 0 auto - padding 0 16px - max-width 700px - - > h1 - margin 0 - padding 24px 0 0 0 - font-size 24px - font-weight normal - - > i - font-style normal - color #f43b16 - - > main - > * - margin 24px 0 - padding-top 24px - border-top solid 1px #eee - - > h2 - margin 0 0 12px 0 - font-size 18px - font-weight normal - - > footer - margin 24px 0 - text-align center - - > a - color #546567 - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - - this.on('mount', () => { - this.$root.$data.os.api('stats').then(stats => { - this.update({ - initializing: false, - stats - }); - }); - }); - </script> -</mk-index> - -<mk-posts> - <h2>%i18n:stats.posts-count% <b>{ stats.postsCount }</b></h2> - <mk-posts-chart v-if="!initializing" data={ data }/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - this.stats = this.opts.stats; - - this.on('mount', () => { - this.$root.$data.os.api('aggregation/posts', { - limit: 365 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - </script> -</mk-posts> - -<mk-users> - <h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2> - <mk-users-chart v-if="!initializing" data={ data }/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - this.stats = this.opts.stats; - - this.on('mount', () => { - this.$root.$data.os.api('aggregation/users', { - limit: 365 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - </script> -</mk-users> - -<mk-posts-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title> - <polyline - riot-points={ pointsPost } - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - riot-points={ pointsReply } - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - riot-points={ pointsRepost } - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - riot-points={ pointsTotal } - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - this.viewBoxX = 365; - this.viewBoxY = 80; - - this.data = this.opts.data.reverse(); - this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - pointsPost: this.data.map((d, i) => `${i},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '), - pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '), - pointsRepost: this.data.map((d, i) => `${i},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '), - pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ') - }); - }; - </script> -</mk-posts-chart> - -<mk-users-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <polyline - riot-points={ createdPoints } - fill="none" - stroke-width="1" - stroke="#1cde84"/> - <polyline - riot-points={ totalPoints } - fill="none" - stroke-width="1" - stroke="#555"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - this.viewBoxX = 365; - this.viewBoxY = 80; - - this.data = this.opts.data.reverse(); - const totalPeak = Math.max.apply(null, this.data.map(d => d.total)); - const createdPeak = Math.max.apply(null, this.data.map(d => d.created)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - totalPoints: this.data.map((d, i) => `${i},${(1 - (d.total / totalPeak)) * this.viewBoxY}`).join(' '), - createdPoints: this.data.map((d, i) => `${i},${(1 - (d.created / createdPeak)) * this.viewBoxY}`).join(' ') - }); - }; - </script> -</mk-users-chart> diff --git a/src/server/web/app/stats/tags/index.ts b/src/server/web/app/stats/tags/index.ts deleted file mode 100644 index f41151949f..0000000000 --- a/src/server/web/app/stats/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./index.tag'); diff --git a/src/server/web/app/status/style.styl b/src/server/web/app/status/style.styl deleted file mode 100644 index 5ae230ea56..0000000000 --- a/src/server/web/app/status/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -html - color #456267 - background #fff - -body - margin 0 - padding 0 diff --git a/src/server/web/app/status/tags/index.tag b/src/server/web/app/status/tags/index.tag deleted file mode 100644 index 899467097a..0000000000 --- a/src/server/web/app/status/tags/index.tag +++ /dev/null @@ -1,201 +0,0 @@ -<mk-index> - <h1>Misskey<i>Status</i></h1> - <p>%fa:info-circle%%i18n:status.all-systems-maybe-operational%</p> - <main> - <mk-cpu-usage connection={ connection }/> - <mk-mem-usage connection={ connection }/> - </main> - <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> - <style lang="stylus" scoped> - :scope - display block - margin 0 auto - padding 0 16px - max-width 700px - - > h1 - margin 0 - padding 24px 0 16px 0 - font-size 24px - font-weight normal - - > [data-fa] - font-style normal - color #f43b16 - - > p - display block - margin 0 - padding 12px 16px - background #eaf4ef - //border solid 1px #99ccb2 - border-radius 4px - - > [data-fa] - margin-right 5px - - > main - > * - margin 24px 0 - - > h2 - margin 0 0 12px 0 - font-size 18px - font-weight normal - - > footer - margin 24px 0 - text-align center - - > a - color #546567 - </style> - <script lang="typescript"> - import Connection from '../../common/scripts/streaming/server-stream'; - - this.mixin('api'); - - this.initializing = true; - this.connection = new Connection(); - - this.on('mount', () => { - this.$root.$data.os.api('meta').then(meta => { - this.update({ - initializing: false, - meta - }); - }); - }); - - this.on('unmount', () => { - this.connection.close(); - }); - - </script> -</mk-index> - -<mk-cpu-usage> - <h2>CPU <b>{ percentage }%</b></h2> - <mk-line-chart ref="chart"/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - this.$refs.chart.addData(1 - stats.cpu_usage); - - const percentage = (stats.cpu_usage * 100).toFixed(0); - - this.update({ - percentage - }); - }; - </script> -</mk-cpu-usage> - -<mk-mem-usage> - <h2>MEM <b>{ percentage }%</b></h2> - <mk-line-chart ref="chart"/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - stats.mem.used = stats.mem.total - stats.mem.free; - this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total)); - - const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0); - - this.update({ - percentage - }); - }; - </script> -</mk-mem-usage> - -<mk-line-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <defs> - <linearGradient id={ gradientId } x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="rgba(244, 59, 22, 0)"></stop> - <stop offset="100%" stop-color="#f43b16"></stop> - </linearGradient> - <mask id={ maskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> - <polygon - riot-points={ polygonPoints } - fill="#fff" - fill-opacity="0.5"/> - </mask> - </defs> - <line x1="0" y1="0" riot-x2={ viewBoxX } y2="0" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="25%" riot-x2={ viewBoxX } y2="25%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="50%" riot-x2={ viewBoxX } y2="50%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="75%" riot-x2={ viewBoxX } y2="75%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="100%" riot-x2={ viewBoxX } y2="100%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <rect - x="-1" y="-1" - riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } - style="stroke: none; fill: url(#{ gradientId }); mask: url(#{ maskId })"/> - <polyline - riot-points={ polylinePoints } - fill="none" - stroke="#f43b16" - stroke-width="0.5"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - padding 16px - border-radius 8px - background #1c2531 - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - import uuid from 'uuid'; - - this.viewBoxX = 100; - this.viewBoxY = 30; - this.data = []; - this.gradientId = uuid(); - this.maskId = uuid(); - - this.addData = data => { - this.data.push(data); - if (this.data.length > 100) this.data.shift(); - - const polylinePoints = this.data.map((d, i) => `${this.viewBoxX - ((this.data.length - 1) - i)},${d * this.viewBoxY}`).join(' '); - const polygonPoints = `${this.viewBoxX - (this.data.length - 1)},${ this.viewBoxY } ${ polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - this.update({ - polylinePoints, - polygonPoints - }); - }; - </script> -</mk-line-chart> diff --git a/src/server/web/app/status/tags/index.ts b/src/server/web/app/status/tags/index.ts deleted file mode 100644 index f41151949f..0000000000 --- a/src/server/web/app/status/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./index.tag'); diff --git a/src/server/web/app/sw.js b/src/server/web/app/sw.js deleted file mode 100644 index 669703b16c..0000000000 --- a/src/server/web/app/sw.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Service Worker - */ - -import composeNotification from './common/scripts/compose-notification'; - -// キャッシュするリソース -const cachee = [ - '/' -]; - -// インストールされたとき -self.addEventListener('install', ev => { - console.info('installed'); - - ev.waitUntil(Promise.all([ - self.skipWaiting(), // Force activate - caches.open(_VERSION_).then(cache => cache.addAll(cachee)) // Cache - ])); -}); - -// アクティベートされたとき -self.addEventListener('activate', ev => { - // Clean up old caches - ev.waitUntil( - caches.keys().then(keys => Promise.all( - keys - .filter(key => key != _VERSION_) - .map(key => caches.delete(key)) - )) - ); -}); - -// リクエストが発生したとき -self.addEventListener('fetch', ev => { - ev.respondWith( - // キャッシュがあるか確認してあればそれを返す - caches.match(ev.request).then(response => - response || fetch(ev.request) - ) - ); -}); - -// プッシュ通知を受け取ったとき -self.addEventListener('push', ev => { - console.log('pushed'); - - // クライアント取得 - ev.waitUntil(self.clients.matchAll({ - includeUncontrolled: true - }).then(clients => { - // クライアントがあったらストリームに接続しているということなので通知しない - if (clients.length != 0) return; - - const { type, body } = ev.data.json(); - - console.log(type, body); - - const n = composeNotification(type, body); - return self.registration.showNotification(n.title, { - body: n.body, - icon: n.icon, - }); - })); -}); - -self.addEventListener('message', ev => { - if (ev.data == 'clear') { - caches.keys().then(keys => keys.forEach(key => caches.delete(key))); - } -}); diff --git a/src/server/web/app/tsconfig.json b/src/server/web/app/tsconfig.json deleted file mode 100644 index e31b52dab1..0000000000 --- a/src/server/web/app/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": false, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "es2017", - "module": "commonjs", - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": false - }, - "compileOnSave": false, - "include": [ - "./**/*.ts" - ] -} diff --git a/src/server/web/app/v.d.ts b/src/server/web/app/v.d.ts deleted file mode 100644 index 8f3a240d80..0000000000 --- a/src/server/web/app/v.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.vue" { - import Vue from 'vue'; - export default Vue; -} diff --git a/src/server/web/assets/404.js b/src/server/web/assets/404.js deleted file mode 100644 index f897f0db6b..0000000000 --- a/src/server/web/assets/404.js +++ /dev/null @@ -1,21 +0,0 @@ -const yn = window.confirm( - 'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?'); - -if (yn) { - // Clear cache (serive worker) - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => registration.unregister()); - }); - } catch (e) { - console.error(e); - } - - localStorage.removeItem('v'); - - location.reload(true); -} else { - alert('問題が解決しない場合はサーバー管理者までお問い合せください。'); -} diff --git a/src/server/web/assets/code-highlight.css b/src/server/web/assets/code-highlight.css deleted file mode 100644 index f0807dc9c3..0000000000 --- a/src/server/web/assets/code-highlight.css +++ /dev/null @@ -1,93 +0,0 @@ -.hljs { - font-family: Consolas, 'Courier New', Courier, Monaco, monospace; -} - -.hljs, -.hljs-subst { - color: #444; -} - -.hljs-comment { - color: #888888; -} - -.hljs-keyword { - color: #2973b7; -} - -.hljs-number { - color: #ae81ff; -} - -.hljs-string { - color: #e96900; -} - -.hljs-regexp { - color: #e9003f; -} - -.hljs-attribute, -.hljs-selector-tag, -.hljs-meta-keyword, -.hljs-doctag, -.hljs-name { - font-weight: bold; -} - -.hljs-type, -.hljs-selector-id, -.hljs-selector-class, -.hljs-quote, -.hljs-template-tag, -.hljs-deletion { - color: #880000; -} - -.hljs-title, -.hljs-section { - color: #880000; - font-weight: bold; -} - -.hljs-symbol, -.hljs-variable, -.hljs-template-variable, -.hljs-link, -.hljs-selector-attr, -.hljs-selector-pseudo { - color: #BC6060; -} - -/* Language color: hue: 90; */ - -.hljs-literal { - color: #78A960; -} - -.hljs-built_in, -.hljs-bullet, -.hljs-code, -.hljs-addition { - color: #397300; -} - -/* Meta color: hue: 200 */ - -.hljs-meta { - color: #1f7199; -} - -.hljs-meta-string { - color: #4d99bf; -} - -/* Misc effects */ - -.hljs-emphasis { - font-style: italic; -} - -.hljs-strong { - font-weight: bold; -} diff --git a/src/server/web/assets/error.jpg b/src/server/web/assets/error.jpg Binary files differdeleted file mode 100644 index 872b1a3f5d..0000000000 --- a/src/server/web/assets/error.jpg +++ /dev/null diff --git a/src/server/web/assets/favicon.ico b/src/server/web/assets/favicon.ico Binary files differdeleted file mode 100644 index ed9820d5f4..0000000000 --- a/src/server/web/assets/favicon.ico +++ /dev/null diff --git a/src/server/web/assets/label.svg b/src/server/web/assets/label.svg deleted file mode 100644 index b1f85f3c07..0000000000 --- a/src/server/web/assets/label.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="96px" height="96px" viewBox="0 0 96 96" enable-background="new 0 0 96 96" xml:space="preserve">
-<polygon fill="#0B8AEA" points="0,45.255 45.254,0 84.854,0 0,84.854 "/>
-</svg>
diff --git a/src/server/web/assets/manifest.json b/src/server/web/assets/manifest.json deleted file mode 100644 index 783d0539ac..0000000000 --- a/src/server/web/assets/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "short_name": "Misskey", - "name": "Misskey", - "start_url": "/", - "display": "standalone", - "background_color": "#313a42" -} diff --git a/src/server/web/assets/message.mp3 b/src/server/web/assets/message.mp3 Binary files differdeleted file mode 100644 index 6427744475..0000000000 --- a/src/server/web/assets/message.mp3 +++ /dev/null diff --git a/src/server/web/assets/othello-put-me.mp3 b/src/server/web/assets/othello-put-me.mp3 Binary files differdeleted file mode 100644 index 4e0e72091c..0000000000 --- a/src/server/web/assets/othello-put-me.mp3 +++ /dev/null diff --git a/src/server/web/assets/othello-put-you.mp3 b/src/server/web/assets/othello-put-you.mp3 Binary files differdeleted file mode 100644 index 9244189c2d..0000000000 --- a/src/server/web/assets/othello-put-you.mp3 +++ /dev/null diff --git a/src/server/web/assets/post.mp3 b/src/server/web/assets/post.mp3 Binary files differdeleted file mode 100644 index d3da88a933..0000000000 --- a/src/server/web/assets/post.mp3 +++ /dev/null diff --git a/src/server/web/assets/reactions/angry.png b/src/server/web/assets/reactions/angry.png Binary files differdeleted file mode 100644 index d81c431a25..0000000000 --- a/src/server/web/assets/reactions/angry.png +++ /dev/null diff --git a/src/server/web/assets/reactions/confused.png b/src/server/web/assets/reactions/confused.png Binary files differdeleted file mode 100644 index cfaa60146f..0000000000 --- a/src/server/web/assets/reactions/confused.png +++ /dev/null diff --git a/src/server/web/assets/reactions/congrats.png b/src/server/web/assets/reactions/congrats.png Binary files differdeleted file mode 100644 index 350adda322..0000000000 --- a/src/server/web/assets/reactions/congrats.png +++ /dev/null diff --git a/src/server/web/assets/reactions/hmm.png b/src/server/web/assets/reactions/hmm.png Binary files differdeleted file mode 100644 index a9a7e9ac88..0000000000 --- a/src/server/web/assets/reactions/hmm.png +++ /dev/null diff --git a/src/server/web/assets/reactions/laugh.png b/src/server/web/assets/reactions/laugh.png Binary files differdeleted file mode 100644 index cd2225ffe1..0000000000 --- a/src/server/web/assets/reactions/laugh.png +++ /dev/null diff --git a/src/server/web/assets/reactions/like.png b/src/server/web/assets/reactions/like.png Binary files differdeleted file mode 100644 index 9fe67c9109..0000000000 --- a/src/server/web/assets/reactions/like.png +++ /dev/null diff --git a/src/server/web/assets/reactions/love.png b/src/server/web/assets/reactions/love.png Binary files differdeleted file mode 100644 index b8a7532ef0..0000000000 --- a/src/server/web/assets/reactions/love.png +++ /dev/null diff --git a/src/server/web/assets/reactions/pudding.png b/src/server/web/assets/reactions/pudding.png Binary files differdeleted file mode 100644 index 27a6b048e8..0000000000 --- a/src/server/web/assets/reactions/pudding.png +++ /dev/null diff --git a/src/server/web/assets/reactions/surprise.png b/src/server/web/assets/reactions/surprise.png Binary files differdeleted file mode 100644 index 5904cb2c6c..0000000000 --- a/src/server/web/assets/reactions/surprise.png +++ /dev/null diff --git a/src/server/web/assets/recover.html b/src/server/web/assets/recover.html deleted file mode 100644 index 4922b68d35..0000000000 --- a/src/server/web/assets/recover.html +++ /dev/null @@ -1,36 +0,0 @@ -<!DOCTYPE html> - -<html> - <head> - <meta charset="utf-8"> - <title>Misskeyのリカバリ</title> - <script> - - const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches?'); - - if (yn) { - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => registration.unregister()); - }); - - } catch (e) { - console.error(e); - } - - alert('キャッシュをクリアしました。'); - - alert('まもなくページを再度読み込みします。再度読み込みが終わると、再度キャッシュをクリアするか尋ねられるので、「キャンセル」を選択して抜けてください。'); - - setTimeout(() => { - location.reload(true); - }, 100); - } else { - location.href = '/'; - } - - </script> - </head> -</html> diff --git a/src/server/web/assets/title.svg b/src/server/web/assets/title.svg deleted file mode 100644 index 747fcd38b1..0000000000 --- a/src/server/web/assets/title.svg +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle fill="#2B2F2D" cx="128" cy="153.6" r="19.201"/>
-<circle fill="#2B2F2D" cx="51.2" cy="153.6" r="19.2"/>
-<circle fill="#2B2F2D" cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#2B2F2D" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle fill="#2B2F2D" cx="89.6" cy="102.4" r="19.2"/>
-<circle fill="#2B2F2D" cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/server/web/assets/unread.svg b/src/server/web/assets/unread.svg deleted file mode 100644 index 8c3cc9f475..0000000000 --- a/src/server/web/assets/unread.svg +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
-<circle fill="#3AA2DC" cx="16.5" cy="16.5" r="6"/>
-</svg>
diff --git a/src/server/web/assets/welcome-bg.svg b/src/server/web/assets/welcome-bg.svg deleted file mode 100644 index ba8cd8dc0a..0000000000 --- a/src/server/web/assets/welcome-bg.svg +++ /dev/null @@ -1,579 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="1920" - height="1080" - viewBox="0 0 507.99999 285.75001" - version="1.1" - id="svg8" - inkscape:version="0.92.1 r15371" - sodipodi:docname="welcome-bg.svg"> - <defs - id="defs2"> - <pattern - inkscape:collect="always" - xlink:href="#Checkerboard" - id="pattern7194" - patternTransform="scale(1.3152942)" /> - <linearGradient - id="linearGradient7169" - inkscape:collect="always"> - <stop - id="stop7165" - offset="0" - style="stop-color:#eaeaea;stop-opacity:1" /> - <stop - id="stop7167" - offset="1" - style="stop-color:#000000;stop-opacity:1" /> - </linearGradient> - <linearGradient - inkscape:collect="always" - id="linearGradient7044"> - <stop - style="stop-color:#000000;stop-opacity:1;" - offset="0" - id="stop7040" /> - <stop - style="stop-color:#ffffff;stop-opacity:1" - offset="1" - id="stop7042" /> - </linearGradient> - <pattern - inkscape:collect="always" - xlink:href="#Checkerboard" - id="pattern7010" - patternTransform="matrix(1.673813,0,0,1.673813,-177.6001,-146.38611)" /> - <pattern - inkscape:stockid="Checkerboard" - id="Checkerboard" - patternTransform="translate(0,0) scale(10,10)" - height="2" - width="2" - patternUnits="userSpaceOnUse" - inkscape:collect="always" - inkscape:isstock="true"> - <rect - id="rect6201" - height="1" - width="1" - y="0" - x="0" - style="fill:black;stroke:none" /> - <rect - id="rect6203" - height="1" - width="1" - y="1" - x="1" - style="fill:black;stroke:none" /> - </pattern> - <linearGradient - id="linearGradient5406" - osb:paint="solid"> - <stop - style="stop-color:#000000;stop-opacity:1;" - offset="0" - id="stop5404" /> - </linearGradient> - <pattern - patternUnits="userSpaceOnUse" - width="15.999999" - height="16.000025" - patternTransform="matrix(0.26458333,0,0,0.26458333,-16.933332,263.1333)" - id="pattern6465"> - <path - d="m 8.0000542,8.0000126 h 7.9998878 c 3e-5,0 5.7e-5,3.78e-5 5.7e-5,3.78e-5 V 15.99995 c 0,3.7e-5 -2.7e-5,7.5e-5 -5.7e-5,7.5e-5 H 8.0000542 c -3.03e-5,0 -5.67e-5,-3.8e-5 -5.67e-5,-7.5e-5 V 8.0000504 c 0,0 2.64e-5,-3.78e-5 5.67e-5,-3.78e-5 z M 5.6692913e-5,0 H 7.9999408 c 3.02e-5,0 5.67e-5,3.7795275e-5 5.67e-5,7.5590551e-5 V 7.999937 c 0,3.78e-5 -2.65e-5,7.56e-5 -5.67e-5,7.56e-5 H 5.6692913e-5 C 2.2677165e-5,8.0000126 0,7.9999748 0,7.999937 V 7.5590551e-5 C 0,3.7795276e-5 2.2677165e-5,0 5.6692913e-5,0 Z" - style="opacity:1;fill:#db1545;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="rect6445-2" - inkscape:connector-curvature="0" /> - </pattern> - <linearGradient - inkscape:collect="always" - xlink:href="#linearGradient7044" - id="linearGradient6476" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(3.223659,0,0,2.5556636,-579.27357,808.39)" - x1="86.490868" - y1="-216.62756" - x2="176.77992" - y2="-216.62756" /> - <mask - maskUnits="userSpaceOnUse" - id="mask6472"> - <rect - transform="rotate(-90)" - y="-0.91986513" - x="-300.45657" - height="511.36566" - width="291.06116" - id="rect6474" - style="opacity:1;fill:url(#linearGradient6476);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92238116;stroke-miterlimit:4;stroke-dasharray:none" /> - </mask> - <pattern - patternUnits="userSpaceOnUse" - width="2340.7208" - height="2340.7236" - patternTransform="matrix(0.26458333,0,0,0.26458333,-63.499801,-58.601683)" - id="pattern7142"> - <path - d="m 1170.3684,1170.3628 h 1170.3448 c 0,0 0.01,0 0.01,0 v 1170.3457 c 0,0 0,0.011 -0.01,0.011 H 1170.3684 c 0,0 -0.01,0 -0.01,-0.011 v -1170.344 c 0,0 0,0 0.01,0 z M 0.00869291,1.1338583e-5 H 1170.352 c 0,0 0.01,0.0052913414 0.01,0.01096063142 V 1170.3511 c 0,0 0,0.011 -0.01,0.011 H 0.00869291 C 0.00340157,1170.3625 0,1170.3549 0,1170.3511 V 0.01096063 C 0,0.00566929 0.00312945,0 0.00869291,0 Z" - style="opacity:1;fill:#763971;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2340.72119141;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path7135" - inkscape:connector-curvature="0" /> - </pattern> - <linearGradient - inkscape:collect="always" - xlink:href="#linearGradient7169" - id="linearGradient7157" - x1="-3.631536" - y1="155.11069" - x2="511.52777" - y2="155.11069" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(2.184742,0,0,6.5696504,-17.948376,-1979.8074)" /> - <linearGradient - inkscape:collect="always" - xlink:href="#linearGradient7169" - id="linearGradient7200" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(0.57804632,0,0,1.73822,6.5011419,-523.82404)" - x1="-3.631536" - y1="155.11069" - x2="511.52777" - y2="155.11069" /> - <mask - maskUnits="userSpaceOnUse" - id="mask7196"> - <rect - transform="rotate(90)" - y="-512.56537" - x="4.4019437" - height="516.7157" - width="297.78595" - id="rect7198" - style="opacity:1;fill:url(#linearGradient7200);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.1217103;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> - </mask> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#1e1d65" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.84705882" - inkscape:pageshadow="2" - inkscape:zoom="0.79170474" - inkscape:cx="1093.7227" - inkscape:cy="695.27372" - inkscape:document-units="mm" - inkscape:current-layer="layer5" - showgrid="true" - units="px" - inkscape:pagecheckerboard="false" - inkscape:window-width="1920" - inkscape:window-height="1017" - inkscape:window-x="-8" - inkscape:window-y="1072" - inkscape:window-maximized="1" - objecttolerance="1" - guidetolerance="10000" - gridtolerance="10000" - inkscape:snap-bbox="true" - inkscape:bbox-paths="true" - inkscape:bbox-nodes="true" - inkscape:snap-bbox-edge-midpoints="true" - inkscape:snap-bbox-midpoints="true" - showguides="false" - inkscape:lockguides="true"> - <inkscape:grid - type="xygrid" - id="grid6443" - spacingx="2.1166667" - spacingy="2.1166667" - empspacing="4" - color="#3f3fff" - opacity="0.1254902" - enabled="false" /> - <sodipodi:guide - position="-69.219003,3.872392" - orientation="1,0" - id="guide6508" - inkscape:locked="true" /> - </sodipodi:namedview> - <metadata - id="metadata5"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title /> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="レイヤー 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(0,-11.249983)" - style="display:inline" - sodipodi:insensitive="true"> - <rect - style="display:inline;opacity:0.2;fill:url(#pattern7194);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="rect7178" - width="568.07599" - height="367.82269" - x="-37.871731" - y="-52.665051" - mask="url(#mask7196)" /> - </g> - <g - inkscape:groupmode="layer" - id="layer2" - inkscape:label="レイヤー 2" - style="display:inline"> - <rect - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:none;stroke:none;stroke-width:140.99996948" - width="596.8999" - height="596.90082" - x="-63.49987" - y="-58.600021" - id="rect6468" - mask="url(#mask6472)" /> - <path - transform="translate(0,-11.249983)" - sodipodi:type="star" - style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6921" - sodipodi:sides="4" - sodipodi:cx="117.63232" - sodipodi:cy="102.13793" - sodipodi:r1="5.7652407" - sodipodi:r2="2.8826203" - sodipodi:arg1="1.4464413" - sodipodi:arg2="2.2318395" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 118.34741,107.85865 -2.48485,-3.44532 -3.95096,-1.56031 3.44531,-2.48485 1.56032,-3.950959 2.48484,3.445318 3.95097,1.560311 -3.44532,2.48485 z" - inkscape:transform-center-x="1.481982e-006" - inkscape:transform-center-y="-1.1450451e-006" /> - <path - transform="translate(0,-11.249983)" - sodipodi:type="star" - style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6923" - sodipodi:sides="4" - sodipodi:cx="317.5" - sodipodi:cy="75.679596" - sodipodi:r1="3.949214" - sodipodi:r2="1.974607" - sodipodi:arg1="1.6614562" - sodipodi:arg2="2.4468544" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 317.14246,79.612591 -1.1594,-2.668882 -2.41606,-1.621658 2.66889,-1.15939 1.62165,-2.41606 1.1594,2.668882 2.41606,1.621658 -2.66889,1.15939 z" - inkscape:transform-center-x="4.0000001e-006" /> - <path - transform="translate(0,-11.249983)" - sodipodi:type="star" - style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6925" - sodipodi:sides="4" - sodipodi:cx="230.97409" - sodipodi:cy="57.802349" - sodipodi:r1="2.2613134" - sodipodi:r2="1.1306567" - sodipodi:arg1="1.2490458" - sodipodi:arg2="2.0344439" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 231.68918,59.947619 -1.22073,-1.13398 -1.63963,-0.2962 1.13398,-1.220735 0.2962,-1.639625 1.22074,1.13398 1.63962,0.2962 -1.13398,1.220735 z" - inkscape:transform-center-x="2.9099099e-006" /> - <path - transform="translate(0,-11.249983)" - sodipodi:type="star" - style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6927" - sodipodi:sides="4" - sodipodi:cx="260.65033" - sodipodi:cy="106.42847" - sodipodi:r1="1.59899" - sodipodi:r2="0.79949504" - sodipodi:arg1="2.0344439" - sodipodi:arg2="2.8198421" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 259.93524,107.85865 -0.0434,-1.17736 -0.67171,-0.96791 1.17736,-0.0434 0.96791,-0.67171 0.0434,1.17735 0.67171,0.96792 -1.17736,0.0434 z" - inkscape:transform-center-x="3.2837838e-006" - inkscape:transform-center-y="-1.1990991e-006" /> - <path - sodipodi:type="star" - style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6925-2" - sodipodi:sides="4" - sodipodi:cx="87.956078" - sodipodi:cy="127.16609" - sodipodi:r1="2.2613134" - sodipodi:r2="1.1306567" - sodipodi:arg1="1.2490458" - sodipodi:arg2="2.0344439" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 88.671168,129.31136 -1.220735,-1.13398 -1.639626,-0.2962 1.13398,-1.22073 0.296201,-1.63963 1.220735,1.13398 1.639625,0.2962 -1.13398,1.22074 z" - inkscape:transform-center-x="2.4830149e-006" - transform="matrix(0.91666666,0,0,1,7.1509006,-11.249983)" /> - <ellipse - style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06383465" - id="path5313-3-7" - cx="178.44102" - cy="110.95996" - rx="21.691566" - ry="5.0825601" - transform="rotate(-1.570553,-410.38805,-5.6250559)" /> - <ellipse - style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.08063243" - id="path5313-3-7-5" - cx="200.1326" - cy="116.80371" - rx="27.399597" - ry="6.4200115" - transform="rotate(-1.570553,-410.38805,-5.6250559)" /> - <ellipse - style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06734787" - id="path5313-3-7-2" - cx="-429.23041" - cy="90.631134" - rx="24.144913" - ry="5.0825605" - transform="matrix(-0.99537478,-0.09606802,-0.09606802,0.99537478,0,-11.249983)" /> - <ellipse - style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.08507013" - id="path5313-3-7-5-9" - cx="-405.08548" - cy="96.474884" - rx="30.498529" - ry="6.4200115" - transform="matrix(-0.99537478,-0.09606802,-0.09606802,0.99537478,0,-11.249983)" /> - <ellipse - style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.05208009" - id="path5313-3-7-2-9" - cx="-46.428764" - cy="163.90004" - rx="18.893074" - ry="3.884198" - transform="matrix(-0.99073724,0.13579293,0.14607844,0.98927301,0,-11.249983)" /> - <ellipse - style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06578472" - id="path5313-3-7-5-9-1" - cx="-27.535677" - cy="168.36595" - rx="23.864695" - ry="4.9063048" - transform="matrix(-0.99073724,0.13579293,0.14607844,0.98927301,0,-11.249983)" /> - <path - transform="translate(0,-11.249983)" - sodipodi:type="star" - style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6923-9" - sodipodi:sides="4" - sodipodi:cx="459.82239" - sodipodi:cy="139.8455" - sodipodi:r1="3.949214" - sodipodi:r2="1.9746071" - sodipodi:arg1="1.6614562" - sodipodi:arg2="2.4468544" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 459.46484,143.7785 -1.15939,-2.66888 -2.41606,-1.62166 2.66889,-1.15939 1.62165,-2.41606 1.15939,2.66888 2.41606,1.62166 -2.66888,1.15939 z" - inkscape:transform-center-x="4.0000001e-006" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.81509405;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path5229" - cx="192.18326" - cy="74.677902" - r="2.7216933" /> - <path - sodipodi:type="star" - style="fill:#ffffff;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6923-8" - sodipodi:sides="4" - sodipodi:cx="53.989292" - sodipodi:cy="88.908768" - sodipodi:r1="3.949214" - sodipodi:r2="1.9746071" - sodipodi:arg1="1.6614562" - sodipodi:arg2="2.4468544" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 53.631747,92.841763 -1.15939,-2.668883 -2.41606,-1.621657 2.668883,-1.159391 1.621657,-2.41606 1.15939,2.668883 2.416061,1.621658 -2.668883,1.15939 z" - inkscape:transform-center-x="2.0634674e-006" - transform="matrix(0.61390676,-0.48689202,0.48689202,0.61390676,-23.159158,48.648961)" - inkscape:transform-center-y="1.4320049e-006" /> - <path - sodipodi:type="star" - style="fill:#ffffff;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - id="path6923-8-3" - sodipodi:sides="4" - sodipodi:cx="53.989292" - sodipodi:cy="88.908768" - sodipodi:r1="3.949214" - sodipodi:r2="1.9746071" - sodipodi:arg1="1.6614562" - sodipodi:arg2="2.4468544" - inkscape:flatsided="false" - inkscape:rounded="0" - inkscape:randomized="0" - d="m 53.631747,92.841763 -1.15939,-2.668883 -2.41606,-1.621657 2.668883,-1.159391 1.621657,-2.41606 1.15939,2.668883 2.416061,1.621658 -2.668883,1.15939 z" - inkscape:transform-center-x="3.0260172e-006" - transform="matrix(0.58032639,0.43093706,-0.43093706,0.58032639,446.58431,23.35553)" - inkscape:transform-center-y="-1.3594204e-006" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28035584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path5229-6" - cx="347.17841" - cy="36.709366" - r="0.9361406" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28035584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path5229-6-5" - cx="116.0927" - cy="42.136036" - r="0.9361406" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.15;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.55002564;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path5229-0" - cx="456.28247" - cy="47.488548" - r="1.8365992" /> - </g> - <g - inkscape:groupmode="layer" - id="layer5" - inkscape:label="レイヤー 4" - style="display:none"> - <path - transform="translate(0,-11.249983)" - style="display:inline;fill:#ffff7c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none" - d="m 377.25876,69.781182 a 18.234796,18.234796 0 0 1 8.1747,15.19442 18.234796,18.234796 0 0 1 -18.23455,18.235058 18.234796,18.234796 0 0 1 -10.14098,-3.08921 20.380066,20.380066 0 0 0 17.64905,10.2402 20.380066,20.380066 0 0 0 20.38015,-20.380152 20.380066,20.380066 0 0 0 -17.82837,-20.200316 z" - id="path6914" - inkscape:connector-curvature="0" /> - </g> - <g - inkscape:groupmode="layer" - id="layer4" - inkscape:label="レイヤー 3" - style="display:none"> - <circle - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.36438358;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path5306" - cx="168.31279" - cy="2.1908164" - r="36.253109" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.39123487px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 201.1259,19.428383 2.66976,2.617062 1.21734,-1.978474 -0.34264,5.194221 -4.15215,2.110811 1.0283,-1.928856 -2.76172,-2.210044 z" - id="path5168" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.89719725px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 196.25421,26.631949 6.0286,8.817373 -3.70059,3.384671 -1.84127,-4.638447 -2.48924,2.916491 -2.23471,-6.507119 z" - id="path5174" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.1;fill:#000500;fill-opacity:1;stroke:none;stroke-width:0.05121958px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 187.00695,34.050482 1.26268,2.214392 1.44195,-0.54357 1.31981,0.86123 0.21375,1.739039 -1.36828,1.61618 -1.80409,0.265403 -1.1589,-1.059687 -0.23516,-1.721875 1.11047,-0.916698 -0.43413,-0.680502 -0.4102,0.997264 0.74387,1.070883 -0.49255,1.027197 -1.26776,0.228606 -0.5501,-0.871237 0.15467,-0.82956 0.93559,-0.424446 0.58058,-1.450625 -0.75664,-1.131455 z" - id="path6985" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - style="display:inline;opacity:0.1;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.04695854px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 203.23593,14.367789 4.43345,3.766934 0.87976,-0.995725 0.46812,0.475437 -0.80488,0.995031 0.83731,0.705238 0.86731,-0.962102 0.50998,0.516259 -0.87206,0.921255 0.99505,0.941692 -0.44277,0.42746 -0.91483,-0.900095 -0.8367,0.879711 -0.43031,-0.474867 0.78065,-0.831436 -0.86665,-0.779727 -0.81136,0.912638 -0.55866,-0.483362 0.8179,-0.927279 -4.48211,-3.638676 z" - id="path6891-8" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.58045781px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 204.43932,-5.3971152 6.34563,7.5781721 -3.73895,4.9604312 0.33681,4.6546149 -5.20345,5.793617 c 2.83273,-8.049795 3.31033,-11.8140092 3.09986,-18.9271334 z" - id="path5208" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccc" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.11183073px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 205.60259,0.56695919 1.24493,0.127049 0.0916,-0.59592195 0.28719,0.07174803 -0.065,0.56786179 0.62071,0.0788993 -0.0423,0.36840374 -0.62423,-0.048236 -0.0804,0.8381885 0.52004,0.075191 -0.0192,0.3709729 -0.5764,-0.058257 -0.10087,0.8125312 0.54747,0.039404 -0.04,0.4153104 -0.5593,-0.071919 -0.0636,0.6224815 -0.3736,0.00386 0.0816,-0.6437327 -1.20305,-0.1533942 0.0499,-0.3674909 1.2006,0.1064631 0.11092,-0.7647515 -1.19622,-0.1448386 0.027,-0.3701253 1.23042,0.1176518 0.12327,-0.8721654 -1.26199,-0.1134749 z" - id="path7229" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccccccccccc" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.16325578px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 204.68821,9.1424652 1.78173,-0.049987 -1.44996,0.7563273 1.12166,0.7127945 -1.34099,0.0029 0.93885,1.309289 -1.59949,-0.942185 z" - id="path7212-4-6" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.71902335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 180.87434,36.932251 -8.12162,8.095249 -6.61262,-3.934427 -5.68596,1.043018 -7.6496,-6.371879 c 10.33078,4.527622 19.43137,4.062311 28.0698,1.168039 z" - id="path5208-6" - inkscape:connector-curvature="0" - sodipodi:nodetypes="cccccc" /> - <path - style="display:inline;opacity:0.1;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.04569969px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 156.79314,37.138611 -0.83209,5.600235 1.27513,0.214749 -0.15211,0.631281 -1.23602,-0.153244 -0.15211,1.0545 1.24093,0.221743 -0.16427,0.686859 -1.20964,-0.246683 -0.26626,1.306416 -0.58089,-0.145968 0.27316,-1.218758 -1.15712,-0.238846 0.17092,-0.599741 1.08842,0.21735 0.19853,-1.117028 -1.17126,-0.200972 0.11204,-0.710141 1.18676,0.198837 0.70106,-5.574493 z" - id="path6891-8-9" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.84177661px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 143.96364,29.933272 -4.59686,9.216397 3.65156,2.834687 1.22043,-4.692866 2.51661,2.524357 1.39851,-6.542721 z" - id="path5174-1" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56489706px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 142.60658,28.70585 -2.96842,6.930652 -3.79379,-3.925042 4.56394,-5.124749 z" - id="path5285" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.35393918px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 137.9306,23.319484 -3.42616,1.224261 1.2143,1.906916 -4.40128,-2.508612 -0.0822,-4.53226 1.25123,1.720316 3.10894,-1.477793 z" - id="path5168-0" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.1;fill:#000500;fill-opacity:1;stroke:none;stroke-width:0.0498465px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 132.55595,11.444656 -2.31852,0.882408 0.30663,1.468015 -1.02588,1.140069 -1.70428,-0.05499 -1.34908,-1.557886 0.015,-1.774566 1.1926,-0.955614 1.69096,0.03182 0.7151,1.205156 0.71942,-0.315492 -0.89748,-0.543864 -1.14121,0.554849 -0.91394,-0.627513 -0.0299,-1.2533405 0.92017,-0.3984462 0.77453,0.2730438 0.26797,0.9632459 1.30792,0.775623 1.20137,-0.558052 z" - id="path6985-7" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15882961px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 131.32384,2.4817954 -1.6313,-0.4305236 1.16551,1.0474206 -1.19547,0.453907 1.23564,0.290212 -1.16202,1.0740836 1.68796,-0.5749329 z" - id="path7212-4-6-8" - inkscape:connector-curvature="0" /> - <path - style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.55575538px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 137.04207,-21.420699 -7.13207,5.035868 1.31743,5.70794 -2.10914,4.1341529 2.26645,6.93249012 c 0.67636,-8.23493742 2.69888,-15.39599902 5.65733,-21.81045102 z" - id="path5208-4" - inkscape:connector-curvature="0" - sodipodi:nodetypes="cccccc" /> - </g> -</svg> diff --git a/src/server/web/assets/welcome-fg.svg b/src/server/web/assets/welcome-fg.svg deleted file mode 100644 index 5c795c3027..0000000000 --- a/src/server/web/assets/welcome-fg.svg +++ /dev/null @@ -1,380 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="1920" - height="1080" - viewBox="0 0 507.99999 285.75001" - version="1.1" - id="svg8" - inkscape:version="0.92.1 r15371" - sodipodi:docname="welcome-fg.svg"> - <defs - id="defs2"> - <linearGradient - inkscape:collect="always" - id="linearGradient7044"> - <stop - style="stop-color:#000000;stop-opacity:1;" - offset="0" - id="stop7040" /> - <stop - style="stop-color:#ffffff;stop-opacity:1" - offset="1" - id="stop7042" /> - </linearGradient> - <pattern - inkscape:collect="always" - xlink:href="#Checkerboard" - id="pattern7010" - patternTransform="matrix(1.673813,0,0,1.673813,-177.6001,-146.38611)" /> - <pattern - inkscape:stockid="Checkerboard" - id="Checkerboard" - patternTransform="translate(0,0) scale(10,10)" - height="2" - width="2" - patternUnits="userSpaceOnUse" - inkscape:collect="always"> - <rect - id="rect6201" - height="1" - width="1" - y="0" - x="0" - style="fill:black;stroke:none" /> - <rect - id="rect6203" - height="1" - width="1" - y="1" - x="1" - style="fill:black;stroke:none" /> - </pattern> - <linearGradient - id="linearGradient5406" - osb:paint="solid"> - <stop - style="stop-color:#000000;stop-opacity:1;" - offset="0" - id="stop5404" /> - </linearGradient> - <pattern - patternUnits="userSpaceOnUse" - width="15.999999" - height="16.000025" - patternTransform="matrix(0.26458333,0,0,0.26458333,-16.933332,263.1333)" - id="pattern6465"> - <path - d="m 8.0000542,8.0000126 h 7.9998878 c 3e-5,0 5.7e-5,3.78e-5 5.7e-5,3.78e-5 V 15.99995 c 0,3.7e-5 -2.7e-5,7.5e-5 -5.7e-5,7.5e-5 H 8.0000542 c -3.03e-5,0 -5.67e-5,-3.8e-5 -5.67e-5,-7.5e-5 V 8.0000504 c 0,0 2.64e-5,-3.78e-5 5.67e-5,-3.78e-5 z M 5.6692913e-5,0 H 7.9999408 c 3.02e-5,0 5.67e-5,3.7795275e-5 5.67e-5,7.5590551e-5 V 7.999937 c 0,3.78e-5 -2.65e-5,7.56e-5 -5.67e-5,7.56e-5 H 5.6692913e-5 C 2.2677165e-5,8.0000126 0,7.9999748 0,7.999937 V 7.5590551e-5 C 0,3.7795276e-5 2.2677165e-5,0 5.6692913e-5,0 Z" - style="opacity:1;fill:#db1545;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="rect6445-2" - inkscape:connector-curvature="0" /> - </pattern> - <linearGradient - inkscape:collect="always" - xlink:href="#linearGradient7044" - id="linearGradient6476" - gradientUnits="userSpaceOnUse" - gradientTransform="matrix(3.223659,0,0,2.5556636,-579.27357,808.39)" - x1="86.490868" - y1="-216.62756" - x2="176.77992" - y2="-216.62756" /> - <mask - maskUnits="userSpaceOnUse" - id="mask6472"> - <rect - transform="rotate(-90)" - y="-0.91986513" - x="-300.45657" - height="511.36566" - width="291.06116" - id="rect6474" - style="opacity:1;fill:url(#linearGradient6476);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92238116;stroke-miterlimit:4;stroke-dasharray:none" /> - </mask> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#1e1d65" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.84705882" - inkscape:pageshadow="2" - inkscape:zoom="0.6363961" - inkscape:cx="720.54406" - inkscape:cy="371.58659" - inkscape:document-units="mm" - inkscape:current-layer="layer1" - showgrid="true" - units="px" - inkscape:pagecheckerboard="true" - inkscape:window-width="1920" - inkscape:window-height="1057" - inkscape:window-x="1912" - inkscape:window-y="1143" - inkscape:window-maximized="1" - objecttolerance="1" - guidetolerance="10000" - gridtolerance="10000" - inkscape:snap-bbox="true" - inkscape:bbox-paths="true" - inkscape:bbox-nodes="true" - inkscape:snap-bbox-edge-midpoints="true" - inkscape:snap-bbox-midpoints="true" - showguides="false"> - <inkscape:grid - type="xygrid" - id="grid6443" - spacingx="2.1166667" - spacingy="2.1166667" - empspacing="4" - color="#3f3fff" - opacity="0.1254902" - enabled="false" /> - <sodipodi:guide - position="-69.219003,3.872392" - orientation="1,0" - id="guide6508" - inkscape:locked="false" /> - </sodipodi:namedview> - <metadata - id="metadata5"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title /> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:groupmode="layer" - id="layer2" - inkscape:label="Back" - style="display:inline"> - <path - style="fill:#253276;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 500.58203,825.29688 -54.2207,18.9121 18.91406,56.74219 -45.39258,10.08594 -11.34765,-39.08789 -46.6543,12.60937 13.87109,34.04493 -55.48047,15.13086 -12.60937,-44.13086 -47.91406,13.86914 13.86914,44.13086 -32.78321,11.3496 17.65235,35.30469 278.66211,-63.04492 z m -11.0957,26.45312 0.44726,11.5918 -12.03711,2.67382 -3.5664,-9.80664 z m 4.90429,24.51953 0.89258,9.80859 -9.36328,2.67383 -4.45703,-9.36133 z m -201.5,32.09766 v 11.14453 l -8.4707,1.7832 -4.9043,-8.91601 z" - id="path4522" - inkscape:connector-curvature="0" - transform="scale(0.26458333)" /> - <path - transform="translate(0,-11.249983)" - style="fill:#253276;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 381.65643,238.28361 -47.37344,16.34717 116.09827,29.02457 -14.01186,-23.68672 -31.02626,-0.33362 z" - id="path4520" - inkscape:connector-curvature="0" /> - </g> - <g - inkscape:label="Ground" - inkscape:groupmode="layer" - id="layer1" - transform="translate(0,-11.249983)" - style="display:inline"> - <circle - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:1.99730551" - id="path5392" - cx="253.06117" - cy="887.61829" - r="642.68146" /> - </g> - <g - inkscape:groupmode="layer" - id="layer3" - inkscape:label="Front"> - <path - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:1.00157475;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - d="m 565.38867,666.80078 -115.20508,24.36914 70.24414,231.09766 121.20118,-18.97656 8.61523,-148.01368 -76.28906,21.625 z m -30.15234,38.82813 3.09765,47.0625 -11.44531,2.49414 -9.14062,-46.10743 z m -26.41211,5.20898 10.30664,46.03906 -9.47852,2.06641 -17.14257,-44.88672 z m 41.45508,65.93945 2.80078,44.04493 -12.50391,3.40234 L 532.1543,781.75 Z m -25.15039,6.90039 9.4414,42.18165 -9.54297,2.59765 -13.99804,-40.91015 z m 85.48242,50.83789 1,42.35938 -22.15235,4.89648 -4.53906,-41.66406 z m -54.21485,10.16797 4.54102,41.66211 -7.67188,1.89649 -8.07421,-40.73047 z m -16.66992,4.20899 9.05469,40.45703 -8.88477,2.19727 -12.02734,-39.66016 z" - id="path5398" - transform="scale(0.26458333)" - inkscape:connector-curvature="0" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 329.51477,199.15082 -32.04286,18.26817 12.8142,1.28619 -6.02656,28.18505 32.94792,3.49531 0.51681,-27.76301 11.91226,1.00737 z m -14.10711,25.93826 6.27123,0.90288 -1.15019,5.4805 -6.00929,-0.898 z m 13.58524,2.09643 0.42171,5.50053 -6.35262,-0.44337 1.22618,-5.67857 z m -15.04127,5.73678 6.21844,0.90138 -1.87301,4.94347 -5.07899,-0.81761 z m 8.80707,1.53673 6.3403,1.10313 0.43128,4.98637 -7.83808,-1.19409 z" - id="path6874" - inkscape:connector-curvature="0" - sodipodi:nodetypes="cccccccccccccccccccccccccccc" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 366.28967,254.78298 7.49431,-30.40441 -7.41388,-2.66046 1.18763,-3.36104 7.21205,2.27141 1.38362,-5.73044 -7.20912,-2.66047 1.28561,-3.65794 7.01313,2.7643 2.17341,-7.01022 3.35519,1.48161 -2.1734,6.51147 6.70747,2.66046 -1.28564,3.16213 -6.31255,-2.46154 -1.68638,6.02735 6.80837,2.46447 -0.9887,3.84808 -6.90052,-2.47031 -6.71038,30.41026 z" - id="path6891" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 74.047433,217.56203 -1.20251,0.65577 2.314585,6.84299 -4.564578,1.31517 13.625009,41.10395 21.186821,-5.50251 -7.183542,-43.56323 -22.044649,6.35259 z m 16.734379,10.06088 1.478463,10.23607 -8.339026,1.96939 -3.82509,-9.42992 z m 3.780131,14.55519 0.781863,9.82627 -7.001121,1.81797 -3.593063,-9.29297 z" - id="path6944" - inkscape:connector-curvature="0" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.24600939px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 43.603475,280.06036 -10.564819,-28.58824 -6.574764,2.28618 -0.916385,-3.37337 6.23111,-2.47535 -2.011396,-5.37101 -6.431418,2.16468 -1.002197,-3.66725 6.348194,-1.96596 -2.123972,-6.85578 3.11982,-0.81419 1.86458,6.45975 6.080155,-1.86705 0.744318,3.27357 -5.700174,1.79072 1.953823,5.78639 6.048884,-2.08256 1.308957,3.64208 -6.116434,2.13257 11.116753,28.12778 z" - id="path6891-8" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 411.98753,264.70523 3.91734,-12.57157 -7.13355,-3.53259 -1.396,-8.02014 5.81668,-6.93436 10.92618,-0.52461 7.35863,5.88054 0.0806,8.11138 -5.67524,6.95564 -7.37536,-0.96565 -1.04168,4.03744 5.21293,-1.96321 1.42492,-6.58308 5.61592,-1.7579 5.33002,3.98422 -1.35343,5.14755 -3.67857,2.33882 -4.89966,-2.03926 -7.52592,2.91667 -1.60892,6.84465 z" - id="path6985" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccc" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.27861062px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 438.77767,272.41521 -0.009,-2.99656 1.24656,2.44908 1.28337,-1.87551 -0.0534,2.25473 2.30831,-1.55949 -1.70125,2.67579 z" - id="path7212" - inkscape:connector-curvature="0" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.29395995px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 387.1467,259.13862 -0.3913,-3.17093 1.60741,2.46066 1.09423,-2.12083 0.23196,2.39229 2.19942,-1.8946 -1.42637,3.01207 z" - id="path7212-4" - inkscape:connector-curvature="0" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 457.96894,278.42384 1.02302,-2.77836 -1.31183,-0.56021 0.33336,-0.616 1.26318,0.48291 0.54568,-1.37607 0.81934,0.31324 -0.47741,1.4022 1.87364,0.67714 0.47795,-1.14765 0.83893,0.26207 -0.47245,1.28672 1.80283,0.70884 0.41215,-1.23149 0.92825,0.33529 -0.49337,1.23952 1.38917,0.51162 -0.21081,0.85845 -1.42731,-0.56527 -1.05878,2.6669 -0.81279,-0.33034 0.94975,-2.68892 -1.68742,-0.7038 -1.03512,2.65627 -0.83236,-0.27915 0.99293,-2.75061 -1.92628,-0.79522 -1.00194,2.82543 z" - id="path7229" - inkscape:connector-curvature="0" - sodipodi:nodetypes="ccccccccccccccccccccccccccccc" /> - <path - transform="translate(0,-11.249983)" - id="path7233" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.3185696px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 73.482785,265.42476 4.944364,-1.72314 -0.207904,-0.52164 -2.012479,0.86151 -0.0213,-0.63037 -0.837931,0.3339 0.324488,0.46118 -2.371778,0.68852 z m 0.497305,0.21764 4.223597,-1.35549 0.556753,4.37406 -2.879727,0.92419 z" - inkscape:connector-curvature="0" - sodipodi:nodetypes="cccccccccccccc" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 156.55184,206.61884 0.47605,-0.20403 1.0201,8.90891 -0.47605,0.20402 z" - id="path7236" - inkscape:connector-curvature="0" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 160.97229,209.47512 0.20402,4.96451 0.47605,-0.068 0.068,-5.03251 z" - id="path7238" - inkscape:connector-curvature="0" /> - <path - transform="translate(0,-11.249983)" - style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.34364724px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" - d="m 23.838748,287.33572 -2.186787,-3.04882 3.027872,1.63785 -0.07842,-2.79635 1.585239,2.33549 1.177306,-3.18042 0.241718,3.90016 z" - id="path7212-4-6" - inkscape:connector-curvature="0" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535" - cx="120.03474" - cy="193.66763" - r="2.5126758" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-2" - cx="97.333473" - cy="218.84901" - r="2.5126758" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-24" - cx="70.128021" - cy="226.19046" - r="2.5126758" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.25;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.41842699;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-25" - cx="118.05532" - cy="234.83446" - r="1.6838019" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-9" - cx="110.59546" - cy="252.2408" - r="1.5653913" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-9-7" - cx="122.43651" - cy="242.53113" - r="1.5653913" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-9-2" - cx="64.415337" - cy="265.26596" - r="1.5653913" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-24-4" - cx="69.61615" - cy="226.18503" - r="7.648705" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-24-4-4" - cx="97.333473" - cy="218.84901" - r="7.648705" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-24-4-2" - cx="119.52941" - cy="193.50121" - r="7.648705" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-9-2-6" - cx="64.415337" - cy="265.26596" - r="4.9115925" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-9-2-6-7" - cx="110.59546" - cy="252.2408" - r="4.9115925" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-9-2-6-3" - cx="122.43651" - cy="242.53113" - r="4.9115925" /> - <circle - transform="translate(0,-11.249983)" - style="opacity:0.05;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path4535-24-4-4-8" - cx="117.52492" - cy="234.88242" - r="7.648705" /> - </g> -</svg> diff --git a/src/server/web/const.styl b/src/server/web/const.styl deleted file mode 100644 index f16e077828..0000000000 --- a/src/server/web/const.styl +++ /dev/null @@ -1,4 +0,0 @@ -json('../../const.json') - -$theme-color = themeColor -$theme-color-foreground = themeColorForeground diff --git a/src/server/web/docs/about.en.pug b/src/server/web/docs/about.en.pug deleted file mode 100644 index 893d9dd6a1..0000000000 --- a/src/server/web/docs/about.en.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 About Misskey - -p Misskey is a mini blog SNS. diff --git a/src/server/web/docs/about.ja.pug b/src/server/web/docs/about.ja.pug deleted file mode 100644 index fec933b0c6..0000000000 --- a/src/server/web/docs/about.ja.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 Misskeyについて - -p MisskeyはミニブログSNSです。 diff --git a/src/server/web/docs/api.ja.pug b/src/server/web/docs/api.ja.pug deleted file mode 100644 index 665cfdc4b8..0000000000 --- a/src/server/web/docs/api.ja.pug +++ /dev/null @@ -1,103 +0,0 @@ -h1 Misskey API - -p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。 -p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。 - -section - h2 自分の所有するアカウントからAPIにアクセスする場合 - p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。 - p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。 - div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 - p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 - -section - h2 アプリケーションからAPIにアクセスする場合 - p - | 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、 - | アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、 - | そのトークンをリクエストのパラメータに含める必要があります。 - div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます - - p それでは、アクセストークンを取得するまでの流れを説明します。 - - section - h3 1.アプリケーションを登録する - p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 - p - a(href=common.config.dev_url, target="_blank") デベロッパーセンター - | にアクセスし、「アプリ > アプリ作成」に進みます。 - | フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです: - - table - thead - tr - th 名前 - th 説明 - tbody - tr - td アプリケーション名 - td あなたのアプリの名称。 - tr - td アプリの概要 - td あなたのアプリの簡単な説明や紹介。 - tr - td コールバックURL - td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 - tr - td 権限 - td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 - - p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 - div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。 - - section - h3 2.ユーザーに認証させる - p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。 - p - | 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。 - | リクエスト形式はJSONで、メソッドはPOSTです。 - | レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。 - - p - | あなたのアプリがコールバックURLを設定している場合、 - | ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 - - p - | あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 - - section - h3 3.ユーザーのアクセストークンを取得する - p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します: - table - thead - tr - th 名前 - th 型 - th 説明 - tbody - tr - td appSecret - td string - td あなたのアプリのシークレットキー - tr - td token - td string - td セッションのトークン - p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! - - p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。 - - p 「i」パラメータの生成方法を擬似コードで表すと次のようになります: - pre: code - | const i = sha256(accessToken + secretKey); - - p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 - -section - h2 Misskey APIの利用 - p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。 - p APIリファレンスもご確認ください。 - - section - h3 レートリミット - p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。 diff --git a/src/server/web/docs/api/endpoints/posts/create.yaml b/src/server/web/docs/api/endpoints/posts/create.yaml deleted file mode 100644 index 11d9f40c54..0000000000 --- a/src/server/web/docs/api/endpoints/posts/create.yaml +++ /dev/null @@ -1,53 +0,0 @@ -endpoint: "posts/create" - -desc: - ja: "投稿します。" - en: "Compose new post." - -params: - - name: "text" - type: "string" - optional: true - desc: - ja: "投稿の本文" - en: "The text of your post" - - name: "mediaIds" - type: "id(DriveFile)[]" - optional: true - desc: - ja: "添付するメディア(1~4つ)" - en: "Media you want to attach (1~4)" - - name: "replyId" - type: "id(Post)" - optional: true - desc: - ja: "返信する投稿" - en: "The post you want to reply" - - name: "repostId" - type: "id(Post)" - optional: true - desc: - ja: "引用する投稿" - en: "The post you want to quote" - - name: "poll" - type: "object" - optional: true - desc: - ja: "投票" - en: "The poll" - defName: "poll" - def: - - name: "choices" - type: "string[]" - optional: false - desc: - ja: "投票の選択肢" - en: "Choices of a poll" - -res: - - name: "createdPost" - type: "entity(Post)" - optional: false - desc: - ja: "作成した投稿" - en: "A post that created" diff --git a/src/server/web/docs/api/endpoints/posts/timeline.yaml b/src/server/web/docs/api/endpoints/posts/timeline.yaml deleted file mode 100644 index 9c44dd736a..0000000000 --- a/src/server/web/docs/api/endpoints/posts/timeline.yaml +++ /dev/null @@ -1,32 +0,0 @@ -endpoint: "posts/timeline" - -desc: - ja: "タイムラインを取得します。" - en: "Get your timeline." - -params: - - name: "limit" - type: "number" - optional: true - desc: - ja: "取得する最大の数" - - name: "sinceId" - type: "id(Post)" - optional: true - desc: - ja: "指定すると、この投稿を基点としてより新しい投稿を取得します" - - name: "untilId" - type: "id(Post)" - optional: true - desc: - ja: "指定すると、この投稿を基点としてより古い投稿を取得します" - - name: "sinceDate" - type: "number" - optional: true - desc: - ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" - - name: "untilDate" - type: "number" - optional: true - desc: - ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" diff --git a/src/server/web/docs/api/endpoints/style.styl b/src/server/web/docs/api/endpoints/style.styl deleted file mode 100644 index 2af9fe9a77..0000000000 --- a/src/server/web/docs/api/endpoints/style.styl +++ /dev/null @@ -1,21 +0,0 @@ -@import "../style" - -#url - padding 8px 12px 8px 8px - font-family Consolas, 'Courier New', Courier, Monaco, monospace - color #fff - background #222e40 - border-radius 4px - - > .method - display inline-block - margin 0 8px 0 0 - padding 0 6px - color #f4fcff - background #17afc7 - border-radius 4px - user-select none - pointer-events none - - > .host - opacity 0.7 diff --git a/src/server/web/docs/api/endpoints/view.pug b/src/server/web/docs/api/endpoints/view.pug deleted file mode 100644 index d271a5517a..0000000000 --- a/src/server/web/docs/api/endpoints/view.pug +++ /dev/null @@ -1,32 +0,0 @@ -extends ../../layout.pug -include ../mixins - -block meta - link(rel="stylesheet" href="/assets/api/endpoints/style.css") - -block main - h1= endpoint - - p#url - span.method POST - span.host - = url.host - | / - span.path= url.path - - p#desc= desc[lang] || desc['ja'] - - section - h2 %i18n:docs.api.endpoints.params% - +propTable(params) - - if paramDefs - each paramDef in paramDefs - section(id= paramDef.name) - h3= paramDef.name - +propTable(paramDef.params) - - if res - section - h2 %i18n:docs.api.endpoints.res% - +propTable(res) diff --git a/src/server/web/docs/api/entities/drive-file.yaml b/src/server/web/docs/api/entities/drive-file.yaml deleted file mode 100644 index 02ab0d608e..0000000000 --- a/src/server/web/docs/api/entities/drive-file.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: "DriveFile" - -desc: - ja: "ドライブのファイル。" - en: "A file of Drive." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "ファイルID" - en: "The ID of this file" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "アップロード日時" - en: "The upload date of this file" - - name: "userId" - type: "id(User)" - optional: false - desc: - ja: "所有者ID" - en: "The ID of the owner of this file" - - name: "user" - type: "entity(User)" - optional: true - desc: - ja: "所有者" - en: "The owner of this file" - - name: "name" - type: "string" - optional: false - desc: - ja: "ファイル名" - en: "The name of this file" - - name: "md5" - type: "string" - optional: false - desc: - ja: "ファイルのMD5ハッシュ値" - en: "The md5 hash value of this file" - - name: "type" - type: "string" - optional: false - desc: - ja: "ファイルの種類" - en: "The type of this file" - - name: "datasize" - type: "number" - optional: false - desc: - ja: "ファイルサイズ(bytes)" - en: "The size of this file (bytes)" - - name: "url" - type: "string" - optional: false - desc: - ja: "ファイルのURL" - en: "The URL of this file" - - name: "folderId" - type: "id(DriveFolder)" - optional: true - desc: - ja: "フォルダID" - en: "The ID of the folder of this file" - - name: "folder" - type: "entity(DriveFolder)" - optional: true - desc: - ja: "フォルダ" - en: "The folder of this file" diff --git a/src/server/web/docs/api/entities/post.yaml b/src/server/web/docs/api/entities/post.yaml deleted file mode 100644 index 74d7973e38..0000000000 --- a/src/server/web/docs/api/entities/post.yaml +++ /dev/null @@ -1,168 +0,0 @@ -name: "Post" - -desc: - ja: "投稿。" - en: "A post." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "投稿ID" - en: "The ID of this post" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "投稿日時" - en: "The posted date of this post" - - name: "viaMobile" - type: "boolean" - optional: true - desc: - ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" - en: "Whether this post sent via a mobile device" - - name: "text" - type: "string" - optional: true - desc: - ja: "投稿の本文" - en: "The text of this post" - - name: "mediaIds" - type: "id(DriveFile)[]" - optional: true - desc: - ja: "添付されているメディアのID" - en: "The IDs of the attached media" - - name: "media" - type: "entity(DriveFile)[]" - optional: true - desc: - ja: "添付されているメディア" - en: "The attached media" - - name: "userId" - type: "id(User)" - optional: false - desc: - ja: "投稿者ID" - en: "The ID of author of this post" - - name: "user" - type: "entity(User)" - optional: true - desc: - ja: "投稿者" - en: "The author of this post" - - name: "myReaction" - type: "string" - optional: true - desc: - ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" - en: "The your <a href='/docs/api/reactions'>reaction</a> of this post" - - name: "reactionCounts" - type: "object" - optional: false - desc: - ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" - - name: "replyId" - type: "id(Post)" - optional: true - desc: - ja: "返信した投稿のID" - en: "The ID of the replyed post" - - name: "reply" - type: "entity(Post)" - optional: true - desc: - ja: "返信した投稿" - en: "The replyed post" - - name: "repostId" - type: "id(Post)" - optional: true - desc: - ja: "引用した投稿のID" - en: "The ID of the quoted post" - - name: "repost" - type: "entity(Post)" - optional: true - desc: - ja: "引用した投稿" - en: "The quoted post" - - name: "poll" - type: "object" - optional: true - desc: - ja: "投票" - en: "The poll" - defName: "poll" - def: - - name: "choices" - type: "object[]" - optional: false - desc: - ja: "投票の選択肢" - en: "The choices of this poll" - defName: "choice" - def: - - name: "id" - type: "number" - optional: false - desc: - ja: "選択肢ID" - en: "The ID of this choice" - - name: "isVoted" - type: "boolean" - optional: true - desc: - ja: "自分がこの選択肢に投票したかどうか" - en: "Whether you voted to this choice" - - name: "text" - type: "string" - optional: false - desc: - ja: "選択肢本文" - en: "The text of this choice" - - name: "votes" - type: "number" - optional: false - desc: - ja: "この選択肢に投票された数" - en: "The number voted for this choice" - - name: "geo" - type: "object" - optional: true - desc: - ja: "位置情報" - en: "Geo location" - defName: "geo" - def: - - name: "coordinates" - type: "number[]" - optional: false - desc: - ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" - - name: "altitude" - type: "number" - optional: false - desc: - ja: "高度。メートル単位で表す。" - - name: "accuracy" - type: "number" - optional: false - desc: - ja: "緯度、経度の精度。メートル単位で表す。" - - name: "altitudeAccuracy" - type: "number" - optional: false - desc: - ja: "高度の精度。メートル単位で表す。" - - name: "heading" - type: "number" - optional: false - desc: - ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" - - name: "speed" - type: "number" - optional: false - desc: - ja: "速度。メートル / 秒数で表す。" diff --git a/src/server/web/docs/api/entities/style.styl b/src/server/web/docs/api/entities/style.styl deleted file mode 100644 index bddf0f53ab..0000000000 --- a/src/server/web/docs/api/entities/style.styl +++ /dev/null @@ -1 +0,0 @@ -@import "../style" diff --git a/src/server/web/docs/api/entities/user.yaml b/src/server/web/docs/api/entities/user.yaml deleted file mode 100644 index a1fae1482b..0000000000 --- a/src/server/web/docs/api/entities/user.yaml +++ /dev/null @@ -1,173 +0,0 @@ -name: "User" - -desc: - ja: "ユーザー。" - en: "A user." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "ユーザーID" - en: "The ID of this user" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "アカウント作成日時" - en: "The registered date of this user" - - name: "username" - type: "string" - optional: false - desc: - ja: "ユーザー名" - en: "The username of this user" - - name: "description" - type: "string" - optional: false - desc: - ja: "アカウントの説明(自己紹介)" - en: "The description of this user" - - name: "avatarId" - type: "id(DriveFile)" - optional: true - desc: - ja: "アバターのID" - en: "The ID of the avatar of this user" - - name: "avatarUrl" - type: "string" - optional: false - desc: - ja: "アバターのURL" - en: "The URL of the avatar of this user" - - name: "bannerId" - type: "id(DriveFile)" - optional: true - desc: - ja: "バナーのID" - en: "The ID of the banner of this user" - - name: "bannerUrl" - type: "string" - optional: false - desc: - ja: "バナーのURL" - en: "The URL of the banner of this user" - - name: "followersCount" - type: "number" - optional: false - desc: - ja: "フォロワーの数" - en: "The number of the followers for this user" - - name: "followingCount" - type: "number" - optional: false - desc: - ja: "フォローしているユーザーの数" - en: "The number of the following users for this user" - - name: "isFollowing" - type: "boolean" - optional: true - desc: - ja: "自分がこのユーザーをフォローしているか" - - name: "isFollowed" - type: "boolean" - optional: true - desc: - ja: "自分がこのユーザーにフォローされているか" - - name: "isMuted" - type: "boolean" - optional: true - desc: - ja: "自分がこのユーザーをミュートしているか" - en: "Whether you muted this user" - - name: "postsCount" - type: "number" - optional: false - desc: - ja: "投稿の数" - en: "The number of the posts of this user" - - name: "pinnedPost" - type: "entity(Post)" - optional: true - desc: - ja: "ピン留めされた投稿" - en: "The pinned post of this user" - - name: "pinnedPostId" - type: "id(Post)" - optional: true - desc: - ja: "ピン留めされた投稿のID" - en: "The ID of the pinned post of this user" - - name: "driveCapacity" - type: "number" - optional: false - desc: - ja: "ドライブの容量(bytes)" - en: "The capacity of drive of this user (bytes)" - - name: "host" - type: "string | null" - optional: false - desc: - ja: "ホスト (例: example.com:3000)" - en: "Host (e.g. example.com:3000)" - - name: "account" - type: "object" - optional: false - desc: - ja: "このサーバーにおけるアカウント" - en: "The account of this user on this server" - defName: "account" - def: - - name: "lastUsedAt" - type: "date" - optional: false - desc: - ja: "最終利用日時" - en: "The last used date of this user" - - name: "isBot" - type: "boolean" - optional: true - desc: - ja: "botか否か(自己申告であることに留意)" - en: "Whether is bot or not" - - name: "twitter" - type: "object" - optional: true - desc: - ja: "連携されているTwitterアカウント情報" - en: "The info of the connected twitter account of this user" - defName: "twitter" - def: - - name: "userId" - type: "string" - optional: false - desc: - ja: "ユーザーID" - en: "The user ID" - - name: "screenName" - type: "string" - optional: false - desc: - ja: "ユーザー名" - en: "The screen name of this user" - - name: "profile" - type: "object" - optional: false - desc: - ja: "プロフィール" - en: "The profile of this user" - defName: "profile" - def: - - name: "location" - type: "string" - optional: true - desc: - ja: "場所" - en: "The location of this user" - - name: "birthday" - type: "string" - optional: true - desc: - ja: "誕生日 (YYYY-MM-DD)" - en: "The birthday of this user (YYYY-MM-DD)" diff --git a/src/server/web/docs/api/entities/view.pug b/src/server/web/docs/api/entities/view.pug deleted file mode 100644 index 2156463dc7..0000000000 --- a/src/server/web/docs/api/entities/view.pug +++ /dev/null @@ -1,20 +0,0 @@ -extends ../../layout.pug -include ../mixins - -block meta - link(rel="stylesheet" href="/assets/api/entities/style.css") - -block main - h1= name - - p#desc= desc[lang] || desc['ja'] - - section - h2 %i18n:docs.api.entities.properties% - +propTable(props) - - if propDefs - each propDef in propDefs - section(id= propDef.name) - h3= propDef.name - +propTable(propDef.params) diff --git a/src/server/web/docs/api/gulpfile.ts b/src/server/web/docs/api/gulpfile.ts deleted file mode 100644 index 37935413de..0000000000 --- a/src/server/web/docs/api/gulpfile.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Gulp tasks - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as glob from 'glob'; -import * as gulp from 'gulp'; -import * as pug from 'pug'; -import * as yaml from 'js-yaml'; -import * as mkdirp from 'mkdirp'; - -import locales from '../../../../../locales'; -import I18nReplacer from '../../../../build/i18n'; -import fa from '../../../../build/fa'; -import config from './../../../../conf'; - -import generateVars from '../vars'; - -const langs = Object.keys(locales); - -const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); - -const parseParam = param => { - const id = param.type.match(/^id\((.+?)\)|^id/); - const entity = param.type.match(/^entity\((.+?)\)/); - const isObject = /^object/.test(param.type); - const isDate = /^date/.test(param.type); - const isArray = /\[\]$/.test(param.type); - if (id) { - param.kind = 'id'; - param.type = 'string'; - param.entity = id[1]; - if (isArray) { - param.type += '[]'; - } - } - if (entity) { - param.kind = 'entity'; - param.type = 'object'; - param.entity = entity[1]; - if (isArray) { - param.type += '[]'; - } - } - if (isObject) { - param.kind = 'object'; - } - if (isDate) { - param.kind = 'date'; - param.type = 'string'; - if (isArray) { - param.type += '[]'; - } - } - - return param; -}; - -const sortParams = params => { - params.sort((a, b) => { - if (a.name < b.name) - return -1; - if (a.name > b.name) - return 1; - return 0; - }); - return params; -}; - -const extractDefs = params => { - let defs = []; - - params.forEach(param => { - if (param.def) { - defs.push({ - name: param.defName, - params: sortParams(param.def.map(p => parseParam(p))) - }); - - const childDefs = extractDefs(param.def); - - defs = defs.concat(childDefs); - } - }); - - return sortParams(defs); -}; - -gulp.task('doc:api', [ - 'doc:api:endpoints', - 'doc:api:entities' -]); - -gulp.task('doc:api:endpoints', async () => { - const commonVars = await generateVars(); - glob('./src/server/web/docs/api/endpoints/**/*.yaml', (globErr, files) => { - if (globErr) { - console.error(globErr); - return; - } - //console.log(files); - files.forEach(file => { - const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); - const vars = { - endpoint: ep.endpoint, - url: { - host: config.api_url, - path: ep.endpoint - }, - desc: ep.desc, - params: sortParams(ep.params.map(p => parseParam(p))), - paramDefs: extractDefs(ep.params), - res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null, - resDefs: ep.res ? extractDefs(ep.res) : null, - }; - langs.forEach(lang => { - pug.renderFile('./src/server/web/docs/api/endpoints/view.pug', Object.assign({}, vars, { - lang, - title: ep.endpoint, - src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/endpoints/${ep.endpoint}.yaml`, - kebab, - common: commonVars - }), (renderErr, html) => { - if (renderErr) { - console.error(renderErr); - return; - } - const i18n = new I18nReplacer(lang); - html = html.replace(i18n.pattern, i18n.replacement); - html = fa(html); - const htmlPath = `./built/server/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`; - mkdirp(path.dirname(htmlPath), (mkdirErr) => { - if (mkdirErr) { - console.error(mkdirErr); - return; - } - fs.writeFileSync(htmlPath, html, 'utf-8'); - }); - }); - }); - }); - }); -}); - -gulp.task('doc:api:entities', async () => { - const commonVars = await generateVars(); - glob('./src/server/web/docs/api/entities/**/*.yaml', (globErr, files) => { - if (globErr) { - console.error(globErr); - return; - } - files.forEach(file => { - const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); - const vars = { - name: entity.name, - desc: entity.desc, - props: sortParams(entity.props.map(p => parseParam(p))), - propDefs: extractDefs(entity.props), - }; - langs.forEach(lang => { - pug.renderFile('./src/server/web/docs/api/entities/view.pug', Object.assign({}, vars, { - lang, - title: entity.name, - src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/entities/${kebab(entity.name)}.yaml`, - kebab, - common: commonVars - }), (renderErr, html) => { - if (renderErr) { - console.error(renderErr); - return; - } - const i18n = new I18nReplacer(lang); - html = html.replace(i18n.pattern, i18n.replacement); - html = fa(html); - const htmlPath = `./built/server/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`; - mkdirp(path.dirname(htmlPath), (mkdirErr) => { - if (mkdirErr) { - console.error(mkdirErr); - return; - } - fs.writeFileSync(htmlPath, html, 'utf-8'); - }); - }); - }); - }); - }); -}); diff --git a/src/server/web/docs/api/mixins.pug b/src/server/web/docs/api/mixins.pug deleted file mode 100644 index 686bf6a2b6..0000000000 --- a/src/server/web/docs/api/mixins.pug +++ /dev/null @@ -1,37 +0,0 @@ -mixin propTable(props) - table.props - thead: tr - th %i18n:docs.api.props.name% - th %i18n:docs.api.props.type% - th %i18n:docs.api.props.optional% - th %i18n:docs.api.props.description% - tbody - each prop in props - tr - td.name= prop.name - td.type - i= prop.type - if prop.kind == 'id' - if prop.entity - | ( - a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity - | ID) - else - | (ID) - else if prop.kind == 'entity' - | ( - a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity - | ) - else if prop.kind == 'object' - if prop.def - | ( - a(href=`#${prop.defName}`)= prop.defName - | ) - else if prop.kind == 'date' - | (Date) - td.optional - if prop.optional - | %i18n:docs.api.props.yes% - else - | %i18n:docs.api.props.no% - td.desc!= prop.desc[lang] || prop.desc['ja'] diff --git a/src/server/web/docs/api/style.styl b/src/server/web/docs/api/style.styl deleted file mode 100644 index 3675a4da6f..0000000000 --- a/src/server/web/docs/api/style.styl +++ /dev/null @@ -1,11 +0,0 @@ -@import "../style" - -table.props - .name - font-weight bold - - .name - .type - .optional - font-family Consolas, 'Courier New', Courier, Monaco, monospace - diff --git a/src/server/web/docs/gulpfile.ts b/src/server/web/docs/gulpfile.ts deleted file mode 100644 index 7b36cf6675..0000000000 --- a/src/server/web/docs/gulpfile.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Gulp tasks - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as glob from 'glob'; -import * as gulp from 'gulp'; -import * as pug from 'pug'; -import * as mkdirp from 'mkdirp'; -import stylus = require('gulp-stylus'); -import cssnano = require('gulp-cssnano'); - -import I18nReplacer from '../../../build/i18n'; -import fa from '../../../build/fa'; -import generateVars from './vars'; - -require('./api/gulpfile.ts'); - -gulp.task('doc', [ - 'doc:docs', - 'doc:api', - 'doc:styles' -]); - -gulp.task('doc:docs', async () => { - const commonVars = await generateVars(); - - glob('./src/server/web/docs/**/*.*.pug', (globErr, files) => { - if (globErr) { - console.error(globErr); - return; - } - files.forEach(file => { - const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/); - const vars = { - common: commonVars, - lang: lang, - title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1], - src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/${name}.${lang}.pug`, - }; - pug.renderFile(file, vars, (renderErr, content) => { - if (renderErr) { - console.error(renderErr); - return; - } - - pug.renderFile('./src/server/web/docs/layout.pug', Object.assign({}, vars, { - content - }), (renderErr2, html) => { - if (renderErr2) { - console.error(renderErr2); - return; - } - const i18n = new I18nReplacer(lang); - html = html.replace(i18n.pattern, i18n.replacement); - html = fa(html); - const htmlPath = `./built/server/web/docs/${lang}/${name}.html`; - mkdirp(path.dirname(htmlPath), (mkdirErr) => { - if (mkdirErr) { - console.error(mkdirErr); - return; - } - fs.writeFileSync(htmlPath, html, 'utf-8'); - }); - }); - }); - }); - }); -}); - -gulp.task('doc:styles', () => - gulp.src('./src/server/web/docs/**/*.styl') - .pipe(stylus()) - .pipe((cssnano as any)()) - .pipe(gulp.dest('./built/server/web/docs/assets/')) -); diff --git a/src/server/web/docs/index.en.pug b/src/server/web/docs/index.en.pug deleted file mode 100644 index 1fcc870d3d..0000000000 --- a/src/server/web/docs/index.en.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 Misskey Docs - -p Welcome to docs of Misskey. diff --git a/src/server/web/docs/index.ja.pug b/src/server/web/docs/index.ja.pug deleted file mode 100644 index 4a0bf7fa1d..0000000000 --- a/src/server/web/docs/index.ja.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 Misskey ドキュメント - -p Misskeyのドキュメントへようこそ diff --git a/src/server/web/docs/layout.pug b/src/server/web/docs/layout.pug deleted file mode 100644 index 9dfd0ab7af..0000000000 --- a/src/server/web/docs/layout.pug +++ /dev/null @@ -1,41 +0,0 @@ -doctype html - -html(lang= lang) - head - meta(charset="UTF-8") - meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no") - title - | #{title} | Misskey Docs - link(rel="stylesheet" href="/assets/style.css") - block meta - - //- FontAwesome style - style #{common.facss} - - body - nav - ul - each doc in common.docs - li: a(href=`/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] - section - h2 API - ul - li Entities - ul - each entity in common.entities - li: a(href=`/${lang}/api/entities/${common.kebab(entity)}`)= entity - li Endpoints - ul - each endpoint in common.endpoints - li: a(href=`/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint - main - article - block main - if content - | !{content} - - footer - p - | %i18n:docs.edit-this-page-on-github% - a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link% - small= common.copyright diff --git a/src/server/web/docs/license.en.pug b/src/server/web/docs/license.en.pug deleted file mode 100644 index 45d8b76473..0000000000 --- a/src/server/web/docs/license.en.pug +++ /dev/null @@ -1,17 +0,0 @@ -h1 License - -div!= common.license - -details - summary Libraries - - section - h2 Libraries - - each dependency, name in common.dependencies - details - summary= name - - section - h3= name - pre= dependency.licenseText diff --git a/src/server/web/docs/license.ja.pug b/src/server/web/docs/license.ja.pug deleted file mode 100644 index 6eb9ac308e..0000000000 --- a/src/server/web/docs/license.ja.pug +++ /dev/null @@ -1,17 +0,0 @@ -h1 ライセンス - -div!= common.license - -details - summary サードパーティ - - section - h2 サードパーティ - - each dependency, name in common.dependencies - details - summary= name - - section - h3= name - pre= dependency.licenseText diff --git a/src/server/web/docs/mute.ja.pug b/src/server/web/docs/mute.ja.pug deleted file mode 100644 index 5e79af5f8c..0000000000 --- a/src/server/web/docs/mute.ja.pug +++ /dev/null @@ -1,13 +0,0 @@ -h1 ミュート - -p ユーザーページから、そのユーザーをミュートすることができます。 - -p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります: -ul - li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost) - li そのユーザーからの通知 - li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴 - -p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。 - -p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。 diff --git a/src/server/web/docs/search.ja.pug b/src/server/web/docs/search.ja.pug deleted file mode 100644 index e14e8c867e..0000000000 --- a/src/server/web/docs/search.ja.pug +++ /dev/null @@ -1,120 +0,0 @@ -h1 検索 - -p 投稿を検索することができます。 -p - | キーワードを半角スペースで区切ると、and検索になります。 - | 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。 - -section - h2 キーワードの除外 - p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。 - p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります: - code git -コミット - -section - h2 完全一致 - p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。 - p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。 - -section - h2 タグ - p キーワードの前に「#」(シャープ)をプリフィクスすると、そのキーワードと一致するタグを持つ投稿に限定します。 - -section - h2 オプション - p - | オプションを使用して、より高度な検索を行えます。 - | オプションを指定するには、「オプション名:値」という形式でクエリに含めます。 - p 利用可能なオプション一覧です: - - table - thead - tr - th 名前 - th 説明 - tbody - tr - td user - td - | 指定されたユーザー名のユーザーの投稿に限定します。 - | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 - br - | 例えば、 - code user:himawari,sakurako - | と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。 - | (つまりユーザーのホワイトリストです) - tr - td exclude_user - td - | 指定されたユーザー名のユーザーの投稿を除外します。 - | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 - br - | 例えば、 - code exclude_user:akari,chinatsu - | と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。 - | (つまりユーザーのブラックリストです) - tr - td follow - td - | true ... フォローしているユーザーに限定。 - br - | false ... フォローしていないユーザーに限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td mute - td - | mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト) - br - | mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する - br - | mute_direct ... ミュートしているユーザーの投稿だけ除外する - br - | disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める - br - | direct_only ... ミュートしているユーザーの投稿だけに限定 - br - | related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定 - br - | all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定 - tr - td reply - td - | true ... 返信に限定。 - br - | false ... 返信でない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td repost - td - | true ... Repostに限定。 - br - | false ... Repostでない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td media - td - | true ... メディアが添付されている投稿に限定。 - br - | false ... メディアが添付されていない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td poll - td - | true ... 投票が添付されている投稿に限定。 - br - | false ... 投票が添付されていない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td until - td 上限の日時。(YYYY-MM-DD) - tr - td since - td 下限の日時。(YYYY-MM-DD) - - p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります: - code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey diff --git a/src/server/web/docs/server.ts b/src/server/web/docs/server.ts deleted file mode 100644 index b2e50457e5..0000000000 --- a/src/server/web/docs/server.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Docs Server - */ - -import * as express from 'express'; - -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); - -app.use('/assets', express.static(`${__dirname}/assets`)); - -/** - * Routing - */ -app.get(/^\/([a-z_\-\/]+?)$/, (req, res) => - res.sendFile(`${__dirname}/${req.params[0]}.html`)); - -module.exports = app; diff --git a/src/server/web/docs/style.styl b/src/server/web/docs/style.styl deleted file mode 100644 index bc165f8728..0000000000 --- a/src/server/web/docs/style.styl +++ /dev/null @@ -1,120 +0,0 @@ -@import "../style" -@import "./ui" - -body - margin 0 - color #34495e - word-break break-word - -main - margin 0 0 0 256px - padding 64px - width 100% - max-width 768px - - section - margin 32px 0 - - h1 - margin 0 0 24px 0 - padding 16px 0 - font-size 1.5em - border-bottom solid 2px #eee - - h2 - margin 0 0 24px 0 - padding 0 0 16px 0 - font-size 1.4em - border-bottom solid 1px #eee - - h3 - margin 0 - padding 0 - font-size 1.25em - - h4 - margin 0 - - p - margin 1em 0 - line-height 1.6em - - footer - margin 32px 0 0 0 - border-top solid 2px #eee - - > small - margin 16px 0 0 0 - color #aaa - -nav - display block - position fixed - z-index 10000 - top 0 - left 0 - width 256px - height 100% - overflow auto - padding 32px - background #fff - border-right solid 2px #eee - -@media (max-width 1025px) - main - margin 0 - max-width 100% - - nav - position relative - width 100% - max-height 128px - background #f9f9f9 - border-right none - -@media (max-width 768px) - main - padding 32px - -@media (max-width 512px) - main - padding 16px - -table - display block - width 100% - max-width 100% - overflow auto - border-spacing 0 - border-collapse collapse - - thead - font-weight bold - border-bottom solid 2px #eee - - tr - th - text-align left - - tbody - tr - &:nth-child(odd) - background #fbfbfb - - th, td - padding 8px 16px - min-width 128px - -code - display inline-block - padding 8px 10px - font-family Consolas, 'Courier New', Courier, Monaco, monospace - color #295c92 - background #f2f2f2 - border-radius 4px - -pre - overflow auto - - > code - display block diff --git a/src/server/web/docs/tou.ja.pug b/src/server/web/docs/tou.ja.pug deleted file mode 100644 index 7663258f82..0000000000 --- a/src/server/web/docs/tou.ja.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 利用規約 - -p 公序良俗に反する行為はおやめください。 diff --git a/src/server/web/docs/ui.styl b/src/server/web/docs/ui.styl deleted file mode 100644 index 8d5515712f..0000000000 --- a/src/server/web/docs/ui.styl +++ /dev/null @@ -1,19 +0,0 @@ -.ui.info - display block - margin 1em 0 - padding 0 1em - font-size 90% - color rgba(#000, 0.87) - background #f8f8f9 - border-radius 4px - overflow hidden - - > p - opacity 0.8 - - > [data-fa]:first-child - margin-right 0.25em - - &.warn - color #573a08 - background #FFFAF3 diff --git a/src/server/web/docs/vars.ts b/src/server/web/docs/vars.ts deleted file mode 100644 index 5096a39c9e..0000000000 --- a/src/server/web/docs/vars.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as fs from 'fs'; -import * as util from 'util'; -import * as glob from 'glob'; -import * as yaml from 'js-yaml'; -import * as licenseChecker from 'license-checker'; -import * as tmp from 'tmp'; - -import { fa } from '../../../build/fa'; -import config from '../../../conf'; -import { licenseHtml } from '../../../build/license'; -const constants = require('../../../const.json'); - -export default async function(): Promise<{ [key: string]: any }> { - const vars = {} as { [key: string]: any }; - - const endpoints = glob.sync('./src/server/web/docs/api/endpoints/**/*.yaml'); - vars['endpoints'] = endpoints.map(ep => { - const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8')); - return _ep.endpoint; - }); - - const entities = glob.sync('./src/server/web/docs/api/entities/**/*.yaml'); - vars['entities'] = entities.map(x => { - const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8')); - return _x.name; - }); - - const docs = glob.sync('./src/server/web/docs/**/*.*.pug'); - vars['docs'] = {}; - docs.forEach(x => { - const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/); - if (vars['docs'][name] == null) { - vars['docs'][name] = { - name, - title: {} - }; - } - vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]; - }); - - vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); - - vars['config'] = config; - - vars['copyright'] = constants.copyright; - - vars['facss'] = fa.dom.css(); - - vars['license'] = licenseHtml; - - const tmpObj = tmp.fileSync(); - fs.writeFileSync(tmpObj.name, JSON.stringify({ - licenseText: '' - }), 'utf-8'); - const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({ - start: __dirname + '/../../../../', - customPath: tmpObj.name - }); - tmpObj.removeCallback(); - - vars['dependencies'] = dependencies; - - return vars; -} diff --git a/src/server/web/element.scss b/src/server/web/element.scss deleted file mode 100644 index 7e6d0e7099..0000000000 --- a/src/server/web/element.scss +++ /dev/null @@ -1,12 +0,0 @@ -/* Element variable definitons */ -/* SEE: http://element.eleme.io/#/en-US/component/custom-theme */ - -@import '../../const.json'; - -/* theme color */ -$--color-primary: $themeColor; - -/* icon font path, required */ -$--font-path: '~element-ui/lib/theme-chalk/fonts'; - -@import "~element-ui/packages/theme-chalk/src/index"; diff --git a/src/server/web/server.ts b/src/server/web/server.ts index b117f6ae81..2fc8f1b8ab 100644 --- a/src/server/web/server.ts +++ b/src/server/web/server.ts @@ -1,5 +1,5 @@ /** - * Web Server + * Web Client Server */ import * as path from 'path'; @@ -11,9 +11,9 @@ import * as bodyParser from 'body-parser'; import * as favicon from 'serve-favicon'; import * as compression from 'compression'; -/** - * Init app - */ +const client = `${__dirname}/../../client/`; + +// Create server const app = express(); app.disable('x-powered-by'); @@ -25,51 +25,40 @@ app.use(bodyParser.json({ })); app.use(compression()); -/** - * Initialize requests - */ app.use((req, res, next) => { res.header('X-Frame-Options', 'DENY'); next(); }); -/** - * Static assets - */ -app.use(favicon(`${__dirname}/assets/favicon.ico`)); -app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${__dirname}/assets/apple-touch-icon.png`)); -app.use('/assets', express.static(`${__dirname}/assets`, { +//#region static assets + +app.use(favicon(`${client}/assets/favicon.ico`)); +app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${client}/assets/apple-touch-icon.png`)); +app.use('/assets', express.static(`${client}/assets`, { maxAge: ms('7 days') })); -app.use('/assets/*.js', (req, res) => res.sendFile(`${__dirname}/assets/404.js`)); +app.use('/assets/*.js', (req, res) => res.sendFile(`${client}/assets/404.js`)); app.use('/assets', (req, res) => { res.sendStatus(404); }); -app.use('/recover', (req, res) => res.sendFile(`${__dirname}/assets/recover.html`)); +app.use('/recover', (req, res) => res.sendFile(`${client}/assets/recover.html`)); -/** - * ServiceWroker - */ +// ServiceWroker app.get(/^\/sw\.(.+?)\.js$/, (req, res) => - res.sendFile(`${__dirname}/assets/sw.${req.params[0]}.js`)); + res.sendFile(`${client}/assets/sw.${req.params[0]}.js`)); -/** - * Manifest - */ +// Manifest app.get('/manifest.json', (req, res) => - res.sendFile(`${__dirname}/assets/manifest.json`)); + res.sendFile(`${client}/assets/manifest.json`)); -/** - * Common API - */ -app.get(/\/api:url/, require('./service/url-preview')); +//#endregion -/** - * Routing - */ +app.get(/\/api:url/, require('./url-preview')); + +// Render base html for all requests app.get('*', (req, res) => { - res.sendFile(path.resolve(`${__dirname}/app/base.html`), { + res.sendFile(path.resolve(`${client}/app/base.html`), { maxAge: ms('7 days') }); }); diff --git a/src/server/web/style.styl b/src/server/web/style.styl deleted file mode 100644 index 6d1e53e5a6..0000000000 --- a/src/server/web/style.styl +++ /dev/null @@ -1,37 +0,0 @@ -@charset 'utf-8' - -@import "./const" - -/* - ::selection - background $theme-color - color #fff -*/ - -* - position relative - box-sizing border-box - background-clip padding-box !important - tap-highlight-color transparent - -webkit-tap-highlight-color transparent - -html, body - margin 0 - padding 0 - scroll-behavior smooth - text-size-adjust 100% - font-family sans-serif - -a - text-decoration none - color $theme-color - cursor pointer - tap-highlight-color rgba($theme-color, 0.7) !important - -webkit-tap-highlight-color rgba($theme-color, 0.7) !important - - &:hover - text-decoration underline - - * - cursor pointer - diff --git a/src/server/web/service/url-preview.ts b/src/server/web/url-preview.ts index 0c5fd8a78e..0c5fd8a78e 100644 --- a/src/server/web/service/url-preview.ts +++ b/src/server/web/url-preview.ts |