diff --git a/.config/example.yml b/.config/example.yml index cd08f76d61a2685027e62255477961f0df3ff77a..91580d9ecbe459505b02df704b3f007ab2cd4bdf 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -111,10 +111,6 @@ id: 'aid' # ┌─────────────────────┠#───┘ Other configuration └───────────────────────────────────── -# If enabled: -# The first account created is automatically marked as Admin. -autoAdmin: true - # Whether disable HSTS #disableHsts: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a35dc5481a69b1b8b26120a977fc54351d05db6..3b6e2c59a615e0f7a774e4503faec378e9b9732c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ ChangeLog ========= +12.0.0 indigo (unreleased) +-------------------- +### Breaking Chnages +* ãŠçŸ¥ã‚‰ã›ãŒãƒªã‚»ãƒƒãƒˆã•ã‚Œã¾ã™ã€‚ +* 通知ãŒãƒªã‚»ãƒƒãƒˆã•ã‚Œã¾ã™ã€‚ +* モデレーターãŒã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹è¨å®šã‚’閲覧ã—ãŸã‚Šå¤‰æ›´ã—ãŸã‚Šã§ããªããªã‚Šã¾ã™(ãれらãŒã§ãã‚‹ã®ã¯Adminã®ã¿ã«ãªã‚Šã¾ã™)。 + * モデレーターãŒå‡ºæ¥ã‚‹ã®ã¯ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã‚µã‚¤ãƒ¬ãƒ³ã‚¹/å‡çµãªã©ã«é™ã‚‰ã‚Œã¾ã™ã€‚ + * 従æ¥ã¨åŒã˜æ¨©é™ã‚’与ãˆãŸã„å ´åˆã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã‚’Adminã«è¨å®šã™ã‚‹ã“ã¨ã‚’検討ã—ã¦ãã ã•ã„(Adminã¯è¤‡æ•°äººè¨å®šå¯èƒ½ã§ã™)。 +* notes/search APIã®ãƒšãƒ¼ã‚¸ãƒ³ã‚°ãŒoffsetã§ã¯ãªãuntilIdæ–¹å¼ã« +* クライアントã®ãƒ†ãƒ¼ãƒžã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆãŒèª¿æ•´ã•ã‚Œã¾ã—ãŸã€‚ + * 旧テーマを変æ›ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹æ©Ÿèƒ½ãŒäºˆå®šã•ã‚Œã¦ã„ã¾ã™ +* ノートã«ä½ç½®æƒ…å ±ã‚’æ·»ä»˜ã§ãã‚‹æ©Ÿèƒ½ã‚’å»ƒæ¢ +* ノートã«ä½•ã®ã‚¢ãƒ—リã‹ã‚‰æŠ•ç¨¿ã—ãŸã‹ã¨ã„ã†æƒ…å ±ã‚’å«ã‚ã‚‹ã®ã‚’å»ƒæ¢ + +### ✨Improvements +* Webクライアントを一新 + * Syuilo Design System (仮称)を採用ã—ã€å„コンãƒãƒ¼ãƒãƒ³ãƒˆãŒçµ±ä¸€ã•ã‚Œä¸€è²«ã—ãŸãƒ‡ã‚¶ã‚¤ãƒ³ã« + * レスãƒãƒ³ã‚·ãƒ–デザインã«ãªã‚Šã€ãƒ‡ã‚¹ã‚¯ãƒˆãƒƒãƒ—/タブレット/スマートフォンã§åŒã˜æ©Ÿèƒ½ãŒä½¿ãˆã‚‹ã‚ˆã†ã« + * 複数アカウントã«å¯¾å¿œã—ã€ç°¡å˜ã«åˆ¥ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«åˆ‡ã‚Šæ›¿ãˆã‚‰ã‚Œã‚‹ã‚ˆã†ã« + * 通知ã‹ã‚‰ç›´æŽ¥ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’許å¯/æ‹’å¦ã§ãるよã†ã« + * ユーザーã®ç™»éŒ²æ—¥ã‚’表示ã™ã‚‹ã‚ˆã†ã« + * ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’è¿½åŠ + * ユーザーをé¸æŠžã™ã‚‹æ“作ãŒä¾¿åˆ©ã« + * ユーザーページã‹ã‚‰ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ã‚Œã‚‹ã‚ˆã†ã« + * ユーザーページã‹ã‚‰ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨ãƒˆãƒ¼ã‚¯ã‚’開始ã§ãるよã†ã« + * 「戻るã€ãƒœã‚¿ãƒ³ã‚’è¿½åŠ ã—ã€PWAフレンドリー㫠+ * 軽é‡åŒ– +* ãŠçŸ¥ã‚‰ã›æ©Ÿèƒ½ã®å¼·åŒ– + * ãŠçŸ¥ã‚‰ã›ãŒæœªèªã‹æ—¢èªã‹ç®¡ç†ã•ã‚Œã‚‹ã‚ˆã†ã«ãªã‚Šã€æœªèªã®ãŠçŸ¥ã‚‰ã›ãŒã‚ã‚‹ã¨åˆ†ã‹ã‚Šã‚„ã™ã表示ã•ã‚Œã‚‹ã‚ˆã†ã« + * 何人ãŒãŠçŸ¥ã‚‰ã›ã‚’èªã‚“ã ã‹åˆ†ã‹ã‚‹ã‚ˆã†ã« +* アンテナ機能 + * 指定ã—ãŸæ¡ä»¶(ã‚ーワードã€ãƒ•ã‚¡ã‚¤ãƒ«æ·»ä»˜ã®æœ‰ç„¡ãªã©)ã«ãƒžãƒƒãƒã™ã‚‹æŠ•ç¨¿ã®ã‚¿ã‚¤ãƒ ラインを見れる機能 + * æ–°ã—ã„投稿ãŒã‚ã£ãŸã¨ã通知ã™ã‚‹ã‚ˆã†ã«ã‚‚ã§ãã‚‹ + * ウィジェットã¨ã—ã¦ã‚‚表示å¯èƒ½ +* Elasticsearchをインストールã—ãªãã¦ã‚‚全文検索ã§ãるよã†ã« +* リモートã®ã‚«ã‚¹ã‚¿ãƒ 絵文å—をコピーã—ã¦ãã‚‹æ©Ÿèƒ½ã‚’è¿½åŠ +* 自分ã®é€ã£ãŸãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæ‰¿èªã•ã‚ŒãŸã¨ãã®é€šçŸ¥ã‚’è¿½åŠ +* 他多数 + +### ðŸ›Fixes +* ミュートã—ã¦ã„る人ã‹ã‚‰ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³é€šçŸ¥ãŒã‚ã‚‹ã¨ã€é€šçŸ¥ãŒã‚ã‚‹ã¨è¡¨ç¤ºã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ +* 投稿メニューを開ã„ã¦æ“作ã—ãŸå¾Œã«ã‚‚ã†ä¸€åº¦ãƒ¡ãƒ‹ãƒ¥ãƒ¼ã‚’é–‹ã“ã†ã¨ã—ã¦ã‚‚ã§ããªã„å•é¡Œã‚’ä¿®æ£ +* リモートã®ãƒŽãƒ¼ãƒˆã®URLãŒæ›¸ã‹ã‚Œã¦ã„ãŸå ´åˆã€å‹•ä½œãŒãŠã‹ã—ã„å•é¡Œã‚’ä¿®æ£ +* リストTLã ã¨Tã§ã®TLフォーカスãŒåŠ¹ã‹ãªã„å•é¡Œã‚’ä¿®æ£ +* OAuthèªè¨¼ç”»é¢ã®é…色ãŒãŠã‹ã—ã„å•é¡Œã‚’ä¿®æ£ +* è¨å®šç”»é¢ã§ã€ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ã¦ã‚‚ã‚¢ãƒã‚¿ãƒ¼ã®ç”»åƒãŒãã®å ´ã§æ›´æ–°ã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +* 投稿詳細/ユーザー詳細 ç”»é¢ã§adminã‚„å…¬å¼ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒžãƒ¼ã‚¯ãŒè¡¨ç¤ºã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +* APIã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆæ–¹æ³•(websocket/HTTP)ã«ã‚ˆã£ã¦è¿”ã£ã¦ãるエラーã®å†…容ã«é•ã„ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +* Pages: VERSION 変数ãŒå¸¸ã« null ãªå•é¡Œã‚’ä¿®æ£ + 11.37.1 (2020/01/07) -------------------- ### ðŸ›Fixes diff --git a/gulpfile.ts b/gulpfile.ts index 2ba30aace19947f6ec5dc972023979130c300a83..274f05a5a84d9e0e943df5336f6ee8e2d5b1884a 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -2,38 +2,25 @@ * Gulp tasks */ +import * as fs from 'fs'; import * as gulp from 'gulp'; import * as ts from 'gulp-typescript'; -const sourcemaps = require('gulp-sourcemaps'); -import tslint from 'gulp-tslint'; -const stylus = require('gulp-stylus'); import * as rimraf from 'rimraf'; -import * as chalk from 'chalk'; import * as rename from 'gulp-rename'; -import * as mocha from 'gulp-mocha'; -import * as replace from 'gulp-replace'; const cleanCSS = require('gulp-clean-css'); -const terser = require('gulp-terser'); +const sass = require('gulp-dart-sass'); +const fiber = require('fibers'); const locales = require('./locales'); - -const env = process.env.NODE_ENV || 'development'; -const isDebug = env !== 'production'; - -if (isDebug) { - console.warn(chalk.yellow.bold('WARNING! NODE_ENV is not "production".')); - console.warn(chalk.yellow.bold(' built script will not be compressed.')); -} +const meta = require('./package.json'); gulp.task('build:ts', () => { const tsProject = ts.createProject('./tsconfig.json'); return tsProject .src() - .pipe(sourcemaps.init()) .pipe(tsProject()) .on('error', () => {}) - .pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' })) .pipe(gulp.dest('./built/')); }); @@ -41,47 +28,25 @@ gulp.task('build:copy:views', () => gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views')) ); -gulp.task('build:copy:fonts', () => - gulp.src('./node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/client/assets/fonts/')) -); +gulp.task('build:copy:locales', cb => { + fs.mkdirSync('./built/client/assets/locales', { recursive: true }); -gulp.task('build:copy', gulp.parallel('build:copy:views', 'build:copy:fonts', () => + for (const [lang, locale] of Object.entries(locales)) { + fs.writeFileSync(`./built/client/assets/locales/${lang}.${meta.version}.json`, JSON.stringify(locale), 'utf-8'); + } + + cb(); +}); + +gulp.task('build:copy', gulp.parallel('build:copy:views', 'build:copy:locales', () => gulp.src([ - './src/const.json', './src/emojilist.json', './src/server/web/views/**/*', './src/**/assets/**/*', - '!./src/client/app/**/assets/**/*' + '!./src/client/assets/**/*' ]).pipe(gulp.dest('./built/')) )); -gulp.task('lint', () => - gulp.src('./src/**/*.ts') - .pipe(tslint({ - formatter: 'verbose' - })) - .pipe(tslint.report()) -); - -gulp.task('format', () => - gulp.src('./src/**/*.ts') - .pipe(tslint({ - formatter: 'verbose', - fix: true - })) - .pipe(tslint.report()) -); - -gulp.task('mocha', () => - gulp.src('./test/**/*.ts') - .pipe(mocha({ - exit: true, - require: 'ts-node/register' - } as any)) -); - -gulp.task('test', gulp.task('mocha')); - gulp.task('clean', cb => rimraf('./built', cb) ); @@ -90,20 +55,9 @@ gulp.task('cleanall', gulp.parallel('clean', cb => rimraf('./node_modules', cb) )); -gulp.task('build:client:script', () => { - const client = require('./built/meta.json'); - return gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js']) - .pipe(replace('VERSION', JSON.stringify(client.version))) - .pipe(replace('ENV', JSON.stringify(env))) - .pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) - .pipe(terser({ - toplevel: true - })) - .pipe(gulp.dest('./built/client/assets/')); -}); - gulp.task('build:client:styles', () => - gulp.src('./src/client/app/init.css') + gulp.src('./src/client/style.scss') + .pipe(sass({ fiber })) .pipe(cleanCSS()) .pipe(gulp.dest('./built/client/assets/')) ); @@ -112,7 +66,6 @@ gulp.task('copy:client', () => gulp.src([ './assets/**/*', './src/client/assets/**/*', - './src/client/app/*/assets/**/*' ]) .pipe(rename(path => { path.dirname = path.dirname!.replace('assets', '.'); @@ -120,15 +73,7 @@ gulp.task('copy:client', () => .pipe(gulp.dest('./built/client/assets/')) ); -gulp.task('doc', () => - gulp.src('./src/docs/**/*.styl') - .pipe(stylus()) - .pipe(cleanCSS()) - .pipe(gulp.dest('./built/docs/assets/')) -); - gulp.task('build:client', gulp.parallel( - 'build:client:script', 'build:client:styles', 'copy:client' )); @@ -137,7 +82,6 @@ gulp.task('build', gulp.parallel( 'build:ts', 'build:copy', 'build:client', - 'doc' )); gulp.task('default', gulp.task('build')); diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml deleted file mode 100644 index ed97d539c095cf1413af30cc23dea272095b97dd..0000000000000000000000000000000000000000 --- a/locales/ca-ES.yml +++ /dev/null @@ -1 +0,0 @@ ---- diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml deleted file mode 100644 index 96135478c11d0ff9a173cddc606d9db11ed11fa2..0000000000000000000000000000000000000000 --- a/locales/cs-CZ.yml +++ /dev/null @@ -1,1481 +0,0 @@ ---- -meta: - lang: "ÄŒeÅ¡tina" -common: - misskey: "â ve fedivesmÃru" - about-title: "â ve fedivesmÃru." - about: "DÄ›kujeme, že jste naÅ¡li Misskey. Misskey je <b>decentralizovaná mikroblogovacà platforma</b> zrozená na Zemi. NeboÅ¥ existuje ve fedivesmÃru (vesmÃru, kde jsou organizovány různé sociálnà sÃtÄ›), je vzájemnÄ› propojena s jinými sociálnÃmi sÃtÄ›mi. Co takhle si chvilku odpoÄinout od ruchu a shonu mÄ›sta a ponoÅ™it se do nového internetu?" - intro: - title: "Co je Misskey?" - about: "Misskey je open-source <b>decentralizovaný mikroblogovacà software</b>. Má sofistikované, zcela pÅ™izpůsobitelné uživatelské rozhranÃ, různé způsoby reagovánà na pÅ™ÃspÄ›vky, bezplatné úložiÅ¡tÄ› souborů nabÃzejÃcà integrovaný management system, a dalÅ¡Ã pokroÄilé vlastnosti. Misskey je navÃc pÅ™ipojeno k systému sÃtà zvanému „fedivesmÃr“ nebo „fediverse“, který nám dovoluje komunikovat s uživateli na jiných sociálnÃch sÃtÃ. Pokud napÅ™Ãklad nÄ›co napÃÅ¡ete, nebude to posláno pouze uživatelů Misskey, ale také lidem na sÃtÃch Mastodon a Pleroma. Jen si pÅ™edstavte, že planeta posÃlá jiné planetÄ› rádiový signál, aby s nà komunikovala." - features: "Vlastnosti" - rich-contents: "PÅ™ÃspÄ›vky" - rich-contents-desc: "Pouze napiÅ¡te svoje nápady, žhavá témata a cokoliv, co chcete sdÃlet. Můžete ozdobit svá slova, pÅ™ipojit vaÅ¡e oblÃbené obrázky, posÃlat soubory vÄetnÄ› videà Äi vytvoÅ™it hlasovánà – to je jen nÄ›kolik vÄ›cÃ, co můžete dÄ›lat s Misskey!" - reaction: "Reakce" - reaction-desc: "NejsnadnÄ›jÅ¡Ã způsob, jak vyjádÅ™it své emoce. Misskey vám dovoluje pÅ™idávat k pÅ™ÃspÄ›vkům ostatnÃch lidà různé reakce. EmoÄnà zážitek na Misskey nebude nikdy na jiných sociálnÃch sÃtÃch, které mohou dávat pouze „lajky“." - ui: "RozhranÃ" - ui-desc: "Jediné rozhranà nenà pro vÅ¡echny. Misskey má proto vysoce pÅ™izpůsobitelné rozhranà pro váš vkus. Můžete si vytvoÅ™it svou vlastnà originálnà domovskou stránku upravenÃm vzhledu vaÅ¡Ã Äasové osy a posunovánÃm widgetů, které můžete snadno upravit, a uÄinit toto mÃsto svým vlastnÃm." - drive: "Disk" - drive-desc: "Chcete sdÃlet obrázek, který jste již nahráli? Chcete vaÅ¡e nahrané soubory organizovat, pojmenovávat a vytvářet pro nÄ› složky? Misskey Disk je pro vás nejlepÅ¡Ã Å™eÅ¡enÃ. Je velmi snadné sdÃlet vaÅ¡e soubory online." - outro: "PodÃvejte se na unikátnà vlastnosti Misskey vlastnÃma oÄima! Pokud si myslÃte, že tato instance nenà pro vás, zkuste jiné instance, neboÅ¥ Misskey je decentralizovaná sociálnà sÃÅ¥, takže můžete snadno najÃt své přátele. HodnÄ› Å¡tÄ›stà a zábavy!" - application-authorization: "Autorizované aplikace" - close: "ZavÅ™Ãt" - do-not-copy-paste: "ProsÃm nezadávejte ani nevkládejte sem kód. Váš úÄet může být kompromitován." - load-more: "NaÄÃst vÃce" - enter-password: "ProsÃm zadejte heslo" - 2fa: "Dvoufaktorová autentikace" - customize-home: "PÅ™izpůsobit vzhled domovské stránky" - featured-notes: "OblÃbené poznámky" - dark-mode: "Tmavý režim" - signin: "PÅ™ihlásit" - signup: "Registrovat" - signout: "Odhlásit" - reload-to-apply-the-setting: "Pro uplatnÄ›nà tohoto nastavenà musÃte znovu naÄÃst tuto stránku. Chcete ji naÄÃst teÄ?" - fetching-as-ap-object: "NaÄÃtám data z Fediversu..." - delete-confirm: "Opravdu chcete smazat tento pÅ™ÃspÄ›vek?" - signin-required: "PÅ™ihlaÅ¡te se, prosÃm" - notification-type: "Typy oznámenÃ" - notification-types: - all: "VÅ¡echny" - pollVote: "Hlasy" - follow: "SledovanÃ" - receiveFollowRequest: "Žádost o sledovánÃ" - reply: "OdpovÄ›di" - quote: "Citace" - renote: "Renotovat" - mention: "ZmÃnky" - reaction: "Reakce" - got-it: "RozumÃm!" - customization-tips: - title: "Tipy pro pÅ™izpůsobenÃ" - paragraph: "<p>PÅ™izpůsobovánà domovské stránky vám dovoluje pÅ™idávat/odstraňovat, pÅ™etahovat a a uspořádat widgety.</p><p>Můžete zmÄ›nit zobrazenà <strong><strong>pravým</strong> kliknutÃm</strong> na nÄ›které widgety.</p><p>Můžete widgety smazat jejich pÅ™etaženÃm do <strong>prostoru oznaÄeného jako „Koš“</strong> v záhlavà stránky.</p><p>PÅ™izpůsobovánà dokonÄÃte kliknutÃm na tlaÄÃtko „Hotovo“ v pravéh hornÃm rohu.</p>" - gotit: "RozumÃm!" - notification: - file-uploaded: "Soubor nahrán!" - message-from: "Zpráva od uživatele {}:" - reversi-invited: "Pozván/a ke hÅ™e" - reversi-invited-by: "Pozván/a uživatelem {}:" - notified-by: "Oznámenà od uživatele {}:" - reply-from: "OdpovÄ›Ä uživatele {}:" - quoted-by: "Citováno uživatelem {}:" - time: - unknown: "neznámý Äas" - future: "budoucÃ" - just_now: "teÄ" - seconds_ago: "pÅ™ed {} s" - minutes_ago: "pÅ™ed {} min" - hours_ago: "pÅ™ed {} h" - days_ago: "pÅ™ed {} d" - weeks_ago: "pÅ™ed {} týd" - months_ago: "pÅ™ed {} mÄ›s" - years_ago: "pÅ™ed {} lety" - month-and-day: "{day}. {month}." - trash: "KoÅ¡" - drive: "Disk" - pages: "Stránky" - messaging: "Konverzace" - home: "Domů" - deck: "Deck" - timeline: "ÄŒasová osa" - explore: "Objevovat" - following: "SledovanÃ" - followers: "SledujÃcÃ" - favorites: "OblÃbené" - permissions: - "read:account": "Zobrazit informace o úÄtu" - "write:account": "NarábÄ›t s úÄtem" - "read:blocks": "ProhlÞet blokovánÃ" - "write:blocks": "NarábÄ›t s blokovánÃm" - "read:drive": "ProhlÞet Disk" - "write:drive": "Pracovat s Diskem" - "read:favorites": "ProhlÞet oblÃbené" - "write:favorites": "NarábÄ›t s oblÃbenÃmi" - "read:following": "ProhlÞet následovánÃ" - "write:following": "Pracovat s následovánÃm" - "read:messaging": "ProhlÞet konverzaci" - "write:messaging": "Pracovat s konverzaci" - "read:mutes": "ProhlÞet ztlumené" - "write:mutes": "NarábÄ›t s utÃÅ¡enÃmi" - "write:notes": "NarábÄ›t s poznámkami" - "read:notifications": "ProhlÞet oznámenÃ" - "write:notifications": "Pracovat s oznámenÃmi" - "read:reactions": "ProhlÞet reakce" - "write:reactions": "NarabÄ›t s reakcemi" - "write:votes": "Hlasovat" - "read:pages": "Zhlédnutà stránky" - "write:pages": "Upravit stránky" - "write:user-groups": "Upravit uživatelskou skupinu" - empty-timeline-info: - follow-users-to-make-your-timeline: "Poznámky sledujÃcÃch se zobrazà ve vaÅ¡Ã Äasové ose" - explore: "NajÃt uživatele" - post-form: - attach-location-information: "PÅ™idat informace o lokaci" - hide-contents: "Schovat obsah" - reply-placeholder: "OdpovÄ›dÄ›t na tento pÅ™ÃspÄ›vek" - quote-placeholder: "Citovat tento pÅ™ÃspÄ›vek" - quote-attached: "PÅ™iložit citaci" - submit: "Odeslat" - reply: "OdpovÄ›dÄ›t" - renote: "Renotovat" - posting: "PosÃlánÃ" - attach-media-from-local: "Nahrát soubor z vaÅ¡eho zaÅ™ÃzenÃ" - attach-media-from-drive: "PÅ™iložit soubory z vaÅ¡eho Drivu" - insert-a-kao: "v('ω')v" - create-poll: "VytvoÅ™it anketu" - text-remain: "zbývá jeÅ¡tÄ› {} znaků" - recent-tags: "NejnovÄ›jÅ¡Ã" - local-only-message: "Publikovat zprávu pouze lokálnÄ›" - click-to-tagging: "Klikni pro otágovánÃ" - visibility: "Viditelnost" - geolocation-alert: "VaÅ¡e zaÅ™Ãzenà nedalo k dispozici lokaci" - error: "Chyba" - enter-username: "Zadejte své uživatelské jméno" - specified-recipient: "Pro" - add-visible-user: "PÅ™idat uživatele" - username-prompt: "Zadejte své uživatelské jméno" - enter-file-name: "Upravit název souboru" - weekday-short: - sunday: "Ne" - monday: "Po" - tuesday: "Út" - wednesday: "St" - thursday: "ÄŒt" - friday: "Pá" - saturday: "So" - weekday: - sunday: "NedÄ›le" - monday: "PondÄ›lÃ" - tuesday: "Úterý" - wednesday: "StÅ™eda" - thursday: "ÄŒtvrtek" - friday: "Pátek" - saturday: "Sobota" - reactions: - like: "Lajk" - love: "Super" - laugh: "SmÃch" - hmm: "Hmm...?" - surprise: "PÅ™ekvapenÃ" - congrats: "Gratuluji!" - angry: "NaÅ¡tvaný" - confused: "Zmatený" - rip: "RIP" - pudding: "Pudink" - note-visibility: - public: "VeÅ™ejná" - home: "Domovská" - home-desc: "Poslat pouze na domovskou Äasovou osu" - followers: "Pro sledujÃcÃ" - followers-desc: "Poslat pouze sledujÃcÃm" - specified: "PÅ™Ãmá" - specified-desc: "Poslat pouze zmÃnÄ›ným uživatelům" - local-public: "VeÅ™ejná (pouze mÃstnÃ)" - local-home: "Domovská (pouze mÃstnÃ)" - local-followers: "Pro sledujÃcà (pouze mÃstnÃ)" - note-placeholders: - a: "Co právÄ› dÄ›láte?" - b: "Co se dÄ›je?" - c: "Co se vám honà hlavou?" - d: "NapÃÅ¡ete pár slov?" - e: "PiÅ¡te sem" - f: "ÄŒekám, až nÄ›co napÃÅ¡ete..." - settings: "NastavenÃ" - _settings: - profile: "Profil" - notification: "OznámenÃ" - apps: "Aplikace" - tags: "Hashtagy" - mute-and-block: "Ztlumit/blokovat" - blocking: "BlokovánÃ" - security: "ZabezpeÄenÃ" - signin: "Historie pÅ™ihlášenÃ" - password: "Heslo" - other: "OstatnÃ" - appearance: "Vzhled" - behavior: "ChovánÃ" - reactions: "Reakce" - fetch-on-scroll: "NekoneÄné naÄÃtanà posuvem" - fetch-on-scroll-desc: "Pokud budete rolovat dolů po stránce, automaticky bude naÄten dalÅ¡Ã obsah." - note-visibility: "Viditelnost pÅ™ÃspÄ›vku" - default-note-visibility: "Výchozà viditelnost pÅ™ÃspÄ›vku" - remember-note-visibility: "Zapamatovat viditelnost pÅ™ÃspÄ›vků" - web-search-engine: "Webové vyhledávaÄe" - web-search-engine-desc: "NapÅ™Ãklad: https://www.google.com/?#q={{query}}" - paste: "Vložit" - pasted-file-name-desc: "NapÅ™Ãklad: \"rrrr-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" - paste-dialog: "Upravit název vloženého souboru" - keep-cw: "Zachovat varovánà o obsahu" - keep-cw-desc: "PÅ™i odpovÄ›di na pÅ™ÃspÄ›vek bude varovánà o obsahu nastaveno stejnÄ› jako původnà pÅ™ÃspÄ›vek." - i-like-sushi: "Mam radÅ¡i sushi (než puding)" - show-reversi-board-labels: "Zobrazit oznaÄenà řad a sloupců v Reversi" - use-avatar-reversi-stones: "PoužÃt avatar jako figurku v Reversi" - disable-animated-mfm: "Vypnout pohyblivé texty v pÅ™ÃspÄ›vku" - disable-showing-animated-images: "NepÅ™ehrávat animované obrázky" - suggest-recent-hashtags: "Navrhovat nedávné hashtagy v rámci psacÃho pole" - always-show-nsfw: "Vždycky ukázat NSFW obsah" - always-mark-nsfw: "OznaÄovat vÅ¡echny pÅ™ÃspÄ›vky za delikátnÃ" - show-full-acct: "Zaradit hostovacà server jako souÄast pÅ™ezdÃvky" - show-via: "zobrazit pÅ™es" - reduce-motion: "SnÞit pohyb v rozhranÃ" - this-setting-is-this-device-only: "Pouze pro toto zaÅ™ÃzenÃ" - use-os-default-emojis: "PoužÃt výchozà emoji systému" - line-width: "Hrubka lÃnie" - line-width-thin: "Úzka" - line-width-normal: "Běžná" - line-width-thick: "Tlustá" - font-size: "Velikost pÃsma" - font-size-x-small: "Malé" - font-size-small: "Dost malé" - font-size-medium: "PrůmÄ›rné" - font-size-large: "Dost velké" - font-size-x-large: "Velké" - deck-column-align: "Zarovnánà sloupců v Decku" - deck-column-align-center: "Na stÅ™ed" - deck-column-align-left: "Vlevo" - deck-column-align-flexible: "FlexibilnÃ" - deck-column-width: "Å ÃÅ™ka sloupců v Decku" - deck-column-width-narrow: "Úzké" - deck-column-width-narrower: "PonÄ›kud úzké" - deck-column-width-normal: "NormálnÃ" - deck-column-width-wider: "PonÄ›kud Å¡iroké" - deck-column-width-wide: "Å iroké" - use-shadow: "PoužÃvat v rozhranà stÃny" - rounded-corners: "Zakulatit rohy v rozhranÃ" - circle-icons: "PoužÃvat kulaté avatary" - contrasted-acct: "PÅ™idat uživatelskému úÄtu kontrast" - wallpaper: "Obrázek na pozadÃ" - choose-wallpaper: "Zvolit pozadÃ" - delete-wallpaper: "Odstranit pozadÃ" - post-form-on-timeline: "Zobrazit formulář pro nové pÅ™ÃspÄ›vky nad Äasovou osou" - show-clock-on-header: "Zobrazit hodiny v pravém hornÃm rohu" - show-reply-target: "Zobrazit cÃl odpovÄ›di" - timeline: "ÄŒasová osa" - show-my-renotes: "Zobrazit moje renoty v Äasové ose" - show-renoted-my-notes: "Zobrazit renoty vaÅ¡ich vlastnÃch pÅ™ÃspÄ›vků v Äasové ose" - show-local-renotes: "Zobrazit renoty mÃstnÃch pÅ™ÃspÄ›vků v Äasové ose" - remain-deleted-note: "I nadále zobrazovat odstranÄ›né pÅ™ÃspÄ›vky" - sound: "Zvuk" - enable-sounds: "Povolit zvuk" - enable-sounds-desc: "PÅ™ehrát zvuk, napÅ™Ãklad pÅ™i odeslánà nebo pÅ™ijetà pÅ™ÃspÄ›vku, Äi zprávy. Toto nastavenà je uloženo v prohlÞeÄi." - volume: "Hlasitost" - test: "Test" - update: "Aktualizace Misskey" - version: "Verze:" - latest-version: "NejnovÄ›jÅ¡Ã verze:" - update-checking: "Kontroluji aktualizace" - do-update: "Zkontrolovat aktualizace" - update-settings: "PokroÄilá nastavenÃ" - no-updates: "Nejsou dostupné žádné aktualizace" - no-updates-desc: "Váš server Misskey je aktuálnÃ." - update-available: "Je dostupná nová verze" - update-available-desc: "Aktualizace budou aplikovány po znovunaÄtenà stránky." - advanced-settings: "PokroÄilá nastavenÃ" - debug-mode: "Povolit režim ladÄ›nÃ" - debug-mode-desc: "Toto nastavenà je uloženo v prohlÞeÄi." - navbar-position: "Poloha navigaÄnÃho panelu" - navbar-position-top: "NahoÅ™e" - navbar-position-left: "Vlevo" - navbar-position-right: "Vpravo" - i-am-under-limited-internet: "Mam omezený (pomalý) internet" - post-style: "Styl zobrazenà poznámek" - post-style-standard: "StandardnÃ" - post-style-smart: "Chytrý" - notification-position: "Poloha oznámenÃ" - notification-position-bottom: "Dole" - notification-position-top: "NahoÅ™e" - disable-via-mobile: "NeoznaÄovat pÅ™ÃspÄ›vky jako „z mobilu“" - load-raw-images: "Zobrazovat obrázky v původnà kvalitÄ›" - load-remote-media: "Zobrazovat média ze vzdáleného serveru" - sync: "Synchronizace" - save: "Uložit" - saved: "Uloženo" - preview: "Náhled" - room: "MÃstnost" - _room: - graphicsQuality: "Kvalita grafiky" - _graphicsQuality: - ultra: "NejvyÅ¡Å¡Ã" - high: "Vysoká" - medium: "StÅ™ednÃ" - low: "NÃzká" - cheep: "NejnižšÃ" - search: "HledánÃ" - delete: "Odstranit" - loading: "NaÄÃtám..." - ok: "OK" - cancel: "ZruÅ¡it" - update-available-title: "Aktualizace k dispozici" - update-available: "Je k dispozici nová verze Misskey ({newer},vaÅ¡e verze je {current}). Pro aplikovánà nové verze znovunaÄtÄ›te stránku." - my-token-regenerated: "Váš token byl regenerován, proto budete odhlášen/a." - hide-password: "Skrýt heslo" - show-password: "Zobrazit heslo" - enter-username: "Zadejte své uživatelské jméno" - do-not-use-in-production: "Tohle je vývojářský build. NepoužÃvejte v produkci." - user-suspended: "Tomuto uživateli byl pozastaven úÄet." - is-remote-user: "Informace o tomto uživateli nemusà být kompletnÃ." - is-remote-post: "Obsah tohoto pÅ™ÃspÄ›vku je zrcadlen." - view-on-remote: "Pro kompletnost jej zobrazte vzdálenÄ›." - renoted-by: "{user} renotoval/a" - no-notes: "Bez poznámek" - turn-on-darkmode: "PÅ™epnout na tmavý režim" - turn-off-darkmode: "SvÄ›tlý režim" - error: - title: "NÄ›co se stalo :(" - retry: "Zkusit znovu" - reversi: - drawn: "RemÃza" - my-turn: "Váš tah" - opponent-turn: "Je Å™ada na protivnÃkovi" - turn-of: "{name} je na tahu" - past-turn-of: "{name} byl/a na tahu" - won: "{name} vyhrál/a" - black: "ÄŒerná" - white: "BÃlá" - total: "Celkem" - this-turn: "{count}. kolo" - widgets: - analog-clock: "Analogové hodiny" - profile: "Profil" - calendar: "Kalendář" - timemachine: "Kalendář (Stroj Äasu)" - activity: "Aktivita" - rss: "RSS ÄteÄka" - memo: "Rychlé poznámky" - trends: "Trendy" - photo-stream: "Proud fotek" - posts-monitor: "Grafy pÅ™ÃspÄ›vků" - slideshow: "Prezentace" - version: "Verze" - broadcast: "Rozhlas" - notifications: "OznámenÃ" - users: "DoporuÄenà uživatelé" - polls: "Ankety" - post-form: "Formulář pro psanÃ" - server: "Informace o serveru" - nav: "Navigace" - tips: "Tipy" - hashtags: "Hashtagy" - queue: "Ve frontÄ›" - dev: "NepodaÅ™ilo se vytvoÅ™it aplikace. ProsÃm zkuste to znovu." - ai-chan-kawaii: "Ai-chan kawaii!" - you: "Vy" -auth/views/form.vue: - share-access: "Chcete dovolit aplikaci <i>{name}</i> pÅ™Ãstup k vaÅ¡emu úÄtu?" - permission-ask: "Tato aplikace vyžaduje následujÃcà oprávnÄ›nÃ:" - cancel: "ZruÅ¡it" - accept: "Povolit pÅ™Ãstup" -auth/views/index.vue: - loading: "NaÄÃtám..." - denied-paragraph: "Tato aplikace nebude mÃt pÅ™Ãstup k VaÅ¡emu úÄtu." - already-authorized: "Tato aplikace byla již autorizována." - callback-url: "Zpátky do aplikace." - please-go-back: "ProsÃm vraÅ¥te se zpátky do aplikace." - error: "Taková relace neexistuje." - sign-in: "ProsÃm pÅ™ihlaste se." -common/views/pages/explore.vue: - popular-users: "Populárnà uživatelé" - recently-updated-users: "Nedávno aktÃvni uživatelé" - recently-registered-users: "Nedávno registrovanà uživatelé" - popular-tags: "Populárnà tagy" - federated: "Z fedivesmÃru" - explore: "Prozkoumat {host}" - users-info: "AktuálnÄ› je zde registrováno {users} uživatelů" -common/views/components/url-preview.vue: - enable-player: "OtevÅ™Ãt v pÅ™ehrávaÄi" - disable-player: "ZavÅ™Ãt pÅ™ehrávaÄ" -common/views/components/user-list.vue: - no-users: "Žádnà uživatelé" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "ÄŒeká se na {}" - cancel: "ZruÅ¡it" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Vzdát se" - surrendered: "VzdanÃm se" - looped-map: "Zacyklená mapa" - can-put-everywhere: "Lze položit kamkoliv" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "Hrajte Reversi s VaÅ¡imi kamarády!" - invite: "Pozvat" - rule: "Jak hrát" - mode-invite: "Pozvat" - invitations: "Jste pozvanà ke hÅ™e!" - my-games: "Moje hra" - all-games: "VÅ¡echny hry" - enter-username: "Zadejte své uživatelské jméno" - game-state: - ended: "UkonÄené" - playing: "ProbÃhajÃ" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Nastavenà hry" - choose-map: "Vybrat mapu" - random: "NáhodnÄ›" - black-or-white: "ÄŒerné/bÃlé" - black-is: "ÄŒerná je {}" - rules: "Pravidla" - looped-map: "Zacyklená mapa" - settings-of-the-bot: "Nastavenà Botu" - this-game-is-started-soon: "Hra zaÄne za pár vteÅ™in" - waiting-for-other: "ÄŒeká se na protivnÃka" - waiting-for-me: "ÄŒeká se na Vás" - waiting-for-both: "PÅ™ipravuji" - cancel: "ZruÅ¡it" - ready: "PÅ™ipraveno" - cancel-ready: "PokraÄovat v pÅ™ÃpravÄ›" -common/views/components/connect-failed.vue: - title: "Nelze se pÅ™ipojit k serveru" - description: "Nastal problém s VaÅ¡Ãm pÅ™ipojenÃm k internetu, nebo server nenà dostupný nebo zrovna probÃhá údržba. ProsÃm {zkuste to znova} za pár minut." - thanks: "DÄ›kujeme že jste použili Misskey." - troubleshoot: "OdstranÄ›nà problémů" -common/views/components/connect-failed.troubleshooter.vue: - title: "Poradce pÅ™i potÞÃch" - network: "SÃÅ¥ové pÅ™ipojenÃ" - checking-network: "Prověřit sÃÅ¥ové pÅ™ipojenÃ" - internet: "PÅ™ipojenà k internetu" - checking-internet: "Ověřuji pÅ™ipojenà k internetu." - server: "PÅ™ipojenà k serveru" - checking-server: "Spojuji se se serverem" - finding: "VyÅ¡etÅ™ovánà problému" - no-network: "Žádné pÅ™ipojenà k sÃti" - no-network-desc: "UjistÄ›te se že jste pÅ™ipojeni k Internetu." - no-internet: "Nejste pÅ™ipojeni k internetu" - no-internet-desc: "Jste pÅ™ipojen k sÃti, ale zdá se že stále chybà pÅ™ipojenà k Internetu. ProsÃm zkontrolujte VaÅ¡e pÅ™ipojenà k Internetu." - no-server: "Nelze se pÅ™ipojit k serveru Misskey" - success: "ÚspěšnÄ› se podaÅ™ilo spojit s Misskey serverem" - flush: "VyÄistit mezipaměť" - set-version: "Vyberte verzi" -common/views/components/media-banner.vue: - sensitive: "Choulostivý obsah" - click-to-show: "KliknÄ›te pro zobrazenÃ" -common/views/components/theme.vue: - theme: "Vzhled" - light-theme: "Motiv pro použità ve svÄ›tlém vzhledu" - dark-theme: "Motiv pro použità v tmavém vzhledu" - light-themes: "SvÄ›tlý vzhled" - dark-themes: "Tmavý vzhled" - install-a-theme: "Nainstalovat motiv" - theme-code: "Kód motivu" - install: "Nainstalovat" - installed: "\"{}\" byl nainstalován" - create-a-theme: "VytvoÅ™it motiv" - save-created-theme: "Uložit motiv" - primary-color: "Základnà barva" - text-color: "Barva textu" - base-theme: "Základnà vzhled" - base-theme-light: "SvÄ›tlý" - base-theme-dark: "Tmavý" - find-more-theme: "NajÃt dalÅ¡Ã vzhledy" - theme-name: "Jméno vzhledu" - preview-created-theme: "Náhled" - invalid-theme: "Vzhled nenà validnÃ" - already-installed: "Tento vzhled je již nainstalován." - saved: "Uloženo" - manage-themes: "Správa vzhledů" - builtin-themes: "Standardnà vzhledy" - my-themes: "Moje vzhledy" - installed-themes: "Nainstalované vzhledy" - select-theme: "Zvolte vzhled" - uninstall: "Odinstalovat" - uninstalled: "\"{}\" byl odinstalován" - author: "Autor" - desc: "Popis" - export: "Exportovat" - import: "Importovat" - import-by-code: "nebo zkopÃrujte kód" - theme-name-required: "Jméno vzhledu je povinné" -common/views/components/cw-button.vue: - hide: "Skrýt" - show: "VÃce" - chars: "{count} znaků" - files: "{count} souborů" - poll: "Anketa" -common/views/components/messaging.vue: - search-user: "NajÃt uživatele" - you: "Vy" - no-history: "Žádná historie" - user: "Uživatel" - group: "Skupina" - start-with-user: "Zahájit konverzaci s uživatelem" - start-with-group: "Zahájit skupinovou konverzaci" - select-group: "Vybrat skupinu" -common/views/components/messaging-room.vue: - new-message: "Máte novou zprávu" -common/views/components/messaging-room.form.vue: - input-message-here: "Sem zadejte zprávu" - send: "Odeslat" - attach-from-local: "PÅ™iložit soubory z VaÅ¡eho zaÅ™ÃzenÃ" -common/views/components/messaging-room.message.vue: - is-read: "PÅ™eÄtené" - deleted: "Tato zpráva byla odstranÄ›na" -common/views/components/nav.vue: - about: "O Misskey" - stats: "Statistiky" - status: "Status" - wiki: "Wiki" - donors: "Dárci" - repository: "ÚložiÅ¡tÄ›" - develop: "Vývojáři" - feedback: "ZpÄ›tná vazba" - tos: "PodmÃnky užÃvánÃ" -common/views/components/note-menu.vue: - mention: "ZmÃnÄ›nÃ" - detail: "VÃce" - copy-content: "ZkopÃrovat obsah" - copy-link: "ZkopÃrovat odkaz" - favorite: "PÅ™idat do oblÃbených" - unfavorite: "Odebrat z oblÃzených" - watch: "Sledovat" - unwatch: "PÅ™estat sledovat" - pin: "PÅ™ipnout" - unpin: "Odepnout" - delete: "Odstranit" - delete-confirm: "Opravdu chcete smazat tento pÅ™ÃspÄ›vek?" - delete-and-edit: "Smazat a upravit" - remote: "Ukázat originálnà poznámku" -common/views/components/user-menu.vue: - mention: "ZmÃnÄ›nÃ" - mute: "UmlÄet" - unmute: "ZruÅ¡it umlÄenÃ" - block: "Blokován" - unblock: "Odblokovat" - push-to-list: "PÅ™idat do seznamu" - select-list: "Vyberte seznam" - report-abuse: "Nahlásit spam" - report-abuse-reported: "Problém byl nahlášen administrátorovi. DÄ›kujeme za VaÅ¡Ã kooperaci." - silence: "Ztlumit" - suspend: "Zmrazit" -common/views/components/poll.vue: - vote-count: "{} hlasů" - total-votes: "{} hlasů celkem" - vote: "Hlasovat" - show-result: "PodÃvat se na výsledky" - voted: "Už jste hlasovaly" - closed: "UkonÄeno" - remaining-days: "zbývá {d} dnů, {h} hodin" - remaining-hours: "zbývá {h} hodin, a {m} minut" - remaining-minutes: "zbývá {m} minut, a {s} sekund" - remaining-seconds: "zbývá {s} sekund" -common/views/components/poll-editor.vue: - no-only-one-choice: "MusÃte vybrat alespoň dvÄ› možnosti" - choice-n: "Volba {}" - remove: "Odstranit tuto možnost" - add: "+ PÅ™idat možnost" - destroy: "Zahodit dotaznÃk" - multiple: "VÃce odpovÄ›dà je povoleno" - expiration: "TermÃn" - infinite: "NekoneÄne" - at: "VýbÄ›r data a Äasu" - no-more: "VÃce už pÅ™idat nemůžete" - deadline-date: "TermÃn ukonÄenÃ" - deadline-time: "Doba trvánÃ" - interval: "TrvánÃ" - unit: "Jednotka" - second: "Sekunda" - minute: "Minuta" - hour: "Hodina" - day: "Ne" -common/views/components/reaction-picker.vue: - choose-reaction: "Vyberte svoji reakci" - input-reaction-placeholder: "nebo vložte Emoji" -common/views/components/emoji-picker.vue: - custom-emoji: "Emoji" - people: "Lidé" - animals-and-nature: "ZvÃÅ™ata a pÅ™Ãroda" - food-and-drink: "JÃdlo a pitÃ" - activity: "Aktivita" - travel-and-places: "MÃsta a cestovánÃ" - objects: "Objekty" - symbols: "Symboly" - flags: "Vlajky" -common/views/components/settings/app-type.vue: - title: "Režim" - intro: "Můžete vybrat zdali chcete použÃt stolnÃ, nebo mobilnà vzhled." - choices: - auto: "Vybrat vzhled automaticky" - desktop: "Vždy použÃt stolnà vzhled" - mobile: "Vždy použÃt mobilnà vzhled" - info: "Pro aktivovánà zmÄ›n musÃte znovu naÄÃst stránky." -common/views/components/signin.vue: - username: "PÅ™ezdÃvka" - password: "Heslo" - token: "Token" - signing-in: "PÅ™ihlaÅ¡ovánÃ..." - or: "Nebo" - signin-with-twitter: "PÅ™ihlásit se pomocà úÄtu Twitter" - signin-with-github: "PÅ™ihlásit se pomocà úÄtu GitHub" - signin-with-discord: "PÅ™ihlásit se pomocà úÄtu Discord" - login-failed: "Nelze se pÅ™ihlásit. Zkontrolujte prosÃm své uživatelské jméno a heslo." - enter-2fa-code: "Vložte Váš verifikaÄnà kód" -common/views/components/signup.vue: - invitation-code: "Kód pozvánky" - invitation-info: "Pokud máte pozvánku, prosÃm kontaktujte <a href=\"{}\">administrátora</a>." - username: "PÅ™ezdÃvka" - checking: "Kontroluji..." - available: "Dostupná" - unavailable: "Obsazená" - error: "Chyba pÅ™ipojenÃ" - invalid-format: "PÃsmena, ÄÃsla a _ jsou povolená." - too-short: "Nesmà být prázdné!" - too-long: "Do 20 znaků." - password: "Heslo" - password-placeholder: "VÃce jak 8 znaků je doporuÄováno." - weak-password: "Slabé heslo" - normal-password: "PrůmÄ›rné heslo" - strong-password: "Silné heslo" - retype: "Zadejte znovu" - retype-placeholder: "Zadejte znovu pro kontrolu" - password-matched: "OK" - password-not-matched: "Neshodujà se" - recaptcha: "PotvrzenÃ" - agree-to: "SouhlasÃm s {0}." - tos: "PodmÃnky užÃvánÃ" - create: "VytvoÅ™it úÄet" - some-error: "Pokus o vytvoÅ™enà úÄtu selhal. ProsÃm zkuste to znovu." -common/views/components/special-message.vue: - new-year: "Šťastný nový rok!" - christmas: "Šťastné a veselé vánoce!" -common/views/components/stream-indicator.vue: - connecting: "PÅ™ipojovánÃ" - reconnecting: "PÅ™ipojuji se znovu" - connected: "PÅ™ipojenà navázáno" -common/views/components/notification-settings.vue: - title: "OznámenÃ" - mark-as-read-all-notifications: "OznaÄit vÅ¡echna oznámenà za pÅ™eÄtená" - mark-as-read-all-unread-notes: "OznaÄit vÅ¡echny pÅ™ÃspÄ›vky za pÅ™eÄtené" - mark-as-read-all-talk-messages: "OznaÄit vÅ¡echny zprávy za pÅ™eÄtené" -common/views/components/integration-settings.vue: - title: "Integrace" - connect: "PÅ™ipojit" - disconnect: "Odpojit" - connected-to: "Jste pÅ™ipojen k tomuto GitHub úÄtu" -common/views/components/github-setting.vue: - description: "Jakmile spojÃte Váš GitHub úÄet s VaÅ¡Ãm Misskey úÄtem, uvidÃte informace o VaÅ¡em GitHub úÄtu na VaÅ¡em profilu a budete se moci pÅ™ihlásit skrze GitHub." - connected-to: "Je pÅ™ipojen k tomuto GitHub úÄtu" - detail: "VÃce…" - reconnect: "Znovu pÅ™ipojit" - connect: "PÅ™ipojit Váš GitHub úÄet" - disconnect: "Odpojit" -common/views/components/discord-setting.vue: - description: "Jakmile spojÃte Váš Discord úÄet s VaÅ¡Ãm Misskey úÄtem, uvidÃte informace o VaÅ¡em Discord úÄtu na VaÅ¡em profilu a budete se moci pÅ™ihlásit skrze Discord." - connected-to: "Je pÅ™ipojen k tomuto Discord úÄtu" - detail: "VÃce…" - reconnect: "Znovu pÅ™ipojit" - connect: "PÅ™ipojit Váš Discord úÄet" - disconnect: "Odpojit" -common/views/components/uploader.vue: - waiting: "ÄŒekáme" -common/views/components/visibility-chooser.vue: - public: "VeÅ™ejné" - home: "Domů" - home-desc: "Poslat pouze na domovskou Äasovou osu" - specified: "PÅ™Ãmá" - specified-desc: "Poslat pouze zmÃnÄ›ným uživatelům" - local-public: "VeÅ™ejná (pouze mÃstnÃ)" - local-public-desc: "Nepublikovat na vzdálených serverech" - local-home: "Domovská (pouze mÃstnÃ)" - local-followers: "Pro sledujÃcà (pouze mÃstnÃ)" -common/views/components/trends.vue: - count: "{} zmÃnÄ›ných uživatelů" - empty: "Žádný trend" -common/views/components/language-settings.vue: - title: "Zobrazit jazyky" - pick-language: "Zvolte jazyk" - recommended: "DoporuÄené" - auto: "Automaticky" - specify-language: "Vyberte jazyk" - info: "Pro aktivovánà zmÄ›n musÃte znovu naÄÃst stránky." -common/views/components/profile-editor.vue: - title: "Profil" - name: "Jméno" - account: "ÚÄet" - location: "Lokace" - description: "O mnÄ›" - you-can-include-hashtags: "V popisku o Vás můžete použÃt i hastagy." - language: "Jazyk" - birthday: "Datum narozenÃ" - avatar: "Avatar" - banner: "Baner" - is-cat: "Tento úÄet je koÄka" - is-bot: "Tento úÄet je Bot" - advanced: "OstatnÃ" - privacy: "Osobnà údaje" - save: "Uložit" - saved: "Profil byl úspěšnÄ› aktualizován" - uploading: "Nahrávám" - upload-failed: "Nahrávánà selhalo" - unable-to-process: "Operace nemohla být dokonÄena." - email: "Nastavenà e-mailů" - email-address: "Emailová adresa" - email-verified: "Váš e-mail byl ověřen" - email-not-verified: "Váš email nenà potvrzen. ProsÃm zkontrolujte si svou schránku." - export: "Exportovat" - import: "Importovat" - export-and-import: "Import / Export" - export-targets: - following-list: "Seznam sledujÃcÃch" - mute-list: "Seznam ztlumených uživatelů" - blocking-list: "Seznam blokovaných uživatelů" - user-lists: "Seznamy" - enter-password: "ProsÃm, zadejte VaÅ¡e heslo" - danger-zone: "NebezpeÄná zóna" - delete-account: "Smazat úÄet" - account-deleted: "Váš úÄet byl smazán. Může chvilku trvat než zmizà vÅ¡echna data." - profile-metadata: "Metadata profilu" - metadata-label: "Popis" - metadata-content: "Obsah" -common/views/components/user-list-editor.vue: - users: "Uživatel" - rename: "PÅ™ejmenovat seznam" - delete: "Smazat seznam" - remove-user: "Odebrat z tohoto seznamu" - delete-are-you-sure: "Smazat seznam \"$1\"?" - deleted: "Smazáno" - add-user: "PÅ™idat uživatele" -common/views/components/user-group-editor.vue: - users: "ÄŒlenové" - rename: "PÅ™ejmenovat skupinu" - delete: "Odstranit skupinu" - transfer: "PÅ™esunout skupinu" - transfer-are-you-sure: "Jste si jistà že chcete pÅ™idat @$2 do skupiny: $1?" - transferred: "Skupina pÅ™esunuta" - remove-user: "Odebrat uživatele z této skupiny" - delete-are-you-sure: "Jste si jistà že chcete smazat skupinu \"$1\"?" - deleted: "Smazáno" - invite: "Pozvat" - invited: "Pozvánka byla úspěšnÄ› odeslána" -common/views/components/user-lists.vue: - user-lists: "Seznamy" - create-list: "VytvoÅ™it seznam" - list-name: "Název seznamu" -common/views/components/user-groups.vue: - user-groups: "Skupiny" - create-group: "VytvoÅ™it skupinu" - group-name: "Název skupiny" - owned-groups: "Moje skupiny" - invites: "Pozvat" - accept-invite: "PÅ™idat se" - reject-invite: "OdmÃtnout" -common/views/widgets/broadcast.vue: - fetching: "NaÄÃtám" - no-broadcasts: "Žádná nová oznámenÃ" - have-a-nice-day: "PÅ™ejeme Vám pÅ™Ãjemný den!" - next: "DalÅ¡Ã" - prev: "PÅ™edchozÃ" -common/views/widgets/calendar.vue: - year: "Rok {}" - month: "{}," - day: "{}" - today: "Dneska: " - this-month: "MÄ›sÃc:" - this-year: "Rok:" -common/views/widgets/photo-stream.vue: - title: "Foto stream" - no-photos: "Žádné obrázky" -common/views/widgets/posts-monitor.vue: - title: "Grafy pÅ™ÃspÄ›vků" - toggle: "PÅ™epnout zobrazenÃ" -common/views/widgets/hashtags.vue: - title: "Hashtagy" -common/views/widgets/server.vue: - title: "Informace o serveru" - toggle: "PÅ™epnout zobrazenÃ" -common/views/widgets/memo.vue: - title: "Poznámky" - memo: "PiÅ¡te sem!" - save: "Uložit" -common/views/widgets/slideshow.vue: - no-image: "V této složce nebyly nalezeny žádné fotky." -common/views/widgets/tips.vue: - tips-line23: "Ai-chan kawaii!" -common/views/pages/not-found.vue: - page-not-found: "Stránka nenalezena" -common/views/pages/follow.vue: - following: "SledovánÃ" - follow: "Sledovat" -common/views/pages/follow-requests.vue: - accept: "PÅ™ijmout" - reject: "OdmÃtnout" -desktop: - banner: "Baner" - avatar-crop-title: "Vyberte Äást, která se zobrazà jako avatar" - avatar: "Avatar" - uploading-avatar: "Nahrál nový avatar" - avatar-updated: "VaÅ¡e avatar byl aktualizován" - unable-to-process: "Operace nemohla být dokonÄena." - invalid-filetype: "Tento formát souboru nenà podporován" -desktop/views/components/activity.chart.vue: - total: "ÄŒerná ... Celkem" - notes: "Modrá ... Poznámky" - replies: "ÄŒervená ... OdpovÄ›di" - renotes: "Zelená ... Renoty" -desktop/views/components/activity.vue: - title: "Aktivita" - toggle: "PÅ™epnout zobrazenÃ" -desktop/views/components/calendar.vue: - title: "{month}. {year}" - prev: "PÅ™edchozà mÄ›sÃc" - next: "NásledujÃcà mÄ›sÃc" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count} souborů vybráno" - upload: "Nahrajte soubory z vaÅ¡eho zaÅ™ÃzenÃ" - cancel: "ZruÅ¡it" - ok: "OK" - choose-prompt: "Vybrat soubory" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "ZruÅ¡it" - ok: "OK" - choose-prompt: "Zvolte adresář" -desktop/views/components/crop-window.vue: - cancel: "ZruÅ¡it" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "využito" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Baner" - nsfw: "NSFW" - contextmenu: - rename: "PÅ™ejmenovat" - copy-url: "KopÃrovat URL" - download: "Stáhnout" - else-files: "OstatnÃ" - set-as-avatar: "Nastavit jako avatar" - set-as-banner: "Nastavit jako baner" - open-in-app: "OtevÅ™Ãt v aplikaci" - add-app: "PÅ™idat aplikaci" - rename-file: "PÅ™ejmenovat soubor" - input-new-file-name: "Zadejte nový název" - copied: "KopÃrovánà dokonÄeno" - copied-url-to-clipboard: "URL zkopÃrována do schránky" -desktop/views/components/drive.folder.vue: - unable-to-process: "Operace nemohla být dokonÄena." - unhandled-error: "Neznámá chyba" - contextmenu: - move-to-this-folder: "PÅ™esunout do této složky" - show-in-new-window: "OtevÅ™Ãt v novém oknÄ›" - rename: "PÅ™ejmenovat" - rename-folder: "PÅ™ejmenovat složku" - input-new-folder-name: "Zadejte nové jméno" - else-folders: "OstatnÃ" -desktop/views/components/drive.vue: - search: "VyhledávánÃ" - empty-drive-description: "KliknÄ›te pravým tlaÄÃtkem myÅ¡i pro otevÅ™enà menu, nebo sem pÅ™etáhnÄ›te soubor pro nahránÃ." - empty-folder: "Tato složka je prázdná" - unable-to-process: "Operace nemohla být dokonÄena." - unhandled-error: "Neznámá chyba" - url-upload: "Nahrát z URL adresy" - url-of-file: "URL adresa souboru, který chcete nahrát" - may-take-time: "Může trvat nÄ›jakou dobu, dokud nebude dokonÄeno nahrávánÃ." - create-folder: "VytvoÅ™it složku" - folder-name: "Název složky" - contextmenu: - create-folder: "VytvoÅ™it složku" - upload: "Nahrát soubor" - url-upload: "Nahrát z URL" -desktop/views/components/media-video.vue: - click-to-show: "KliknÄ›te pro zobrazenÃ" -desktop/views/components/followers.vue: - empty: "Vypadá to že Vás nikdo nesleduje." -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "Hotovo" - add-widget: "PÅ™idat widget:" - add: "PÅ™idat" -desktop/views/input-dialog.vue: - cancel: "ZruÅ¡it" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "Tento pÅ™ÃspÄ›vek je soukromý" - deleted: "Tento pÅ™ÃspÄ›vek byl odstranÄ›n" - location: "Lokace" - renote: "Renotovat" - add-reaction: "PÅ™idat reakci" - undo-reaction: "Odebrat reakci" -desktop/views/components/note.vue: - reply: "OdpovÄ›dÄ›t" - renote: "Renote" - add-reaction: "PÅ™idat reakci" - undo-reaction: "Odebrat reakci" - detail: "VÃce" - private: "Tento pÅ™ÃspÄ›vek je soukromý" - deleted: "Tento pÅ™ÃspÄ›vek byl odstranÄ›n" -desktop/views/components/notes.vue: - error: "NaÄÃtánà selhalo." - retry: "Opakovat" -desktop/views/components/notifications.vue: - empty: "Žádné nové notifikace!" -desktop/views/components/post-form.vue: - posted: "Odesláno!" - replied: "OdpovÄ›dÄ›no!" - reposted: "Renotováno!" - note-failed: "NepodaÅ™ilo se pÅ™idat pÅ™ÃspÄ›vek" - renote-failed: "Renotovánà neuspÄ›lo" -desktop/views/components/post-form-window.vue: - note: "Nový pÅ™ÃspÄ›vek" - reply: "OdpovÄ›dÄ›t" -desktop/views/components/progress-dialog.vue: - waiting: "ÄŒekáme" -desktop/views/components/renote-form.vue: - quote: "Citovat..." - cancel: "ZruÅ¡it" - renote: "Renotovat" - renote-home: "Renote (domů)" - reposting: "Renotuji..." - success: "Renotováno!" - failure: "Renotovánà neuspÄ›lo" -desktop/views/components/renote-form-window.vue: - title: "Chcete tohle renotovat?" -desktop/views/components/settings.2fa.vue: - detail: "VÃce…" - url: "https://www.google.cz/landing/2step/" - register: "PÅ™idat zaÅ™ÃzenÃ" - already-registered: "Toto zaÅ™Ãzenà je již pÅ™ipojené" - unregister: "Odebrat" - enter-password: "ProsÃm zadejte heslo" - authenticator: "Nejprve musÃte nainstalovat Google Authenticator na VaÅ¡em zaÅ™ÃzenÃ:" - howtoinstall: "Jak nainstalovat" - token: "Token" - scan: "Poté naskenujte QR kód:" - done: "ProsÃm vložte kód zobrazený na VaÅ¡em zaÅ™ÃzenÃ:" - submit: "Uložit" - success: "Nastavenà uloženo!" - failed: "NepodaÅ™ilo se spárovat. ProsÃm zkontrolujte správnost bezpeÄnostnÃho kódu." - totp-header: "Ověřovacà aplikace" - security-key-header: "BezpeÄnostnà klÃÄ" - last-used: "Naposledy použito:" - activate-key: "KliknÄ›te pro aktivaci bezpeÄnostnÃho klÃÄe" - security-key-name: "Název klÃÄe" - key-unregistered: "BezpeÄnostnà klÃÄ byl odstranÄ›n" -common/views/components/media-image.vue: - sensitive: "NSFW" - click-to-show: "KliknÄ›te pro zobrazenÃ" -common/views/components/api-settings.vue: - caution: "NepoužÃvejte tento kód v žádné jiné aplikace nebo ho sdÃlejte s ostatnÃmi (jinak můžete ohrozit svojà bezpeÄnost)." - token: "Token:" - enter-password: "ProsÃm zadejte heslo" - console: - title: "API konzole" - endpoint: "Endpoint" - parameter: "Parametry" - send: "Odeslat" - sending: "OdesÃlám" - response: "Výsledek" -desktop/views/components/settings.apps.vue: - no-apps: "Žádné pÅ™ipojené aplikace" -common/views/components/drive-settings.vue: - max: "Velikost úložiÅ¡tÄ›" - in-use: "využito" - stats: "Statistiky" -common/views/components/mute-and-block.vue: - mute-and-block: "UmlÄet/blokovat" - mute: "UmlÄet" - block: "Blokován" - no-muted-users: "Žádný uživatel nebyl umlÄen" - no-blocked-users: "Žádný uživatel nenà blokován" - save: "Uložit" -common/views/components/password-settings.vue: - reset: "ZmÄ›nit heslo" - enter-current-password: "ProsÃm, vložte své souÄasné heslo" - enter-new-password: "Zadejte své nové heslo" - enter-new-password-again: "Znovu zadejte své nové heslo" - not-match: "Nová hesla se neshodujÃ" - changed: "Heslo bylo úspěšnÄ› zmÄ›nÄ›no" - failed: "NepodaÅ™ilo se zmÄ›nit heslo" -desktop/views/components/sub-note-content.vue: - private: "Tento pÅ™ÃspÄ›vek je soukromý" - deleted: "Tento pÅ™ÃspÄ›vek byl odstranÄ›n" - poll: "Anketa" -desktop/views/components/settings.tags.vue: - title: "Tagy" - add: "PÅ™idat" - save: "Uložit" -desktop/views/components/timeline.vue: - home: "Domů" - local: "LokálnÃ" - global: "GlobálnÃ" - mentions: "ZmÃnÄ›nÃ" - list: "Seznamy" - hashtag: "Hashtag" - add-list: "PÅ™idat do seznamu" - list-name: "Název seznamu" -desktop/views/components/ui.header.vue: - welcome-back: "VÃtejte zpátky," - adjective: "Pán" -desktop/views/components/ui.header.account.vue: - profile: "Váš profil" - lists: "Seznamy" - groups: "Skupiny" - admin: "Administrace" - room: "MÃstnost" -desktop/views/components/ui.header.nav.vue: - game: "Hry" -desktop/views/components/ui.header.notifications.vue: - title: "OznámenÃ" -desktop/views/components/ui.header.post.vue: - post: "Nový pÅ™ÃspÄ›vek" -desktop/views/components/ui.header.search.vue: - placeholder: "VyhledávánÃ" -desktop/views/components/user-preview.vue: - notes: "PÅ™ÃspÄ›vky" -desktop/views/components/users-list.vue: - all: "VÅ¡echny" - iknow: "Znáte" - fetching: "NaÄÃtám…" -desktop/views/components/window.vue: - close: "ZavÅ™Ãt" -admin/views/index.vue: - instance: "Instance" - emoji: "Emoji" - moderators: "ModerátoÅ™i" - users: "Uživatelé" - federation: "Z fedivesmÃru" - announcements: "OznámenÃ" - queue: "Fronta úloh" - logs: "Logy" - db: "Databáze" - back-to-misskey: "ZpÄ›t na Misskey" -admin/views/db.vue: - tables: "Tabulky" - vacuum: "VysavaÄ" - vacuum-info: "Uklidà databázi. Neohrozà data a snÞà využità disku. Tohle se dÄ›je automaticky a opakovanÄ›." - vacuum-exclamation: "VysavaÄ může doÄasnÄ› pÅ™etÞit databázi a doÄasnÄ› omezit akce uživatelů." -admin/views/dashboard.vue: - dashboard: "Kontrolnà panel" - accounts: "ÚÄty" - notes: "Poznámky" - drive: "Disk" - instances: "Instance" - this-instance: "Tato instance" - federated: "Z fedivesmÃru" -admin/views/queue.vue: - title: "Ve frontÄ›" - remove-all-jobs: "VyÄistit frontu" - jobs: "Úkoly" - queue: "Ve frontÄ›" - domains: - inbox: "Obdržené" - db: "Databáze" - states: - active: "V procesu" - delayed: "Naplánováno" - waiting: "Ve frontÄ›" - result-is-truncated: "Zkrácený výsledek" -admin/views/logs.vue: - logs: "Logy" - domain: "Doména" - level: "Úroveň" - levels: - all: "VÅ¡e" - info: "Informace" - success: "PodaÅ™ilo se" - warning: "VarovánÃ" - error: "Chyba" - debug: "Debug" - delete-all: "Smazat vÅ¡e" -admin/views/abuse.vue: - target: "CÃl" - details: "Popis" - remove-report: "Odstranit" -admin/views/instance.vue: - instance: "Instance" - instance-name: "Název instance" - instance-description: "Popis instance" - host: "Hostitel" - icon-url: "URL ikonky" - logo-url: "URL loga" - banner-url: "URL pro baner" - error-image-url: "URL pro chybový obrázek" - languages: "Jazyk této instance" - languages-desc: "Můžete nastavit vÃce než jeden, oddÄ›lte mezerami." - tos-url: "URL pro smluvnà podmÃnky" - repository-url: "URL adresa repositáře" - feedback-url: "URL pro zpÄ›tnou vazbu" - maintainer-config: "Informace o administrátorovi" - maintainer-name: "Jméno administrátora" - maintainer-email: "Kontakt na administrátora" - advanced-config: "DalÅ¡Ã nastavenÃ" - object-storage-base-url: "URL" - object-storage-prefix: "PÅ™edpona" - object-storage-endpoint: "Endpoint" - object-storage-region: "Region" - object-storage-port: "Port" - object-storage-access-key: "PÅ™Ãstupový klÃÄ" - object-storage-secret-key: "Tajný KlÃÄ (Secret Key)" - object-storage-use-ssl: "PoužÃt SSL" - object-storage-s3-info-here: "zde" - mb: "V megabajtech" - recaptcha-config: "nastavenà služby reCAPTCHA" - recaptcha-info: "reCAPTCHA token je povinný. Můžete jej zÃskat na https://www.google.com/recaptcha/intro/" - enable-recaptcha: "povolit reCAPTCHA" - recaptcha-secret-key: "Tajný KlÃÄ (Secret Key)" - recaptcha-preview: "Náhled" - twitter-integration-config: "Nastavenà spojenà s Twitterem" - twitter-integration-info: "The callback URL is set on {url}." - enable-twitter-integration: "Povolit pÅ™ipojenà k Twitteru" - twitter-integration-consumer-key: "Consumer key" - twitter-integration-consumer-secret: "Consumer Secret" - github-integration-config: "Nastavenà spojenà s GitHubem" - github-integration-info: "The callback URL is set on {url}." - enable-github-integration: "Povolit pÅ™ipojenà ke GitHubu" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-config: "Nastavenà spojenà s Discordem" - discord-integration-info: "The callback URL is set to {url}." - enable-discord-integration: "Povolit pÅ™ipojenà ke Discordu" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - invite: "Pozvat" - save: "Uložit" - saved: "Uloženo" - email: "Emailová adresa" - smtp-port: "SMTP Port" - smtp-auth: "Provést SMTP autentikaci" - smtp-user: "SMTP uživatel" - smtp-pass: "SMTP heslo" - test-email: "Test" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "Povolit ServiceWorker" - vapid-publickey: "VAPID veÅ™ejný klÃÄ" - vapid-privatekey: "VAPID osobnà klÃÄ" -admin/views/charts.vue: - title: "Graf" - per-day: "za den" - per-hour: "za hodinu" - federation: "Federace" - notes: "PÅ™ÃspÄ›vky" - users: "Uživatelé" - drive: "Disk" - network: "SÃÅ¥" - charts: - federation-instances: "PoÄet instancÃ: zvýšenÃ/snÞenÃ" - federation-instances-total: "Celkový poÄet instancÃ" - notes-total: "Celkem pÅ™ÃspÄ›vků" - users-total: "Celkem uživatelů" - active-users: "Aktivnà uživatelé" - network-requests: "Požadavek" - network-time: "Doba odezvy" - network-usage: "SÃÅ¥ový provoz" -admin/views/drive.vue: - operation: "Operace" - fileid-or-url: "ID nebo URL souboru" - file-not-found: "Soubor nebyl nalezen" - sort: - title: "SeÅ™adit" - createdAtAsc: "VÄ›k - od nejstarÅ¡Ãho" - createdAtDesc: "VÄ›k - od nejmladÅ¡Ãho" - sizeAsc: "Velikost - od nejmenÅ¡Ãch" - sizeDesc: "Velikost – od nejvÄ›tÅ¡Ãch" - origin: - title: "Původ" - combined: "Lokálnà + Vzdálené" - local: "LokálnÃ" - remote: "Vzdálené" - delete: "Smazat" - deleted: "Smazáno" -admin/views/users.vue: - operation: "Operace" - username-or-userid: "Uživatelské jméno nebo ID uživatele" - user-not-found: "Uživatel nebyl nalezen" - reset-password: "Resetovat heslo" - reset-password-confirm: "Opravdu chcete resetovat VaÅ¡e heslo?" - password-updated: "Heslo je nynà \"{password}\"" - update-remote-user: "Aktualizovat informace o vzdáleném úÄtu" - username: "PÅ™ezdÃvka" - host: "Hostitel" - users: - title: "Uživatel" - state: - all: "VÅ¡echny" - moderator: "Moderátor" - adminOrModerator: "Admin/Moderátor" - origin: - title: "Původ" - combined: "Lokálnà + Vzdálené" - local: "LokálnÃ" - remote: "Vzdálené" - createdAt: "VytvoÅ™eno" - updatedAt: "Aktualizováno" -admin/views/moderators.vue: - add-moderator: - title: "VytvoÅ™it moderátora" - logs: - title: "Logy" - moderator: "ModerátoÅ™i" - type: "Operace" - info: "Informace" -admin/views/emoji.vue: - add-emoji: - title: "PÅ™idat emoji" - name: "Jméno emoji" - name-desc: "Můžete použÃt následujÃcà znaky a~z 0~9 _" - aliases: "Aliasy" - aliases-desc: "Můžete nastavit vÃce než jeden, oddÄ›lte mezerami." - url: "URL obrázku" - add: "PÅ™idat" - info: "DoporuÄujeme obrázky ve formátu PNG pod 50 kB." - added: "Emoji bylo pÅ™idáno" - emojis: - title: "Seznam smajlÃků" - update: "Aktualizovat" - remove: "Odstranit" - remove-emoji: - are-you-sure: "Odstranit „$1“?" - removed: "Smazáno" -admin/views/announcements.vue: - announcements: "OznámenÃ" - save: "Uložit" - remove: "Odstranit" - add: "PÅ™idat" - title: "Titulek" - text: "Obsah" - saved: "Uloženo" - _remove: - are-you-sure: "Odstranit \"$1\"?" - removed: "Smazáno" -admin/views/hashtags.vue: - hided-tags: "Skryté tagy" -admin/views/federation.vue: - instance: "Instance" - host: "Hostitel" - notes: "Poznámky" - users: "Uživatelé" - following: "SledovánÃ" - caught-at: "VytvoÅ™eno" - status: "Status" - latest-request-received-at: "Poslednà požadavek pÅ™ijat" - block: "Blokován" - instances: "Z fedivesmÃru" - states: - all: "VÅ¡echny" - blocked: "Blokován" - not-responding: "Bez odpovÄ›di" - marked-as-closed: "OznaÄeno jako uzavÅ™ené" - charts: "Graf" - chart-srcs: - requests: "Požadavek" - users-total: "Celkem uživatelů" - notes-total: "Celkem pÅ™ÃspÄ›vků" - chart-spans: - hour: "za hodinu" - day: "za den" - blocked-hosts: "Blokován" - save: "Uložit" -desktop/views/pages/welcome.vue: - about: "O Misskey" - timeline: "ÄŒasová osa" - announcements: "OznámenÃ" - photos: "Nedávné obrázky" - powered-by-misskey: "Běžà na <b>Misskey</b>." - info: "Informace" -desktop/views/pages/drive.vue: - title: "Misskey Disk" -desktop/views/pages/note.vue: - prev: "PÅ™edchozà pÅ™ÃspÄ›vÄ›k" - next: "NásledujÃcà pÅ™ÃspÄ›vek" -desktop/views/pages/selectdrive.vue: - title: "Vyberte soubor(y)" - ok: "OK" - cancel: "ZruÅ¡it" - upload: "Nahrajte soubory z vaÅ¡eho zaÅ™ÃzenÃ" -desktop/views/pages/search.vue: - not-available: "Vyhledávánà je vypnuté pro tuto instanci." - not-found: "Pro '{q}' nebyly nalezeny žádné pÅ™ÃspÄ›vky." -desktop/views/pages/tag.vue: - no-posts-found: "Nebyly nalezeny žádné pÅ™ÃspÄ›vky s \"{q}\"." -desktop/views/pages/user-list.users.vue: - users: "Uživatel" - add-user: "PÅ™idat uživatele" - username: "PÅ™ezdÃvka" -desktop/views/pages/user/user.followers-you-know.vue: - loading: "NaÄÃtám..." -desktop/views/pages/user/user.friends.vue: - title: "ÄŒastá zmÃnÄ›nÃ" - loading: "NaÄÃtám..." - no-users: "Žádná Äastá zmÃnÄ›nÃ" -desktop/views/pages/user/user.photos.vue: - title: "Fotky" - loading: "NaÄÃtám..." - no-photos: "Žádné obrázky" -desktop/views/pages/user/user.header.vue: - posts: "Poznámky" - following: "SledovanÃ" - followers: "SledujÃcÃ" - month: "Po" - day: "Ne" -desktop/views/widgets/notifications.vue: - title: "OznámenÃ" -desktop/views/widgets/polls.vue: - title: "Ankety" - nothing: "Žádné nové notifikace!" -desktop/views/widgets/trends.vue: - nothing: "Žádné nové notifikace!" -desktop/views/widgets/users.vue: - title: "DoporuÄenà uživatelé" -mobile/views/components/drive.vue: - used: "využito" - file-count: "Soubor(ů)" - folder-is-empty: "Tato složka je prázdná" - folder-name: "Název složky" - url-prompt: "URL adresa souboru, který chcete nahrát" - uploading: "Byl zahájen upload. Může chvilku trvat než bude dokonÄen." -mobile/views/components/drive-file-chooser.vue: - select-file: "Vybrat soubory" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "Vyberte složku" -mobile/views/components/drive.file-detail.vue: - download: "Stáhnout" - rename: "PÅ™ejmenovat" - move: "PÅ™esunout" - hash: "Hash (md5)" - exif: "EXIF" -mobile/views/components/media-video.vue: - click-to-show: "KliknÄ›te pro zobrazenÃ" -common/views/components/follow-button.vue: - following: "SledovánÃ" - follow-processing: "Zpracovávám" -mobile/views/components/note.vue: - private: "Tento pÅ™ÃspÄ›vek je soukromý" - deleted: "Tento pÅ™ÃspÄ›vek byl odstranÄ›n" - location: "Lokace" -mobile/views/components/note-detail.vue: - reply: "OdpovÄ›dÄ›t" - reaction: "Reakce" - private: "Tento pÅ™ÃspÄ›vek je soukromý" - deleted: "Tento pÅ™ÃspÄ›vek byl odstranÄ›n" - location: "Lokace" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "koÄka" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "koÄka" -mobile/views/components/notifications.vue: - empty: "Žádné nové notifikace!" -mobile/views/components/sub-note-content.vue: - private: "Tento pÅ™ÃspÄ›vek je soukromý" - deleted: "Tento pÅ™ÃspÄ›vek byl odstranÄ›n" - poll: "Ankety" -mobile/views/components/ui.header.vue: - welcome-back: "VÃtejte zpátky," - adjective: "Pán" -mobile/views/components/ui.nav.vue: - timeline: "ÄŒasová osa" - notifications: "OznámenÃ" - search: "VyhledávánÃ" - user-lists: "Seznamy" - user-groups: "Skupiny" - widgets: "Widgety" - game: "Hry" - admin: "Administrace" - about: "O Misskey" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Nahrát soubor" - create-folder: "VytvoÅ™it složku" -mobile/views/pages/signup.vue: - lets-start: "Váš úÄet je pÅ™ipraven! 📦" -mobile/views/pages/home.vue: - home: "Domů" - local: "LokálnÃ" - global: "GlobálnÃ" - mentions: "ZmÃnÄ›nÃ" -mobile/views/pages/tag.vue: - no-posts-found: "Nebyly nalezeny žádné pÅ™ÃspÄ›vky s \"{q}\"." -mobile/views/pages/widgets.vue: - add-widget: "PÅ™idat" - customization-tips: "Tipy pro pÅ™izpůsobenÃ" -mobile/views/pages/widgets/activity.vue: - activity: "Aktivita" -mobile/views/pages/share.vue: - share-with: "SdÃlet na {name}" -mobile/views/pages/note.vue: - prev: "PÅ™edchozà pÅ™ÃspÄ›vÄ›k" - next: "NásledujÃcà pÅ™ÃspÄ›vek" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "VyhledávánÃ" - not-found: "Pro '{q}' nebyly nalezeny žádné pÅ™ÃspÄ›vky." -mobile/views/pages/selectdrive.vue: - select-file: "Vybrat soubory" -mobile/views/pages/notifications.vue: - notifications: "OznámenÃ" -mobile/views/pages/user/home.vue: - activity: "Aktivita" - frequently-replied-users: "ÄŒastá zmÃnÄ›nÃ" -mobile/views/pages/user/home.photos.vue: - no-photos: "Žádné obrázky" -deck: - widgets: "Widgety" - home: "Domů" - local: "LokálnÃ" - hashtag: "Hashtagy" - global: "GlobálnÃ" - mentions: "ZmÃnÄ›nÃ" - notifications: "OznámenÃ" - list: "Seznamy" - select-list: "Vyberte seznam" - swap-left: "Posunout doleva" - swap-right: "Posunout doprava" - rename: "PÅ™ejmenovat" -deck/deck.user-column.vue: - activity: "Aktivita" -dev/views/new-app.vue: - app-name-desc: "Jméno vaÅ¡Ã aplikace" -pages: - pin-this-page: "PÅ™ipnout" - unpin-this-page: "Odepnout" - like: "Lajk" - title: "Titulek" - blocks: - post: "Formulář pro psanÃ" - _post: - text: "Obsah" - _textInput: - text: "Titulek" - _textareaInput: - text: "Titulek" - _numberInput: - text: "Titulek" - _switch: - text: "Titulek" - _counter: - text: "Titulek" - _button: - text: "Titulek" - _action: - _dialog: - content: "Obsah" - _radioButton: - title: "Titulek" - script: - categories: - random: "NáhodnÄ›" - list: "Seznamy" - blocks: - _join: - arg1: "Seznamy" - random: "NáhodnÄ›" - _randomPick: - arg1: "Seznamy" - _dailyRandomPick: - arg1: "Seznamy" - _seedRandomPick: - arg2: "Seznamy" - _pick: - arg1: "Seznamy" - _listLen: - arg1: "Seznamy" - types: - array: "Seznamy" -room: - translate: "PÅ™esunout" - save: "Uložit" - saved: "Uloženo" - furnitures: - moon: "MÄ›sÃc" - bin: "KoÅ¡" diff --git a/locales/da-DK.yml b/locales/da-DK.yml deleted file mode 100644 index de326526a09393756e552f0515efaff76c765ea7..0000000000000000000000000000000000000000 --- a/locales/da-DK.yml +++ /dev/null @@ -1,1921 +0,0 @@ ---- -meta: - lang: "Dansk" -common: - misskey: "En â i fediverset" - about-title: "En â i fediverset." - about: "Tak, fordi du fandt Misskey. Misskey er en <b> decentral mikroblog platform </b> født pÃ¥ Jorden. Den findes i Fediverset (et univers med forskellige sociale medieplatforme). Den er tæt integreret med andre sociale medier platforme. Hvorfor ikke tage en pause fra trængsel og travlhed i storbyen og hoppe ind i en ny type internet?" - intro: - title: "Hvad er Misskey?" - about: "Misskey er en open-source, <b>decentraliseret microblogging platform</b>. Den har en sofistikeret brugerflade, som kan tilpasses fuldstændigt. Den giver mulighed for at udtrykke mange forskellige reaktioner pÃ¥ poster. Desuden tilbyder den gratis opbevaring af filer med et integreret hÃ¥ndteringssystem samt andre avancerede funktioner. Oven i dette er Misskey tilknyttet et netværk ved navn “Fediverseâ€, som gør os i stand til at kommunikere med brugere pÃ¥ andre SNS'er. For eksempel vil en post, som du har skrevet, ikke kun blive sendt til brugere af Misskey men ogsÃ¥ til brugere af Mastodon og Pleroma. Det svarer lidt til at sende radio transmissioner mellem planeter for at etablere en kommunikation." - features: "Funktioner" - rich-contents: "Post" - rich-contents-desc: "Bare skriv løs om dine ideer, aktuelle emner eller alt muligt andet, som du gerne vil dele med andre. Det kan være, at du gerne vil udsmykke dine ord, vedhæfte dine yndlingsbilleder, sende filer, tilføje videoer eller oprette en afstemning. Alle de nævnte ting er muligt med Misskey!" - reaction: "Reaktioner" - reaction-desc: "Den nemmeste mÃ¥de at udtrykke dine reaktioner pÃ¥. Misskey giver mulighed for at tilføje forskellige reaktioner pÃ¥ andres poster. Reaktionerne vil aldrig blive vist pÃ¥ andre SNS'er, som kun er i stand til at udveksle \"likes\"." - ui: "Brugerflade" - ui-desc: "En enkelt brugerflade vil aldrig passe helt for alle. Derfor er Misskey's brugerflade gennemført justerbar, sÃ¥ den kan ramme dine ønsker helt præcist. Du kan designe dit helt eget personlige udtryk ved at rette layoutet af din tidslinje og tilpasse udvalgte widgets, som desuden kan flyttes frit rundt." - drive: "Drev" - drive-desc: "Vil du poste et billede, som du tidligere har uploadet? Har du brug for at navngive filer og organisere dem i mapper, som du selv har navngivet? SÃ¥ er Misskey Drev den bedste løsning for dig. Den gør det sÃ¥ let som ingenting at dele dine filer online." - outro: "Tjek Misskey's unikke funktioner ved at se dem med dine egne øjne. Hvis du kommer frem til, at den ene server ikke er noget for dig, sÃ¥ kan du prøve en anden. Misskey er et decentraliseret SNS, sÃ¥ du kan lettere finde frem til brugere, som du klikker med. God fornøjelse!" - application-authorization: "Adgangsstyring" - close: "Luk" - do-not-copy-paste: "UndgÃ¥ venligst at skrive eller klistre kode ind her. I modsat fald kan din konto blive kompromitteret." - load-more: "Læs mere" - enter-password: "Skriv din adgangskode" - 2fa: "To-faktor adgangsstyring" - customize-home: "Tilpas dit layout" - featured-notes: "Fremhævede poster" - dark-mode: "Nat design" - signin: "Log ind" - signup: "Bliv bruger" - signout: "Log ud" - reload-to-apply-the-setting: "Denne indstilling slÃ¥r først igennem, nÃ¥r du har genindlæst siden. Vil du genindlæse siden nu?" - fetching-as-ap-object: "Tilladelse til sammenkobling" - delete-confirm: "Er du helt sikker pÃ¥, at du vil slette denne post?" - notification-types: - all: "Alle" - follow: "Følger" - reply: "Svar" - renote: "Gen-postering" - reaction: "Reaktion" - got-it: "Det er OK" - customization-tips: - title: "Tips om tilpasning" - paragraph: "<p>Tilpasning giver mulighed for at tilføje, slette og flytte rundt pÃ¥ widgets med træk-og-slip.</p><p>Du kan ændre visningen af visse widgets ved at <strong>højre-klikke</strong> pÃ¥ dem.</p><p>En widget slettes ved at trække den med musen hen til <strong>skaldespanden</strong> i toppen af siden.</p><p>Du afslutter tilpasningen ved at klikke pÃ¥ \"Færdig\" øverst til højre.</p>" - gotit: "Det er OK" - notification: - file-uploaded: "Filen er overført!" - message-from: "Besked fra {}:" - reversi-invited: "Invitation til spil" - reversi-invited-by: "Inviteret af {}:" - notified-by: "Besked fra {}:" - reply-from: "Svar fra {}:" - quoted-by: "Citeret af {}:" - time: - unknown: "ukendt" - future: "fremtidig" - just_now: "nu" - seconds_ago: "{} sekund(er) siden" - minutes_ago: "{} minut(ter) siden" - hours_ago: "{} time(r) siden" - days_ago: "{} dag(e) siden" - weeks_ago: "{} uge(r) siden" - months_ago: "{} mÃ¥ned(er) siden" - years_ago: "{} Ã¥r siden" - month-and-day: "{day}-{month}" - trash: "Skraldespand" - drive: "Drev" - pages: "Sider" - messaging: "Konversationer" - home: "Startside" - deck: "Stabel" - timeline: "Tidslinje" - explore: "Udforsk" - following: "Følger" - followers: "Følgere" - favorites: "Favoritter" - permissions: - "read:account": "Se konto indstillinger" - "write:account": "Opdater dine konto informationer" - "read:blocks": "Vis blokke" - "write:blocks": "Rediger blokke" - "read:drive": "Gennemse drevet" - "write:drive": "Rediger drevet" - "read:favorites": "Mine favoritter" - "write:favorites": "Rediger favoritterne" - "read:following": "Vis info om følgere" - "write:following": "Rediger info om følgere" - "read:messaging": "Se meddelelser" - "write:messaging": "Rediger meddelelser" - "read:mutes": "Se annullerede poster" - "write:mutes": "Rediger annullerede poster" - "write:notes": "Opret og slet poster" - "read:notifications": "Vis notifikationer" - "write:notifications": "Rediger notifikationer" - "read:reactions": "Vis reaktioner" - "write:reactions": "Rediger reaktioner" - "write:votes": "Stem" - empty-timeline-info: - follow-users-to-make-your-timeline: "Følgende brugere vil fÃ¥ vist deres poster pÃ¥ tidslinjen." - explore: "Find brugere" - post-form: - submit: "Post" - reply: "Svar" - renote: "Gen-postering" - error: "Fejl" - enter-username: "Angiv brugernavn" - add-visible-user: "Tilføj en bruger" - username-prompt: "Angiv brugernavn" - weekday-short: - sunday: "Søn" - monday: "Man" - tuesday: "Tirs" - wednesday: "Ons" - thursday: "Tors" - friday: "Fre" - saturday: "Lør" - weekday: - sunday: "Søndag" - monday: "Mandag" - tuesday: "Tirsdag" - wednesday: "Onsdag" - thursday: "Torsdag" - friday: "Fredag" - saturday: "Lørdag" - reactions: - like: "Synes om" - love: "Elsker" - laugh: "Ler" - hmm: "Hmm...?" - surprise: "Wauw" - congrats: "Tillykke" - angry: "Vred" - confused: "Forvirret" - rip: "Hvil i fred" - pudding: "Budding" - note-visibility: - public: "Offentlig" - home: "Startside" - home-desc: "Post udelukkende til tidslinjen" - followers: "Følgere" - followers-desc: "Skriv kun til dine følgere" - specified: "Direkte" - specified-desc: "Skriv kun til udvalgte brugere" - local-public: "Offentlig (pÃ¥ den lokale server)" - local-home: "Startside (pÃ¥ den lokale server)" - local-followers: "Følgere (pÃ¥ den lokale server)" - note-placeholders: - a: "Hvad laver du?" - b: "Hvad sker der?" - c: "Hvad har du i tankerne?" - d: "Hvad vil du gerne sige?" - e: "Skriv her" - f: "Venter pÃ¥ din indtastning." - settings: "Indstillinger" - _settings: - profile: "Profil" - notification: "Notifikation" - apps: "Apps" - tags: "Hashtag" - mute-and-block: "Sluk / Blokér" - blocking: "Blokér" - security: "Sikkerhed" - signin: "Login historik" - password: "Adgangskode" - other: "Andet" - appearance: "Udseende" - behavior: "Opførsel" - reactions: "Reaktion" - fetch-on-scroll: "Uendeligt scroll" - fetch-on-scroll-desc: "NÃ¥r du scroller ned ad siden, hentes der automatisk nyt indhold ind" - note-visibility: "Post synlighed" - default-note-visibility: "Standard synlighed" - remember-note-visibility: "Husk post synlighed" - web-search-engine: "Søgemaskine" - web-search-engine-desc: "Eksempel: https://www.google.com/?#q={{query}}" - keep-cw: "Bevar indholdsvarsel" - keep-cw-desc: "Det indholdsvarsel, som stÃ¥r pÃ¥ det oprindelige indlæg, vil som standard blive overført til eventuelle svar pÃ¥ indlægget." - i-like-sushi: "Jeg foretrækker sushi frem for budding" - show-reversi-board-labels: "Vis række- og kolonne-etiketter i Reversi" - use-avatar-reversi-stones: "Anvend avatar som en sten i Reversi" - disable-animated-mfm: "Deaktiver animeret tekst i en post" - disable-showing-animated-images: "Afspil ikke animerede billeder" - suggest-recent-hashtags: "Vis de seneste populære hashtags pÃ¥ post formularen" - always-show-nsfw: "Vis altid indhold, der er markeret som Upassende PÃ¥ Jobbet" - always-mark-nsfw: "Marker altid poster med medie bilag som Upassende PÃ¥ Jobbet" - show-full-acct: "Vis aldrig værtsnavnet pÃ¥ brugernavnet" - show-via: "vis via" - reduce-motion: "Reducer bevægelser" - this-setting-is-this-device-only: "Indstillingen gælder kun for denne enhed" - use-os-default-emojis: "Anvend standard emojis fra operativsystemet" - line-width: "Linjebredde" - line-width-thin: "Tynd linje" - line-width-normal: "Normal" - line-width-thick: "Tyk linje" - font-size: "Tekst størrelse" - font-size-x-small: "Meget lille" - font-size-small: "Lille" - font-size-medium: "Normal" - font-size-large: "Stor" - font-size-x-large: "Meget stor" - deck-column-align: "Justering af kolonner" - deck-column-align-center: "Midten" - deck-column-align-left: "Venstre" - deck-column-align-flexible: "Højre" - deck-column-width: "Kolonne bredde" - deck-column-width-narrow: "Smal" - deck-column-width-narrower: "Smallere" - deck-column-width-normal: "Normal" - deck-column-width-wider: "Lidt bredere" - deck-column-width-wide: "Bred" - use-shadow: "Vis skygger" - rounded-corners: "Vis afrundede hjørner" - circle-icons: "Anvend cykliske avatar" - contrasted-acct: "Tilføj kontrast til brugerkontoen" - wallpaper: "Baggrundsbillede" - choose-wallpaper: "Vælg en baggrund" - delete-wallpaper: "Fjern baggrund" - post-form-on-timeline: "Vis post formularen oven over tidslinjen" - show-clock-on-header: "Vis uret i øverste højre hjørne" - show-reply-target: "Vis hvad der svares pÃ¥" - timeline: "Tidslinje" - show-my-renotes: "Vis mine gen-posteringer pÃ¥ tidslinjen" - show-renoted-my-notes: "Vis gen-posteringer af dine egne poster pÃ¥ tidslinjen" - show-local-renotes: "Vis gen-posteringer af lokale poster pÃ¥ tidslinjen" - remain-deleted-note: "Fortsæt med at vise slettede poster" - sound: "Lyd" - enable-sounds: "Aktiver lyd" - enable-sounds-desc: "Afspil en lyd, nÃ¥r du modtager en post/besked. Denne indstilling gemmes i browseren." - volume: "Volumen" - test: "Test" - update: "Misskey opdatering" - version: "Aktuel version:" - latest-version: "Seneste version:" - update-checking: "Kikker efter opdateringer" - do-update: "Kikker efter opdateringer" - update-settings: "Avancerede indstillinger" - no-updates: "Der er ikke kommet nogen opdateringer" - no-updates-desc: "Din Misskey er opdateret" - update-available: "Der er kommet en ny version" - update-available-desc: "Opdateringer vil slÃ¥ igennem efter genindlæsning af siden." - advanced-settings: "Avancerede indstillinger" - debug-mode: "Aktiver debug" - debug-mode-desc: "Denne indstilling er gemt i browseren" - navbar-position: "Placering af navigationsbaren" - navbar-position-top: "Top" - navbar-position-left: "Venstre" - navbar-position-right: "Højre" - i-am-under-limited-internet: "Mit internet kører med lav hastighed" - post-style: "Stil for visning af poster" - post-style-standard: "Standard" - post-style-smart: "Smart" - notification-position: "Vis notifikationer" - notification-position-bottom: "Bund" - notification-position-top: "Top" - disable-via-mobile: "Marker aldrig posten som \"fra mobil\"" - load-raw-images: "Vis vedhæftede bilag i original kvalitet" - load-remote-media: "Vis medie-materiale fra en ekstern server" - save: "Gem" - saved: "Gemt" - preview: "Før-visning" - search: "Søg" - delete: "Slet" - loading: "Henter" - ok: "Bekræft" - cancel: "Afbryd" - update-available-title: "Opdatering tilgængelig" - update-available: "En ny version af Misskey er nu tilgængelig ({newer}, den aktuelle version er {current}). Genindlæs siden for at fÃ¥ opdateringerne til at slÃ¥ igennem." - my-token-regenerated: "Din nøgle er blevet genopbygget, sÃ¥ du bliver logget ud." - hide-password: "Skjul adgangskoden" - show-password: "Vis adgangskoden" - enter-username: "Indtast brugernavn" - do-not-use-in-production: "Dette er en instans til udvikling. Bør ikke benyttes til produktion." - user-suspended: "Denne bruger er blevet udelukket." - is-remote-user: "Oplysningerne om denne bruger er muligvis ikke fyldestgørende" - is-remote-post: "Indholdet af denne post er spejlet fra andetsteds" - view-on-remote: "Se den fulde version eksternt" - renoted-by: "Gen-posteret af {user}" - no-notes: "Uden poster" - turn-on-darkmode: "Skift til mørk baggrund" - turn-off-darkmode: "Lys baggrund" - error: - title: "Noget gik galt :(" - retry: "Prøv igen" - reversi: - drawn: "Tegn" - my-turn: "Din tur" - opponent-turn: "Modstanderens tur" - turn-of: "{name}s tur" - past-turn-of: "{name}s tur forinden" - won: "{name} vandt" - black: "Sort" - white: "Hvid" - total: "I alt" - this-turn: "Runde {count}" - widgets: - analog-clock: "Analogt ur" - profile: "Profil" - calendar: "Kalender" - timemachine: "Kalender (tidsmaskine)" - activity: "Aktivitet" - rss: "RSS læser" - memo: "Selvklæbende noter" - trends: "Tendenser" - photo-stream: "Billedkavalkade" - posts-monitor: "Graf over poster" - slideshow: "Billedkarrusel" - version: "Version" - broadcast: "Offentliggør" - notifications: "Notifikation" - users: "Anbefalede brugere" - polls: "Afstemninger" - post-form: "Post formular" - server: "Server info" - nav: "Navigation" - tips: "Tips og tricks" - hashtags: "Hashtags" - queue: "Kø" - dev: "Fejl under oprettelse af app. Prøv igen." - ai-chan-kawaii: "Ai Chan Kawaii!" - you: "Du" -auth/views/form.vue: - share-access: "Vil du tillade, at <i>{name}</i> fÃ¥r adgang til din konto?" - permission-ask: "Denne app kræver følgende tilladelser:" - cancel: "Annuller" - accept: "Ã…bn for adgang." -auth/views/index.vue: - loading: "Henter" - denied: "Adgang til app er blevet afvist." - denied-paragraph: "Denne app vil ikke give adgang for din konto." - already-authorized: "Der er allerede adgang til denne app." - allowed: "Der er adgang til app." - callback-url: "Hopper tilbage til app." - please-go-back: "Hop tilbage til app." - error: "Sessionen eksisterer ikke." - sign-in: "Log ind." -common/views/pages/explore.vue: - pinned-users: "Fremhævede brugere" - popular-users: "Populære brugere" - recently-updated-users: "Senest aktive brugere" - recently-registered-users: "Brugere som er kommet til for nyligt" - popular-tags: "Populære tags" - federated: "Fra Fediverset" - explore: "Udforsk {host}" - users-info: "Lige nu er {users} brugere registreret her" -common/views/components/url-preview.vue: - enable-player: "Aktiver afspilning" - disable-player: "Stop afspilning" -common/views/components/user-list.vue: - no-users: "Der er ingen brugere" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "Venter pÃ¥ {}" - cancel: "Annuller" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Giv op" - surrendered: "Af taberen" - is-llotheo: "Den med færrest vinder (Llotheo)" - looped-map: "Vendebrikker" - can-put-everywhere: "Kan placeres hvorsomhelst" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "Spil Reversi med dine venner!" - invite: "Inviter" - rule: "Spilleregler" - rule-desc: "Reversi er et strategi spil for to deltagere, og det spilles pÃ¥ et bræt med 8 gange 8 felter. PÃ¥ felterne skal placeres 64 ens brikker, som er sorte pÃ¥ den ene side og hvide pÃ¥ den anden. Deltagerne vælger hver sin farve og placerer pÃ¥ skift en brik med deres egen farve opad. Det gælder om at placere brikker med sin egen farve i hver sin ende af en stribe brikker med modstanderens farve, for det giver ret til at vende de mellemliggende brikker rundt, sÃ¥ de fÃ¥r ens egen farve. Vinderen er den, som til sidst har erobret flest felter pÃ¥ brættet." - mode-invite: "Inviter" - mode-invite-desc: "Spil med en udvalgt bruger" - invitations: "Du har fÃ¥et en invitation!" - my-games: "Mine spil" - all-games: "Alle spil" - enter-username: "Angiv brugernavn" - game-state: - ended: "Slut" - playing: "I gang" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Spilleindstillinger" - choose-map: "Vælg en brikfarve" - random: "Tilfældig" - black-or-white: "Sort/hvid" - black-is: "Sort er {}" - rules: "Regler" - is-llotheo: "Den med færrest vinder (Llotheo)" - looped-map: "Vendebrikker" - can-put-everywhere: "Kan placeres hvorsomhelst" - settings-of-the-bot: "Bot indstillinger" - this-game-is-started-soon: "Spillet begynder lige om lidt" - waiting-for-other: "Venter pÃ¥ modstanderen" - waiting-for-me: "Venter pÃ¥, at du bliver klar" - waiting-for-both: "Venter pÃ¥, at spillerne er klar" - cancel: "Annuller" - ready: "Klar" - cancel-ready: "Fortryd din klar-melding" -common/views/components/connect-failed.vue: - title: "Ingen kontakt med serveren" - description: "Der er et problem med din internet forbindelse, eller sÃ¥ er serveren nede eller under vedligeholdelse. Tag og {try again} senere." - thanks: "Tak, fordi du bruger Misskey." - troubleshoot: "Fejlfinding" -common/views/components/connect-failed.troubleshooter.vue: - title: "Fejlfinding" - network: "Netværksforbindelse" - checking-network: "Tjekker netværksforbindelsen" - internet: "Internetforbindelse" - checking-internet: "Tjekker internetforbindelse" - server: "Forbindelse til server" - checking-server: "Tjekker forbindelsen til server" - finding: "Prøver at finde problemet" - no-network: "Ingen forbindelse" - no-network-desc: "Tjek en ekstra gang, om du har netværksforbindelse." - no-internet: "Det er ingen internetforbindelse" - no-internet-desc: "Tjek en ekstra gang, at du har forbindelse til internettet" - no-server: "Ude af stand til at skabe forbindelse til Misskey serveren" - no-server-desc: "Netværksforbindelsen pÃ¥ din enhed er normal, men du kunne ikke koble dig pÃ¥ Misskey serveren. Ã…rsagen kan være, at serveren er nede, eller at den er under vedligeholdelse. Prøv igen senere." - success: "Du er nu blevet koblet til Misskey serveren" - success-desc: "Det ser ud til, at der er forbindelse. Genindlæs siden." - flush: "Ryd cachen" - set-version: "Angiv version" -common/views/components/media-banner.vue: - sensitive: "Upassende PÃ¥ Jobbet" - click-to-show: "Klik for at se" -common/views/components/theme.vue: - theme: "Tema" - light-theme: "Tema i tilknytning til lys baggrund" - dark-theme: "Tema i tilknytning til mørk baggrund" - light-themes: "Lyst tema" - dark-themes: "Mørkt tema" - install-a-theme: "Installer et tema" - theme-code: "Tema kode" - install: "Installer" - installed: "\"{}\" er blevet installeret" - create-a-theme: "Opret et tema" - save-created-theme: "Gem tema" - primary-color: "Primær farve" - secondary-color: "Sekundær farve" - text-color: "Tekst farve" - base-theme: "Grundtema" - base-theme-light: "Lyst" - base-theme-dark: "Mørkt" - find-more-theme: "Find flere temaer" - theme-name: "Tema navn" - preview-created-theme: "Før-visning" - invalid-theme: "Temaet er ikke gyldigt" - already-installed: "Teamet er allerede installeret" - saved: "Gemt" - manage-themes: "Administrer temaer" - builtin-themes: "Standard temaer" - my-themes: "Mine temaer" - installed-themes: "Installerede temaer" - select-theme: "Vælg dit tema" - uninstall: "Afinstaller" - uninstalled: "\"{}\" er blevet afinstalleret" - author: "Skribent" - desc: "Beskrivelse" - export: "Eksport" - import: "Import" - import-by-code: "eller indsæt kode" - theme-name-required: "Temaet skal have et navn" -common/views/components/cw-button.vue: - hide: "Skjul" - show: "Se mere" - chars: "{count} tegn" - files: "{count} filer" - poll: "Afstemninger" -common/views/components/messaging.vue: - search-user: "Find en bruger" - you: "Du" - no-history: "Uden historik" - user: "Bruger" - group: "Gruppe" - start-with-user: "Start chat med bruger" - start-with-group: "Start chat med gruppe" - select-group: "Vælg gruppe" -common/views/components/messaging-room.vue: - not-talked-user: "Ingen bruger sessionshistorik" - not-talked-group: "Intet gruppesessions dokument" - no-history: "Der er ingen yderligere historik" - new-message: "Ny besked" - only-one-file-attached: "Kan kun indeholde én vedhæftning" -common/views/components/messaging-room.form.vue: - input-message-here: "Skriv meddelelsen her" - send: "Send" - attach-from-local: "Vedhæft filen fra din enhed" - attach-from-drive: "Vedhæft filen fra dit drev" - only-one-file-attached: "Kan kun indeholde én vedhæftning" -common/views/components/messaging-room.message.vue: - is-read: "Læst" - deleted: "Denne meddelelse er slettet" -common/views/components/nav.vue: - about: "Om" - stats: "Statistik" - status: "Status" - wiki: "Wiki" - donors: "Donatorer" - repository: "Systemets kode-repo" - develop: "Udviklere" - feedback: "Tilbagemeldinger" - tos: "Brugerbetingelser" -common/views/components/note-menu.vue: - mention: "Omtale" - detail: "Detaljer" - copy-content: "Kopier indholdet" - copy-link: "Kopier link" - favorite: "Marker denne post som favorit" - unfavorite: "Fjern favorit-markering" - watch: "Hold øje med" - unwatch: "Hold ikke længere øje med" - pin: "Tilknyt til din profil" - unpin: "Fjern tilknytning til din profil" - delete: "Slet" - delete-confirm: "Er du helt sikker pÃ¥, at du vil slette denne post?" - remote: "Vis den oprindelige post" -common/views/components/user-menu.vue: - mention: "Omtale" - mute: "Annuller" - unmute: "Ophæv annullering" - mute-confirm: "Er du sikker pÃ¥, at du vil annullere denne bruger?" - unmute-confirm: "Er du sikker pÃ¥, at du vil fjerne annulleringen af denne bruger?" - block: "Bloker" - unblock: "Fjern blokering" - block-confirm: "Er du sikker pÃ¥, at du vil blokere denne bruger?" - unblock-confirm: "Er du sikker pÃ¥, at du vil fjerne blokeringen af denne bruger?" - push-to-list: "Tilføj til liste" - select-list: "Vælg liste" - report-abuse: "Meld misbrug" - report-abuse-detail: "Hvilken form for misbrug har du været ude for?" - report-abuse-reported: "Denne hændelse er nu videresendt til administratoren. Mange tak for hjælpen." - silence: "Gør tavs" - unsilence: "Fortryd at du har gjort tavs" - silence-confirm: "Er du sikker pÃ¥, at du vil gøre denne bruger tavs?" - unsilence-confirm: "Er du sikker pÃ¥, at du har fortrudt, at du har gjort denne bruger tavs?" - suspend: "Udeluk" - unsuspend: "Ophæv udelukkelse" - suspend-confirm: "Er du sikker pÃ¥, at du vil udelukke denne bruger?" - unsuspend-confirm: "Er du sikker pÃ¥, at du vil ophæve udelukkelsen af denne bruger?" -common/views/components/poll.vue: - vote-to: "Stem pÃ¥ '{}'" - vote-count: "{} stemmer" - total-votes: "{} stemmer i alt" - vote: "Stem" - show-result: "Vis resultatet" - voted: "Stemt" - closed: "Afsluttet" - remaining-days: "{d} dage og {h} timer tilbage" - remaining-hours: "{h} timer og {m} minutter tilbage" - remaining-minutes: "{m} minutter og {s} sekunder tilbage" - remaining-seconds: "{s} sekunder tilbage" -common/views/components/poll-editor.vue: - no-only-one-choice: "Der skal vælges mindst to muligheder" - choice-n: "Valgmulighed {}" - remove: "Slet valgmulighed" - add: "Tilføj valgmulighed" - destroy: "Drop afstemningen" - multiple: "Mere end et svar er tilladt" - expiration: "Udløber" - infinite: "Uendelig" - at: "Dato- og tidsvælger" - after: "Angivet tid" - no-more: "Du kan ikke tilføje flere svar" - deadline-date: "Slutdato" - deadline-time: "Varighed" - interval: "Varighed" - unit: "Enhed" - second: "Sekunder" - minute: "Minutter" - hour: "Time" - day: "Søn" -common/views/components/reaction-picker.vue: - choose-reaction: "Vælg reaktion" -common/views/components/emoji-picker.vue: - custom-emoji: "Brugerdefineret emoji" - people: "Personer" - animals-and-nature: "Dyr og natur" - food-and-drink: "Mad og drikke" - activity: "Aktivitet" - travel-and-places: "Rejser og steder" - objects: "Objekt" - symbols: "Symboler" - flags: "Flag" -common/views/components/settings/app-type.vue: - info: "Du er nødt til at genindlæse siden, før ændringerne slÃ¥r igennem." -common/views/components/signin.vue: - username: "Brugernavn" - password: "Adgangskode" - token: "Nøgle" - signing-in: "Log ind" - or: "Eller" - signin-with-twitter: "Log ind med Twitter" - signin-with-github: "Log ind med GitHub" - signin-with-discord: "Log ind med Discord" - login-failed: "Fejl ved log ind. Sørg for, at du har skrevet korrekt brugernavn og adgangskode." -common/views/components/signup.vue: - invitation-code: "Invitationskode" - invitation-info: "Kontakt en <a href=\"{}\">administrator</a>, hvis du ikke har en invitationskode." - username: "Brugernavn" - checking: "Tjekker" - available: "Tilgængelig" - unavailable: "Ikke tilgængelig" - error: "Netværksfejl" - invalid-format: "bogstaver, tal og \"_\" er tilladt." - too-short: "MÃ¥ ikke være tom!" - too-long: "Brug højst 20 tegn." - password: "Adgangskode" - password-placeholder: "Det anbefales at skrive mere end otte tegn" - weak-password: "Svag adgangskode" - normal-password: "Rimelig adgangskode" - strong-password: "Stærk adgangskode" - retype: "Skriv igen" - retype-placeholder: "Bekræft din adgangskode" - password-matched: "Godkendt" - password-not-matched: "Ikke godkendt" - recaptcha: "Verificering" - agree-to: "Enig {0}" - tos: "Brugerbetingelser" - create: "Opret en konto" - some-error: "Af en eller anden grund mislykkedes forsøget pÃ¥ at oprette en konto. Prøv igen." -common/views/components/special-message.vue: - new-year: "Godt nytÃ¥r!" - christmas: "Glædelig jul!" -common/views/components/stream-indicator.vue: - connecting: "Tilslutter" - reconnecting: "Tilslutter igen" - connected: "Tilsluttet" -common/views/components/notification-settings.vue: - title: "Notifikationer" - mark-as-read-all-notifications: "Marker alle notifikationer som læste" - mark-as-read-all-unread-notes: "Marker alle poster som læste" - mark-as-read-all-talk-messages: "Marker alle samtaler som læste" - auto-watch: "Automatisk visning af poster" - auto-watch-desc: "Modtag automatisk notifikationer om poster, som du har reageret eller svaret pÃ¥." -common/views/components/integration-settings.vue: - title: "Service samarbejde" - connect: "Tilslut" - disconnect: "Frakobl" - connected-to: "Du er tilsluttet næste konto" -common/views/components/github-setting.vue: - description: "NÃ¥r du tilslutter din GitHub konto til din Misskey konto, bliver du i stand til at se info om din GitHub konto pÃ¥ din profil, og du vil fÃ¥ mulighed for at logge ind via GitHub." - connected-to: "Du er tilsluttet denne GitHub konto" - detail: "Flere detaljer" - reconnect: "Tilslut igen" - connect: "Tilslut til din GitHub konto" - disconnect: "Frakobl" -common/views/components/discord-setting.vue: - description: "NÃ¥r du tilslutter din Discord konto til din Misskey konto, bliver du i stand til at se info om din Discord konto pÃ¥ din profil, og du vil fÃ¥ mulighed for at logge ind via Discord." - connected-to: "Du er tilsluttet denne Discord konto" - detail: "Detaljer..." - reconnect: "Tilslut igen" - connect: "Tilslut din Discord konto" - disconnect: "Frakobl" -common/views/components/uploader.vue: - waiting: "Venter" -common/views/components/visibility-chooser.vue: - public: "Offentlig" - home: "Startside" - home-desc: "Post kun til startsiden" - followers: "Følgere" - followers-desc: "Post kun til følgere" - specified: "Direkte" - specified-desc: "Post kun til udvalgte brugere" - local-public: "Offentlig (pÃ¥ den lokale server)" - local-public-desc: "Offentliggør ikke til eksterne" - local-home: "Startside (pÃ¥ den lokale server)" - local-followers: "Følgere (pÃ¥ den lokale server)" -common/views/components/trends.vue: - count: "{} brugere nævnt" - empty: "Ingen tendenser" -common/views/components/language-settings.vue: - title: "Vis sprog" - pick-language: "Vælg sprog" - recommended: "Anbefalet" - auto: "Automatisk" - specify-language: "Angiv sprog" - info: "Du er nødt til at genindlæse siden, før ændringerne slÃ¥r igennem." -common/views/components/profile-editor.vue: - title: "Profil" - name: "Navn" - account: "Konto" - location: "Placering" - description: "Om mig" - you-can-include-hashtags: "Du mÃ¥ gerne bruge hashtags i din profil beskrivelse" - language: "Sprog" - birthday: "Fødselsdag" - avatar: "Avatar" - banner: "Banner" - is-cat: "Denne konto er en Kat" - is-bot: "Denne konto er en Bot" - is-locked: "Anmodning fra følgere kræver godkendelse" - careful-bot: "Følger anmodninger fra bots kræver godkendelse" - auto-accept-followed: "Accepter automatisk følgere af personer, som du selv følger" - advanced: "Avanceret" - privacy: "Privatliv" - save: "Gem" - saved: "Profil er opdateret med succes" - uploading: "Overfører" - upload-failed: "Fejl ved overførsel" - unable-to-process: "Handlingen kunne ikke gennemføres." - email: "Email indstillinger" - email-address: "Email adresse" - email-verified: "Din email er blevet bekræftet" - email-not-verified: "Email adresse er ikke bekræftet. Tjek indbakken i din mailboks." - export: "Eksport" - import: "Import" - export-and-import: "Eksport og import" - export-targets: - all-notes: "Alle poster" - following-list: "Liste over følgere" - mute-list: "Liste over annullerede konti" - blocking-list: "Liste over blokerede konti" - user-lists: "Lister" - export-requested: "Du har bedt om en eksport. Det kan tage et stykke tid. NÃ¥r eksporten er gennemført, vil eksport-filen blive lagt pÃ¥ dit drev." - import-requested: "Du har sat en import i gang. Det kan tage et stykke tid." - enter-password: "Angiv din adgangskode" - danger-zone: "Risici" - delete-account: "Slet kontoen" - account-deleted: "Kontoen er slettet. Det kan vare lidt, inden alle data forsvinder." - metadata-content: "Indhold" -common/views/components/user-list-editor.vue: - users: "Bruger" - rename: "Omdøb listen" - delete: "Slet liste" - remove-user: "Fjern fra denne liste" - delete-are-you-sure: "Slet liste \"$1\"?" - deleted: "Slettet med succes" - add-user: "Tilføj en bruger" -common/views/components/user-group-editor.vue: - users: "Brugere" - rename: "Omdøb gruppe" - delete: "Slet gruppe" - remove-user: "Fjern bruger fra denne gruppe" - delete-are-you-sure: "Er du sikker pÃ¥, at du vil slette gruppen \"$ 1\"?" - deleted: "Slettet" - invite: "Inviter" - invited: "Inviterede" -common/views/components/user-lists.vue: - user-lists: "Lister" - create-list: "Opret en liste" - list-name: "Liste navn" -common/views/components/user-groups.vue: - user-groups: "Gruppe" - create-group: "Opret gruppe" - group-name: "Gruppenavn" - owned-groups: "Egne grupper" - joined-groups: "Tilsluttede grupper" - invites: "Inviter" - accept-invite: "Tag imod invitation" - reject-invite: "Afvis" -common/views/widgets/broadcast.vue: - fetching: "Tjekker" - no-broadcasts: "Ingen meddelelser" - have-a-nice-day: "Hav en god dag!" - next: "Næste" -common/views/widgets/calendar.vue: - year: "Ã…r {}" - month: "{}," - day: "{}" - today: "I dag:" - this-month: "MÃ¥ned:" - this-year: "Ã…r:" -common/views/widgets/photo-stream.vue: - title: "Billedkavalkade" - no-photos: "Ingen billeder" -common/views/widgets/posts-monitor.vue: - title: "Graf over poster" - toggle: "Skift mellem visninger" -common/views/widgets/hashtags.vue: - title: "Hashtags" -common/views/widgets/server.vue: - title: "Server info" - toggle: "Skift mellem visninger" -common/views/widgets/memo.vue: - title: "Selvklæbende noter" - memo: "Skriv her!" - save: "Gem" -common/views/widgets/slideshow.vue: - folder-customize-mode: "For at kunne angive en mappe er du nødt til at gÃ¥ ud af tilpasnings indstillingerne" - folder: "Klik og angiv en mappe" - no-image: "Der er ikke noget billede i denne mappe" -common/views/widgets/tips.vue: - tips-line1: "Du kan fokusere pÃ¥ tidslinjen med <kbd>t</kbd>" - tips-line2: "Ã…bn post formularen med <kbd>p</kbd> eller <kbd>n</kbd>." - tips-line3: "Du kan trække og slippe filer pÃ¥ post formularen" - tips-line4: "Du kan indsætte et billede fra klippebordet pÃ¥ afsendelses formularen" - tips-line5: "Du kan overføre filer ved at trække og slippe dem pÃ¥ dit drev" - tips-line6: "Du kan flytte en mappe ved at trække den inden for dit drev" - tips-line7: "Du kan flytte mapper ved at trække dem inden for dit drev" - tips-line8: "Startsidens layout kan tilpasses fra indstillingerne" - tips-line9: "Misskey er licenseret under AGPLv3." - tips-line10: "Widgeten med tidsmaskinen gør det let at \"spole\" tilbage til den tidligere tidslinje." - tips-line11: "Du kan sende poster til bruger siden ved at klikke pÃ¥ \"...\"" - tips-line13: "Alle filer tilknyttet en post gemmes pÃ¥ dit drev." - tips-line14: "NÃ¥r du tilpasser layoutet pÃ¥ startsiden, kan du højreklikke pÃ¥ en widget for at ændre dens design." - tips-line17: "Du kan fremhæve en tekstbid ved at omgive den med ** **." - tips-line19: "Flere vinduer kan kobles af og vises uden for browseren." - tips-line20: "Procentdelen af kalender-widgeten viser procentdelen af den tid, der er gÃ¥et." - tips-line21: "Du kan ogsÃ¥ bruge Misskey's API til at udvikle bots." - tips-line23: "Ai Chan Kawaii!" - tips-line24: "Misskey har været i drift siden 2014." - tips-line25: "Du kan modtage notifikationer, selv om Misskey ikke er Ã¥ben, hvis du anvender en browser, der er i stand til at hÃ¥ndtere notifikationer." -common/views/pages/not-found.vue: - page-not-found: "Siden kan ikke findes" -common/views/pages/follow.vue: - signed-in-as: "Logget ind som {}" - following: "Følger" - follow: "Følg" - request-pending: "Ventende anmodninger om at blive følger" - follow-processing: "Anmoder om behandling" - follow-request: "Anmodning om at blive følger" -common/views/pages/follow-requests.vue: - received-follow-requests: "Anmodninger om at blive følgere" - accept: "Accepter" - reject: "Afvis" -desktop: - banner-crop-title: "Beskær den del, der vises som et banner" - banner: "Banner" - uploading-banner: "Overfør et nyt banner" - banner-updated: "Banner er overført med succes" - choose-banner: "Vælg banner" - avatar-crop-title: "Beskær den del, der vises som en avatar" - avatar: "Avatar" - uploading-avatar: "Overfør en ny avatar" - avatar-updated: "Avatar er overført med succes" - choose-avatar: "Vælg et billede til din avatar" - unable-to-process: "Handlingen kunne ikke gennemføres." - invalid-filetype: "Denne filtype kan ikke benyttes her" -desktop/views/components/activity.chart.vue: - total: "Sort ... Total" - notes: "BlÃ¥ ... Noter" - replies: "Rød ... Svar" - renotes: "Grøn ... Gen-postering" -desktop/views/components/activity.vue: - title: "Aktivitet" - toggle: "Skift mellem visninger" -desktop/views/components/calendar.vue: - title: "{year} / {month}" - prev: "Forrige mÃ¥ned" - next: "Næste mÃ¥ned" - go: "Klik for at navigere" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count} fil(er) er valgt" - upload: "Overfør filer fra din enhed" - cancel: "Annuller" - ok: "OK" - choose-prompt: "Vælg filer" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Annuller" - ok: "OK" - choose-prompt: "Vælg en mappe" -desktop/views/components/crop-window.vue: - skip: "Afbryd beskæring" - cancel: "Annuller" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "I brug" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Banner" - nsfw: "Upassende PÃ¥ Jobbet" - contextmenu: - rename: "Omdøb" - mark-as-sensitive: "Marker som 'følsom'" - unmark-as-sensitive: "Fjern markering som 'følsom'" - copy-url: "Kopier webadresse" - download: "Download" - else-files: "Avanceret" - set-as-avatar: "Vælg som avatar" - set-as-banner: "Vælg som banner" - open-in-app: "Ã…bn i app" - add-app: "Tilføj app" - rename-file: "Omdøb fil" - input-new-file-name: "Angiv nyt navn" - copied: "Kopieret" - copied-url-to-clipboard: "Webadressen er kopieret til klippebordet" -desktop/views/components/drive.folder.vue: - unable-to-process: "Handlingen kunne ikke gennemføres." - circular-reference-detected: "Destinationsmappen er en undermappe til den mappe, som du forsøger at flytte." - unhandled-error: "Ukendt fejl" - contextmenu: - move-to-this-folder: "Flyt til denne mappe" - show-in-new-window: "Ã…bn i nyt vindue" - rename: "Omdøb" - rename-folder: "Omdøb mappe" - input-new-folder-name: "Angiv nyt navn" - else-folders: "Avanceret" -desktop/views/components/drive.vue: - search: "Søg" - empty-draghover: "Smid det her! Fordi du ved, at jeg er meget sød, ikke?" - empty-drive: "Dit medielager er tomt" - empty-drive-description: "Højreklik for at Ã¥bne menuen, eller træk og slip filer her for at overføre." - empty-folder: "Denne mappe er tom" - unable-to-process: "Handlingen kunne ikke gennemføres." - circular-reference-detected: "Destinationsmappen er en undermappe til den mappe, som du forsøger at flytte." - unhandled-error: "Ukendt fejl" - url-upload: "Overfør fra webadresse" - url-of-file: "Webadresse pÃ¥ filen, som du vil overføre" - url-upload-requested: "Der er anmodet om overførsel" - may-take-time: "Det kan tage noget tid at gennemføre overførslen." - create-folder: "Opret en mappe" - folder-name: "Mappenavn" - contextmenu: - create-folder: "Opret en mappe" - upload: "Overfør en fil" - url-upload: "Overfør fra webadresse" -desktop/views/components/media-video.vue: - sensitive: "Indholdet er Upassende PÃ¥ Jobbet" - click-to-show: "Klik for at vise" -desktop/views/components/followers-window.vue: - followers: "{}s følgere" -desktop/views/components/followers.vue: - empty: "Det ser ikke ud til, at du har nogen følgere." -desktop/views/components/following-window.vue: - following: "Følger {}" -desktop/views/components/following.vue: - empty: "Det ser ikke ud til, at du følger brugeren..." -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "Send" - add-widget: "Tilføj widget:" - add: "Tilføj" -desktop/views/input-dialog.vue: - cancel: "Annuller" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "Posten er privat" - deleted: "Posten er blevet fjernet" - location: "Placering" - renote: "Gen-postering" - add-reaction: "Tilføj en reaktion" - undo-reaction: "Fortryd reaktion" -desktop/views/components/note.vue: - reply: "Svar" - renote: "Gen-postering" - add-reaction: "Tilføj en reaktion" - undo-reaction: "Fortryd reaktion" - detail: "Detaljer" - private: "Posten er privat" - deleted: "Posten er blevet fjernet" -desktop/views/components/notes.vue: - error: "Fejl ved indlæsning" - retry: "Prøv igen" -desktop/views/components/notifications.vue: - empty: "Der er ingen notifikationer!" -desktop/views/components/post-form.vue: - posted: "Afsendt!" - replied: "Besvaret!" - reposted: "Gen-posteret!" - note-failed: "Fejl under afsendelse" - reply-failed: "Fejl under besvarelse" - renote-failed: "Fejl under gen-postering" -desktop/views/components/post-form-window.vue: - note: "Ny post" - reply: "Svar" - attaches: "{} medier er vedhæftet" - uploading-media: "Har overført {} medier" -desktop/views/components/progress-dialog.vue: - waiting: "Venter" -desktop/views/components/renote-form.vue: - quote: "Citat" - cancel: "Annuller" - renote: "Gen-postering" - renote-home: "Gen-postering (pÃ¥ startsiden)" - reposting: "Gen-posterer" - success: "Gen-posteret!" - failure: "Fejl under gen-postering" -desktop/views/components/renote-form-window.vue: - title: "Ønsker du at gen-postere den?" -desktop/views/pages/user-following-or-followers.vue: - following: "{user} følger" - followers: "{user}s følger" -desktop/views/components/settings.2fa.vue: - intro: "Du kan forbedre sikkerheden ved at indføre to-faktor godkendelse. I sÃ¥ fald vil du bÃ¥de fÃ¥ brug for en adgangskode til log ind og en fysisk enhed (f.eks. en smartphone), som er registreret pÃ¥ forhÃ¥nd." - detail: "Mere info..." - url: "https://www.google.com/landing/2step/" - caution: "Hvis du mister adgangen til din registrerede enhed, vil du ikke længere være i stand til at koble dig pÃ¥ Misskey!" - register: "Registrer en enhed" - already-registered: "Denne enhed er allerede registreret" - unregister: "Af-registrer" - unregistered: "To-faktor godkendelse er de-aktiveret." - enter-password: "Angiv adgangskoden" - authenticator: "Allerførst skal du installere Google Authenticator pÃ¥ din enhed:" - howtoinstall: "SÃ¥dan installerer du" - token: "Nøgle" - scan: "Og derefter, scan QR koden:" - done: "Angiv den nøgle, som vises pÃ¥ din enhed:" - submit: "Send" - success: "Indstillingerne er gemt!" - failed: "Fejl ved opsætningen. Tjek at din nøgle er korrekt." - info: "Næste gang du logger pÃ¥ Misskey, vil den nøgle og adgangskode, der vises pÃ¥ din enhed, være obligatorisk." -common/views/components/media-image.vue: - sensitive: "Indholdet er Upassende PÃ¥ Jobbet" - click-to-show: "Klik for at vise" -common/views/components/api-settings.vue: - intro: "For at fÃ¥ adgang til API'en skal du sætte denne nøgle til at være søgeordet \"i\" i anmodningsparameteren." - caution: "Indtast ikke denne nøgle i nogen app, og fortæl heller ikke andre om den. I modsat fald kan din konto blive kompromitteret." - regeneration-of-token: "Hvis din nøgle er kompromitteret, kan du genskabe den." - regenerate-token: "Genskab nøgle" - token: "Nøgle:" - enter-password: "Angiv adgangskoden" - console: - title: "API konsol" - endpoint: "Endpoint" - parameter: "Parametre" - credential-info: "Denne konsol kræver ikke parameteren \"i\"." - send: "Send" - sending: "Sender" - response: "Svar" -desktop/views/components/settings.apps.vue: - no-apps: "Der er ingen tilsluttede apps" -common/views/components/drive-settings.vue: - max: "Kapacitet" - in-use: "I brug" - stats: "Statistik" - default-upload-folder-name: "Mappe(r)" -common/views/components/mute-and-block.vue: - mute-and-block: "Annuller / Bloker" - mute: "Annuller" - block: "Bloker" - no-muted-users: "Ingen annullerede brugere" - no-blocked-users: "Ingen blokerede brugere" - word-mute: "Ordfilter" - muted-words: "Frafiltrerede ord" - muted-words-description: "Mellemrum mellem ord vil blive hÃ¥ndteret, som om der stÃ¥r AND i mellem ordene i søgningen (dvs. alle ord skal være til stede). Linjeskift mellem ord vil føre til, at der søges med OR mellem ordene (dvs. kun det ene af ordene behøver være til stede)." - unmute-confirm: "Er du sikker pÃ¥, at du vil fjerne annulleringen af denne bruger?" - unblock-confirm: "Er du sikker pÃ¥, at du vil fjerne blokeringen af denne bruger?" - save: "Gem" -common/views/components/password-settings.vue: - reset: "Skift adgangskode" - enter-current-password: "Angiv den nuværende adgangskode" - enter-new-password: "Angiv den nye adgangskode" - enter-new-password-again: "Skriv den nye adgangskode igen" - not-match: "Du har skrevet den nye adgangskode pÃ¥ to forskellige mÃ¥der" - changed: "Adgangskoden er ændret" - failed: "Fejl ved ændring af adgangskode" -common/views/components/post-form-attaches.vue: - attach-cancel: "Fjern den vedhæftede fil" - mark-as-sensitive: "Marker som 'følsom'" - unmark-as-sensitive: "Fjern markering som 'følsom'" -desktop/views/components/sub-note-content.vue: - private: "Posten er privat" - deleted: "Posten er blevet fjernet" - media-count: "{} medie(r) er vedhæftet" - poll: "Afstemninger" -desktop/views/components/settings.tags.vue: - title: "Tags" - query: "Søgning (valgfri)" - add: "Tilføj" - save: "Gem" -desktop/views/components/timeline.vue: - home: "Startside" - local: "Lokal" - hybrid: "Social" - global: "Global" - mentions: "Omtaler" - messages: "Direkte poster" - list: "Liste" - hashtag: "Hashtags" - add-tag-timeline: "Tilføj hashtag sky" - add-list: "Tilføj liste" - list-name: "Navn pÃ¥ liste" -desktop/views/components/ui.header.vue: - welcome-back: "Velkommen tilbage!" - adjective: "Hr." -desktop/views/components/ui.header.account.vue: - profile: "Din profil" - lists: "Lister" - groups: "Gruppe" - follow-requests: "Anmodninger om at blive følger" - admin: "Administration" -desktop/views/components/ui.header.nav.vue: - game: "Spil" -desktop/views/components/ui.header.notifications.vue: - title: "Notifikationer" -desktop/views/components/ui.header.post.vue: - post: "Ny post" -desktop/views/components/ui.header.search.vue: - placeholder: "Søg" -desktop/views/components/user-preview.vue: - notes: "Poster" - following: "Følger" - followers: "Følgere" -desktop/views/components/users-list.vue: - all: "Alle" - iknow: "Som du ved" - fetching: "Indlæser..." -desktop/views/components/users-list-item.vue: - followed: "Følger dig" -desktop/views/components/window.vue: - popout: "Pop op vindue" - close: "Luk" -admin/views/index.vue: - dashboard: "Kontrolpanel" - instance: "Instans" - emoji: "Emoji" - moderators: "Redaktører" - users: "Brugere" - federation: "Forening" - announcements: "Annonceringer" - abuse: "Misbrug" - queue: "Job kø" - logs: "Logs" - back-to-misskey: "Tilbage til Misskey" -admin/views/dashboard.vue: - dashboard: "Kontrolpanel" - accounts: "Konto" - notes: "Poster" - drive: "Drev" - instances: "Instans" - this-instance: "Denne instans" - federated: "Forenede" -admin/views/queue.vue: - title: "Kø" - remove-all-jobs: "Ryd alle job køer" - queue: "Kø" - state: "Sorter efter" -admin/views/logs.vue: - logs: "Logs" - levels: - info: "Information" - error: "Fejl" -admin/views/abuse.vue: - title: "Misbrug" - target: "MÃ¥l" - reporter: "Kilde" - details: "Detaljer" - remove-report: "Slet" -admin/views/instance.vue: - instance: "Instans" - instance-name: "Instans navn" - instance-description: "Beskrivelse af instans" - host: "Vært" - icon-url: "Ikonets webadresse" - logo-url: "Logoets webadresse" - banner-url: "Banner billedets webadresse" - error-image-url: "Fejl billedets webadresse" - languages: "Sproget pÃ¥ denne instans" - languages-desc: "Du kan angive flere ved at adskille med mellemrum" - tos-url: "Webadresse for brugerbetingelser" - repository-url: "Webadresse for systemets kode-repo" - feedback-url: "Webadresse for tilbagemeldinger om systemet" - maintainer-config: "Administrator info" - maintainer-name: "Administrator navn" - maintainer-email: "Kontakt administrator" - advanced-config: "Andre indstillinger" - note-and-tl: "Poster og tidslinje" - drive-config: "Indstillinger for drev" - use-object-storage: "Brug af eksternt lager" - object-storage-base-url: "Webadresse" - object-storage-bucket: "Navn pÃ¥ eksternt lager" - object-storage-prefix: "Præfiks" - object-storage-endpoint: "Endpoint" - object-storage-region: "Region" - object-storage-port: "Port" - object-storage-access-key: "Genvejstast" - object-storage-secret-key: "Nøgle" - object-storage-use-ssl: "Brug SSL" - object-storage-s3-info: "NÃ¥r du bruger Amazon S3 som eksternt lager, skal du bekræfte indstillingerne for {0} samt den dertil hørende \"Terminal\" og \"Region\"." - object-storage-s3-info-here: "Her" - object-storage-gcs-info: "NÃ¥r du bruger Google Cloud Storage som eksternt lager, skal du indstille \"Terminal\" til storage.googleapis.com og forlade feltet \"Region\"." - cache-remote-files: "Cache eksterne filer" - local-drive-capacity-mb: "Kapacitet pÃ¥ hver lokal brugers drev" - remote-drive-capacity-mb: "Kapacitet pÃ¥ hver ekstern brugers drev" - mb: "I megabytes (MB)" - recaptcha-config: "Indstillinger for verificering" - recaptcha-info: "Du skal bruge en verificeringsnøgle for at aktivere verificering. Nøglen fÃ¥s pÃ¥ https://www.google.com/recaptcha/intro/" - enable-recaptcha: "Aktiver verificering" - recaptcha-site-key: "Nøgle for webstedet" - recaptcha-secret-key: "Verificeringsnøgle" - recaptcha-preview: "Før-visning" - hidden-tags: "Skjulte hashtags" - hidden-tags-info: "Brug linjeskift til at adskille de hashtags, som du vil skjule." - external-service-integration-config: "Opret forbindelse til eksterne tjenester" - twitter-integration-config: "Indstillinger for forbindelse til Twitter" - twitter-integration-info: "Webadressen for callback er sat til {url}." - enable-twitter-integration: "Aktiver forbindelsen til Twitter" - twitter-integration-consumer-key: "Brugernøgle" - twitter-integration-consumer-secret: "Brugerhemmelighed" - github-integration-config: "Indstillinger for forbindelse til GitHub" - github-integration-info: "Webadressen for callback er sat til {url}." - enable-github-integration: "Aktiver forbindelsen til GitHub" - github-integration-client-id: "Bruger ID" - github-integration-client-secret: "Brugerhemmelighed" - discord-integration-config: "Indstillinger for forbindelse til Discord" - discord-integration-info: "Webadressen for callback er sat til {url}." - enable-discord-integration: "Aktiver forbindelsen til Discord" - discord-integration-client-id: "Bruger ID" - discord-integration-client-secret: "Brugerhemmelighed" - proxy-account-config: "Indstillinger for proxy-konto" - proxy-account-info: "En proxy-konto kan følge en ekstern brugers aktivitet og hente data fra vedkommende. NÃ¥r du tilføjer en ekstern bruger, som ikke følges af nogen pÃ¥ denne instans, til din liste, vil proxy-kontoen følge ham eller hende i stedet for dine følgere." - proxy-account-username: "Brugernavn for proxy konto" - proxy-account-username-desc: "Angiv brugernavnet pÃ¥ den konto, der bliver brugt som proxy." - proxy-account-warn: "Du er nødt til at oprette en konto med dette brugernavn først." - max-note-text-length: "Det højeste antal tegn pr. post" - disable-registration: "Deaktiver oprettelse af nye brugere" - disable-local-timeline: "Deaktiver den lokale tidslinje" - disable-global-timeline: "Deaktiver den globale tidslinje" - disabling-timelines-info: "Administratorer og redaktører kan stadig bruge en tidslinje, selv om den er deaktiveret." - enable-emoji-reaction: "Aktiver piktogram med reaktioner" - use-star-for-reaction-fallback: "Fald tilbage pÃ¥ en stjerne, hvis der kommer en ukendt reaktion." - invite: "Inviter" - save: "Gem" - saved: "Gemt" - pinned-users: "Fremhæv bruger" - pinned-users-info: "Angiv brugere, du vil fremhæve, adskilt af linjeskift." - email-config: "Indstillinger for email server" - email-config-info: "Bruges til bekræftelse af email adresse og nulstilling af adgangskode." - enable-email: "Aktiver levering af emails" - email: "Email adresse" - smtp-secure: "Brug implicit SSL / TLS til SMTP-forbindelsen" - smtp-secure-info: "Sluk STARTTLS, nÃ¥r du bruger den." - smtp-host: "SMTP vært" - smtp-port: "SMTP port" - smtp-auth: "Udfør autentifikation af SMTP" - smtp-user: "SMTP bruger" - smtp-pass: "SMTP adgangskode" - test-email: "Test" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "Aktiver ServiceWorker" - serviceworker-info: "Skal være aktiveret for at give push notifikationer." - vapid-publickey: "Offentlig nøgle til VAPID" - vapid-privatekey: "Privat nøgle til VAPID" - vapid-info: "Hvis du vil aktivere ServiceWorker, skal du generere en VAPID-nøgle. Medmindre du har angivet den globale node_modules placering andetsteds, skal du køre den som root:" -admin/views/charts.vue: - title: "Graf" - per-day: "pr. dag" - per-hour: "pr. time" - federation: "Forening" - notes: "Poster" - users: "Brugere" - drive: "Drev" - network: "Netværk" - charts: - federation-instances: "Sæt antallet af instanser op eller ned" - federation-instances-total: "Antal instanser i alt" - notes: "Fremgang eller tilbagegang i antal poster (lokale + eksterne)" - local-notes: "Fremgang eller tilbagegang i antal poster (lokale)" - remote-notes: "Fremgang eller tilbagegang i antal poster (eksterne)" - notes-total: "Antal poster i alt" - users: "Fremgang eller tilbagegang i antal brugere" - users-total: "Antal brugere i alt" - active-users: "Aktive brugere" - drive: "Fremgang eller tilbagegang i brugen af drevet" - drive-total: "Forbrugt plads pÃ¥ drevet i alt" - drive-files: "Fremgang eller tilbagegang i antal filer pÃ¥ drevet" - drive-files-total: "Antal filer i alt pÃ¥ drevet" - network-requests: "Netværkskald" - network-time: "Svartid" - network-usage: "Trafik" -admin/views/drive.vue: - operation: "Drift" - fileid-or-url: "Fil ID eller webadresse" - file-not-found: "Filen kunne ikke findes" - lookup: "Forespørgsel" - sort: - title: "Sorter efter" - createdAtAsc: "Alder - ældste først" - createdAtDesc: "Alder - seneste først" - sizeAsc: "Størrelse - mindste først" - sizeDesc: "Størrelse - største først" - origin: - title: "Oprindelse" - combined: "Lokal + ekstern" - local: "Lokal" - remote: "Ekstern" - delete: "Slet" - deleted: "Slettet" - mark-as-sensitive: "Marker som 'følsom'" - unmark-as-sensitive: "Fjern markering som 'følsom'" - marked-as-sensitive: "Marker som 'følsom'" - unmarked-as-sensitive: "Fjern markering som 'følsom'" -admin/views/users.vue: - operation: "Drift" - username-or-userid: "Brugernavn eller bruger ID" - user-not-found: "Bruger kunne ikke findes" - lookup: "Opslag" - reset-password: "Nulstil adgangskode" - reset-password-confirm: "Er du sikker pÃ¥, at du vil nulstille din adgangskode?" - password-updated: "Adgangskoden er nu \"{password}\"" - suspend: "Udeluk" - suspend-confirm: "Er du sikker pÃ¥, at du vil udelukke denne konto?" - suspended: "Udelukket med succes" - unsuspend: "Annuller udelukkelse" - unsuspend-confirm: "Er du sikker pÃ¥, at du vil annullere udelukkelsen pÃ¥ denne konto?" - unsuspended: "Brugerens udelukkelse er annulleret med succes" - make-silence: "Gør tavs" - silence-confirm: "Vil du gøre brugeren tavs?" - unmake-silence: "Vil du annullere, at brugeren er gjort tavs?" - unsilence-confirm: "Er du sikker pÃ¥, at du vil omgøre, at brugeren er gjort tavs?" - update-remote-user: "Opdater informationen om den eksterne bruger" - remote-user-updated: "Informationen om den eksterne bruger er nu blevet opdateret." - delete-all-files: "Slet alle filer" - delete-all-files-confirm: "Er du sikker pÃ¥, at alle filerne skal slettes?" - username: "Brugernavn" - host: "Vært" - users: - title: "Bruger" - sort: - title: "Sorter efter" - createdAtAsc: "Tidspunkt for oprettelse (ældste først)" - createdAtDesc: "Tidspunkt for oprettelse (seneste først)" - updatedAtAsc: "Tidspunkt for seneste opdatering (ældste først)" - updatedAtDesc: "Tidspunkt for seneste opdatering (seneste først)" - state: - title: "Sorter efter" - all: "Alle" - admin: "Administrator" - moderator: "Redaktører" - adminOrModerator: "Administrator/Redaktør" - silenced: "Brugeren er i forvejen gjort tavs" - suspended: "Udelukket" - origin: - title: "Oprindelse" - combined: "Lokal + Ekstern" - local: "Lokal" - remote: "Ekstern" - createdAt: "Oprettet den" - updatedAt: "Opdateret den" -admin/views/moderators.vue: - add-moderator: - title: "Opret redaktør" - add: "Opret" - added: "Redaktør er oprettet" - remove: "Fjern" - removed: "Redaktøren er nu fjernet" - logs: - title: "Logs" - moderator: "Redaktører" - type: "Drift" - info: "Information" -admin/views/emoji.vue: - add-emoji: - title: "Tilføj emoji" - name: "Navn pÃ¥ emoji" - name-desc: "Du kan bruge tegnene fra a til z, 0 til 9 og \"_\"" - aliases: "Aliasser" - aliases-desc: "Du kan tilføje flere med mellemrum imellem" - url: "Webadresse for billede" - add: "Tilføj" - info: "Vi anbefaler PNG billeder under 50 KB." - added: "Emoji er tilføjet" - emojis: - title: "Emojis" - update: "Opdatering" - remove: "Slet" - updated: "Opdateret" - remove-emoji: - are-you-sure: "Er du sikker pÃ¥, at du vil slette \"$1\"?" - removed: "Slettet" -admin/views/announcements.vue: - announcements: "Annonceringer" - save: "Gem" - remove: "Slet" - add: "Tilføj" - title: "Titel" - text: "Indhold" - saved: "Gemt" - _remove: - are-you-sure: "Er du sikker pÃ¥, at du vil slette \"$1\"?" - removed: "Slettet" -admin/views/hashtags.vue: - hided-tags: "Skjulte tags" -admin/views/federation.vue: - instance: "Instans" - host: "Vært" - notes: "Poster" - users: "Brugere" - following: "Følger" - followers: "Følgere" - caught-at: "Oprettet den" - status: "Status" - latest-request-sent-at: "Tidspunkt for afsendelse af seneste forespørgsel" - latest-request-received-at: "Seneste forespørgsel blev modtaget den" - remove-all-following: "Annuller alle følgere" - remove-all-following-info: "Annuller alle konti fra værten {host}. Udføres nÃ¥r værten ikke længere eksisterer." - delete-all-files: "Slet alle filer" - block: "Bloker" - marked-as-closed: "Marker som lukket" - lookup: "Opslag" - instances: "Forenede" - instance-not-registered: "Instansen kan ikke findes" - sort: "Sorter efter" - sorts: - caughtAtAsc: "Tidspunkt for oprettelse (ældste først)" - caughtAtDesc: "Tidspunkt for oprettelse (seneste først)" - lastCommunicatedAtAsc: "Tidspunktet for den tidligere interaktion" - lastCommunicatedAtDesc: "Tidspunktet for den senere interaktion" - notesAsc: "Poster (mindste først)" - notesDesc: "Poster (største først)" - usersAsc: "Antal brugere (færrest først)" - usersDesc: "Antal brugere (flest først)" - followingAsc: "Antal følgere (færrest først)" - followingDesc: "Antal følgere (flest først)" - followersAsc: "Antal følgere (færrest først)" - followersDesc: "Antal følgere (flest først)" - driveUsageAsc: "Diskforbrug (mindst først)" - driveUsageDesc: "Diskforbrug (størst først)" - driveFilesAsc: "Antal filer pÃ¥ drev (færrest først)" - driveFilesDesc: "Antal filer pÃ¥ drev (flest først)" - state: "Sorter efter" - states: - all: "Alle" - blocked: "Bloker" - not-responding: "Uden svar" - marked-as-closed: "Markeret som lukkede" - result-is-truncated: "Vis de øverste {n} elementer." - charts: "Graf" - chart-srcs: - requests: "Netværkskald" - users: "Fremgang eller tilbagegang i antal brugere" - users-total: "Antal brugere i alt" - notes: "Sæt antallet af poster op eller ned" - notes-total: "Antal poster i alt" - ff: "Stigning i antal følgere" - ff-total: "Det samlede antal følgere" - drive-usage: "Fremgang eller tilbagegang i brugen af drevet" - drive-usage-total: "Forbrugt plads pÃ¥ drevet i alt" - drive-files: "Sæt antallet af filer pÃ¥ drevet op eller ned" - drive-files-total: "Samlet antal filer pÃ¥ drev" - chart-spans: - hour: "pr. time" - day: "pr. dag" - blocked-hosts: "Bloker" - blocked-hosts-info: "Beskrivelse af værterne du vil blokere, adskilt af linjeskift." - save: "Gem" -desktop/views/pages/welcome.vue: - about: "Mere info..." - timeline: "Tidslinje" - announcements: "Annonceringer" - photos: "Seneste billeder" - powered-by-misskey: "Leveret af <b>Misskey</b>." - info: "Information" -desktop/views/pages/drive.vue: - title: "Misskey drev" -desktop/views/pages/note.vue: - prev: "Forrige post" - next: "Næste post" -desktop/views/pages/selectdrive.vue: - title: "Vælg fil(er)" - ok: "OK" - cancel: "Annuller" - upload: "Overfør filer fra din enhed" -desktop/views/pages/search.vue: - not-available: "Søgefunktionen er deaktiverede i indstillingerne for denne instans." - not-found: "Ingen poster fundet for \"{q}\"" -desktop/views/pages/tag.vue: - no-posts-found: "Ingen poster fundet med \"{q}\"." -desktop/views/pages/user-list.users.vue: - users: "Bruger" - add-user: "Tilføj en bruger" - username: "Brugernavn" -desktop/views/pages/user/user.followers-you-know.vue: - title: "Følgere som du mÃ¥ske kender" - loading: "Henter" - no-users: "Ingen følgere du kender" -desktop/views/pages/user/user.friends.vue: - title: "Hyppige omtaler" - loading: "Henter" - no-users: "Ingen hyppige omtaler" -desktop/views/pages/user/user.photos.vue: - title: "Billeder" - loading: "Henter" - no-photos: "Ingen billeder" -desktop/views/pages/user/user.header.vue: - posts: "Poster" - following: "Følger" - followers: "Følgere" - is-bot: "Denne konto er en bot" - no-description: "Ingen brugerbeskrivelse" - years-old: "{age} Ã¥r" - year: "-" - month: "Man" - day: "Søn" - follows-you: "Følger dig" -desktop/views/pages/user/user.timeline.vue: - default: "Poster" - with-replies: "Poster og svar" - with-media: "Medier" - my-posts: "Mine poster" -desktop/views/widgets/notifications.vue: - title: "Notifikationer" -desktop/views/widgets/polls.vue: - title: "Afstemninger" - refresh: "Genopfrisk" - nothing: "Der er ingen notifikationer!" -desktop/views/widgets/post-form.vue: - title: "Post" - note: "Post" - something-happened: "Kunne af uvisse Ã¥rsager ikke postes" -desktop/views/widgets/profile.vue: - update-banner: "Klik for at redigere dit banner" - update-avatar: "Klik for at redigere din avatar" -desktop/views/widgets/trends.vue: - title: "Tendenser" - refresh: "Genopfrisk" - nothing: "Der er ingen notifikationer!" -desktop/views/widgets/users.vue: - title: "Anbefalede brugere" - refresh: "Genopfrisk" - no-one: "Hvad synes du?" -mobile/views/components/drive.vue: - used: "I brug" - folder-count: "Mappe(r)" - count-separator: "," - file-count: "Fil(er)" - nothing-in-drive: "Der er ikke gemt noget" - folder-is-empty: "Denne mappe er tom" - folder-name: "Mappenavn" - here-is-root: "Lige nu befinder du dig i bunden - ikke i en mappe." - url-prompt: "Webadresse pÃ¥ filen, som du vil overføre" - uploading: "Overførslen er sat i gang. Det kan tage et stykke tid." - folder-name-cannot-empty: "Et mappe er nødt til at have et navn" -mobile/views/components/drive-file-chooser.vue: - select-file: "Vælg fil" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "Vælg mappe" -mobile/views/components/drive.file.vue: - nsfw: "Indholdet er Upassende PÃ¥ Jobbet" -mobile/views/components/drive.file-detail.vue: - download: "Download" - rename: "Omdøb" - move: "Flyt" - hash: "Hashtag (md5)" - exif: "EXIF" - nsfw: "Indholdet er Upassende PÃ¥ Jobbet" - mark-as-sensitive: "Marker som 'følsom'" - unmark-as-sensitive: "Fjern markering som 'følsom'" -mobile/views/components/media-video.vue: - sensitive: "Indholdet er Upassende PÃ¥ Jobbet" - click-to-show: "Klik for at vise" -common/views/components/follow-button.vue: - following: "Følger" - follow: "Følger" - request-pending: "Ventende anmodninger om at blive følger" - follow-processing: "Anmoder om behandling" - follow-request: "Anmodninger om at blive følger" -mobile/views/components/note.vue: - private: "Posten er privat" - deleted: "Posten er blevet fjernet" - location: "Placering" -mobile/views/components/note-detail.vue: - reply: "Svar" - reaction: "Reaktion" - private: "Posten er privat" - deleted: "Posten er blevet fjernet" - location: "Placering" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/notifications.vue: - empty: "Der er ingen notifikationer!" -mobile/views/components/sub-note-content.vue: - private: "Posten er privat" - deleted: "Posten er blevet fjernet" - media-count: "{} medie(r) er vedhæftet" - poll: "Afstemninger" -mobile/views/components/ui.header.vue: - welcome-back: "Velkommen tilbage!" - adjective: "Hr." -mobile/views/components/ui.nav.vue: - timeline: "Tidslinje" - notifications: "Notifikationer" - follow-requests: "Anmodninger om at blive følger" - search: "Søg" - user-lists: "Lister" - user-groups: "Gruppe" - widgets: "Widgets" - game: "Spil" - admin: "Administration" - about: "Om" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Overfør en fil" - url-upload: "Overfør fil fra webadresse" - create-folder: "Opret en mappe" - rename-folder: "Omdøb mappe" - move-folder: "Flyt denne mappe" - delete-folder: "Slet denne mappe" -mobile/views/pages/signup.vue: - lets-start: "Din konto er nu klar! 📦" -mobile/views/pages/followers.vue: - followers-of: "{name}s følgere" -mobile/views/pages/following.vue: - following-of: "{name}s følger" -mobile/views/pages/home.vue: - home: "Startside" - local: "Lokal" - hybrid: "Social" - global: "Global" - mentions: "Omtaler" - messages: "Direkte poster" -mobile/views/pages/tag.vue: - no-posts-found: "Ingen poster fundet med \"{q}\"." -mobile/views/pages/widgets.vue: - dashboard: "Kontrolpanel" - widgets-hints: "Du kan tilføje / slette / flytte rundt pÃ¥ widgets. For at flytte widgeten skal du trække “三â€. Klik pÃ¥ \"×\" for at slette widgeten. Nogle widgets kan tilpasses direkte ved at klikke pÃ¥ dem." - add-widget: "Tilføj" - customization-tips: "Tips om tilpasning" -mobile/views/pages/widgets/activity.vue: - activity: "Aktivitet" -mobile/views/pages/share.vue: - share-with: "Del med {name}" -mobile/views/pages/note.vue: - title: "Post" - prev: "Forrige post" - next: "Næste post" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "Søg" - not-found: "Ingen poster fundet for \"{q}\"" -mobile/views/pages/selectdrive.vue: - select-file: "Vælg fil(er)" -mobile/views/pages/notifications.vue: - notifications: "Notifikationer" -mobile/views/pages/settings.vue: - signed-in-as: "Logget ind som {}" -mobile/views/pages/user.vue: - follows-you: "Følger dig" - following: "Følger" - followers: "Følgere" - notes: "Poster" - overview: "Oversigt" - timeline: "Tidslinje" - media: "Medier" - years-old: "{age} Ã¥r" -mobile/views/pages/user/home.vue: - recent-notes: "Seneste poster" - images: "Billeder" - activity: "Aktivitet" - keywords: "Nøgleord" - domains: "Domæner" - frequently-replied-users: "Hyppige omtaler" - followers-you-know: "Følgere som du mÃ¥ske kender" - last-used-at: "Senest aktiv:" -mobile/views/pages/user/home.photos.vue: - no-photos: "Ingen billeder" -deck: - widgets: "Widgets" - home: "Startside" - local: "Lokal" - hybrid: "Social" - hashtag: "Hashtags" - global: "Global" - mentions: "Omtaler" - direct: "Direkte poster" - notifications: "Notifikationer" - list: "Lister" - select-list: "Vælg liste" - swap-left: "Flyt til venstre" - swap-right: "Flyt til højre" - swap-up: "Flyt op" - swap-down: "Flyt ned" - remove: "Fjern" - add-column: "Tilføj en kolonne" - rename: "Omdøb" - stack-left: "Fold sammen til venstre" - pop-right: "Parker til højre" - disabled-timeline: - title: "Tidslinjen er deaktiveret" - description: "Tidslinjen er deaktiveret af serverens administrator" -deck/deck.tl-column.vue: - is-media-only: "Poster med medieindhold" - edit: "Valgmuligheder" -deck/deck.user-column.vue: - follows-you: "Følger dig" - posts: "Poster" - following: "Følger" - followers: "Følgere" - images: "Billeder" - activity: "Aktivitet" - timeline: "Tidslinje" - pinned-notes: "Fremhævede poster" -docs: - edit-this-page-on-github: "Har du fundet en fejl, eller ønsker at bidrage til dokumentationen?" - edit-this-page-on-github-link: "Rediger denne side pÃ¥ GitHub" -dev/views/index.vue: - manage-apps: "Administrer apps" -dev/views/apps.vue: - manage-apps: "Administrer apps" - create-app: "Opret app" - app-missing: "Ingen apps" -dev/views/new-app.vue: - new-app: "Ny app" - new-app-info: "Du kan ogsÃ¥ oprette en app fra API'en. (app / opret)" - create-app: "Opretter app" - app-name: "Navn pÃ¥ app" - app-name-placeholder: "F.eks. Misskey for iOS" -pages: - pin-this-page: "Tilknyt til din profil" - unpin-this-page: "Fjern tilknytning til din profil" - like: "Synes om" - title: "Titel" - blocks: - section: "Sektion" - image: "Billeder" - button: "Knap" - if: "Hvis" - _if: - variable: "Variabel" - post: "Post formular" - _post: - text: "Indhold" - textInput: "Indtastet tekst" - _textInput: - name: "Variabel navn" - text: "Titel" - default: "Standard værdi" - textareaInput: "Tekst med flere linjer" - _textareaInput: - name: "Variabel navn" - text: "Titel" - default: "Standard værdi" - numberInput: "Indtastet tal" - _numberInput: - name: "Variabel navn" - text: "Titel" - default: "Standard værdi" - switch: "Kontakt" - _switch: - name: "Variabel navn" - text: "Titel" - default: "Standard værdi" - counter: "Tæller" - _counter: - name: "Variabel navn" - text: "Titel" - inc: "Forhøj tallet med en" - _button: - text: "Titel" - action: "Handling nÃ¥r der trykkes pÃ¥ knappen" - _action: - dialog: "Vis dialogboks" - _dialog: - content: "Indhold" - resetRandom: "Nulstil tilfældigt tal" - _radioButton: - name: "Variabel navn" - title: "Titel" - default: "Standard værdi" - script: - categories: - flow: "Kontrol" - logical: "Logisk handling" - operation: "Eksekver" - comparison: "Sammenlign" - random: "Tilfældig" - value: "Værdi" - fn: "Funktion" - text: "Tekstmanipulation" - convert: "Konverter" - list: "Lister" - blocks: - text: "Text" - multiLineText: "Tekst med linjeskift" - textList: "Punktliste" - _textList: - info: "Brug linjeskift til at adskille linjerne" - strLen: "Længde af tekststrengen" - _strLen: - arg1: "Tekst" - strPick: "Udtræk et tegn" - _strPick: - arg1: "Text" - arg2: "Tegnets position" - strReplace: "Erstat tekststreng(e)" - _strReplace: - arg1: "Tekst" - arg2: "Før søg-og-erstat" - arg3: "Efter søg-og-erstat" - strReverse: "Vend teksten rundt" - _strReverse: - arg1: "Tekst" - join: "Sammenflet tekst" - _join: - arg1: "Lister" - arg2: "Skilletegn" - add: "+ Plus" - _add: - arg1: "A" - arg2: "B" - subtract: "- Minus" - _subtract: - arg1: "A" - arg2: "B" - multiply: "× Multiplicer" - _multiply: - arg1: "A" - arg2: "B" - divide: "÷ Divider" - _divide: - arg1: "A" - arg2: "B" - _mod: - arg1: "A" - arg2: "B" - _round: - arg1: "Tal" - eq: "A og B er ens" - _eq: - arg1: "A" - arg2: "B" - notEq: "A og B er forskellige" - _notEq: - arg1: "A" - arg2: "B" - and: "A og B" - _and: - arg1: "A" - arg2: "B" - or: "A eller B" - _or: - arg1: "A" - arg2: "B" - lt: "A er mindre end B" - _lt: - arg1: "A" - arg2: "B" - gt: "A er større end B" - _gt: - arg1: "A" - arg2: "B" - ltEq: "A er mindre end eller lig med B" - _ltEq: - arg1: "A" - arg2: "B" - gtEq: "A er større end eller lig med B" - _gtEq: - arg1: "A" - arg2: "B" - if: "Branch" - _if: - arg1: "Hvis" - arg2: "sÃ¥" - arg3: "ellers" - not: "negation" - _not: - arg1: "negation" - random: "Tilfældig" - _random: - arg1: "Sandsynlighed" - rannum: "Tilfældigt tal" - _rannum: - arg1: "Minimum" - arg2: "Maximum" - randomPick: "Vælg tilfældig fra listen" - _randomPick: - arg1: "Lister" - dailyRandom: "Tilfældigt (pr. bruger pr. dag)" - _dailyRandom: - arg1: "Sandsynlighed" - dailyRannum: "Tilfældigt antal (pr. bruger pr. dag)" - _dailyRannum: - arg1: "Minimum" - arg2: "Maximum" - dailyRandomPick: "Tilfældigt valgt fra listen (pr. bruger pr. dag)" - _dailyRandomPick: - arg1: "Lister" - seedRandom: "Tilfældig (frø)" - _seedRandom: - arg1: "Frø" - arg2: "Sandsynlighed" - seedRannum: "Tilfældigt antal (frø)" - _seedRannum: - arg1: "Frø" - arg2: "Minimum" - arg3: "Maximum" - seedRandomPick: "Vælg tilfældigt (frø) fra listen" - _seedRandomPick: - arg1: "Frø" - arg2: "Lister" - DRPWPM: "Tilfældigt valgt fra sandsynlighedslisten (pr. bruger pr. dag)" - _DRPWPM: - arg1: "Punktliste" - pick: "Vælg fra listen" - _pick: - arg1: "Lister" - arg2: "Position" - _listLen: - arg1: "Lister" - number: "Tal" - stringToNumber: "Tekst til tal" - _stringToNumber: - arg1: "Tekst" - numberToString: "Tal til tekst" - _numberToString: - arg1: "Tal" - splitStrByLine: "Del teksten op i linjer" - _splitStrByLine: - arg1: "Tekst" - ref: "Variabel" - fn: "Funktion" - _fn: - slots: "Slot-funktion" - slots-info: "Brug linjeskift til at adskille slot-funktionerne" - arg1: "Output" - for: "Gentag" - _for: - arg1: "Antal gange" - arg2: "Proces" - typeError: "Slot-funktionen {slot} skal gennemløbe i \"{expect}\", men den faktiske indtastning er \"{actual}\"!" - thereIsEmptySlot: "Slot-funktionen {slot} er tom!" - types: - string: "Tekst" - number: "Tal" - boolean: "Sand/falsk" - array: "Lister" - stringArray: "Punktliste" - emptySlot: "Tomt slot" - enviromentVariables: "Miljø variabel" - pageVariables: "Side element" - argVariables: "Input slot" -room: - translate: "Flyt" - save: "Gem" - saved: "Gemt" - furnitures: - moon: "MÃ¥ne" - bin: "Skraldespand" diff --git a/locales/de-DE.yml b/locales/de-DE.yml deleted file mode 100644 index db3daf5246716b874da341167180eb4a66cb832e..0000000000000000000000000000000000000000 --- a/locales/de-DE.yml +++ /dev/null @@ -1,954 +0,0 @@ ---- -meta: - lang: "Deutsch" -common: - misskey: "Ein â des Fediversums" - about-title: "Ein â des Fediversums." - about: "Danke, dass Du Misskey gefunden hast. Misskey ist eine <b>dezentralisierte Microblogging-Plattform</b>, welche auf der ganzen Welt verteilt ist. Da es innerhalb es Fediversums existiert (ein Universum, in dem verschiedene Soziale Netzwerke organisiert sind), ist es unmittelbar mit anderen sozialen Netzwerken verbunden. Warum nimmst du dir nicht einmal eine Auszeit von dem Trubel der Stadt und tauchst in das neue Internet hinein?" - intro: - title: "Was ist Misskey?" - about: "Misskey ist eine Quelloffene, <b>dezentralisierte microblogging Software</b>. Es bietet eine erweiterbare Benutzeroberfläche, verschiedenste Möglichkeiten auf Beiträge zu reagieren, kostenlosen Datenspeicher, und andere fortschrittliche Funktionen. Zusätzlich ist Misskey dazu in der Lage, sich mittels des Fediverse mit beliebig vielen anderen ActivityPub-kompatiblen Diensten zu verbinden. Wenn du zum Beispiel einen Betrag mit Misskey abschickst, wird dieser auch für Nutzer von Mastodon oder Pleroma sichtbar sein. So ähnlich wie eine Radioübertragung zwischen Planeten." - features: "Funktionen" - rich-contents: "Notizen" - rich-contents-desc: "Poste einfach deine Ideen, Interessen und alles, was du teilen möchtest. Gestalte deine Nachrichten, teile deine Lieblingsbilder, sende Dateien und Videos und erstelle Umfragen – das und mehr kann Misskey!" - reaction: "Reaktionen" - reaction-desc: "Der einfachste Weg, deine Gefühle mit anderen zu teilen. Mit Misskey kannst du auf verschiedenste Arten auf Beiträge reagieren, statt nur zu „liken“." - ui: "Benutzeroberfläche" - ui-desc: "Geschmäcker sind verschieden. Deswegen ist Misskeys Oberfläche hochanpassbar und modular. Mache die Startseite zu deiner Startseite, indem du das Layout deiner Timeline änderst und mit Widgets staffierst." - drive: "Drive" - drive-desc: "Du willst ein hochgeladenes Foto nochmal posten? Deine Dateien benennen und in Ordnern sortieren? Misskeys Drive ist der beste Ort dafür. Damit wird das Teilen zum Kinderspiel!" - outro: "Probiere Misskey aus und entdecke Misskeys einzigartige Funktionen. Wenn dir diese Instanz nicht zusagt, nimm einfach eine andere. Dank Misskeys dezentralem System kannst du dich überall mit deinen Freunden verbinden. Also dann, GLHF!" - application-authorization: "Autorisierte Anwendungen" - close: "Schließen" - do-not-copy-paste: "Bitte keinen Code einfügen. Ihr Account könnte gefährdet werden." - load-more: "Mehr laden" - enter-password: "Bitte Passwort eingeben" - 2fa: "Zwei-Faktor-Authentifizierung" - customize-home: "Layout Anpassen" - featured-notes: "Beliebt" - dark-mode: "Dunkler Modus" - signin: "Einloggen" - signup: "Registrieren" - signout: "Ausloggen" - reload-to-apply-the-setting: "Die Seite muss zum Ãœbernehmen dieser Einstellung aktualisiert werden. Soll die Seite jetzt neu geladen werden?" - fetching-as-ap-object: "Hole Daten…" - delete-confirm: "Diesen Beitrag löschen?" - notification-types: - reply: "Antworten" - renote: "Anmerkung" - got-it: "Verstanden!" - customization-tips: - title: "Anpassung-Tipps" - paragraph: "<p>Du kannst deine Startseite anpassen, indem du Widgets hinzufügst und verschiebst.</p><p><strong>Klicke <strong>rechts</strong></strong> auf ein Widget, um dessen Aussehen zu verändern.</p><p>Um ein Widget zu löschen, klicke und ziehe es auf den <strong>Papierkorb</strong> am Kopfende der Seite.</p><p>Wenn du fertig bist, drücke auf den Beenden-Knopf oben rechts.</p>" - gotit: "Verstanden!" - notification: - file-uploaded: "Datei hochgeladen!" - message-from: "Nachricht von {}:" - reversi-invited: "Zu einem Spiel eingeladen" - reversi-invited-by: "Eingeladen von {}:" - notified-by: "Benachrichtigt von {}:" - reply-from: "Antwort von {}:" - quoted-by: "Zitiert von {}:" - time: - unknown: "Unbekannt" - future: "Zukunft" - just_now: "Gerade eben" - seconds_ago: "vor {} Sekunde(n)" - minutes_ago: "vor {} Minute(n)" - hours_ago: "vor {} Stunde(n)" - days_ago: "vor {} Tag(en)" - weeks_ago: "vor {} Woche(n)" - months_ago: "vor {} Monat(en)" - years_ago: "vor {} Jahr(en)" - month-and-day: "{day}/{month}" - trash: "Papierkorb" - drive: "Drive" - pages: "Seite" - messaging: "Unterhaltungen" - home: "Home" - deck: "Deck" - timeline: "Zeitleiste" - explore: "Entdecken" - following: "Folgt" - followers: "Folgende" - favorites: "Favoriten" - permissions: - "read:account": "Accountinformationen anzeigen." - "write:account": "Accountinformationen bearbeiten." - "read:blocks": "Blöcke anzeigen" - "write:blocks": "Auf Sperrungen zugreifen" - "read:drive": "Dateien anzeigen" - "write:drive": "Dateien bearbeiten" - "read:favorites": "Favoriten anzeigen" - "write:favorites": "Auf Favoriten zugreifen" - "read:following": "Follower-Daten lesen" - "write:following": "Folgestatus bearbeiten" - "read:messaging": "Unterhaltung anzeigen" - "write:messaging": "Unterhaltung bearbeiten" - "read:mutes": "Stummschaltungen lesen" - "write:mutes": "Stummschaltungen bearbeiten" - "write:notes": "Beiträge löschen und verfassen" - "read:notifications": "Benachrichtigungen lesen" - "write:notifications": "Benachrichtigungen bearbeiten" - "read:reactions": "Reaktionen sehen" - "write:reactions": "Reaktionen hinzufügen und bearbeiten" - "write:votes": "Abstimmen" - empty-timeline-info: - follow-users-to-make-your-timeline: "Beiträge von Benutzern, denen du folgst, werden in der Zeitleiste angezeigt." - explore: "Benutzer finden" - post-form: - reply: "Antworten" - renote: "Anmerkung" - enter-username: "Bitte gib einen Benutzernamen ein" - username-prompt: "Bitte gib einen Benutzernamen ein" - weekday-short: - sunday: "So" - monday: "Mo" - tuesday: "Di" - wednesday: "Mi" - thursday: "Do" - friday: "Fr" - saturday: "Sa" - weekday: - sunday: "Sonntag" - monday: "Montag" - tuesday: "Dienstag" - wednesday: "Mittwoch" - thursday: "Donnerstag" - friday: "Freitag" - saturday: "Samstag" - reactions: - like: "Gefällt mir" - love: "Lieben" - laugh: "Lachen" - hmm: "Hmm...?" - surprise: "Wow" - congrats: "Glückwunsch!" - angry: "Wütend" - confused: "Verwirrt" - rip: "RIP" - pudding: "Pudding" - note-visibility: - public: "Öffentlich" - home: "Startseite" - home-desc: "Auf die Startseite posten" - followers: "Abonnenten" - followers-desc: "Nur für diejenigen sichtbar, die dir folgen" - specified: "Direkt" - specified-desc: "Nur für bestimmte Benutzer sichtbar" - local-public: "Öffentlich (nur lokal)" - local-home: "Home (nur lokal)" - local-followers: "Follower (nur lokal)" - note-placeholders: - a: "Was machst du gerade?" - b: "Was ist so passiert?" - c: "Was geht dir durch den Kopf?" - d: "Willst du etwas sagen?" - e: "Schreib hier etwas!" - f: "Warte darauf, das du schreibst..." - settings: "Einstellungen" - _settings: - profile: "Dein Profil" - notification: "Benachrichtigungen" - apps: "Anwendungen" - tags: "Hashtags" - mute-and-block: "Stummschalten/Blocken" - blocking: "Blocken" - security: "Sicherheit" - signin: "Login-Verlauf" - password: "Passwort" - other: "Mehr" - appearance: "Designs" - behavior: "Verhalten" - fetch-on-scroll: "Unendliches laden beim scrollen" - fetch-on-scroll-desc: "Wenn beim scrollen das Ende erreicht wird, lädt die Anwendung automatisch neue Inhalte nach." - note-visibility: "Sichtbarkeit von Beiträgen" - default-note-visibility: "Die Standardsichtbarkeit" - remember-note-visibility: "Erinnerung an Sichtbarkeit von Beiträgen" - web-search-engine: "Web-Suchmaschine" - web-search-engine-desc: "Beispiel: https://www.google.de/search?q={{query}}" - keep-cw: "Inhaltswarnung beibehalten" - keep-cw-desc: "Wenn auf einen Beitrag geantwortet wird, wird die Inhaltswarnung des Originalbeitrags übernommen." - i-like-sushi: "Ich bevorzuge Sushi anstelle von Pudding" - show-reversi-board-labels: "Zeige Reihen- und Spaltenbeschreibungen in Reversi an" - use-avatar-reversi-stones: "Avatar als Stein in Reversi anzeigen" - disable-animated-mfm: "Animierten Text in Beiträgen deaktivieren" - disable-showing-animated-images: "Animierte Grafiken deaktivieren" - suggest-recent-hashtags: "Beim Verfassen von Beiträgen letzte Hashtags anzeigen" - always-show-nsfw: "Sensible Inhalte (NSFW) immer anzeigen" - always-mark-nsfw: "Meine Anhänge immer als NSFW markieren" - show-full-acct: "Servername bei Benutzernamen immer anzeigen" - show-via: "„via“ anzeigen" - reduce-motion: "Animationen der Benutzeroberfläche reduzieren" - this-setting-is-this-device-only: "Nur auf diesem Gerät" - use-os-default-emojis: "Betriebssystem-Emojis nutzen" - line-width: "Linienstärke" - line-width-thin: "Dünn" - line-width-normal: "Normal" - line-width-thick: "Dick" - font-size: "Schriftgröße" - font-size-x-small: "Sehr klein" - font-size-small: "Klein" - font-size-medium: "Normal" - font-size-large: "Groß" - font-size-x-large: "Sehr groß" - deck-column-align: "Spaltenaufteilung der Deck-Ansicht" - deck-column-align-center: "Mitte" - deck-column-align-left: "Links" - deck-column-align-flexible: "Flexibel" - deck-column-width: "Spaltenbreite des Decks" - deck-column-width-narrow: "Sehr eng" - deck-column-width-narrower: "Eng" - deck-column-width-normal: "Normal" - deck-column-width-wider: "Breit" - deck-column-width-wide: "Sehr breit" - use-shadow: "Nutze Schatten" - rounded-corners: "Abgerundete Ecken" - circle-icons: "Kreisförmige Avatar" - contrasted-acct: "Nutzernamen kontrastreicher darstellen" - wallpaper: "Hintergrund" - choose-wallpaper: "Hintergrund auswählen" - delete-wallpaper: "Hintergrund entfernen" - post-form-on-timeline: "Beitragsformular über Timeline anzeigen" - show-clock-on-header: "Uhr am oberen rechten Rand anzeigen" - show-reply-target: "Zeige bei einer Antwort die ursprüngliche Nachricht" - timeline: "Timeline" - show-my-renotes: "Zeige eigene Renotes in der Timeline" - show-renoted-my-notes: "Zeige Renotes deiner Posts in der Timeline" - show-local-renotes: "Zeige Renotes lokaler Posts in der Timeline" - remain-deleted-note: "Gelöschte Beiträge weiterhin anzeigen" - sound: "Töne" - enable-sounds: "Töne aktivieren" - enable-sounds-desc: "Spiel einen Ton ab beim Erhalten eines Beitrags bzw. einer Nachricht. Diese Einstellung wird im Browser gespeichert." - volume: "Lautstärke" - test: "Test" - update: "Misskey-Update" - version: "Version:" - latest-version: "Neuste Version:" - update-checking: "Suche nach Updates" - do-update: "Nach Updates suchen" - update-settings: "Erweiterte Einstellungen" - no-updates: "Kein Update verfügbar" - no-updates-desc: "Misskey ist aktuell." - update-available: "Eine neue Version ist verfügbar!" - update-available-desc: "Änderungen werden beim Neuladen der Seite angewendet." - advanced-settings: "Erweiterte Einstellungen" - debug-mode: "Debug-Modus einschalten" - debug-mode-desc: "Diese Einstellung wird im Browser gespeichert." - navbar-position: "Postion der Navigationsleiste" - navbar-position-top: "Oben" - navbar-position-left: "Links" - navbar-position-right: "Rechts" - i-am-under-limited-internet: "Ich möchte Datenvolumen sparen" - post-style: "Beitrags-Anzeigestil" - post-style-standard: "Standard" - post-style-smart: "Smart" - notification-position: "Benachrichtigungen anzeigen" - notification-position-bottom: "Unten" - notification-position-top: "Oben" - disable-via-mobile: "Beitrag nicht als „vom Handy“ markieren" - load-raw-images: "Anhänge in voller Größe laden" - load-remote-media: "Zeige Inhalte von fremden Servern" - save: "Speichern" - saved: "Gespeichert" - preview: "Vorschau" - search: "Suche" - delete: "Löschen" - loading: "Laden" - ok: "Okay" - cancel: "Abbrechen" - update-available-title: "Aktualisierung verfügbar" - update-available: "Eine neue Version von Misskey ist verfügbar ({newer}, aktuell ist {current}). Lade die Seite neu um die aktuelle Version zu laden" - my-token-regenerated: "Dein Token wurde generiert. Du wirst jetzt abgemeldet." - hide-password: "Passwort verbergen" - show-password: "Passwort zeigen" - enter-username: "Kontonamen eingeben" - do-not-use-in-production: "Dies ist eine Entwicklungsversion. Nicht in einer Produktivumgebung verwenden." - user-suspended: "Dieser Nutzer wurde gesperrt." - is-remote-user: "Diese Nutzerinformationen können unvollständig sein." - is-remote-post: "Dies ist ein entfernter Post." - view-on-remote: "Vollständige Infos auf Ursprungsserver anzeigen" - renoted-by: "Renote von {user}" - no-notes: "Keine Beiträge" - turn-on-darkmode: "Dunkles Design" - turn-off-darkmode: "Helles Design" - error: - title: "Allgemeiner Fehler" - retry: "Erneut versuchen" - reversi: - drawn: "Unentschieden" - my-turn: "Du bist am Zug" - opponent-turn: "Dein Gegner ist an der Reihe" - turn-of: "{name}s Zug" - past-turn-of: "Zug von {name}" - won: "{name} hat gewonnen" - black: "Schwarz" - white: "Weiß" - total: "Gesamt" - this-turn: "{count}. Zug" - widgets: - analog-clock: "Analoge Uhr" - profile: "Profil" - calendar: "Kalender" - timemachine: "Kalender (Zeitmaschine)" - activity: "Aktivitäten" - rss: "RSS Leser" - memo: "Notizen" - trends: "Trends" - photo-stream: "Bilder" - posts-monitor: "Beitrags-Aktivität" - slideshow: "Diashow" - version: "Version" - broadcast: "Ankündigungen" - notifications: "Benachrichtigungen" - users: "Empfohlene Benutzer" - polls: "Umfrage" - post-form: "\"Neuer Beitrag\"-Formular" - server: "Server-Info" - nav: "Navigation" - tips: "Tipps" - hashtags: "Hashtags" - queue: "Warteschlange" - dev: "Fehler beim Erstellen der Applikation. Bitte versuche es erneut." - ai-chan-kawaii: "Ai-chan kawaii!" - you: "Du" -auth/views/form.vue: - share-access: "Erlaubst Du <i>{name}</i> auf deinen Account zuzugreifen?" - permission-ask: "Diese Applikation benötigt folgende Berechtigungen:" - cancel: "Abbrechen" - accept: "Zugriff erlauben." -auth/views/index.vue: - loading: "Lädt" - denied: "Autorisierung der Anwendung wurde verweigert." - denied-paragraph: "Diese App kann nicht auf deinen Account zugreifen." - already-authorized: "Diese Anwendung ist bereits autorisiert." - allowed: "Autorisierung der Anwendung wurde erlaubt." - callback-url: "Zur App zurückkehren" - please-go-back: "Bitte gehe zurück zur Anwendung." - error: "Sitzung ist nicht vorhanden." - sign-in: "Bitte melde dich an." -common/views/pages/explore.vue: - pinned-users: "Vorgeschlagen" - popular-users: "Beliebt" - recently-updated-users: "Kürzlich aktiv" - recently-registered-users: "Neue Benutzer" - popular-tags: "Beliebte Tags" - federated: "Aus dem Fediverse" - explore: "{host} erkunden" - users-info: "Momentan sind {users} Nutzer hier registriert" -common/views/components/url-preview.vue: - enable-player: "Player öffnen" - disable-player: "Player schließen" -common/views/components/user-list.vue: - no-users: "Keine Benutzer" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "Warten auf {}" - cancel: "Abbrechen" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Aufgeben" - surrendered: "durch Aufgabe" - is-llotheo: "Der niedrigere gewinnt (Llotheo)" - looped-map: "Spielbrettenden verbinden" - can-put-everywhere: "Setzen ist überall erlaubt" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "Spiele Reversi mit deinen Freunden!" - invite: "Einladen" - rule: "Spielanleitung" - mode-invite: "Einladen" - all-games: "Alle Spiele" - enter-username: "Bitte gib einen Benutzernamen ein" - game-state: - ended: "Fertig" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Spieleinstellungen" - choose-map: "Wähle eine Karte" - random: "Zufällige Auswahl" - black-or-white: "Schwarz/Weiß" - black-is: "Schwarz ist {}" - rules: "Regeln" - is-llotheo: "Der niedrigere gewinnt (Llotheo)" - looped-map: "Spielbrettenden verbinden" - can-put-everywhere: "Setzen ist überall erlaubt" - settings-of-the-bot: "Bot-Einstellungen" - this-game-is-started-soon: "Spiel beginnt gleich" - waiting-for-other: "Warte auf den Gegner" - waiting-for-me: "Warten, bis du bereit bist" - waiting-for-both: "Vorbereiten…" - cancel: "Abbrechen" - ready: "Bereit" -common/views/components/connect-failed.vue: - title: "Verbindung zum Server ist fehlgeschlagen" - description: "Entweder gibt es ein Problem mit deiner Internetverbindung oder der Server ist zur Zeit nicht erreichbar oder wird gewartet. Bitte versuche es später noch einmal." - thanks: "Vielen Dank für das nutzen von Misskey." - troubleshoot: "Problembehandlung" -common/views/components/connect-failed.troubleshooter.vue: - title: "Problembehandlung" - network: "Netzwerkverbindung" - checking-network: "Prüfen der Netzwerkverbindung" - internet: "Internetverbindung" - checking-internet: "Internetverbindung wird getestet" - server: "Serververbindung" - checking-server: "Ãœberprüfung der Server-Verbindung" - finding: "Nach dem Problem suchen" - no-network: "Keine Netzwerkverbindung" - no-network-desc: "Bitte stelle sicher, dass du mit dem Internet verbunden bist." - no-internet: "Keine Internetverbindung" - no-internet-desc: "Bitte vergewissere dich, dass du mit dem Internet verbunden bist." - no-server: "Verbindung mit dem Server nicht möglich" - no-server-desc: "Die Internetverbindung scheint in Ordnung zu sein, aber eine Verbindung mit dem Misskey-Server konnte nicht hergestellt werden. Möglicherweise ist dieser zur Zeit offline oder wird gewartet. Bitte versuche es später noch einmal." - success: "Erfolgreich mit dem Misskey-Server verbunden" - success-desc: "Die Verbindung scheint zu funktionieren. Bitte lade die Seite neu." - flush: "Cache leeren" - set-version: "Version angeben" -common/views/components/media-banner.vue: - sensitive: "Dieser Inhalt ist NSFW" - click-to-show: "Klicke zum den Inhalt anzusehen" -common/views/components/theme.vue: - theme: "Design" - light-theme: "Thema" - dark-theme: "Thema während des Nachtmodus" - light-themes: "Helles Thema" - dark-themes: "Dunkles Thema" - install-a-theme: "Design wird installiert" - theme-code: "Design-Quelltext" - install: "Anwenden" - installed: "\"{}\" wurde installiert" - create-a-theme: "Thema erstellen" - save-created-theme: "Thema speichern" - primary-color: "Primäre Farbe" - secondary-color: "Sekundäre Farbe" - text-color: "Textfarbe" - base-theme: "Basisthema" - base-theme-light: "Hell" - base-theme-dark: "Dunkel" - find-more-theme: "Mehr Designs finden" - theme-name: "Name des Themas" - preview-created-theme: "Vorschau" - invalid-theme: "Thema ist ungültig" - already-installed: "Thema ist bereits installiert" - saved: "Gespeichert" - manage-themes: "Designs verwalten" - builtin-themes: "Standard-Designs" - my-themes: "Meine Designs" - installed-themes: "Installierte Designs" - select-theme: "Design wählen" - uninstall: "Deinstallieren" - uninstalled: "„{}“ wurde deinstalliert" - author: "Autor" - desc: "Beschreibung" - export: "Exportieren" - import: "Importieren" - import-by-code: "oder Quelltext einfügen" - theme-name-required: "Design-Name ist erforderlich" -common/views/components/cw-button.vue: - hide: "Ausblenden" - show: "Mehr" - chars: "{count} Zeichen" - files: "{count} Dateien" - poll: "Umfrage" -common/views/components/messaging.vue: - search-user: "Einen Nutzer suchen" - you: "Du" - no-history: "Keine Chronik" - user: "Benutzer" - group: "Gruppen" -common/views/components/messaging-room.vue: - no-history: "Keine weitere Chronik vorhanden" - new-message: "Neue Nachricht" -common/views/components/messaging-room.form.vue: - input-message-here: "Nachricht hier eingeben" - send: "Senden" - attach-from-local: "Wähle Dateien von deinem PC aus" - attach-from-drive: "Wähle Dateien von deinem Speicher aus" -common/views/components/messaging-room.message.vue: - is-read: "Gelesen" - deleted: "Diese Nachricht wurde gelöscht" -common/views/components/nav.vue: - about: "Ãœber" - stats: "Statistiken" - status: "Status" - wiki: "Wiki" - donors: "Spender" - repository: "Quellcode" - develop: "Entwickler" - feedback: "Feedback" -common/views/components/note-menu.vue: - mention: "Erwähnungen" - detail: "Details" - copy-content: "Inhalt kopieren" - copy-link: "Link kopieren" - favorite: "Diesen Beitrag favorisieren" - unfavorite: "Aus Favoriten entfernen" - watch: "Beobachten" - unwatch: "Nicht mehr beobachten" - pin: "An die Profilseite pinnen" - unpin: "Lösen" - delete: "Löschen" - delete-confirm: "Diesen Beitrag löschen?" - remote: "Auf Quelle anzeigen" -common/views/components/user-menu.vue: - mention: "Erwähnungen" - mute: "Stummschalten" - unmute: "Stummschaltung aufheben" - mute-confirm: "Bist du sicher, dass du diesen Nutzer stummschalten möchtest?" - unmute-confirm: "Stummschaltung für diesen Nutzer aufheben?" - block: "Sperren" - unblock: "Sperrung aufheben" - block-confirm: "Diesen Nutzer wirklich sperren?" -common/views/components/poll.vue: - vote-to: "Stimme für '{}'" - vote-count: "{} Stimmen" - vote: "Abstimmen" - show-result: "Zeige Ergebnis" - voted: "Abgestimmt" -common/views/components/poll-editor.vue: - no-only-one-choice: "Du musst zwei oder mehr Entscheidungen angeben" - choice-n: "Auswahl {}" - remove: "Diese Auswahl entfernen" - add: "+ Eine Auswahl hinzufügen" - destroy: "Diese Abstimmung löschen" - day: "So" -common/views/components/reaction-picker.vue: - choose-reaction: "Wähle eine Reaktion aus" -common/views/components/emoji-picker.vue: - activity: "Aktivität" -common/views/components/signin.vue: - username: "Benutzername" - password: "Passwort" - token: "Token" - signing-in: "Melde an..." - or: "Oder" -common/views/components/signup.vue: - username: "Benutzername" - checking: "Ãœberprüfung..." - available: "Verfügbar" - unavailable: "Nicht verfügbar" - error: "Verbindungsfehler" - invalid-format: "Benutze nur Buchstaben, Zahlen und _" - too-short: "Bitte mindestens ein Zeichen eingeben" - too-long: "Bitte maximal 20 Zeichen verwenden" - password: "Passwort" - password-placeholder: "Wir empfehlen mindestens 8 Zeichen" - weak-password: "Schwaches Passwort" - normal-password: "Normales Passwort" - strong-password: "Starkes Passwort" - retype: "Wiederholen" - retype-placeholder: "Bitte das Passwort erneut eingeben" - password-matched: "OK" - password-not-matched: "Stimmt nicht überein" - recaptcha: "Captcha" - create: "Account erstellen" - some-error: "Die Anmeldung konnte aufgrund eines Fehler nicht abgeschlossen werden. Bitte versuche es erneut." -common/views/components/special-message.vue: - new-year: "Frohes neues Jahr!" - christmas: "Frohe Weihnachten!" -common/views/components/stream-indicator.vue: - connecting: "Verbindung wird hergestellt" - reconnecting: "Erneut verbinden" - connected: "Verbindung hergestellt" -common/views/components/notification-settings.vue: - title: "Benachrichtigungen" -common/views/components/uploader.vue: - waiting: "Warten" -common/views/components/visibility-chooser.vue: - public: "Öffentlich" - home: "Home" - home-desc: "Auf die Startseite posten" - followers: "Folgende" - followers-desc: "Nur für diejenigen sichtbar, die dir folgen" - specified: "Direkt" - specified-desc: "Nur für bestimmte Benutzer sichtbar" - local-public: "Öffentlich (nur lokal)" - local-home: "Home (nur lokal)" - local-followers: "Follower (nur lokal)" -common/views/components/profile-editor.vue: - title: "Dein Profil" - name: "Name" - avatar: "Avatar" - banner: "Banner" - save: "Speichern" - unable-to-process: "Der Vorgang konnte nicht abgeschlossen werden" - export: "Exportieren" - import: "Importieren" - export-targets: - user-lists: "Listen" - enter-password: "Bitte Passwort eingeben" -common/views/components/user-group-editor.vue: - invite: "Einladen" -common/views/components/user-lists.vue: - user-lists: "Listen" -common/views/components/user-groups.vue: - user-groups: "Gruppen" - owned-groups: "Meine Gruppen" - invites: "Einladen" -common/views/widgets/broadcast.vue: - fetching: "Laden" - no-broadcasts: "Keine Broadcasts" - have-a-nice-day: "Schönen Tag!" - next: "Nächster" -common/views/widgets/photo-stream.vue: - title: "Fotostream" - no-photos: "Keine Fotos" -common/views/widgets/posts-monitor.vue: - title: "Beitrags-Aktivität" - toggle: "Sicht umschalten" -common/views/widgets/server.vue: - title: "Serverinformationen" - toggle: "Sicht umschalten" -common/views/widgets/memo.vue: - title: "Notizen" - memo: "Schreib hier!" - save: "Speichern" -desktop: - banner: "Banner" - avatar: "Avatar" - unable-to-process: "Der Vorgang konnte nicht abgeschlossen werden" -desktop/views/components/activity.chart.vue: - total: "Schwarz ... komplett" - notes: "Blau ... Hinweise" - replies: "Rot ... Antworten" - renotes: "Grün ... Anmerkungen" -desktop/views/components/activity.vue: - title: "Aktivität" - toggle: "Sichten umschalten" -desktop/views/components/calendar.vue: - prev: "Vorheriger Monat" - next: "Nächster Monat" - go: "Klicke zur Navigation" -desktop/views/components/choose-file-from-drive-window.vue: - upload: "Dateien von deinem PC hochladen" - cancel: "Abbrechen" - ok: "OK" - choose-prompt: "Wähle eine Datei aus" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Abbrechen" - ok: "OK" - choose-prompt: "Wähle einen Ordner" -desktop/views/components/crop-window.vue: - skip: "Zuschneiden überspringen" - cancel: "Abbrechen" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "benutzt" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Banner" - contextmenu: - rename: "Umbenennen" - copy-url: "URL kopieren" - download: "Download" - set-as-avatar: "Als Avatar festlegen" - set-as-banner: "Setze als Banner" - open-in-app: "In der App öffnen" - add-app: "App hinzufügen" - rename-file: "Datei umbennen" - input-new-file-name: "Gib den neuen Dateinamen an" - copied: "Kopieren erfolgreich" - copied-url-to-clipboard: "URL wurde in die Zwischenablage kopiert" -desktop/views/components/drive.folder.vue: - unable-to-process: "Der Vorgang konnte nicht abgeschlossen werden" - circular-reference-detected: "Das Zielverzeichnis ist innerhalb des Verzeichnisses, dass du verschieben möchtest" - unhandled-error: "Unbekannter Fehler" - contextmenu: - move-to-this-folder: "Verschiebe in diesen Ordner" - show-in-new-window: "In einem neuen Fenster anzeigen" - rename: "Umbenennen" - rename-folder: "Ordner umbenennen" - input-new-folder-name: "Namen für neuen Ordner eingeben" -desktop/views/components/drive.vue: - search: "Suchen" - empty-draghover: "Herzlich Willkommen!" - empty-drive: "Dein Speicher ist leer" - empty-drive-description: "Du kannst rechts klicken und \"Datei hochladen\" auswählen oder eine Datei per Drag and Drop auf das Fenster ziehen." - empty-folder: "Dieser Ordner ist leer" - unable-to-process: "Der Vorgang konnte nicht beendet werden" - circular-reference-detected: "Das Zielverzeichnis ist innerhalb des Verzeichnisses, dass du verschieben möchtest" - unhandled-error: "Unbekannter Fehler" - url-upload: "Von einer URL hochladen" - url-of-file: "URL der Datei, welche du hochladen möchtest" - url-upload-requested: "Upload angefordert" - may-take-time: "Es kann eine Weile dauern, bis der Upload fertiggestellt ist." - create-folder: "Ein Verzeichnis erstellen" - folder-name: "Ordnername" - contextmenu: - create-folder: "Ein Verzeichnis erstellen" - upload: "Eine Datei hochladen" - url-upload: "Von einer URL hochladen" -desktop/views/components/followers.vue: - empty: "Dir scheint niemand zu folgen." -desktop/views/components/following.vue: - empty: "Du folgst niemanden" -desktop/views/components/home.vue: - done: "Beenden" - add-widget: "Widget hinzufügen:" - add: "Hinzufügen" -desktop/views/input-dialog.vue: - cancel: "Abbrechen" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "Dieser Beitrag ist privat" - deleted: "Dieser Beitrag wurde entfernt" - location: "Ort" - renote: "Anmerkung" - add-reaction: "Reaktion hinzufügen" -desktop/views/components/note.vue: - reply: "Antworten" - renote: "Anmerkung" - detail: "Details" - private: "Dieser Beitrag ist privat" - deleted: "Dieser Beitrag wurde entfernt" -desktop/views/components/notes.vue: - error: "Laden fehlgeschlagen." - retry: "Erneut versuchen" -desktop/views/components/notifications.vue: - empty: "Keine Benachrichtigungen" -desktop/views/components/post-form.vue: - posted: "Gepostet!" - replied: "Geantwortet!" - reposted: "Weitergesagt!" - note-failed: "Anmerkung fehlgeschlagen" - reply-failed: "Antwort fehlgeschlagen" - renote-failed: "Anmerkung fehlgeschlagen" -desktop/views/components/post-form-window.vue: - note: "Neuer Beitrag" - reply: "Antworten" - attaches: "{} Medien hinzugefügt" - uploading-media: "Lade {} Medien hoch" -desktop/views/components/progress-dialog.vue: - waiting: "Warten" -desktop/views/components/renote-form.vue: - quote: "Zitieren..." - cancel: "Abbrechen" - renote: "Anmerkung" - reposting: "Weitersagen..." - success: "Weitergesagt!" - failure: "Weitersagen fehlgeschlagen" -desktop/views/components/renote-form-window.vue: - title: "Bist du dir sicher, dass du das weitersagen willst?" -desktop/views/components/settings.2fa.vue: - url: "https://www.google.de/intl/de/landing/2step/" - register: "Ein Gerät registrieren" - already-registered: "Das Gerät wurde bereits registriert" - unregister: "Abschalten" - unregistered: "Zwei-Faktor-Authentifizierung wurde deaktiviert." - enter-password: "Bitte Passwort eingeben" - token: "Token" -common/views/components/api-settings.vue: - enter-password: "Bitte Passwort eingeben" - console: - parameter: "Parameter" - send: "Senden" -common/views/components/drive-settings.vue: - in-use: "benutzt" - stats: "Statistiken" -common/views/components/mute-and-block.vue: - unmute-confirm: "Stummschaltung für diesen Nutzer aufheben?" - save: "Speichern" -desktop/views/components/sub-note-content.vue: - private: "Dieser Beitrag ist privat" - deleted: "Dieser Beitrag wurde entfernt" - poll: "Umfrage" -desktop/views/components/settings.tags.vue: - add: "Hinzufügen" - save: "Speichern" -desktop/views/components/timeline.vue: - home: "Home" - local: "Lokal" - global: "Global" - list: "Listen" -desktop/views/components/ui.header.account.vue: - profile: "Dein Profil" - lists: "Listen" - groups: "Gruppen" -desktop/views/components/ui.header.nav.vue: - game: "Spielen" -desktop/views/components/ui.header.notifications.vue: - title: "Benachrichtigungen" -desktop/views/components/ui.header.post.vue: - post: "Einen neuen Post erstellen" -desktop/views/components/ui.header.search.vue: - placeholder: "Suchen" -desktop/views/components/users-list.vue: - fetching: "Lade…" -admin/views/dashboard.vue: - drive: "Drive" -admin/views/abuse.vue: - details: "Details" - remove-report: "Löschen" -admin/views/instance.vue: - recaptcha-preview: "Vorschau" - invite: "Einladen" - save: "Speichern" - saved: "Gespeichert" - test-email: "Test" -admin/views/charts.vue: - drive: "Drive" -admin/views/drive.vue: - origin: - local: "Lokal" - delete: "Löschen" -admin/views/users.vue: - username: "Benutzername" - users: - origin: - local: "Lokal" -admin/views/emoji.vue: - add-emoji: - add: "Hinzufügen" - emojis: - remove: "Löschen" -admin/views/announcements.vue: - save: "Speichern" - remove: "Löschen" - add: "Hinzufügen" - saved: "Gespeichert" -admin/views/federation.vue: - status: "Status" - save: "Speichern" -desktop/views/pages/note.vue: - prev: "Vorheriger Kommentar" - next: "Nächster Kommentar" -desktop/views/pages/selectdrive.vue: - title: "Wähle Datei(en) aus" - ok: "OK" - cancel: "Abbrechen" -desktop/views/pages/user-list.users.vue: - username: "Benutzername" -desktop/views/pages/user/user.followers-you-know.vue: - loading: "Laden" -desktop/views/pages/user/user.friends.vue: - loading: "Laden" -desktop/views/pages/user/user.photos.vue: - loading: "Laden" - no-photos: "Keine Fotos" -desktop/views/pages/user/user.header.vue: - month: "Mo" - day: "So" -desktop/views/widgets/notifications.vue: - title: "Benachrichtigungen" -desktop/views/widgets/polls.vue: - title: "Umfrage" - nothing: "Keine Benachrichtigungen" -desktop/views/widgets/trends.vue: - nothing: "Keine Benachrichtigungen" -mobile/views/components/drive.vue: - used: "benutzt" - folder-name: "Ordnername" - url-prompt: "URL der Datei, welche du hochladen möchtest" -mobile/views/components/drive.file-detail.vue: - download: "Download" - rename: "Umbenennen" -mobile/views/components/note.vue: - private: "Dieser Beitrag ist privat" - deleted: "Dieser Beitrag wurde entfernt" - location: "Ort" -mobile/views/components/note-detail.vue: - reply: "Antworten" - private: "Dieser Beitrag ist privat" - deleted: "Dieser Beitrag wurde entfernt" - location: "Ort" -mobile/views/components/notifications.vue: - empty: "Keine Benachrichtigungen" -mobile/views/components/sub-note-content.vue: - private: "Dieser Beitrag ist privat" - deleted: "Dieser Beitrag wurde entfernt" - poll: "Umfrage" -mobile/views/components/ui.nav.vue: - notifications: "Benachrichtigungen" - search: "Suchen" - user-lists: "Listen" - user-groups: "Gruppen" - game: "Spielen" - about: "Ãœber" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Eine Datei hochladen" - create-folder: "Ein Verzeichnis erstellen" -mobile/views/pages/home.vue: - home: "Home" - local: "Lokal" - global: "Global" -mobile/views/pages/widgets.vue: - add-widget: "Hinzufügen" - customization-tips: "Anpassungs-Tipps" -mobile/views/pages/widgets/activity.vue: - activity: "Aktivität" -mobile/views/pages/note.vue: - prev: "Vorheriger Kommentar" - next: "Nächster Kommentar" -mobile/views/pages/search.vue: - search: "Suchen" -mobile/views/pages/notifications.vue: - notifications: "Benachrichtigungen" -mobile/views/pages/user/home.vue: - activity: "Aktivität" - keywords: "Schlagwörter" -mobile/views/pages/user/home.photos.vue: - no-photos: "Keine Fotos" -deck: - home: "Home" - local: "Lokal" - global: "Global" - notifications: "Benachrichtigungen" - list: "Listen" - rename: "Umbenennen" -deck/deck.user-column.vue: - following: "Folgen" - followers: "Folgende" - images: "Bilder" - activity: "Aktivität" - timeline: "Zeitleiste" - pinned-notes: "Angeheftete Beiträge" -docs: - edit-this-page-on-github: "Hast Du einen Fehler gefunden oder Lust, diese Dokumentation zu verbessern?" - edit-this-page-on-github-link: "Seite auf GitHub bearbeiten!" -dev/views/index.vue: - manage-apps: "Anwendungen verwalten" -dev/views/apps.vue: - manage-apps: "Anwendungen verwalten" - create-app: "Anwendung erstellen" - app-missing: "Keine Anwendungen" -dev/views/new-app.vue: - create-app: "Erstelle Anwendung" - app-name: "Name der Anwendung" - app-name-desc: "Der Name der Anwendung" - app-overview: "Beschreibung der Anwendung" - callback-url: "Callback-URL (optional)" - callback-url-desc: "Die URL, auf die nach erfolgreicher Authentifizierung umgeleitet werden soll." - authority: "Berechtigungen" - authority-desc: "Nur die hier eingetragenen Berechtigungen, werden per API zur Verfügung stehen." - authority-warning: "Dies kann auch nach dem erstellen der Anwendung geändert werden, allerdings werden dann alle bisher generierten Token ungültig." -pages: - pin-this-page: "An die Profilseite pinnen" - unpin-this-page: "Lösen" - like: "Gefällt mir" - blocks: - post: "\"Neuer Beitrag\"-Formular" - script: - categories: - random: "Zufällige Auswahl" - list: "Listen" - blocks: - _join: - arg1: "Listen" - random: "Zufällige Auswahl" - _randomPick: - arg1: "Listen" - _dailyRandomPick: - arg1: "Listen" - _seedRandomPick: - arg2: "Listen" - _pick: - arg1: "Listen" - _listLen: - arg1: "Listen" - types: - array: "Listen" -room: - save: "Speichern" - saved: "Gespeichert" - furnitures: - moon: "Mond" - bin: "Papierkorb" diff --git a/locales/en-US.yml b/locales/en-US.yml deleted file mode 100644 index 94f728b4767111a16c5a1168bc66fbb36a329926..0000000000000000000000000000000000000000 --- a/locales/en-US.yml +++ /dev/null @@ -1,2175 +0,0 @@ ---- -meta: - lang: "English" -common: - misskey: "A â of the fediverse" - about-title: "A â of the fediverse." - about: "Thank you for finding Misskey. Misskey is a <b>decentralized microblogging platform</b> born on Earth. Since it exists within the Fediverse (a universe where various social media platforms are organized), it is mutually linked with other social media platforms. Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet?" - intro: - title: "What is Misskey?" - about: "Misskey is an open-source, <b>decentralized microblogging software</b>. It has a sophisticated, fully customizable user interface, a variety of ways for expressing a reaction to posts, free file storage providing an integrated management system, and other advanced features are available. In addition, Misskey connects to a network system called the “Fediverseâ€, which enables us to communicate with users on other SNSs. For example, when you post something, it will be sent not only to Misskey users, but also those on Mastodon and Pleroma. Just imagine that the planet is sending a radio transmission to another planet, in order to communicate." - features: "Features" - rich-contents: "Post" - rich-contents-desc: "Just post your idea, hot topics, and anything you want to share. You may want to decorate your words, attach your favorite pictures, send files, including videos, or create a poll - those are some of the things you can do with Misskey!" - reaction: "Reactions" - reaction-desc: "The easiest way to express your emotions. Misskey allows you to add various reactions to posts. Other SNSs only have a \"like\" reaction." - ui: "Interface" - ui-desc: "No single UI can suit everyone. Therefore, Misskey has a highly customizable UI for your tastes. You can make your home original by editing the layout of your timeline, and moving around selectable widgets that you can easily adjust to make this place your own." - drive: "Drive" - drive-desc: "Wanna post a picture you have already uploaded? Wish to organize, name and create a folder for your uploaded files? Misskey Drive is the best solution for you. Very easy to share your files online." - outro: "Check Misskey-unique features by seeing them with your own eyes! If you feel like this instance is not for you, try other instances, as Misskey is a decentralized SNS, so that you can easily find your mates. Then, GLHF!" - application-authorization: "Application authorizations" - close: "Close" - do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised." - load-more: "Read more" - enter-password: "Enter your password" - 2fa: "Two-factor authentication" - customize-home: "Customize home layout" - featured-notes: "Featured notes" - dark-mode: "Dark Mode" - signin: "Login" - signup: "Sign up" - signout: "Logout" - reload-to-apply-the-setting: "You'll need to reload the page to reflect this setting. Do you want to reload it now?" - fetching-as-ap-object: "Inquiring to fediverse" - unfollow-confirm: "Do you want to unfollow {name}?" - delete-confirm: "Are you sure you want to delete this post?" - signin-required: "Please login" - notification-type: "Notification Type" - notification-types: - all: "All" - pollVote: "Votes" - follow: "Following" - receiveFollowRequest: "Follow requests" - reply: "Reply" - quote: "Quote" - renote: "Renote" - mention: "Mentions" - reaction: "Reaction" - got-it: "Got it!" - customization-tips: - title: "Customization tips" - paragraph: "<p>Home customization allows you to add/delete, drag and drop and rearrange widgets.</p><p>You can change the display by <strong><strong>right</strong> clicking</strong> on some widgets.</p><p>To delete a widget, drag and drop the widget onto <strong>the area labeled \"Trash\"</strong> in the header.</p><p>To finish the customization, click \"Done\" on the upper right.</p>" - gotit: "Got it!" - notification: - file-uploaded: "File uploaded!" - message-from: "Message from {}:" - reversi-invited: "Invited to a game" - reversi-invited-by: "Invited by {}:" - notified-by: "Notified by {}:" - reply-from: "Reply from {}:" - quoted-by: "Quoted by {}:" - time: - unknown: "unknown" - future: "future" - just_now: "now" - seconds_ago: "{}s ago" - minutes_ago: "{}m ago" - hours_ago: "{}h ago" - days_ago: "{}d ago" - weeks_ago: "{}week(s) ago" - months_ago: "{}month(s) ago" - years_ago: "{}year(s) ago" - month-and-day: "{month}/{day}" - trash: "Trash" - drive: "Drive" - pages: "Pages" - messaging: "Talk" - home: "Home" - deck: "Deck" - timeline: "Timeline" - explore: "Explore" - following: "Following" - followers: "Followers" - favorites: "Favorites" - permissions: - "read:account": "View account information" - "write:account": "Update your account information" - "read:blocks": "View Blocks" - "write:blocks": "Work with Blocks" - "read:drive": "Browse the Drive" - "write:drive": "Work with the Drive" - "read:favorites": "View Favorites" - "write:favorites": "Work with Favorites" - "read:following": "View your Follow info" - "write:following": "Work with Follow info" - "read:messaging": "View Messaging" - "write:messaging": "Work with Messaging" - "read:mutes": "View Muted" - "write:mutes": "Work with Muted" - "write:notes": "Create and delete posts" - "read:notifications": "View notifications" - "write:notifications": "Work with notifications" - "read:reactions": "View reactions" - "write:reactions": "Work with reactions" - "write:votes": "Vote" - "read:pages": "View Pages" - "write:pages": "Update Pages" - "read:page-likes": "View Likes on Pages" - "write:page-likes": "Update Likes on Pages" - "read:user-groups": "View User groups" - "write:user-groups": "Edit User groups" - empty-timeline-info: - follow-users-to-make-your-timeline: "Following users will show their posts in your timeline." - explore: "Find users" - post-form: - attach-location-information: "Attach location information" - hide-contents: "Hide contents" - reply-placeholder: "Reply to this post..." - quote-placeholder: "Quote this Post..." - option-quote-placeholder: "Quote this post... (optional)" - quote-attached: "Quoted" - quote-question: "Do you want to append a quote?" - submit: "Post" - reply: "Reply" - renote: "Renote" - posting: "Posting" - attach-media-from-local: "Attach media from your device" - attach-media-from-drive: "Attach media from your Drive" - insert-a-kao: "v('ω')v" - create-poll: "Create a poll" - text-remain: "{} characters remaining" - recent-tags: "Recent" - local-only-message: "This post will only be published locally" - click-to-tagging: "Click to tagging" - visibility: "Visibility" - geolocation-alert: "Your device does not provide location services" - error: "Error" - enter-username: "Please enter username" - specified-recipient: "Recipient" - add-visible-user: "Add a user" - cw-placeholder: "Comments for the post (optional)" - username-prompt: "Please enter username" - enter-file-name: "Edit file name" - weekday-short: - sunday: "S" - monday: "M" - tuesday: "T" - wednesday: "W" - thursday: "T" - friday: "F" - saturday: "S" - weekday: - sunday: "Sunday" - monday: "Monday" - tuesday: "Tuesday" - wednesday: "Wednesday" - thursday: "Thursday" - friday: "Friday" - saturday: "Saturday" - reactions: - like: "Like" - love: "Love" - laugh: "Laugh" - hmm: "Hmm...?" - surprise: "Wow" - congrats: "Congrats!" - angry: "Angry" - confused: "Confused" - rip: "RIP" - pudding: "Pudding" - note-visibility: - public: "Public" - home: "Home" - home-desc: "Post to the home timeline only" - followers: "Followers" - followers-desc: "Post to followers only" - specified: "Direct" - specified-desc: "Post to specified users only" - local-public: "Public (Only local)" - local-home: "Home (Only local)" - local-followers: "Followers (Only local)" - note-placeholders: - a: "What are you doing?" - b: "What's happening?" - c: "What’s on your mind?" - d: "What do you want to say?" - e: "Write here" - f: "Waiting for your writing." - settings: "Settings" - _settings: - profile: "Profile" - notification: "Notification" - apps: "Apps" - tags: "Hashtag" - mute-and-block: "Mute / Block" - blocking: "Block" - security: "Security" - signin: "Login History" - password: "Password" - other: "Other" - appearance: "Appearance" - behavior: "Behavior" - reactions: "Reaction" - reactions-description: "Customize Emojis of Reaction picker delimited by line breaks" - fetch-on-scroll: "Endless loading on scroll" - fetch-on-scroll-desc: "When you scroll down the page, it automatically fetches additional content." - note-visibility: "Post visibility" - default-note-visibility: "Default visibility" - remember-note-visibility: "Remember post visibility" - web-search-engine: "Web search engine" - web-search-engine-desc: "Example: https://www.google.com/?#q={{query}}" - paste: "Paste" - pasted-file-name: "Template for pasted file name" - pasted-file-name-desc: "Example: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" - paste-dialog: "Edit the pasted file name" - paste-dialog-desc: "Display a dialog to edit the file name when you paste a file." - keep-cw: "Preserve content warning" - keep-cw-desc: "When replying to a post, the same content warning is set by default to the reply, as has been set by the original post." - i-like-sushi: "I prefer sushi rather than pudding" - show-reversi-board-labels: "Show row and column labels in Reversi" - use-avatar-reversi-stones: "Use avatar as a stone in reversi" - disable-animated-mfm: "Disable animated texts in a post" - disable-showing-animated-images: "Do not play animated images" - enable-quick-notification-view: "Enable Quick Notification View" - suggest-recent-hashtags: "Show recent popular hashtags on the post form" - always-show-nsfw: "Always show NSFW contents" - always-mark-nsfw: "Always mark posts with media attachments as NSFW" - show-full-acct: "Do not omit the hostname from the username" - show-via: "Show via" - reduce-motion: "Reduce motion in UI" - this-setting-is-this-device-only: "Only for this device" - use-os-default-emojis: "Use the OS default Emojis" - line-width: "Line thickness" - line-width-thin: "Thin" - line-width-normal: "Regular" - line-width-thick: "Thick" - font-size: "Text size" - font-size-x-small: "Very small" - font-size-small: "Small" - font-size-medium: "Medium" - font-size-large: "Big" - font-size-x-large: "Very big" - deck-column-align: "Deck column alignment" - deck-column-align-center: "Center" - deck-column-align-left: "Left" - deck-column-align-flexible: "Flexible" - deck-column-width: "Deck column width" - deck-column-width-narrow: "Narrow" - deck-column-width-narrower: "Narrower" - deck-column-width-normal: "Regular" - deck-column-width-wider: "Slightly wide" - deck-column-width-wide: "Wide" - use-shadow: "Use shadows in the UI" - rounded-corners: "Round the corners of the UI" - circle-icons: "Use circular avatar icon" - contrasted-acct: "Add contrast to user account" - wallpaper: "Background image" - choose-wallpaper: "Choose a background" - delete-wallpaper: "Remove background" - post-form-on-timeline: "Display the posting form at the top of the timeline" - show-clock-on-header: "Show clock on the upper-right" - show-reply-target: "Show reply target" - timeline: "Timeline" - show-my-renotes: "Show my renotes in the timeline" - show-renoted-my-notes: "Show renotes of your own posts in the timeline" - show-local-renotes: "Show renotes of local posts on the timeline" - remain-deleted-note: "Continue to show deleted notes" - sound: "Sound" - enable-sounds: "Enable sounds" - enable-sounds-desc: "Play a sound when you receive a post/message. This setting is stored in the browser." - volume: "Volume" - test: "Test" - update: "Misskey Update" - version: "Current version:" - latest-version: "Latest version:" - update-checking: "Checking for updates" - do-update: "Check for updates" - update-settings: "Advanced settings" - no-updates: "No updates are available" - no-updates-desc: "Your Misskey is up to date." - update-available: "A new version is available" - update-available-desc: "Updates will be applied after reloading the page." - advanced-settings: "Advanced Settings" - debug-mode: "Enable debug mode" - debug-mode-desc: "This setting is stored in the browser." - navbar-position: "Navbar position" - navbar-position-top: "Top" - navbar-position-left: "Left" - navbar-position-right: "Right" - i-am-under-limited-internet: "I have limited bandwidth" - post-style: "Note display style" - post-style-standard: "Standard" - post-style-smart: "Smart" - notification-position: "Show notifications" - notification-position-bottom: "Bottom" - notification-position-top: "Top" - disable-via-mobile: "Don't mark the post as 'from mobile'" - load-raw-images: "Show attached images in original quality" - load-remote-media: "Show media from a remote server" - sync: "Sync" - save: "Save" - saved: "Saved" - preview: "Preview" - home-profile: "Home profile" - deck-profile: "Deck profile" - room: "Room" - _room: - graphicsQuality: "Graphics Quality" - _graphicsQuality: - ultra: "Ultra" - high: "High" - medium: "Medium" - low: "Low" - cheep: "Cheep" - useOrthographicCamera: "Use Orthographic Camera" - search: "Search" - delete: "Delete" - loading: "Loading" - ok: "Confirm" - cancel: "Cancel" - update-available-title: "Update available" - update-available: "A new version of Misskey is now available({newer}, the current version is {current}). Reload the page to apply updates." - my-token-regenerated: "Your token has been regenerated, so you will be signed out." - hide-password: "Hide Password" - show-password: "Show Password" - enter-username: "Please enter username" - do-not-use-in-production: "This is a development build. Do not use in production." - user-suspended: "This user has been suspended." - is-remote-user: "The information about this user may not be entirely complete." - is-remote-post: "These post contents are mirrored." - view-on-remote: "For completion, view it remotely." - renoted-by: "Renoted by {user}" - no-notes: "Without any notes" - turn-on-darkmode: "Switch to Dark mode" - turn-off-darkmode: "Light mode" - error: - title: "Something happened :(" - retry: "Retry" - reversi: - drawn: "Draw" - my-turn: "Your turn" - opponent-turn: "Opponent's turn" - turn-of: "{name}'s turn" - past-turn-of: "{name}'s turn" - won: "{name} won" - black: "Black" - white: "White" - total: "Total" - this-turn: "Turn {count}" - widgets: - analog-clock: "Analog clock" - profile: "Profile" - calendar: "Calendar" - timemachine: "Calendar (Time Machine)" - activity: "Activity" - rss: "RSS reader" - memo: "Sticky note" - trends: "Trends" - photo-stream: "Photostream" - posts-monitor: "Chart of posts" - slideshow: "Slideshow" - version: "Version" - broadcast: "Broadcast" - notifications: "Notifications" - users: "Recommended users" - polls: "Polls" - post-form: "Post form" - server: "Server info" - nav: "Navigation" - tips: "Tips" - hashtags: "Hashtags" - queue: "Queue" - dev: "Failed to create the application. Please try again." - ai-chan-kawaii: "Ai-chan kawaii!" - you: "You" -auth/views/form.vue: - share-access: "Would you allow <i>{name}</i> to access your account?" - permission-ask: "This application requires the following permissions:" - cancel: "Cancel" - accept: "Allow access." -auth/views/index.vue: - loading: "Loading" - denied: "Application authorization has been denied." - denied-paragraph: "This application will not access your account." - already-authorized: "This application has already been authorized." - allowed: "Application authorizations allowed." - callback-url: "Going back to the application." - please-go-back: "Please go back to the application." - error: "Session does not exist." - sign-in: "Please sign in." -common/views/pages/explore.vue: - pinned-users: "Pinned users" - popular-users: "Popular users" - recently-updated-users: "Recently active users" - recently-registered-users: "Users who joined recently" - recently-discovered-users: "Recently Discovered Users" - popular-tags: "Popular Tags" - federated: "From the fediverse" - explore: "Explore {host}" - explore-fediverse: "Explore Fediverse" - users-info: "Currently, {users} users are registered here" -common/views/components/reactions-viewer.details.vue: - few-users: "{users} reacted with {reaction}" - many-users: "{users}, and {omitted} more reacted with {reaction}" -common/views/components/url-preview.vue: - enable-player: "Enable playback" - disable-player: "Close the player" -common/views/components/user-list.vue: - no-users: "There are no users." -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "Waiting for {}" - cancel: "Cancel" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Surrender" - surrendered: "By surrender" - is-llotheo: "The lesser one wins(Llotheo)" - looped-map: "Looped map" - can-put-everywhere: "Can put everywhere" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "Play reversi with your friends!" - invite: "Invite" - rule: "How to play" - rule-desc: "Reversi is a strategy board game for two players, played on an 8×8 uncheckered board. There are sixty-four identical game pieces called disks (often spelled \"discs\"), which are light on one side and dark on the other. Players take turns placing disks on the board with their assigned color facing up. During a play, any disks of the opponent's color that are in a straight line and bounded by the disk just placed and another disk of the current player's color are turned over to the current player's color. The object of the game is to have the majority of disks turned to display your color when the last playable empty square is filled." - mode-invite: "Invite" - mode-invite-desc: "Game with a specified user." - invitations: "You’ve got an invitation!" - my-games: "My game" - all-games: "All games" - enter-username: "Please enter username" - game-state: - ended: "Finished" - playing: "In Progress" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Game settings" - choose-map: "Choose a map" - random: "Random" - black-or-white: "Black/White" - black-is: "Black is {}" - rules: "Rules" - is-llotheo: "The lesser one wins(Llotheo)" - looped-map: "Looped map" - can-put-everywhere: "Can put everywhere" - settings-of-the-bot: "Bot settings" - this-game-is-started-soon: "The game will begin in seconds" - waiting-for-other: "Waiting for the opponent" - waiting-for-me: "Waiting for the your preparation" - waiting-for-both: "Preparing" - cancel: "Cancel" - ready: "Ready" - cancel-ready: "Cancel \"Ready\"" -common/views/components/connect-failed.vue: - title: "Unable to connect to the server" - description: "There is a problem with your Internet connection, or the server may be down or under maintenance. Please {try again} later." - thanks: "Thank you for using Misskey." - troubleshoot: "Troubleshoot" -common/views/components/connect-failed.troubleshooter.vue: - title: "Troubleshooting" - network: "Network connection" - checking-network: "Checking network connection" - internet: "Internet connection" - checking-internet: "Checking Internet connection" - server: "Server connection" - checking-server: "Checking server connection" - finding: "Searching for issues" - no-network: "No connection" - no-network-desc: "Please make sure that you have a network connection." - no-internet: "There is no Internet connection" - no-internet-desc: "Please make sure you are connected to the Internet." - no-server: "Unable to connect to the Misskey server" - no-server-desc: "The network connection of your device is normal, but you could not connect to the Misskey server. There is a possibility that the server is either down, or under maintenance, please try again later." - success: "Successfully connected to the Misskey server" - success-desc: "Looks like we have a connection. Please reload the page." - flush: "Clean cache" - set-version: "Specify version" -common/views/components/media-banner.vue: - sensitive: "NSFW" - click-to-show: "Click to show" -common/views/components/theme.vue: - theme: "Theme" - light-theme: "Theme to use in Light mode" - dark-theme: "Theme during dark mode" - light-themes: "Light theme" - dark-themes: "Dark theme" - install-a-theme: "Install a theme" - theme-code: "Theme code" - install: "Install" - installed: "\"{}\" has been installed" - create-a-theme: "Create a theme" - save-created-theme: "Save theme" - primary-color: "Primary color" - secondary-color: "Secondary color" - text-color: "Text color" - base-theme: "Base theme" - base-theme-light: "Light" - base-theme-dark: "Dark" - find-more-theme: "Find more themes" - theme-name: "Theme name" - preview-created-theme: "Preview" - invalid-theme: "Not valid theme" - already-installed: "This theme is already installed." - saved: "Saved" - manage-themes: "Themes manager" - builtin-themes: "Standard themes" - my-themes: "My themes" - installed-themes: "Installed themes" - select-theme: "Select your theme" - uninstall: "Uninstall" - uninstalled: "\"{}\" has been uninstalled" - author: "Author" - desc: "Description" - export: "Export" - import: "Import" - import-by-code: "or paste code" - theme-name-required: "Theme name is required" -common/views/components/cw-button.vue: - hide: "Hide" - show: "See more" - chars: "{count} chars" - files: "{count} files" - poll: "Poll" -common/views/components/messaging.vue: - search-user: "Find a user" - you: "You" - no-history: "Without history" - user: "User" - group: "Group" - start-with-user: "Start chatting with a user" - start-with-group: "Start a group and chat" - select-group: "Select a group" -common/views/components/messaging-room.vue: - not-talked-user: "You have not talked to this user yet" - not-talked-group: "There is no conversation in this group" - no-history: "There is no further history" - new-message: "New message" - only-one-file-attached: "You can only attach one file to a message" -common/views/components/messaging-room.form.vue: - input-message-here: "Enter message here" - send: "Send" - attach-from-local: "Attach files from your device" - attach-from-drive: "Attach files from your Drive" - only-one-file-attached: "You can only attach one file to a message" -common/views/components/messaging-room.message.vue: - is-read: "Read" - deleted: "This message has been deleted" -common/views/components/nav.vue: - about: "About" - stats: "Stats" - status: "Status" - wiki: "Wiki" - donors: "Donators" - repository: "Repository" - develop: "Developers" - feedback: "Feedback" - tos: "Terms Of Service" -common/views/components/note-menu.vue: - mention: "Mention" - detail: "Details" - copy-content: "Copy the contents" - copy-link: "Copy link" - favorite: "Favorite this note" - unfavorite: "Unfavorite" - watch: "Watch" - unwatch: "Unwatch" - pin: "Pin to your profile" - unpin: "Unpin" - delete: "Delete" - delete-confirm: "Are you sure you want to delete this post?" - delete-and-edit: "Delete and Edit" - delete-and-edit-confirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it." - remote: "Show original note" - pin-limit-exceeded: "You can't pin any more posts." -common/views/components/user-menu.vue: - mention: "Mention" - mute: "Mute" - unmute: "Unmute" - mute-confirm: "Are you sure you want to mute this user?" - unmute-confirm: "Are you certain that you want to unmute this user?" - block: "Block" - unblock: "Unblock" - block-confirm: "Are you sure you want to block this user?" - unblock-confirm: "Are you certain that you want to unblock this user?" - push-to-list: "Add to list" - select-list: "Select a list" - report-abuse: "Report abuse" - report-abuse-detail: "What kind of nuisance did you encounter?" - report-abuse-reported: "The issue has been reported to the administrator. Your cooperation is much appreciated." - silence: "Silence" - unsilence: "Unsilence" - silence-confirm: "Are you sure that you want to silence this user?" - unsilence-confirm: "Are you sure that you want to stop silencing this user?" - suspend: "Suspend" - unsuspend: "Unsuspend" - suspend-confirm: "Are you sure that you want to suspend this user?" - unsuspend-confirm: "Are you sure that you want to unsuspend this user?" -common/views/components/poll.vue: - vote-to: "Vote for '{}'" - vote-count: "{} votes" - total-votes: "{} votes in total" - vote: "Vote" - show-result: "Show results" - voted: "Voted" - closed: "Ended" - remaining-days: "{d} days, {h} hours remain" - remaining-hours: "{h} hours, and {m} minutes remain" - remaining-minutes: "{m} minutes, and {s} seconds remaining" - remaining-seconds: "{s} seconds remaining" -common/views/components/poll-editor.vue: - no-only-one-choice: "At least two choices are required" - choice-n: "Choice {}" - remove: "Delete the choice" - add: "+ Add a choice" - destroy: "Discard the poll" - multiple: "More than one answer is allowed" - expiration: "Valid until" - infinite: "Indefinitely" - at: "Date and time pick" - after: "Progression specifics" - no-more: "You cannot add any more" - deadline-date: "Finish date" - deadline-time: "Time duration" - interval: "Duration" - unit: "Unit" - second: "Seconds" - minute: "Minutes" - hour: "Hours" - day: "S" -common/views/components/reaction-picker.vue: - choose-reaction: "Send a reaction" - input-reaction-placeholder: "or input Emoji" -common/views/components/emoji-picker.vue: - recent-emoji: "Recently used" - custom-emoji: "Custom Emoji" - no-category: "Uncategorized" - people: "People" - animals-and-nature: "Animals & Nature" - food-and-drink: "Food & drink" - activity: "Activity" - travel-and-places: "Travel & Places" - objects: "Objects" - symbols: "Symbols" - flags: "Flags" -common/views/components/settings/app-type.vue: - title: "Mode" - intro: "You can specify whether you want to use the desktop, or the mobile layout." - choices: - auto: "Choose layout automatically" - desktop: "Always use the desktop layout" - mobile: "Always use the mobile layout" - info: "You need to reload the page for the changes to take effect." -common/views/components/signin.vue: - username: "Username" - password: "Password" - token: "Token" - signing-in: "Signing in..." - or: "Or" - signin-with-twitter: "Log in with Twitter" - signin-with-github: "Sign in with GitHub" - signin-with-discord: "Sign in with Discord" - login-failed: "Unable to log in. The username or password you entered is incorrect." - tap-key: "Click on the Security Key to log in" - enter-2fa-code: "Enter your verification code" -common/views/components/signup.vue: - invitation-code: "Invitation code" - invitation-info: "If you do not have an invitation code, please contact an <a href=\"{}\">administrator</a>." - username: "Username" - checking: "Confirming..." - available: "Available" - unavailable: "Unavailable" - error: "Network error" - invalid-format: "letters, numbers and _ are acceptable." - too-short: "Should not be blank!" - too-long: "Enter within 20 characters." - password: "Password" - password-placeholder: "More than 8 characters are recommended." - weak-password: "Weak password" - normal-password: "Fair password" - strong-password: "Strong password" - retype: "Re-enter" - retype-placeholder: "Confirm your password" - password-matched: "OK" - password-not-matched: "Doesn't match" - recaptcha: "Verification" - agree-to: "Accept {0}." - tos: "Terms Of Service" - create: "Create an Account" - some-error: "An attempt at account creation has failed for some reason. Please try again." -common/views/components/special-message.vue: - new-year: "Happy New Year!" - christmas: "Merry Christmas!" -common/views/components/stream-indicator.vue: - connecting: "Connecting" - reconnecting: "Reconnecting" - connected: "Connected" -common/views/components/notification-settings.vue: - title: "Notifications" - mark-as-read-all-notifications: "Mark all notifications as read" - mark-as-read-all-unread-notes: "Mark all posts as read" - mark-as-read-all-talk-messages: "Mark all conversations as read" - auto-watch: "Automatically watch out for posts" - auto-watch-desc: "Automatically receive notifications about posts you react to, or respond to." -common/views/components/integration-settings.vue: - title: "Service cooperation" - connect: "Connect" - disconnect: "Disconnect" - connected-to: "You are connected to this account" -common/views/components/github-setting.vue: - description: "Once you connect your GitHub account to your Misskey account, you will be able to see information about your GitHub account on your profile, and you will be able to sign-in via GitHub." - connected-to: "You are connected to this GitHub account" - detail: "More..." - reconnect: "Reconnect" - connect: "Link your GitHub account" - disconnect: "Disconnect" -common/views/components/discord-setting.vue: - description: "Once you connect your Discord account to your Misskey account, you will be able to see information from your Discord account on your profile, and you will be able to sign-in using Discord." - connected-to: "You are connected to this Discord account" - detail: "Details…" - reconnect: "Reconnect" - connect: "Link your Discord account" - disconnect: "Disconnect" -common/views/components/uploader.vue: - waiting: "Waiting" -common/views/components/visibility-chooser.vue: - public: "Public" - home: "Home" - home-desc: "Post to Home only" - followers: "Followers" - followers-desc: "Post to Followers only" - specified: "Direct" - specified-desc: "Post to specified users only" - local-public: "Local (Public)" - local-public-desc: "Do not publish to remote" - local-home: "Home (Only local)" - local-followers: "Followers (Only local)" -common/views/components/trends.vue: - count: "{} users mentioned" - empty: "No popular hashtag trends" -common/views/components/language-settings.vue: - title: "Display Language" - pick-language: "Select a language" - recommended: "Recommended" - auto: "Auto" - specify-language: "Specify language" - info: "You need to reload the page for the changes to take effect." -common/views/components/profile-editor.vue: - title: "Profile" - name: "Name" - account: "Account" - location: "Location" - description: "About me" - you-can-include-hashtags: "You can also include hashtags in your profile description." - language: "Language" - birthday: "Birthday" - avatar: "Avatar" - banner: "Banner" - is-cat: "This account is a Cat" - is-bot: "This account is a Bot" - is-locked: "Follower requests require approval" - careful-bot: "Follower requests from bots require approval" - auto-accept-followed: "Automatically approve follows from the people you follow" - advanced: "Other" - privacy: "Privacy" - save: "Save" - saved: "Profile updated successfully" - uploading: "Uploading" - upload-failed: "Failed to upload" - unable-to-process: "The operation could not be completed." - avatar-not-an-image: "The file you specified as an avatar is not an image" - banner-not-an-image: "The file you specified as a banner is not an image" - email: "Email settings" - email-address: "Email Address" - email-verified: "Your email has been verified." - email-not-verified: "Email address is not confirmed. Please check your inbox." - export: "Export" - import: "Import" - export-and-import: "Export and Import" - export-targets: - all-notes: "All posted Notes" - following-list: "List of followers" - mute-list: "List of muted accounts" - blocking-list: "List of blocked accounts" - user-lists: "Lists" - export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive." - import-requested: "You have initiated an import. This may take quite some time." - enter-password: "Please enter your password" - danger-zone: "Cautious options" - delete-account: "Remove the account" - account-deleted: "The account has been deleted. It may take some time until all of the data disappears." - profile-metadata: "Profile metadata" - metadata-label: "Label" - metadata-content: "Content" -common/views/components/user-list-editor.vue: - users: "User" - rename: "Rename list" - delete: "Delete list" - remove-user: "Remove from this list" - delete-are-you-sure: "Delete list \"$1\"?" - deleted: "Deleted successfully" - add-user: "Add a user" -common/views/components/user-group-editor.vue: - users: "Members" - rename: "Rename group" - delete: "Delete group" - transfer: "transfer group" - transfer-are-you-sure: "Are you sure you want to add @$2 to the group $1?" - transferred: "Group transferred" - remove-user: "Remove a user from this group" - delete-are-you-sure: "Are you sure to delete group \"$1\"?" - deleted: "Deleted" - invite: "Invite" - invited: "The invitation was successfully sent" -common/views/components/user-lists.vue: - user-lists: "Lists" - create-list: "Create a list" - list-name: "List name" -common/views/components/user-groups.vue: - user-groups: "Groups" - create-group: "Create a group" - group-name: "Group name" - owned-groups: "My groups" - joined-groups: "Membership in groups" - invites: "Invite" - accept-invite: "Join" - reject-invite: "Decline" -common/views/widgets/broadcast.vue: - fetching: "Checking" - no-broadcasts: "No announcements" - have-a-nice-day: "Have a nice day!" - next: "Next" - prev: "Previous" -common/views/widgets/calendar.vue: - year: "Year {}" - month: "{}," - day: "{}" - today: "Today: " - this-month: "Month:" - this-year: "Year:" -common/views/widgets/photo-stream.vue: - title: "Photo stream" - no-photos: "No photos" -common/views/widgets/posts-monitor.vue: - title: "Chart of posts" - toggle: "Toggle views" -common/views/widgets/hashtags.vue: - title: "Hashtags" -common/views/widgets/server.vue: - title: "Server info" - toggle: "Toggle views" -common/views/widgets/memo.vue: - title: "Sticky note" - memo: "Write here!" - save: "Save" -common/views/widgets/slideshow.vue: - folder-customize-mode: "To specify a folder, please exit customization mode" - folder: "Please click and specify a folder" - no-image: "There is no image in this folder" -common/views/widgets/tips.vue: - tips-line1: "You can focus on the timeline with <kbd>t</kbd>." - tips-line2: "Open posting form with <kbd>p</kbd> or <kbd>n</kbd>." - tips-line3: "You can drag and drop files on the post form." - tips-line4: "You can paste an image from the clipboard into the submission form." - tips-line5: "You can upload files by dragging and dropping them to Drive." - tips-line6: "You can move a folder by dragging it within the Drive." - tips-line7: "You can move folders by dragging them within the Drive." - tips-line8: "The Home layout can be customized from the settings." - tips-line9: "Misskey is licensed under AGPLv3." - tips-line10: "Using the Time Machine widget makes it easy to trace back to the past timeline." - tips-line11: "You can pin posts to user page by clicking on \"...\"" - tips-line13: "All the files attached to the post are saved to Drive." - tips-line14: "While customizing your home layout, you can right click on a widget to change its design." - tips-line17: "Surrounding the text with ** ** will highlight it." - tips-line19: "Several windows can be detached outside the browser." - tips-line20: "The percentage of the calendar widget shows the percentage of time elapsed." - tips-line21: "You can also use the API to develop bots." - tips-line23: "Ai-chan kawaii!" - tips-line24: "Misskey has been running since 2014." - tips-line25: "In a browser compatible with notification features, you can receive notifications in case Misskey is not open" -common/views/pages/not-found.vue: - page-not-found: "The page has not been found" -common/views/pages/follow.vue: - signed-in-as: "Signed in as {}" - following: "Following" - follow: "Follow" - request-pending: "Pending follow request" - follow-processing: "Processing follow" - follow-request: "Follow request" -common/views/pages/follow-requests.vue: - received-follow-requests: "Follow requests" - accept: "Accept" - reject: "Reject" -desktop: - banner-crop-title: "Crop the part that appears as a banner" - banner: "Banner" - uploading-banner: "Uploading a new banner" - banner-updated: "Successfully updated the banner" - choose-banner: "Choose the banner" - avatar-crop-title: "Crop the part that appears as an avatar" - avatar: "Avatar" - uploading-avatar: "Uploading a new avatar" - avatar-updated: "Successfully updated the avatar" - choose-avatar: "Select an image for the avatar" - unable-to-process: "The operation could not be completed." - invalid-filetype: "This filetype is not acceptable here" -desktop/views/components/activity.chart.vue: - total: "Black ... Total" - notes: "Blue ... Notes" - replies: "Red ... Replies" - renotes: "Green ... Renotes" -desktop/views/components/activity.vue: - title: "Activity" - toggle: "Toggle views" -desktop/views/components/calendar.vue: - title: "{year} / {month}" - prev: "Previous month" - next: "Next month" - go: "Click to navigate" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count} File(s) selected" - upload: "Upload files from your device" - cancel: "Cancel" - ok: "OK" - choose-prompt: "Choose files" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Cancel" - ok: "OK" - choose-prompt: "Choose a folder" -desktop/views/components/crop-window.vue: - skip: "Skip cropping" - cancel: "Cancel" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "used" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Banner" - nsfw: "NSFW" - contextmenu: - rename: "Rename" - mark-as-sensitive: "Mark as 'sensitive'" - unmark-as-sensitive: "Unmark as 'sensitive'" - copy-url: "Copy URL" - download: "Download" - else-files: "Other" - set-as-avatar: "Set as avatar" - set-as-banner: "Set as a banner" - open-in-app: "Open in app" - add-app: "Add app" - rename-file: "Rename file" - input-new-file-name: "Enter new name" - copied: "Copied" - copied-url-to-clipboard: "URL has been copied to clipboard" -desktop/views/components/drive.folder.vue: - upload-folder: "Default Upload location" - unable-to-process: "The operation could not be completed." - circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move." - unhandled-error: "Unknown error" - unable-to-delete: "Unable to delete" - has-child-files-or-folders: "Since this folder is not empty, it can not be deleted." - contextmenu: - move-to-this-folder: "Move to this folder" - show-in-new-window: "Open in new window" - rename: "Rename" - rename-folder: "Rename folder" - input-new-folder-name: "Enter new name" - else-folders: "Other" - set-as-upload-folder: "Set as default upload folder" -desktop/views/components/drive.vue: - search: "Search" - empty-draghover: "Drop it here! Yep, cuz you know I'm cute, right?" - empty-drive: "Your media storage is empty" - empty-drive-description: "Right-click to open the menu, or drag and drop a file onto here for uploading." - empty-folder: "This folder is empty" - unable-to-process: "The operation could not be completed." - circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move." - unhandled-error: "Unknown error" - url-upload: "Upload from a URL" - url-of-file: "URL of file you want to upload" - url-upload-requested: "Upload requested" - may-take-time: "It may take some time until the upload is complete." - create-folder: "Create a folder" - folder-name: "Folder name" - contextmenu: - create-folder: "Create a folder" - upload: "Upload a file" - url-upload: "Upload from a URL" -desktop/views/components/media-video.vue: - sensitive: "The content is NSFW" - click-to-show: "Click to show" -desktop/views/components/followers-window.vue: - followers: "{}'s followers" -desktop/views/components/followers.vue: - empty: "Seems like you don’t have any followers." -desktop/views/components/following-window.vue: - following: "Following {}" -desktop/views/components/following.vue: - empty: "It seems you don't have any following users…" -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "Done" - add-widget: "Add widget:" - add: "Add" -desktop/views/input-dialog.vue: - cancel: "Cancel" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "Post is private" - deleted: "Post has been removed" - location: "Location" - renote: "Renote" - add-reaction: "Add a reaction" - undo-reaction: "Reverse reaction" -desktop/views/components/note.vue: - reply: "Reply" - renote: "Renote" - add-reaction: "Add a reaction" - undo-reaction: "Reverse reaction" - detail: "Details" - private: "This post is private" - deleted: "This post has been deleted" -desktop/views/components/notes.vue: - error: "Loading failed." - retry: "Retry" -desktop/views/components/notifications.vue: - empty: "No notifications!" -desktop/views/components/post-form.vue: - posted: "Posted!" - replied: "Replied!" - reposted: "Renoted!" - note-failed: "Failed to post" - reply-failed: "Failed to reply" - renote-failed: "Failed to Renote" -desktop/views/components/post-form-window.vue: - note: "New Post" - reply: "Reply" - attaches: "{} media attached" - uploading-media: "Uploading {} media" -desktop/views/components/progress-dialog.vue: - waiting: "Waiting" -desktop/views/components/renote-form.vue: - quote: "Quote..." - cancel: "Cancel" - renote: "Renote" - renote-home: "Renote (Home)" - reposting: "Renoting..." - success: "Renoted!" - failure: "Failed to Renote" -desktop/views/components/renote-form-window.vue: - title: "Do you want to renote it?" -desktop/views/pages/user-following-or-followers.vue: - following: "{user}'s following" - followers: "{user}'s follower" -desktop/views/components/settings.2fa.vue: - intro: "If you set up 2-step verification, you will not only need a password at sign-in, but also a pre-registered physical device (such as your smartphone), which will improve security." - detail: "Details…" - url: "https://www.google.com/landing/2step/" - caution: "If you lose access to your registered device, you won't be able to connect to Misskey anymore!" - register: "Register a device" - already-registered: "This device is already registered" - unregister: "Unregister" - unregistered: "Two-factor authentication has been disabled." - enter-password: "Enter the password" - authenticator: "First, you need to install Google Authenticator on your device:" - howtoinstall: "How to install" - token: "Token" - scan: "And then, scan the QR code:" - done: "Please enter the token displayed on your device:" - submit: "Submit" - success: "Settings saved!" - failed: "Failed to setup. Please ensure that the token is correct." - info: "From the next time you sign in to Misskey, the token displayed on your device will be necessary too, as well as the password." - totp-header: "Authenticator App" - security-key-header: "Security Key" - security-key: "For additional security, you can log in to your account using a hardware Security Key that supports FIDO2. When you then sign in, you'll need the registered Security Key, or an authenticator app with you." - last-used: "Last used:" - activate-key: "Click to activate the Security Key" - security-key-name: "Name the Key" - register-security-key: "Complete Key registration" - something-went-wrong: "Wow! There was a problem registering the Key:" - key-unregistered: "The Key has been deleted" - use-password-less-login: "Use Password-less login" -common/views/components/media-image.vue: - sensitive: "NSFW" - click-to-show: "Click to show" -common/views/components/api-settings.vue: - intro: "To access the API, set this token as the key 'i' of request parameters." - caution: "Do not enter this token to any apps nor tell this token to others otherwise your account may get compromised." - regeneration-of-token: "If your token gets leaked, you can regenerate it." - regenerate-token: "Regenerate the token" - token: "Token:" - enter-password: "Enter the password" - console: - title: "API console" - endpoint: "Endpoint" - parameter: "Parameters" - credential-info: "Parameter \"i\" is not required at this console." - send: "Send" - sending: "Sending" - response: "Result" -desktop/views/components/settings.apps.vue: - no-apps: "No linked applications" -common/views/components/drive-settings.vue: - max: "Max" - in-use: "In use" - stats: "Statistics" - default-upload-folder: "Default upload folder location" - default-upload-folder-name: "Folder(s)" - change-default-upload-folder: "Change folder" -common/views/components/mute-and-block.vue: - mute-and-block: "Mute / Block" - mute: "Mute" - block: "Blocking" - no-muted-users: "No muted users" - no-blocked-users: "No blocked users" - word-mute: "Word mute" - muted-words: "Muted keywords" - muted-words-description: "Separating with spaces results in AND specifications, and delimiting with line breaks results in OR specifications" - unmute-confirm: "Are you certain that you want to unmute this user?" - unblock-confirm: "Are you certain that you want to unblock this user?" - save: "Save" -common/views/components/password-settings.vue: - reset: "Change password" - enter-current-password: "Enter the current password" - enter-new-password: "Enter the new password" - enter-new-password-again: "Enter the new password again" - not-match: "The new passwords do not match" - changed: "Password changed" - failed: "Failed to change password" -common/views/components/post-form-attaches.vue: - attach-cancel: "Remove Attachment" - mark-as-sensitive: "Mark as 'sensitive'" - unmark-as-sensitive: "Unmark as 'sensitive'" -desktop/views/components/sub-note-content.vue: - private: "This post is private" - deleted: "This post has been deleted" - media-count: "{} media attached" - poll: "Poll" -desktop/views/components/settings.tags.vue: - title: "Tags" - query: "Query (optional)" - add: "Add" - save: "Save" -desktop/views/components/timeline.vue: - home: "Home" - local: "Local" - hybrid: "Social" - global: "Global" - mentions: "Mentions" - messages: "Direct posts" - list: "Lists" - hashtag: "Hashtag" - add-tag-timeline: "Add hashtag cloud" - add-list: "Add list" - list-name: "List name" -desktop/views/components/ui.header.vue: - welcome-back: "Welcome back," - adjective: "-san" -desktop/views/components/ui.header.account.vue: - profile: "Your profile" - lists: "Lists" - groups: "Groups" - follow-requests: "Follow requests" - admin: "Admin" - room: "Room" -desktop/views/components/ui.header.nav.vue: - game: "Games" -desktop/views/components/ui.header.notifications.vue: - title: "Notifications" -desktop/views/components/ui.header.post.vue: - post: "Compose new Post" -desktop/views/components/ui.header.search.vue: - placeholder: "Search" -desktop/views/components/user-preview.vue: - notes: "Posts" - following: "Following" - followers: "Followers" -desktop/views/components/users-list.vue: - all: "All" - iknow: "You know" - fetching: "Loading…" -desktop/views/components/users-list-item.vue: - followed: "Follows you" -desktop/views/components/window.vue: - popout: "Pop-out" - close: "Close" -admin/views/index.vue: - dashboard: "Dashboard" - instance: "Instance" - emoji: "Emoji" - moderators: "Moderators" - users: "Users" - federation: "Federation" - announcements: "Announcements" - abuse: "Abuse" - queue: "Job Queue" - logs: "Logs" - db: "Database" - back-to-misskey: "Back to Misskey" -admin/views/db.vue: - tables: "Tables" - vacuum: "Vacuum" - vacuum-info: "Tidies up the database. Keeps the data intact and reduces disk usage. This is usually done automatically and periodically." - vacuum-exclamation: "Vacuuming can overload the database for a while, and cause users not to be able to participate in interactions." -admin/views/dashboard.vue: - dashboard: "Dashboard" - accounts: "Accounts" - notes: "Notes" - drive: "Drive" - instances: "Instances" - this-instance: "This instance" - federated: "Federated" -admin/views/queue.vue: - title: "Queue" - remove-all-jobs: "Clear all queued jobs" - jobs: "Jobs" - queue: "Queue" - domains: - deliver: "Delivers" - inbox: "Received" - db: "Database" - objectStorage: "Object Storage" - state: "Sort" - states: - active: "Running" - delayed: "Scheduled" - waiting: "Queued" - result-is-truncated: "Result is truncated" - other-queues: "Other queues" -admin/views/logs.vue: - logs: "Logs" - domain: "Domain" - level: "Level" - levels: - all: "All" - info: "Information" - success: "Success" - warning: "Warning" - error: "Error" - debug: "Debug" - delete-all: "Remove All" -admin/views/abuse.vue: - title: "Abuse" - target: "Target" - reporter: "Reporter" - details: "Details" - remove-report: "Remove" -admin/views/instance.vue: - instance: "Instance" - instance-name: "Instance name" - instance-description: "Instance description" - host: "Host" - icon-url: "URL of the icon" - logo-url: "URL of the logo" - banner-url: "Banner image URL" - error-image-url: "Error image URL" - languages: "Language of this instance" - languages-desc: "You can add more than one, separated by spaces." - tos-url: "Terms of Service URL" - repository-url: "Repository URL" - feedback-url: "URL for feedback" - maintainer-config: "Administrator information" - maintainer-name: "Administrator name" - maintainer-email: "Contact Administrator" - advanced-config: "Other settings" - note-and-tl: "Notes and timelines" - drive-config: "Drive settings" - use-object-storage: "Use Object Storage" - object-storage-base-url: "URL" - object-storage-bucket: "Bucket Name" - object-storage-prefix: "Prefix" - object-storage-endpoint: "Endpoint" - object-storage-region: "Region" - object-storage-port: "Port" - object-storage-access-key: "Access Key" - object-storage-secret-key: "Secret Key" - object-storage-use-ssl: "Use SSL" - object-storage-s3-info: "If you are going to use Amazon S3 as Object Storage, Please refer {0} to configure 'Endpoint' and 'Region'." - object-storage-s3-info-here: "here" - object-storage-gcs-info: "If you are going to use Google Cloud Storage as Object Storage, Set the 'Endpoint' as storage.googleapis.com, and keep the 'Region' is blank." - cache-remote-files: "Cache remote files" - cache-remote-files-desc: "If disabled, All remote files going to be linked to their origin server directly. This will be an effective solution to save your server storage. However, Since no thumbnail will be generated, It will make increasing data usage, and also may remote files are invisible to users who set direct-link disabled. It is recommended that this config set enabled or enabling the next config, 'Proxy remote files'." - proxy-remote-files: "Proxy remote files" - proxy-remote-files-desc: "If enabled, Remote files that not stored locally or deleted by storage overusage will be proxied locally and also thumbnails will be generated." - local-drive-capacity-mb: "Volume of Drive per user" - remote-drive-capacity-mb: "Volume of Drive per remote user" - mb: "In megabytes" - recaptcha-config: "the reCAPTCHA settings" - recaptcha-info: "reCAPTCHA token is required. Please get it on https://www.google.com/recaptcha/intro/" - recaptcha-info2: "v3 is not supported. Please use v2." - enable-recaptcha: "enable reCAPTCHA" - recaptcha-site-key: "Site key" - recaptcha-secret-key: "Secret Key" - recaptcha-preview: "Preview" - hidden-tags: "Hidden hashtags" - hidden-tags-info: "List up the hashtags delimited by line breaks that you want exclude from statistics." - external-service-integration-config: "Connect an external service" - twitter-integration-config: "Settings of connecting to Twitter" - twitter-integration-info: "The callback URL is set on {url}." - enable-twitter-integration: "Enable connection to Twitter" - twitter-integration-consumer-key: "Consumer key" - twitter-integration-consumer-secret: "Consumer Secret" - github-integration-config: "Setting of connecting to GitHub" - github-integration-info: "The callback URL is set on {url}." - enable-github-integration: "Enable connection to GitHub" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-config: "Discord Integration settings" - discord-integration-info: "The callback URL is set to {url}." - enable-discord-integration: "Enable Discord connection" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - proxy-account-config: "Proxy account" - proxy-account-info: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the activity will not be delivered to the server if no one is following that user, so the proxy account will follow instead." - proxy-account-username: "Proxy account user name" - proxy-account-username-desc: "Specify the user name of the account that is used as a proxy." - proxy-account-warn: "You must make an account having this username before this action." - max-note-text-length: "Maximum numbers of post characters" - disable-registration: "Disable new user registration" - disable-local-timeline: "Disable the Local Timeline" - disable-global-timeline: "Disable global timeline" - disabling-timelines-info: "Even if you disable these timelines, the administrator as well as moderators can use them continually." - enable-emoji-reaction: "Enable pictograms for reactions" - use-star-for-reaction-fallback: "Use the star as fallback for unknown reaction" - invite: "Invite" - save: "Save" - saved: "Saved" - pinned-users: "Pinned user" - pinned-users-info: "List up the users delimited by line breaks that you want to show as 'Pinned Users'." - email-config: "Email server settings" - email-config-info: "Used to confirm email and password reset etc." - enable-email: "Enable email delivery" - email: "Email Address" - smtp-secure: "Use implicit SSL/TLS in the SMTP connection" - smtp-secure-info: "Turn off STARTTLS when used that." - smtp-host: "SMTP Host" - smtp-port: "SMTP Port" - smtp-auth: "Perform SMTP authentication" - smtp-user: "SMTP User" - smtp-pass: "SMTP Password" - test-email: "Test" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "Enable ServiceWorker" - serviceworker-info: "Must be enabled for push notifications." - vapid-publickey: "VAPID public key" - vapid-privatekey: "VAPID private key" - vapid-info: "If you want to enable ServiceWorker, you need to generate VAPID keys. Unless you have set your global node_modules location elsewhere, you need to run this as root:" -admin/views/charts.vue: - title: "Chart" - per-day: "per Day" - per-hour: "per Hour" - federation: "Federation" - notes: "Posts" - users: "Users" - drive: "Media storage" - network: "Network" - charts: - federation-instances: "The number of instances: increase/decrease" - federation-instances-total: "Total number of instances" - notes: "Increase, or decrease in the number of posts (Combined)" - local-notes: "Increase, or decrease in the number of posts (Local)" - remote-notes: "Increase, or decrease in the number of posts (Remote)" - notes-total: "Total posts" - users: "The number of users: increase/decrease" - users-total: "Total users" - active-users: "Active users" - drive: "Increase and decrease in storage capacity use" - drive-total: "Total usage of Drive" - drive-files: "The number of files on the storage: increase/decrease" - drive-files-total: "Total number of files on Drive" - network-requests: "Requests" - network-time: "Response time" - network-usage: "Traffic" -admin/views/drive.vue: - operation: "Operations" - fileid-or-url: "File ID or URL" - file-not-found: "File not found" - lookup: "Look up" - sort: - title: "Sort" - createdAtAsc: "Age - Oldest First" - createdAtDesc: "Age - Newest First" - sizeAsc: "Size - Smallest First" - sizeDesc: "Size - Largest First" - origin: - title: "Origin" - combined: "Local + Remote" - local: "Local" - remote: "Remote" - delete: "Delete" - deleted: "Deleted successfully" - mark-as-sensitive: "Mark as 'sensitive'" - unmark-as-sensitive: "Unmark as 'sensitive'" - marked-as-sensitive: "Set a sensitive content notice" - unmarked-as-sensitive: "Remove the sensitive content notice" - clean-remote-files: "Clear the remote files cache" - clean-remote-files-are-you-sure: "Are you sure you want to remove all cached files from remote?" - clean-up: "Clean up" -admin/views/users.vue: - operation: "Operations" - username-or-userid: "Username or user ID" - user-not-found: "User not found" - lookup: "Look up" - reset-password: "Reset password" - reset-password-confirm: "Do you want to reset your password?" - password-updated: "The password is now \"{password}\"" - suspend: "Suspend" - suspend-confirm: "Do you want to suspend this account?" - suspended: "Successfully suspended." - unsuspend: "Unsuspend" - unsuspend-confirm: "Are you sure you want to unsuspend this account?" - unsuspended: "The user has successfully unsuspended." - make-silence: "Silence" - silence-confirm: "Silence user?" - unmake-silence: "Unsilence" - unsilence-confirm: "Are you certain that you want to stop silencing this user?" - update-remote-user: "Update information about remote user" - remote-user-updated: "The information regarding the remote user has been updated." - delete-all-files: "Delete all files" - delete-all-files-confirm: "Are you sure that you want to delete all files?" - username: "Username" - host: "Host" - users: - title: "Users" - sort: - title: "Sort" - createdAtAsc: "Date Registered (Ascending)" - createdAtDesc: "Date Registered (Descending)" - updatedAtAsc: "Last Updated (Ascending)" - updatedAtDesc: "Last Updated (Descending)" - state: - title: "Sort" - all: "All" - available: "Available" - admin: "Administrator" - moderator: "Moderator" - adminOrModerator: "Admin/Moderator" - silenced: "Already silenced" - suspended: "Suspended" - origin: - title: "Origin" - combined: "Local + Remote" - local: "Local" - remote: "Remote" - createdAt: "Created at" - updatedAt: "Updated at" -admin/views/moderators.vue: - add-moderator: - title: "Register Moderator" - add: "Register" - added: "Registered a Moderator." - remove: "Discharge" - removed: "The moderator has been discharged" - logs: - title: "Logs" - moderator: "Moderators" - type: "Operations" - at: "Timestamp" - info: "Information" -admin/views/emoji.vue: - add-emoji: - title: "Add emoji" - name: "Emoji name" - name-desc: "You can use the characters a~z 0~9 _" - category: "Categories" - aliases: "Aliases" - aliases-desc: "You can add more than one, separated by spaces." - url: "Image URL" - add: "Add" - info: "We recommend PNG images under 50KB." - added: "Emoji was added" - emojis: - title: "Emojis" - update: "Update" - remove: "Remove" - updated: "Updated" - remove-emoji: - are-you-sure: "Delete \"$1\"?" - removed: "Deleted" -admin/views/announcements.vue: - announcements: "Announcements" - save: "Save" - remove: "Remove" - add: "Add" - title: "Title" - text: "Content" - saved: "Saved" - _remove: - are-you-sure: "Delete \"$1\"?" - removed: "Deleted" -admin/views/hashtags.vue: - hided-tags: "Hidden Tags" -admin/views/federation.vue: - instance: "Instance" - host: "Host" - notes: "Notes" - users: "Users" - following: "Following" - followers: "Followers" - caught-at: "Created at" - status: "Statuses" - latest-request-sent-at: "Time of last request sent" - latest-request-received-at: "Last request received at" - remove-all-following: "Withold all followers" - remove-all-following-info: "Unfollow all accounts from {host}. Please run this if the instance no longer exists." - delete-all-files: "Remove all files" - block: "Block" - marked-as-closed: "Marked as closed" - lookup: "Look up" - instances: "Federated" - instance-not-registered: "The instance has not been discovered" - sort: "Sort by" - sorts: - caughtAtAsc: "Date of discovery (Ascending)" - caughtAtDesc: "Date of discovery (Descending)" - lastCommunicatedAtAsc: "The date and time of the older interactions" - lastCommunicatedAtDesc: "The date and time of the newer interactions" - notesAsc: "Least Notes posted" - notesDesc: "Most Notes posted" - usersAsc: "Less followers" - usersDesc: "More followers" - followingAsc: "Least followed" - followingDesc: "Most followed" - followersAsc: "Having less followers" - followersDesc: "The largest number of followers" - driveUsageAsc: "Least storage used" - driveUsageDesc: "Most storage used" - driveFilesAsc: "Least files stored on Drive" - driveFilesDesc: "The largest number of files stored on Drive" - state: "Sort" - states: - all: "All" - blocked: "Blocked" - not-responding: "Without response" - marked-as-closed: "Marked as closed" - result-is-truncated: "Displaying the top {n} items." - charts: "Charts" - chart-srcs: - requests: "Requests" - users: "Increase, or decrease in the number of users" - users-total: "Users in total" - notes: "Increase, or decrease in the number of notes" - notes-total: "Total number of notes" - ff: "Increase of followers" - ff-total: "Total number of follows accumulated" - drive-usage: "Increase and decrease in storage use" - drive-usage-total: "Total usage of the Drive" - drive-files: "Increase, or decrease in the number of files stored on Drive" - drive-files-total: "The number of files accumulated on Drive" - chart-spans: - hour: "Hourly" - day: "Daily" - blocked-hosts: "Blocking" - blocked-hosts-info: "List up the hosts delimited by line breaks that you want to block." - save: "Save" -desktop/views/pages/welcome.vue: - about: "More details..." - timeline: "Timeline" - announcements: "Announcements" - photos: "Recent Images" - powered-by-misskey: "Powered by <b>Misskey</b>." - info: "Information" -desktop/views/pages/drive.vue: - title: "Misskey storage" -desktop/views/pages/note.vue: - prev: "Previous post" - next: "Next post" -desktop/views/pages/selectdrive.vue: - title: "Choose file(s)" - ok: "OK" - cancel: "Cancel" - upload: "Upload files from your device" -desktop/views/pages/search.vue: - not-available: "Search feature is turned off in the settings for this instance." - not-found: "No posts were found for '{q}'" -desktop/views/pages/tag.vue: - no-posts-found: "No posts contains \"{q}\" found." -desktop/views/pages/user-list.users.vue: - users: "User" - add-user: "Add a user" - username: "Username" -desktop/views/pages/user/user.followers-you-know.vue: - title: "Followers you may know" - loading: "Loading" - no-users: "No followers you know" -desktop/views/pages/user/user.friends.vue: - title: "Frequent mentions" - loading: "Loading" - no-users: "No frequent mentions" -desktop/views/pages/user/user.photos.vue: - title: "Photos" - loading: "Loading" - no-photos: "No photos" -desktop/views/pages/user/user.header.vue: - posts: "Notes" - following: "Following" - followers: "Followers" - is-bot: "This account is a Bot" - no-description: "This user has not written their profile introduction" - years-old: "{age} years old" - year: "/" - month: "/" - day: "-" - follows-you: "Follows you" -desktop/views/pages/user/user.timeline.vue: - default: "Posts" - with-replies: "Posts and replies" - with-media: "Media" - my-posts: "My posts" -desktop/views/widgets/notifications.vue: - title: "Notifications" -desktop/views/widgets/polls.vue: - title: "Polls" - refresh: "refresh" - nothing: "No polls found!" -desktop/views/widgets/post-form.vue: - title: "Post" - note: "Post" - something-happened: "Could not be posted in this circumstance." -desktop/views/widgets/profile.vue: - update-banner: "Click to edit your banner" - update-avatar: "Click to edit your avatar" -desktop/views/widgets/trends.vue: - title: "Trend" - refresh: "refresh" - nothing: "No trends found!" -desktop/views/widgets/users.vue: - title: "Recommended users" - refresh: "refresh" - no-one: "Anyone!" -mobile/views/components/drive.vue: - used: "used" - folder-count: "Folder(s)" - count-separator: ", " - file-count: "File(s)" - nothing-in-drive: "There's nothing stored." - folder-is-empty: "This folder is empty" - folder-name: "Folder name" - here-is-root: "Currently, you are on the root, not inside of any folder." - url-prompt: "URL of the file you want to upload" - uploading: "Upload requested. It may take a while for the upload to finish." - folder-name-cannot-empty: "Folder name cannot be blank." -mobile/views/components/drive-file-chooser.vue: - select-file: "Choose files" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "Choose a folder" -mobile/views/components/drive.file.vue: - nsfw: "NSFW" -mobile/views/components/drive.file-detail.vue: - download: "Download" - rename: "Rename" - move: "Move" - hash: "Hash (md5)" - exif: "EXIF" - nsfw: "NSFW" - mark-as-sensitive: "Mark as 'sensitive'" - unmark-as-sensitive: "Unmark as 'sensitive'" -mobile/views/components/media-video.vue: - sensitive: "The content is NSFW" - click-to-show: "Click to show" -common/views/components/follow-button.vue: - following: "Following" - follow: "Follow" - request-pending: "Pending" - follow-processing: "Processing" - follow-request: "Follow request" -mobile/views/components/note.vue: - private: "This post is private" - deleted: "This post has been deleted" - location: "Location" -mobile/views/components/note-detail.vue: - reply: "Reply" - reaction: "Reaction" - private: "This post is private" - deleted: "This post has been deleted" - location: "Location" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/notifications.vue: - empty: "No notifications" -mobile/views/components/sub-note-content.vue: - private: "This post is private" - deleted: "This post has been deleted" - media-count: "{} media attached" - poll: "Poll" -mobile/views/components/ui.header.vue: - welcome-back: "Welcome back, " - adjective: "Sir" -mobile/views/components/ui.nav.vue: - timeline: "Timeline" - notifications: "Notifications" - follow-requests: "Follow requests" - search: "Search" - user-lists: "Lists" - user-groups: "Groups" - widgets: "Widgets" - game: "Games" - admin: "Admin" - about: "About Misskey" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Upload a file" - url-upload: "Upload file from a URL" - create-folder: "Create a folder" - rename-folder: "Rename folder" - move-folder: "Move this folder" - delete-folder: "Delete this folder" -mobile/views/pages/signup.vue: - lets-start: "Your account is now ready! 📦" -mobile/views/pages/followers.vue: - followers-of: "{name}'s followers" -mobile/views/pages/following.vue: - following-of: "{name}'s following" -mobile/views/pages/home.vue: - home: "Home" - local: "Local" - hybrid: "Social" - global: "Global" - mentions: "Mentions" - messages: "Direct posts" -mobile/views/pages/tag.vue: - no-posts-found: "No posts contains \"{q}\" found." -mobile/views/pages/widgets.vue: - dashboard: "Dashboard" - widgets-hints: "You can add/delete/rearrange widgets. To move the widget, drag \"三\". Tap \"x\" to delete the widget. Some widgets can change display by tapping." - add-widget: "Add" - customization-tips: "Customization tips" -mobile/views/pages/widgets/activity.vue: - activity: "Activity" -mobile/views/pages/share.vue: - share-with: "Share on {name}" -mobile/views/pages/note.vue: - title: "Post" - prev: "Previous note" - next: "Next note" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "Search" - not-found: "No posts were found for '{q}'" -mobile/views/pages/selectdrive.vue: - select-file: "Choose files" -mobile/views/pages/notifications.vue: - notifications: "Notifications" -mobile/views/pages/settings.vue: - signed-in-as: "Signed in as {}" -mobile/views/pages/user.vue: - follows-you: "Follows you" - following: "Following" - followers: "Followers" - notes: "Posts" - overview: "Overview" - timeline: "Timeline" - media: "Media" - years-old: "{age} years old" -mobile/views/pages/user/home.vue: - recent-notes: "Recent notes" - images: "Images" - activity: "Activity" - keywords: "Keywords" - domains: "Domains" - frequently-replied-users: "Frequent mentions" - followers-you-know: "Followers you know" - last-used-at: "Last active:" -mobile/views/pages/user/home.photos.vue: - no-photos: "No photos" -deck: - widgets: "Widgets" - home: "Home" - local: "Local" - hybrid: "Social" - hashtag: "Hashtag" - global: "Global" - mentions: "Mentions" - direct: "Direct posts" - notifications: "Notifications" - list: "List" - select-list: "Select a list" - swap-left: "Move left" - swap-right: "Move right" - swap-up: "Move up" - swap-down: "Move down" - remove: "Remove" - add-column: "Add a column" - rename: "Rename" - stack-left: "Stack to the left" - pop-right: "Dock on the right" - disabled-timeline: - title: "The timeline has been disabled" - description: "This timeline has been disabled by the server's administrator." -deck/deck.tl-column.vue: - is-media-only: "Only media posts" - edit: "Options" -deck/deck.user-column.vue: - follows-you: "Follows you" - posts: "Posts" - following: "Following" - followers: "Followers" - images: "Images" - activity: "Activity" - timeline: "Timeline" - pinned-notes: "Pinned posts" - pinned-page: "Pinned page" -docs: - edit-this-page-on-github: "Found an error, or do you want to contribute to the documentation?" - edit-this-page-on-github-link: "Edit this page at GitHub!" -dev/views/index.vue: - manage-apps: "Manage apps" -dev/views/apps.vue: - manage-apps: "Manage apps" - create-app: "Create app" - app-missing: "No apps" -dev/views/new-app.vue: - new-app: "New Application" - new-app-info: "You can also create an application with the API. (app/create)" - create-app: "Creating application" - app-name: "Application name" - app-name-placeholder: "ex) Misskey for iOS" - app-name-desc: "The name of your app" - app-overview: "Application summary" - app-overview-placeholder: " ex) Misskey iOS Client." - app-overview-desc: "A brief description, or an introduction of your app." - callback-url: "The callback URL (optional)" - callback-url-placeholder: "ex) https://your.app.example.com/callback.php" - callback-url-desc: "The URL to redirect to after the user is authenticated via the authentication form." - authority: "Permissions" - authority-desc: "Only the functions requested here can be accessed via the API." - authority-warning: "You can change it even after creating the application, but if you give different permissions, all user keys associated at that time will be invalidated." -pages: - new-page: "Create a page" - edit-page: "Edit a page" - read-page: "Viewing the source" - page-created: "Created the page!" - page-updated: "Updated the page" - name-already-exists: "The specified page name already exists" - title-invalid-name: "The specified page URL is invalid" - text-invalid-name: "Check whether that is not a blank" - are-you-sure-delete: "Do you want to delete this page?" - page-deleted: "The page has been deleted" - edit-this-page: "Edit this page" - pin-this-page: "Pin to your profile" - unpin-this-page: "Unpin" - view-source: "View Source" - view-page: "View page" - like: "Like" - unlike: "Unlike" - liked-pages: "Favorite pages" - my-pages: "My pages" - inspector: "Inspector" - content: "Page block" - variables: "Variables" - variables-info: "You can make your page more dynamic by using variables. If you write down <b>{ variable name }</b> in the text, you can embed the value of the variable there. For example, if the source text is <b>Hello { thing } world!</b> and the value of variable 'thing' is <b> ai </b>, that text becomes to <b>Hello ai world!</b>." - variables-info2: "Because the evaluation(=calculating) of variables are performed from top to bottom, the variable cannot refer another variable which exists on later line. For example, when defining three variables <b>A</b>, <b>B</b> and <b>C</b>, variable <b>C</b> <i>can</i> refer the variable <b>A</b> and <b>B</b> in its expression, but variable <b>A</b> <i>cannot</i> refer the variable <b>B</b> or <b>C</b> in its expression." - variables-info3: "If you want to get some input from the user, place a 'User Input' block on the page and set the variable name as which you want to store that input in 'variable name' (variables are created automatically). You can use that variable to perform actions in response to user's input." - variables-info4: "Function allows make your processing logic as group in a reusable way. To create a function, create a variable of type 'Function'. A function can have a slot (Argument) whose value is available as a variable in the function. There are also functions that take functions as arguments in the AiScript standard (called the higher-order function.). In addition to the predefined functions, you can also set them in the slots of such higher-order functions on the fly." - more-details: "Description" - title: "Title" - url: "Page URL" - summary: "Summary of page" - align-center: "Center align" - hide-title-when-pinned: "Hide page title when pinned to profile" - font: "Font" - fontSerif: "Serif" - fontSansSerif: "Sans Serif" - set-eye-catching-image: "Set an eye-catching image" - remove-eye-catching-image: "Delete an eye-catching image" - choose-block: "Add a block" - select-type: "Select a type" - enter-variable-name: "Please choose a variable name" - the-variable-name-is-already-used: "This variable name is already used" - content-blocks: "Content" - input-blocks: "Input" - special-blocks: "Special" - post-from-post-form: "Post this content" - posted-from-post-form: "Posted!" - blocks: - text: "Text" - textarea: "Text area" - section: "Section" - image: "Images" - button: "Button" - if: "If" - _if: - variable: "Variables" - post: "Post form" - _post: - text: "Content" - textInput: "Text input" - _textInput: - name: "Variable name" - text: "Title" - default: "Default value" - textareaInput: "Multiple type text input" - _textareaInput: - name: "Variable name" - text: "Title" - default: "Default value" - numberInput: "Numeric input" - _numberInput: - name: "Variable name" - text: "Title" - default: "Default value" - switch: "Switch" - _switch: - name: "Variable name" - text: "Title" - default: "Default value" - counter: "Counter" - _counter: - name: "Variable name" - text: "Title" - inc: "Increase number" - _button: - text: "Title" - colored: "Color" - action: "Operation when the button pressed" - _action: - dialog: "Show a dialog" - _dialog: - content: "Content" - resetRandom: "Reset a random number" - pushEvent: "Send an event" - _pushEvent: - event: "Name of the event" - message: "Message to display when pressed" - variable: "Variable to send" - no-variable: "None" - radioButton: "Choices" - _radioButton: - name: "Variable name" - title: "Title" - values: "Item of choices that delimited by line breaks" - default: "Default value" - script: - categories: - flow: "Control" - logical: "Logical operation" - operation: "Compute" - comparison: "Compare" - random: "Random" - value: "Value" - fn: "Function" - text: "Text operation" - convert: "Variable" - list: "Lists" - blocks: - text: "Text" - multiLineText: "Text (Multiple lines)" - textList: "List of text" - _textList: - info: "Separate each one with a newline" - strLen: "Length of text" - _strLen: - arg1: "Text" - strPick: "Extract character" - _strPick: - arg1: "Text" - arg2: "Position of character" - strReplace: "Replace string(s)" - _strReplace: - arg1: "Text" - arg2: "Before replacement" - arg3: "After replacement" - strReverse: "Flip text" - _strReverse: - arg1: "Text" - join: "Text Concatenation" - _join: - arg1: "Lists" - arg2: "Separator" - add: "+ Plus" - _add: - arg1: "A" - arg2: "B" - subtract: "- Minus" - _subtract: - arg1: "A" - arg2: "B" - multiply: "× Multiply" - _multiply: - arg1: "A" - arg2: "B" - divide: "÷ Divide" - _divide: - arg1: "A" - arg2: "B" - mod: "÷ Remaindering" - _mod: - arg1: "A" - arg2: "B" - round: "Round decimal" - _round: - arg1: "Number" - eq: "A and B are equal" - _eq: - arg1: "A" - arg2: "B" - notEq: "A and B are different" - _notEq: - arg1: "A" - arg2: "B" - and: "A and B" - _and: - arg1: "A" - arg2: "B" - or: "A or B" - _or: - arg1: "A" - arg2: "B" - lt: "A is smaller than B" - _lt: - arg1: "A" - arg2: "B" - gt: "A is bigger than B" - _gt: - arg1: "A" - arg2: "B" - ltEq: "A is smaller or same than B" - _ltEq: - arg1: "A" - arg2: "B" - gtEq: "A is bigger or same than B" - _gtEq: - arg1: "A" - arg2: "B" - if: "Branch" - _if: - arg1: "If" - arg2: "then" - arg3: "else" - not: "denial" - _not: - arg1: "denial" - random: "Random" - _random: - arg1: "Probability" - rannum: "Random number" - _rannum: - arg1: "Minimum" - arg2: "Maximum" - randomPick: "Choose at random from the list" - _randomPick: - arg1: "Lists" - dailyRandom: "Random (Daily for each user)" - _dailyRandom: - arg1: "Probability" - dailyRannum: "Random number (Daily for each user)" - _dailyRannum: - arg1: "Minimum" - arg2: "Maximum" - dailyRandomPick: "Choose at random from the list (Daily for each user)" - _dailyRandomPick: - arg1: "Lists" - seedRandom: "Random (Seed)" - _seedRandom: - arg1: "Seed" - arg2: "Probability" - seedRannum: "Random number (Seed)" - _seedRannum: - arg1: "Seed" - arg2: "Minimum" - arg3: "Maximum" - seedRandomPick: "Randomly selected from list (Seed)" - _seedRandomPick: - arg1: "Seed" - arg2: "Lists" - DRPWPM: "Randomly selected from weighted list (Daily updated per user)" - _DRPWPM: - arg1: "List of text" - pick: "Select from list" - _pick: - arg1: "Lists" - arg2: "Position" - listLen: "Get length of list" - _listLen: - arg1: "Lists" - number: "Number" - stringToNumber: "Text to number" - _stringToNumber: - arg1: "Text" - numberToString: "Number to text" - _numberToString: - arg1: "Number" - splitStrByLine: "Split the text by lines" - _splitStrByLine: - arg1: "Text" - ref: "Variables" - fn: "Function" - _fn: - slots: "Slots" - slots-info: "Please delimit each slot with a line break" - arg1: "Output" - for: "Repeat" - _for: - arg1: "Count" - arg2: "Action" - typeError: "Slot {slot} accepts \"{expect}\" type, but It actually contains \"{actual}\" type!" - thereIsEmptySlot: "Slot {slot} is empty!" - types: - string: "Text" - number: "Number" - boolean: "Flag" - array: "Lists" - stringArray: "List of text" - emptySlot: "Empty slot" - enviromentVariables: "Environment variable" - pageVariables: "Page element" - argVariables: "Input slot" -room: - add-furniture: "Place furniture" - translate: "Move" - rotate: "Rotate" - exit: "Deselect" - remove: "Remove" - save: "Save" - saved: "Saved" - clear: "Remove All" - clear-confirm: "Are you sure to remove all furnitures in your room?" - leave-confirm: "There are unsaved changes. Do you really want to leave?" - chooseImage: "Select an image" - room-type: "Room type" - carpet-color: "Color of carpet" - rooms: - default: "Default" - washitsu: "Japanese-style" - furnitures: - milk: "Milk carton" - bed: "Bed" - low-table: "Low Table" - desk: "Desk" - chair: "Chair" - chair2: "Chair 2" - fan: "Fan" - pc: "Computer" - plant: "Houseplant" - plant2: "Houseplant 2" - eraser: "Eraser" - pencil: "Pencil" - pudding: "Pudding" - cardboard-box: "Cardboard Box" - cardboard-box2: "Cardboard Box 2" - cardboard-box3: "Cardboard Box 3" - book: "Book" - book2: "Book 2" - piano: "Piano" - facial-tissue: "Facial tissue" - server: "Servers" - moon: "Moon" - corkboard: "Cork board" - mousepad: "Mousepad" - monitor: "Monitor" - keyboard: "Keyboard" - carpet-stripe: "Carpet (stripe)" - mat: "Mat" - color-box: "Bookshelf" - wall-clock: "Wall clock" - photoframe: "Picture frame" - cube: "Cube" - tv: "TV" - pinguin: "Penguin" - rubik-cube: "Rubik's Cube" - poster-h: "Poster (Horizontal)" - poster-v: "Poster (Vertical)" - sofa: "Sofa" - spiral: "Spiral Staircase" - bin: "Waste bin" - cup-noodle: "Cup noodle" - holo-display: "Holographic display" - energy-drink: "Energy drink" diff --git a/locales/es-ES.yml b/locales/es-ES.yml deleted file mode 100644 index 67a546b86ccbba0d9194d415a25f7ccffffad2ea..0000000000000000000000000000000000000000 --- a/locales/es-ES.yml +++ /dev/null @@ -1,1194 +0,0 @@ ---- -meta: - lang: "Español" -common: - misskey: "Una âï¸ del fediverso" - about-title: "Una âï¸ del fediverso" - about: "Gracias por encontrar Misskey. Misskey es una <b>plataforma descentralizada de microblogging</b> nacida en la Tierra. Porque el servicio existe dentro del Fediverso (un universo donde se organizan varias plataformas sociales), se encuentra enlazado mutuamente con otras plataformas sociales. ¿Por qué no te tomas un respiro del caos de la ciudad y te sumerges es una nueva manera de entender Internet?" - intro: - title: "¿Misskey?" - about: "Misskey es un <b>Servicio de red social descentralizada de microblogging</b> de código abierto. Contiene una interfaz de usuario altamente personalizable, reacciones a posts, almacenamiento para poder manejar archivos y otras funciones avanzadas. Además de conectarse con la red llamada Fediverso, puede intercambiar mensajes con otras redes sociales. Por ejemplo, si contribuyes con algo, esa contribución es transmitida no sólo a Misskey sino a otras redes sociales. Imagina que se parece a transmitir una onda de radio de un planeta a otro." - features: "CaracterÃsticas" - rich-contents: "Posts" - rich-contents-desc: "Escribe sobre tus pensamientos, eventos, todo lo que quieras compartir. Si es necesario, puedes usar varias sintaxis, decorar tus posts y añadir tus imágenes favoritas, archivos de viddeo y encuestas." - reaction: "Reacciones" - reaction-desc: "La forma mas facil de expresar tus emociones. Misskey te permite añadir varios tipos de reacciones a los posts de otros usuarios. La emperiencia emocional en Misskey nunca será igual que en otra red social, donde solo puedes poner \"likes\"." - ui: "Interfaz" - ui-desc: "No hay ninguna interfaz que le vaya bien a todos. Por eso, Misskey tiene una interfaz altamente personalizable para tus gustos. Puedes hacer tu página principal única editando la interfaz de tu timeline y moviendo varios widgets para conseguir hacer de este lugar uno propio." - drive: "Drive" - drive-desc: "¿Quieres postear de nuevo la imagen que has posteado antes? Si es asÃ, ¿Quieres separar y ordenar en carpetas los archivos que has subido? La caracterÃstica Drive incorporada en la base de Misskey es la solución. Compartir archivos es simple." - outro: "Aún hay caracterÃsticas que solamente están en Misskey, asegúrate de eso con tus propios ojos. Misskey es un servicio de red social distribuida, si no te gusta esta instancia, puedes probar otra instancia. Asà que, ¡buena suerte!" - application-authorization: "Autorizaciones de la aplicación." - close: "Cerrar" - do-not-copy-paste: "Por favor no copies código aquÃ. Tu cuenta puede resultar comprometida." - load-more: "Leer más" - enter-password: "Escribe una contraseña" - 2fa: "Autenticación de dos factores" - customize-home: "Personaliza la página principal" - featured-notes: "Destacados" - dark-mode: "Modo oscuro" - signin: "Iniciar sesión" - signup: "¡RegÃstrate!" - signout: "Cerrar sesión" - reload-to-apply-the-setting: "Para aplicar esta configuración, hay que recargar la página. ¿Quiere recargar ahora?" - fetching-as-ap-object: "Consultar en el fediverso" - unfollow-confirm: "¿Quiere dejar de seguir a {name}?" - delete-confirm: "¿Seguro que quieres borrar la publicación?" - signin-required: "Inicie sesion" - notification-type: "Tipo de notificación" - notification-types: - all: "Todo" - pollVote: "Encuestas" - follow: "Seguimientos" - receiveFollowRequest: "Solicitudes de seguimiento" - reply: "Responder" - quote: "Citas" - renote: "Volver a publicar" - mention: "Menciones" - reaction: "Reacciones" - got-it: "¡Listo!" - customization-tips: - title: "Consejos de personalización" - paragraph: "<p>Se puede personalizar el inicio agregando/quitando widgets, arrastrarlos, soltarlos y ordenarlos.</p><p>Haciendo <strong>Click <strong>derecho</strong></strong>, se puede modificar la muestra de un widget</p><p>Para quitar un widget, arrastre y suelte el widget en el area que dice <strong>\"Papelera\"</strong> en el cabezal</p><p>Para acabar de personalizar, haga click en \"Listo\" arriba a la derecha</p>" - gotit: "¡Comprendido!" - notification: - file-uploaded: "Archivo cargado." - message-from: "Mensaje de {}:" - reversi-invited: "Invitado a un juego" - reversi-invited-by: "Invitado por {}:" - notified-by: "Notificado por {}:" - reply-from: "Respuesta de {}:" - quoted-by: "Citado por {}:" - time: - unknown: "Desconocido" - future: "Futuro" - just_now: "Ahora mismo" - seconds_ago: "Hace {}" - minutes_ago: "Hace {} minuto(s)" - hours_ago: "Hace {} hora(s)" - days_ago: "Hace {} dia(s)" - weeks_ago: "Hace {} semana(s)" - months_ago: "Hace {} mes(es)" - years_ago: "Hace {} año(s)" - month-and-day: "{day} de {month}" - trash: "Papelera" - drive: "Drive" - pages: "Páginas" - messaging: "Conversación" - home: "Inicio" - deck: "Deck" - timeline: "Timeline" - explore: "Explorar" - following: "Siguiendo" - followers: "Seguidores" - favorites: "Me gusta esta nota" - permissions: - "read:account": "Ver información de la cuenta" - "write:account": "Editar información de la cuenta" - "read:blocks": "Ver bloques" - "write:blocks": "Editar bloques" - "read:drive": "Explorar el drive" - "write:drive": "Administrar el drive" - "read:favorites": "Ver favoritos" - "write:favorites": "Editar favoritos" - "read:following": "Ver información de seguidor" - "write:following": "Seguir/Dejar de seguir" - "read:messaging": "Ver conversación" - "write:messaging": "Administrar coversación" - "read:mutes": "Ver silenciados" - "write:mutes": "Administrar silenciados" - "write:notes": "Crear y eliminar articulos" - "read:notifications": "Ver notificaciones" - "write:notifications": "Administrar notificaciones" - "read:reactions": "Ver reacciones" - "write:reactions": "Administrar reacciones" - "write:votes": "Vota" - "read:pages": "Ver páginas" - "write:pages": "Administrar páginas" - "read:page-likes": "Ver páginas que te gustan" - "write:page-likes": "Administrar páginas que te gustan" - "read:user-groups": "Ver grupos de usuarios" - "write:user-groups": "Administrar grupos de usuarios" - empty-timeline-info: - follow-users-to-make-your-timeline: "Seguir al usuario mostrará sus posts en la linea de tiempo" - explore: "Explorar usuarios" - post-form: - reply: "Responder" - renote: "Volver a publicar" - attach-media-from-local: "Agregar medios de tu dispositivo" - insert-a-kao: "v('ω')v" - recent-tags: "Reciente" - error: "Error" - enter-username: "Ingresar nombre de usuario" - add-visible-user: "Agregar usuario" - username-prompt: "Ingresar nombre de usuario" - enter-file-name: "Editar nombre del archivo" - weekday-short: - sunday: "domingo" - monday: "lunes" - tuesday: "martes" - wednesday: "miércoles" - thursday: "jueves" - friday: "viernes" - saturday: "sábado" - weekday: - sunday: "Domingo" - monday: "Lunes" - tuesday: "Martes" - wednesday: "Miércoles" - thursday: "Jueves" - friday: "Viernes" - saturday: "Sábado" - reactions: - like: "Me gusta" - love: "amor" - laugh: "risa" - hmm: "hmm" - surprise: "sorpresa" - congrats: "felicidades" - angry: "enfadado" - confused: "confundido" - rip: "RIP" - pudding: "Chafado" - note-visibility: - public: "Público" - home: "Inicio" - home-desc: "Sólo en el timeline de inicio" - followers: "Seguidores" - followers-desc: "Sólo para tus seguidores" - specified: "Mensaje directo" - specified-desc: "Sólo para ciertos usuarios" - local-public: "Público (sólo local)" - local-home: "Inicio (sólo local)" - local-followers: "Seguidores (sólo local)" - note-placeholders: - a: "¿Qué haces?" - b: "¿Qué está pasando?" - c: "¿Qué te pasa por la cabeza?" - d: "¿Quieres decir algo?" - e: "¡Escribe aquÃ!" - f: "Esperando a que escribas algo..." - settings: "Configuración" - _settings: - profile: "Tu perfil" - notification: "Notificaciones" - apps: "Aplicaciones" - tags: "Etiquetas" - mute-and-block: "Silenciar/Bloquear" - blocking: "Bloquear" - security: "Seguridad" - signin: "Historial de ingresos" - password: "Contraseña" - other: "Otros" - appearance: "Diseño" - behavior: "Comportamiento" - reactions: "Reacciones" - fetch-on-scroll-desc: "Cuando te deslizas al final de la página nuevo contenido se carga automáticamente." - note-visibility: "Visibilidad de la publicación" - default-note-visibility: "Rango de publicación predeterminado" - web-search-engine: "Buscador web" - web-search-engine-desc: "Ejemplo: https://www.google.com/?#q={{query}}" - keep-cw: "Mantener CW" - this-setting-is-this-device-only: "Solo para este dispositivo" - use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo" - line-width: "Grosor de lÃnea" - line-width-thin: "Fino" - line-width-normal: "Normal" - line-width-thick: "Grosor" - font-size: "Tamaño del texto" - font-size-x-small: "Muy pequeño" - font-size-small: "Pequeño" - font-size-medium: "Normal" - font-size-large: "Grande" - font-size-x-large: "Muy grande" - deck-column-align: "Alineamiento de las columnas" - deck-column-align-center: "Centrar" - deck-column-align-left: "Izquierda" - deck-column-align-flexible: "Flexible" - deck-column-width: "Ancho de las columnas" - deck-column-width-narrow: "Estrecho" - deck-column-width-narrower: "Un poco estrecho" - deck-column-width-normal: "Normal" - deck-column-width-wider: "Un poco ancho" - deck-column-width-wide: "Ancho" - use-shadow: "Usar sombras en la Interfaz de Usuario" - rounded-corners: "Esquinas redondeadas en la Interfaz de Usuario" - circle-icons: "Usar avatar circulares" - contrasted-acct: "Añadir contraste al nombre de usuario" - wallpaper: "Fondo de pantalla" - choose-wallpaper: "Escoge un fondo de pantalla" - delete-wallpaper: "Quitar fondo de pantalla" - post-form-on-timeline: "Mostrar el formulario de las entradas encima de la lÃnea de tiempo" - show-clock-on-header: "Muestra el reloj en la parte superior derecha" - show-reply-target: "Mostrar destinatario de la mención" - timeline: "Timeline" - show-my-renotes: "Mostrar mis renotes en la timeline" - show-renoted-my-notes: "Mostrar renotes de mis posts en la timeline" - sound: "Sonido" - enable-sounds: "Habilitar sonido" - volume: "Volúmen" - test: "Prueba" - update: "Actualizar Misskey" - version: "Versión" - latest-version: "Última versión" - update-checking: "Buscando actualizaciones" - no-updates: "No hay actualizaciones disponibles" - no-updates-desc: "Tu Misskey está actualizado" - update-available: "¡Una nueva versión está disponible!" - update-available-desc: "Las actualizaciones se aplicarán cuando la página se vuelva a cargar." - advanced-settings: "Configuraciones avanzadas" - navbar-position-left: "Izquierda" - save: "Guardar" - saved: "Guardado" - preview: "Vista previa" - search: "Buscar" - delete: "eliminar" - loading: "cargando" - ok: "Confirmar" - cancel: "Cancelar" - update-available-title: "Actualización disponible" - update-available: "Hay disponible una nueva versión de Misskey ({newer}, la versión actual es {current}). Refresca la página para aplicar las actualizaciones." - my-token-regenerated: "Tu token se ha regenerado vas a ser desconectado." - hide-password: "Ocultar contraseña" - show-password: "Mostrar contraseña" - enter-username: "Ingresar nombre de usuario" - do-not-use-in-production: "Esto está en desarrollo, no usarlo para producción." - user-suspended: "Este usuario ha sido suspendido" - is-remote-user: "La información sobre este usuario puede no estar completa" - is-remote-post: "Es una publicación remota" - view-on-remote: "Consultar el perfil completo" - renoted-by: "Renotado por {user}" - no-notes: "No hay publicaciones" - turn-on-darkmode: "Cambiar a modo oscuro" - turn-off-darkmode: "Modo claro" - error: - title: "Se ha producido un problema :(" - retry: "Inténtalo otra vez" - reversi: - drawn: "Empatado" - my-turn: "Mi turno" - opponent-turn: "Turno del oponente" - turn-of: "Turno de {name}" - past-turn-of: "Turno de {name}" - won: "{name} ha ganado" - black: "Negro" - white: "Blanco" - total: "Total" - this-turn: "Turno {count}" - widgets: - analog-clock: "Reloj analógico" - profile: "Perfil" - calendar: "Calendario" - timemachine: "Calendario (máquina del tiempo)" - activity: "Actividad" - rss: "Lector RSS" - memo: "Notas adhesivas" - trends: "Tendencias" - photo-stream: "Secuencia de fotos" - posts-monitor: "Gráfico de publicaciones" - slideshow: "Diapositivas" - version: "Versión" - broadcast: "Transmisión" - notifications: "Notificaciones" - users: "Usuarios destacados" - polls: "Encuestas" - post-form: "Formulario" - server: "Información del servidor" - nav: "Navegación" - tips: "Consejos" - hashtags: "Etiquetas" - queue: "En cola" - dev: "Se ha producido un error creando la aplicación. Intentelo de nuevo." - ai-chan-kawaii: "Ai-chan es muy mona!" - you: "Tú" -auth/views/form.vue: - share-access: "¿Deseas permitir a <i>{name}</i> acceder a tu cuenta?" - permission-ask: "La aplicación requiere los siguientes permisos:" - cancel: "Cancelar" - accept: "Garantizar acceso." -auth/views/index.vue: - loading: "Cargando" - denied: "Acceso de aplicación denegado." - denied-paragraph: "Esta aplicación no tendrá acceso a tu cuenta." - already-authorized: "Esta aplicación ha sido previamente autorizada." - allowed: "Accesos de aplicaciones autorizados." - callback-url: "Volviendo a la aplicación." - please-go-back: "Por favor, vuelve a la aplicación." - error: "Esta sesión no existe." - sign-in: "Por favor inicia sesión." -common/views/pages/explore.vue: - popular-users: "Usuarios populares" - recently-updated-users: "Usuarios activos recientemente" - recently-registered-users: "Usuarios que se han unido recientemente" - popular-tags: "Etiquetas populares" - federated: "Desde el fediverso" - explore: "Explorar {host}" - users-info: "Actualmente hay {users} registrados aquÃ" -common/views/components/url-preview.vue: - enable-player: "Activar reproducción" - disable-player: "Cerrar el reproductor" -common/views/components/user-list.vue: - no-users: "No hay usuarios." -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "Esperando por {}" - cancel: "Cancelar" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Rendirse" - surrendered: "Por rendirse" - is-llotheo: "El último gana (Llotheo)" - looped-map: "Mapa en bucle" - can-put-everywhere: "Puedes colocar donde quieras" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "¡Juega Reversi con tus amigos!" - invite: "Invitar" - rule: "Cómo jugar" - rule-desc: "Reversi es un juego de estrategia para dos jugadores, el cual se juega en un tablero de 8x8. Hay 64 fichas llamadas discos, las cuales son claras de un lado y oscuras del otro. Los jugadores toman turnos colocando fichas en el tablero con su color asignado mirando hacia arriba. Durante una jugada, cualquier disco del color del oponente que esté en fila entre un disco del oponente y otro del mismo color, será volteado para tener el color del jugador que haya hecho la movida. El objetivo del juego es tener la mayorÃa de los discos de tu color cuando el último cuadro es llenado." - mode-invite: "Invitar" - mode-invite-desc: "Invitar un usuario al juego." - invitations: "¡Has recibido una invitación!" - my-games: "Mis juegos" - all-games: "Todos los juegos" - enter-username: "Ingresar nombre de usuario" - game-state: - ended: "Finalizado" - playing: "En progreso" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Configuración de juego" - choose-map: "Elije un mapa" - random: "Aleatorio" - black-or-white: "Negro/Blanco" - black-is: "Negro es {}" - rules: "Reglas" - is-llotheo: "El que tenga menos gana" - looped-map: "Mapa en bucle" - can-put-everywhere: "Puedes colocar donde quieras" - settings-of-the-bot: "Configuración de bot" - this-game-is-started-soon: "El juego comenzará pronto" - waiting-for-other: "Esperando a que se prepare el adversario" - waiting-for-me: "Esperando por la preparación" - waiting-for-both: "Esperando por ti" - cancel: "Cancelar" - ready: "Listo" - cancel-ready: "Cancelar \"Listo\"" -common/views/components/connect-failed.vue: - title: "Imposible conectar al servidor" - description: "Hay un problema en tu conexió o puede que el servidor esté caido o en mantenimiento. Por favor {try again} más tarde." - thanks: "Gracias por usar Misskey." - troubleshoot: "Problemas más frecuentes" -common/views/components/connect-failed.troubleshooter.vue: - title: "Resolución de problemas" - network: "Conexión de red" - checking-network: "Verificar la conexión a la red" - internet: "Conexión a Internet" - checking-internet: "Comprobando la conexión a Internet" - server: "Conexión al servidor" - checking-server: "Probando la conexión al servidor" - finding: "Buscando cualquier problema" - no-network: "Sin conexión" - no-network-desc: "Por favor, asegurate que estás conectado a una red" - no-internet: "Sin conexión a Internet" - no-internet-desc: "Por favor, asegurate de estar conectado a Internet." - no-server: "Imposible conectarse al servidor de Misskey" - no-server-desc: "La conexión de red de tu PC es correcta, aún asà no puedes conectarte al servidor de Misskey. Es posible que el servidor esté caido o en mantenimiento. Por favor vuelve a intentarlo más tarde." - success: "Conectado al servidor de Misskey de manera correcta" - success-desc: "Parece que la conexión ha sido posible. Por favor refresca la página." - flush: "Limpiar la memoria caché" - set-version: "Escoge la versión" -common/views/components/media-banner.vue: - sensitive: "Este contenido no es apropiado para ver en el trabajo" - click-to-show: "Click para mostrar" -common/views/components/theme.vue: - theme: "Tema" - light-theme: "Tema a usar en Light mode" - dark-theme: "Tema a usar en dark mode" - light-themes: "Tema claro" - dark-themes: "Tema oscuro" - install-a-theme: "Instalar tema" - theme-code: "Código del tema" - install: "Instalación" - installed: "\"{}\" se ha instalado" - create-a-theme: "Crear tema" - save-created-theme: "Guardar tema" - primary-color: "Color primario" - secondary-color: "Color secundario" - text-color: "Color del texto" - base-theme: "Tema base" - base-theme-light: "Claro" - base-theme-dark: "Oscuro" - find-more-theme: "Obtener más temas" - theme-name: "Nombre del tema" - preview-created-theme: "Vista previa" - invalid-theme: "No es un tema válido" - already-installed: "Este tema ya está instalado." - saved: "Guardado" - manage-themes: "Gestor de temas" - builtin-themes: "Temas estandar" - my-themes: "Mis temas" - installed-themes: "Temas instalados" - select-theme: "Elegir tema" - uninstall: "Desinstalar" - uninstalled: "\"{}\" ha sido desinstalado" - author: "Autor" - desc: "Descripción" - export: "Exportar" - import: "Importar" - import-by-code: "o pega el código" -common/views/components/cw-button.vue: - show: "Mostrar" - chars: "{count} letras" - files: "{count} archivos" - poll: "Encuesta" -common/views/components/messaging.vue: - search-user: "Encuentra un usuario" - you: "Tu" - no-history: "Sin historial" -common/views/components/messaging-room.vue: - no-history: "El historial se ha acabado" - new-message: "Nuevo mensaje" -common/views/components/messaging-room.form.vue: - input-message-here: "Escribe el mensaje aquÃ" - send: "Enviar" - attach-from-local: "Adjunta ficheros desde tu PC" - attach-from-drive: "Adjunta ficheros desde tu disco" -common/views/components/messaging-room.message.vue: - is-read: "Leer" - deleted: "El mensaje se ha borrado" -common/views/components/nav.vue: - about: "Sobre" - stats: "EstadÃsticas" - status: "Estado" - wiki: "Wiki" - donors: "Donantes" - repository: "Repositorio" - develop: "Desarrolladores" - feedback: "Opiniones" -common/views/components/note-menu.vue: - mention: "Menciones" - detail: "Detalles" - copy-link: "Copiar enlace" - favorite: "Me gusta esta nota" - pin: "Fijar en el perfil" - delete: "Borrar" - delete-confirm: "¿Seguro que quieres borrar la publicación?" - remote: "Ver el original" -common/views/components/user-menu.vue: - mention: "Menciones" - mute: "Silenciar" - block: "Bloquear" -common/views/components/poll.vue: - vote-to: "'{}' para votar" - vote-count: "{} votos" - vote: "Vota" - show-result: "Mostrar resultados" - voted: "Votado" -common/views/components/poll-editor.vue: - no-only-one-choice: "Selecciona dos o más opciones." - choice-n: "{} opcion(es)" - remove: "Borra la opción" - add: "+ Añade una opción" - destroy: "Cancelar la encuesta" - day: "domingo" -common/views/components/reaction-picker.vue: - choose-reaction: "Escoge una reacción" -common/views/components/emoji-picker.vue: - custom-emoji: "Personalizados" - people: "Gente" - animals-and-nature: "Naturaleza" - food-and-drink: "Comida y bebida" - activity: "Actividad" - travel-and-places: "Viajes y lugares" - objects: "Objetos" - symbols: "SÃmbolos" - flags: "PaÃses" -common/views/components/settings/app-type.vue: - info: "Necesitas recargar la página para que los cambios tengan efecto." -common/views/components/signin.vue: - username: "Usuario" - password: "Contraseña" - token: "Identificador" - signing-in: "Entrando..." - or: "O" - signin-with-twitter: "Ingresar con Twitter" - signin-with-github: "Ingresar con Github" - signin-with-discord: "Ingresar con Discord" - login-failed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos." -common/views/components/signup.vue: - invitation-code: "Código de invitación" - invitation-info: "Si no tienes un código de invitación, por favor contacta un <a href=\"{}\">administrador</a>." - username: "Usuario" - checking: "Comprobando..." - available: "Disponible" - unavailable: "Utilizado" - error: "Error de conexión" - invalid-format: "utiliza letras, números y/o -." - too-short: "¡MÃnimo tienes que introducir un caracter!" - too-long: "No puedes usar más de 20 caracteres." - password: "Contraseña" - password-placeholder: "Te recomendamos más de 8 caracteres" - weak-password: "Contraseña débil" - normal-password: "No está mal" - strong-password: "Muy buena contraseña" - retype: "Inténtalo otra vez" - retype-placeholder: "Confirma la contraseña" - password-matched: "OK" - password-not-matched: "Las contraseñas no son las mismas" - recaptcha: "Verificar" - create: "Crea una cuenta" - some-error: "Por algún motivo no se ha podido crear la cuenta. Por favor inténtalo de nuevo." -common/views/components/special-message.vue: - new-year: "¡Feliz Año Nuevo!" - christmas: "¡Feliz Navidad!" -common/views/components/stream-indicator.vue: - connecting: "Conectando" - reconnecting: "Reconectando" - connected: "Conectado" -common/views/components/notification-settings.vue: - title: "Notificaciones" -common/views/components/integration-settings.vue: - title: "Integraciones" - connect: "Conectar" - disconnect: "Desconectarse" - connected-to: "Estas conectado a la siguiente cuenta" -common/views/components/github-setting.vue: - description: "Una vez conectada tu cuenta de GitHub a Misskey podrás ver la información sobre tu perfil de GitHub y además podrás registrarte mediante tu cuenta de GitHub." - connected-to: "Estas conectado a esta cuenta de GitHub" - detail: "Ver detalles..." - reconnect: "Reconectar" - connect: "Vincular tu cuenta de GitHub" - disconnect: "Desconectarse" -common/views/components/discord-setting.vue: - description: "Una vez conectada tu cuenta de Discord a Misskey podrás ver la información sobre tu perfil de Discord y además podrás registrarte mediante tu cuenta de Discord." - connected-to: "Estas conectado a esta cuenta de Discord" - detail: "Ver detalles..." - reconnect: "Reconectar" - connect: "Vincular tu cuenta de Discord" - disconnect: "Desconectarse" -common/views/components/uploader.vue: - waiting: "Un momento" -common/views/components/visibility-chooser.vue: - public: "Público" - home: "Inicio" - home-desc: "Publica solo en la página de inicio" - followers: "Seguidores" - followers-desc: "Piblica solo para tus seguidores" - specified: "Directo" - specified-desc: "Publica solo para los seguidores que quieras" - local-public: "Público (sólo local)" - local-public-desc: "No publicar para remoto" - local-home: "Inicio (sólo local)" - local-followers: "Seguidores (sólo local)" -common/views/components/trends.vue: - count: "{} usuarios mencionados" - empty: "Ninguna tendencia popular ahora" -common/views/components/language-settings.vue: - title: "Mostrar idioma" - pick-language: "Selecciona un idioma" - recommended: "Recomendado" - auto: "Automático" - specify-language: "Especifica el idioma" - info: "Necesitas recargar la página para que los cambios tengan efecto." -common/views/components/profile-editor.vue: - title: "Perfil" - name: "Nombre" - account: "Cuenta" - location: "Localización" - description: "Acerca de mÃ" - you-can-include-hashtags: "También puedes incluir hashtags en la descripción de tu perfil." - language: "Idioma" - birthday: "Fecha de nacimiento" - avatar: "Avatar" - banner: "Banner" - is-cat: "Esta cuenta es un gato" - is-bot: "Esta cuenta es un bot" - is-locked: "Las peticiones de seguimiento necesitan aprobación" - careful-bot: "Las peticiones de seguimiento de bots necesitan aprobación" - auto-accept-followed: "Aprobar automaticamente las peticiones de follow de gente a la que sigues" - advanced: "Otros" - privacy: "Privacidad" - save: "Guardar" - saved: "Perfil actualizado con exito" - uploading: "Subiendo" - upload-failed: "Error al subir" - unable-to-process: "La operación no se puede llevar a cabo" - email: "Preferencias de correo" - email-address: "Correo electrónico" - email-verified: "Tu cuenta de correo ha sido verificada." - email-not-verified: "Tu cuenta de correo no está verificada. Por favor comprueba tu bandeja de entrada." - export: "Exportar" - import: "Importar" - export-and-import: "Exportar/Importar" - export-targets: - all-notes: "Todas las notas publicadas" - following-list: "Seguidores" - mute-list: "Silenciar" - blocking-list: "Bloquear" - user-lists: "Listas" - export-requested: "Has solicitado una exportación. Esto puede tardar un rato. Después de que termine la exportación el archivo se añadirá al drive." - import-requested: "Has empezado una importación. Esto puede tardar un rato." - enter-password: "Escribe una contraseña" - danger-zone: "Zona de peligro" - delete-account: "Eliminar cuenta" - account-deleted: "Esta cuenta ha sido eliminada. Puede tardar un rato hasta que toda la información desaparazca." -common/views/components/user-list-editor.vue: - users: "Usuarios" - rename: "Cambiar el nombre de la lista" - delete: "Eliminar lista" - remove-user: "Eliminar de la lista" -common/views/components/user-group-editor.vue: - invite: "Invitar" -common/views/components/user-lists.vue: - user-lists: "Listas" - list-name: "Nombre de lista" -common/views/components/user-groups.vue: - invites: "Invitar" -common/views/widgets/broadcast.vue: - fetching: "Recuperando" - no-broadcasts: "Sin emisión" - have-a-nice-day: "¡Buenos dias!" - next: "Siguiente" -common/views/widgets/calendar.vue: - year: "Año {}" - month: "Mes {}" - day: "DÃa {}" - today: "Hoy:" - this-month: "Este mes:" - this-year: "Este año:" -common/views/widgets/photo-stream.vue: - title: "GalerÃa de fotos" - no-photos: "No hay fotos." -common/views/widgets/posts-monitor.vue: - title: "Tabla de publicaciones" - toggle: "Alternar vistas" -common/views/widgets/hashtags.vue: - title: "Etiquetas" -common/views/widgets/server.vue: - title: "Información del servidor" - toggle: "Alternar vistas" -common/views/widgets/memo.vue: - title: "Notas" - memo: "¡Escribe aquÃ!" - save: "Guardar" -common/views/widgets/slideshow.vue: - folder-customize-mode: "Para especificar una carpeta, por favor sal de modo de personalización" - folder: "Por favor, cliquea y especifica una carpeta" - no-image: "No hay imágenes en esta carpeta" -common/views/widgets/tips.vue: - tips-line1: "Puedes enfocarte en las publicaciones con <kbd>t</kbd>" - tips-line2: "Abrir formulario de publicación con <kbd>p</kbd> or <kbd>n</kbd>" - tips-line3: "Puedes arrastrar y soltar archivos en el formulario de publicación" - tips-line4: "Puedes pegar una imagen del portapapeles en el formulario de publicación" - tips-line5: "Puedes cargar archivos con sólo arrastrarlos y soltarlos en Drive" - tips-line6: "Puedes mover una carpeta arrastrándola hacia el Drive" - tips-line7: "Puedes mover una carpeta arrastrándola hacia el Drive" - tips-line8: "Inicio se puede personalizar desde la configuración" - tips-line9: "Misskey está hecho bajo licencia AGPLv3" - tips-line10: "Usando el accesorio de Máquina del Tiempo puedes encontrar publicaciones antiguas" - tips-line11: "Puedes resaltar publicaciones en la página de usuario haciendo click en \"...\"" - tips-line13: "Todos los archivos añadidos a la publicación se han guardado en tu unidad." - tips-line14: "Cuando personalizas el inicio puedas dar click derecho a un accesorio y cambiar el diseño." - tips-line17: "Al colocar ** delante y luego del texto, lo estarás destacando en negrillas" - tips-line19: "Algunas ventanas pueden ser separadas fuera del navegador" - tips-line20: "El porcentaje mostrando en el accesorio de calendario indica el porcentaje de tiempo transcurrido." - tips-line21: "También puedes usar la API para desarrollar tus propios bots." - tips-line24: "Misskey inició en 2014." - tips-line25: "Puedes recibir notificaciones incluso si Misskey no está abierto en un navegador compatible." -common/views/pages/follow.vue: - signed-in-as: "Autenticado como {}" - following: "Siguiendo" - follow: "Seguir" - request-pending: "Solicitud pendiente" - follow-processing: "Solicitud en proceso" - follow-request: "Solicitar suscripción" -common/views/pages/follow-requests.vue: - received-follow-requests: "Solicitudes de seguimiento" -desktop: - banner-crop-title: "Corta la parte que aparece como un banner" - banner: "Banner" - uploading-banner: "Cargando un nuevo banner" - banner-updated: "Banner actualizado" - choose-banner: "Escoge un banner" - avatar-crop-title: "Corta la parte que aparece como un avatar" - avatar: "Avatar" - uploading-avatar: "Cargando un nuevo avatar" - avatar-updated: "Avatar actualizado" - choose-avatar: "Escoge una imagen de avatar" - unable-to-process: "La operación no se puede llevar a cabo" - invalid-filetype: "Este tipo de archivo no es compatible aquÃ" -desktop/views/components/activity.chart.vue: - total: "Negro ... Total" - notes: "Azul ... Notas" - replies: "Rojo ... Respuestas" - renotes: "Verde ... Republicaciones" -desktop/views/components/activity.vue: - title: "Actividad" - toggle: "Alternar vistas" -desktop/views/components/calendar.vue: - title: "{year} / {month}" - prev: "Mes anterior" - next: "Próximo mes" - go: "Click para navegar" -desktop/views/components/choose-file-from-drive-window.vue: - upload: "Cargar archivos de tu dispositivo" - cancel: "Cancelar" - ok: "OK" - choose-prompt: "Escoger archivos" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Cancelar" - ok: "OK" - choose-prompt: "Escoge una Carpeta" -desktop/views/components/crop-window.vue: - skip: "Ignorar el cortado" - cancel: "Cancelar" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "usado" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Banner" - nsfw: "Ver más" - contextmenu: - rename: "Renombrar" - mark-as-sensitive: "Marcar como 'sensible'" - unmark-as-sensitive: "Desmarcar como 'sensible'" - copy-url: "Copia la URL" - download: "Descargar" - else-files: "Otros" - set-as-avatar: "Utilizar como avatar" - set-as-banner: "Utilizar como banner" - open-in-app: "Abrir en la aplicación" - add-app: "Añadir aplicación" - rename-file: "Renombra el fichero" - input-new-file-name: "Escribe el nombre nuevo" - copied: "Copiado" - copied-url-to-clipboard: "URL copiada al porta papeles" -desktop/views/components/drive.folder.vue: - unable-to-process: "La operación no se puede llevar a cabo" - circular-reference-detected: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover." - unhandled-error: "Error desconocido" - contextmenu: - move-to-this-folder: "Mover a esta carpeta" - show-in-new-window: "Abrir en una ventana nueva" - rename: "Renombrar" - rename-folder: "Renombrar carpeta" - input-new-folder-name: "Escribe el nombre nuevo" - else-folders: "Otros" -desktop/views/components/drive.vue: - search: "Buscar" - empty-draghover: "¡Saluda!" - empty-drive: "Tu disco está vacio" - empty-drive-description: "También puedes subir archivos seleccionándolos y con el botón derecho selecciona \"Subir fichero\" o puedes arrastrarlo hasta la ventana." - empty-folder: "La carpeta está vacia" - unable-to-process: "La operación no se puede llevar a cabo." - circular-reference-detected: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover." - unhandled-error: "Errer desconocido" - url-upload: "Subir desde una URL" - url-of-file: "URL del fichero que quieres subir" - url-upload-requested: "Subida solicitada" - may-take-time: "Subir el fichero puede tardar un tiempo." - create-folder: "Crear una carpeta" - folder-name: "Nombre de la carpeta" - contextmenu: - create-folder: "Crear una carpeta" - upload: "Subir fichero" - url-upload: "Subir desde una URL" -desktop/views/components/media-video.vue: - sensitive: "Este contenido no es apropiado para ver en el trabajo" - click-to-show: "Click para mostrar" -desktop/views/components/followers-window.vue: - followers: "{} seguidores" -desktop/views/components/followers.vue: - empty: "Parece que no tienes seguidores aún." -desktop/views/components/following-window.vue: - following: "Siguiendo {}" -desktop/views/components/following.vue: - empty: "Parece que aún no sigues a nadie." -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "Listo" - add-widget: "Agregar accesorio:" - add: "Agregar" -desktop/views/input-dialog.vue: - cancel: "Cancelar" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "Esta publicación es privada" - deleted: "Esta publicación ha sido removida" - location: "Localización" - renote: "Republicar" - add-reaction: "Agregar una reacción" -desktop/views/components/note.vue: - reply: "Responder" - renote: "Volver a publicar" - add-reaction: "Reacción" - detail: "Detalles" - private: "Esta publicación es privada" - deleted: "Esta publicación ha sido removida" -desktop/views/components/notes.vue: - error: "Error al cargar." - retry: "Reintentar" -desktop/views/components/notifications.vue: - empty: "No hay notificaciones" -desktop/views/components/post-form.vue: - posted: "¡Publicado!" - replied: "¡Respondido!" - reposted: "¡Republicado!" - note-failed: "Error al publicar nota" - reply-failed: "Error al responder" - renote-failed: "Error al republicar" -desktop/views/components/post-form-window.vue: - note: "Nota nueva" - reply: "Responder" - attaches: "{} archivo(s) multimedia adjuntados" - uploading-media: "Subiendo {} archivo(s) multimedia" -desktop/views/components/progress-dialog.vue: - waiting: "Un momento" -desktop/views/components/renote-form.vue: - quote: "Cita..." - cancel: "Cancelar" - renote: "Volver a publicar" - reposting: "Publicando de nuevo..." - success: "¡Publicado!" - failure: "La publicación ha fallado" -desktop/views/components/renote-form-window.vue: - title: "¿Seguro qué quieres volver a publicarlo?" -desktop/views/pages/user-following-or-followers.vue: - following: "{user} sigue a" - followers: "Seguidores de {user}" -desktop/views/components/settings.2fa.vue: - detail: "Ver detalles..." - url: "https://www.google.com/landing/2step/" - caution: "Si pierdes acceso al dispositivo, no podrás conectarte a Misskey." - register: "Registrar un dispositivo" - already-registered: "Un dispositivo ya fue registrado" - unregister: "Inhabilitado" - unregistered: "Autenticación de dos pasos fue deshabilitada." - enter-password: "Escribe una contraseña" - authenticator: "Primero, necesitas instalar Google Authenticator en tu dispositivo:" - howtoinstall: "Cómo instalar" - token: "Token" - scan: "Luego, escanea el código QR:" - done: "Por favor ingresa el token mostrado en tu dispositivo:" - submit: "Enviar" - success: "¡Configuraciones guardadas!" - failed: "Error al configurar. Por favor asegúrate de que el token es correcto." - info: "Desde ahora, ingresa el token que se muestra en tu dispositivo adicionalmente a tu contraseña cuando inicies sesión en Misskey" -common/views/components/media-image.vue: - sensitive: "Este contenido no es apropiado para ver en el trabajo" - click-to-show: "Click para mostrar" -common/views/components/api-settings.vue: - token: "Token:" - enter-password: "Escribe una contraseña" - console: - title: "Consola API" - send: "Enviar" -desktop/views/components/settings.apps.vue: - no-apps: "No hay aplicaciones asociadas" -common/views/components/drive-settings.vue: - in-use: "usado" - stats: "EstadÃsticas" -common/views/components/mute-and-block.vue: - mute-and-block: "Silenciar y bloquear" - mute: "Silenciar" - block: "Bloquear" - save: "Guardar" -common/views/components/post-form-attaches.vue: - mark-as-sensitive: "Marcar como 'sensible'" - unmark-as-sensitive: "Desmarcar como 'sensible'" -desktop/views/components/sub-note-content.vue: - private: "Esta publicación es privada" - deleted: "Esta publicación ha sido removida" - poll: "Encuesta" -desktop/views/components/settings.tags.vue: - title: "Etiqueta" - add: "Agregar" - save: "Guardar" -desktop/views/components/timeline.vue: - home: "Inicio" - local: "Local" - hybrid: "Social" - global: "Global" - list: "Listas" - hashtag: "Hashtags" - list-name: "Nombre de lista" -desktop/views/components/ui.header.vue: - welcome-back: "Bienvenido/a de vuelta," - adjective: "-san" -desktop/views/components/ui.header.account.vue: - profile: "Tu perfil" - lists: "Listas" - follow-requests: "Solicitudes de seguimiento" - admin: "Admin" -desktop/views/components/ui.header.nav.vue: - game: "Juegos" -desktop/views/components/ui.header.notifications.vue: - title: "Notificaciones" -desktop/views/components/ui.header.post.vue: - post: "Crear una publicación" -desktop/views/components/ui.header.search.vue: - placeholder: "Buscar" -desktop/views/components/user-preview.vue: - notes: "Publicaciones" - following: "Sigue" - followers: "Seguidores" -desktop/views/components/users-list.vue: - all: "Todo" -desktop/views/components/window.vue: - close: "Cerrar" -admin/views/index.vue: - dashboard: "Panel de control" - instance: "Instancia" - moderators: "Moderadores" - users: "Usuarios" - federation: "Federado" - queue: "Cola de trabajos" - logs: "Registros" - back-to-misskey: "Volver a Misskey" -admin/views/dashboard.vue: - dashboard: "Panel de Control" - accounts: "Cuenta" - notes: "Publicaciones" - drive: "Drive" - instances: "Instancias" - this-instance: "Esta instancia" - federated: "Federado" -admin/views/queue.vue: - title: "Cola" - remove-all-jobs: "Limpiar todos los trabajos pendientes" - queue: "Cola" -admin/views/logs.vue: - logs: "Registros" -admin/views/abuse.vue: - title: "Abuso" - target: "Destinatario" - reporter: "Informador" - details: "Detalles" - remove-report: "eliminar" -admin/views/instance.vue: - instance: "Instancia" - instance-name: "Nombre de la instancia" - instance-description: "Descripción de la instancia" - host: "Host" - banner-url: "URL de la imagen de banner" - error-image-url: "Error en la URL de la imagen" - languages: "Idioma de esta instancia" - languages-desc: "Puedes añadir mas de uno, separado por espacios." - maintainer-config: "Información del administrador" - maintainer-name: "Nombre del administrador" - maintainer-email: "Contactar con el administrador" - drive-config: "Ajustes del Drive" - cache-remote-files: "Mantener en cache los archivos remotos" - recaptcha-preview: "Vista previa" - invite: "Invitar" - save: "Guardar" - saved: "Guardado" - email: "Correo electrónico" - smtp-host: "Host SMTP" - smtp-port: "Puerto SMTP" - smtp-user: "Usuario SMTP" - smtp-pass: "Contraseña SMTP" - test-email: "Prueba" -admin/views/charts.vue: - title: "Gráficos" - per-day: "Por dÃa" - per-hour: "Por hora" - federation: "Federación" - users: "Usuarios" - drive: "Drive" - network: "Red" -admin/views/drive.vue: - sort: - title: "Ordenar" - origin: - combined: "Local+Remoto" - local: "Local" - remote: "Remoto" - delete: "eliminar" - mark-as-sensitive: "Marcar como 'sensible'" - unmark-as-sensitive: "Desmarcar como 'sensible'" -admin/views/users.vue: - username: "Usuario" - host: "Host" - users: - state: - all: "Todo" - moderator: "Moderadores" - origin: - local: "Local" -admin/views/moderators.vue: - logs: - title: "Registros" - moderator: "Moderadores" -admin/views/emoji.vue: - add-emoji: - add: "Agregar" - emojis: - remove: "eliminar" -admin/views/announcements.vue: - save: "Guardar" - remove: "eliminar" - add: "Agregar" - saved: "Guardado" -admin/views/federation.vue: - instance: "Instancia" - host: "Host" - following: "Siguiendo" - status: "Estado" - block: "Bloquear" - instances: "Federado" - states: - all: "Todo" - blocked: "Bloquear" - charts: "Gráficos" - chart-spans: - hour: "Por hora" - day: "Por dÃa" - blocked-hosts: "Bloquear" - save: "Guardar" -desktop/views/pages/welcome.vue: - timeline: "Timeline" -desktop/views/pages/selectdrive.vue: - cancel: "Cancelar" -desktop/views/pages/user-list.users.vue: - username: "Usuario" -desktop/views/pages/user/user.followers-you-know.vue: - loading: "cargando" -desktop/views/pages/user/user.friends.vue: - loading: "cargando" -desktop/views/pages/user/user.photos.vue: - loading: "cargando" - no-photos: "No hay fotos." -desktop/views/pages/user/user.header.vue: - month: "lunes" - day: "domingo" -desktop/views/pages/user/user.timeline.vue: - default: "Posts" - with-replies: "Posts y respuestas" - with-media: "Multimedia" - my-posts: "Mis posts" -desktop/views/widgets/notifications.vue: - title: "Notificaciones" -desktop/views/widgets/polls.vue: - title: "Encuestas" - nothing: "No hay notificaciones" -desktop/views/widgets/trends.vue: - nothing: "No hay notificaciones" -mobile/views/components/drive.vue: - used: "usado" - folder-name: "Nombre de la carpeta" - url-prompt: "URL del fichero que quieres subir" -mobile/views/components/drive.file.vue: - nsfw: "Este contenido no es apropiado para ver en el trabajo" -mobile/views/components/drive.file-detail.vue: - download: "Descargar" - rename: "Renombrar" - nsfw: "Este contenido no es apropiado para ver en el trabajo" - mark-as-sensitive: "Marcar como 'sensible'" - unmark-as-sensitive: "Desmarcar como 'sensible'" -mobile/views/components/media-video.vue: - sensitive: "Este contenido no es apropiado para ver en el trabajo" - click-to-show: "Click para mostrar" -common/views/components/follow-button.vue: - following: "Siguiendo" - request-pending: "Solicitud pendiente" - follow-processing: "Solicitud en proceso" - follow-request: "Solicitudes de seguimiento" -mobile/views/components/note.vue: - private: "Esta publicación es privada" - deleted: "Esta publicación ha sido removida" - location: "Localización" -mobile/views/components/note-detail.vue: - reply: "Responder" - private: "Esta publicación es privada" - deleted: "Esta publicación ha sido removida" - location: "Localización" -mobile/views/components/notifications.vue: - empty: "No hay notificaciones" -mobile/views/components/sub-note-content.vue: - private: "Esta publicación es privada" - deleted: "Esta publicación ha sido removida" - poll: "Encuestas" -mobile/views/components/ui.header.vue: - welcome-back: "Bienvenido/a de vuelta," - adjective: "-san" -mobile/views/components/ui.nav.vue: - timeline: "Timeline" - notifications: "Notificaciones" - follow-requests: "Solicitudes de seguimiento" - search: "Buscar" - user-lists: "Listas" - game: "Juegos" - admin: "Admin" - about: "Sobre" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Subir fichero" - create-folder: "Crear una carpeta" -mobile/views/pages/home.vue: - home: "Inicio" - local: "Local" - hybrid: "Social" - global: "Global" -mobile/views/pages/widgets.vue: - dashboard: "Panel de control" - add-widget: "Agregar" - customization-tips: "Consejos de personalización" -mobile/views/pages/widgets/activity.vue: - activity: "Actividad" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "Buscar" -mobile/views/pages/notifications.vue: - notifications: "Notificaciones" -mobile/views/pages/user.vue: - timeline: "Timeline" -mobile/views/pages/user/home.vue: - activity: "Actividad" -mobile/views/pages/user/home.photos.vue: - no-photos: "No hay fotos." -deck: - home: "Inicio" - local: "Local" - hybrid: "Social" - hashtag: "Etiquetas" - global: "Global" - notifications: "Notificaciones" - list: "Listas" - rename: "Renombrar" -deck/deck.user-column.vue: - activity: "Actividad" - timeline: "Timeline" -pages: - pin-this-page: "Fijar en el perfil" - like: "Me gusta" - blocks: - post: "Formulario" - script: - categories: - random: "Aleatorio" - list: "Listas" - blocks: - _join: - arg1: "Listas" - random: "Aleatorio" - _randomPick: - arg1: "Listas" - _dailyRandomPick: - arg1: "Listas" - _seedRandomPick: - arg2: "Listas" - _pick: - arg1: "Listas" - _listLen: - arg1: "Listas" - types: - array: "Listas" -room: - save: "Guardar" - saved: "Guardado" - furnitures: - moon: "Luna" - bin: "Papelera" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml deleted file mode 100644 index 3f698f1c1588d576f578f71c6f1bad4bd5e8179c..0000000000000000000000000000000000000000 --- a/locales/fr-FR.yml +++ /dev/null @@ -1,2135 +0,0 @@ ---- -meta: - lang: "Français" -common: - misskey: "Une â du fédiverse" - about-title: "Une â du fédiverse." - about: "Merci d’avoir choisis Misskey. Misskey est une <b>plateforme de microblogage distribuée</b> née sur Terre et fait partie du Fédiverse (un univers composé de diverses plateformes de réseaux sociaux organisées), elle est connectée mutuellement avec d’autres plateformes de réseaux sociaux. Désirez-vous prendre une pause, un court instant, loin de l’agitation de la ville et plonger dans un Internet d’un nouveau genre ?" - intro: - title: "C’est quoi Misskey ?" - about: "Misskey est un <b>réseau social de Microblogage</b> open source. Il offre une interface utilisateur riche et hautement personnalisable, une variété de réactions aux publications et un lecteur pour la gestion centralisée de fichiers. De plus, comme il est possible de se connecter au reste du Fédiverse, vous pouvez interagir avec d'autres plateformes fédérées. Par exemple, si vous publiez quelque chose, la note sera transmise non seulement aux utilisateurs de Misskey, mais aussi à d'autres plateformes de réseaux sociaux dans le Fédiverse. Imaginez que vous puissiez transmettre des ondes radio d'une planète vers l'autre." - features: "Options" - rich-contents: "Notes" - rich-contents-desc: "Partagez vos idées, les événements et les sujets qui vous tiennent à cÅ“ur ainsi que tout autre chose que vous souhaitez partager avec les autres. Si vous le désirez, vous pouvez décorer vos messages en utilisant une syntaxe différente ou en y joignant des sondages et des fichiers, tels que les photos ou les vidéos que vous aimez." - reaction: "Réactions" - reaction-desc: "Une manière simple d'exprimer vos émotions. Misskey peut attacher diverses réactions aux publications des autres utilisateur·rice·s. Si vous essayez les réactions sur Misskey, vous ne pourrez plus retourner sur une autre plateforme de réseaux sociaux n'offrant que des « J'aime »." - ui: "Interface" - ui-desc: "Aucune interface graphique ne peut plaire à tout le monde. Par conséquent, Misskey possède une interface utilisateur hautement personnalisable selon vos goûts. Vous pouvez rendre votre page d'accueil originale en modifiant la mise en page de votre fil et en déplaçant les widgets que vous pouvez facilement ajuster pour vous approprier cet espace." - drive: "Drive" - drive-desc: "Vous voulez poster une photo que vous avez déjà transférée ? Vous souhaitez organiser, nommer et créer un dossier pour vos fichiers téléversés ? Misskey Drive est la meilleure solution pour vous. Très facile de partager vos fichiers en ligne." - outro: "Découvrez vous-même les fonctionnalités de Misskey. Étant donné que Misskey est un réseau social fédéré, vous pouvez essayer d’autres instances afin de trouver vos amis si la présente instance ne vous correspond pas. Bonne chance et amusez-vous bien !" - application-authorization: "Autorisations de l’application" - close: "Fermer" - do-not-copy-paste: "Veuillez ne pas entrer ou coller le code ici. Le compte pourrait être compromis." - load-more: "Charger plus" - enter-password: "Veuillez entrer le mot de passe" - 2fa: "Authentification à deux facteurs" - customize-home: "Personnaliser la disposition de votre accueil" - featured-notes: "Les notes mises en avant" - dark-mode: "Mode nuit" - signin: "Se connecter" - signup: "S'enregistrer" - signout: "Se déconnecter" - reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?" - fetching-as-ap-object: "Récupération depuis le fédiverse" - unfollow-confirm: "Désirez-vous vous désabonner de {name} ?" - delete-confirm: "Supprimer cette publication ?" - signin-required: "Veuillez vous connecter" - notification-type: "Type de notifications" - notification-types: - all: "Tout" - pollVote: "Sondages" - follow: "Abonnements" - receiveFollowRequest: "Demandes d’abonnements" - reply: "Réponses" - quote: "Cité par" - renote: "Partages" - mention: "Mentions" - reaction: "Réactions" - got-it: "J’ai compris !" - customization-tips: - title: "Conseils de personnalisation" - paragraph: "<p>La personnalisation de la page d'accueil vous permet d'ajouter/supprimer, glisser-déposer et réarranger les widgets.</p><p>Vous pouvez changer l'apparence de certain widget avec le <strong><strong>clic</strong>droit</strong>.</p><p>Pour supprimer un widget, faites glisser le widget sur <strong>la zone \"Corbeille\"</strong> dans l'en-tête.</p><p>Pour terminer la personnalisation, cliquez sur \"Terminé\" en haut à droite.</p>" - gotit: "Compris !" - notification: - file-uploaded: "Le fichier a été téléversé !" - message-from: "Message de {} :" - reversi-invited: "Invité à jouer" - reversi-invited-by: "Invité par {} :" - notified-by: "Notifié par {} :" - reply-from: "Réponse de {} :" - quoted-by: "Cité par {} :" - time: - unknown: "inconnu" - future: "à l’instant" - just_now: "à l'instant" - seconds_ago: "Il y a {} seconde(s)" - minutes_ago: "Il y a {} min" - hours_ago: "Il y a {} h" - days_ago: "Il y a {} j" - weeks_ago: "Il y a {} semaines" - months_ago: "Il y a {} mois" - years_ago: "Il y a {} an(s)" - month-and-day: "{day}-{month}" - trash: "Corbeille" - drive: "Drive" - pages: "Pages" - messaging: "Conversations" - home: "Principal" - deck: "Deck" - timeline: "Fil" - explore: "Découvrir" - following: "Suit" - followers: "Abonné·e·s" - favorites: "Favorites" - permissions: - "read:account": "Afficher les informations du compte" - "write:account": "Mettre à jour les informations de votre compte" - "read:blocks": "Voir les blocs" - "write:blocks": "Écrire des blocs" - "read:drive": "Parcourir le Drive" - "write:drive": "Écrire sur le Drive" - "read:favorites": "Afficher les favoris" - "write:favorites": "Écrire des favoris" - "read:following": "Voir les informations de l'abonné" - "write:following": "Suivre/Ne plus suivre" - "read:messaging": "Lire les conversations" - "write:messaging": "Utiliser la messagerie" - "read:mutes": "Voir les comptes masqués" - "write:mutes": "Gérer les comptes muets" - "write:notes": "Créer ou supprimer des publications" - "read:notifications": "Afficher les notifications" - "write:notifications": "Gérer vos notifications" - "read:reactions": "Lire les réactions" - "write:reactions": "Gérer vos réactions" - "write:votes": "Vote" - "read:pages": "Afficher la page" - "write:pages": "Mettre à jour les Pages" - "read:page-likes": "Lire les favoris sur les Pages" - "write:page-likes": "Mettre à jour les favoris sur les Pages" - "read:user-groups": "Voir les groupes d'utilisateur·rice·s" - "write:user-groups": "Éditer les groupes des utilisateur·rice·s" - empty-timeline-info: - follow-users-to-make-your-timeline: "Les utilisateur·rice·s suivant·e·s afficheront leurs publications sur votre fil." - explore: "Trouver des utilisateur·rice·s" - post-form: - attach-location-information: "Joindre des informations de localisation" - hide-contents: "Masquer les contenus" - reply-placeholder: "Répondre à cette note …" - quote-placeholder: "Citer cette note …" - option-quote-placeholder: "Citer ce billet ... (Facultatif)" - quote-attached: "Cité" - quote-question: "Souhaitez-vous ajoutez une citation ?" - submit: "Publication" - reply: "Répondre" - renote: "Republier" - posting: "Publication …" - attach-media-from-local: "Joindre un média depuis votre appareil" - attach-media-from-drive: "Joindre un média depuis votre Drive" - insert-a-kao: "v('ω')v" - create-poll: "Créer un sondage" - text-remain: "{} caractères restants" - recent-tags: "Récent" - local-only-message: "Ce message sera publié uniquement sur le fil local" - click-to-tagging: "Cliquer pour taguer" - visibility: "Visibilité" - geolocation-alert: "Votre appareil ne prend pas en charge les services de localisation" - error: "Erreur" - enter-username: "Saisir un nom d'utilisateur" - specified-recipient: "Correspondant·e" - add-visible-user: "Ajouter un utilisateur" - cw-placeholder: "Commenter le contenu (optionnel)" - username-prompt: "Saisir un nom d'utilisateur" - enter-file-name: "Éditer le nom du fichier" - weekday-short: - sunday: "D" - monday: "L" - tuesday: "M" - wednesday: "M" - thursday: "J" - friday: "V" - saturday: "S" - weekday: - sunday: "Dimanche" - monday: "Lundi" - tuesday: "Mardi" - wednesday: "Mercredi" - thursday: "Jeudi" - friday: "Vendredi" - saturday: "Samedi" - reactions: - like: "Bien" - love: "Adore" - laugh: "Rire" - hmm: "Hmm … ?" - surprise: "Wow" - congrats: "Félicitations !" - angry: "En colère" - confused: "Confus" - rip: "RIP" - pudding: "Pudding" - note-visibility: - public: "Public" - home: "Principal" - home-desc: "Publier sur le fil principal uniquement" - followers: "Abonné·e·s" - followers-desc: "Publier à vos abonné·e·s uniquement" - specified: "Direct" - specified-desc: "Publier uniquement aux utilisateur·rice·s mentionné·e·s" - local-public: "Local (Public)" - local-home: "Accueil (local uniquement)" - local-followers: "Abonné·e·s (Local uniquement)" - note-placeholders: - a: "Que faites-vous maintenant ?" - b: "Quoi de neuf ?" - c: "Qu’avez-vous en tête ?" - d: "Désirez-vous publier quelques mots ?" - e: "Écrivez ici" - f: "En attente de vos écrits" - settings: "Paramètres" - _settings: - profile: "Votre profil" - notification: "Notifications" - apps: "Applications" - tags: "Hashtags" - mute-and-block: "Masqués / Bloqués" - blocking: "En cours blocage" - security: "Sécurité" - signin: "Historique des connexions" - password: "Mot de passe" - other: "Avancé" - appearance: "Apparence" - behavior: "Comportement" - reactions: "Réaction" - reactions-description: "Personnaliser les émojis à afficher dans le sélecteur de réactions, délimités par les sauts de ligne." - fetch-on-scroll: "Chargement automatique lors du défilement" - fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page." - note-visibility: "Visibilité de la publication" - default-note-visibility: "Visibilité par défaut" - remember-note-visibility: "Se souvenir du mode de visibilité de la publication" - web-search-engine: "Moteur de recherche Web" - web-search-engine-desc: "Exemple : https://www.google.com/?#q={{query}}" - paste: "Coller" - pasted-file-name: "Modèle de nom de fichier collé" - pasted-file-name-desc: "Exemple : \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" - paste-dialog: "Modifier le nom du fichier collé" - keep-cw: "Maintenir l'avertissement de contenu" - keep-cw-desc: "Lorsque vous répondez à un message, le même avertissement de contenu est reprit par défaut dans la réponse, le même que celui qui a été défini dans le message original." - i-like-sushi: "Je préfère les sushis plutôt que le pudding" - show-reversi-board-labels: "Afficher les étiquettes des lignes et colonnes dans Reversi" - use-avatar-reversi-stones: "Utiliser l’avatar comme pion dans Reversi" - disable-animated-mfm: "Désactiver les textes animés dans les publications" - disable-showing-animated-images: "Désactiver l'animation des images" - enable-quick-notification-view: "Activer l'affichage rapide des notifications" - suggest-recent-hashtags: "Afficher les hashtags populaires dans le champs de saisie" - always-show-nsfw: "Toujours afficher les contenus sensibles" - always-mark-nsfw: "Toujours marquer les notes ayant des médias comme sensibles" - show-full-acct: "Afficher l’adresse complète de l’utilisateur" - show-via: "Afficher via" - reduce-motion: "Réduire les animations dans l’interface utilisateur" - this-setting-is-this-device-only: "Uniquement sur cet appareil" - use-os-default-emojis: "Utiliser les émojis standards du système" - line-width: "Epaisseur du trait" - line-width-thin: "Fine" - line-width-normal: "Normale" - line-width-thick: "Épaisse" - font-size: "Taille du texte" - font-size-x-small: "Très petit" - font-size-small: "Petite" - font-size-medium: "Normale" - font-size-large: "Grande" - font-size-x-large: "Large" - deck-column-align: "Alignement des colonnes du Deck" - deck-column-align-center: "Centrer" - deck-column-align-left: "À gauche" - deck-column-align-flexible: "Flexible" - deck-column-width: "Largeur des colonnes du Deck" - deck-column-width-narrow: "Étroite" - deck-column-width-narrower: "Légèrement étroite" - deck-column-width-normal: "Normale" - deck-column-width-wider: "Légèrement large" - deck-column-width-wide: "Large" - use-shadow: "Utiliser les ombres dans l'interface utilisateur" - rounded-corners: "Coins arrondis de l'interface utilisateur" - circle-icons: "Utiliser des avatar circulaires" - contrasted-acct: "Ajouter du contraste au nom de l’utilisateur" - wallpaper: "Image du fond d'écran" - choose-wallpaper: "Sélectionner un fond d'écran" - delete-wallpaper: "Supprimer le fond d'écran" - post-form-on-timeline: "Afficher le champs de saisie en haut du fil" - show-clock-on-header: "Afficher l'horloge sur le coté supérieur droit" - show-reply-target: "Afficher les réponses" - timeline: "Fil d’actualité" - show-my-renotes: "Afficher mes republications dans le fil" - show-renoted-my-notes: "Afficher les partages de mes propres notes sur le fil" - show-local-renotes: "Afficher les partages locaux sur les fils" - remain-deleted-note: "Continuer à afficher les notes supprimées" - sound: "Son" - enable-sounds: "Activer les sons" - enable-sounds-desc: "Jouer un son lorsque vous recevez un message/publication. Ce paramètre est sauvegardé dans le navigateur." - volume: "Volume" - test: "Test" - update: "Mise à jour de Misskey" - version: "Version actuelle :" - latest-version: "Dernière version :" - update-checking: "Recherche de mises à jour" - do-update: "Rechercher des mises à jour" - update-settings: "Paramètres avancés" - no-updates: "Aucune mise à jour disponible" - no-updates-desc: "Votre Misskey est à jour." - update-available: "Nouvelle version disponible !" - update-available-desc: "Les mises à jour seront appliquées une fois la page est rechargée." - advanced-settings: "Paramètres avancés" - debug-mode: "Activer le mode débogage" - debug-mode-desc: "Ce paramètre est stocké dans le navigateur." - navbar-position: "Position de la barre de navigation" - navbar-position-top: "En haut" - navbar-position-left: "À gauche" - navbar-position-right: "À droite" - i-am-under-limited-internet: "J'ai un accès Internet limité" - post-style: "Style d'affichage des notes" - post-style-standard: "Standard" - post-style-smart: "Intelligent" - notification-position: "Afficher les notifications" - notification-position-bottom: "en bas" - notification-position-top: "En haut" - disable-via-mobile: "Enlever la mention publié via 'mobile'" - load-raw-images: "Afficher les photos jointes dans leur qualité originale" - load-remote-media: "Afficher les médias depuis le serveur distant" - sync: "Synchroniser" - save: "Enregistrer" - saved: "enregistré" - preview: "Prévisualisation" - home-profile: "Profil principal" - deck-profile: "Profil deck" - room: "Pièce" - _room: - graphicsQuality: "Qualité des graphismes" - _graphicsQuality: - ultra: "Très élevée" - high: "Élevée" - medium: "Moyenne" - low: "Basse" - cheep: "Minimale" - useOrthographicCamera: "Utiliser une caméra orthographique" - search: "Recherche" - delete: "Supprimer" - loading: "Chargement en cours …" - ok: "Confirmer" - cancel: "Quitter" - update-available-title: "Mise à jour disponible" - update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour." - my-token-regenerated: "Votre jeton vient d’être généré, vous allez maintenant être déconnecté." - hide-password: "Masquer le mot de passe" - show-password: "Afficher le mot de passe" - enter-username: "Saisir un nom d'utilisateur" - do-not-use-in-production: "Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production." - user-suspended: "Cet·te utilisateur·trice a été suspendu·e" - is-remote-user: "Les informations à propos de ce compte peuvent être incomplètes." - is-remote-post: "Ceci est une publication distante." - view-on-remote: " Consulter le profil complet" - renoted-by: "Renoté par {user}" - no-notes: "Sans aucune note" - turn-on-darkmode: "Mode nuit" - turn-off-darkmode: "Mode jour" - error: - title: "Une erreur est survenue" - retry: "Réessayer" - reversi: - drawn: "Partie nulle" - my-turn: "C’est votre tour" - opponent-turn: "Tour de l’adversaire" - turn-of: "Tour de {name}" - past-turn-of: "Tour de {name}" - won: "{name} a gagné" - black: "Noirs" - white: "Blancs" - total: "Total" - this-turn: "Tour {count}" - widgets: - analog-clock: "Horloge analogique" - profile: "Profil" - calendar: "Calendrier" - timemachine: "Calendrier (Machine temporelle)" - activity: "Activité" - rss: "Lecteur de flux RSS" - memo: "Pense-bête" - trends: "Tendances" - photo-stream: "Flux de photos" - posts-monitor: "Graphe des publications" - slideshow: "Diaporama" - version: "Version" - broadcast: "Diffusion" - notifications: "Notifications" - users: "Utilisateur·rice·s recommandé·e·s" - polls: "Sondages" - post-form: "Champs de publication" - server: "Infos sur le serveur" - nav: "Navigation" - tips: "Conseils" - hashtags: "Hashtags" - queue: "File d'attente" - dev: "Échec lors de la création de l’application. Veuillez réessayer." - ai-chan-kawaii: "Ai-Chan est mignonne !" - you: "Vous" -auth/views/form.vue: - share-access: "Désirez-vous autoriser <i>{name}</i> à avoir accès à votre compte ?" - permission-ask: "Cette application nécessite les autorisations suivantes :" - cancel: "Annuler" - accept: "Autoriser l’accès" -auth/views/index.vue: - loading: "Chargement en cours" - denied: "L’autorisation de l’application a été refusée." - denied-paragraph: "Cette application ne va pas accéder à votre compte." - already-authorized: "Cette application est déjà autorisée." - allowed: "Permissions autorisées de l’application." - callback-url: "Retour vers l’application." - please-go-back: "Veillez retourner à l'application." - error: "La session n’existe pas." - sign-in: "Veuillez vous connecter" -common/views/pages/explore.vue: - pinned-users: "Utilisateur·rice·s épinglé·e·s" - popular-users: "Utilisateur·rice·s populaires" - recently-updated-users: "Utilisateur·rice·s actif·ve·s récemment" - recently-registered-users: "Les nouveaux inscrits" - recently-discovered-users: "Utilisateurs récemment découverts" - popular-tags: "Mots-clés populaires" - federated: "Du Fédiverse" - explore: "Explorer {host}" - explore-fediverse: "Explorer le Fédiverse" - users-info: "Actuellement, {users} utilisateur·rice·s se sont inscrit ici" -common/views/components/reactions-viewer.details.vue: - few-users: "{users} ont réagit avec {reaction}" -common/views/components/url-preview.vue: - enable-player: "Activer la lecture" - disable-player: "Fermer le lecteur" -common/views/components/user-list.vue: - no-users: "Il n'y a aucun utilisateur" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "En attente de {}" - cancel: "Annuler" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Se rendre" - surrendered: "Par abandon" - is-llotheo: "Celui ou celle qui a moins de pierres gagne (Roseo)" - looped-map: "Carte en boucle" - can-put-everywhere: "Peut poser partout" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "Jouer à Reversi avec vos amis !" - invite: "Inviter" - rule: "Comment jouer ?" - rule-desc: "Reversi est un jeu qui se joue sur un tablier et dans lequel les joueurs placent des pions sur ce dernier, à tour de rôle avec l'adversaire. Le but du jeu est d'avoir plus de pions de sa couleur que l'adversaire à la fin de la partie, celle-ci s'achevant lorsque aucun des deux joueurs ne peut plus jouer de coup légal, généralement lorsque les 64 cases sont occupées." - mode-invite: "Inviter" - mode-invite-desc: "Inviter un joueur." - invitations: "Vous avez reçu une invitation !" - my-games: "Mes jeux" - all-games: "Tous les jeux" - enter-username: "Saisir un nom d'utilisateur" - game-state: - ended: "Terminée" - playing: "En cours" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Paramètres du jeu" - choose-map: "Sélectionnez une carte" - random: "Aléatoire" - black-or-white: "Noirs/Blancs" - black-is: "{} Noirs" - rules: "Règles" - is-llotheo: "Celui ou celle qui a le moins de pièces gagne (Llotheo)" - looped-map: "Carte en boucle" - can-put-everywhere: "Peut poser partout" - settings-of-the-bot: "Configuration du bot" - this-game-is-started-soon: "La partie commencera dans quelques instants" - waiting-for-other: "En attente que l'adversaire soit prêt" - waiting-for-me: "En attente que vous soyez prêt" - waiting-for-both: "En attente que vous soyez prêt" - cancel: "Annuler" - ready: "Prêt" - cancel-ready: "Annuler « Prêt »" -common/views/components/connect-failed.vue: - title: "Échec de connexion au serveur" - description: "Il se peut qu’il y est un problème avec votre connexion internet, ou le serveur est hors-ligne ou en maintenance. Veuillez {réessayer} plus tard." - thanks: "On vous remercie d’avoir choisi d’utiliser Misskey." - troubleshoot: "Dépanner" -common/views/components/connect-failed.troubleshooter.vue: - title: "Dépannage" - network: "Connexion au réseau" - checking-network: "Vérification de la connexion au réseau" - internet: "Connexion Internet" - checking-internet: "Vérification de la connexion internet" - server: "Connexion au serveur" - checking-server: "Vérification de la connexion au serveur" - finding: "Recherche d'un problème" - no-network: "Aucune connexion au réseau" - no-network-desc: "Veuillez vérifier que vous êtes bien connecté au réseau." - no-internet: "Aucune connexion internet." - no-internet-desc: "Assurez-vous que vous êtes bien connectés à internet." - no-server: "Impossible de se connecter au serveur" - no-server-desc: "Votre connexion semble correcte, mais il a été impossible de vous connecter au serveur de Misskey. Il se peut que le serveur soit hors-ligne ou en maintenance, veuillez ressayer plus tard." - success: "Connexion au serveur de Misskey réussie !" - success-desc: "Succès de la connexion au serveur de Misskey. Veuillez recharger la page." - flush: "Vider le cache" - set-version: "Choisissez une version" -common/views/components/media-banner.vue: - sensitive: "Contenu sensible" - click-to-show: "Cliquer pour afficher" -common/views/components/theme.vue: - theme: "Thème" - light-theme: "Thème en mode jour" - dark-theme: "Thème en mode nuit" - light-themes: "Thème clair" - dark-themes: "Thème sombre" - install-a-theme: "Installer un thème" - theme-code: "Code du thème" - install: "Installation" - installed: "« {} » a été installé" - create-a-theme: "Créer un thème" - save-created-theme: "Enregistrer le thème" - primary-color: "Couleur primaire" - secondary-color: "Couleur secondaire" - text-color: "Couleur du texte" - base-theme: "Thème de base" - base-theme-light: "Clair" - base-theme-dark: "Sombre" - find-more-theme: "Obtenir d’autres thèmes" - theme-name: "Nom du Thème" - preview-created-theme: "Prévisualisation" - invalid-theme: "Thème n’est pas valide." - already-installed: "Le thème est déjà installé." - saved: "enregistré" - manage-themes: "Gestion des thèmes" - builtin-themes: "Thèmes standards" - my-themes: "Mes thèmes" - installed-themes: "Thèmes installés" - select-theme: "Veuillez sélectionner un thème" - uninstall: "Désinstaller" - uninstalled: "« {} » a été désinstallé" - author: "Auteur" - desc: "Description" - export: "Exporter" - import: "Importer" - import-by-code: "Ou coller du code" - theme-name-required: "Nom du thème est obligatoire." -common/views/components/cw-button.vue: - hide: "Masquer" - show: "Voir plus" - chars: "{count} caractères" - files: "{count} fichiers" - poll: "Sondage" -common/views/components/messaging.vue: - search-user: "Trouver un utilisateur" - you: "Vous" - no-history: "Pas d'historique" - user: "Utilisateur·rice·s" - group: "Groupe" - start-with-user: "Initier une discussion avec un·e utilisateur·rice" - start-with-group: "Démarrer un groupe et converser" - select-group: "Sélectionner un groupe" -common/views/components/messaging-room.vue: - not-talked-user: "Vous n'avez pas encore discuté avec cet·te utilisateur·rice" - not-talked-group: "Il n y a aucune conversation dans ce groupe" - no-history: "Aucun historique" - new-message: "Nouveau message" - only-one-file-attached: "Vous ne pouvez joindre qu'un seul fichier au message" -common/views/components/messaging-room.form.vue: - input-message-here: "Tapez ici votre message" - send: "Envoyer" - attach-from-local: "Joindre un fichier depuis votre ordinateur" - attach-from-drive: "Joindre un fichier depuis votre Drive" - only-one-file-attached: "Vous ne pouvez joindre qu'un seul fichier au message" -common/views/components/messaging-room.message.vue: - is-read: "Lu" - deleted: "Ce message a été supprimé" -common/views/components/nav.vue: - about: "À propos" - stats: "Statistiques" - status: "Statut" - wiki: "Wiki" - donors: "Donateur·rice·s" - repository: "Dépôt" - develop: "Développeurs" - feedback: "Suggestions" - tos: "Conditions d'utilisation" -common/views/components/note-menu.vue: - mention: "Mention" - detail: "Détails" - copy-content: "Copier le contenu" - copy-link: "Copier le lien" - favorite: "Mettre cette note en favoris" - unfavorite: "Retirer des favoris" - watch: "Surveiller" - unwatch: "Ne plus surveiller" - pin: "Épingler sur votre profil" - unpin: "Désépingler" - delete: "Supprimer" - delete-confirm: "Supprimer cette publication ?" - delete-and-edit: "Supprimer et réécrire" - delete-and-edit-confirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses." - remote: "Afficher la note originale" - pin-limit-exceeded: "Vous ne pouvez plus épingler davantage de publications." -common/views/components/user-menu.vue: - mention: "Mention" - mute: "Silencier" - unmute: "Enlever la sourdine" - mute-confirm: "Rendre muet cet utilisateur ?" - unmute-confirm: "Ne plus masquer cet utilisateur ?" - block: "Bloquer" - unblock: "Débloquer" - block-confirm: "Bloquer cet utilisateur ?" - unblock-confirm: "Débloquer cet utilisateur ?" - push-to-list: "Ajouter à une liste" - select-list: "Sélectionnez une liste" - report-abuse: "Signaler un abus" - report-abuse-detail: "Détail du signalement" - report-abuse-reported: "Transmit à l’administrateur. Merci de votre collaboration." - silence: "Mettre en sourdine" - unsilence: "Enlever la sourdine" - silence-confirm: "Êtes-vous surs de vouloir mettre cet·te utilisateur·rice en sourdine ?" - suspend: "Suspendre" - unsuspend: "Ne plus suspendre" - suspend-confirm: "Êtes-vous surs de vouloir suspendre cet·te utilisateur·rice ?" - unsuspend-confirm: "Êtes-vous sûr de vouloir débloquer cet utilisateur ?" -common/views/components/poll.vue: - vote-to: "Voter pour '{}'" - vote-count: "{} votes" - total-votes: "{} Total des votes" - vote: "Vote" - show-result: "Montrer les résultats" - voted: "Voté" - closed: "Terminé" - remaining-days: "{d} jours, {h} heures restantes" - remaining-hours: "{h} heures et {m} minutes restantes" - remaining-minutes: "{m} minutes et {s} secondes restantes" - remaining-seconds: "{s} secondes restantes" -common/views/components/poll-editor.vue: - no-only-one-choice: "Vous devez saisir au moins deux choix." - choice-n: "Choix {}" - remove: "Supprimer ce choix" - add: "+ Ajouter un choix" - destroy: "Annuler ce sondage" - multiple: "Autoriser le multi-choix" - expiration: "Valide jusqu'à " - infinite: "Illimité" - at: "Choisir une date et une durée" - after: "Choisir la durée" - no-more: "Vous ne pouvez pas en ajouter davantage" - deadline-date: "Date d’échéance" - deadline-time: "Durée" - interval: "Durée" - unit: "Unité" - second: "secondes" - minute: "Minutes" - hour: "Heures" - day: "D" -common/views/components/reaction-picker.vue: - choose-reaction: "Envoyer une réaction" - input-reaction-placeholder: "ou insérez un émoji" -common/views/components/emoji-picker.vue: - recent-emoji: "Utilisés récemment" - custom-emoji: "Émoji personnalisé" - no-category: "Sans catégorie" - people: "Personnes" - animals-and-nature: "Animaux et nature" - food-and-drink: "Nourriture et boisson" - activity: "Activités" - travel-and-places: "Lieux et voyages" - objects: "Objets" - symbols: "Symboles" - flags: "Drapeaux" -common/views/components/settings/app-type.vue: - title: "Mode" - intro: "Vous pouvez choisir, si vous voulez utiliser la disposition de bureau ou mobile." - choices: - auto: "Choisir la disposition automatiquement" - desktop: "Toujours utiliser la disposition de bureau" - mobile: "Toujours utiliser la disposition mobile" - info: "Le rechargement de la page est requis afin d'appliquer les modifications." -common/views/components/signin.vue: - username: "Nom d'utilisateur·rice" - password: "Mot de passe" - token: "Jeton" - signing-in: "Connexion…" - or: "Ou" - signin-with-twitter: "Se connecter via Twitter" - signin-with-github: "Se connecter avec GitHub" - signin-with-discord: "Se connecter avec Discord" - login-failed: "Échec d’authentification. Veuillez vérifier que votre nom d’utilisateur et mot de passe sont corrects." - tap-key: "Cliquez sur la clé de sécurité pour vous connecter" - enter-2fa-code: "Entrez votre code de vérification" -common/views/components/signup.vue: - invitation-code: "Code d’invitation" - invitation-info: "Si vous n’avez pas de code d’invitation, contactez un <a href=\"{}\">administrateur</a>." - username: "Nom d'utilisateur·rice" - checking: "Vérification…" - available: "Disponible" - unavailable: "Non disponible" - error: "Erreur du réseau" - invalid-format: "Vous pouvez utiliser des lettres, des nombres et _." - too-short: "Veuillez saisir au moins un caractère !" - too-long: "Veuillez entrer au maximum 20 caractères." - password: "Mot de passe" - password-placeholder: "Nous recommandons au moins 8 caractères." - weak-password: "Faible" - normal-password: "Moyen" - strong-password: "Fort" - retype: "Retapez" - retype-placeholder: "Confirmez votre mot de passe" - password-matched: "OK" - password-not-matched: "Les mots de passe ne correspondent pas." - recaptcha: "Vérifier" - agree-to: "Accepter {0}." - tos: "Conditions d'utilisation" - create: "Créer un compte" - some-error: "La création du compte a échoué. Veuillez réessayer." -common/views/components/special-message.vue: - new-year: "Bonne année !" - christmas: "Joyeux Noël !" -common/views/components/stream-indicator.vue: - connecting: "Connexion en cours" - reconnecting: "Reconnexion en cours" - connected: "Connecté" -common/views/components/notification-settings.vue: - title: "Notifications" - mark-as-read-all-notifications: "Marquer toutes les notifications comme lues" - mark-as-read-all-unread-notes: "Marquer toutes les notes comme lues" - mark-as-read-all-talk-messages: "Marquer toutes les conversations comme lues" - auto-watch: "Surveiller automatiquement les publications" - auto-watch-desc: "Recevoir automatiquement des notifications à propos des publications auxquelles vous avez réagi ou répondu" -common/views/components/integration-settings.vue: - title: "Intégrations" - connect: "Connecter" - disconnect: "Déconnecter" - connected-to: "Vous êtes connectés aux services suivants" -common/views/components/github-setting.vue: - description: "Si vous liez votre compte GitHub à votre compte Misskey, vous verrez votre compte GitHub s’afficher sur votre profil, vous aurez également la possibilité de vous connecter à Misskey en utilisant ce dernier." - connected-to: "Vous êtes connecté à votre compte GitHub" - detail: "Détails …" - reconnect: "Reconnecter" - connect: "Se connecter avec GitHub" - disconnect: "Déconnecter" -common/views/components/discord-setting.vue: - description: "Si vous liez votre compte Discord à votre compte Misskey, vous serez en mesure de voir votre compte Twitter s'afficher sur votre profil, vous aurez aussi la possibilité de vous connecter à Misskey en utilisant votre compte Discord." - connected-to: "Vous êtes connecté à votre compte Discord" - detail: "Détails…" - reconnect: "Reconnecter" - connect: "Lier votre compte Discord" - disconnect: "Déconnecter" -common/views/components/uploader.vue: - waiting: "Veuillez patienter" -common/views/components/visibility-chooser.vue: - public: "Public" - home: "Accueil" - home-desc: "Publier sur le fil d’Accueil uniquement" - followers: "Abonné·e·s" - followers-desc: "Publier à vos abonné·e·s uniquement" - specified: "Direct" - specified-desc: "Publier uniquement aux utilisateur·rice·s mentionné·e·s" - local-public: "Local (Public)" - local-public-desc: "Ne pas publier pour les distants" - local-home: "Accueil (local uniquement)" - local-followers: "Abonné·e·s (Local uniquement)" -common/views/components/trends.vue: - count: "{} utilisateur·rice·s mentionné·e·s" - empty: "Aucune tendance" -common/views/components/language-settings.vue: - title: "Langue " - pick-language: "Sélectionner une langue" - recommended: "Recommandé" - auto: "Automatique" - specify-language: "Spécifier la langue" - info: "Le rechargement de la page est requis afin d'appliquer les modifications." -common/views/components/profile-editor.vue: - title: "Profil" - name: "Nom" - account: "Compte" - location: "Lieu" - description: "À propos de moi" - you-can-include-hashtags: "Vous pouvez également inclure un hashtag sur votre description de profile." - language: "Langue" - birthday: "Date de naissance" - avatar: "Avatar" - banner: "Bannière" - is-cat: "Ce compte est un Chat" - is-bot: "Ce compte est un Bot" - is-locked: "Demandes d’abonnements requièrent l’approbation" - careful-bot: "Les demandes d’abonnements venant de Bots requièrent l’approbation" - auto-accept-followed: "Accepter automatiquement les demandes d’abonnement venant des gens que vous suivez" - advanced: "Avancé" - privacy: "Vie privée" - save: "Mettre à jour le profil" - saved: "Profil mis à jour avec succès" - uploading: "En cours d’envoi …" - upload-failed: "Échec de l'envoi" - unable-to-process: "L'opération n'a pas pu être complétée" - avatar-not-an-image: "Le fichier sélectionné pour votre avatar n'est pas une image" - banner-not-an-image: "Le fichier sélectionné pour votre bannière n'est pas une image" - email: "Paramètres de messagerie" - email-address: "Adresse de courrier électronique" - email-verified: "L’adresse du courrier électronique a été vérifiée." - email-not-verified: "Adresse de courriel n’est pas confirmée. Veuillez vérifier votre boite de réception." - export: "Exporter" - import: "Importer" - export-and-import: "Exportation et importation" - export-targets: - all-notes: "Toutes les notes publiées" - following-list: "Liste des abonnements" - mute-list: "Liste des comptes mis en sourdine" - blocking-list: "Liste des comptes bloqués" - user-lists: "Listes" - export-requested: "Vous avez demandé une exportation. Cela peut prendre un certain temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive." - import-requested: "Vous avez initié un import. Ceci peut prendre un peu de temps." - enter-password: "Veuillez saisir votre mot de passe" - danger-zone: "Zone de danger" - delete-account: "Supprimer le compte" - account-deleted: "Le compte a été supprimé. Cela peut prendre un certain temps avant que toutes les données disparaissent." - profile-metadata: "Métadonnées du profil" - metadata-label: "Étiquette" - metadata-content: "Contenu" -common/views/components/user-list-editor.vue: - users: "Utilisateur·rice" - rename: "Renommer la liste" - delete: "Supprimer la liste" - remove-user: "Retirer de cette liste" - delete-are-you-sure: "Voulez-vous vraiment supprimer la liste « $1 » ?" - deleted: "Supprimé" - add-user: "Ajouter un utilisateur" -common/views/components/user-group-editor.vue: - users: "Membres" - rename: "Renommer le groupe" - delete: "Supprimer le groupe" - transfer: "Transférer de groupe" - transfer-are-you-sure: "Êtes vous surs de vouloir ajouter @$2 au groupe $1 ?" - transferred: "Groupe transféré" - remove-user: "Enlever un utilisateur de ce groupe" - delete-are-you-sure: "Désirez-vous vraiment supprimer le groupe $1 ?" - deleted: "Supprimé" - invite: "Inviter" - invited: "Invitation envoyée avec succès" -common/views/components/user-lists.vue: - user-lists: "Listes" - create-list: "Créer une liste" - list-name: "Nom de la liste" -common/views/components/user-groups.vue: - user-groups: "Groupe" - create-group: "Créer un groupe" - group-name: "Nom du groupe" - owned-groups: "Mes groupes" - joined-groups: "Membre dans les groupes" - invites: "Inviter" - accept-invite: "Participer" - reject-invite: "Refuser" -common/views/widgets/broadcast.vue: - fetching: "Récupération" - no-broadcasts: "Aucune annonce" - have-a-nice-day: "Passez une bonne journée !" - next: "Suivant" - prev: "Précédent" -common/views/widgets/calendar.vue: - year: "Année {}" - month: "{}," - day: "{}" - today: "Aujourd’hui :" - this-month: "Ce mois-ci :" - this-year: "Cette année :" -common/views/widgets/photo-stream.vue: - title: "Flux de photos" - no-photos: "Pas de photo" -common/views/widgets/posts-monitor.vue: - title: "Graphe des publications" - toggle: "Basculer entre les vues" -common/views/widgets/hashtags.vue: - title: "Hashtags" -common/views/widgets/server.vue: - title: "Informations sur le serveur" - toggle: "Afficher les vues" -common/views/widgets/memo.vue: - title: "Pense-bête" - memo: "Écrivez ici !" - save: "Enregistrer" -common/views/widgets/slideshow.vue: - folder-customize-mode: "Pour pouvoir spécifier un dossier, veuillez quitter le mode de personnalisation" - folder: "Veuillez cliquer pour spécifier le dossier" - no-image: "Il n'y a aucune image dans ce dossier" -common/views/widgets/tips.vue: - tips-line1: "Vous pouvez vous concentrer sur le fil avec <kbd>t</kbd>" - tips-line2: "Ouvre la fenêtre de publication en appuyant sur <kbd>p</kbd> ou <kbd>n</kbd>." - tips-line3: "Vous pouvez glisser et déposer des fichiers sur la fenêtre de la note" - tips-line4: "Vous pouvez coller des images à partir du presse-papier sur la fenêtre de la note" - tips-line5: "Vous pouvez téléverser des fichiers sur le Drive en faisant un glisser-déposer" - tips-line6: "Vous pouvez déplacer un dossier en le glissant dans le Drive" - tips-line7: "Vous pouvez déplacer des dossiers en les glissant dans le Drive" - tips-line8: "Vous pouvez personnaliser l'Accueil via les paramètres" - tips-line9: "Misskey est sous licence AGPLv3" - tips-line10: "L'utilisation du widget Time Machine permet de remonter facilement dans le passé du fil." - tips-line11: "Vous pouvez épingler des notes sur votre page en cliquant sur « … »" - tips-line13: "Tous les fichiers attachés à cette publication sont sauvegardés dans le Drive" - tips-line14: "Lorsque vous personnalisez la disposition de votre page d’accueil, vous pouvez effectuer un clique droit sur un widget pour changer son apparence." - tips-line17: "Vous pouvez mettre un texte en surbrillance en le mettant entre ** **" - tips-line19: "Plusieurs fenêtres peuvent être détachées en dehors du navigateur." - tips-line20: "Pourcentage sur le widget calendrier qui indique le pourcentage de temps passé" - tips-line21: "Vous pouvez aussi utiliser l'API pour développer des Bots." - tips-line23: "Ai-chan kawaii!" - tips-line24: "Misskey est fonctionnel depuis 2014" - tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible" -common/views/pages/not-found.vue: - page-not-found: "La page demandée est introuvable !" -common/views/pages/follow.vue: - signed-in-as: "Connecté en tant que {}" - following: "Suit" - follow: "Suivre" - request-pending: "Demande d’abonnement en attente" - follow-processing: "Demande en attente" - follow-request: "Demande d’abonnement" -common/views/pages/follow-requests.vue: - received-follow-requests: "Demandes d’abonnement" - accept: "Accepter" - reject: "Refuser" -desktop: - banner-crop-title: "Découpez la partie qui apparaîtra comme bannière" - banner: "Bannière" - uploading-banner: "Téléversement d'une nouvelle bannière" - banner-updated: "Mise à jour de la bannière avec succès" - choose-banner: "Choisir une bannière" - avatar-crop-title: "Découpez la partie qui apparaîtra comme avatar" - avatar: "Avatar" - uploading-avatar: "Téléversement du nouvel avatar" - avatar-updated: "Mise à jour de l’avatar avec succès" - choose-avatar: "Choisir un avatar" - unable-to-process: "L'opération n'a pas pu être complétée" - invalid-filetype: "Ce format de fichier n’est pas pris en charge" -desktop/views/components/activity.chart.vue: - total: "Noirs ... Total" - notes: "Bleu ... Notes" - replies: "Rouge ... Réponses" - renotes: "Vert ... Partages" -desktop/views/components/activity.vue: - title: "Activité" - toggle: "Afficher les vues" -desktop/views/components/calendar.vue: - title: "{month} - {year}" - prev: "Mois précédent" - next: "Mois suivant" - go: "Cliquez pour naviguer" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count} fichier(s) sélectionné(s)" - upload: "Téléverser des fichiers à partir de votre ordinateur" - cancel: "Annuler" - ok: "OK" - choose-prompt: "Choisir un fichier" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Annuler" - ok: "OK" - choose-prompt: "Choisir un dossier" -desktop/views/components/crop-window.vue: - skip: "Ignorer la découpe" - cancel: "Annuler" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "utilisé" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Bannière" - nsfw: "CW" - contextmenu: - rename: "Renommer" - mark-as-sensitive: "Marquer comme sensible" - unmark-as-sensitive: "Ne pas marquer comme sensible" - copy-url: "Copier l’URL" - download: "Télécharger" - else-files: "Avancé" - set-as-avatar: "Utiliser en tant qu'avatar" - set-as-banner: "Utiliser en tant que bannière" - open-in-app: "Ouvrir dans l'application" - add-app: "Ajouter une application" - rename-file: "Renommer le ficher" - input-new-file-name: "Entrer un nouveau nom" - copied: "Copié" - copied-url-to-clipboard: "L'URL a été copiée dans le presse-papier" -desktop/views/components/drive.folder.vue: - upload-folder: "Emplacement de téléversement par défaut" - unable-to-process: "L'opération n'a pas pu être complétée" - circular-reference-detected: "Le dossier de destination est un sous-dossier du dossier que vous souhaitez déplacer." - unhandled-error: "Erreur inconnue" - unable-to-delete: "Ne peut pas être supprimé" - has-child-files-or-folders: "Ce dossier n'est pas vide, il ne peut pas être supprimé" - contextmenu: - move-to-this-folder: "Déplacer dans ce dossier" - show-in-new-window: "Ouvrir dans une nouvelle fenêtre" - rename: "Renommer" - rename-folder: "Renommer le dossier" - input-new-folder-name: "Entrer un nouveau nom" - else-folders: "Avancé" - set-as-upload-folder: "Spécifier en tant que dossier de téléversement par défaut" -desktop/views/components/drive.vue: - search: "Rechercher" - empty-draghover: "Drop Welcome!" - empty-drive: "Votre Drive est vide" - empty-drive-description: "Vous pouvez également téléverser le fichier en faisant un clic droit et en choisissant « Téléverser » ou tout simplement en faisant glisser votre fichier." - empty-folder: "Ce dossier est vide" - unable-to-process: "L'opération n'a pas pu être complétée" - circular-reference-detected: "Le dossier de destination est un sous-dossier du dossier que vous souhaitez déplacer." - unhandled-error: "Erreur inconnue" - url-upload: "Téléverser via une URL" - url-of-file: "URL de l'image que vous souhaitez téléverser." - url-upload-requested: "Téléversement demandé" - may-take-time: "Le téléversement de votre fichier peut prendre un certain temps." - create-folder: "Créer un dossier" - folder-name: "Nom du dossier" - contextmenu: - create-folder: "Créer un dossier" - upload: "Téléverser un fichier" - url-upload: "Téléverser à partir d’une URL" -desktop/views/components/media-video.vue: - sensitive: "Le contenu est NSFW" - click-to-show: "Cliquer pour afficher" -desktop/views/components/followers-window.vue: - followers: "Abonné·e·s de {}" -desktop/views/components/followers.vue: - empty: "Il semble que vous n’avez pas encore d’abonné·e·s." -desktop/views/components/following-window.vue: - following: "Suit {}" -desktop/views/components/following.vue: - empty: "Vous ne suivez aucun compte." -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "Envoyer" - add-widget: "Ajouter un widget" - add: "Ajouter" -desktop/views/input-dialog.vue: - cancel: "Annuler" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "cette publication est privée" - deleted: "cette publication a été supprimée" - location: "Géolocalisation" - renote: "Republier" - add-reaction: "Ajouter une réaction" - undo-reaction: "Annuler la réaction" -desktop/views/components/note.vue: - reply: "Répondre" - renote: "Partager" - add-reaction: "Ajouter votre réaction" - undo-reaction: "Inverser la réaction" - detail: "Détails" - private: "Cette publication est privée" - deleted: "Cette publication a été supprimée" -desktop/views/components/notes.vue: - error: "Échec du chargement." - retry: "Réessayer" -desktop/views/components/notifications.vue: - empty: "Aucune de notification !" -desktop/views/components/post-form.vue: - posted: "Publié !" - replied: "Répondu !" - reposted: "Reposté !" - note-failed: "La note à échoué" - reply-failed: "La réponse a échoué" - renote-failed: "Échec lors de la republication" -desktop/views/components/post-form-window.vue: - note: "Nouvelle note" - reply: "Répondre" - attaches: "{} media joint(s)" - uploading-media: "Téléversement du média {}" -desktop/views/components/progress-dialog.vue: - waiting: "En attente" -desktop/views/components/renote-form.vue: - quote: "Citer..." - cancel: "Annuler" - renote: "Republier" - renote-home: "Renote (accueil)" - reposting: "Republication en cours…" - success: "Republié !" - failure: "La renote a échoué" -desktop/views/components/renote-form-window.vue: - title: "Êtes vous sûr de vouloir renote cette note?" -desktop/views/pages/user-following-or-followers.vue: - following: "{user} suit" - followers: "Abonné·e·s de {user}" -desktop/views/components/settings.2fa.vue: - intro: "Si vous configurez la vérication en deux étapes vous aurez non seulement besoin de votre mot de passe mais aussi un appareil déjà pré-enregistré(tel que votre smartphone) ce qui ameliora grandement la sécurité de votre compte." - detail: "Voir les détails..." - url: "https://www.google.com/landing/2step/" - caution: "Activer la vérification en deux étapes vient aussi avec des contraintes, si vous perdez votre appareil ou ne pouvez tout simplement plus y accéder vous ne serez plus en mesure de vous connecter à Misskey." - register: "Enregistrer un appareil" - already-registered: "Cette étape à déjà été complétée" - unregister: "Désactiver" - unregistered: "L'authentification à deux facteurs a été désactivée." - enter-password: "Entrez un mot de passe" - authenticator: "Vous devez au préalable installer Google Authenticator sur votre appareil :" - howtoinstall: "Comment installer" - token: "Jeton" - scan: "Ensuite, scannez le code QR affiché sur votre écran :" - done: "Veuillez entrer le token qui s'affiche sur votre appareil :" - submit: "Envoyer" - success: "Sauvegarde des paramètres avec succès !" - failed: "L’opération a échoué. Veuillez vous assurer que le jeton a été saisi correctement." - info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil." - totp-header: "Application d'authentification" - security-key-header: "Clé de sécurité" - security-key: "Pour plus de sécurité, vous pouvez vous connecter à votre compte à l'aide d'une clé de sécurité matérielle qui prend en charge FIDO2. Lorsque vous vous connecterez, vous aurez besoin de la clé de sécurité enregistrée ou d'une application d'authentification avec vous." - last-used: "Dernière utilisation :" - activate-key: "Cliquez pour activer la clé de sécurité" - security-key-name: "Nom de la clé" - something-went-wrong: "Oula ! Il y a eu un problème lors de l’enregistrement de la clé." - key-unregistered: "La clé a été supprimée" - use-password-less-login: "Utiliser une connexion sans mot de passe" -common/views/components/media-image.vue: - sensitive: "Contenu sensible" - click-to-show: "Cliquer pour afficher" -common/views/components/api-settings.vue: - intro: "Pour accéder à l'API, définissez ce jeton comme la clé de « i » dans les paramètres de requête." - caution: "Merci de ne pas introduire ce jeton dans aucune application ou le divulguer à quiconque. Ceci risque de compromettre votre compte." - regeneration-of-token: "Si votre jeton est compromis, vous pouvez le régénérer." - regenerate-token: "Régénérer le jeton" - token: "Jeton :" - enter-password: "Entrez le mot de passe" - console: - title: "Console API" - endpoint: "Point de terminaison" - parameter: "Paramètres" - credential-info: "Le paramètre « i » est requis dans la console." - send: "Envoyer" - sending: "Envoi en cours" - response: "Résultat" -desktop/views/components/settings.apps.vue: - no-apps: "Aucune application autorisée" -common/views/components/drive-settings.vue: - max: "Maximale" - in-use: "utilisé" - stats: "Statistiques" - default-upload-folder: "Emplacement par défaut du dossier de transfert" - default-upload-folder-name: "Dossier·s" - change-default-upload-folder: "Changer de dossier" -common/views/components/mute-and-block.vue: - mute-and-block: "Masqués / Bloqués" - mute: "Mis en sourdine" - block: "Vous avez bloqué" - no-muted-users: "Aucun utilisateur n’est mis en sourdine" - no-blocked-users: "Aucun utilisateur n’est bloqué" - word-mute: "Filtre de mots" - muted-words: "Mots masqués" - muted-words-description: "Description des mots mis en sourdine" - unmute-confirm: "Ne plus masquer cet utilisateur ?" - unblock-confirm: "Débloquer cet utilisateur ?" - save: "Enregistrer" -common/views/components/password-settings.vue: - reset: "Modifier le mot de passe" - enter-current-password: "Entrez votre mot de passe actuel" - enter-new-password: "Saisissez le nouveau mot de passe" - enter-new-password-again: "Entrez à nouveau le nouveau mot de passe" - not-match: "Les nouveaux mots de passe ne sont pas identiques" - changed: "Mot de passe modifié avec succès" - failed: "Échec lors de la modification du mot de passe" -common/views/components/post-form-attaches.vue: - attach-cancel: "Enlever le fichier attaché" - mark-as-sensitive: "Marquer comme sensible" - unmark-as-sensitive: "Ne pas marquer comme sensible" -desktop/views/components/sub-note-content.vue: - private: "cette publication est privée" - deleted: "cette publication a été supprimée" - media-count: "{} médias attachés" - poll: "Sondage" -desktop/views/components/settings.tags.vue: - title: "Étiquettes" - query: "Requête (optionnelle)" - add: "Ajouter" - save: "Enregistrer" -desktop/views/components/timeline.vue: - home: "Accueil" - local: "Local" - hybrid: "Social" - global: "Global" - mentions: "Mentions" - messages: "Messages directs" - list: "Listes" - hashtag: "Hashtag" - add-tag-timeline: "Ajouter un fil de hashtags" - add-list: "Ajouter une nouvelle liste" - list-name: "Nom de la liste" -desktop/views/components/ui.header.vue: - welcome-back: "Content de vous revoir !" - adjective: "M." -desktop/views/components/ui.header.account.vue: - profile: "Votre profil" - lists: "Listes" - groups: "Groupes" - follow-requests: "Demandes d’abonnement" - admin: "Admin" - room: "Pièce" -desktop/views/components/ui.header.nav.vue: - game: "Jeux" -desktop/views/components/ui.header.notifications.vue: - title: "Notifications" -desktop/views/components/ui.header.post.vue: - post: "Rédiger une nouvelle publication" -desktop/views/components/ui.header.search.vue: - placeholder: "Chercher" -desktop/views/components/user-preview.vue: - notes: "Publications" - following: "Abonné à " - followers: "Abonné·e·s" -desktop/views/components/users-list.vue: - all: "Tout" - iknow: "Vous connaissez" - fetching: "Chargement..." -desktop/views/components/users-list-item.vue: - followed: "vous suit" -desktop/views/components/window.vue: - popout: "Fenêtre contextuelle" - close: "Fermer" -admin/views/index.vue: - dashboard: "Tableau de bord" - instance: "Instance" - emoji: "Émoji" - moderators: "Modérateurs" - users: "Utilisateur·rice·s" - federation: "Fédération" - announcements: "Annonces" - abuse: "Abus" - queue: "File d’attente" - logs: "Journaux" - db: "Base de données" - back-to-misskey: "Retour vers Misskey" -admin/views/db.vue: - tables: "Tables" - vacuum: "Vacuum" - vacuum-info: "Range la base de données. Conserve les données intactes et réduit l'utilisation du disque. Cela se fait généralement automatiquement et périodiquement." -admin/views/dashboard.vue: - dashboard: "Tableau de bord" - accounts: "Comptes" - notes: "Notes" - drive: "Lecteur" - instances: "Instances" - this-instance: "Cette instance" - federated: "Fédérées" -admin/views/queue.vue: - title: "File d'attente" - remove-all-jobs: "Enlever toutes les tâches en attente" - jobs: "Tâches" - queue: "File d'attente" - domains: - deliver: "Délivrées" - inbox: "Reçues" - db: "Base de données" - objectStorage: "Stockage d'objets" - state: "État" - states: - active: "en cours" - delayed: "Programmé" - waiting: "En file d'attente" - result-is-truncated: "Le résultat est tronqué" - other-queues: "Autres files d’attente" -admin/views/logs.vue: - logs: "Journaux" - domain: "Domaine" - level: "Niveau" - levels: - all: "Tous" - info: "Informations" - success: "Succès" - warning: "Avertissement" - error: "Erreur" - debug: "Débogage" - delete-all: "Effacer tout" -admin/views/abuse.vue: - title: "Abus" - target: "Cible" - reporter: "Signalé par" - details: "Détails" - remove-report: "Supprimer" -admin/views/instance.vue: - instance: "Instance" - instance-name: "Nom de l’instance" - instance-description: "Description de l’instance" - host: "Hôte" - icon-url: "URL de l'icône" - logo-url: "URL do logo" - banner-url: "URL de l’image de la bannière" - error-image-url: "URL de l’image d’erreur" - languages: "Langue de l’instance" - languages-desc: "Vous pouvez en définir plus d’une, séparées par des espaces." - tos-url: "URL des conditions d'utilisation" - repository-url: "URL du dépôt" - feedback-url: "URL pour les commentaires" - maintainer-config: "Informations de l’administrateur" - maintainer-name: "Nom de l’administrateur" - maintainer-email: "Contact administratif" - advanced-config: "Autres réglages" - note-and-tl: "Notes et fils" - drive-config: "Paramètres du lecteur" - use-object-storage: "Utiliser le stockage d'objets" - object-storage-base-url: "URL" - object-storage-prefix: "Préfixe" - object-storage-endpoint: "Point de terminaison" - object-storage-region: "Région" - object-storage-port: "Port" - object-storage-access-key: "Clé d'accès" - object-storage-secret-key: "Clé secrète" - object-storage-use-ssl: "Utiliser SSL" - object-storage-s3-info-here: "ici" - cache-remote-files: "Mettre en cache des fichiers distants" - local-drive-capacity-mb: "Volume du lecteur par utilisateur" - remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant" - mb: "en mégaoctets" - recaptcha-config: "Paramètres de reCAPTCHA" - recaptcha-info: "Si activé, un jeton reCAPTCHA est requis. Vous pouvez en obtenir un sur https://www.google.com/recaptcha/intro/" - recaptcha-info2: "v3 n'est pas supportée. Veuillez utiliser v2." - enable-recaptcha: "Activation de reCAPTCHA" - recaptcha-site-key: "Clé du site" - recaptcha-secret-key: "Clé secrète" - recaptcha-preview: "Prévisualisation" - hidden-tags: "Tags cachés" - external-service-integration-config: "Services connectés" - twitter-integration-config: "Paramètres de connexion à Twitter" - twitter-integration-info: "L'URL de callback est {url}." - enable-twitter-integration: "Activer la connexion à Twitter" - twitter-integration-consumer-key: "Clé du consommateur" - twitter-integration-consumer-secret: "Secret du consommateur" - github-integration-config: "Paramètres d’authentification GitHub" - github-integration-info: "L'URL de callback est {url}." - enable-github-integration: "Activer l’authentification avec Github" - github-integration-client-id: "ID client" - github-integration-client-secret: "Secret client" - discord-integration-config: "Paramètres d’authentification Discord" - discord-integration-info: "L'URL de callback est {url}." - enable-discord-integration: "Activer l’authentification avec Discord" - discord-integration-client-id: "ID client" - discord-integration-client-secret: "Secret client" - proxy-account-config: "Compte proxy" - proxy-account-info: "Un compte proxy se comporte, dans certaines conditions, comme un·e abonné·e distant pour les utilisateurs d'autres instances.\nExemple : quand un·e utilisateur·rice distant·e est ajouté·e à une liste, ses publications ne serait pas visibles sur l'instance si personne ne le·la suit. Le compte proxy va donc le·la suivre pour que ses publications soient acheminées." - proxy-account-username: "Nom d’utilisateur du compte proxy" - proxy-account-username-desc: "Spécifiez le nom d’utilisateur du compte utilisé comme proxy." - proxy-account-warn: "Avant d’entamer cette action, vous devez au préalable avoir créé un compte avec ce nom d’utilisateur." - max-note-text-length: "Nombre maximal de caractères pour les messages" - disable-registration: "Désactiver les inscriptions" - disable-local-timeline: "Désactiver le fil local" - disable-global-timeline: "Désactiver le fil global" - disabling-timelines-info: "Même si vous désactivez ces fils, l'administrateur et les modérateurs peuvent continuer à les utiliser." - enable-emoji-reaction: "Activer les pictogrammes dans les réactions" - use-star-for-reaction-fallback: "Utiliser une étoile si une réaction est inconnue" - invite: "Inviter" - save: "Sauvegarder" - saved: "Enregistré" - pinned-users: "Utilisateur·rice épinglé·e" - email-config: "Paramètres du serveur de messagerie" - email-config-info: "Utilisé pour confirmer votre adresse de courrier électronique et la réinitialisation de votre mot de passe." - enable-email: "Activation de la distribution du courrier" - email: "Adresse de courrier électronique" - smtp-secure: "Utiliser SSL/TLS implicitement dans la connexion SMTP" - smtp-secure-info: "Désactiver STARTTLS lorsque celui-ci est utilisé." - smtp-host: "Hôte SMTP" - smtp-port: "Port SMTP" - smtp-auth: "Effectuer une authentification SMTP" - smtp-user: "Utilisateur SMTP" - smtp-pass: "Mot de passe SMTP" - test-email: "Test" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "Activer ServiceWorker" - serviceworker-info: "Devrait être activé pour les notifications push." - vapid-publickey: "Clé Publique VAPID" - vapid-privatekey: "Clé privée VAPID" - vapid-info: "Vous devez activer ServiceWorker pour pouvoir générer les clés VAPID. Vous devez lancer ceci en mode root :" -admin/views/charts.vue: - title: "Graphe" - per-day: "par jour" - per-hour: "par heure" - federation: "Fédération" - notes: "Publications" - users: "Utilisateur·rice·s" - drive: "Lecteur" - network: "Réseau" - charts: - federation-instances: "Nombre d’instances : augmentation/diminution" - federation-instances-total: "Nombre total d’instances" - notes: "Nombre de publications : augmentation/diminution (combinés)" - local-notes: "Nombre des publications : augmentation/diminution (Local)" - remote-notes: "Nombre de publications : augmentation/diminution (distants)" - notes-total: "Total des notes" - users: "Nombre d’utilisateur·rice·s : augmentation/diminution" - users-total: "Nombre total des utilisateur·rice·s" - active-users: "Utilisateur·rice·s actif·ve·s" - drive: "Capacité utilisée comme stockage : augmentation/diminution" - drive-total: "Utilisation totale du lecteur" - drive-files: "Le nombre de fichiers sur l'espace de stockage : augmentation/diminution" - drive-files-total: "Nombre total de fichiers sur le lecteur" - network-requests: "Requêtes" - network-time: "Temps de réponse" - network-usage: "Traffic" -admin/views/drive.vue: - operation: "Actions" - fileid-or-url: "ID du fichier ou URL" - file-not-found: "Fichier non trouvé" - lookup: "Recherche" - sort: - title: "Tri" - createdAtAsc: "Âge - Du plus ancien" - createdAtDesc: "Âge - Du plus récent" - sizeAsc: "Taille - Ascendant" - sizeDesc: "Taille - Volumineux en premier" - origin: - title: "Origine" - combined: "Locaux et distants combinés" - local: "Local" - remote: "Distant" - delete: "Supprimer" - deleted: "Supprimé" - mark-as-sensitive: "Marquer comme sensible" - unmark-as-sensitive: "Ne pas marquer comme sensible" - marked-as-sensitive: "Marqué comme sensible" - unmarked-as-sensitive: "Marqué comme non sensible" - clean-remote-files: "Nettoyer le cache des fichiers distants" - clean-remote-files-are-you-sure: "Êtes-vous sûr de vouloir effacer tout les fichiers distants mis en cache ?" - clean-up: "Nettoyage" -admin/views/users.vue: - operation: "Actions" - username-or-userid: "Nom d’utilisateur·rice ou ID utilisateur" - user-not-found: "Utilisateur non trouvé" - lookup: "Recherche" - reset-password: "Réinitialiser mot de passe" - reset-password-confirm: "Souhaitez-vous réinitialiser votre mot de passe ?" - password-updated: "Le mot de passe est « {password} »" - suspend: "Suspendre" - suspend-confirm: "Désirez-vous suspendre ce compte ?" - suspended: "Suspendu avec succès." - unsuspend: "Suspension levée" - unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?" - unsuspended: "La suspension de l’utilisateur a été levée avec succès" - make-silence: "Mettre en sourdine" - silence-confirm: "Mettre l'utilisateur sous silence ?" - unmake-silence: "Enlever la sourdine" - update-remote-user: "Mettre à jour les informations de l’utilisateur·rice distant·e" - remote-user-updated: "Les informations de l’utilisateur·rice distant·e ont étés mis à jour" - delete-all-files: "Supprimer tous les fichiers" - delete-all-files-confirm: "Êtes vous surs de vouloir supprimer tous les fichiers ?" - username: "Nom d'utilisateur·rice" - host: "Hôte" - users: - title: "Utilisateur·rice·s" - sort: - title: "Trier par" - createdAtAsc: "Date d’inscription (Ascendant)" - createdAtDesc: "Date d’inscription (Descendant)" - updatedAtAsc: "Mis à jour récemment (Ascendant)" - updatedAtDesc: "Mis à jour récemment (descendant)" - state: - title: "État" - all: "Tout" - available: "Disponible" - admin: "Admin" - moderator: "Modérateur" - adminOrModerator: "Administrateur/Modérateur" - silenced: "Déjà mis en sourdine" - suspended: "Suspendu" - origin: - title: "Origine" - combined: "Locaux + distants" - local: "Locaux" - remote: "Distants" - createdAt: "Créé le" - updatedAt: "Mis à jour le" -admin/views/moderators.vue: - add-moderator: - title: "Ajout d’un modérateur" - add: "Ajouter" - added: "Ajouté en tant que modérateur" - remove: "Révoquer" - removed: "Le modérateur a été révoqué" - logs: - title: "Journaux" - moderator: "Modérateurs" - type: "Actions" - at: "Date de modification" - info: "Informations" -admin/views/emoji.vue: - add-emoji: - title: "Ajouter un émoji" - name: "Nom de l’émoji" - name-desc: "Vous pouvez utiliser les caractères a~z 0~9 _" - category: "Catégories" - aliases: "Aliases" - aliases-desc: "Vous pouvez définir plus d’un, séparés par des espaces." - url: "URL de l’image" - add: "Ajouter" - info: "Nous recommandons l’usage d’images PNG moins de 50 Ko." - added: "Émoji ajouté avec succès" - emojis: - title: "Émojis" - update: "Mise à jour" - remove: "Supprimer" - updated: "À été mis à jour" - remove-emoji: - are-you-sure: "Supprimer « %1$s » ?" - removed: "Supprimé" -admin/views/announcements.vue: - announcements: "Annonces" - save: "Enregistrer" - remove: "Supprimer" - add: "Ajouter" - title: "Titre" - text: "Contenu" - saved: "Sauvegardé" - _remove: - are-you-sure: "Supprimer « %1$s » ?" - removed: "Supprimé" -admin/views/hashtags.vue: - hided-tags: "Tags cachés" -admin/views/federation.vue: - instance: "Instance" - host: "Hôte" - notes: "Notes" - users: "Utilisateur·rice·s" - following: "Abonnements" - followers: "Abonné·e·s" - caught-at: "Créé le" - status: "Statuts" - latest-request-sent-at: "Dernière requête envoyée" - latest-request-received-at: "Dernière requête reçue" - remove-all-following-info: "Se désabonner de tous les comptes de {host}. Exécutez cette commande si l'instance n'existe plus." - delete-all-files: "Supprimer tous les fichiers" - block: "Bloquer" - marked-as-closed: "Marquées comme fermées" - lookup: "Recherche" - instances: "Fédérées" - instance-not-registered: "L’instance n’a pas encore été découverte" - sort: "Trier par" - sorts: - caughtAtAsc: "Date d’inscription (Ascendant)" - caughtAtDesc: "Date d’inscription (Descendant)" - lastCommunicatedAtAsc: "La date et l'heure des interactions plus anciennes" - lastCommunicatedAtDesc: "La date et l'heure des nouvelles interactions" - notesDesc: "Description des notes" - usersAsc: "Peu d'abonné·e·s" - followingAsc: "Les moins suivies" - followingDesc: "Ayant le plus d'abonné·e·s" - followersAsc: "Ayant le moins d'abonné·e·s" - followersDesc: "Ayant le plus d'abonné·e·s" - driveUsageAsc: "Moins d'espace de stockage utilisé" - state: "État" - states: - all: "Tout" - blocked: "Bloquées" - not-responding: "Sans réponse" - marked-as-closed: "Marquée comme fermée" - charts: "Graphs" - chart-srcs: - requests: "Requêtes" - users: "Nombre d’utilisateur·trice·s : augmentation/diminution" - users-total: "Nombre total des utilisateur·rice·s" - notes: "Augmentation/diminution du nombre des notes" - notes-total: "Nombre total des notes" - ff: "Augmentation des abonné·e·s" - ff-total: "Nombre total d'abonnements" - drive-usage: "Augmentation et diminution de la capacité stockage" - drive-usage-total: "Utilisation totale du stockage" - drive-files-total: "Nombre total des fichiers sur le Drive" - chart-spans: - hour: "Par heure" - day: "Par jour" - blocked-hosts: "En cours blocage" - save: "Enregistrer" -desktop/views/pages/welcome.vue: - about: "à propos" - timeline: "Fil d’actualité" - announcements: "Notices" - photos: "Images récentes" - powered-by-misskey: "Propulsé par <b>Misskey</b>." - info: "Informations" -desktop/views/pages/drive.vue: - title: "Lecteur de Misskey" -desktop/views/pages/note.vue: - prev: "Note précédente" - next: "Note suivante" -desktop/views/pages/selectdrive.vue: - title: "Choisir fichier(s)" - ok: "OK" - cancel: "Annuler" - upload: "Téléverser des fichiers à partir de votre ordinateur" -desktop/views/pages/search.vue: - not-available: "La fonction de recherche est désactivée dans les paramètres de l’instance." - not-found: "Aucune publication trouvée pour « {q} »." -desktop/views/pages/tag.vue: - no-posts-found: "Aucune publication contenant « {q} » n’a été trouvée." -desktop/views/pages/user-list.users.vue: - users: "Utilisateur·rice·s" - add-user: "Ajouter un utilisateur" - username: "Nom d'utilisateur" -desktop/views/pages/user/user.followers-you-know.vue: - title: "Abonné·e·s que vous connaissez" - loading: "Chargement en cours" - no-users: "Aucun·e abonné·e connu·e" -desktop/views/pages/user/user.friends.vue: - title: "Mentions fréquentes" - loading: "Chargement en cours" - no-users: "Aucune mention fréquente" -desktop/views/pages/user/user.photos.vue: - title: "Photos" - loading: "Chargement en cours" - no-photos: "Pas de photos" -desktop/views/pages/user/user.header.vue: - posts: "Notes" - following: "Suit" - followers: "Abonné·e·s" - is-bot: "Ce compte est un Bot" - no-description: "L'utilisateur n'a pas renseigné d'introduction sur son profile" - years-old: "{age} ans" - year: "/" - month: "/" - day: "-" - follows-you: "Vous suit" -desktop/views/pages/user/user.timeline.vue: - default: "Publications" - with-replies: "Publications et réponses" - with-media: "Média" - my-posts: "Mes Messages" -desktop/views/widgets/notifications.vue: - title: "Notifications" -desktop/views/widgets/polls.vue: - title: "Sondages" - refresh: "Afficher d'autres" - nothing: "Rien" -desktop/views/widgets/post-form.vue: - title: "Publication" - note: "Publication" -desktop/views/widgets/profile.vue: - update-banner: "Cliquer pour éditer votre bannière" - update-avatar: "Cliquer pour éditer votre avatar" -desktop/views/widgets/trends.vue: - title: "Tendances" - refresh: "Afficher d'autres" - nothing: "Rien" -desktop/views/widgets/users.vue: - title: "Utilisateurs·rices" - refresh: "Afficher d'autres" - no-one: "Personne" -mobile/views/components/drive.vue: - used: "utilisé" - folder-count: "Dossier·s" - count-separator: ", " - file-count: "Fichier·s" - nothing-in-drive: "Rien" - folder-is-empty: "Ce dossier est vide" - folder-name: "Nom du dossier" - here-is-root: "Actuellement, vous êtes dans la racine et non pas dans un dossier." - url-prompt: "URL du fichier que vous souhaitez téléverser" - uploading: "Envoi demandé. Le téléversement pourrait prendre un certain temps avant de s'achever." - folder-name-cannot-empty: "Le nom du dossier ne peut être laissé vide." -mobile/views/components/drive-file-chooser.vue: - select-file: "Choisissez un fichier" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "Choisissez un dossier" -mobile/views/components/drive.file.vue: - nsfw: "CW" -mobile/views/components/drive.file-detail.vue: - download: "Télécharger" - rename: "Renommer" - move: "Déplacer" - hash: "Hash (md5)" - exif: "EXIF" - nsfw: "CW" - mark-as-sensitive: "Marquer comme sensible" - unmark-as-sensitive: "Ne pas marquer comme sensible" -mobile/views/components/media-video.vue: - sensitive: "Le contenu est NSFW" - click-to-show: "Cliquer pour afficher" -common/views/components/follow-button.vue: - following: "Abonné·e" - follow: " Suivre" - request-pending: "Demande en attente" - follow-processing: "En cours d’abonnement" - follow-request: "Demande d’abonnement" -mobile/views/components/note.vue: - private: "cette publication est privée" - deleted: "cette publication a été supprimée" - location: "Géolocalisation" -mobile/views/components/note-detail.vue: - reply: "Répondre" - reaction: "Réaction" - private: "cette publication est privée" - deleted: "cette publication a été supprimée" - location: "Lieu" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "chat" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "chat" -mobile/views/components/notifications.vue: - empty: "Aucune de notification !" -mobile/views/components/sub-note-content.vue: - private: "cette publication est privée" - deleted: "cette publication a été supprimée" - media-count: "{} médias attachés" - poll: "Sondage" -mobile/views/components/ui.header.vue: - welcome-back: "Content de vous revoir ! " - adjective: "M." -mobile/views/components/ui.nav.vue: - timeline: "Fil d’actualité" - notifications: "Notifications" - follow-requests: "Demandes d’abonnement" - search: "Rechercher" - user-lists: "Listes" - user-groups: "Groupe" - widgets: "Modules" - game: "Jeux" - admin: "Admin" - about: "À propos de Misskey" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Téléverser un fichier" - url-upload: "Transférer un fichier depuis une URL" - create-folder: "Créer un dossier" - rename-folder: "Renommer le dossier" - move-folder: "Déplacer ce dossier" - delete-folder: "Supprimer ce dossier" -mobile/views/pages/signup.vue: - lets-start: "Votre compte est prêt ! 📦" -mobile/views/pages/followers.vue: - followers-of: "Abonné·e·s de {name}" -mobile/views/pages/following.vue: - following-of: "Abonné·e·s de {name}" -mobile/views/pages/home.vue: - home: "Accueil" - local: "Local" - hybrid: "Social" - global: "Global" - mentions: "Mentions" - messages: "Messages directs" -mobile/views/pages/tag.vue: - no-posts-found: "Aucune publication ayant pour hashtag « {q} » n’a été trouvée." -mobile/views/pages/widgets.vue: - dashboard: "Tableau de bord" - add-widget: "Ajouter" - customization-tips: "Conseils de personnalisation" -mobile/views/pages/widgets/activity.vue: - activity: "Activité" -mobile/views/pages/share.vue: - share-with: "Partager avec {name}" -mobile/views/pages/note.vue: - title: "Publication" - prev: "Note précédente" - next: "Note suivante" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "Chercher" - not-found: "Aucune publication trouvée pour « {q} »." -mobile/views/pages/selectdrive.vue: - select-file: "Choisissez un fichier" -mobile/views/pages/notifications.vue: - notifications: "Notifications" -mobile/views/pages/settings.vue: - signed-in-as: "Connecté·e en tant que {}" -mobile/views/pages/user.vue: - follows-you: "Vous suit" - following: "Abonnements" - followers: "Abonné·e·s" - notes: "Notes" - overview: "Aperçu" - timeline: "Fil d’actualité" - media: "Média" - years-old: "{age} ans" -mobile/views/pages/user/home.vue: - recent-notes: "Notes récentes" - images: "Images" - activity: "Activité" - keywords: "Mot clés" - domains: "Domaines" - frequently-replied-users: "Mentions fréquentes" - followers-you-know: "Abonné·e·s que vous connaissez" - last-used-at: "Dernière connexion il y a" -mobile/views/pages/user/home.photos.vue: - no-photos: "Pas de photos" -deck: - widgets: "Widgets" - home: "Principal" - local: "Local" - hybrid: "Social" - hashtag: "Hashtags" - global: "Global" - mentions: "Mentions" - direct: "Messages directs" - notifications: "Notifications" - list: "Listes" - select-list: "Sélectionnez une liste" - swap-left: "Déplacer à gauche" - swap-right: "Déplacer à droite" - swap-up: "Déplacer vers le haut" - swap-down: "Déplacer vers le bas" - remove: "Supprimer la colonne" - add-column: "Ajouter une colonne" - rename: "Renommer" - stack-left: "Empiler à gauche" - pop-right: "Vers la droite" - disabled-timeline: - title: "Le fil été désactivé" - description: "Ce fil a été désactivé par l'administrateur du serveur." -deck/deck.tl-column.vue: - is-media-only: "Les publications médias uniquement" - edit: "Option" -deck/deck.user-column.vue: - follows-you: "Vous suit" - posts: "Notes" - following: "Suit" - followers: "Abonné·e·s" - images: "Images" - activity: "Activité" - timeline: "Fil d’actualité" - pinned-notes: "Notes épinglées" - pinned-page: "Page épinglée" -docs: - edit-this-page-on-github: "Vous avez trouvé une erreur ou vous voulez contribuer à la documentation ?" - edit-this-page-on-github-link: "Éditez cette page sur GitHub !" -dev/views/index.vue: - manage-apps: "Gestion des applications" -dev/views/apps.vue: - manage-apps: "Gestion des applications" - create-app: "Créer une app" - app-missing: "Aucune application" -dev/views/new-app.vue: - new-app: "Nouvelle application" - new-app-info: "Vous pouvez aussi créer une application avec l'API. (app/create)" - create-app: "Création d’une application" - app-name: "Nom de l’application" - app-name-placeholder: "p. ex. Misskey pour iOS" - app-name-desc: "Le nom de votre application" - app-overview: "Description courte de l’application" - app-overview-placeholder: "p. ex. Misskey pour iOS" - app-overview-desc: "Brève description introductive à votre application." - callback-url: "L’Url de callback (facultatif)" - callback-url-placeholder: "p. ex. https://votre.app.example.com/callback.php" - callback-url-desc: "Vous pouvez définir l’URL de redirection lorsque l’utilisateur s’est authentifié via formulaire d’authentification." - authority: "Autorisations " - authority-desc: "Sont accessibles via l’API, uniquement les fonctionnalités demandées ici." - authority-warning: "Vous pouvez le changer même après avoir créé l'application, mais si vous attribuez une nouvelle permission, toutes les clés utilisateur associées seront dès lors invalides." -pages: - new-page: "Créer une page" - edit-page: "Modifier une page" - read-page: "Voir la source" - page-created: "Page a été créée !" - page-updated: "A mis à jour la page" - name-already-exists: "Une page portant le même nom existe déjà " - title-invalid-name: "L’URL de la page spécifiée n’est pas valide" - are-you-sure-delete: "Confirmez-vous la suppression de cette page ?" - page-deleted: "La page a bien été supprimée." - edit-this-page: "Éditer cette page" - pin-this-page: "Épingler sur votre profil" - unpin-this-page: "Désépingler" - view-source: "Afficher la source" - view-page: "Afficher la page" - like: "Bien" - unlike: "Je n’aime pas" - liked-pages: "Pages favorites" - my-pages: "Mes pages" - inspector: "Inspecteur" - content: "Bloc de page" - variables: "Variables" - more-details: "Description" - title: "Titre" - url: "URL de page" - summary: "Résumé de page" - align-center: "Centrée" - hide-title-when-pinned: "Masquer le titre de la page lorsque celle-ci est épinglée au profil" - font: "Police de caractères" - fontSerif: "Serif" - fontSansSerif: "Sans Serif" - set-eye-catching-image: "Définir une image attirante" - remove-eye-catching-image: "Supprimer une image attirante" - choose-block: "Ajouter un bloc" - select-type: "Choisir un type" - enter-variable-name: "Veuillez choisir un nom de variable" - the-variable-name-is-already-used: "Cette variable est déjà utilisée" - content-blocks: "Contenu du cadre" - input-blocks: "Entrée" - special-blocks: "Spécial" - post-from-post-form: "Publier ce contenu" - posted-from-post-form: "Publié !" - blocks: - text: "Texte" - textarea: "Zone de texte" - section: "Section" - image: "Images" - button: "Bouton" - if: "Si" - _if: - variable: "Variables" - post: "Champs de publication" - _post: - text: "Contenu" - textInput: "Entrée textuelle" - _textInput: - name: "Nom de la variable" - text: "Titre" - default: "Valeur par défaut" - _textareaInput: - name: "Nom de la variable" - text: "Titre" - default: "Valeur par défaut" - numberInput: "Entrée numérique" - _numberInput: - name: "Nom de la variable" - text: "Titre" - default: "Valeur par défaut" - switch: "Basculer" - _switch: - name: "Nom de la variable" - text: "Titre" - default: "Valeur par défaut" - counter: "Compteur" - _counter: - name: "Nom de la variable" - text: "Titre" - inc: "Augmenter le chiffre" - _button: - text: "Titre" - colored: "Couleur" - action: "L'opération lorsque le bouton sera pressé" - _action: - dialog: "Afficher une fenêtre de dialogue" - _dialog: - content: "Contenu" - resetRandom: "Réinitialiser le nombre aléatoire" - pushEvent: "Envoyer un évènement" - _pushEvent: - event: "Nom de l'évènement" - message: "Message à afficher lorsque appuyé" - variable: "Variable à envoyer" - no-variable: "Aucune" - radioButton: "Choix" - _radioButton: - name: "Nom de la variable" - title: "Titre" - default: "Valeur par défaut" - script: - categories: - flow: "Contrôle" - logical: "Opération logique" - operation: "Calculer" - comparison: "Comparer" - random: "Aléatoire" - value: "Valeur" - fn: "Fonction" - text: "Actions texte" - convert: "Convertir" - list: "Listes" - blocks: - text: "Texte" - multiLineText: "Texte (Multi-lignes)" - textList: "Liste de texte" - strLen: "Longueur du texte" - _strLen: - arg1: "Texte" - strPick: "Extraire un caractère" - _strPick: - arg1: "Texte" - arg2: "Position du joueur" - strReplace: "Remplacement de texte" - _strReplace: - arg1: "Texte" - arg2: "Avant le remplacement" - arg3: "Après le remplacement" - strReverse: "Inverser le texte" - _strReverse: - arg1: "Texte" - _join: - arg1: "Listes" - arg2: "Séparateur" - add: "+ Plus" - _add: - arg1: "A" - arg2: "B" - subtract: "- Moins" - _subtract: - arg1: "A" - arg2: "B" - multiply: "× Multiplier par" - _multiply: - arg1: "A" - arg2: "B" - divide: "÷ Diviser par" - _divide: - arg1: "A" - arg2: "B" - _mod: - arg1: "A" - arg2: "B" - _round: - arg1: "Numérique" - eq: "A et B sont équivalents" - _eq: - arg1: "A" - arg2: "B" - notEq: "A et B sont différents" - _notEq: - arg1: "A" - arg2: "B" - and: "A et B" - _and: - arg1: "A" - arg2: "B" - or: "A ou B" - _or: - arg1: "A" - arg2: "B" - lt: "A est plus petit que B" - _lt: - arg1: "A" - arg2: "B" - gt: "A est supérieur à B" - _gt: - arg1: "A" - arg2: "B" - ltEq: "A est plus petit ou égal à B" - _ltEq: - arg1: "A" - arg2: "B" - gtEq: "A est supérieur ou égal à B" - _gtEq: - arg1: "A" - arg2: "B" - if: "Branche" - _if: - arg1: "Si" - arg2: "donc" - arg3: "sinon" - not: "négation" - _not: - arg1: "négation" - random: "Aléatoire" - _random: - arg1: "Probabilité" - rannum: "Nombre aléatoire" - _rannum: - arg1: "Minimum" - arg2: "Maximum" - randomPick: "Choisir aléatoirement depuis la liste" - _randomPick: - arg1: "Listes" - dailyRandom: "Aléatoire (Quotidien pour chaque utilisateur)" - _dailyRandom: - arg1: "Probabilité" - _dailyRannum: - arg1: "Minimum" - arg2: "Maximum" - _dailyRandomPick: - arg1: "Listes" - seedRandom: "Aléatoire (graine)" - _seedRandom: - arg1: "Graine" - arg2: "Probabilité" - seedRannum: "Nombre aléatoire (Graine)" - _seedRannum: - arg1: "Graine" - arg2: "Min" - arg3: "Max" - seedRandomPick: "Sélection aléatoire dans une liste (Graine)" - _seedRandomPick: - arg1: "Graine" - arg2: "Listes" - DRPWPM: "Sélection aléatoire à partir d'une liste pondérée (mise à jour quotidienne par utilisateur)" - _DRPWPM: - arg1: "Liste de texte" - pick: "Sélectionner dans la liste" - _pick: - arg1: "Listes" - arg2: "Position" - listLen: "Longueur de la liste" - _listLen: - arg1: "Listes" - number: "Numérique" - stringToNumber: "Chaîne en chiffres" - _stringToNumber: - arg1: "Texte" - numberToString: "Chiffres en chaîne" - _numberToString: - arg1: "Numérique" - splitStrByLine: "Séparer le texte par lignes" - _splitStrByLine: - arg1: "Texte" - ref: "Variables" - fn: "Fonction" - _fn: - slots: "Emplacement" - slots-info: "Veuillez délimiter chaque emplacement par un saut de ligne" - arg1: "Sortie" - for: "Répéter" - _for: - arg1: "Compter" - arg2: "Action" - thereIsEmptySlot: "Slot {slot} est vide !" - types: - string: "Texte" - number: "Numérique" - boolean: "Marqueur" - array: "Listes" - stringArray: "Liste de texte" - emptySlot: "Slot vide" - enviromentVariables: "Variables d'environnement" - pageVariables: "Élément de page" - argVariables: "Entrée vide" -room: - add-furniture: "Placer des meubles" - translate: "Déplacer" - rotate: "Tourner" - exit: "Retour" - remove: "Enlever" - save: "Enregistrer" - saved: "enregistré" - clear: "Tout enlever" - clear-confirm: "Désirez-vous enlever tout les meubles de votre chambre ?" - leave-confirm: "Vous avez des modifications non-sauvegardées. Voulez-vous vraiment quitter ?" - chooseImage: "Sélectionnez une image" - room-type: "Type de chambre" - carpet-color: "Couleur du tapis" - rooms: - default: "Par défaut" - washitsu: "Style japonnais" - furnitures: - milk: "Lait en carton" - bed: "Lit" - low-table: "Table basse" - desk: "Bureau" - chair: "Chaise" - chair2: "Chaise 2" - fan: "Ventilateur" - pc: "Ordinateur" - plant: "Plante d’intérieur" - plant2: "Plante d’intérieur 2" - eraser: "Gomme" - pencil: "Crayon" - pudding: "Pudding" - cardboard-box: "Boîte en carton" - cardboard-box2: "Boîte en carton 2" - cardboard-box3: "Boîte en carton 3" - book: "Livre" - book2: "Livre 2" - piano: "Piano" - facial-tissue: "Mouchoirs en papier" - server: "Serveurs" - moon: "Lune" - corkboard: "Tableau en liège" - mousepad: "Tapis de souris" - monitor: "Écran" - keyboard: "Clavier" - carpet-stripe: "Tapis (zébré)" - mat: "Tapis" - color-box: "Étagère" - wall-clock: "Horloge murale" - photoframe: "Cadre photo" - cube: "Cube" - tv: "Téléviseur" - pinguin: "Pingouin" - rubik-cube: "Cube de Rubik" - poster-h: "Affiche (horizontale)" - poster-v: "Affiche (verticale)" - sofa: "Canapé" - spiral: "Escaliers en spirale" - bin: "Corbeille" - cup-noodle: "Bol de nouilles" - holo-display: "Affichage holographique" - energy-drink: "Boisson énergétique" diff --git a/locales/index.js b/locales/index.js index b71625dbc69f320bfa665d859bba44fc9b50be85..749a3e0e1333d26e2655ce67708afec26eebe19d 100644 --- a/locales/index.js +++ b/locales/index.js @@ -14,19 +14,19 @@ const merge = (...args) => args.reduce((a, c) => ({ }), {}); const languages = [ - 'cs-CZ', + /*'cs-CZ', 'da-DK', 'de-DE', 'en-US', 'es-ES', - 'fr-FR', + 'fr-FR',*/ 'ja-JP', - 'ja-KS', + /*'ja-KS', 'ko-KR', 'nl-NL', 'pl-PL', 'zh-CN', - 'zh-TW', + 'zh-TW',*/ ]; const primaries = { diff --git a/locales/it-IT.yml b/locales/it-IT.yml deleted file mode 100644 index 8b14dc617055a440e380f305cf3d125b457b4f2b..0000000000000000000000000000000000000000 --- a/locales/it-IT.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -meta: - lang: "Italiano" -common: - misskey: "A â of the fediverse" - about-title: "A â of the fediverse." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7ced7bdca97b2826776e60525862c556bb6a4d86..5673a2d41692681f4af29fdc0759683e7979e74f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1,2039 +1,499 @@ -meta: - lang: "日本語" - divider: "" - -common: - misskey: "A â of fediverse" - about-title: "A â of fediverse." - about: "Misskeyを見ã¤ã‘ã¦ã„ãŸã ãã€ã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ã€‚Misskeyã¯ã€åœ°çƒã§ç”Ÿã¾ã‚ŒãŸ<b>分散マイクãƒãƒ–ãƒã‚°SNS</b>ã§ã™ã€‚Fediverse(様々ãªSNSã§æ§‹æˆã•ã‚Œã‚‹å®‡å®™)ã®ä¸ã«å˜åœ¨ã™ã‚‹ãŸã‚ã€ä»–ã®SNSã¨ç›¸äº’ã«ç¹‹ãŒã£ã¦ã„ã¾ã™ã€‚æš«ã—都会ã®å–§é¨’ã‹ã‚‰é›¢ã‚Œã¦ã€æ–°ã—ã„インターãƒãƒƒãƒˆã«ãƒ€ã‚¤ãƒ–ã—ã¦ã¿ã¾ã›ã‚“ã‹ã€‚" - intro: - title: "Misskeyã£ã¦ï¼Ÿ" - about: "Misskeyã¯ã‚ªãƒ¼ãƒ—ンソースã®<b>分散型マイクãƒãƒ–ãƒã‚°SNS</b>ã§ã™ã€‚リッãƒã§é«˜åº¦ã«ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã§ãã‚‹UIã€æŠ•ç¨¿ã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€ãƒ•ã‚¡ã‚¤ãƒ«ã‚’一元管ç†ã§ãるドライブãªã©ã€å…ˆé€²çš„ãªæ©Ÿèƒ½ã‚’æƒãˆã¦ã„ã¾ã™ã€‚ã¾ãŸã€Fediverseã¨å‘¼ã°ã‚Œã‚‹ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«æŽ¥ç¶šã§ãã‚‹ãŸã‚ã€ä»–ã®SNSã¨ã‚‚ã‚„ã‚Šå–ã‚Šã§ãã¾ã™ã€‚例ãˆã°ã€ã‚ãªãŸãŒä½•ã‹æŠ•ç¨¿ã™ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã¯Misskeyã ã‘ã§ãªãä»–ã®SNSã«ã‚‚ä¼ã‚ã‚Šã¾ã™ã€‚ã¡ã‚‡ã†ã©ã‚る惑星ã‹ã‚‰ä»–ã®æƒ‘星ã«é›»æ³¢ã‚’発信ã—ã¦ã„る様åをイメージã—ã¦ãã ã•ã„。" - features: "特徴" - rich-contents: "投稿" - rich-contents-desc: "自分ã®è€ƒãˆã€è©±é¡Œã®å‡ºæ¥äº‹ã€çš†ã¨å…±æœ‰ã—ãŸã„ã“ã¨ã«ã¤ã„ã¦ç™ºä¿¡ã—ã¦ãã ã•ã„。必è¦ã§ã‚ã‚Œã°ã€æ§˜ã€…ãªæ§‹æ–‡ã‚’使ã£ã¦æŠ•ç¨¿ã‚’装飾ã—ãŸã‚Šã€å¥½ããªç”»åƒã€å‹•ç”»ãªã©ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚„アンケートを添付ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" - reaction: "リアクション" - reaction-desc: "ã‚ãªãŸã®æ°—æŒã¡ã‚’ä¼ãˆã‚‹æœ€ã‚‚ç°¡å˜ãªæ–¹æ³•ã§ã™ã€‚Misskeyã¯ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ã«æ§˜ã€…ãªãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’付ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã„ã¡ã©Misskeyã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ©Ÿèƒ½ã‚’体験ã—ã¦ã—ã¾ã†ã¨ã€ã‚‚ã†ã€Œã„ã„ãã€ã®æ¦‚念ã—ã‹å˜åœ¨ã—ãªã„SNSã«ã¯æˆ»ã‚Œãªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。" - ui: "インターフェース" - ui-desc: "ã©ã®ã‚ˆã†ãªUIãŒä½¿ã„ã‚„ã™ã„ã‹ã¯äººãã‚Œãžã‚Œã§ã™ã€‚ã ã‹ã‚‰ã€Misskeyã¯è‡ªç”±åº¦ã®é«˜ã„UIã‚’æŒã£ã¦ã„ã¾ã™ã€‚レイアウトやデザインを調整ã—ãŸã‚Šã€ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºå¯èƒ½ãªæ§˜ã€…ãªã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’é…ç½®ã—ãŸã‚Šã—ã¦ã€è‡ªåˆ†ã ã‘ã®ãƒ›ãƒ¼ãƒ を作ã£ã¦ãã ã•ã„。" - drive: "ドライブ" - drive-desc: "以å‰æŠ•ç¨¿ã—ãŸã“ã¨ã®ã‚ã‚‹ç”»åƒã‚’ã¾ãŸæŠ•ç¨¿ã—ãŸããªã£ãŸã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“ã‹ï¼Ÿã‚‚ã—ãã¯ã€ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã‚’フォルダ分ã‘ã—ã¦æ•´ç†ã—ãŸããªã£ãŸã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“ã‹ï¼ŸMisskeyã®æ ¹å¹¹ã«çµ„ã¿è¾¼ã¾ã‚ŒãŸãƒ‰ãƒ©ã‚¤ãƒ–機能ã«ã‚ˆã£ã¦ãれらãŒè§£æ±ºã—ã¾ã™ã€‚ファイルã®å…±æœ‰ã‚‚ç°¡å˜ã§ã™ã€‚" - outro: "ä»–ã«ã‚‚Misskeyã«ã—ã‹ãªã„機能ã¯ã¾ã ã¾ã ã‚ã‚‹ã®ã§ã€ãœã²ã‚ãªãŸè‡ªèº«ã®ç›®ã§ç¢ºã‹ã‚ã¦ãã ã•ã„。Misskeyã¯åˆ†æ•£åž‹SNSãªã®ã§ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ãŒæ°—ã«å…¥ã‚‰ãªã‘ã‚Œã°ä»–ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’試ã™ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ãã‚Œã§ã¯ã€GLHF!" - application-authorization: "アプリã®é€£æº" - close: "é–‰ã˜ã‚‹" - do-not-copy-paste: "ã“ã“ã«ã‚³ãƒ¼ãƒ‰ã‚’入力ã—ãŸã‚Šå¼µã‚Šä»˜ã‘ãŸã‚Šã—ãªã„ã§ãã ã•ã„。アカウントãŒä¸æ£åˆ©ç”¨ã•ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚" - load-more: "ã‚‚ã£ã¨èªã¿è¾¼ã‚€" - enter-password: "パスワードを入力ã—ã¦ãã ã•ã„" - 2fa: "二段階èªè¨¼" - customize-home: "ホームをカスタマイズ" - featured-notes: "ãƒã‚¤ãƒ©ã‚¤ãƒˆ" - dark-mode: "ダークモード" - signin: "ãƒã‚°ã‚¤ãƒ³" - signup: "æ–°è¦ç™»éŒ²" - signout: "ãƒã‚°ã‚¢ã‚¦ãƒˆ" - reload-to-apply-the-setting: "ã“ã®è¨å®šã‚’åæ˜ ã™ã‚‹ã«ã¯ãƒšãƒ¼ã‚¸ã‚’リãƒãƒ¼ãƒ‰ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚今ã™ãリãƒãƒ¼ãƒ‰ã—ã¾ã™ã‹ï¼Ÿ" - fetching-as-ap-object: "連åˆã«ç…§ä¼šä¸" - unfollow-confirm: "{name}ã•ã‚“をフォãƒãƒ¼è§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" - delete-confirm: "ã“ã®æŠ•ç¨¿ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" - signin-required: "ãƒã‚°ã‚¤ãƒ³ã—ã¦ãã ã•ã„" - notification-type: "通知ã®ç¨®é¡ž" - notification-types: - all: "ã™ã¹ã¦" - pollVote: "投票" - follow: "フォãƒãƒ¼" - receiveFollowRequest: "フォãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆ" - reply: "返信" - quote: "引用" - renote: "Renote" - mention: "言åŠ" - reaction: "リアクション" - - got-it: "ã‚ã‹ã£ãŸ" - customization-tips: - title: "カスタマイズã®ãƒ’ント" - paragraph: "<p>ホームã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã§ã¯ã€ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’è¿½åŠ /削除ã—ãŸã‚Šã€ãƒ‰ãƒ©ãƒƒã‚°&ドãƒãƒƒãƒ—ã—ã¦ä¸¦ã¹æ›¿ãˆãŸã‚Šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚</p><p>一部ã®ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã¯ã€<strong><strong>å³</strong>クリック</strong>ã™ã‚‹ã“ã¨ã§è¡¨ç¤ºã‚’変更ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚</p><p>ウィジェットを削除ã™ã‚‹ã«ã¯ã€ãƒ˜ãƒƒãƒ€ãƒ¼ã®<strong>「ゴミ箱ã€</strong>ã¨æ›¸ã‹ã‚ŒãŸã‚¨ãƒªã‚¢ã«ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’ドラッグ&ドãƒãƒƒãƒ—ã—ã¾ã™ã€‚</p><p>カスタマイズを終了ã™ã‚‹ã«ã¯ã€å³ä¸Šã®ã€Œå®Œäº†ã€ã‚’クリックã—ã¾ã™ã€‚</p>" - gotit: "Got it!" - notification: - file-uploaded: "ファイルãŒã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã•ã‚Œã¾ã—ãŸ" - message-from: "{}ã•ã‚“ã‹ã‚‰ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸:" - reversi-invited: "対局ã¸ã®æ‹›å¾…ãŒã‚ã‚Šã¾ã™" - reversi-invited-by: "{}ã•ã‚“ã‹ã‚‰" - notified-by: "{}ã•ã‚“ã‹ã‚‰" - reply-from: "{}ã•ã‚“ã‹ã‚‰è¿”ä¿¡:" - quoted-by: "{}ã•ã‚“ãŒå¼•ç”¨:" - time: - unknown: "ãªãžã®ã˜ã‹ã‚“" - future: "未æ¥" - just_now: "ãŸã£ãŸä»Š" - seconds_ago: "{}秒å‰" - minutes_ago: "{}分å‰" - hours_ago: "{}時間å‰" - days_ago: "{}æ—¥å‰" - weeks_ago: "{}週間å‰" - months_ago: "{}ヶ月å‰" - years_ago: "{}å¹´å‰" - month-and-day: "{month}月 {day}æ—¥" +_ago: + unknown: "謎" + future: "未æ¥" + justNow: "ãŸã£ãŸä»Š" + secondsAgo: "{n}秒å‰" + minutesAgo: "{n}分å‰" + hoursAgo: "{n}時間å‰" + daysAgo: "{n}æ—¥å‰" + weeksAgo: "{n}週間å‰" + monthsAgo: "{n}ヶ月å‰" + yearsAgo: "{n}å¹´å‰" + +_time: + second: "秒" + minute: "分" + hour: "時間" + day: "æ—¥" - trash: "ゴミ箱" - drive: "ドライブ" - pages: "ページ" - messaging: "トーク" - home: "ホーム" - deck: "デッã‚" +introMisskey: "よã†ã“ãï¼Misskeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スã§ã™ã€‚\n「ノートã€ã‚’作æˆã—ã¦ã€ã„ã¾èµ·ã“ã£ã¦ã„ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ãªãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「リアクションã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ðŸ‘\næ–°ã—ã„世界を探検ã—よã†ðŸš€" +monthAndDay: "{month}月 {day}æ—¥" +search: "検索" +notifications: "通知" +username: "ユーザーå" +password: "パスワード" +fetchingAsApObject: "連åˆã«ç…§ä¼šä¸" +ok: "OK" +gotIt: "ã‚ã‹ã£ãŸ" +cancel: "ã‚ャンセル" +enterUsername: "ユーザーåを入力" +renotedBy: "{user}ãŒRenote" +noNotes: "投稿ã¯ã‚ã‚Šã¾ã›ã‚“" +noNotifications: "通知ã¯ã‚ã‚Šã¾ã›ã‚“" +instance: "インスタンス" +settings: "è¨å®š" +profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" +timeline: "タイムライン" +noAccountDescription: "自己紹介ã¯ã‚ã‚Šã¾ã›ã‚“" +login: "ãƒã‚°ã‚¤ãƒ³" +loggingIn: "ãƒã‚°ã‚¤ãƒ³ä¸" +logout: "ãƒã‚°ã‚¢ã‚¦ãƒˆ" +signup: "æ–°è¦ç™»éŒ²" +uploading: "アップãƒãƒ¼ãƒ‰ä¸" +save: "ä¿å˜" +users: "ユーザー" +addUser: "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ " +favorite: "ãŠæ°—ã«å…¥ã‚Š" +favorites: "ãŠæ°—ã«å…¥ã‚Š" +unfavorite: "ãŠæ°—ã«å…¥ã‚Šè§£é™¤" +pin: "ピン留ã‚" +unpin: "ピン留ã‚解除" +copyContent: "内容をコピー" +copyLink: "リンクをコピー" +delete: "削除" +addToList: "リストã«è¿½åŠ " +sendMessage: "メッセージをé€ä¿¡" +copyUsername: "ユーザーåをコピー" +reply: "返信" +loadMore: "ã‚‚ã£ã¨è¦‹ã‚‹" +youGotNewFollower: "フォãƒãƒ¼ã•ã‚Œã¾ã—ãŸ" +receiveFollowRequest: "フォãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚Œã¾ã—ãŸ" +followRequestAccepted: "フォãƒãƒ¼ãŒæ‰¿èªã•ã‚Œã¾ã—ãŸ" +mentions: "ã‚ãªãŸå®›ã¦" +directNotes: "ダイレクト投稿" +importAndExport: "インãƒãƒ¼ãƒˆã¨ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆ" +import: "インãƒãƒ¼ãƒˆ" +export: "エクスãƒãƒ¼ãƒˆ" +files: "ファイル" +download: "ダウンãƒãƒ¼ãƒ‰" +driveFileDeleteConfirm: "ファイル「{name}ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付ã—ãŸæŠ•ç¨¿ã‚‚消ãˆã¾ã™ã€‚" +unfollowConfirm: "{name}ã®ãƒ•ã‚©ãƒãƒ¼ã‚’解除ã—ã¾ã™ã‹ï¼Ÿ" +exportRequested: "エクスãƒãƒ¼ãƒˆã‚’リクエストã—ã¾ã—ãŸã€‚ã“ã‚Œã«ã¯æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚エクスãƒãƒ¼ãƒˆãŒçµ‚ã‚ã‚‹ã¨ã€ã€Œãƒ‰ãƒ©ã‚¤ãƒ–ã€ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚" +importRequested: "インãƒãƒ¼ãƒˆã‚’リクエストã—ã¾ã—ãŸã€‚ã“ã‚Œã«ã¯æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" +lists: "リスト" +noLists: "リストã¯ã‚ã‚Šã¾ã›ã‚“" +notes: "投稿" +following: "フォãƒãƒ¼" +followers: "フォãƒãƒ¯ãƒ¼" +followsYou: "フォãƒãƒ¼ã•ã‚Œã¦ã„ã¾ã™" +createList: "リスト作æˆ" +manageLists: "リストã®ç®¡ç†" +error: "å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ" +retry: "å†è©¦è¡Œ" +enterListName: "リストåを入力" +renameList: "リストåを変更" +deleteList: "リストを削除" +privacy: "プライãƒã‚·ãƒ¼" +makeFollowManuallyApprove: "フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã™ã‚‹" +defaultNoteVisibility: "デフォルトã®å…¬é–‹ç¯„囲" +followRequests: "フォãƒãƒ¼ç”³è«‹" +enterEmoji: "絵文å—を入力" +renote: "Renote" +quote: "引用" +pinnedNote: "ピン留ã‚ã•ã‚ŒãŸæŠ•ç¨¿" +you: "ã‚ãªãŸ" +clickToShow: "クリックã—ã¦è¡¨ç¤º" +sensitive: "閲覧注æ„" +add: "è¿½åŠ " +reaction: "リアクション" +reactionSettingDescription: "リアクションピッカーã«è¡¨ç¤ºã™ã‚‹ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚" +rememberNoteVisibility: "公開範囲を記憶ã™ã‚‹" +renameFile: "ファイルåを変更" +attachCancel: "添付å–り消ã—" +markAsSensitive: "閲覧注æ„ã«ã™ã‚‹" +unmarkAsSensitive: "閲覧注æ„を解除ã™ã‚‹" +enterFileName: "ファイルåを入力" +mute: "ミュート" +unmute: "ミュート解除" +block: "ブãƒãƒƒã‚¯" +unblock: "ブãƒãƒƒã‚¯è§£é™¤" +suspend: "å‡çµ" +unsuspend: "解å‡" +blockConfirm: "ブãƒãƒƒã‚¯ã—ã¾ã™ã‹ï¼Ÿ" +unblockConfirm: "ブãƒãƒƒã‚¯è§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" +suspendConfirm: "å‡çµã—ã¾ã™ã‹ï¼Ÿ" +unsuspendConfirm: "解å‡ã—ã¾ã™ã‹ï¼Ÿ" +selectList: "リストをé¸æŠž" +customEmojis: "カスタム絵文å—" +emojiName: "絵文å—å" +emojiUrl: "絵文å—ç”»åƒURL" +addEmoji: "絵文å—ã‚’è¿½åŠ " +cacheRemoteFiles: "リモートã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã‚ャッシュã™ã‚‹" +cacheRemoteFilesDescription: "ã“ã®è¨å®šã‚’無効ã«ã™ã‚‹ã¨ã€ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã‚ャッシュã›ãšç›´ãƒªãƒ³ã‚¯ã™ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚サーãƒãƒ¼ã®ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã‚’節約ã§ãã¾ã™ãŒã€ã‚µãƒ ãƒã‚¤ãƒ«ãŒç”Ÿæˆã•ã‚Œãªã„ã®ã§é€šä¿¡é‡ãŒå¢—åŠ ã—ã¾ã™ã€‚" +flagAsBot: "Botã¨ã—ã¦è¨å®š" +flagAsCat: "Catã¨ã—ã¦è¨å®š" +autoAcceptFollowed: "フォãƒãƒ¼ä¸ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒªã‚¯ã‚’自動承èª" +addAcount: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆè¿½åŠ " +loginFailed: "ãƒã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ã¾ã—ãŸ" +showOnRemote: "リモートã§è¡¨ç¤º" +general: "全般" +wallpaper: "å£ç´™" +removeWallpaper: "å£ç´™ã‚’削除" +searchWith: "検索: {q}" +youHaveNoLists: "リストãŒã‚ã‚Šã¾ã›ã‚“" +followConfirm: "{name}をフォãƒãƒ¼ã—ã¾ã™ã‹ï¼Ÿ" +proxyAccount: "プãƒã‚シアカウント" +proxyAccountDescription: "プãƒã‚シアカウントã¯ã€ç‰¹å®šã®æ¡ä»¶ä¸‹ã§ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚©ãƒãƒ¼ã‚’代行ã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã™ã€‚例ãˆã°ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’リストã«å…¥ã‚ŒãŸã¨ãã€ãƒªã‚¹ãƒˆã«å…¥ã‚Œã‚‰ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’誰もフォãƒãƒ¼ã—ã¦ã„ãªã„ã¨ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティãŒã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«é…é”ã•ã‚Œãªã„ãŸã‚ã€ä»£ã‚ã‚Šã«ãƒ—ãƒã‚シアカウントãŒãƒ•ã‚©ãƒãƒ¼ã™ã‚‹ã‚ˆã†ã«ã—ã¾ã™ã€‚" +host: "ホスト" +selectUser: "ユーザーをé¸æŠž" +recipient: "宛先" +annotation: "注釈" +federation: "連åˆ" +instances: "インスタンス" +registeredAt: "åˆè¦³æ¸¬" +latestRequestSentAt: "ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆé€ä¿¡" +latestRequestReceivedAt: "ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆå—ä¿¡" +latestStatus: "ç›´è¿‘ã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹" +storageUsage: "ストレージ使用é‡" +charts: "ãƒãƒ£ãƒ¼ãƒˆ" +perHour: "1時間ã”ã¨" +perDay: "1æ—¥ã”ã¨" +stopActivityDelivery: "アクティビティã®é…é€ã‚’åœæ¢" +blockThisInstance: "ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ブãƒãƒƒã‚¯" +operations: "æ“作" +software: "ソフトウェア" +version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" +metadata: "メタデータ" +withNFiles: "{n}ã¤ã®ãƒ•ã‚¡ã‚¤ãƒ«" +monitor: "モニター" +jobQueue: "ジョブã‚ュー" +cpuAndMemory: "CPUã¨ãƒ¡ãƒ¢ãƒª" +network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" +disk: "ディスク" +instanceInfo: "ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æƒ…å ±" +statistics: "統計" +clearQueue: "ã‚ューをクリア" +clearQueueConfirmTitle: "ã‚ューをクリアã—ã¾ã™ã‹ï¼Ÿ" +clearQueueConfirmText: "未é…é”ã®æŠ•ç¨¿ã¯é…é€ã•ã‚Œãªããªã‚Šã¾ã™ã€‚通常ã“ã®æ“作を行ã†å¿…è¦ã¯ã‚ã‚Šã¾ã›ã‚“。" +clearCachedFiles: "ã‚ャッシュをクリア" +clearCachedFilesConfirm: "ã‚ャッシュã•ã‚ŒãŸãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã™ã¹ã¦å‰Šé™¤ã—ã¾ã™ã‹ï¼Ÿ" +blockedInstances: "インスタンスブãƒãƒƒã‚¯" +blockedInstancesDescription: "ブãƒãƒƒã‚¯ã—ãŸã„インスタンスã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚ブãƒãƒƒã‚¯ã•ã‚ŒãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¨ã‚„ã‚Šå–ã‚Šã§ããªããªã‚Šã¾ã™ã€‚" +muteAndBlock: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" +mutedUsers: "ミュートã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" +blockedUsers: "ブãƒãƒƒã‚¯ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" +noUsers: "ユーザーã¯ã„ã¾ã›ã‚“" +editProfile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’編集" +noteDeleteConfirm: "ã“ã®æŠ•ç¨¿ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" +pinLimitExceeded: "ã“れ以上ピン留ã‚ã§ãã¾ã›ã‚“" +intro: "Misskeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ã¾ã—ãŸï¼ç®¡ç†è€…アカウントを作æˆã—ã¾ã—ょã†ã€‚" +done: "完了" +processing: "処ç†ä¸" +preview: "プレビュー" +noCustomEmojis: "絵文å—ã¯ã‚ã‚Šã¾ã›ã‚“" +customEmojisOfRemote: "リモートã®çµµæ–‡å—" +noJobs: "ジョブã¯ã‚ã‚Šã¾ã›ã‚“" +federating: "連åˆä¸" +blocked: "ブãƒãƒƒã‚¯ä¸" +suspended: "é…ä¿¡åœæ¢" +all: "å…¨ã¦" +subscribing: "è³¼èªä¸" +publishing: "é…ä¿¡ä¸" +notResponding: "å¿œç”ãªã—" +instanceFollowing: "インスタンスã®ãƒ•ã‚©ãƒãƒ¼" +instanceFollowers: "インスタンスã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" +instanceUsers: "インスタンスã®ãƒ¦ãƒ¼ã‚¶ãƒ¼" +changePassword: "パスワードを変更" +security: "ã‚»ã‚ュリティ" +retypedNotMatch: "入力ãŒä¸€è‡´ã—ã¾ã›ã‚“。" +currentPassword: "ç¾åœ¨ã®ãƒ‘スワード" +newPassword: "æ–°ã—ã„パスワード" +newPasswordRetype: "æ–°ã—ã„パスワード(å†å…¥åŠ›)" +attachFile: "ファイルを添付" +more: "ã‚‚ã£ã¨ï¼" +featured: "ãƒã‚¤ãƒ©ã‚¤ãƒˆ" +usernameOrUserId: "ユーザーåã‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ID" +noSuchUser: "ユーザーãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" +lookup: "照会" +announcements: "ãŠçŸ¥ã‚‰ã›" +imageUrl: "ç”»åƒURL" +remove: "削除" +removed: "削除ã—ã¾ã—ãŸ" +removeAreYouSure: "「{x}ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" +saved: "ä¿å˜ã—ã¾ã—ãŸ" +messaging: "トーク" +upload: "アップãƒãƒ¼ãƒ‰" +fromDrive: "ドライブã‹ã‚‰" +fromUrl: "URLã‹ã‚‰" +editWidgets: "ウィジェットを編集" +exitEdit: "編集を終了" +explore: "ã¿ã¤ã‘ã‚‹" +games: "Misskey Games" +messageRead: "æ—¢èª" +recentUsedEmojis: "最近使用ã—ãŸçµµæ–‡å—" +noMoreHistory: "ã“れよりéŽåŽ»ã®å±¥æ´ã¯ã‚ã‚Šã¾ã›ã‚“" +startMessaging: "トークを開始" +nUsersRead: "{n}人ãŒèªã¿ã¾ã—ãŸ" +agreeTo: "{0}ã«åŒæ„" +tos: "利用è¦ç´„" +start: "始ã‚ã‚‹" +home: "ホーム" +remoteUserCaution: "リモートユーザーã®ãŸã‚ã€æƒ…å ±ãŒä¸å®Œå…¨ã§ã™ã€‚" +activity: "アクティビティ" +images: "ç”»åƒ" +birthday: "誕生日" +yearsOld: "{age}æ³" +registeredDate: "登録日" +location: "å ´æ‰€" +theme: "テーマ" +lightThemes: "明るã„テーマ" +darkThemes: "æš—ã„テーマ" +drive: "ドライブ" +selectFile: "ファイルをé¸æŠž" +selectFiles: "ファイルをé¸æŠž" +renameFolder: "フォルダーåを変更" +createFolder: "フォルダーを作æˆ" +deleteFolder: "フォルダーを削除" +addFile: "ãƒ•ã‚¡ã‚¤ãƒ«ã‚’è¿½åŠ " +emptyDrive: "ドライブã¯ç©ºã§ã™" +emptyFolder: "フォルダーã¯ç©ºã§ã™" +copyUrl: "URLをコピー" +rename: "åå‰ã‚’変更" +avatar: "アイコン" +banner: "ãƒãƒŠãƒ¼" +nsfw: "閲覧注æ„" +disconnectedFromServer: "サーãƒãƒ¼ã‹ã‚‰åˆ‡æ–ã•ã‚Œã¾ã—ãŸ" +reloadConfirm: "リãƒãƒ¼ãƒ‰ã—ã¾ã™ã‹ï¼Ÿ" +watch: "ウォッãƒ" +unwatch: "ウォッãƒè§£é™¤" +accept: "許å¯" +reject: "æ‹’å¦" +instanceName: "インスタンスå" +instanceDescription: "インスタンスã®ç´¹ä»‹" +maintainerName: "管ç†è€…ã®åå‰" +maintainerEmail: "管ç†è€…ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹" +tosUrl: "利用è¦ç´„URL" +thisYear: "今年" +thisMonth: "今月" +today: "今日" +dayX: "{day}æ—¥" +monthX: "{month}月" +yearX: "{year}å¹´" +pages: "ページ" +integration: "連æº" +connectSerice: "接続ã™ã‚‹" +disconnectSerice: "切æ–ã™ã‚‹" +enableLocalTimeline: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを有効ã«ã™ã‚‹" +enableGlobalTimeline: "ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインを有効ã«ã™ã‚‹" +disablingTimelinesInfo: "ã“れらã®ã‚¿ã‚¤ãƒ ラインを無効化ã—ã¦ã‚‚ã€åˆ©ä¾¿æ€§ã®ãŸã‚管ç†è€…ãŠã‚ˆã³ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã¯å¼•ã続ã利用ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" +registration: "登録" +enableRegistration: "誰ã§ã‚‚æ–°è¦ç™»éŒ²ã§ãるよã†ã«ã™ã‚‹" +invite: "招待" +proxyRemoteFiles: "リモートã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’プãƒã‚ã‚·ã™ã‚‹" +proxyRemoteFilesDescription: "ã“ã®è¨å®šã‚’有効ã«ã™ã‚‹ã¨ã€æœªä¿å˜ã¾ãŸã¯ä¿å˜å®¹é‡è¶…éŽã§å‰Šé™¤ã•ã‚ŒãŸãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’ãƒãƒ¼ã‚«ãƒ«ã§ãƒ—ãƒã‚ã‚·ã—ã€ã‚µãƒ ãƒã‚¤ãƒ«ã‚‚生æˆã™ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚サーãƒãƒ¼ã®ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“ã€" +driveCapacityPerLocalAccount: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" +driveCapacityPerRemoteAccount: "リモートユーザーã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" +inMb: "メガãƒã‚¤ãƒˆå˜ä½" +iconUrl: "アイコン画åƒã®URL" +bannerUrl: "ãƒãƒŠãƒ¼ç”»åƒã®URL" +basicInfo: "åŸºæœ¬æƒ…å ±" +pinnedUsers: "ピン留ã‚ユーザー" +pinnedUsersDescription: "「ã¿ã¤ã‘ã‚‹ã€ãƒšãƒ¼ã‚¸ãªã©ã«ãƒ”ン留ã‚ã—ãŸã„ユーザーを改行ã§åŒºåˆ‡ã£ã¦è¨˜è¿°ã—ã¾ã™ã€‚" +recaptcha: "reCAPTCHA" +enableRecaptcha: "reCAPTCHAを有効ã«ã™ã‚‹" +recaptchaSiteKey: "サイトã‚ー" +recaptchaSecretKey: "シークレットã‚ー" +antennas: "アンテナ" +manageAntennas: "アンテナã®ç®¡ç†" +name: "åå‰" +antennaSource: "å—信ソース" +antennaKeywords: "å—ä¿¡ã‚ーワード" +antennaKeywordsDescription: "スペースã§åŒºåˆ‡ã‚‹ã¨AND指定ã«ãªã‚Šã€æ”¹è¡Œã§åŒºåˆ‡ã‚‹ã¨OR指定ã«ãªã‚Šã¾ã™" +notifyAntenna: "æ–°ã—ã„投稿を通知ã™ã‚‹" +withFileAntenna: "ファイルãŒæ·»ä»˜ã•ã‚ŒãŸæŠ•ç¨¿ã®ã¿" +serviceworker: "ServiceWorker" +enableServiceworker: "ServiceWorkerを有効ã«ã™ã‚‹" +antennaUsersDescription: "ユーザーåを改行ã§åŒºåˆ‡ã£ã¦æŒ‡å®šã—ã¾ã™" +caseSensitive: "大文å—å°æ–‡å—を区別ã™ã‚‹" +withReplies: "返信をå«ã‚€" +connectedTo: "次ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã™" +notesAndReplies: "投稿ã¨è¿”ä¿¡" +withFiles: "ファイル付ã" +silence: "サイレンス" +silenceConfirm: "サイレンスã—ã¾ã™ã‹ï¼Ÿ" +unsilenceConfirm: "サイレンス解除ã—ã¾ã™ã‹ï¼Ÿ" +popularUsers: "人気ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼" +recentlyUpdatedUsers: "最近投稿ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" +recentlyRegisteredUsers: "最近登録ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" +recentlyDiscoveredUsers: "最近発見ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" +exploreUsersCount: "{count}ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã¾ã™" +exploreFediverse: "Fediverseを探索" +popularTags: "人気ã®ã‚¿ã‚°" +userList: "リスト" +about: "æƒ…å ±" +aboutMisskey: "Misskeyã«ã¤ã„ã¦" +aboutMisskeyText: "Misskeyã¯syuiloã«ã‚ˆã£ã¦2014å¹´ã‹ã‚‰é–‹ç™ºã•ã‚Œã¦ã„ã‚‹ã€ã‚ªãƒ¼ãƒ—ンソースã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚" +misskeyMembers: "ç¾åœ¨ä»¥ä¸‹ã®ãƒ¡ãƒ³ãƒãƒ¼ã«ã‚ˆã£ã¦é–‹ç™ºãƒ»ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã•ã‚Œã¦ã„ã¾ã™:" +misskeySource: "ソースコードã¯ã“ã“ã§å…¬é–‹ã•ã‚Œã¦ã„ã¾ã™:" +administrator: "管ç†è€…" +token: "トークン" +twoStepAuthentication: "二段階èªè¨¼" + +_2fa: + registerDevice: "デãƒã‚¤ã‚¹ã‚’登録" + step1: "ã¾ãšã€{a}ã‚„{b}ãªã©ã®èªè¨¼ã‚¢ãƒ—リをãŠä½¿ã„ã®ãƒ‡ãƒã‚¤ã‚¹ã«ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ã¾ã™ã€‚" + step2: "次ã«ã€è¡¨ç¤ºã•ã‚Œã¦ã„ã‚‹QRコードをアプリã§ã‚¹ã‚ャンã—ã¾ã™ã€‚" + step3: "アプリã«è¡¨ç¤ºã•ã‚Œã¦ã„るトークンを入力ã—ã¦å®Œäº†ã§ã™ã€‚" + step4: "ã“ã‚Œã‹ã‚‰ãƒã‚°ã‚¤ãƒ³ã™ã‚‹ã¨ãã‚‚ã€åŒã˜ã‚ˆã†ã«ãƒˆãƒ¼ã‚¯ãƒ³ã‚’入力ã—ã¾ã™ã€‚" + +_permissions: + "read:account": "アカウントã®æƒ…å ±ã‚’è¦‹ã‚‹" + "write:account": "アカウントã®æƒ…å ±ã‚’å¤‰æ›´ã™ã‚‹" + "read:blocks": "ブãƒãƒƒã‚¯ã‚’見る" + "write:blocks": "ブãƒãƒƒã‚¯ã‚’æ“作ã™ã‚‹" + "read:drive": "ドライブを見る" + "write:drive": "ドライブをæ“作ã™ã‚‹" + "read:favorites": "ãŠæ°—ã«å…¥ã‚Šã‚’見る" + "write:favorites": "ãŠæ°—ã«å…¥ã‚Šã‚’æ“作ã™ã‚‹" + "read:following": "フォãƒãƒ¼ã®æƒ…å ±ã‚’è¦‹ã‚‹" + "write:following": "フォãƒãƒ¼ãƒ»ãƒ•ã‚©ãƒãƒ¼è§£é™¤ã™ã‚‹" + "read:messaging": "トークを見る" + "write:messaging": "トークをæ“作ã™ã‚‹" + "read:mutes": "ミュートを見る" + "write:mutes": "ミュートをæ“作ã™ã‚‹" + "write:notes": "投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹" + "read:notifications": "通知を見る" + "write:notifications": "通知をæ“作ã™ã‚‹" + "read:reactions": "リアクションを見る" + "write:reactions": "リアクションをæ“作ã™ã‚‹" + "write:votes": "投票ã™ã‚‹" + "read:pages": "ページを見る" + "write:pages": "ページをæ“作ã™ã‚‹" + "read:page-likes": "ページã®ã„ã„ãを見る" + "write:page-likes": "ページã®ã„ã„ãã‚’æ“作ã™ã‚‹" + "read:user-groups": "ユーザーグループを見る" + "write:user-groups": "ユーザーグループをæ“作ã™ã‚‹" + +_auth: + shareAccess: "「{name}ã€ãŒã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’許å¯ã—ã¾ã™ã‹ï¼Ÿ" + permissionAsk: "ã“ã®ã‚¢ãƒ—リã¯æ¬¡ã®æ¨©é™ã‚’è¦æ±‚ã—ã¦ã„ã¾ã™" + +_antennaSources: + all: "å…¨ã¦ã®æŠ•ç¨¿" + homeTimeline: "フォãƒãƒ¼ã—ã¦ã„るユーザーã®æŠ•ç¨¿" + users: "指定ã—ãŸä¸€äººã¾ãŸã¯è¤‡æ•°ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿" + userList: "指定ã—ãŸãƒªã‚¹ãƒˆã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿" + +_weekday: + sunday: "日曜日" + monday: "月曜日" + tuesday: "ç«æ›œæ—¥" + wednesday: "水曜日" + thursday: "木曜日" + friday: "金曜日" + saturday: "土曜日" + +_widgets: + memo: "付箋" + notifications: "通知" timeline: "タイムライン" - explore: "ã¿ã¤ã‘ã‚‹" - following: "フォãƒãƒ¼ä¸" - followers: "フォãƒãƒ¯ãƒ¼" - favorites: "ãŠæ°—ã«å…¥ã‚Š" - - permissions: - "read:account": "アカウントã®æƒ…å ±ã‚’è¦‹ã‚‹" - "write:account": "アカウントã®æƒ…å ±ã‚’å¤‰æ›´ã™ã‚‹" - "read:blocks": "ブãƒãƒƒã‚¯ã‚’見る" - "write:blocks": "ブãƒãƒƒã‚¯ã‚’æ“作ã™ã‚‹" - "read:drive": "ドライブを見る" - "write:drive": "ドライブをæ“作ã™ã‚‹" - "read:favorites": "ãŠæ°—ã«å…¥ã‚Šã‚’見る" - "write:favorites": "ãŠæ°—ã«å…¥ã‚Šã‚’æ“作ã™ã‚‹" - "read:following": "フォãƒãƒ¼ã®æƒ…å ±ã‚’è¦‹ã‚‹" - "write:following": "フォãƒãƒ¼ãƒ»ãƒ•ã‚©ãƒãƒ¼è§£é™¤ã™ã‚‹" - "read:messaging": "トークを見る" - "write:messaging": "トークをæ“作ã™ã‚‹" - "read:mutes": "ミュートを見る" - "write:mutes": "ミュートをæ“作ã™ã‚‹" - "write:notes": "投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹" - "read:notifications": "通知を見る" - "write:notifications": "通知をæ“作ã™ã‚‹" - "read:reactions": "リアクションを見る" - "write:reactions": "リアクションをæ“作ã™ã‚‹" - "write:votes": "投票ã™ã‚‹" - "read:pages": "ページを見る" - "write:pages": "ページをæ“作ã™ã‚‹" - "read:page-likes": "ページã®ã„ã„ãを見る" - "write:page-likes": "ページã®ã„ã„ãã‚’æ“作ã™ã‚‹" - "read:user-groups": "ユーザーグループを見る" - "write:user-groups": "ユーザーグループをæ“作ã™ã‚‹" - - empty-timeline-info: - follow-users-to-make-your-timeline: "ユーザーをフォãƒãƒ¼ã™ã‚‹ã¨æŠ•ç¨¿ãŒã‚¿ã‚¤ãƒ ラインã«è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚" - explore: "ユーザーを探索ã™ã‚‹" - - post-form: - attach-location-information: "ä½ç½®æƒ…å ±ã‚’æ·»ä»˜ã™ã‚‹" - hide-contents: "å†…å®¹ã‚’éš ã™" - reply-placeholder: "ã“ã®æŠ•ç¨¿ã¸ã®è¿”ä¿¡..." - quote-placeholder: "ã“ã®æŠ•ç¨¿ã‚’引用..." - option-quote-placeholder: "ã“ã®æŠ•ç¨¿ã‚’引用... (オプション)" - quote-attached: "引用付ã" - quote-question: "引用ã¨ã—ã¦æ·»ä»˜ã—ã¾ã™ã‹ï¼Ÿ" - submit: "投稿" - reply: "返信" - renote: "Renote" - posting: "投稿ä¸" - attach-media-from-local: "PCã‹ã‚‰ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’添付" - attach-media-from-drive: "ドライブã‹ã‚‰ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’添付" - insert-a-kao: "v('ω')v" - create-poll: "アンケートを作æˆ" - text-remain: "残り{}æ–‡å—" - recent-tags: "最近" - local-only-message: "ã“ã®æŠ•ç¨¿ã¯ãƒãƒ¼ã‚«ãƒ«ã«ã®ã¿å…¬é–‹ã•ã‚Œã¾ã™" - click-to-tagging: "クリックã§ã‚¿ã‚°ä»˜ã‘" - visibility: "公開範囲" - geolocation-alert: "ãŠä½¿ã„ã®ç«¯æœ«ã¯ä½ç½®æƒ…å ±ã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“" - error: "エラー" - enter-username: "ユーザーåを入力ã—ã¦ãã ã•ã„" - specified-recipient: "宛先" - add-visible-user: "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ " - cw-placeholder: "内容ã¸ã®æ³¨é‡ˆ (オプション)" - username-prompt: "ユーザーåを入力ã—ã¦ãã ã•ã„" - enter-file-name: "ファイルåを編集" - - weekday-short: - sunday: "æ—¥" - monday: "月" - tuesday: "ç«" - wednesday: "æ°´" - thursday: "木" - friday: "金" - saturday: "土" - - weekday: - sunday: "日曜日" - monday: "月曜日" - tuesday: "ç«æ›œæ—¥" - wednesday: "水曜日" - thursday: "木曜日" - friday: "金曜日" - saturday: "土曜日" - - reactions: - like: "ã„ã„ã" - love: "ã—ã‚…ã" - laugh: "笑" - hmm: "ãµã…~む" - surprise: "ã‚ãŠ" - congrats: "ãŠã‚ã§ã¨ã†" - angry: "ãŠã“" - confused: "ã“ã¾ã“ã¾ã®ã“ã¾ã‚Š" - rip: "RIP" - pudding: "Pudding" - - note-visibility: - public: "公開" - home: "ホーム" - home-desc: "ホームタイムラインã«ã®ã¿å…¬é–‹" - followers: "フォãƒãƒ¯ãƒ¼" - followers-desc: "自分ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã«ã®ã¿å…¬é–‹" - specified: "ダイレクト" - specified-desc: "指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã®ã¿å…¬é–‹" - local-public: "公開 (ãƒãƒ¼ã‚«ãƒ«ã®ã¿)" - local-home: "ホーム(ãƒãƒ¼ã‚«ãƒ«ã®ã¿)" - local-followers: "フォãƒãƒ¯ãƒ¼ (ãƒãƒ¼ã‚«ãƒ«ã®ã¿)" - - note-placeholders: - a: "今ã©ã†ã—ã¦ã‚‹ï¼Ÿ" - b: "何ã‹ã‚ã‚Šã¾ã—ãŸã‹ï¼Ÿ" - c: "何をãŠè€ƒãˆã§ã™ã‹ï¼Ÿ" - d: "言ã„ãŸã„ã“ã¨ã¯ï¼Ÿ" - e: "ã“ã“ã«æ›¸ã„ã¦ãã ã•ã„" - f: "ã‚ãªãŸãŒæ›¸ãã®ã‚’å¾…ã£ã¦ã„ã¾ã™..." - - settings: "è¨å®š" - _settings: - profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - notification: "通知" - apps: "アプリ" - tags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - mute-and-block: "ミュート/ブãƒãƒƒã‚¯" - blocking: "ブãƒãƒƒã‚¯" - security: "ã‚»ã‚ュリティ" - signin: "ãƒã‚°ã‚¤ãƒ³å±¥æ´" - password: "パスワード" - other: "ãã®ä»–" - appearance: "デザイン" - behavior: "動作" - reactions: "リアクション" - reactions-description: "リアクションピッカーã«è¡¨ç¤ºã™ã‚‹ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚" - fetch-on-scroll: "スクãƒãƒ¼ãƒ«ã§è‡ªå‹•èªã¿è¾¼ã¿" - fetch-on-scroll-desc: "ページを下ã¾ã§ã‚¹ã‚¯ãƒãƒ¼ãƒ«ã—ãŸã¨ãã«è‡ªå‹•ã§è¿½åŠ ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ã‚’èªã¿è¾¼ã¿ã¾ã™ã€‚" - note-visibility: "投稿ã®å…¬é–‹ç¯„囲" - default-note-visibility: "デフォルトã®å…¬é–‹ç¯„囲" - remember-note-visibility: "投稿ã®å…¬é–‹ç¯„囲を記憶ã™ã‚‹" - web-search-engine: "ウェブ検索エンジン" - web-search-engine-desc: "例: https://www.google.com/?#q={{query}}" - paste: "ペースト" - pasted-file-name: "ペーストã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«åã®ãƒ†ãƒ³ãƒ—レート" - pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" - paste-dialog: "ペースト時ã«ãƒ•ã‚¡ã‚¤ãƒ«åを編集" - paste-dialog-desc: "ペースト時ã«ãƒ•ã‚¡ã‚¤ãƒ«åを編集ã™ã‚‹ãƒ€ã‚¤ã‚¢ãƒã‚°ã‚’表示ã™ã‚‹ã‚ˆã†ã«ã—ã¾ã™ã€‚" - keep-cw: "CWä¿æŒ" - keep-cw-desc: "投稿ã«ãƒªãƒ—ライã™ã‚‹éš›ã€ãƒªãƒ—ライ元ã®æŠ•ç¨¿ã«CWãŒè¨å®šã•ã‚Œã¦ã„ãŸã¨ãã€ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§åŒã˜CWã‚’è¨å®šã™ã‚‹ã‚ˆã†ã«ã—ã¾ã™ã€‚" - i-like-sushi: "ç§ã¯(プリンよりむã—ã‚)寿å¸ãŒå¥½ã" - show-reversi-board-labels: "リãƒãƒ¼ã‚·ã®ãƒœãƒ¼ãƒ‰ã®è¡Œã¨åˆ—ã®ãƒ©ãƒ™ãƒ«ã‚’表示" - use-avatar-reversi-stones: "リãƒãƒ¼ã‚·ã®çŸ³ã«ã‚¢ãƒã‚¿ãƒ¼ã‚’使ã†" - disable-animated-mfm: "投稿内ã®å‹•ãã®ã‚るテã‚ストを無効ã«ã™ã‚‹" - disable-showing-animated-images: "アニメーション画åƒã‚’å†ç”Ÿã—ãªã„" - enable-quick-notification-view: "通知ã®ã‚¯ã‚¤ãƒƒã‚¯ãƒ“ューを有効ã«ã™ã‚‹" - suggest-recent-hashtags: "最近ã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’投稿フォームã«è¡¨ç¤ºã™ã‚‹" - always-show-nsfw: "常ã«é–²è¦§æ³¨æ„ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’表示ã™ã‚‹" - always-mark-nsfw: "常ã«ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’閲覧注æ„ã¨ã—ã¦æŠ•ç¨¿" - show-full-acct: "ユーザーåã®ãƒ›ã‚¹ãƒˆã‚’çœç•¥ã—ãªã„" - show-via: "viaを表示ã™ã‚‹" - reduce-motion: "UIã®å‹•ãを減らã™" - this-setting-is-this-device-only: "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã®ã¿" - use-os-default-emojis: "OS標準ã®çµµæ–‡å—を使用" - line-width: "ç·šã®å¤ªã•" - line-width-thin: "ç´°ã„" - line-width-normal: "普通" - line-width-thick: "太ã„" - font-size: "æ–‡å—ã®å¤§ãã•" - font-size-x-small: "å°ã•ã„" - font-size-small: "å°‘ã—å°ã•ã„" - font-size-medium: "普通" - font-size-large: "å°‘ã—大ãã„" - font-size-x-large: "大ãã„" - deck-column-align: "デッã‚ã®ã‚«ãƒ©ãƒ ã®é…ç½®" - deck-column-align-center: "ä¸å¤®" - deck-column-align-left: "å·¦" - deck-column-align-flexible: "フレã‚シブル" - deck-column-width: "デッã‚ã®ã‚«ãƒ©ãƒ ã®å¹…" - deck-column-width-narrow: "ç‹" - deck-column-width-narrower: "ã‚„ã‚„ç‹" - deck-column-width-normal: "普通" - deck-column-width-wider: "やや広" - deck-column-width-wide: "広" - use-shadow: "UIã«å½±ã‚’使用" - rounded-corners: "UIã®è§’を丸ã‚ã‚‹" - circle-icons: "円形ã®ã‚¢ãƒã‚¿ãƒ¼ã‚’使用" - contrasted-acct: "ユーザーåã«ã‚³ãƒ³ãƒˆãƒ©ã‚¹ãƒˆã‚’付ã‘ã‚‹" - wallpaper: "å£ç´™" - choose-wallpaper: "å£ç´™ã‚’é¸æŠž" - delete-wallpaper: "å£ç´™ã‚’削除" - post-form-on-timeline: "タイムライン上部ã«æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ を表示ã™ã‚‹" - show-clock-on-header: "å³ä¸Šã«æ™‚計を表示ã™ã‚‹" - show-reply-target: "リプライ先を表示ã™ã‚‹" - timeline: "タイムライン" - show-my-renotes: "自分ã®è¡Œã£ãŸRenoteをタイムラインã«è¡¨ç¤ºã™ã‚‹" - show-renoted-my-notes: "自分ã®æŠ•ç¨¿ã®Renoteをタイムラインã«è¡¨ç¤ºã™ã‚‹" - show-local-renotes: "ãƒãƒ¼ã‚«ãƒ«ã®æŠ•ç¨¿ã®Renoteをタイムラインã«è¡¨ç¤ºã™ã‚‹" - remain-deleted-note: "削除ã•ã‚ŒãŸæŠ•ç¨¿ã‚’表示ã—続ã‘ã‚‹" - sound: "サウンド" - enable-sounds: "サウンドを有効ã«ã™ã‚‹" - enable-sounds-desc: "投稿やメッセージをé€å—ä¿¡ã—ãŸã¨ããªã©ã«ã‚µã‚¦ãƒ³ãƒ‰ã‚’å†ç”Ÿã—ã¾ã™ã€‚ã“ã®è¨å®šã¯ãƒ–ラウザã«è¨˜æ†¶ã•ã‚Œã¾ã™ã€‚" - volume: "ボリューム" - test: "テスト" - update: "Misskey Update" - version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³:" - latest-version: "最新ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³:" - update-checking: "アップデートを確èªä¸" - do-update: "アップデートを確èª" - update-settings: "詳細è¨å®š" - no-updates: "利用å¯èƒ½ãªæ›´æ–°ã¯ã‚ã‚Šã¾ã›ã‚“" - no-updates-desc: "ãŠä½¿ã„ã®Misskeyã¯æœ€æ–°ã§ã™ã€‚" - update-available: "æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒåˆ©ç”¨å¯èƒ½ã§ã™" - update-available-desc: "ページをå†åº¦èªã¿è¾¼ã¿ã™ã‚‹ã¨æ›´æ–°ãŒé©ç”¨ã•ã‚Œã¾ã™ã€‚" - advanced-settings: "高度ãªè¨å®š" - debug-mode: "デãƒãƒƒã‚°ãƒ¢ãƒ¼ãƒ‰ã‚’有効ã«ã™ã‚‹" - debug-mode-desc: "ã“ã®è¨å®šã¯ãƒ–ラウザã«è¨˜æ†¶ã•ã‚Œã¾ã™ã€‚" - navbar-position: "ナビゲーションãƒãƒ¼ã®ä½ç½®" - navbar-position-top: "上" - navbar-position-left: "å·¦" - navbar-position-right: "å³" - i-am-under-limited-internet: "ç§ã¯é€šä¿¡ã‚’制é™ã•ã‚Œã¦ã„ã‚‹" - post-style: "投稿ã®è¡¨ç¤ºã‚¹ã‚¿ã‚¤ãƒ«" - post-style-standard: "標準" - post-style-smart: "スマート" - notification-position: "通知ã®è¡¨ç¤º" - notification-position-bottom: "下" - notification-position-top: "上" - disable-via-mobile: "「モãƒã‚¤ãƒ«ã‹ã‚‰ã®æŠ•ç¨¿ã€ãƒ•ãƒ©ã‚°ã‚’付ã‘ãªã„" - load-raw-images: "添付ã•ã‚ŒãŸç”»åƒã‚’高画質ã§è¡¨ç¤ºã™ã‚‹" - load-remote-media: "リモートサーãƒãƒ¼ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’表示ã™ã‚‹" - sync: "åŒæœŸ" - save: "ä¿å˜" - saved: "ä¿å˜ã—ã¾ã—ãŸ" - preview: "プレビュー" - home-profile: "ホームã®ãƒ—ãƒãƒ•ã‚¡ã‚¤ãƒ«" - deck-profile: "デッã‚ã®ãƒ—ãƒãƒ•ã‚¡ã‚¤ãƒ«" - room: "ルーム" - _room: - graphicsQuality: "グラフィックã®å“質" - _graphicsQuality: - ultra: "最高" - high: "高" - medium: "ä¸" - low: "低" - cheep: "最低" - useOrthographicCamera: "平行投影カメラを使用" - - search: "検索" - delete: "削除" - loading: "èªã¿è¾¼ã¿ä¸" - ok: "ãŠï½‹" - cancel: "ã‚„ã‚ã‚‹" - update-available-title: "æ›´æ–°ãŒã‚ã‚Šã¾ã™" - update-available: "Misskeyã®æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒã‚ã‚Šã¾ã™({newer}。ç¾åœ¨{current}を利用ä¸)。ページをå†åº¦èªã¿è¾¼ã¿ã™ã‚‹ã¨æ›´æ–°ãŒé©ç”¨ã•ã‚Œã¾ã™ã€‚" - my-token-regenerated: "ã‚ãªãŸã®ãƒˆãƒ¼ã‚¯ãƒ³ãŒæ›´æ–°ã•ã‚ŒãŸã®ã§ã‚µã‚¤ãƒ³ã‚¢ã‚¦ãƒˆã—ã¾ã™ã€‚" - hide-password: "ãƒ‘ã‚¹ãƒ¯ãƒ¼ãƒ‰ã‚’éš ã™" - show-password: "パスワードを表示ã™ã‚‹" - enter-username: "ユーザーåを入力ã—ã¦ãã ã•ã„" - - do-not-use-in-production: "ã“ã‚Œã¯é–‹ç™ºãƒ“ルドã§ã™ã€‚本番環境ã§ä½¿ç”¨ã—ãªã„ã§ãã ã•ã„。" - user-suspended: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯å‡çµã•ã‚Œã¦ã„ã¾ã™ã€‚" - is-remote-user: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±ã¯ä¸æ£ç¢ºãªå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚" - is-remote-post: "ã“ã®æŠ•ç¨¿æƒ…å ±ã¯ã‚³ãƒ”ーã§ã™ã€‚" - view-on-remote: "æ£ç¢ºãªæƒ…å ±ã‚’è¦‹ã‚‹" - renoted-by: "{user}ãŒRenote" - no-notes: "投稿ãŒã‚ã‚Šã¾ã›ã‚“" - turn-on-darkmode: "é—‡ã«é£²ã¾ã‚Œã‚‹" - turn-off-darkmode: "å…‰ã‚ã‚Œ" - - error: - title: "å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ" - retry: "ã‚„ã‚Šç›´ã™" - - reversi: - drawn: "引ã分ã‘" - my-turn: "ã‚ãªãŸã®ã‚¿ãƒ¼ãƒ³ã§ã™" - opponent-turn: "相手ã®ã‚¿ãƒ¼ãƒ³ã§ã™" - turn-of: "{name}ã®ã‚¿ãƒ¼ãƒ³ã§ã™" - past-turn-of: "{name}ã®ã‚¿ãƒ¼ãƒ³" - won: "{name}ã®å‹ã¡" - black: "é»’" - white: "白" - total: "åˆè¨ˆ" - this-turn: "{count}ターン目" - - widgets: - analog-clock: "アナãƒã‚°æ™‚計" - profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - calendar: "カレンダー" - timemachine: "カレンダー(タイムマシン)" - activity: "アクティビティ" - rss: "RSSリーダー" - memo: "付箋" - trends: "トレンド" - photo-stream: "フォトストリーム" - posts-monitor: "投稿ãƒãƒ£ãƒ¼ãƒˆ" - slideshow: "スライドショー" - version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" - broadcast: "ブãƒãƒ¼ãƒ‰ã‚ャスト" - notifications: "通知" - users: "ãŠã™ã™ã‚ユーザー" - polls: "アンケート" - post-form: "投稿フォーム" - server: "サーãƒãƒ¼æƒ…å ±" - nav: "ナビゲーション" - tips: "ヒント" - hashtags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - queue: "ã‚ュー" - - dev: "アプリã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚å†åº¦ãŠè©¦ã—ãã ã•ã„。" - ai-chan-kawaii: "è—ã¡ã‚ƒã‹ã‚ã„ã„" - you: "ã‚ãªãŸ" - -auth/views/form.vue: - share-access: "<i>{name}</i>ãŒã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’許å¯ã—ã¾ã™ã‹ï¼Ÿ" - permission-ask: "ã“ã®ã‚¢ãƒ—リã¯æ¬¡ã®æ¨©é™ã‚’è¦æ±‚ã—ã¦ã„ã¾ã™:" - cancel: "ã‚ャンセル" - accept: "アクセスを許å¯" - -auth/views/index.vue: - loading: "èªã¿è¾¼ã¿ä¸" - denied: "アプリケーションã®é€£æºã‚’ã‚ャンセルã—ã¾ã—ãŸã€‚" - denied-paragraph: "ã“ã®ã‚¢ãƒ—リãŒã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“。" - already-authorized: "ã“ã®ã‚¢ãƒ—リã¯æ—¢ã«é€£æºæ¸ˆã¿ã§ã™" - allowed: "アプリケーションã®é€£æºã‚’許å¯ã—ã¾ã—ãŸ" - callback-url: "アプリケーションã«æˆ»ã£ã¦ã„ã¾ã™" - please-go-back: "アプリケーションã«æˆ»ã£ã¦ã€ã‚„ã£ã¦ã„ã£ã¦ãã ã•ã„。" - error: "セッションãŒå˜åœ¨ã—ã¾ã›ã‚“。" - sign-in: "サインインã—ã¦ãã ã•ã„" - -common/views/pages/explore.vue: - pinned-users: "ピン留ã‚ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" - popular-users: "人気ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼" - recently-updated-users: "最近投稿ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" - recently-registered-users: "æ–°è¦ãƒ¦ãƒ¼ã‚¶ãƒ¼" - recently-discovered-users: "最近発見ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" - popular-tags: "人気ã®ã‚¿ã‚°" - federated: "連åˆ" - explore: "{host}を探索" - explore-fediverse: "Fediverseを探索" - users-info: "ç¾åœ¨{users}ユーザーãŒç™»éŒ²ã•ã‚Œã¦ã„ã¾ã™" + calendar: "カレンダー" + trends: "トレンド" + clock: "時計" -common/views/components/reactions-viewer.details.vue: - few-users: "{users}ãŒ{reaction}をリアクション" - many-users: "{users}ã¨ä»–{omitted}人ãŒ{reaction}をリアクション" - -common/views/components/url-preview.vue: - enable-player: "プレイヤーを開ã" - disable-player: "プレイヤーを閉ã˜ã‚‹" - -common/views/components/user-list.vue: - no-users: "ユーザーãŒã„ã¾ã›ã‚“" - -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "{}ã‚’å¾…ã£ã¦ã„ã¾ã™" - cancel: "ã‚ャンセル" - -common/views/components/games/reversi/reversi.game.vue: - surrender: "投了" - surrendered: "投了ã«ã‚ˆã‚Š" - is-llotheo: "石ã®å°‘ãªã„æ–¹ãŒå‹ã¡(ãƒã‚»ã‚ª)" - looped-map: "ループマップ" - can-put-everywhere: "ã©ã“ã§ã‚‚ç½®ã‘るモード" - -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "ä»–ã®Misskeyユーザーã¨ãƒªãƒãƒ¼ã‚·ã§å¯¾æˆ¦ã—よã†" - invite: "招待" - rule: "éŠã³æ–¹" - rule-desc: "リãƒãƒ¼ã‚·ã¯ã€ç›¸æ‰‹ã¨äº¤äº’ã«çŸ³ã‚’ボードã«ç½®ã„ã¦ã€ç›¸æ‰‹ã®çŸ³ã‚’挟んã§è‡ªåˆ†ã®è‰²ã«å¤‰ãˆã¦ã‚†ãã€æœ€çµ‚çš„ã«æ®‹ã£ãŸçŸ³ãŒå¤šã„æ–¹ãŒå‹ã¡ã¨ã„ã†ãƒœãƒ¼ãƒ‰ã‚²ãƒ¼ãƒ ã§ã™ã€‚" - mode-invite: "招待" - mode-invite-desc: "指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨å¯¾æˆ¦ã™ã‚‹ãƒ¢ãƒ¼ãƒ‰ã§ã™ã€‚" - invitations: "対局ã®æ‹›å¾…ãŒã‚ã‚Šã¾ã™ï¼" - my-games: "自分ã®å¯¾å±€" - all-games: "ã¿ã‚“ãªã®å¯¾å±€" - enter-username: "ユーザーåを入力ã—ã¦ãã ã•ã„" - game-state: - ended: "終了" - playing: "進行ä¸" - -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "ゲームã®è¨å®š" - choose-map: "マップをé¸æŠž" - random: "ランダム" - black-or-white: "先手/後手" - black-is: "{}ãŒé»’" - rules: "ルール" - is-llotheo: "石ã®å°‘ãªã„æ–¹ãŒå‹ã¡(ãƒã‚»ã‚ª)" - looped-map: "ループマップ" - can-put-everywhere: "ã©ã“ã§ã‚‚ç½®ã‘るモード" - settings-of-the-bot: "Botã®è¨å®š" - this-game-is-started-soon: "ゲームã¯æ•°ç§’後ã«é–‹å§‹ã•ã‚Œã¾ã™" - waiting-for-other: "相手ã®æº–å‚™ãŒå®Œäº†ã™ã‚‹ã®ã‚’å¾…ã£ã¦ã„ã¾ã™" - waiting-for-me: "ã‚ãªãŸã®æº–å‚™ãŒå®Œäº†ã™ã‚‹ã®ã‚’å¾…ã£ã¦ã„ã¾ã™" - waiting-for-both: "準備ä¸" - cancel: "ã‚ャンセル" - ready: "準備完了" - cancel-ready: "準備続行" - -common/views/components/connect-failed.vue: - title: "サーãƒãƒ¼ã«æŽ¥ç¶šã§ãã¾ã›ã‚“" - description: "インターãƒãƒƒãƒˆå›žç·šã«å•é¡ŒãŒã‚ã‚‹ã‹ã€ã‚µãƒ¼ãƒãƒ¼ãŒãƒ€ã‚¦ãƒ³ã¾ãŸã¯ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ã—ã°ã‚‰ãã—ã¦ã‹ã‚‰{å†åº¦ãŠè©¦ã—}ãã ã•ã„。" - thanks: "ã„ã¤ã‚‚Misskeyã‚’ã”利用ã„ãŸã ãã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ã€‚" - troubleshoot: "トラブルシュート" - -common/views/components/connect-failed.troubleshooter.vue: - title: "トラブルシューティング" - network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶š" - checking-network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶šã‚’確èªä¸" - internet: "インターãƒãƒƒãƒˆæŽ¥ç¶š" - checking-internet: "インターãƒãƒƒãƒˆæŽ¥ç¶šã‚’確èªä¸" - server: "サーãƒãƒ¼æŽ¥ç¶š" - checking-server: "サーãƒãƒ¼æŽ¥ç¶šã‚’確èªä¸" - finding: "å•é¡Œã‚’調ã¹ã¦ã„ã¾ã™" - no-network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã›ã‚“" - no-network-desc: "ãŠä½¿ã„ã®PCã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶šãŒæ£å¸¸ã‹ç¢ºèªã—ã¦ãã ã•ã„。" - no-internet: "インターãƒãƒƒãƒˆã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã›ã‚“" - no-internet-desc: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«ã¯æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã™ãŒã€ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆã«ã¯æŽ¥ç¶šã•ã‚Œã¦ã„ãªã„よã†ã§ã™ã€‚ãŠä½¿ã„ã®PCã®ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆæŽ¥ç¶šãŒæ£å¸¸ã‹ç¢ºèªã—ã¦ãã ã•ã„。" - no-server: "Misskeyã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶šã§ãã¾ã›ã‚“" - no-server-desc: "ãŠä½¿ã„ã®PCã®ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆæŽ¥ç¶šã¯æ£å¸¸ã§ã™ãŒã€Misskeyã®ã‚µãƒ¼ãƒãƒ¼ã«ã¯æŽ¥ç¶šã§ãã¾ã›ã‚“ã§ã—ãŸã€‚サーãƒãƒ¼ãŒãƒ€ã‚¦ãƒ³ã¾ãŸã¯ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§ã€ã—ã°ã‚‰ãã—ã¦ã‹ã‚‰å†åº¦å¾¡ã‚¢ã‚¯ã‚»ã‚¹ãã ã•ã„。" - success: "Misskeyã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶šã§ãã¾ã—ãŸ" - success-desc: "æ£å¸¸ã«æŽ¥ç¶šã§ãるよã†ã§ã™ã€‚ページをå†åº¦èªã¿è¾¼ã¿ã—ã¦ãã ã•ã„。" - flush: "ã‚ャッシュã®å‰Šé™¤" - set-version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³æŒ‡å®š" - -common/views/components/media-banner.vue: - sensitive: "閲覧注æ„" - click-to-show: "クリックã—ã¦è¡¨ç¤º" - -common/views/components/theme.vue: - theme: "テーマ" - light-theme: "éžãƒ€ãƒ¼ã‚¯ãƒ¢ãƒ¼ãƒ‰æ™‚ã«ä½¿ç”¨ã™ã‚‹ãƒ†ãƒ¼ãƒž" - dark-theme: "ダークモード時ã«ä½¿ç”¨ã™ã‚‹ãƒ†ãƒ¼ãƒž" - light-themes: "明るã„テーマ" - dark-themes: "æš—ã„テーマ" - install-a-theme: "テーマã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«" - theme-code: "テーマコード" - install: "インストール" - installed: "「{}ã€ã‚’インストールã—ã¾ã—ãŸ" - create-a-theme: "テーマã®ä½œæˆ" - save-created-theme: "テーマをä¿å˜" - primary-color: "プライマリ カラー" - secondary-color: "セカンダリ カラー" - text-color: "æ–‡å—色" - base-theme: "ベーステーマ" - base-theme-light: "Light" - base-theme-dark: "Dark" - find-more-theme: "ãã®ä»–ã®ãƒ†ãƒ¼ãƒžã‚’入手" - theme-name: "テーマå" - preview-created-theme: "プレビュー" - invalid-theme: "テーマãŒæ£ã—ãã‚ã‚Šã¾ã›ã‚“。" - already-installed: "æ—¢ã«ãã®ãƒ†ãƒ¼ãƒžã¯ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã¾ã™ã€‚" - saved: "ä¿å˜ã—ã¾ã—ãŸ" - manage-themes: "テーマã®ç®¡ç†" - builtin-themes: "標準テーマ" - my-themes: "マイテーマ" - installed-themes: "インストールã•ã‚ŒãŸãƒ†ãƒ¼ãƒž" - select-theme: "テーマをé¸æŠžã—ã¦ãã ã•ã„" - uninstall: "アンインストール" - uninstalled: "「{}ã€ã‚’アンインストールã—ã¾ã—ãŸ" - author: "作者" - desc: "説明" - export: "エクスãƒãƒ¼ãƒˆ" - import: "インãƒãƒ¼ãƒˆ" - import-by-code: "ã¾ãŸã¯ã‚³ãƒ¼ãƒ‰ã‚’ペースト" - theme-name-required: "テーマåã¯å¿…é ˆã§ã™ã€‚" - -common/views/components/cw-button.vue: +_cw: hide: "éš ã™" show: "ã‚‚ã£ã¨è¦‹ã‚‹" chars: "{count}æ–‡å—" files: "{count}ファイル" poll: "アンケート" -common/views/components/messaging.vue: - search-user: "ユーザーを探ã™" - you: "ã‚ãªãŸ" - no-history: "å±¥æ´ã¯ã‚ã‚Šã¾ã›ã‚“" - user: "ユーザー" - group: "グループ" - start-with-user: "ユーザーã¨ãƒˆãƒ¼ã‚¯ã‚’開始" - start-with-group: "グループã¨ãƒˆãƒ¼ã‚¯ã‚’開始" - select-group: "グループをé¸æŠžã—ã¦ãã ã•ã„" - -common/views/components/messaging-room.vue: - not-talked-user: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨ã®ä¼šè©±ã¯ã‚ã‚Šã¾ã›ã‚“" - not-talked-group: "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã§ã®ä¼šè©±ã¯ã‚ã‚Šã¾ã›ã‚“" - no-history: "ã“れよりéŽåŽ»ã®å±¥æ´ã¯ã‚ã‚Šã¾ã›ã‚“" - new-message: "æ–°ã—ã„メッセージãŒã‚ã‚Šã¾ã™" - only-one-file-attached: "メッセージã«æ·»ä»˜ã§ãるファイルã¯ã²ã¨ã¤ã§ã™" - -common/views/components/messaging-room.form.vue: - input-message-here: "ã“ã“ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’入力" - send: "é€ä¿¡" - attach-from-local: "PCã‹ã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付ã™ã‚‹" - attach-from-drive: "ドライブã‹ã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付ã™ã‚‹" - only-one-file-attached: "メッセージã«æ·»ä»˜ã§ãるファイルã¯ã²ã¨ã¤ã§ã™" - -common/views/components/messaging-room.message.vue: - is-read: "æ—¢èª" - deleted: "ã“ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - -common/views/components/nav.vue: - about: "Misskeyã«ã¤ã„ã¦" - stats: "統計" - status: "ステータス" - wiki: "Wiki" - donors: "ドナー" - repository: "リãƒã‚¸ãƒˆãƒª" - develop: "開発者" - feedback: "フィードãƒãƒƒã‚¯" - tos: "利用è¦ç´„" - -common/views/components/note-menu.vue: - mention: "メンション" - detail: "詳細" - copy-content: "内容をコピー" - copy-link: "リンクをコピー" - favorite: "ãŠæ°—ã«å…¥ã‚Š" - unfavorite: "ãŠæ°—ã«å…¥ã‚Šè§£é™¤" - watch: "ウォッãƒ" - unwatch: "ウォッãƒè§£é™¤" - pin: "ピン留ã‚" - unpin: "ピン留ã‚解除" - delete: "削除" - delete-confirm: "ã“ã®æŠ•ç¨¿ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" - delete-and-edit: "削除ã—ã¦ç·¨é›†" - delete-and-edit-confirm: "ã“ã®æŠ•ç¨¿ã‚’削除ã—ã¦ã‚‚ã†ä¸€åº¦ç·¨é›†ã—ã¾ã™ã‹ï¼Ÿã“ã®æŠ•ç¨¿ã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€Renoteã€è¿”ä¿¡ã‚‚å…¨ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚" - remote: "投稿元ã§è¦‹ã‚‹" - pin-limit-exceeded: "ã“れ以上ピン留ã‚ã§ãã¾ã›ã‚“。" - -common/views/components/user-menu.vue: - mention: "メンション" - mute: "ミュート" - unmute: "ミュート解除" - mute-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ミュートã—ã¾ã™ã‹ï¼Ÿ" - unmute-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ミュート解除ã—ã¾ã™ã‹ï¼Ÿ" - block: "ブãƒãƒƒã‚¯" - unblock: "ブãƒãƒƒã‚¯è§£é™¤" - block-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ブãƒãƒƒã‚¯ã—ã¾ã™ã‹ï¼Ÿ" - unblock-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ブãƒãƒƒã‚¯è§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" - push-to-list: "リストã«è¿½åŠ " - select-list: "リストをé¸æŠžã—ã¦ãã ã•ã„" - report-abuse: "ã‚¹ãƒ‘ãƒ ã‚’å ±å‘Š" - report-abuse-detail: "ã©ã®ã‚ˆã†ãªè¿·æƒ‘行為を行ã£ã¦ã„ã¾ã™ã‹ï¼Ÿ" - report-abuse-reported: "管ç†è€…ã«å ±å‘Šã•ã‚Œã¾ã—ãŸã€‚ã”å”力ã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã—ãŸã€‚" - silence: "サイレンス" - unsilence: "サイレンス解除" - silence-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’サイレンスã—ã¾ã™ã‹ï¼Ÿ" - unsilence-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’サイレンス解除ã—ã¾ã™ã‹ï¼Ÿ" - suspend: "å‡çµ" - unsuspend: "å‡çµè§£é™¤" - suspend-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å‡çµã—ã¾ã™ã‹ï¼Ÿ" - unsuspend-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å‡çµè§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" - -common/views/components/poll.vue: - vote-to: "「{}ã€ã«æŠ•ç¥¨ã™ã‚‹" - vote-count: "{}票" - total-votes: "計{}票" - vote: "投票ã™ã‚‹" - show-result: "çµæžœã‚’見る" - voted: "投票済ã¿" - closed: "終了済ã¿" - remaining-days: "終了ã¾ã§ã‚ã¨{d}æ—¥{h}時間" - remaining-hours: "終了ã¾ã§ã‚ã¨{h}時間{m}分" - remaining-minutes: "終了ã¾ã§ã‚ã¨{m}分{s}秒" - remaining-seconds: "終了ã¾ã§ã‚ã¨{s}秒" - -common/views/components/poll-editor.vue: - no-only-one-choice: "アンケートã«ã¯ã€é¸æŠžè‚¢ãŒæœ€ä½Ž2ã¤å¿…è¦ã§ã™" - choice-n: "é¸æŠžè‚¢{}" - remove: "ã“ã®é¸æŠžè‚¢ã‚’削除" - add: "+é¸æŠžè‚¢ã‚’è¿½åŠ " - destroy: "ã‚¢ãƒ³ã‚±ãƒ¼ãƒˆã‚’ç ´æ£„" - multiple: "複数回ç”å¯" +_poll: + noOnlyOneChoice: "é¸æŠžè‚¢ã¯æœ€ä½Ž2ã¤å¿…è¦ã§ã™" + choiceN: "é¸æŠžè‚¢{n}" + noMore: "ã“ã‚Œä»¥ä¸Šè¿½åŠ ã§ãã¾ã›ã‚“" + canMultipleVote: "複数回ç”å¯" expiration: "期é™" infinite: "無期é™" at: "日時指定" after: "経éŽæŒ‡å®š" - no-more: "ã“ã‚Œä»¥ä¸Šè¿½åŠ ã§ãã¾ã›ã‚“" - deadline-date: "期日" - deadline-time: "時間" - interval: "期間" - unit: "å˜ä½" - second: "秒" - minute: "分" - hour: "時間" - day: "æ—¥" - -common/views/components/reaction-picker.vue: - choose-reaction: "リアクションをé¸æŠž" - input-reaction-placeholder: "ã¾ãŸã¯çµµæ–‡å—を入力" - -common/views/components/emoji-picker.vue: - recent-emoji: "最近使ã£ãŸçµµæ–‡å—" - custom-emoji: "カスタム絵文å—" - no-category: "カテゴリãªã—" - people: "人" - animals-and-nature: "動物&自然" - food-and-drink: "食ã¹ç‰©&飲ã¿ç‰©" - activity: "アクティビティ" - travel-and-places: "å ´æ‰€" - objects: "物" - symbols: "記å·" - flags: "æ——" - -common/views/components/settings/app-type.vue: - title: "モード" - intro: "デスクトップ版ã¨ãƒ¢ãƒã‚¤ãƒ«ç‰ˆã®ã©ã¡ã‚‰ã‚’使ã†ã‹ã‚’指定ã§ãã¾ã™ã€‚" - choices: - auto: "自動ã§é¸æŠž" - desktop: "デスクトップ版ã«å›ºå®š" - mobile: "モãƒã‚¤ãƒ«ç‰ˆã«å›ºå®š" - info: "変更ã¯ãƒšãƒ¼ã‚¸ã®å†åº¦èªã¿è¾¼ã¿å¾Œã«åæ˜ ã•ã‚Œã¾ã™ã€‚" - -common/views/components/signin.vue: - username: "ユーザーå" - password: "パスワード" - token: "トークン" - signing-in: "ã‚„ã£ã¦ã¾ã™..." - or: "ã¾ãŸã¯" - signin-with-twitter: "Twitterã§ãƒã‚°ã‚¤ãƒ³" - signin-with-github: "GitHubã§ãƒã‚°ã‚¤ãƒ³" - signin-with-discord: "Discordã§ãƒã‚°ã‚¤ãƒ³" - login-failed: "ãƒã‚°ã‚¤ãƒ³ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ユーザーåã¨ãƒ‘スワードを確èªã—ã¦ãã ã•ã„。" - tap-key: "ã‚»ã‚ュリティã‚ーをクリックã—ã¦ãƒã‚°ã‚¤ãƒ³" - enter-2fa-code: "èªè¨¼ã‚³ãƒ¼ãƒ‰ã‚’入力ã—ã¦ãã ã•ã„" - -common/views/components/signup.vue: - invitation-code: "招待コード" - invitation-info: "招待コードをãŠæŒã¡ã§ãªã„æ–¹ã¯ã€<a href=\"{}\">管ç†è€…</a>ã¾ã§ã”連絡ãã ã•ã„。" - username: "ユーザーå" - checking: "確èªã—ã¦ã„ã¾ã™..." - available: "利用ã§ãã¾ã™" - unavailable: "æ—¢ã«åˆ©ç”¨ã•ã‚Œã¦ã„ã¾ã™" - error: "通信エラー" - invalid-format: "a~zã€A~Zã€0~9ã€_ãŒä½¿ãˆã¾ã™" - too-short: "1æ–‡å—以上ã§ãŠé¡˜ã„ã—ã¾ã™ï¼" - too-long: "20æ–‡å—以内ã§ãŠé¡˜ã„ã—ã¾ã™" - password: "パスワード" - password-placeholder: "8æ–‡å—以上を推奨ã—ã¾ã™" - weak-password: "å¼±ã„パスワード" - normal-password: "ã¾ã‚ã¾ã‚ã®ãƒ‘スワード" - strong-password: "å¼·ã„パスワード" - retype: "å†å…¥åŠ›" - retype-placeholder: "確èªã®ãŸã‚å†å…¥åŠ›ã—ã¦ãã ã•ã„" - password-matched: "確èªã•ã‚Œã¾ã—ãŸ" - password-not-matched: "一致ã—ã¦ã„ã¾ã›ã‚“" - recaptcha: "èªè¨¼" - agree-to: "{0}ã«åŒæ„ã—ã¾ã™ã€‚" - tos: "利用è¦ç´„" - create: "アカウント作æˆ" - some-error: "何らã‹ã®åŽŸå› ã«ã‚ˆã‚Šã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚å†åº¦ãŠè©¦ã—ãã ã•ã„。" - -common/views/components/special-message.vue: - new-year: "Happy New Year!" - christmas: "Merry Christmas!" - -common/views/components/stream-indicator.vue: - connecting: "接続ä¸" - reconnecting: "å†æŽ¥ç¶šä¸" - connected: "接続完了" - -common/views/components/notification-settings.vue: - title: "通知" - mark-as-read-all-notifications: "ã™ã¹ã¦ã®é€šçŸ¥ã‚’æ—¢èªã«ã™ã‚‹" - mark-as-read-all-unread-notes: "ã™ã¹ã¦ã®æŠ•ç¨¿ã‚’æ—¢èªã«ã™ã‚‹" - mark-as-read-all-talk-messages: "ã™ã¹ã¦ã®ãƒˆãƒ¼ã‚¯ã‚’æ—¢èªã«ã™ã‚‹" - auto-watch: "投稿ã®è‡ªå‹•ã‚¦ã‚©ãƒƒãƒ" - auto-watch-desc: "リアクションã—ãŸã‚Šè¿”ä¿¡ã—ãŸã‚Šã—ãŸæŠ•ç¨¿ã«é–¢ã™ã‚‹é€šçŸ¥ã‚’自動的ã«å—ã‘å–るよã†ã«ã—ã¾ã™ã€‚" - -common/views/components/instance.vue: - start: 始ã‚ã‚‹ - -common/views/components/integration-settings.vue: - title: "サービス連æº" - connect: "接続ã™ã‚‹" - disconnect: "切æ–ã™ã‚‹" - connected-to: "次ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã™" - -common/views/components/github-setting.vue: - description: "ãŠä½¿ã„ã®GitHubアカウントをãŠä½¿ã„ã®Misskeyアカウントã«æŽ¥ç¶šã—ã¦ãŠãã¨ã€ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã§GitHubã‚¢ã‚«ã‚¦ãƒ³ãƒˆæƒ…å ±ãŒè¡¨ç¤ºã•ã‚Œã‚‹ã‚ˆã†ã«ãªã£ãŸã‚Šã€GitHubを用ã„ãŸä¾¿åˆ©ãªã‚µã‚¤ãƒ³ã‚¤ãƒ³ã‚’利用ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" - connected-to: "次ã®GitHubアカウントã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã™" - detail: "詳細..." - reconnect: "å†æŽ¥ç¶šã™ã‚‹" - connect: "GitHubã¨æŽ¥ç¶šã™ã‚‹" - disconnect: "切æ–ã™ã‚‹" - -common/views/components/discord-setting.vue: - description: "ãŠä½¿ã„ã®DiscordアカウントをãŠä½¿ã„ã®Misskeyアカウントã«æŽ¥ç¶šã—ã¦ãŠãã¨ã€ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã§Discordã‚¢ã‚«ã‚¦ãƒ³ãƒˆæƒ…å ±ãŒè¡¨ç¤ºã•ã‚Œã‚‹ã‚ˆã†ã«ãªã£ãŸã‚Šã€Discordを用ã„ãŸä¾¿åˆ©ãªã‚µã‚¤ãƒ³ã‚¤ãƒ³ã‚’利用ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" - connected-to: "次ã®Discordアカウントã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã™" - detail: "詳細..." - reconnect: "å†æŽ¥ç¶šã™ã‚‹" - connect: "Discordã¨æŽ¥ç¶šã™ã‚‹" - disconnect: "切æ–ã™ã‚‹" - -common/views/components/uploader.vue: - waiting: "å¾…æ©Ÿä¸" - -common/views/components/visibility-chooser.vue: - public: "公開" + deadlineDate: "期日" + deadlineTime: "時間" + duration: "期間" + votesCount: "{n}票" + totalVotes: "計{n}票" + vote: "投票ã™ã‚‹" + showResult: "çµæžœã‚’見る" + voted: "投票済ã¿" + closed: "終了済ã¿" + remainingDays: "終了ã¾ã§ã‚ã¨{d}æ—¥{h}時間" + remainingHours: "終了ã¾ã§ã‚ã¨{h}時間{m}分" + remainingMinutes: "終了ã¾ã§ã‚ã¨{m}分{s}秒" + remainingSeconds: "終了ã¾ã§ã‚ã¨{s}秒" + +_visibility: + public: "パブリック" + publicDescription: "å…¨ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«å…¬é–‹" home: "ホーム" - home-desc: "ホームタイムラインã«ã®ã¿å…¬é–‹" + homeDescription: "ホームタイムラインã®ã¿ã«å…¬é–‹" followers: "フォãƒãƒ¯ãƒ¼" - followers-desc: "自分ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã«ã®ã¿å…¬é–‹" + followersDescription: "自分ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã®ã¿ã«å…¬é–‹" specified: "ダイレクト" - specified-desc: "指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã®ã¿å…¬é–‹" - local-public: "公開 (ãƒãƒ¼ã‚«ãƒ«ã®ã¿)" - local-public-desc: "リモートã¸ã¯å…¬é–‹ã—ãªã„" - local-home: "ホーム(ãƒãƒ¼ã‚«ãƒ«ã®ã¿)" - local-followers: "フォãƒãƒ¯ãƒ¼ (ãƒãƒ¼ã‚«ãƒ«ã®ã¿)" - -common/views/components/trends.vue: - count: "{}人ãŒæŠ•ç¨¿" - empty: "トレンドãªã—" - -common/views/components/language-settings.vue: - title: "表示言語" - pick-language: "言語をé¸æŠž" - recommended: "推奨" - auto: "自動" - specify-language: "言語を指定" - info: "変更ã¯ãƒšãƒ¼ã‚¸ã®å†åº¦èªã¿è¾¼ã¿å¾Œã«åæ˜ ã•ã‚Œã¾ã™ã€‚" + specifiedDescription: "指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ã«å…¬é–‹" + +_postForm: + replyPlaceholder: "ã“ã®æŠ•ç¨¿ã«è¿”ä¿¡..." + quotePlaceholder: "ã“ã®æŠ•ç¨¿ã‚’引用..." + post: "投稿" + _placeholders: + a: "ã„ã¾ã©ã†ã—ã¦ã‚‹ï¼Ÿ" + b: "何ã‹ã‚ã‚Šã¾ã—ãŸã‹ï¼Ÿ" + c: "何をãŠè€ƒãˆã§ã™ã‹ï¼Ÿ" + d: "言ã„ãŸã„ã“ã¨ã¯ï¼Ÿ" + e: "ã“ã“ã«æ›¸ã„ã¦ãã ã•ã„" + f: "ã‚ãªãŸãŒæ›¸ãã®ã‚’å¾…ã£ã¦ã„ã¾ã™..." -common/views/components/profile-editor.vue: - title: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" +_profile: name: "åå‰" - account: "アカウント" - location: "å ´æ‰€" - description: "自己紹介" - you-can-include-hashtags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - language: "言語" - birthday: "誕生日" - avatar: "ã‚¢ãƒã‚¿ãƒ¼" - banner: "ãƒãƒŠãƒ¼" - is-cat: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯Catã§ã™" - is-bot: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯Botã§ã™" - is-locked: "フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã™ã‚‹" - careful-bot: "Botã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã ã‘承èªåˆ¶ã«ã™ã‚‹" - auto-accept-followed: "フォãƒãƒ¼ã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã‚’自動承èªã™ã‚‹" - advanced: "ãã®ä»–" - privacy: "プライãƒã‚·ãƒ¼" - save: "ä¿å˜" - saved: "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’ä¿å˜ã—ã¾ã—ãŸ" - uploading: "アップãƒãƒ¼ãƒ‰ä¸" - upload-failed: "アップãƒãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸ" - unable-to-process: "æ“作を完了ã§ãã¾ã›ã‚“" - avatar-not-an-image: "ã‚¢ãƒã‚¿ãƒ¼ã¨ã—ã¦æŒ‡å®šã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã¯ç”»åƒã§ã¯ã‚ã‚Šã¾ã›ã‚“" - banner-not-an-image: "ãƒãƒŠãƒ¼ã¨ã—ã¦æŒ‡å®šã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã¯ç”»åƒã§ã¯ã‚ã‚Šã¾ã›ã‚“" - email: "メールè¨å®š" - email-address: "メールアドレス" - email-verified: "メールアドレスãŒç¢ºèªã•ã‚Œã¾ã—ãŸ" - email-not-verified: "メールアドレスãŒç¢ºèªã•ã‚Œã¦ã„ã¾ã›ã‚“。メールボックスをã”確èªãã ã•ã„。" - export: "エクスãƒãƒ¼ãƒˆ" - import: "インãƒãƒ¼ãƒˆ" - export-and-import: "エクスãƒãƒ¼ãƒˆã¨ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" - export-targets: - all-notes: "ã™ã¹ã¦ã®æŠ•ç¨¿ãƒ‡ãƒ¼ã‚¿" - following-list: "フォãƒãƒ¼" - mute-list: "ミュート" - blocking-list: "ブãƒãƒƒã‚¯" - user-lists: "リスト" - export-requested: "エクスãƒãƒ¼ãƒˆã‚’リクエストã—ã¾ã—ãŸã€‚ã“ã‚Œã«ã¯æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚エクスãƒãƒ¼ãƒˆãŒçµ‚ã‚ã‚‹ã¨ã€ãƒ‰ãƒ©ã‚¤ãƒ–ã«ãƒ•ã‚¡ã‚¤ãƒ«ãŒè¿½åŠ ã•ã‚Œã¾ã™ã€‚" - import-requested: "インãƒãƒ¼ãƒˆã‚’リクエストã—ã¾ã—ãŸã€‚ã“ã‚Œã«ã¯æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" - enter-password: "パスワードを入力ã—ã¦ãã ã•ã„" - danger-zone: "å±é™ºãªè¨å®š" - delete-account: "アカウントを削除" - account-deleted: "アカウントãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚データãŒæ¶ˆãˆã‚‹ã¾ã§æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" - profile-metadata: "プãƒãƒ•ã‚£ãƒ¼ãƒ«è£œè¶³æƒ…å ±" - metadata-label: "ラベル" - metadata-content: "内容" - -common/views/components/user-list-editor.vue: - users: "ユーザー" - rename: "リストåを変更" - delete: "リストを削除" - remove-user: "ã“ã®ãƒªã‚¹ãƒˆã‹ã‚‰å‰Šé™¤" - delete-are-you-sure: "リスト「$1ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" - deleted: "削除ã—ã¾ã—ãŸ" - add-user: "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ " - -common/views/components/user-group-editor.vue: - users: "メンãƒãƒ¼" - rename: "グループåを変更" - delete: "グループを削除" - transfer: "グループをè²æ¸¡" - transfer-are-you-sure: "グループ「$1ã€ã‚’「@$2ã€ã•ã‚“ã«è²æ¸¡ã—ã¾ã™ã‹ï¼Ÿ" - transferred: "グループをè²æ¸¡ã—ã¾ã—ãŸ" - remove-user: "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã‹ã‚‰å‰Šé™¤" - delete-are-you-sure: "グループ「$1ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" - deleted: "削除ã—ã¾ã—ãŸ" - invite: "招待" - invited: "招待をé€ä¿¡ã—ã¾ã—ãŸ" - -common/views/components/user-lists.vue: - user-lists: "リスト" - create-list: "リストを作æˆ" - list-name: "リストå" - -common/views/components/user-groups.vue: - user-groups: "グループ" - create-group: "グループを作æˆ" - group-name: "グループå" - owned-groups: "自分ã®ã‚°ãƒ«ãƒ¼ãƒ—" - joined-groups: "å‚åŠ ã—ã¦ã„るグループ" - invites: "招待" - accept-invite: "å‚åŠ " - reject-invite: "æ‹’å¦" - -common/views/widgets/broadcast.vue: - fetching: "確èªä¸" - no-broadcasts: "ãŠçŸ¥ã‚‰ã›ã¯ã‚ã‚Šã¾ã›ã‚“" - have-a-nice-day: "良ã„一日をï¼" - next: "次" - prev: "å‰" - -common/views/widgets/calendar.vue: - year: "{}å¹´" - month: "{}月" - day: "{}æ—¥" - today: "今日:" - this-month: "今月:" - this-year: "今年:" - -common/views/widgets/photo-stream.vue: - title: "フォトストリーム" - no-photos: "写真ã¯ã‚ã‚Šã¾ã›ã‚“" - -common/views/widgets/posts-monitor.vue: - title: "投稿ãƒãƒ£ãƒ¼ãƒˆ" - toggle: "表示を切り替ãˆ" - -common/views/widgets/hashtags.vue: - title: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - -common/views/widgets/server.vue: - title: "サーãƒãƒ¼æƒ…å ±" - toggle: "表示を切り替ãˆ" - -common/views/widgets/memo.vue: - title: "付箋" - memo: "ã“ã“ã«æ›¸ã„ã¦ï¼" - save: "ä¿å˜" - -common/views/widgets/slideshow.vue: - folder-customize-mode: "フォルダを指定ã™ã‚‹ã«ã¯ã€ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºãƒ¢ãƒ¼ãƒ‰ã‚’終了ã—ã¦ãã ã•ã„" - folder: "クリックã—ã¦ãƒ•ã‚©ãƒ«ãƒ€ã‚’指定ã—ã¦ãã ã•ã„" - no-image: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã«ã¯ç”»åƒãŒã‚ã‚Šã¾ã›ã‚“" - -common/views/widgets/tips.vue: - tips-line1: "<kbd>t</kbd>ã§ã‚¿ã‚¤ãƒ ラインã«ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ã§ãã¾ã™" - tips-line2: "<kbd>p</kbd>ã¾ãŸã¯<kbd>n</kbd>ã§æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ ã‚’é–‹ãã¾ã™" - tips-line3: "投稿フォームã«ã¯ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグ&ドãƒãƒƒãƒ—ã§ãã¾ã™" - tips-line4: "投稿フォームã«ã‚¯ãƒªãƒƒãƒ—ボードã«ã‚ã‚‹ç”»åƒãƒ‡ãƒ¼ã‚¿ã‚’ペーストã§ãã¾ã™" - tips-line5: "ドライブã«ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグ&ドãƒãƒƒãƒ—ã—ã¦ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã§ãã¾ã™" - tips-line6: "ドライブã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグã—ã¦ãƒ•ã‚©ãƒ«ãƒ€ç§»å‹•ã§ãã¾ã™" - tips-line7: "ドライブã§ãƒ•ã‚©ãƒ«ãƒ€ã‚’ドラッグã—ã¦ãƒ•ã‚©ãƒ«ãƒ€ç§»å‹•ã§ãã¾ã™" - tips-line8: "ホームã¯è¨å®šã‹ã‚‰ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã§ãã¾ã™" - tips-line9: "Misskeyã¯AGPLv3ã§ã™" - tips-line10: "タイムマシンウィジェットを利用ã™ã‚‹ã¨ã€ç°¡å˜ã«éŽåŽ»ã®ã‚¿ã‚¤ãƒ ラインã«é¡ã‚Œã¾ã™" - tips-line11: "投稿㮠... をクリックã—ã¦ã€æŠ•ç¨¿ã‚’ユーザーページã«ãƒ”ン留ã‚ã§ãã¾ã™" - tips-line13: "投稿ã«æ·»ä»˜ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã¯å…¨ã¦ãƒ‰ãƒ©ã‚¤ãƒ–ã«ä¿å˜ã•ã‚Œã¾ã™" - tips-line14: "ホームã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºä¸ã€ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ãƒ‡ã‚¶ã‚¤ãƒ³ã‚’変更ã§ãã¾ã™" - tips-line17: "「**ã€ã§ãƒ†ã‚ストを囲むã¨**強調表示**ã•ã‚Œã¾ã™" - tips-line19: "ã„ãã¤ã‹ã®ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã¯ãƒ–ラウザã®å¤–ã«åˆ‡ã‚Šé›¢ã™ã“ã¨ãŒã§ãã¾ã™" - tips-line20: "カレンダーウィジェットã®ãƒ‘ーセンテージã¯ã€çµŒéŽã®å‰²åˆã‚’示ã—ã¦ã„ã¾ã™" - tips-line21: "APIを利用ã—ã¦botã®é–‹ç™ºãªã©ã‚‚è¡Œãˆã¾ã™" - tips-line23: "è—ã‹ã‚ã„ã„よè—" - tips-line24: "Misskeyã¯2014å¹´ã«ã‚µãƒ¼ãƒ“スを開始ã—ã¾ã—ãŸ" - tips-line25: "対応ブラウザã§ã¯Misskeyã‚’é–‹ã„ã¦ã„ãªãã¦ã‚‚通知をå—ã‘å–ã‚Œã¾ã™" - -common/views/pages/not-found.vue: - page-not-found: "ページãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸ" - -common/views/pages/follow.vue: - signed-in-as: "{}ã¨ã—ã¦ã‚µã‚¤ãƒ³ã‚¤ãƒ³ä¸" - following: "フォãƒãƒ¼ä¸" - follow: "フォãƒãƒ¼" - request-pending: "フォãƒãƒ¼è¨±å¯å¾…ã¡" - follow-processing: "フォãƒãƒ¼å‡¦ç†ä¸" - follow-request: "フォãƒãƒ¼ç”³è«‹" - -common/views/pages/follow-requests.vue: - received-follow-requests: "フォãƒãƒ¼ç”³è«‹" - accept: "承èª" - reject: "æ‹’å¦" - -desktop: - banner-crop-title: "ãƒãƒŠãƒ¼ã¨ã—ã¦è¡¨ç¤ºã™ã‚‹éƒ¨åˆ†ã‚’é¸æŠž" - banner: "ãƒãƒŠãƒ¼" - uploading-banner: "æ–°ã—ã„ãƒãƒŠãƒ¼ã‚’アップãƒãƒ¼ãƒ‰ã—ã¦ã„ã¾ã™" - banner-updated: "ãƒãƒŠãƒ¼ã‚’æ›´æ–°ã—ã¾ã—ãŸ" - choose-banner: "ãƒãƒŠãƒ¼ã«ã™ã‚‹ç”»åƒã‚’é¸æŠž" - avatar-crop-title: "ã‚¢ãƒã‚¿ãƒ¼ã¨ã—ã¦è¡¨ç¤ºã™ã‚‹éƒ¨åˆ†ã‚’é¸æŠž" - avatar: "ã‚¢ãƒã‚¿ãƒ¼" - uploading-avatar: "æ–°ã—ã„ã‚¢ãƒã‚¿ãƒ¼ã‚’アップãƒãƒ¼ãƒ‰ã—ã¦ã„ã¾ã™" - avatar-updated: "ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ã¾ã—ãŸ" - choose-avatar: "ã‚¢ãƒã‚¿ãƒ¼ã«ã™ã‚‹ç”»åƒã‚’é¸æŠž" - unable-to-process: "æ“作を完了ã§ãã¾ã›ã‚“" - invalid-filetype: "ã“ã®å½¢å¼ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“" - -desktop/views/components/activity.chart.vue: - total: "Black ... Total" - notes: "Blue ... Notes" - replies: "Red ... Replies" - renotes: "Green ... Renotes" - -desktop/views/components/activity.vue: - title: "アクティビティ" - toggle: "表示を切り替ãˆ" - -desktop/views/components/calendar.vue: - title: "{year}å¹´ {month}月" - prev: "å‰ã®æœˆ" - next: "次ã®æœˆ" - go: "クリックã—ã¦æ™‚é–“é¡è¡Œ" - -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count}ファイルé¸æŠžä¸" - upload: "PCã‹ã‚‰ãƒ‰ãƒ©ã‚¤ãƒ–ã«ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰" - cancel: "ã‚ャンセル" - ok: "決定" - choose-prompt: "ファイルをé¸æŠž" - -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "ã‚ャンセル" - ok: "決定" - choose-prompt: "フォルダをé¸æŠž" - -desktop/views/components/crop-window.vue: - skip: "クãƒãƒƒãƒ—をスã‚ップ" - cancel: "ã‚ャンセル" - ok: "決定" - -desktop/views/components/drive-window.vue: - used: "使用ä¸" - -desktop/views/components/drive.file.vue: - avatar: "ã‚¢ãƒã‚¿ãƒ¼" - banner: "ãƒãƒŠãƒ¼" - nsfw: "閲覧注æ„" - contextmenu: - rename: "åå‰ã‚’変更" - mark-as-sensitive: "閲覧注æ„ã«è¨å®š" - unmark-as-sensitive: "閲覧注æ„を解除" - copy-url: "URLをコピー" - download: "ダウンãƒãƒ¼ãƒ‰" - else-files: "ãã®ä»–" - set-as-avatar: "ã‚¢ãƒã‚¿ãƒ¼ã«è¨å®š" - set-as-banner: "ãƒãƒŠãƒ¼ã«è¨å®š" - open-in-app: "アプリã§é–‹ã" - add-app: "ã‚¢ãƒ—ãƒªã‚’è¿½åŠ " - rename-file: "ファイルåã®å¤‰æ›´" - input-new-file-name: "æ–°ã—ã„ファイルåを入力ã—ã¦ãã ã•ã„" - copied: "コピー完了" - copied-url-to-clipboard: "URLをクリップボードã«ã‚³ãƒ”ーã—ã¾ã—ãŸ" - -desktop/views/components/drive.folder.vue: - upload-folder: "既定アップãƒãƒ¼ãƒ‰å…ˆ" - unable-to-process: "æ“作を完了ã§ãã¾ã›ã‚“" - circular-reference-detected: "移動先ã®ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã¯ã€ç§»å‹•ã™ã‚‹ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã®ã‚µãƒ–フォルダーã§ã™ã€‚" - unhandled-error: "ä¸æ˜Žãªã‚¨ãƒ©ãƒ¼" - unable-to-delete: "削除ã§ãã¾ã›ã‚“" - has-child-files-or-folders: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã¯ç©ºã§ãªã„ãŸã‚ã€å‰Šé™¤ã§ãã¾ã›ã‚“。" - contextmenu: - move-to-this-folder: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã¸ç§»å‹•" - show-in-new-window: "æ–°ã—ã„ウィンドウã§è¡¨ç¤º" - rename: "åå‰ã‚’変更" - rename-folder: "フォルダåã®å¤‰æ›´" - input-new-folder-name: "æ–°ã—ã„フォルダåを入力ã—ã¦ãã ã•ã„" - else-folders: "ãã®ä»–" - set-as-upload-folder: "既定アップãƒãƒ¼ãƒ‰å…ˆã«è¨å®š" - -desktop/views/components/drive.vue: - search: "検索" - empty-draghover: "ドãƒãƒƒãƒ—ã§ã™ã‹ï¼Ÿã„ã„ã§ã™ã‚ˆã€ãƒœã‚¯ã¯ã‚«ãƒ¯ã‚¤ã‚¤ã§ã™ã‹ã‚‰ã" - empty-drive: "ドライブã«ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“。" - empty-drive-description: "å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã€ã‚’é¸ã‚“ã ã‚Šã€ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグ&ドãƒãƒƒãƒ—ã™ã‚‹ã“ã¨ã§ã‚‚アップãƒãƒ¼ãƒ‰ã§ãã¾ã™ã€‚" - empty-folder: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã¯ç©ºã§ã™" - unable-to-process: "æ“作を完了ã§ãã¾ã›ã‚“" - circular-reference-detected: "移動先ã®ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã¯ã€ç§»å‹•ã™ã‚‹ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã®ã‚µãƒ–フォルダーã§ã™ã€‚" - unhandled-error: "ä¸æ˜Žãªã‚¨ãƒ©ãƒ¼" - url-upload: "URLアップãƒãƒ¼ãƒ‰" - url-of-file: "アップãƒãƒ¼ãƒ‰ã—ãŸã„ファイルã®URL" - url-upload-requested: "アップãƒãƒ¼ãƒ‰ã‚’リクエストã—ã¾ã—ãŸ" - may-take-time: "アップãƒãƒ¼ãƒ‰ãŒå®Œäº†ã™ã‚‹ã¾ã§æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" - create-folder: "フォルダー作æˆ" - folder-name: "フォルダーå" - contextmenu: - create-folder: "フォルダーを作æˆ" - upload: "ファイルをアップãƒãƒ¼ãƒ‰" - url-upload: "URLã‹ã‚‰ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰" - -desktop/views/components/media-video.vue: - sensitive: "閲覧注æ„" - click-to-show: "クリックã—ã¦è¡¨ç¤º" - -desktop/views/components/followers-window.vue: - followers: "{} ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - -desktop/views/components/followers.vue: - empty: "フォãƒãƒ¯ãƒ¼ã¯ã„ãªã„よã†ã§ã™ã€‚" - -desktop/views/components/following-window.vue: - following: "{} ã®ãƒ•ã‚©ãƒãƒ¼" - -desktop/views/components/following.vue: - empty: "フォãƒãƒ¼ä¸ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã„ãªã„よã†ã§ã™ã€‚" - -desktop/views/components/game-window.vue: - game: "リãƒãƒ¼ã‚·" - -desktop/views/components/home.vue: - done: "完了" - add-widget: "ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’è¿½åŠ :" - add: "è¿½åŠ " - -desktop/views/input-dialog.vue: - cancel: "ã‚ャンセル" - ok: "決定" - -desktop/views/components/note-detail.vue: - private: "ã“ã®æŠ•ç¨¿ã¯éžå…¬é–‹ã§ã™" - deleted: "ã“ã®æŠ•ç¨¿ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - location: "ä½ç½®æƒ…å ±" - renote: "Renote" - add-reaction: "リアクション" - undo-reaction: "リアクション解除" - -desktop/views/components/note.vue: - reply: "返信" - renote: "Renote" - add-reaction: "リアクション" - undo-reaction: "リアクション解除" - detail: "詳細" - private: "ã“ã®æŠ•ç¨¿ã¯éžå…¬é–‹ã§ã™" - deleted: "ã“ã®æŠ•ç¨¿ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - -desktop/views/components/notes.vue: - error: "èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚" - retry: "リトライ" - -desktop/views/components/notifications.vue: - empty: "ã‚ã‚Šã¾ã›ã‚“ï¼" - -desktop/views/components/post-form.vue: - posted: "投稿ã—ã¾ã—ãŸï¼" - replied: "返信ã—ã¾ã—ãŸï¼" - reposted: "Renoteã—ã¾ã—ãŸï¼" - note-failed: "投稿ã«å¤±æ•—ã—ã¾ã—ãŸ" - reply-failed: "返信ã«å¤±æ•—ã—ã¾ã—ãŸ" - renote-failed: "Renoteã«å¤±æ•—ã—ã¾ã—ãŸ" - -desktop/views/components/post-form-window.vue: - note: "æ–°è¦æŠ•ç¨¿" - reply: "返信" - attaches: "添付: {}メディア" - uploading-media: "{}個ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’アップãƒãƒ¼ãƒ‰ä¸" - -desktop/views/components/progress-dialog.vue: - waiting: "å¾…æ©Ÿä¸" - -desktop/views/components/renote-form.vue: - quote: "引用ã™ã‚‹..." - cancel: "ã‚ャンセル" - renote: "Renote" - renote-home: "Renote (Home)" - reposting: "ã—ã¦ã„ã¾ã™..." - success: "Renoteã—ã¾ã—ãŸï¼" - failure: "Renoteã«å¤±æ•—ã—ã¾ã—ãŸ" - -desktop/views/components/renote-form-window.vue: - title: "ã“ã®æŠ•ç¨¿ã‚’Renoteã—ã¾ã™ã‹ï¼Ÿ" - -desktop/views/pages/user-following-or-followers.vue: - following: "{user}ã®ãƒ•ã‚©ãƒãƒ¼" - followers: "{user}ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - -desktop/views/components/settings.2fa.vue: - intro: "二段階èªè¨¼ã‚’è¨å®šã™ã‚‹ã¨ã€ã‚µã‚¤ãƒ³ã‚¤ãƒ³æ™‚ã«ãƒ‘スワードã ã‘ã§ãªãã€äºˆã‚登録ã—ã¦ãŠã„ãŸç‰©ç†çš„ãªãƒ‡ãƒã‚¤ã‚¹(例ãˆã°ã‚ãªãŸã®ã‚¹ãƒžãƒ¼ãƒˆãƒ•ã‚©ãƒ³ãªã©)ã‚‚å¿…è¦ã«ãªã‚Šã€ã‚ˆã‚Šã‚»ã‚ュリティãŒå‘上ã—ã¾ã™ã€‚" - detail: "詳細..." - url: "https://www.google.co.jp/intl/ja/landing/2step/" - caution: "登録ã—ãŸãƒ‡ãƒã‚¤ã‚¹ã‚’紛失ã™ã‚‹ãªã©ã—ãŸå ´åˆã€Misskeyã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ããªããªã‚Šã¾ã™ã®ã§ã”注æ„ãã ã•ã„。" - register: "デãƒã‚¤ã‚¹ã‚’登録ã™ã‚‹" - already-registered: "æ—¢ã«è¨å®šã¯å®Œäº†ã—ã¦ã„ã¾ã™ã€‚" - unregister: "è¨å®šã‚’解除" - unregistered: "二段階èªè¨¼ãŒç„¡åŠ¹ã«ãªã‚Šã¾ã—ãŸã€‚" - enter-password: "パスワードを入力ã—ã¦ãã ã•ã„" - authenticator: "ã¾ãšã€Google Authenticatorã‚’ãŠä½¿ã„ã®ãƒ‡ãƒã‚¤ã‚¹ã«ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ã¾ã™:" - howtoinstall: "インストール方法ã¯ã“ã¡ã‚‰" - token: "トークン" - scan: "次ã«ã€è¡¨ç¤ºã•ã‚Œã¦ã„ã‚‹QRコードをスã‚ャンã—ã¾ã™:" - done: "ãŠä½¿ã„ã®ãƒ‡ãƒã‚¤ã‚¹ã«è¡¨ç¤ºã•ã‚Œã¦ã„るトークンを入力ã—ã¦å®Œäº†ã—ã¾ã™:" - submit: "完了" - success: "è¨å®šãŒå®Œäº†ã—ã¾ã—ãŸï¼" - failed: "è¨å®šã«å¤±æ•—ã—ã¾ã—ãŸã€‚トークンã«èª¤ã‚ŠãŒãªã„ã‹ã”確èªãã ã•ã„。" - info: "次回サインインã‹ã‚‰ã¯ã€åŒæ§˜ã«ãƒ‘スワードã«åŠ ãˆã¦ãƒ‡ãƒã‚¤ã‚¹ã«è¡¨ç¤ºã•ã‚Œã¦ã„るトークンを入力ã—ã¾ã™ã€‚" - totp-header: "èªè¨¼ã‚¢ãƒ—リ" - security-key-header: "ã‚»ã‚ュリティã‚ー" - security-key: "ã‚»ã‚ュリティを強化ã™ã‚‹ãŸã‚ã«ã€FIDO2をサãƒãƒ¼ãƒˆã™ã‚‹ãƒãƒ¼ãƒ‰ã‚¦ã‚§ã‚¢ã‚»ã‚ュリティã‚ーを使用ã—ã¦ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ãƒã‚°ã‚¤ãƒ³ã§ãã¾ã™ã€‚ サインインã®éš›ã¯ã€ç™»éŒ²ã•ã‚ŒãŸã‚»ã‚ュリティã‚ーã¾ãŸã¯èªè¨¼ã‚¢ãƒ—リãŒå¿…è¦ã«ãªã‚Šã¾ã™ã€‚" - last-used: "最後ã®ä½¿ç”¨:" - activate-key: "クリックã—ã¦ã‚»ã‚ュリティã‚ーをアクティベートã—ã¦ãã ã•ã„" - security-key-name: "ã‚ーå" - register-security-key: "ã‚ーã®ç™»éŒ²ã‚’完了" - something-went-wrong: "ã‚ãƒ¼ï¼ ã‚ーを登録ã™ã‚‹éš›ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ:" - key-unregistered: "ã‚ーãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - use-password-less-login: "パスワードãªã—ã®ãƒã‚°ã‚¤ãƒ³ã‚’使用" - -common/views/components/media-image.vue: - sensitive: "閲覧注æ„" - click-to-show: "クリックã—ã¦è¡¨ç¤º" - -common/views/components/api-settings.vue: - intro: "APIを利用ã™ã‚‹ã«ã¯ã€ä¸Šè¨˜ã®ãƒˆãƒ¼ã‚¯ãƒ³ã‚’「iã€ã¨ã„ã†ã‚ーã§ãƒ‘ラメータã«ä»˜åŠ ã—ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã—ã¾ã™ã€‚" - caution: "アカウントをä¸æ£åˆ©ç”¨ã•ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚ã€ã“ã®ãƒˆãƒ¼ã‚¯ãƒ³ã¯ç¬¬ä¸‰è€…ã«æ•™ãˆãªã„ã§ãã ã•ã„(アプリãªã©ã«ã‚‚入力ã—ãªã„ã§ãã ã•ã„)。" - regeneration-of-token: "万ãŒä¸€ã“ã®ãƒˆãƒ¼ã‚¯ãƒ³ãŒæ¼ã‚ŒãŸã‚Šãã®å¯èƒ½æ€§ãŒã‚ã‚‹å ´åˆã¯ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å†ç”Ÿæˆã§ãã¾ã™ã€‚" - regenerate-token: "トークンをå†ç”Ÿæˆ" - token: "Token:" - enter-password: "パスワードを入力ã—ã¦ãã ã•ã„" - console: - title: "APIコンソール" - endpoint: "エンドãƒã‚¤ãƒ³ãƒˆ" - parameter: "パラメータ" - credential-info: "「iã€ãƒ‘ラメータã¯è‡ªå‹•ã§ä»˜ä¸Žã•ã‚Œã¾ã™ã€‚" - send: "é€ä¿¡" - sending: "å¿œç”å¾…ã¡" - response: "çµæžœ" - -desktop/views/components/settings.apps.vue: - no-apps: "連æºã—ã¦ã„るアプリケーションã¯ã‚ã‚Šã¾ã›ã‚“" - -common/views/components/drive-settings.vue: - max: "容é‡" - in-use: "使用ä¸" - stats: "統計" - default-upload-folder: "既定ã®ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰å…ˆãƒ•ã‚©ãƒ«ãƒ€" - default-upload-folder-name: "フォルダ" - change-default-upload-folder: "フォルダを変更" - -common/views/components/mute-and-block.vue: - mute-and-block: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" - mute: "ミュート" - block: "ブãƒãƒƒã‚¯" - no-muted-users: "ミュートã—ã¦ã„るユーザーã¯ã„ã¾ã›ã‚“" - no-blocked-users: "ブãƒãƒƒã‚¯ã—ã¦ã„るユーザーã¯ã„ã¾ã›ã‚“" - word-mute: "ワードミュート" - muted-words: "ミュートã•ã‚ŒãŸã‚ーワード" - muted-words-description: "スペースã§åŒºåˆ‡ã‚‹ã¨AND指定ã«ãªã‚Šã€æ”¹è¡Œã§åŒºåˆ‡ã‚‹ã¨OR指定ã«ãªã‚Šã¾ã™" - unmute-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ミュート解除ã—ã¾ã™ã‹ï¼Ÿ" - unblock-confirm: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ブãƒãƒƒã‚¯è§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" - save: "ä¿å˜" - -common/views/components/password-settings.vue: - reset: "パスワードを変更ã™ã‚‹" - enter-current-password: "ç¾åœ¨ã®ãƒ‘スワードを入力ã—ã¦ãã ã•ã„" - enter-new-password: "æ–°ã—ã„パスワードを入力ã—ã¦ãã ã•ã„" - enter-new-password-again: "ã‚‚ã†ä¸€åº¦æ–°ã—ã„パスワードを入力ã—ã¦ãã ã•ã„" - not-match: "æ–°ã—ã„パスワードãŒä¸€è‡´ã—ã¾ã›ã‚“" - changed: "パスワードを変更ã—ã¾ã—ãŸ" - failed: "パスワード変更ã«å¤±æ•—ã—ã¾ã—ãŸ" - -common/views/components/post-form-attaches.vue: - attach-cancel: "添付å–り消ã—" - mark-as-sensitive: "閲覧注æ„ã«è¨å®š" - unmark-as-sensitive: "閲覧注æ„を解除" - -desktop/views/components/sub-note-content.vue: - private: "ã“ã®æŠ•ç¨¿ã¯éžå…¬é–‹ã§ã™" - deleted: "ã“ã®æŠ•ç¨¿ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - media-count: "{}ã¤ã®ãƒ¡ãƒ‡ã‚£ã‚¢" - poll: "アンケート" - -desktop/views/components/settings.tags.vue: - title: "ã‚¿ã‚°" - query: "クエリ (çœç•¥å¯)" - add: "è¿½åŠ " - save: "ä¿å˜" - -desktop/views/components/timeline.vue: - home: "ホーム" - local: "ãƒãƒ¼ã‚«ãƒ«" - hybrid: "ソーシャル" - global: "ã‚°ãƒãƒ¼ãƒãƒ«" - mentions: "ã‚ãªãŸå®›ã¦" - messages: "ダイレクト投稿" - list: "リスト" - hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - add-tag-timeline: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’è¿½åŠ " - add-list: "ãƒªã‚¹ãƒˆã‚’è¿½åŠ " - list-name: "リストå" - -desktop/views/components/ui.header.vue: - welcome-back: "ãŠã‹ãˆã‚Šãªã•ã„ã€" - adjective: "ã•ã‚“" - -desktop/views/components/ui.header.account.vue: - profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - lists: "リスト" - groups: "グループ" - follow-requests: "フォãƒãƒ¼ç”³è«‹" - admin: "管ç†" - room: "ルーム" - -desktop/views/components/ui.header.nav.vue: - game: "ゲーム" - -desktop/views/components/ui.header.notifications.vue: - title: "通知" - -desktop/views/components/ui.header.post.vue: - post: "æ–°è¦æŠ•ç¨¿" - -desktop/views/components/ui.header.search.vue: - placeholder: "検索" - -desktop/views/components/user-preview.vue: - notes: "投稿" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - -desktop/views/components/users-list.vue: - all: "ã™ã¹ã¦" - iknow: "知りåˆã„" - fetching: "èªã¿è¾¼ã‚“ã§ã„ã¾ã™" - -desktop/views/components/users-list-item.vue: - followed: "フォãƒãƒ¼ã•ã‚Œã¦ã„ã¾ã™" - -desktop/views/components/window.vue: - popout: "ãƒãƒƒãƒ—アウト" - close: "é–‰ã˜ã‚‹" - -admin/views/index.vue: - dashboard: "ダッシュボード" - instance: "インスタンス" - emoji: "カスタム絵文å—" - moderators: "モデレーター" - users: "ユーザー" - federation: "連åˆ" - announcements: "ãŠçŸ¥ã‚‰ã›" - abuse: "ã‚¹ãƒ‘ãƒ å ±å‘Š" - queue: "ジョブã‚ュー" - logs: "ãƒã‚°" - db: "データベース" - back-to-misskey: "Misskeyã«æˆ»ã‚‹" - -admin/views/db.vue: - tables: "テーブル" - vacuum: "ãƒã‚ューム" - vacuum-info: "データベースã®æŽƒé™¤ã‚’è¡Œã„ã¾ã™ã€‚データã¯ãã®ã¾ã¾ã§ã€ãƒ‡ã‚£ã‚¹ã‚¯ä½¿ç”¨é‡ã‚’減らã—ã¾ã™ã€‚通常ã“ã®æ“作ã¯è‡ªå‹•ã§å®šæœŸçš„ã«è¡Œã‚ã‚Œã¾ã™ã€‚" - vacuum-exclamation: "ãƒã‚ュームを行ã†ã¨ã€ã—ã°ã‚‰ãã®é–“データベースã®è² è·ãŒé«˜ããªã‚Šã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ“作をå—ã‘付ã‘ãªããªã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" - -admin/views/dashboard.vue: - dashboard: "ダッシュボード" - accounts: "アカウント" - notes: "投稿" - drive: "ドライブ" - instances: "インスタンス" - this-instance: "ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹" - federated: "連åˆ" - -admin/views/queue.vue: - title: "ã‚ュー" - remove-all-jobs: "ã™ã¹ã¦ã®ã‚¸ãƒ§ãƒ–をクリア" - jobs: "ジョブ" - queue: "ã‚ュー" - domains: - deliver: "é…é€" - inbox: "å—ä¿¡" - db: "データベース" - objectStorage: "オブジェクトストレージ" - state: "状態" - states: - active: "処ç†ä¸" - delayed: "予約済ã¿" - waiting: "é †ç•ªå¾…ã¡" - result-is-truncated: "çµæžœã¯çœç•¥ã•ã‚Œã¦ã„ã¾ã™" - other-queues: "ãã®ä»–ã®ã‚ュー" - -admin/views/logs.vue: - logs: "ãƒã‚°" - domain: "ドメイン" - level: "レベル" - levels: - all: "å…¨ã¦" - info: "æƒ…å ±" - success: "æˆåŠŸ" - warning: "è¦å‘Š" - error: "エラー" - debug: "デãƒãƒƒã‚°" - delete-all: "å…¨ã¦å‰Šé™¤" - -admin/views/abuse.vue: - title: "ã‚¹ãƒ‘ãƒ å ±å‘Š" - target: "対象" - reporter: "å ±å‘Šè€…" - details: "詳細" - remove-report: "削除" - -admin/views/instance.vue: - instance: "インスタンス" - instance-name: "インスタンスå" - instance-description: "インスタンスã®ç´¹ä»‹" - host: "ホスト" - icon-url: "アイコンURL" - logo-url: "ãƒã‚´URL" - banner-url: "ãƒãƒŠãƒ¼ç”»åƒURL" - error-image-url: "エラー画åƒURL" - languages: "インスタンスã®å¯¾è±¡è¨€èªž" - languages-desc: "スペースã§åŒºåˆ‡ã£ã¦è¤‡æ•°è¨å®šã§ãã¾ã™ã€‚" - tos-url: "利用è¦ç´„URL" - repository-url: "リãƒã‚¸ãƒˆãƒªURL" - feedback-url: "フィードãƒãƒƒã‚¯URL" - maintainer-config: "管ç†è€…æƒ…å ±" - maintainer-name: "管ç†è€…å" - maintainer-email: "管ç†è€…ã®é€£çµ¡å…ˆ" - advanced-config: "ãã®ä»–ã®è¨å®š" - note-and-tl: "投稿ã¨ã‚¿ã‚¤ãƒ ライン" - drive-config: "ドライブã®è¨å®š" - use-object-storage: "オブジェクトストレージを使用ã™ã‚‹" - object-storage-base-url: "URL" - object-storage-bucket: "ãƒã‚±ãƒƒãƒˆå" - object-storage-prefix: "プレフィックス" - object-storage-endpoint: "エンドãƒã‚¤ãƒ³ãƒˆ" - object-storage-region: "リージョン" - object-storage-port: "ãƒãƒ¼ãƒˆ" - object-storage-access-key: "アクセスã‚ー" - object-storage-secret-key: "シークレットã‚ー" - object-storage-use-ssl: "SSLを使用" - object-storage-s3-info: "Amazon S3をオブジェクトストレージã¨ã—ã¦ä½¿ç”¨ã™ã‚‹å ´åˆã®ã€Œã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã€ã¨ã€Œãƒªãƒ¼ã‚¸ãƒ§ãƒ³ã€ã®è¨å®šã«ã¤ã„ã¦ã¯{0}ã‚’ã”確èªãã ã•ã„。" - object-storage-s3-info-here: "ã“ã¡ã‚‰" - object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージã¨ã—ã¦ä½¿ç”¨ã™ã‚‹å ´åˆã€ã€Œã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã€ã¯ storage.googleapis.com ã«è¨å®šã—ã€ã€Œãƒªãƒ¼ã‚¸ãƒ§ãƒ³ã€ã¯ç©ºæ¬„ã«ã—ã¾ã™ã€‚" - cache-remote-files: "リモートã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã‚ャッシュã™ã‚‹" - cache-remote-files-desc: "ã“ã®è¨å®šã‚’無効ã«ã™ã‚‹ã¨ã€ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã‚ャッシュã›ãšç›´ãƒªãƒ³ã‚¯ã™ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚ãã®ãŸã‚サーãƒãƒ¼ã®ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã‚’節約ã§ãã¾ã™ãŒã€ãƒ—ライãƒã‚·ãƒ¼è¨å®šã§ç›´ãƒªãƒ³ã‚¯ã‚’無効ã«ã—ã¦ã„るユーザーã«ã¯ãƒ•ã‚¡ã‚¤ãƒ«ãŒè¦‹ãˆãªããªã£ãŸã‚Šã€ã‚µãƒ ãƒã‚¤ãƒ«ãŒç”Ÿæˆã•ã‚Œãªã„ã®ã§é€šä¿¡é‡ãŒå¢—åŠ ã—ã¾ã™ã€‚通常ã¯ã“ã®è¨å®šã‚’オンã«ã™ã‚‹ã‹æ¬¡ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã®ãƒ—ãƒã‚シを有効ã«ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚" - proxy-remote-files: "リモートã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’プãƒã‚ã‚·ã™ã‚‹" - proxy-remote-files-desc: "ã“ã®è¨å®šã‚’有効ã«ã™ã‚‹ã¨ã€æœªä¿å˜ã¾ãŸã¯ä¿å˜å®¹é‡è¶…éŽã§å‰Šé™¤ã•ã‚ŒãŸãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’ãƒãƒ¼ã‚«ãƒ«ã§ãƒ—ãƒã‚ã‚·ã—ã€ã‚µãƒ ãƒã‚¤ãƒ«ã‚‚生æˆã™ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚" - local-drive-capacity-mb: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" - remote-drive-capacity-mb: "リモートユーザーã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" - mb: "メガãƒã‚¤ãƒˆå˜ä½" - recaptcha-config: "reCAPTCHAã®è¨å®š" - recaptcha-info: "reCAPTCHAを有効ã«ã™ã‚‹å ´åˆã€reCAPTCHAトークンをå–å¾—ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚https://www.google.com/recaptcha/intro/ ã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã—ã¦ãã ã•ã„。" - recaptcha-info2: "v3ã¯éžå¯¾å¿œã§ã™ã€‚v2を使用ã—ã¦ãã ã•ã„。" - enable-recaptcha: "reCAPTCHAを有効ã«ã™ã‚‹" - recaptcha-site-key: "サイトã‚ー" - recaptcha-secret-key: "シークレットã‚ー" - recaptcha-preview: "プレビュー" - hidden-tags: "éžè¡¨ç¤ºãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - hidden-tags-info: "集計ã‹ã‚‰é™¤å¤–ã™ã‚‹ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’改行ã§åŒºåˆ‡ã£ã¦è¨˜è¿°ã—ã¾ã™ã€‚" - external-service-integration-config: "外部サービス連æº" - twitter-integration-config: "Twitter連æºã®è¨å®š" - twitter-integration-info: "コールãƒãƒƒã‚¯URL㯠{url} ã«è¨å®šã—ã¾ã™ã€‚" - enable-twitter-integration: "Twitter連æºã‚’有効ã«ã™ã‚‹" - twitter-integration-consumer-key: "Consumer key" - twitter-integration-consumer-secret: "Consumer secret" - github-integration-config: "GitHub連æºã®è¨å®š" - github-integration-info: "コールãƒãƒƒã‚¯URL㯠{url} ã«è¨å®šã—ã¾ã™ã€‚" - enable-github-integration: "GitHub連æºã‚’有効ã«ã™ã‚‹" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-config: "Discord連æºã®è¨å®š" - discord-integration-info: "コールãƒãƒƒã‚¯URL㯠{url} ã«è¨å®šã—ã¾ã™ã€‚" - enable-discord-integration: "Discord連æºã‚’有効ã«ã™ã‚‹" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - proxy-account-config: "プãƒã‚シアカウントã®è¨å®š" - proxy-account-info: "プãƒã‚シアカウントã¯ã€ç‰¹å®šã®æ¡ä»¶ä¸‹ã§ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚©ãƒãƒ¼ã‚’代行ã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã™ã€‚例ãˆã°ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’リストã«å…¥ã‚ŒãŸã¨ãã€ãƒªã‚¹ãƒˆã«å…¥ã‚Œã‚‰ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’誰もフォãƒãƒ¼ã—ã¦ã„ãªã„ã¨ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティãŒã‚µãƒ¼ãƒãƒ¼ã«é…é”ã•ã‚Œãªã„ãŸã‚ã€ä»£ã‚ã‚Šã«ãƒ—ãƒã‚シアカウントãŒãƒ•ã‚©ãƒãƒ¼ã™ã‚‹ã‚ˆã†ã«ã—ã¾ã™ã€‚" - proxy-account-username: "プãƒã‚シアカウントã®ãƒ¦ãƒ¼ã‚¶ãƒ¼å" - proxy-account-username-desc: "プãƒã‚ã‚·ã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを指定ã—ã¦ãã ã•ã„。" - proxy-account-warn: "アカウントã¯è‡ªå‹•ã§ä½œã‚‰ã‚Œãªã„ãŸã‚ã€ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’予ã‚作æˆã—ã¦ãŠãå¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" - max-note-text-length: "投稿ã®æœ€å¤§æ–‡å—æ•°" - disable-registration: "ユーザー登録ã®å—付をåœæ¢ã™ã‚‹" - disable-local-timeline: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを無効ã«ã™ã‚‹" - disable-global-timeline: "ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインを無効ã«ã™ã‚‹" - disabling-timelines-info: "ã“れらã®ã‚¿ã‚¤ãƒ ラインを無効ã«ã—ã¦ã‚‚ã€ç®¡ç†è€…ãŠã‚ˆã³ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã¯å¼•ã続ã利用ã§ãã¾ã™ã€‚" - enable-emoji-reaction: "リアクションã«çµµæ–‡å—を使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹" - use-star-for-reaction-fallback: "ä¸æ˜Žãªãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ã« star を使ã†" - invite: "招待" - save: "ä¿å˜" - saved: "ä¿å˜ã—ã¾ã—ãŸ" - pinned-users: "ピン留ã‚ユーザー" - pinned-users-info: "ピン留ã‚ã—ãŸã„ユーザーを改行ã§åŒºåˆ‡ã£ã¦è¨˜è¿°ã—ã¾ã™ã€‚" - email-config: "メールサーãƒãƒ¼ã®è¨å®š" - email-config-info: "メールアドレス確èªã‚„パスワードリセットã®éš›ã«ä½¿ã‚ã‚Œã¾ã™ã€‚" - enable-email: "メールé…信を有効ã«ã™ã‚‹" - email: "メールアドレス" - smtp-secure: "SMTP接続ã«æš—黙的ãªSSL/TLSを使用ã™ã‚‹" - smtp-secure-info: "STARTTLS使用時ã¯ã‚ªãƒ•ã«ã—ã¾ã™ã€‚" - smtp-host: "SMTPホスト" - smtp-port: "SMTPãƒãƒ¼ãƒˆ" - smtp-auth: "SMTPèªè¨¼ã‚’è¡Œã†" - smtp-user: "SMTPユーザー" - smtp-pass: "SMTPパスワード" - test-email: "テスト" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "ServiceWorkerを有効ã«ã™ã‚‹" - serviceworker-info: "プッシュ通知を行ã†ã«ã¯æœ‰åŠ¹ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" - vapid-publickey: "VAPID公開éµ" - vapid-privatekey: "VAPID秘密éµ" - vapid-info: "ServiceWorkerを有効ã«ã™ã‚‹å ´åˆã€VAPIDã‚ーペアを生æˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚シェルã§æ¬¡ã®ã‚ˆã†ã«ã—ã¾ã™:" - -admin/views/charts.vue: - title: "ãƒãƒ£ãƒ¼ãƒˆ" - per-day: "1æ—¥ã”ã¨" - per-hour: "1時間ã”ã¨" - federation: "フェデレーション" - notes: "投稿" - users: "ユーザー" - drive: "ドライブ" - network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" - charts: - federation-instances: "インスタンスã®å¢—減" - federation-instances-total: "インスタンスã®ç©ç®—" - notes: "投稿ã®å¢—減 (çµ±åˆ)" - local-notes: "投稿ã®å¢—減 (ãƒãƒ¼ã‚«ãƒ«)" - remote-notes: "投稿ã®å¢—減 (リモート)" - notes-total: "投稿ã®ç©ç®—" - users: "ユーザーã®å¢—減" - users-total: "ユーザーã®ç©ç®—" - active-users: "アクティブユーザー数" - drive: "ドライブ使用é‡ã®å¢—減" - drive-total: "ドライブ使用é‡ã®ç©ç®—" - drive-files: "ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•°ã®å¢—減" - drive-files-total: "ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•°ã®ç©ç®—" - network-requests: "リクエスト" - network-time: "å¿œç”時間" - network-usage: "通信é‡" - -admin/views/drive.vue: - operation: "æ“作" - fileid-or-url: "ファイルIDã¾ãŸã¯ãƒ•ã‚¡ã‚¤ãƒ«URL" - file-not-found: "ファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" - lookup: "照会" - sort: - title: "ソート" - createdAtAsc: "アップãƒãƒ¼ãƒ‰æ—¥æ™‚ãŒå¤ã„é †" - createdAtDesc: "アップãƒãƒ¼ãƒ‰æ—¥æ™‚ãŒæ–°ã—ã„é †" - sizeAsc: "サイズãŒå°ã•ã„é †" - sizeDesc: "サイズãŒå¤§ãã„é †" - origin: - title: "オリジン" - combined: "ãƒãƒ¼ã‚«ãƒ«+リモート" - local: "ãƒãƒ¼ã‚«ãƒ«" - remote: "リモート" - delete: "削除" - deleted: "削除ã—ã¾ã—ãŸ" - mark-as-sensitive: "閲覧注æ„ã«è¨å®š" - unmark-as-sensitive: "閲覧注æ„を解除" - marked-as-sensitive: "閲覧注æ„ã«è¨å®šã—ã¾ã—ãŸ" - unmarked-as-sensitive: "閲覧注æ„を解除ã—ã¾ã—ãŸ" - clean-remote-files: "リモートファイルã®ã‚ャッシュを削除" - clean-remote-files-are-you-sure: "ã™ã¹ã¦ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã®ã‚ャッシュを削除ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" - clean-up: "クリーンアップ" - -admin/views/users.vue: - operation: "æ“作" - username-or-userid: "ユーザーåã¾ãŸã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼ID" - user-not-found: "ユーザーãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" - lookup: "照会" - reset-password: "パスワードをリセット" - reset-password-confirm: "パスワードをリセットã—ã¾ã™ã‹ï¼Ÿ" - password-updated: "パスワードã¯ç¾åœ¨ã€Œ{password}ã€ã§ã™" - suspend: "å‡çµ" - suspend-confirm: "å‡çµã—ã¾ã™ã‹ï¼Ÿ" - suspended: "å‡çµã—ã¾ã—ãŸ" - unsuspend: "å‡çµã®è§£é™¤" - unsuspend-confirm: "å‡çµã‚’解除ã—ã¾ã™ã‹ï¼Ÿ" - unsuspended: "å‡çµã‚’解除ã—ã¾ã—ãŸ" - make-silence: "サイレンス" - silence-confirm: "サイレンスã—ã¾ã™ã‹ï¼Ÿ" - unmake-silence: "サイレンスã®è§£é™¤" - unsilence-confirm: "サイレンスを解除ã—ã¾ã™ã‹ï¼Ÿ" - update-remote-user: "ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±ã®æ›´æ–°" - remote-user-updated: "ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±ã‚’æ›´æ–°ã—ã¾ã—ãŸ" - delete-all-files: "ã™ã¹ã¦ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’削除" - delete-all-files-confirm: "ã™ã¹ã¦ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" username: "ユーザーå" - host: "ホスト" - users: - title: "ユーザー" - sort: - title: "ソート" - createdAtAsc: "登録日時ãŒå¤ã„é †" - createdAtDesc: "登録日時ãŒæ–°ã—ã„é †" - updatedAtAsc: "更新日時ãŒå¤ã„é †" - updatedAtDesc: "更新日時ãŒæ–°ã—ã„é †" - state: - title: "状態" - all: "ã™ã¹ã¦" - available: "利用å¯èƒ½" - admin: "管ç†è€…" - moderator: "モデレーター" - adminOrModerator: "管ç†è€…+モデレーター" - silenced: "サイレンス済ã¿" - suspended: "å‡çµæ¸ˆã¿" - origin: - title: "オリジン" - combined: "ãƒãƒ¼ã‚«ãƒ«+リモート" - local: "ãƒãƒ¼ã‚«ãƒ«" - remote: "リモート" - createdAt: "登録日時" - updatedAt: "更新日時" - -admin/views/moderators.vue: - add-moderator: - title: "モデレーターã®ç™»éŒ²" - add: "登録" - added: "モデレーターを登録ã—ã¾ã—ãŸ" - remove: "解除" - removed: "モデレーター登録を解除ã—ã¾ã—ãŸ" - logs: - title: "ãƒã‚°" - moderator: "モデレーター" - type: "æ“作" - at: "日時" - info: "æƒ…å ±" - -admin/views/emoji.vue: - add-emoji: - title: "絵文å—ã®ç™»éŒ²" - name: "絵文å—å" - name-desc: "a~z 0~9 _ ã®æ–‡å—ãŒä½¿ãˆã¾ã™ã€‚" - category: "カテゴリ" - aliases: "エイリアス" - aliases-desc: "スペースã§åŒºåˆ‡ã£ã¦è¤‡æ•°è¨å®šã§ãã¾ã™ã€‚" - url: "絵文å—ç”»åƒURL" - add: "è¿½åŠ " - info: "50KB以下ã®PNGç”»åƒã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚" - added: "絵文å—を登録ã—ã¾ã—ãŸ" - emojis: - title: "絵文å—一覧" - update: "æ›´æ–°" - remove: "削除" - updated: "æ›´æ–°ã—ã¾ã—ãŸ" - remove-emoji: - are-you-sure: "「$1ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" - removed: "削除ã—ã¾ã—ãŸ" - -admin/views/announcements.vue: - announcements: "ãŠçŸ¥ã‚‰ã›" - save: "ä¿å˜" - remove: "削除" - add: "è¿½åŠ " - title: "タイトル" - text: "内容" - saved: "ä¿å˜ã—ã¾ã—ãŸ" - _remove: - are-you-sure: "「$1ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" - removed: "削除ã—ã¾ã—ãŸ" - -admin/views/hashtags.vue: - hided-tags: "Hidden Tags" - -admin/views/federation.vue: - instance: "インスタンス" - host: "ホスト" - notes: "投稿" - users: "ユーザー" - following: "フォãƒãƒ¼ä¸" - followers: "フォãƒãƒ¯ãƒ¼" - caught-at: "登録日時" - status: "ステータス" - latest-request-sent-at: "ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆé€ä¿¡" - latest-request-received-at: "ç›´è¿‘ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆå—ä¿¡" - remove-all-following: "フォãƒãƒ¼ã‚’全解除" - remove-all-following-info: "{host}ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã‚’ã™ã¹ã¦è§£é™¤ã—ã¾ã™ã€‚ãã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ãŒã‚‚ã†å˜åœ¨ã—ãªããªã£ãŸå ´åˆãªã©ã«å®Ÿè¡Œã—ã¦ãã ã•ã„。" - delete-all-files: "ファイルをã™ã¹ã¦å‰Šé™¤" - block: "ブãƒãƒƒã‚¯" - marked-as-closed: "閉鎖ã•ã‚Œã¦ã„ã‚‹ã¨ãƒžãƒ¼ã‚¯" - lookup: "照会" - instances: "連åˆ" - instance-not-registered: "ãã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ç™»éŒ²ã•ã‚Œã¦ã„ã¾ã›ã‚“" - sort: "ソート" - sorts: - caughtAtAsc: "登録日時ãŒå¤ã„é †" - caughtAtDesc: "登録日時ãŒæ–°ã—ã„é †" - lastCommunicatedAtAsc: "最後ã«ã‚„ã‚Šå–ã‚Šã—ãŸæ—¥æ™‚ãŒå¤ã„é †" - lastCommunicatedAtDesc: "最後ã«ã‚„ã‚Šå–ã‚Šã—ãŸæ—¥æ™‚ãŒæ–°ã—ã„é †" - notesAsc: "投稿ãŒå°‘ãªã„é †" - notesDesc: "投稿ãŒå¤šã„é †" - usersAsc: "ユーザーãŒå°‘ãªã„é †" - usersDesc: "ユーザーãŒå¤šã„é †" - followingAsc: "フォãƒãƒ¼ãŒå°‘ãªã„é †" - followingDesc: "フォãƒãƒ¼ãŒå¤šã„é †" - followersAsc: "フォãƒãƒ¯ãƒ¼ãŒå°‘ãªã„é †" - followersDesc: "フォãƒãƒ¯ãƒ¼ãŒå¤šã„é †" - driveUsageAsc: "ドライブ使用é‡ãŒå°‘ãªã„é †" - driveUsageDesc: "ドライブ使用é‡ãŒå¤šã„é †" - driveFilesAsc: "ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•°ãŒå°‘ãªã„é †" - driveFilesDesc: "ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•°ãŒå¤šã„é †" - state: "状態" - states: - all: "ã™ã¹ã¦" - blocked: "ブãƒãƒƒã‚¯" - not-responding: "å¿œç”ãªã—" - marked-as-closed: "閉鎖ã¨ãƒžãƒ¼ã‚¯æ¸ˆã¿" - result-is-truncated: "上ä½{n}件を表示ã—ã¦ã„ã¾ã™ã€‚" - charts: "ãƒãƒ£ãƒ¼ãƒˆ" - chart-srcs: - requests: "リクエスト" - users: "ユーザーã®å¢—減" - users-total: "ユーザーã®ç©ç®—" - notes: "投稿ã®å¢—減" - notes-total: "投稿ã®ç©ç®—" - ff: "フォãƒãƒ¼/フォãƒãƒ¯ãƒ¼ã®å¢—減" - ff-total: "フォãƒãƒ¼/フォãƒãƒ¯ãƒ¼ã®ç©ç®—" - drive-usage: "ドライブ使用é‡ã®å¢—減" - drive-usage-total: "ドライブ使用é‡ã®ç©ç®—" - drive-files: "ドライブファイル数ã®å¢—減" - drive-files-total: "ドライブファイル数ã®ç©ç®—" - chart-spans: - hour: "1時間ã”ã¨" - day: "1æ—¥ã”ã¨" - blocked-hosts: "ブãƒãƒƒã‚¯" - blocked-hosts-info: "ブãƒãƒƒã‚¯ã—ãŸã„ホストを改行ã§åŒºåˆ‡ã£ã¦è¨˜è¿°ã—ã¾ã™ã€‚" - save: "ä¿å˜" - -desktop/views/pages/welcome.vue: - about: "詳ã—ã..." - timeline: "タイムライン" - announcements: "ãŠçŸ¥ã‚‰ã›" - photos: "最近ã®ç”»åƒ" - powered-by-misskey: "Powered by <b>Misskey</b>." - info: "æƒ…å ±" - -desktop/views/pages/drive.vue: - title: "Misskey Drive" - -desktop/views/pages/note.vue: - prev: "å‰ã®æŠ•ç¨¿" - next: "次ã®æŠ•ç¨¿" - -desktop/views/pages/selectdrive.vue: - title: "ファイルをé¸æŠžã—ã¦ãã ã•ã„" - ok: "決定" - cancel: "ã‚ャンセル" - upload: "PCã‹ã‚‰ãƒ‰ãƒ©ã‚¤ãƒ–ã«ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰" - -desktop/views/pages/search.vue: - not-available: "検索機能ã¯ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®è¨å®šã§ç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ã€‚" - not-found: "「{q}ã€ã«é–¢ã™ã‚‹æŠ•ç¨¿ã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" - -desktop/views/pages/tag.vue: - no-posts-found: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ{q}ã€ãŒä»˜ã‘られãŸæŠ•ç¨¿ã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" - -desktop/views/pages/user-list.users.vue: - users: "ユーザー" - add-user: "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ " - username: "ユーザーå" - -desktop/views/pages/user/user.followers-you-know.vue: - title: "知りåˆã„ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - loading: "èªã¿è¾¼ã¿ä¸" - no-users: "知りåˆã„ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã¯ã„ã¾ã›ã‚“" - -desktop/views/pages/user/user.friends.vue: - title: "よã話ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼" - loading: "èªã¿è¾¼ã¿ä¸" - no-users: "よã話ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã„ã¾ã›ã‚“" - -desktop/views/pages/user/user.photos.vue: - title: "フォト" - loading: "èªã¿è¾¼ã¿ä¸" - no-photos: "写真ã¯ã‚ã‚Šã¾ã›ã‚“" - -desktop/views/pages/user/user.header.vue: - posts: "投稿" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - is-bot: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯Botã§ã™" - no-description: "自己紹介ã¯ã‚ã‚Šã¾ã›ã‚“" - years-old: "{age}æ³" - year: "å¹´" - month: "月" - day: "æ—¥" - follows-you: "フォãƒãƒ¼ã•ã‚Œã¦ã„ã¾ã™" - -desktop/views/pages/user/user.timeline.vue: - default: "投稿" - with-replies: "投稿ã¨è¿”ä¿¡" - with-media: "メディア" - my-posts: "ç§ã®æŠ•ç¨¿" - -desktop/views/widgets/notifications.vue: - title: "通知" - -desktop/views/widgets/polls.vue: - title: "アンケート" - refresh: "他を見る" - nothing: "ã‚ã‚Šã¾ã›ã‚“ï¼" - -desktop/views/widgets/post-form.vue: - title: "投稿" - note: "投稿" - something-happened: "何らã‹ã®äº‹æƒ…ã§æŠ•ç¨¿ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" - -desktop/views/widgets/profile.vue: - update-banner: "クリックã§ãƒãƒŠãƒ¼ç·¨é›†" - update-avatar: "クリックã§ã‚¢ãƒã‚¿ãƒ¼ç·¨é›†" - -desktop/views/widgets/trends.vue: - title: "トレンド" - refresh: "他を見る" - nothing: "ã‚ã‚Šã¾ã›ã‚“ï¼" - -desktop/views/widgets/users.vue: - title: "ãŠã™ã™ã‚ユーザー" - refresh: "他を見る" - no-one: "ã„ã¾ã›ã‚“ï¼" - -mobile/views/components/drive.vue: - used: "使用ä¸" - folder-count: "フォルダ" - count-separator: "ã€" - file-count: "ファイル" - nothing-in-drive: "ドライブã«ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“" - folder-is-empty: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã¯ç©ºã§ã™" - folder-name: "フォルダーå" - here-is-root: "ç¾åœ¨ã„ã‚‹å ´æ‰€ã¯ãƒ«ãƒ¼ãƒˆã§ã€ãƒ•ã‚©ãƒ«ãƒ€ã§ã¯ã‚ã‚Šã¾ã›ã‚“。" - url-prompt: "アップãƒãƒ¼ãƒ‰ã—ãŸã„ファイルã®URL" - uploading: "アップãƒãƒ¼ãƒ‰ã‚’リクエストã—ã¾ã—ãŸã€‚アップãƒãƒ¼ãƒ‰ãŒå®Œäº†ã™ã‚‹ã¾ã§æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" - folder-name-cannot-empty: "フォルダåを空白ã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" - -mobile/views/components/drive-file-chooser.vue: - select-file: "ファイルをé¸æŠž" - -mobile/views/components/drive-folder-chooser.vue: - select-folder: "フォルダーをé¸æŠž" - -mobile/views/components/drive.file.vue: - nsfw: "閲覧注æ„" - -mobile/views/components/drive.file-detail.vue: - download: "ダウンãƒãƒ¼ãƒ‰" - rename: "åå‰ã‚’変更" - move: "移動" - hash: "ãƒãƒƒã‚·ãƒ¥ (md5)" - exif: "EXIF" - nsfw: "閲覧注æ„" - mark-as-sensitive: "閲覧注æ„ã«è¨å®š" - unmark-as-sensitive: "閲覧注æ„を解除" - -mobile/views/components/media-video.vue: - sensitive: "閲覧注æ„" - click-to-show: "クリックã—ã¦è¡¨ç¤º" - -common/views/components/follow-button.vue: - following: "フォãƒãƒ¼ä¸" - follow: "フォãƒãƒ¼" - request-pending: "フォãƒãƒ¼è¨±å¯å¾…ã¡" - follow-processing: "フォãƒãƒ¼å‡¦ç†ä¸" - follow-request: "フォãƒãƒ¼ç”³è«‹" - -mobile/views/components/note.vue: - private: "ã“ã®æŠ•ç¨¿ã¯éžå…¬é–‹ã§ã™" - deleted: "ã“ã®æŠ•ç¨¿ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - location: "ä½ç½®æƒ…å ±" - -mobile/views/components/note-detail.vue: - reply: "返信" - reaction: "リアクション" - private: "ã“ã®æŠ•ç¨¿ã¯éžå…¬é–‹ã§ã™" - deleted: "ã“ã®æŠ•ç¨¿ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - location: "ä½ç½®æƒ…å ±" - -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "cat" - -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "cat" - -mobile/views/components/notifications.vue: - empty: "ã‚ã‚Šã¾ã›ã‚“ï¼" - -mobile/views/components/sub-note-content.vue: - private: "ã“ã®æŠ•ç¨¿ã¯éžå…¬é–‹ã§ã™" - deleted: "ã“ã®æŠ•ç¨¿ã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ" - media-count: "{}ã¤ã®ãƒ¡ãƒ‡ã‚£ã‚¢" - poll: "アンケート" - -mobile/views/components/ui.header.vue: - welcome-back: "ãŠã‹ãˆã‚Šãªã•ã„ã€" - adjective: "ã•ã‚“" - -mobile/views/components/ui.nav.vue: - timeline: "タイムライン" - notifications: "通知" - follow-requests: "フォãƒãƒ¼ç”³è«‹" - search: "検索" - user-lists: "リスト" - user-groups: "グループ" - widgets: "ウィジェット" - game: "ゲーム" - admin: "管ç†" - about: "Misskeyã«ã¤ã„ã¦" - -mobile/views/pages/drive.vue: - contextmenu: - upload: "ファイルをアップãƒãƒ¼ãƒ‰" - url-upload: "ファイルをURLã§ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰" - create-folder: "フォルダーを作æˆ" - rename-folder: "フォルダーåを変更" - move-folder: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã‚’移動" - delete-folder: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã‚’削除" - -mobile/views/pages/signup.vue: - lets-start: "📦 始ã‚ã¾ã—ょã†" - -mobile/views/pages/followers.vue: - followers-of: "{name}ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - -mobile/views/pages/following.vue: - following-of: "{name}ã®ãƒ•ã‚©ãƒãƒ¼" - -mobile/views/pages/home.vue: - home: "ホーム" - local: "ãƒãƒ¼ã‚«ãƒ«" - hybrid: "ソーシャル" - global: "ã‚°ãƒãƒ¼ãƒãƒ«" - mentions: "ã‚ãªãŸå®›ã¦" - messages: "ダイレクト投稿" - -mobile/views/pages/tag.vue: - no-posts-found: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ{q}ã€ãŒä»˜ã‘られãŸæŠ•ç¨¿ã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" - -mobile/views/pages/widgets.vue: - dashboard: "ダッシュボード" - widgets-hints: "ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’è¿½åŠ /削除ã—ãŸã‚Šä¸¦ã¹æ›¿ãˆãŸã‚Šã§ãã¾ã™ã€‚ウィジェットを移動ã™ã‚‹ã«ã¯ã€Œä¸‰ã€ã‚’ドラッグã—ã¾ã™ã€‚ウィジェットを削除ã™ã‚‹ã«ã¯ã€Œxã€ã‚’タップã—ã¾ã™ã€‚ã„ãã¤ã‹ã®ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã¯ã‚¿ãƒƒãƒ—ã™ã‚‹ã“ã¨ã§è¡¨ç¤ºã‚’変更ã§ãã¾ã™ã€‚" - add-widget: "è¿½åŠ " - customization-tips: "カスタマイズã®ãƒ’ント" - -mobile/views/pages/widgets/activity.vue: - activity: "アクティビティ" - -mobile/views/pages/share.vue: - share-with: "{name}ã§å…±æœ‰" - -mobile/views/pages/note.vue: - title: "投稿" - prev: "å‰ã®æŠ•ç¨¿" - next: "次ã®æŠ•ç¨¿" - -mobile/views/pages/games/reversi.vue: - reversi: "リãƒãƒ¼ã‚·" - -mobile/views/pages/search.vue: - search: "検索" - not-found: "「{q}ã€ã«é–¢ã™ã‚‹æŠ•ç¨¿ã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" - -mobile/views/pages/selectdrive.vue: - select-file: "ファイルをé¸æŠž" - -mobile/views/pages/notifications.vue: - notifications: "通知" - -mobile/views/pages/settings.vue: - signed-in-as: "{}ã¨ã—ã¦ã‚µã‚¤ãƒ³ã‚¤ãƒ³ä¸" - -mobile/views/pages/user.vue: - follows-you: "フォãƒãƒ¼ã•ã‚Œã¦ã„ã¾ã™" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - notes: "投稿" - overview: "概è¦" - timeline: "タイムライン" - media: "メディア" - years-old: "{age}æ³" - -mobile/views/pages/user/home.vue: - recent-notes: "最近ã®æŠ•ç¨¿" - images: "ç”»åƒ" - activity: "アクティビティ" - keywords: "ã‚ーワード" - domains: "é »å‡ºãƒ‰ãƒ¡ã‚¤ãƒ³" - frequently-replied-users: "よã話ã™ãƒ¦ãƒ¼ã‚¶ãƒ¼" - followers-you-know: "知りåˆã„ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - last-used-at: "最終ãƒã‚°ã‚¤ãƒ³" - -mobile/views/pages/user/home.photos.vue: - no-photos: "写真ã¯ã‚ã‚Šã¾ã›ã‚“" - -deck: - widgets: "ウィジェット" + description: "自己紹介" + youCanIncludeHashtags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + metadata: "è£œè¶³æƒ…å ±" + metadataLabel: "ラベル" + metadataContent: "内容" + +_exportOrImport: + allNotes: "å…¨ã¦ã®æŠ•ç¨¿" + followingList: "フォãƒãƒ¼" + muteList: "ミュート" + blockingList: "ブãƒãƒƒã‚¯" + userLists: "リスト" + +_charts: + federationInstancesIncDec: "連åˆã®å¢—減" + federationInstancesTotal: "連åˆã®åˆè¨ˆ" + usersIncDec: "ユーザーã®å¢—減" + usersTotal: "ユーザーã®åˆè¨ˆ" + activeUsers: "アクティブユーザー数" + notesIncDec: "投稿ã®å¢—減" + localNotesIncDec: "ãƒãƒ¼ã‚«ãƒ«ã®æŠ•ç¨¿ã®å¢—減" + remoteNotesIncDec: "リモートã®æŠ•ç¨¿ã®å¢—減" + notesTotal: "投稿ã®åˆè¨ˆ" + filesIncDec: "ファイルã®å¢—減" + filesTotal: "ファイルã®åˆè¨ˆ" + storageUsageIncDec: "ストレージ使用é‡ã®å¢—減" + storageUsageTotal: "ストレージ使用é‡ã®åˆè¨ˆ" + +_instanceCharts: + requests: "リクエスト" + users: "ユーザーã®å¢—減" + usersTotal: "ユーザーã®ç©ç®—" + notes: "投稿ã®å¢—減" + notesTotal: "投稿ã®ç©ç®—" + ff: "フォãƒãƒ¼/フォãƒãƒ¯ãƒ¼ã®å¢—減" + ffTotal: "フォãƒãƒ¼/フォãƒãƒ¯ãƒ¼ã®ç©ç®—" + cacheSize: "ã‚ャッシュサイズã®å¢—減" + cacheSizeTotal: "ã‚ャッシュサイズã®ç©ç®—" + files: "ファイル数ã®å¢—減" + filesTotal: "ファイル数ã®ç©ç®—" + +_timelines: home: "ホーム" local: "ãƒãƒ¼ã‚«ãƒ«" - hybrid: "ソーシャル" - hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" + social: "ソーシャル" global: "ã‚°ãƒãƒ¼ãƒãƒ«" - mentions: "ã‚ãªãŸå®›ã¦" - direct: "ダイレクト投稿" - notifications: "通知" - list: "リスト" - select-list: "リストをé¸æŠžã—ã¦ãã ã•ã„" - swap-left: "å·¦ã«ç§»å‹•" - swap-right: "å³ã«ç§»å‹•" - swap-up: "上ã«ç§»å‹•" - swap-down: "下ã«ç§»å‹•" - remove: "カラムを削除" - add-column: "ã‚«ãƒ©ãƒ ã‚’è¿½åŠ " - rename: "åå‰ã‚’変更" - stack-left: "å·¦ã«é‡ãã‚‹" - pop-right: "å³ã«å‡ºã™" - disabled-timeline: - title: "無効化ã•ã‚ŒãŸã‚¿ã‚¤ãƒ ライン" - description: "サーãƒãƒ¼ã®é‹å–¶è€…ã«ã‚ˆã‚Šã€ã“ã®ã‚¿ã‚¤ãƒ ラインã¯ä½¿ç”¨ã§ããªã„状態ã«è¨å®šã•ã‚Œã¦ã„ã¾ã™ã€‚" -deck/deck.tl-column.vue: - is-media-only: "メディア投稿ã®ã¿" - edit: "オプション" - -deck/deck.user-column.vue: - follows-you: "フォãƒãƒ¼ã•ã‚Œã¦ã„ã¾ã™" - posts: "投稿" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - images: "ç”»åƒ" - activity: "アクティビティ" - timeline: "タイムライン" - pinned-notes: "ピン留ã‚ã•ã‚ŒãŸæŠ•ç¨¿" - pinned-page: "ピン留ã‚ã•ã‚ŒãŸãƒšãƒ¼ã‚¸" - -docs: - edit-this-page-on-github: "é–“é•ã„や改善点を見ã¤ã‘ã¾ã—ãŸã‹ï¼Ÿ" - edit-this-page-on-github-link: "ã“ã®ãƒšãƒ¼ã‚¸ã‚’GitHubã§ç·¨é›†" - -dev/views/index.vue: - manage-apps: "アプリã®ç®¡ç†" - -dev/views/apps.vue: - manage-apps: "アプリを管ç†" - create-app: "アプリ作æˆ" - app-missing: "アプリãªã—" - -dev/views/new-app.vue: - new-app: "æ–°ã—ã„アプリケーション" - new-app-info: "アプリケーションã¯APIã‹ã‚‰ã§ã‚‚作æˆã§ãã¾ã™ã€‚ (app/create)" - create-app: "アプリケーションã®ä½œæˆ" - app-name: "アプリケーションå" - app-name-placeholder: "ex) Misskey for iOS" - app-name-desc: "ã‚ãªãŸã®ã‚¢ãƒ—リã®å称。" - app-overview: "アプリã®æ¦‚è¦" - app-overview-placeholder: " ex) Misskey iOSクライアント。" - app-overview-desc: "ã‚ãªãŸã®ã‚¢ãƒ—リã®ç°¡å˜ãªèª¬æ˜Žã‚„紹介。" - callback-url: "コールãƒãƒƒã‚¯URL (オプション)" - callback-url-placeholder: "ex) https://your.app.example.com/callback.php" - callback-url-desc: "ユーザーãŒèªè¨¼ãƒ•ã‚©ãƒ¼ãƒ ã§èªè¨¼ã—ãŸéš›ã«ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã™ã‚‹URLã‚’è¨å®šã§ãã¾ã™ã€‚" - authority: "権é™" - authority-desc: "ã“ã“ã§è¦æ±‚ã—ãŸæ©Ÿèƒ½ã ã‘ãŒAPIã‹ã‚‰ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚" - authority-warning: "アプリ作æˆå¾Œã‚‚変更ã§ãã¾ã™ãŒã€æ–°ãŸãªæ¨©é™ã‚’付与ã™ã‚‹å ´åˆã€ãã®æ™‚点ã§é–¢é€£ä»˜ã‘られã¦ã„るユーザーã‚ーã¯ã™ã¹ã¦ç„¡åŠ¹ã«ãªã‚Šã¾ã™ã€‚" - -pages: +_pages: new-page: "ページã®ä½œæˆ" edit-page: "ページã®ç·¨é›†" read-page: "ソースを表示ä¸" @@ -2185,23 +645,23 @@ pages: _join: arg1: "リスト" arg2: "区切り" - add: "+ 足ã™" + add: "足ã™" _add: arg1: "A" arg2: "B" - subtract: "- 引ã" + subtract: "引ã" _subtract: arg1: "A" arg2: "B" - multiply: "× 掛ã‘ã‚‹" + multiply: "掛ã‘ã‚‹" _multiply: arg1: "A" arg2: "B" - divide: "÷ 割る" + divide: "割る" _divide: arg1: "A" arg2: "B" - mod: "÷ 割ã£ãŸä½™ã‚Š" + mod: "割ã£ãŸä½™ã‚Š" _mod: arg1: "A" arg2: "B" @@ -2323,65 +783,3 @@ pages: enviromentVariables: "環境変数" pageVariables: "ページè¦ç´ " argVariables: "入力スãƒãƒƒãƒˆ" - -room: - add-furniture: "家具を置ã" - translate: "移動" - rotate: "回転" - exit: "戻る" - remove: "ã—ã¾ã†" - save: "ä¿å˜" - saved: "ä¿å˜ã—ã¾ã—ãŸ" - clear: "片付ã‘" - clear-confirm: "å…¨ã¦ã®å®¶å…·ã‚’ã—ã¾ã„ã¾ã™ã‹ï¼Ÿ" - leave-confirm: "未ä¿å˜ã®å¤‰æ›´ãŒã‚ã‚Šã¾ã™ã€ç§»å‹•ã—ã¾ã™ã‹ï¼Ÿ" - chooseImage: "ç”»åƒã‚’é¸æŠž" - room-type: "部屋ã®ã‚¿ã‚¤ãƒ—" - carpet-color: "床ã®è‰²" - rooms: - default: "デフォルト" - washitsu: "和室" - furnitures: - milk: "牛乳パック" - bed: "ベッド" - low-table: "ãƒãƒ¼ãƒ†ãƒ¼ãƒ–ル" - desk: "デスク" - chair: "ãƒã‚§ã‚¢" - chair2: "ãƒã‚§ã‚¢2" - fan: "æ›æ°—扇" - pc: "パソコン" - plant: "観葉æ¤ç‰©" - plant2: "観葉æ¤ç‰©2" - eraser: "消ã—ゴム" - pencil: "鉛ç†" - pudding: "プリン" - cardboard-box: "段ボール箱" - cardboard-box2: "段ボール箱2" - cardboard-box3: "段ボール箱3" - book: "本" - book2: "本2" - piano: "ピアノ" - facial-tissue: "ティッシュボックス" - server: "サーãƒãƒ¼" - moon: "月" - corkboard: "コルクボード" - mousepad: "マウスパッド" - monitor: "モニター" - keyboard: "ã‚ーボード" - carpet-stripe: "カーペット(縞)" - mat: "マット" - color-box: "カラーボックス" - wall-clock: "å£æŽ›ã‘時計" - photoframe: "é¡ç¸" - cube: "ã‚ューブ" - tv: "テレビ" - pinguin: "ピンギン" - rubik-cube: "ルービックã‚ューブ" - poster-h: "ãƒã‚¹ã‚¿ãƒ¼(横長)" - poster-v: "ãƒã‚¹ã‚¿ãƒ¼(縦長)" - sofa: "ソファ" - spiral: "螺旋階段" - bin: "ゴミ箱" - cup-noodle: "カップ麺" - holo-display: "ホãƒã‚°ãƒ©ãƒ•ã‚£ãƒƒã‚¯ãƒ‡ã‚£ã‚¹ãƒ—レイ" - energy-drink: "エナジードリンク" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml deleted file mode 100644 index c2a430e6303df750586cbc9f6801dfe8ea7f8df6..0000000000000000000000000000000000000000 --- a/locales/ja-KS.yml +++ /dev/null @@ -1,1291 +0,0 @@ ---- -meta: - lang: "日本語 (関西å¼)" -common: - misskey: "A â of fediverse" - about-title: "A â of fediverse." - about: "よã†Misskeyを見ã¤ã‘ã¦ãã‚Œã¦ã€ãŠãŠãã«ã‚„ã§ã€‚Misskeyã¯ã€åœ°çƒã§ç”Ÿã¾ã‚ŒãŸ<b>分散マイクãƒãƒ–ãƒã‚°SNS</b>ã‚„ãん。Fediverse(ãŽã‚‡ã†ã•ã‚“ã®SNSã§æ§‹æˆã•ã‚Œã¨ã‚‹å®‡å®™)ã£ã¡ã‚…ã†ã‚‚ã‚“ã®ä¸ã«ãŠã‚‹ã‹ã‚‰ã€ãŠéš£ã•ã‚“ã®SNSã¨ã‚‚仲良ã†ã•ã›ã¦ã‚‚ã‚ã¦ã‚“ãん。ã¡ã‚‡ã„ã¨ã‚„ã‹ã¾ã—ã„心斎橋ã‹ã‚‰é›¢ã‚Œã¦ã€æ–°ã—ã„インターãƒãƒƒãƒˆã«ãƒ€ã‚¤ãƒ–ã—ã¦ã¿ãƒã²ã‚“?" - intro: - title: "Misskeyã£ã¦ãªã‚“ã‚„ãã‚“" - about: "Misskeyã£ã¦ã®ã¯ãªã€ã‚ªãƒ¼ãƒ—ンソースã®<b>分散型マイクãƒãƒ–ãƒã‚°SNS</b>ã®ã“ã¨ã‚„。ã”ã£ã¤ã„ãˆãˆæ„Ÿã˜ã«ã§ãã‚‹UIã‚„ã£ãŸã‚Šã€æŠ•ç¨¿ã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚„ã£ãŸã‚Šã€ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã¾ã¨ã‚ã¨ã‘るドライブやã£ãŸã‚Šã€ã„ã‚ã‚“ãªæ©Ÿèƒ½ãŒç›®ç™½æŠ¼ã—や。Fediverseã«å¯¾å¿œã—ã¨ã‚‹ã‹ã‚‰ã€ã‚ˆãã®SNSã¨ã‚‚ノリツッコミã§ãã‚‹ã‚“ã‚„ã§ã€‚タイガースãŒæ±äº¬ãƒ‰ãƒ¼ãƒ ã«é‡Žçƒã—ã«è¡Œãよã†ãªã‚‚んや。" - features: "ãˆãˆã¨ã“" - rich-contents: "投稿" - rich-contents-desc: "æ€ã£ã¨ã‚‹ã“ã¨ã€ã‚¿ã‚¤ã‚¬ãƒ¼ã‚¹ã®å®Ÿæ³ã€ä»–ã«è¨€ã„ãŸã„ã“ã¨ãŒã‚ã‚Œã°ãªã‚“ã§ã‚‚言ã£ã¦ãˆãˆã§ã€‚ã„ã‚ã‚“ãªæ§‹æ–‡ã‚ã‚‹ã‹ã‚‰ã€å¥½ãã«ã¤ã“ã†ã¦ãれや。画åƒã‚„å‹•ç”»ã€ã‚¢ãƒ³ã‚±ãƒ¼ãƒˆã‚‚添付ã§ãã‚‹ã§ã€‚" - reaction: "リアクション" - reaction-desc: "「何æ€ã£ã¨ã‚‹ã‹è¨€ã†ã¦ã¿ï¼Ÿã€è¨€ã‚ã‚Œã¦ã‚‚ã€ã‚ã‹ã‚‰ã‚“ã‚ï¼ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ä½¿ã†ã¦ã€ã‚¨ãƒ¢ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ダイレクトã«ä¼ãˆã‚‹ã‚“ã‚„ï¼Misskeyã¯ãªã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ã«ã„ã‚ã‚“ãªãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ä»˜ã‘られるんや。もã†ã€Œã„ã„ãã€ã¨ã‹ã„ã†ã‚‚ã‚“ã ã‘ã®SNSã«ã¯æˆ»ã‚Œã¸ã‚“ã‚ãªã€‚551ã®è±šã¾ã‚“食ã†ã¦ã¿ï¼Ÿã‚‚ã†ä»–ã®è±šã¾ã‚“食ãˆã¸ã‚“ã§ï¼Ÿ" - ui: "インターフェイス" - ui-desc: "ã“ã®UIãˆãˆè¨€ã†ã¦ãŸã§ã€çŸ¥ã‚‰ã‚“ã‘ã©ã€‚ã‚ã‚“ãŸã®å¥½ã¿ã®UIãªã‚“ã¦çŸ¥ã£ãŸã“ã£ã¡ã‚ƒãªã„。Misskeyã¯å¥½ãã«ã„ã˜ã‚Œã‚‹ã‹ã‚‰ãªã€ãƒ¬ã‚¤ã‚¢ã‚¦ãƒˆã‚„デザイン変ãˆãŸã‚Šã€è‰²ã‚“ãªã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã²ã£ã¤ã‘ãŸã‚Šã—ã¦ã€ã‚ã‚“ãŸã ã‘ã®Misskey作ã£ã¦æ¥½ã—ã‚“ã§ãªï¼" - drive: "ドライブ" - drive-desc: "「ã“ãªã„ã ã®ç”»åƒã€ã©ã“ã‚„ã£ãŸã‹ãªâ€¦â€¦ã¾ãŸæŠ•ç¨¿ã—ãŸã„ã‚“ã‚„ã‘ã©â€¦â€¦ã€ã€Œã•ã£ãã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚ã®ãƒ•ã‚©ãƒ«ãƒ€ã«ç›´ã—ã¨ã„ã¦ã€ãã‚“ãªã“ã¨è¨€ã‚ã‚“ã¨ã£ã¦ã€‚Misskeyã¯ã‚‚ã¨ã‹ã‚‰ãƒ‰ãƒ©ã‚¤ãƒ–機能æŒã£ã¨ã‚‹ã•ã‹ã„ã€å¿ƒé…ã‚らã¸ã‚“。ファイルã®ã€Œã‚ã‘ã‚ã‘ã€ã—ãŸã£ã¦ãªã€‚" - outro: "Misskeyã®æ©Ÿèƒ½ã¯ç„¡é™å¤§ã‚„ï¼çŸ¥ã‚‰ã‚“ã‘ã©ã€‚知らん言ã†ã¨ã‚‹ã‚„ã‚“ã‘ã€ã‚ã‚“ãŸãŒè¦‹ã«è¡Œã‘ã‚„ï¼Misskeyã¯åˆ†æ•£åž‹SNSã‚„ã‹ã‚‰ã€ã“ã“ãŒã‚ã‹ã‚“ãã¦ã‚‚ä»–ãŒã‚る。阪神ã§ã‚‚オリックスã§ã‚‚ワイã¯å¿œæ´ã™ã‚‹ã§ï¼" - application-authorization: "アプリã®é€£æº" - close: "ã•ã„ãªã‚‰" - do-not-copy-paste: "ã“ã“ã«ã‚³ãƒ¼ãƒ‰ã‚’入力ã—ãŸã‚Šå¼µã‚Šä»˜ã‘ãŸã‚Šã›ã‚“ã¨ã„ã¦ãã ã•ã„。アカウントãŒä¸æ£åˆ©ç”¨ã•ã‚Œã‚‹ã‹ã‚‚分ã‹ã‚‰ã‚“。知らんã‘ã©ã€‚" - load-more: "ã‚‚ã£ã¨ã‚らã¸ã‚“ã®ã‹ï¼" - enter-password: "パスワードを入れã¦ã‚„" - 2fa: "二段階èªè¨¼" - delete-confirm: "ã“ã®æŠ•ç¨¿ã‚’削除ã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" - notification-types: - all: "ã™ã¹ã¦" - follow: "フォãƒãƒ¼" - reply: "è¿”ã™" - renote: "Renote" - reaction: "リアクション" - got-it: "ã»ã„" - customization-tips: - title: "カスタマイズã®ãƒ’ント" - paragraph: "<p>ホームã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã¯ã€ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’増やã—ãŸã‚Šã»ã‹ã—ãŸã‚Šã€ãƒ‰ãƒ©ãƒƒã‚°ï¼†ãƒ‰ãƒãƒƒãƒ—ã—ã¦ä¸¦ã³æ›¿ãˆãŸã‚Šã—ã¦ã„ã˜ã‚Œã‚‹ã§ã€‚</p><p>一部ウィジェットã¯<strong><strong>å³</strong>クリック</strong>ã§è¡¨ç¤ºã‚‚ã„ã˜ã‚Œã‚‹ã‚“や。</p><p>ã»ã‹ã—ãŸã„ã¨ãã¯ãƒ˜ãƒƒãƒ€ãƒ¼ã®<strong>「ゴミ箱ã€</strong>ã«ã»ã†ã‚Šã“ã‚“ã§ã‚‰ã€‚</p><p>「完了ã€æŠ¼ã—ãŸã‚‰ãŠçµ‚ã„ã‚„ã§ã€‚</p>" - gotit: "Got it!" - notification: - file-uploaded: "ファイルãŒã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã•ã‚ŒãŸã§" - message-from: "{}ã¯ã‚“ã‹ã‚‰ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸:" - reversi-invited: "対局ã¸ã®æ‹›å¾…ãŒãã¨ã‚‹ã§" - reversi-invited-by: "{}ã¯ã‚“ã‹ã‚‰" - notified-by: "{}ã¯ã‚“ã‹ã‚‰" - reply-from: "{}ã¯ã‚“ã‹ã‚‰è¿”ä¿¡:" - quoted-by: "{}ã¯ã‚“ãŒå¼•ç”¨:" - time: - unknown: "ãªãžã®ã˜ã‹ã‚“" - future: "未æ¥" - just_now: "ãŸã£ãŸä»Š" - seconds_ago: "{}秒å‰" - minutes_ago: "{}分å‰" - hours_ago: "{}時間å‰" - days_ago: "{}æ—¥å‰" - weeks_ago: "{}週間å‰" - months_ago: "{}ヶ月å‰" - years_ago: "{}å¹´å‰" - month-and-day: "{month}月 {day}æ—¥" - trash: "ゴミ箱" - drive: "ドライブ" - messaging: "トーク" - home: "ホーム" - timeline: "タイムライン" - following: "フォãƒãƒ¼ã—ã¨ã‚‹" - followers: "フォãƒãƒ¯ãƒ¼" - favorites: "ãŠæ°—ã«å…¥ã‚Š" - permissions: - "write:votes": "投票ã™ã‚‹ã§" - post-form: - submit: "投稿" - reply: "è¿”ã™" - renote: "Renote" - error: "エラー" - enter-username: "ユーザーåを入力ã—ã¦ã‚„" - add-visible-user: "ユーザー増やã™" - username-prompt: "ユーザーåを入力ã—ã¦ã‚„" - weekday-short: - sunday: "æ—¥" - monday: "月" - tuesday: "ç«" - wednesday: "æ°´" - thursday: "木" - friday: "金" - saturday: "土" - weekday: - sunday: "日曜日" - monday: "月曜日" - tuesday: "ç«æ›œæ—¥" - wednesday: "水曜日" - thursday: "木曜日" - friday: "金曜日" - saturday: "土曜日" - reactions: - like: "ãˆãˆã‚„ã‚“" - love: "好ãã‚„ãã‚“" - laugh: "ã‚ã‚ãŸ" - hmm: "ãµã…~む" - surprise: "ã‚ãŠ" - congrats: "ãŠã‚ã§ã¨ã†ã•ã‚“" - angry: "何言ã†ã¦ã¾ã‚“ãã‚“" - confused: "ã“ã¾ã“ã¾ã®ã“ã¾ã‚Šã‚„ã‚ã" - rip: "RIP" - pudding: "アメã¡ã‚ƒã‚“ã¡ã‚ƒã†ã‚“ã¡ã‚ƒã†ï¼Ÿ" - note-visibility: - public: "公開" - home: "ホーム" - home-desc: "ホームタイムライン以外ã«è¦‹ã›ã‚“ã¨ã£ã¦" - followers: "フォãƒãƒ¯ãƒ¼" - followers-desc: "自分ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã«è¦‹ã›ã‚“ã¨ã£ã¦" - specified: "ダイレクト" - specified-desc: "今ã‹ã‚‰è¨€ã†ãƒ¦ãƒ¼ã‚¶ãƒ¼ä»¥å¤–ã«è¦‹ã›ã‚“ã¨ã£ã¦ã‚„" - local-public: "公開 (ãƒãƒ¼ã‚«ãƒ«ã ã‘)" - local-home: "ホーム(ãƒãƒ¼ã‚«ãƒ«ã ã‘)" - local-followers: "フォãƒãƒ¯ãƒ¼ (ãƒãƒ¼ã‚«ãƒ«ã ã‘)" - note-placeholders: - a: "今ãªã«ã—ã¦ã‚“?" - b: "何ã‹ã‚ã£ãŸã‚“ã‹ï¼Ÿ" - c: "何考ãˆã¨ã‚Šã¾ã™ã‚“?" - d: "言ã†ã¨ããŸã„ã“ã¨ã¯ï¼Ÿ" - e: "ã“ã“ã«æ›¸ã„ã¦ã‚„" - f: "ã‚ã‚“ã•ã‚“ãŒæ›¸ãã‚“ã‚’å¾…ã£ã¡ã‚‡ã‚Šã¾ã™..." - _settings: - profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - notification: "通知" - tags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - blocking: "ブãƒãƒƒã‚¯" - password: "パスワード" - other: "ãã®ä»–" - reactions: "リアクション" - timeline: "タイムライン" - save: "ä¿å˜" - saved: "ä¿å˜ã—ãŸã§ï¼" - preview: "試ã—ã¦ã¿ã‚‹" - search: "検索" - delete: "削除" - loading: "èªã¿è¾¼ã¿ä¸" - update-available-title: "æ›´æ–°ãŒã‚ã‚“ã§" - update-available: "Misskeyã®æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒã‚ã‚“ã§({newer}。ç¾åœ¨{current}ã‚’ã¤ã“ã¦ã‚‹ã‚)。ページをå†åº¦èªã¿è¾¼ã¿ã—ãŸã‚‹ã¨æ›´æ–°ãŒé©ç”¨ã•ã‚Œã‚‹ã‚。" - my-token-regenerated: "ã‚ã‚“ã•ã‚“ã®ãƒˆãƒ¼ã‚¯ãƒ³ãŒæ›´æ–°ã•ã‚ŒãŸã‚‰ã—ã„ã‚。ã™ã¾ã‚“ãŒã¨ã‚Šã‚ãˆãšã‚µã‚¤ãƒ³ã‚¢ã‚¦ãƒˆã™ã‚“ã§ã€‚" - enter-username: "ユーザーåを入力ã—ã¦ã‚„" - do-not-use-in-production: "開発ビルドや。本番環境ã§ä½¿ã‚ã‚“ã¨ã„ã¦ï¼çŸ¥ã‚‰ã‚“ã§ï¼" - is-remote-post: "ã“ã®æŠ•ç¨¿æƒ…å ±ã¯ã‚³ãƒ”ーã§ã™ã€‚" - view-on-remote: "ã¡ã‚ƒã‚“ã¨ã—ãŸæƒ…å ±è¦‹ã›ã¦ã‚„ï¼" - renoted-by: "{user}ãŒRenote" - error: - title: "å•é¡ŒãŒèµ·ã“ã£ãŸã‚" - retry: "ã‚‚ã£ãºã‚“" - reversi: - drawn: "ãŠã‚ã„ã“" - my-turn: "ã‚ã‚“ã•ã‚“ã®ã‚¿ãƒ¼ãƒ³ã‚„" - opponent-turn: "相手ã®ã‚¿ãƒ¼ãƒ³ã‚„" - turn-of: "{name}ã®ã‚¿ãƒ¼ãƒ³ã‚„" - past-turn-of: "{name}ã®ã‚¿ãƒ¼ãƒ³" - won: "{name}ã®å‹ã¡ã‚„ã§ï¼" - black: "é»’" - white: "白" - total: "åˆè¨ˆ" - this-turn: "{count}ターン目" - widgets: - analog-clock: "アナãƒã‚°æ™‚計" - profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - calendar: "カレンダー" - timemachine: "カレンダー(タイムマシン)" - activity: "アクティビティ" - rss: "RSSリーダー" - memo: "付箋" - trends: "トレンド" - photo-stream: "フォトストリーム" - posts-monitor: "投稿ãƒãƒ£ãƒ¼ãƒˆ" - slideshow: "スライドショー" - version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" - broadcast: "ブãƒãƒ¼ãƒ‰ã‚ャスト" - notifications: "通知" - users: "ãŠã™ã™ã‚ユーザー" - polls: "アンケート" - post-form: "投稿フォーム" - server: "サーãƒãƒ¼æƒ…å ±" - nav: "ナビゲーション" - tips: "ヒント" - hashtags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - dev: "アプリã®ä½œæˆã‚ã‹ã‚“ã‹ã£ãŸã‚。もã£ãºã‚“ã‚„ã£ã¦ã¿ã¦ã€‚" - ai-chan-kawaii: "è—ã¡ã‚ƒã‚ã£ã•ã¹ã£ã´ã‚“ã•ã‚“ã‚„" - you: "ã‚ã‚“ã•ã‚“" -auth/views/form.vue: - share-access: "ã‚ã‚“ãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«<i>{name}</i>ãŒã‚¢ã‚¯ã‚»ã‚¹ã—よã†ã¨ã—ã¦ã‚‹ã§ï¼Ÿãˆãˆã‹ï¼Ÿ" - permission-ask: "ã“ã®ã‚¢ãƒ—リã¯æ¬¡ã®æ¨©é™ã‚’è¦æ±‚ã—ã¦ã‚“ã§:" - cancel: "ã‚„ã‚ã¨ãã‚" - accept: "アクセスを許å¯ã‚„ï¼" -auth/views/index.vue: - loading: "èªã¿è¾¼ã¿ä¸" - denied: "アプリケーションã®é€£æºã‚’ã‚„ã‚ã¨ã„ãŸã‚。" - denied-paragraph: "ã“ã®ã‚¢ãƒ—リãŒã‚ã‚“ã•ã‚“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã¯å¤šåˆ†ã‚らã¸ã‚“。知らんã‘ã©ã€‚" - already-authorized: "ã“ã®ã‚¢ãƒ—リã¯ã‚‚ã†é€£æºæ¸ˆã¿ã‚„ã£ãŸã‚" - allowed: "アプリケーションã®é€£æºã‚’許å¯ã—ãŸã§" - callback-url: "アプリケーションã«æˆ»ã£ã¨ã‚Šã¾ã™" - please-go-back: "アプリケーションã«æˆ»ã£ã¦ã€æ°—å¼µã£ã¦ã£ã¦ãªã€‚" - error: "セッションãŒå˜åœ¨ã—ã¦ã¸ã‚“。" - sign-in: "サインインã—ã¦ã‚„" -common/views/pages/explore.vue: - federated: "連åˆ" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "{}ã‚’å¾…ã£ã¨ã‚Šã¾ã™" - cancel: "ã‚„ã‚ã¨ãã‚" -common/views/components/games/reversi/reversi.game.vue: - surrender: "投了や..." - surrendered: "投了ã«ã‚ˆã‚Š" - is-llotheo: "石ã®å°‘ãªã„æ–¹ãŒå‹ã¡(ãƒã‚»ã‚ª)" - looped-map: "ループマップ" - can-put-everywhere: "ã©ã“ã«ç½®ã„ã¦ã‚‚ãˆãˆãƒ¢ãƒ¼ãƒ‰" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "ãŠéš£ã®ãƒŸã‚¹ã‚ストã¯ã‚“らã¨ãƒªãƒãƒ¼ã‚·ã§å¯¾æˆ¦ã‚„ï¼" - invite: "招待" - rule: "éŠã³æ–¹" - rule-desc: "リãƒãƒ¼ã‚·ã¯ã€ç›¸æ‰‹ã¨äº¤äº’ã«çŸ³ã‚’ボードã«ç½®ã„ã¦ã€ç›¸æ‰‹ã®çŸ³ã‚’挟んã§è‡ªåˆ†ã®è‰²ã«å¤‰ãˆã¦ã£ã¦ã€æœ€çµ‚çš„ã«æ®‹ã£ãŸçŸ³ãŒå¤šã„æ–¹ãŒå‹ã¡ã£ã¡ã‚…ã†ãƒœãƒ¼ãƒ‰ã‚²ãƒ¼ãƒ や。" - mode-invite: "招待" - mode-invite-desc: "指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨å¯¾æˆ¦ã™ã‚‹ãƒ¢ãƒ¼ãƒ‰ã‚„。" - invitations: "対局ã®æ‹›å¾…ãŒãã¦ã‚“ã§ï¼" - my-games: "自分ã®å¯¾å±€" - all-games: "ã¿ã‚“ãªã®å¯¾å±€" - enter-username: "ユーザーåを入力ã—ã¦ã‚„" - game-state: - ended: "終了" - playing: "進行ä¸" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "ゲームã®è¨å®š" - choose-map: "マップをé¸æŠž" - random: "ã„ã‚“ã˜ã‚ƒã‚“ã»ã„" - black-or-white: "先手/後手" - black-is: "{}ãŒé»’ã‚„" - rules: "ルール" - is-llotheo: "石ã®å°‘ãªã„æ–¹ãŒå‹ã¡ã‚„ï¼(ãƒã‚»ã‚ª)" - looped-map: "ループマップ" - can-put-everywhere: "ã©ã“ã«ç½®ã„ã¦ã‚‚ãˆãˆãƒ¢ãƒ¼ãƒ‰" - settings-of-the-bot: "Botã®è¨å®š" - this-game-is-started-soon: "ゲームã¯æ•°ç§’後ã«é–‹å§‹ã•ã‚Œã‚“ã§" - waiting-for-other: "相手ã®æº–å‚™ãŒå®Œäº†ã™ã‚“ã®ã‚’å¾…ã£ã¦ã‚“ã§" - waiting-for-me: "ã‚ã‚“ã•ã‚“ã®æº–å‚™ãŒå®Œäº†ã™ã‚“ã®ã‚’å¾…ã£ã¦ã‚“ã§" - waiting-for-both: "準備ä¸" - cancel: "ã‚„ã‚ã¨ãã‚" - ready: "準備完了" - cancel-ready: "準備続行" -common/views/components/connect-failed.vue: - title: "サーãƒãƒ¼ã«æŽ¥ç¶šã§ã‘ã¸ã‚“ã‚" - description: "インターãƒãƒƒãƒˆå›žç·šã«å•é¡ŒãŒèµ·ãã¨ã‚‹ã‹ã€ã‚µãƒ¼ãƒãƒ¼ãŒãƒ€ã‚¦ãƒ³ã¾ãŸã¯ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã—ã¨ã‚‹ã£ã½ã„ã‚。知らんã‘ã©ã€‚ã¨ã‚Šã‚ãˆãšã‚ã¨ã§{å†è©¦è¡Œ}ã—ã¦ã‚„。" - thanks: "ã„ã¤ã‚‚Misskeyã‚’ã¤ã“ã¦ãã‚Œã¦ã»ã‚“ã¾ã‚ã‚ŠãŒã¨ãªã€‚" - troubleshoot: "トラブルシュート" -common/views/components/connect-failed.troubleshooter.vue: - title: "トラブルシューティング" - network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶š" - checking-network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶šã‚’確èªä¸" - internet: "インターãƒãƒƒãƒˆæŽ¥ç¶š" - checking-internet: "インターãƒãƒƒãƒˆæŽ¥ç¶šã‚’確èªä¸" - server: "サーãƒãƒ¼æŽ¥ç¶š" - checking-server: "サーãƒãƒ¼æŽ¥ç¶šã‚’確èªä¸" - finding: "å•é¡Œã‚’調ã¹ã¨ã‚‹ã§" - no-network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«æŽ¥ç¶šã•ã‚Œã¨ã‚‰ã‚“ã§" - no-network-desc: "ã¤ã“ã¦ã‚‹PCã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶šãŒæ£å¸¸ã‹ç¢ºèªã—ã¦ã‚„。" - no-internet: "インターãƒãƒƒãƒˆã«æŽ¥ç¶šã•ã‚Œã¨ã‚‰ã‚“ã§" - no-internet-desc: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«ã¯æŽ¥ç¶šã•ã‚Œã¨ã‚‹ã‘ã©ã€ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆã«ã¯æŽ¥ç¶šã•ã‚Œã¨ã‚‰ã‚“よã†ã‚„ã‚。ã¤ã“ã¦ã‚‹PCã®ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆæŽ¥ç¶šãŒæ£å¸¸ã‹ç¢ºèªã—ã¦ã‚„。" - no-server: "Misskeyã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶šã§ã‘ã¸ã‚“ã‚" - no-server-desc: "ã¤ã“ã¦ã‚‹PCã®ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆæŽ¥ç¶šã¯æ£å¸¸ã‚„ã‘ã©ã€Misskeyã®ã‚µãƒ¼ãƒãƒ¼ã«ã¯ã¤ãªãŒã‚‰ã‚“ã‚。多分サーãƒãƒ¼ãŒãƒ€ã‚¦ãƒ³ã¾ãŸã¯ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã—ã¨ã‚‹ã‚ã€çŸ¥ã‚‰ã‚“ã‘ã©ã€‚ã™ã¾ã‚“ã‘ã©ã—ã°ã‚‰ãã—ã¦ã‹ã‚‰å†åº¦ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ã¿ã¦ã‚„。" - success: "Misskeyã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶šã§ããŸã‚" - success-desc: "æ£å¸¸ã«æŽ¥ç¶šã§ãるよã†ã‚„ã‚。ページをå†åº¦èªã¿è¾¼ã¿ã—ã¦ãªã€‚" - flush: "ã‚ャッシュã®å‰Šé™¤" - set-version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³æŒ‡å®š" -common/views/components/media-banner.vue: - sensitive: "見ã›ãŸã‚‰ã‚ã‹ã‚“" - click-to-show: "押ã—ã¦ã¿ã€è¦‹ã›ãŸã‚‹ã‚" -common/views/components/theme.vue: - light-theme: "ナイトゲームã¡ã‚ƒã†æ™‚ã®ãƒ†ãƒ¼ãƒžã©ãªã„ã™ã‚‹ï¼Ÿ" - dark-theme: "ナイトゲームã®æ™‚ã®ãƒ†ãƒ¼ãƒžã©ãªã„ã™ã‚‹ï¼Ÿ" - light-themes: "デイゲーム" - dark-themes: "ナイトゲーム" - install-a-theme: "テーマ入れるã§" - theme-code: "テーマコード" - install: "インストール" - installed: "「{}ã€ã‚’入れãŸã§ï¼" - create-a-theme: "テーマ作る" - save-created-theme: "テーマä¿å˜" - primary-color: "ã“ã®è‰²ä¸€ç•ªé‡è¦ã‚„" - secondary-color: "次ã¯ã“ã®è‰²å‡ºã—ãŸã£ã¦" - text-color: "æ–‡å—ã¯ã“ã®è‰²ã‚„ï¼" - base-theme: "ã“ã®è‰²ãŒèƒŒæ™¯ã‚„ï¼" - base-theme-light: "Light" - base-theme-dark: "Dark" - theme-name: "テーマå" - preview-created-theme: "試ã—ã¦ã¿ã‚‹" - invalid-theme: "ã“ã®ãƒ†ãƒ¼ãƒžã‚ã‹ã‚“ã‚ã€ãªã‚“ã‹é–“é•ã†ã¨ã‚‹" - already-installed: "ã“ã®ãƒ†ãƒ¼ãƒžã‚‚ã†ã‚ã‚‹ã§" - saved: "ä¿å˜ã—ãŸã§ï¼" - manage-themes: "テーマã®ç®¡ç†" - builtin-themes: "ã„ã¤ã‚‚ã®ãƒ†ãƒ¼ãƒž" - my-themes: "ワイã®ãƒ†ãƒ¼ãƒž" - installed-themes: "入れãŸãƒ†ãƒ¼ãƒž" - select-theme: "テーマé¸ã‚“ã§ã‚„ï¼" - uninstall: "ã»ã‹ã™" - uninstalled: "「{}ã€ã‚’ã»ã‹ã—ã¦ã‚‚ã†ãŸã‚" - author: "作ã£ãŸäºº" - desc: "説明" - export: "エクスãƒãƒ¼ãƒˆ" - import: "インãƒãƒ¼ãƒˆ" - import-by-code: "ãã‚Œã‹ã‚³ãƒ¼ãƒ‰ã‚’è²¼ã£ã¤ã‘ã‚‹" - theme-name-required: "テーマåã¯çµ¶å¯¾è¦ã‚‹ã§" -common/views/components/cw-button.vue: - hide: "ã‚‚ã†ãˆãˆã‚" - show: "見ãŸã„ã‚„ã‚?" - poll: "アンケート" -common/views/components/messaging.vue: - search-user: "ユーザーを探ã™" - you: "ã‚ã‚“ã•ã‚“" - no-history: "å±¥æ´ã¯ã‚らã¸ã‚“ã§" - user: "ユーザー" -common/views/components/messaging-room.vue: - no-history: "ã“れよりéŽåŽ»ã®å±¥æ´ã¯ã‚らã¸ã‚“ã§" - new-message: "æ–°ã—ã„メッセージãŒã‚ã‚‹ã§" -common/views/components/messaging-room.form.vue: - input-message-here: "ã“ã“ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸æ›¸ã„ã¦ã‚„" - send: "é€ä¿¡" - attach-from-local: "PCã‹ã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付ã™ã‚‹" - attach-from-drive: "ドライブã‹ã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付ã™ã‚‹" -common/views/components/messaging-room.message.vue: - is-read: "æ—¢èª" - deleted: "ã“ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯å‰Šé™¤ã•ã‚ŒãŸã‚" -common/views/components/nav.vue: - about: "Misskeyã«ã¤ã„ã¦" - stats: "統計" - status: "ステータス" - wiki: "Wiki" - donors: "支æ´è€…" - repository: "リãƒã‚¸ãƒˆãƒª" - develop: "開発者" - feedback: "フィードãƒãƒƒã‚¯" -common/views/components/note-menu.vue: - detail: "ã‚‚ã£ã¨" - copy-link: "リンクをコピー" - favorite: "ãŠæ°—ã«å…¥ã‚Š" - unfavorite: "ãŠæ°—ã«å…¥ã‚Šã‚„ã‚ã‚‹" - pin: "ピン留ã‚" - unpin: "ピン留ã‚ã‚„ã‚ã‚‹" - delete: "ã»ã‹ã™" - delete-confirm: "ã“ã®æŠ•ç¨¿ã‚’削除ã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" - remote: "投稿元ã«è¡Œã£ã¦ã¿ã‚ˆã‹" -common/views/components/user-menu.vue: - mute: "ミュート" - block: "ブãƒãƒƒã‚¯" - suspend: "å‡çµ" -common/views/components/poll.vue: - vote-to: "「{}ã€ã«æŠ•ç¥¨ã‚„ï¼" - vote-count: "{}票" - vote: "投票ã™ã‚‹ã§" - show-result: "çµæžœã‚’見よã‹" - voted: "投票済ã¿ã‚„" -common/views/components/poll-editor.vue: - no-only-one-choice: "é¸æŠžè‚¢ãŒæœ€ä½Ž2ã¤å¿…è¦ã‚„ã§" - choice-n: "é¸æŠžè‚¢{}" - remove: "ã“ã®é¸æŠžè‚¢ã‚’消ã™ã§" - add: "+é¸æŠžè‚¢ã‚’è¿½åŠ " - destroy: "アンケートをã»ã‹ã" - day: "æ—¥" -common/views/components/reaction-picker.vue: - choose-reaction: "リアクションã€ã©ã‚Œã«ã™ã‚‹ã‚“や?" -common/views/components/emoji-picker.vue: - custom-emoji: "カスタム絵文å—" - people: "人" - animals-and-nature: "動物&自然" - food-and-drink: "食ã„ã‚‚ã‚“&飲ã¿ã‚‚ã‚“" - activity: "アクティビティ" - travel-and-places: "å ´æ‰€" - objects: "物" - symbols: "記å·" - flags: "æ——" -common/views/components/settings/app-type.vue: - info: "ページもã£ãºã‚“èªã¿è¾¼ã‚“ã らåæ˜ ã—ãŸã‚‹ã§ã€‚" -common/views/components/signin.vue: - username: "ユーザーå" - password: "パスワード" - token: "トークン" - signing-in: "サインインä¸ã‚„..." - or: "ãã‚Œã‹" - signin-with-twitter: "Twitterã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³" - signin-with-github: "GitHubã§ãƒã‚°ã‚¤ãƒ³" - signin-with-discord: "Discordã§ãƒã‚°ã‚¤ãƒ³" - login-failed: "ãªã‚“ã‹ãƒã‚°ã‚¤ãƒ³ã§ãã‚“ã‹ã£ãŸã‚。ユーザーåã¨ãƒ‘スワードã¨ã‹ã‚’確èªã—ã¦ã‚„。" -common/views/components/signup.vue: - invitation-code: "招待コード" - invitation-info: "招待コードをもã£ã¨ã‚‰ã‚“ã®ã‚„ã£ãŸã‚‰ã€<a href=\"{}\">管ç†è€…</a>ã¾ã§é€£çµ¡ã—ã¦ã‚„。" - username: "ユーザーå" - checking: "確èªä¸ã‚„……" - available: "使ãˆã‚‹ã§" - unavailable: "ã‚‚ã†ä½¿ã‚ã‚Œã¨ã‚‹ã§" - error: "通信ã‚ã‹ã‚“ã‚" - invalid-format: "a~zã€A~Zã€0~9ã€_ãŒä½¿ãˆã‚‹ã§" - too-short: "1æ–‡å—以上やã§ï¼" - too-long: "20æ–‡å—以内やã§" - password: "パスワード" - password-placeholder: "8æ–‡å—以上ã«ã—ã¨ãã‚„" - weak-password: "ã¸ã¼ã„パスワード" - normal-password: "ã¼ã¡ã¼ã¡ãªãƒ‘スワード" - strong-password: "良ã•ã’ãªãƒ‘スワード" - retype: "ã‚‚ã£ã‹ã„å…¥åŠ›é ¼ã‚€ã§" - retype-placeholder: "確èªã®ãŸã‚ã‚‚ã£ãºã‚“入力ã—ã¦ã‚„" - password-matched: "一致ã—ã¨ã‚‹ã§" - password-not-matched: "一致ã—ã¨ã‚‰ã‚“ã§" - recaptcha: "èªè¨¼" - create: "アカウント作æˆ" - some-error: "何ã‹ã‚ˆã†åˆ†ã‹ã‚‰ã‚“ã‘ã©ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ä½œæˆã«å¤±æ•—ã—ã¦ã—ã‚‚ãŸã‚。ã™ã¾ã‚“ãŒã‚‚ã£ãºã‚“試ã—ã¦ãã‚Œã¸ã‚“ã‹ï¼Ÿ" -common/views/components/special-message.vue: - new-year: "ãŠãŠãã«ã€‚今年もよã‚ã—ã‚…ã†ã€‚" - christmas: "メリークリスマスï¼" -common/views/components/stream-indicator.vue: - connecting: "ã¤ãªã„ã©ã‚‹ã§" - reconnecting: "ã¤ãªãŽç›´ã™ã§" - connected: "ã¤ãªã„ã ã‚" -common/views/components/notification-settings.vue: - title: "通知" -common/views/components/integration-settings.vue: - title: "サービス連æº" - connect: "ã¤ãªã’ã‚‹" - disconnect: "接続をã»ã‹ã™" - connected-to: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¨ç¹‹ãŒã£ã¨ã‚‹ã§" -common/views/components/github-setting.vue: - description: "ã‚ã‚“ãŸãŒã¤ã“ã¨ã‚‹TwitterアカウントをMisskeyアカウントã«æŽ¥ç¶šã—ã¨ãã¨ã€ã‚ã‚“ã•ã‚“ã®ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã«Twitterã‚¢ã‚«ã‚¦ãƒ³ãƒˆæƒ…å ±ãŒè¡¨ç¤ºã•ã‚Œã‚‹ã‚ˆã†ã«ãªã£ãŸã‚Šã€Twitterを使ã†ãŸä¾¿åˆ©ãªã‚µã‚¤ãƒ³ã‚¤ãƒ³ãŒä½¿ãˆã‚‹ã‚ˆã†ã«ãªã£ãŸã‚Šã™ã‚“ã§ã€‚" - connected-to: "次ã®GitHubアカウントã«æŽ¥ç¶šã•ã‚Œã¨ã‚‹ã§" - detail: "ãã‚ã—ã..." - reconnect: "ã¤ãªãŽç›´ã™" - connect: "GitHubã¨æŽ¥ç¶šã™ã‚‹" - disconnect: "接続をã»ã‹ã™" -common/views/components/discord-setting.vue: - description: "ã‚ã‚“ãŸãŒã¤ã“ã¨ã‚‹DiscordアカウントをMisskeyアカウントã«æŽ¥ç¶šã—ã¨ãã¨ã€ã‚ã‚“ã•ã‚“ã®ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã«Discordã‚¢ã‚«ã‚¦ãƒ³ãƒˆæƒ…å ±ãŒè¡¨ç¤ºã•ã‚Œã‚‹ã‚ˆã†ã«ãªã£ãŸã‚Šã€Discordを使ã†ãŸä¾¿åˆ©ãªã‚µã‚¤ãƒ³ã‚¤ãƒ³ãŒä½¿ãˆã‚‹ã‚ˆã†ã«ãªã£ãŸã‚Šã™ã‚“ã§ã€‚" - connected-to: "次ã®Discordアカウントã«æŽ¥ç¶šã•ã‚Œã¨ã‚‹ã§" - detail: "ãã‚ã—ã..." - reconnect: "ã¤ãªãŽç›´ã™" - connect: "Discordã¨æŽ¥ç¶šã™ã‚‹" - disconnect: "接続をã»ã‹ã™" -common/views/components/uploader.vue: - waiting: "å¾…ã£ã¨ã‚‹" -common/views/components/visibility-chooser.vue: - public: "公開" - home: "ホーム" - home-desc: "ホームタイムライン以外ã«è¦‹ã›ã‚“ã¨ã£ã¦" - followers: "フォãƒãƒ¯ãƒ¼" - followers-desc: "自分ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã«è¦‹ã›ã‚“ã¨ã£ã¦" - specified: "ダイレクト" - specified-desc: "今ã‹ã‚‰è¨€ã†ãƒ¦ãƒ¼ã‚¶ãƒ¼ä»¥å¤–ã«è¦‹ã›ã‚“ã¨ã£ã¦ã‚„" - local-public: "公開 (ãƒãƒ¼ã‚«ãƒ«ã ã‘)" - local-public-desc: "リモートã«ã¯è¦‹ã›ã¸ã‚“" - local-home: "ホーム(ãƒãƒ¼ã‚«ãƒ«ã ã‘)" - local-followers: "フォãƒãƒ¯ãƒ¼ (ãƒãƒ¼ã‚«ãƒ«ã ã‘)" -common/views/components/trends.vue: - count: "{}人ãŒæŠ•ç¨¿" - empty: "æµè¡Œã¯è‡ªåˆ†ã§ä½œã‚‹ã‚“ã‚„" -common/views/components/language-settings.vue: - title: "表示言語" - pick-language: "言語é¸ã‚“ã§ã‚„" - recommended: "ã“ã‚Œãˆãˆã§" - auto: "å‹æ‰‹ã«ã‚„ã‚‹" - specify-language: "言語é¸ã³ã‚„" - info: "ページもã£ãºã‚“èªã¿è¾¼ã‚“ã らåæ˜ ã—ãŸã‚‹ã§ã€‚" -common/views/components/profile-editor.vue: - title: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - name: "åå‰" - account: "アカウント" - location: "å ´æ‰€" - description: "自己紹介" - language: "言語" - birthday: "誕生日" - avatar: "ã‚¢ãƒã‚¿ãƒ¼" - banner: "ãƒãƒŠãƒ¼" - is-cat: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯Catã‚„ã§" - is-bot: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯Botã‚„ã§" - is-locked: "他人ã®ãƒ•ã‚©ãƒãƒ¼ã¯è¨±å¯ã—ã¦ã‹ã‚‰ã‚„ï¼" - careful-bot: "Botã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã ã‘ã¯è¨±å¯åˆ¶ã‚„" - advanced: "ãã®ä»–" - privacy: "プライãƒã‚·ãƒ¼ã£ã¦ãªã‚“や?オカンã®å¹´é½¢ã‹ï¼Ÿ" - save: "ä¿å˜" - saved: "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’ä¿å˜ã—ãŸã§" - uploading: "アップãƒãƒ¼ãƒ‰ã—ã¨ã‚Šã¾ã™" - upload-failed: "ã“れアップãƒãƒ¼ãƒ‰ã§ã‘ã¸ã‚“ã‚" - unable-to-process: "ã‚ã‹ã‚“ã€ç„¡ç†ã‚„ã‚" - email: "メールè¨å®š" - email-address: "メールアドレス" - email-verified: "ã“ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹OKã‚„ï¼" - email-not-verified: "メールアドレスãŒç¢ºèªã•ã‚Œã¨ã‚‰ã‚“。メールボックスもã£ãºã‚“見ã¦ãã‚Œã¸ã‚“?" - export: "エクスãƒãƒ¼ãƒˆ" - import: "インãƒãƒ¼ãƒˆ" - export-targets: - following-list: "フォãƒãƒ¼" - mute-list: "ミュート" - blocking-list: "ブãƒãƒƒã‚¯" - user-lists: "リスト" - enter-password: "パスワードを入れã¦ã‚„" -common/views/components/user-list-editor.vue: - users: "ユーザー" - add-user: "ユーザー増やã™" -common/views/components/user-group-editor.vue: - invite: "招待" -common/views/components/user-lists.vue: - user-lists: "リスト" - list-name: "リストå" -common/views/components/user-groups.vue: - invites: "招待" -common/views/widgets/broadcast.vue: - fetching: "見ã¦ã¿ã‚‹ã‚…" - no-broadcasts: "ãŠçŸ¥ã‚‰ã›ã¯ã‚らã¸ã‚“ã§" - have-a-nice-day: "ãŠãŠãã«ï¼" - next: "次" -common/views/widgets/calendar.vue: - year: "{}å¹´" - month: "{}月" - day: "{}æ—¥" - today: "今日:" - this-month: "今月:" - this-year: "今年:" -common/views/widgets/photo-stream.vue: - title: "フォトストリーム" - no-photos: "写真ã¯ã‚らã¸ã‚“ã§" -common/views/widgets/posts-monitor.vue: - title: "投稿ãƒãƒ£ãƒ¼ãƒˆ" - toggle: "表示を切り替ãˆ" -common/views/widgets/hashtags.vue: - title: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" -common/views/widgets/server.vue: - title: "サーãƒãƒ¼æƒ…å ±" - toggle: "表示を切り替ãˆ" -common/views/widgets/memo.vue: - title: "付箋" - memo: "書ãã‚“ã‚„ï¼" - save: "ä¿å˜" -common/views/widgets/slideshow.vue: - folder-customize-mode: "フォルダを指定ã™ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ä¸€æ—¦ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºãƒ¢ãƒ¼ãƒ‰ã‚’終了ã—ã¦ã‚„" - folder: "クリックã—ã¦ãƒ•ã‚©ãƒ«ãƒ€æ±ºã‚ã¦ã‚„" - no-image: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ã«ã¯ç”»åƒç„¡ã„ã‚" -common/views/widgets/tips.vue: - tips-line1: "<kbd>t</kbd>ã§ã‚¿ã‚¤ãƒ ラインã«ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ã§ãã‚“ã§" - tips-line2: "<kbd>p</kbd>ã¾ãŸã¯<kbd>n</kbd>ã§æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ ã‚’é–‹ãã§" - tips-line3: "投稿フォームã«ã¯ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグ&ドãƒãƒƒãƒ—ã§ãã‚“ã§" - tips-line4: "投稿フォームã«ã‚¯ãƒªãƒƒãƒ—ボードã«ãŠã‚‹ç”»åƒãƒ‡ãƒ¼ã‚¿ã‚’ペーストã§ãã‚“ã§" - tips-line5: "ドライブã«ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグ&ドãƒãƒƒãƒ—ã—ã¦ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã§ãã‚“ã§" - tips-line6: "ドライブやã¨ã€ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグã—ã¦ãƒ•ã‚©ãƒ«ãƒ€ç§»å‹•ã§ãã‚“ã§" - tips-line7: "ドライブやã¨ã€ãƒ•ã‚©ãƒ«ãƒ€ã‚’ドラッグã—ã¦ãƒ•ã‚©ãƒ«ãƒ€ç§»å‹•ã§ãã‚“ã§" - tips-line8: "ホームã¯è¨å®šã‹ã‚‰ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã§ãã‚“ã§" - tips-line9: "Misskeyã¯AGPLv3ã‚„ã§" - tips-line10: "タイムマシンウィジェットを利用ã™ã‚‹ã¨ã€ç°¡å˜ã«éŽåŽ»ã®ã‚¿ã‚¤ãƒ ラインã«é¡ã‚Œã‚“ã§" - tips-line11: "投稿㮠... をクリックã—ã¦ã€ãƒ”ン留ã‚ã‹ã‚‰æŠ•ç¨¿ã‚’ユーザーページã«ãƒ”ン留ã‚ã§ãã‚“ã§" - tips-line13: "投稿ã«æ·»ä»˜ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã¯å…¨ã¦ãƒ‰ãƒ©ã‚¤ãƒ–ã«ä¿å˜ã•ã‚Œã‚“ã§" - tips-line14: "ホームã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºä¸ã€ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ãƒ‡ã‚¶ã‚¤ãƒ³ã‚’変更ã§ãã‚“ã§" - tips-line17: "「**ã€ã§ãƒ†ã‚ストを囲ã£ãŸã‚‹ã¨**強調表示**ã•ã‚Œã‚“ã§" - tips-line19: "ã„ãã¤ã‹ã®ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã¯ãƒ–ラウザã®å¤–ã«åˆ‡ã‚Šé›¢ã™ã“ã¨ãŒã§ãã‚“ã§" - tips-line20: "カレンダーウィジェットã®ãƒ‘ーセンテージã¯ã€çµŒéŽã®å‰²åˆã‚’示ã—ã¦ã‚“ãã‚“" - tips-line21: "APIã‚’ã¤ã“ã¦botã®é–‹ç™ºãªã©ã‚‚è¡Œãˆã‚‹ã§" - tips-line24: "Misskeyã¯2014å¹´ã«ã‚µãƒ¼ãƒ“スを開始ã—ãŸã‚“よ" - tips-line25: "対応ブラウザやã£ãŸã‚‰Misskeyã‚’é–‹ã„ã¨ã‚‰ã‚“ã§ã‚‚通知をå—ã‘å–れんã§" -common/views/pages/follow.vue: - signed-in-as: "{}ã¨ã—ã¦ã‚µã‚¤ãƒ³ã‚¤ãƒ³ä¸" - following: "フォãƒãƒ¼ã—ã¨ã‚‹" - follow: "フォãƒãƒ¼" - request-pending: "フォãƒãƒ¼ã®è¨±ã—å¾…ã£ã¨ã‚‹" - follow-processing: "今フォãƒãƒ¼å‡¦ç†ã‚„ã£ã¨ã‚‹â€¥" - follow-request: "フォãƒãƒ¼è¨±ã—ã¦ãれやï¼è¨€ã†ã¦ã¿ã‚‹" -common/views/pages/follow-requests.vue: - received-follow-requests: "フォãƒãƒ¼è¨±ã—ã¦ãれやï¼è¨€ã†ã¦ã¿ã‚‹" -desktop: - banner-crop-title: "ã©ã“ãƒãƒŠãƒ¼ã¨ã—ã¦å‡ºã™ï¼Ÿ" - banner: "ãƒãƒŠãƒ¼" - uploading-banner: "æ–°ã—ã„ãƒãƒŠãƒ¼ã‚’アップãƒãƒ¼ãƒ‰ã—ã¨ã‚‹ã§" - banner-updated: "ãƒãƒŠãƒ¼ã‚’æ›´æ–°ã—ãŸã§" - choose-banner: "ãƒãƒŠãƒ¼ã«ã™ã‚‹ç”»åƒé¸ã‚“ã§ã‚„" - avatar-crop-title: "ã©ã“ã‚¢ãƒã‚¿ãƒ¼ã¨ã—ã¦å‡ºã—ã¨ã?" - avatar: "ã‚¢ãƒã‚¿ãƒ¼" - uploading-avatar: "æ–°ã—ã„ã‚¢ãƒã‚¿ãƒ¼ã‚’アップãƒãƒ¼ãƒ‰ã—ã¨ã‚‹ã§" - avatar-updated: "ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ãŸã§" - choose-avatar: "ã‚¢ãƒã‚¿ãƒ¼ã«ã™ã‚‹ç”»åƒé¸ã‚“ã§ã‚„" - unable-to-process: "ã‚ã‹ã‚“ã€ç„¡ç†ã‚„ã‚" - invalid-filetype: "ã“ã®å½¢å¼ã®ãƒ•ã‚¡ã‚¤ãƒ«ç„¡ç†ã‚„ãã‚“" -desktop/views/components/activity.chart.vue: - total: "é»’ã„ã® ... 全部" - notes: "é’ã„ã® ... 投稿" - replies: "赤ã„ã® ... 返信" - renotes: "碧ã„ã® ... Renotes" -desktop/views/components/activity.vue: - title: "アクティビティ" - toggle: "表示変ãˆã‚‹" -desktop/views/components/calendar.vue: - title: "{year}å¹´ {month} 月" - prev: "å‰ã®æœˆ" - next: "次ã®æœˆ" - go: "クリックã—ã¦ã‚¿ã‚¤ãƒ リープ" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count}ファイルé¸æŠžä¸" - upload: "PCã‹ã‚‰ãƒ‰ãƒ©ã‚¤ãƒ–ã«ãƒ•ã‚¡ã‚¤ãƒ«ä¸Šã’ã‚‹" - cancel: "ã‚„ã‚ã¨ãã‚" - ok: "ãã†ã™ã‚‹" - choose-prompt: "ファイルé¸ã‚“ã§ã‚„" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "ã‚„ã‚ã¨ãã‚" - ok: "ãã†ã™ã‚‹" - choose-prompt: "フォルダé¸ã‚“ã§ã‚„" -desktop/views/components/crop-window.vue: - skip: "クãƒãƒƒãƒ—ã›ãƒ¼ã¸ã‚“ã‚" - cancel: "ã‚„ã‚ã¨ãã‚" - ok: "ãã†ã™ã‚‹" -desktop/views/components/drive-window.vue: - used: "使ã†ã¨ã‚‹" -desktop/views/components/drive.file.vue: - avatar: "ã‚¢ãƒã‚¿ãƒ¼" - banner: "ãƒãƒŠãƒ¼" - nsfw: "見ãŸã‚‰ã‚ã‹ã‚“ã§" - contextmenu: - rename: "åå‰ã‚’変ãˆã‚‹ã§" - mark-as-sensitive: "見ãŸã‚‰ã‚ã‹ã‚“æ„Ÿã˜ã«ã—ã¨ã" - unmark-as-sensitive: "ã‚„ã£ã±è¦‹ã›ãŸã‚‹ã‚" - copy-url: "URLをコピー" - download: "ダウンãƒãƒ¼ãƒ‰" - else-files: "ãã®ä»–" - set-as-avatar: "ã‚¢ãƒã‚¿ãƒ¼ã«ã™ã‚‹" - set-as-banner: "ãƒãƒŠãƒ¼ã«ã™ã‚‹" - open-in-app: "アプリã§é–‹ã" - add-app: "アプリ増やã™" - rename-file: "ファイルåã‚’ã„らã†(変ãˆã‚‹)" - input-new-file-name: "æ–°ã—ã„ファイルåを入力ã—ã¦ã‚„" - copied: "コピー完了や" - copied-url-to-clipboard: "URLをクリップボードã«å†™ã—ãŸã‚" -desktop/views/components/drive.folder.vue: - unable-to-process: "ã‚ã‹ã‚“ã€ç„¡ç†ã‚„ã‚" - circular-reference-detected: "移動先ã®ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã¯ã€ç§»å‹•ã™ã‚‹ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã®ã‚µãƒ–フォルダーや。" - unhandled-error: "よã†ã‚ã‹ã‚‰ã‚“" - contextmenu: - move-to-this-folder: "ã“ã“ã«æŒã£ã¦ãã‚‹ã‚" - show-in-new-window: "æ–°ã—ã„ウィンドウã§å‡ºã™" - rename: "åå‰ã‚’変ãˆã‚‹ã§" - rename-folder: "フォルダåを変ãˆã‚‹ã§" - input-new-folder-name: "æ–°ã—ã„フォルダåを入力ã—ã¦ã‚„" - else-folders: "ãã®ä»–" -desktop/views/components/drive.vue: - search: "検索" - empty-draghover: "ドãƒãƒƒãƒ—ã™ã‚‹ã«ã‚ƒï¼ãŠéšä»¥å¤–ãªã‚‰ä½•ã§ã‚‚ã„ã„ã«ã‚ƒï¼" - empty-drive: "ドライブã«ã¯ä½•ã‚‚ã‚らã¸ã‚“ã§ã€‚" - empty-drive-description: "å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã€ã‚’é¸ã‚“ã ã‚Šã€ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドラッグ&ドãƒãƒƒãƒ—ã™ã‚‹ã“ã¨ã§ã‚‚アップãƒãƒ¼ãƒ‰ã§ãã‚“ãん。" - empty-folder: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã¯ç©ºã‚„" - unable-to-process: "ã‚ã‹ã‚“ã€ç„¡ç†ã‚„ã‚" - circular-reference-detected: "移動先ã®ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã¯ã€ç§»å‹•ã™ã‚‹ãƒ•ã‚©ãƒ«ãƒ€ãƒ¼ã®ã‚µãƒ–フォルダーや。" - unhandled-error: "よã†ã‚ã‹ã‚‰ã‚“" - url-upload: "URLアップãƒãƒ¼ãƒ‰" - url-of-file: "ã“ã®URLã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã—ãŸã„ãã‚“" - url-upload-requested: "アップãƒãƒ¼ãƒ‰ã—ãŸã„言ã†ã¨ã„ãŸã§" - may-take-time: "アップãƒãƒ¼ãƒ‰çµ‚ã‚ã‚‹ã‚“ã«ã¡ã‚‡ã„時間ã‹ã‹ã‚‹ã‹ã‚‚ã—ã‚Œã¸ã‚“ã‚。" - create-folder: "フォルダー作æˆ" - folder-name: "フォルダーå" - contextmenu: - create-folder: "フォルダー作る" - upload: "ファイル上ã’ã‚‹" - url-upload: "URLã¤ã“ã†ã¦ä¸Šã’ã‚‹" -desktop/views/components/media-video.vue: - sensitive: "ã¡ã‚‡ã£ã¨è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - click-to-show: "クリックã—ã¦è¦‹ã›ã‚‹ã§" -desktop/views/components/followers-window.vue: - followers: "{} ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" -desktop/views/components/followers.vue: - empty: "フォãƒãƒ¯ãƒ¼ã¯ãŠã‚‰ã‚“ã£ã½ã„ã§ã€çŸ¥ã‚‰ã‚“ã‘ã©ã€‚" -desktop/views/components/following-window.vue: - following: "{} ã®ãƒ•ã‚©ãƒãƒ¼" -desktop/views/components/following.vue: - empty: "フォãƒãƒ¼ä¸ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ãŠã‚‰ã‚“ã£ã½ã„ã§ã€çŸ¥ã‚‰ã‚“ã‘ã©ã€‚" -desktop/views/components/game-window.vue: - game: "ゲーム" -desktop/views/components/home.vue: - done: "完了" - add-widget: "ウィジェット増やã™" - add: "増やã™" -desktop/views/input-dialog.vue: - cancel: "ã‚„ã‚ã¨ãã‚" - ok: "ã“れやï¼" -desktop/views/components/note-detail.vue: - private: "ã“ã®æŠ•ç¨¿ã¯è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - deleted: "ã“ã®æŠ•ç¨¿ãªã‚“ã‹ç„¡ããªã£ã¦ã‚‚ã†ãŸã‚" - location: "ã“ã“ãŠã‚‹ã§:" - renote: "Renote" - add-reaction: "リアクション" -desktop/views/components/note.vue: - reply: "è¿”ã™" - renote: "Renote" - add-reaction: "リアクション" - detail: "ã‚‚ã£ã¨" - private: "ã“ã®æŠ•ç¨¿ã¯è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - deleted: "ã“ã®æŠ•ç¨¿ãªã‚“ã‹ç„¡ããªã£ã¦ã‚‚ã†ãŸã‚" -desktop/views/components/notes.vue: - error: "ã‚ã‹ã‚“ã€èªã¿è¾¼ã‚ã¸ã‚“ã‚" - retry: "ã‚‚ã£ãºã‚“" -desktop/views/components/notifications.vue: - empty: "ã‚らã¸ã‚“ï¼" -desktop/views/components/post-form.vue: - posted: "投稿ã—ãŸã§ï¼" - replied: "返信ã—ãŸã§ï¼" - reposted: "Renoteã—ãŸã§ï¼" - note-failed: "投稿ã«å¤±æ•—ã—ãŸã§" - reply-failed: "返信ã«å¤±æ•—ã—ãŸã§" - renote-failed: "Renoteã§ã‘ã¸ã‚“" -desktop/views/components/post-form-window.vue: - note: "æ–°è¦æŠ•ç¨¿" - reply: "è¿”ã™" - attaches: "添付: {}メディア" - uploading-media: "{}個ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’上ã’ã¨ã‚“ãん……" -desktop/views/components/progress-dialog.vue: - waiting: "å¾…ã£ã¨ã‚‹" -desktop/views/components/renote-form.vue: - quote: "å–ã£ã¦ãる……" - cancel: "ã‚„ã‚ã¨ãã‚" - renote: "Renote" - renote-home: "Renote (Home)" - reposting: "ã‚„ã£ã¨ã‚Šã¾ã™..." - success: "Renoteã—ãŸã§ï¼" - failure: "Renoteã§ã‘ã¸ã‚“" -desktop/views/components/renote-form-window.vue: - title: "ã“ã®æŠ•ç¨¿ã‚’Renoteã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" -desktop/views/pages/user-following-or-followers.vue: - following: "{user}ã®ãƒ•ã‚©ãƒãƒ¼" - followers: "{user}ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" -desktop/views/components/settings.2fa.vue: - intro: "二段階èªè¨¼ã‚’è¨å®šã™ã‚‹ã¨ã€ã‚µã‚¤ãƒ³ã‚¤ãƒ³æ™‚ã«ãƒ‘スワードã ã‘ã¨ã¡ã‚ƒã†ãã¦ã€äºˆã‚登録ã—ã¦ãŠã„ãŸç‰©ç†çš„ãªãƒ‡ãƒã‚¤ã‚¹(例ãˆã°ã‚ã‚“ã•ã‚“ã®ã‚¹ãƒžãƒ¼ãƒˆãƒ•ã‚©ãƒ³ãªã©)ã‚‚å¿…è¦ã«ãªã‚Šã€ã‚ˆã‚Šã‚»ã‚ュリティãŒå‘上ã™ã‚“ã§ã€‚" - detail: "詳細..." - url: "https://www.google.co.jp/intl/ja/landing/2step/" - caution: "登録ã—ãŸãƒ‡ãƒã‚¤ã‚¹ã‚’紛失ã—ã¦ã‚‚ã†ãŸã‚‰ã€ã‚‚ã†Misskeyã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ãã‚“ããªã‚‹ã§ã€‚" - register: "デãƒã‚¤ã‚¹ç™»éŒ²ã™ã‚‹" - already-registered: "ã‚‚ã†è¨å®šçµ‚ã‚ã£ã¨ã‚‹ã‚" - unregister: "è¨å®šã‚’ã»ã‹ã™" - unregistered: "二段階èªè¨¼ã‚‚ã†ã›ãƒ¼ã¸ã‚“ã§" - enter-password: "パスワードを入れã¦ã‚„" - authenticator: "ã¾ãšã€Google Authenticatorã¨ã‹ã®ã‚’ã¤ã“ã¦ã‚‹ãƒ‡ãƒã‚¤ã‚¹ã«ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ã¦ã‚„:" - howtoinstall: "インストール方法ã¯ã“ã“ã‚„ã§" - token: "トークン" - scan: "ã‚“ã§ã€ã“ã“ã«å‡ºã¨ã‚‹QRコードをスã‚ャンã—ã¦ãª:" - done: "最後ã«ãƒ‡ãƒã‚¤ã‚¹ã«è¡¨ç¤ºã•ã‚Œã¨ã‚‹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’入力ã—ã¦ãª:" - submit: "é€ä¿¡" - success: "è¨å®šãŒå®Œäº†ã—ãŸã§ï¼" - failed: "ãªã‚“ã‹è¨å®šã«å¤±æ•—ã—ãŸã§ã€‚トークンを間é•ãˆã¨ã‚‰ã‚“ã‹ç¢ºèªã—ã¦ã‚„。" - info: "次ã®ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã‹ã‚‰ã¯ã€ãƒ‘スワードã«åŠ ãˆã¦ãƒ‡ãƒã‚¤ã‚¹ã«å‡ºã¨ã‚‹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’入力ã—ã¦ãªã€‚" -common/views/components/media-image.vue: - sensitive: "ã¡ã‚‡ã£ã¨è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - click-to-show: "クリックã—ã¦è¦‹ã›ã‚‹ã§" -common/views/components/api-settings.vue: - intro: "API使ã†ã‚“ã‚„ã£ãŸã‚‰ã“ã®ãƒˆãƒ¼ã‚¯ãƒ³ã‚’「iã€ã£ã¡ã‚…ã†ãƒ‘ラメータã«ãã£ã¤ã‘ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§ãã‚‹ã§ã€‚" - caution: "アカウントå‹æ‰‹ã«ã„ã˜ã‚‰ã‚Œã‚‹ã‹ã‚‚知れんã‹ã‚‰ã€ã“ã®ãƒˆãƒ¼ã‚¯ãƒ³ã¯æ•™ãˆãŸã‚‰ã‚ã‹ã‚“ã—ã€ã‚¢ãƒ—リã«ã‚‚書ã„ãŸã‚‰ã‚ã‹ã‚“ã§(ã“ã‚Œã¯ãƒ•ãƒªã¡ã‚ƒã†ã§)" - regeneration-of-token: "トークンæ¼ã‚Œã¦ã‚‚ã†ãŸã‚“ã‚„ã£ãŸã‚‰ã‚‚ã£ã‹ã„生æˆã§ãã‚‹ã§ã€‚" - regenerate-token: "トークンもã£ã‹ã„生æˆ" - token: "Token:" - enter-password: "パスワードを入れã¦ã‚„" - console: - title: "APIコンソール" - endpoint: "エンドãƒã‚¤ãƒ³ãƒˆ" - parameter: "パラメータ" - credential-info: "「iã€ãƒ‘ラメータã¯å‹æ‰‹ã«ä»˜ãã§ã€‚" - send: "é€ã‚‹" - sending: "å¿œç”å¾…ã£ã¨ã‚‹" - response: "ã“ã‚“ãªã‚“è¿”ã£ã¦ããŸã‚" -desktop/views/components/settings.apps.vue: - no-apps: "連æºã—ã¦ã„るアプリケーションã¯ã‚らã¸ã‚“ã§" -common/views/components/drive-settings.vue: - max: "容é‡" - in-use: "使ã†ã¨ã‚‹" - stats: "統計" - default-upload-folder-name: "フォルダ" -common/views/components/mute-and-block.vue: - mute-and-block: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" - mute: "ミュート" - block: "ブãƒãƒƒã‚¯" - no-muted-users: "ミュートã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ãŠã‚‰ã‚“ã§" - no-blocked-users: "ブãƒãƒƒã‚¯ã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ãŠã‚‰ã‚“ã§" - word-mute: "ワードミュート" - muted-words: "ミュートã—ã¨ã‚‹ã‚ーワード" - muted-words-description: "スペースã§åŒºåˆ‡ã‚‹ã¨AND指定ã§ã€æ”¹è¡Œã§åŒºåˆ‡ã‚‹ã¨OR指定や" - save: "ä¿å˜" -common/views/components/password-settings.vue: - reset: "パスワード変ãˆã‚‹" - enter-current-password: "今ã®ãƒ‘スワードを入れã¦ã‚„" - enter-new-password: "ã“ã‚“ã©ã®ãƒ‘スワード入れã¦ã‚„" - enter-new-password-again: "ã‚‚ã£ãºã‚“入れã¦ã‚„" - not-match: "パスワードãŒãŠã†ã¨ã‚‰ã‚“" - changed: "パスワード変ãˆãŸã‚" -common/views/components/post-form-attaches.vue: - mark-as-sensitive: "見ãŸã‚‰ã‚ã‹ã‚“æ„Ÿã˜ã«ã—ã¨ã" - unmark-as-sensitive: "ã‚„ã£ã±è¦‹ã›ãŸã‚‹ã‚" -desktop/views/components/sub-note-content.vue: - private: "ã“ã®æŠ•ç¨¿ã¯è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - deleted: "ã“ã®æŠ•ç¨¿ãªã‚“ã‹ç„¡ããªã£ã¦ã‚‚ã†ãŸã‚" - media-count: "{}ã¤ã®ãƒ¡ãƒ‡ã‚£ã‚¢" - poll: "アンケート" -desktop/views/components/settings.tags.vue: - add: "増やã™" - save: "ä¿å˜" -desktop/views/components/timeline.vue: - home: "ホーム" - local: "ãƒãƒ¼ã‚«ãƒ«" - global: "ã‚°ãƒãƒ¼ãƒãƒ«" - mentions: "ã‚ã‚“ãŸå®›ã¦" - messages: "ダイレクト投稿" - list: "リスト" - hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - add-tag-timeline: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°å¢—ã‚„ã™" - add-list: "リストã«å…¥ã‚Œã‚‹" - list-name: "リストå" -desktop/views/components/ui.header.vue: - welcome-back: "ãŠã‹ãˆã‚Šã€" - adjective: "ã¯ã‚“" -desktop/views/components/ui.header.account.vue: - profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" - lists: "リスト" - follow-requests: "フォãƒãƒ¼è¨±ã—ã¦ãれやï¼è¨€ã†ã¦ã¿ã‚‹" - admin: "管ç†" -desktop/views/components/ui.header.nav.vue: - game: "ゲーム" -desktop/views/components/ui.header.notifications.vue: - title: "通知" -desktop/views/components/ui.header.post.vue: - post: "æ–°è¦æŠ•ç¨¿" -desktop/views/components/ui.header.search.vue: - placeholder: "検索" -desktop/views/components/user-preview.vue: - notes: "投稿" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" -desktop/views/components/users-list.vue: - all: "ã™ã¹ã¦" - iknow: "知ã£ã¨ã‚‹" - fetching: "èªã¿è¾¼ã‚“ã©ã‚Šã¾ã™" -desktop/views/components/users-list-item.vue: - followed: "フォãƒãƒ¼ã•ã‚Œã¨ã‚‹ã§" -desktop/views/components/window.vue: - popout: "ãƒãƒƒãƒ—アウト" - close: "ã•ã„ãªã‚‰" -admin/views/index.vue: - dashboard: "ダッシュボード" - instance: "インスタンス" - emoji: "カスタム絵文å—" - moderators: "モデレーター" - users: "ユーザー" - federation: "連åˆ" - announcements: "知ã£ã¨ã„ã¦ã‚„" - back-to-misskey: "Misskeyã«æˆ»ã‚‹" -admin/views/dashboard.vue: - dashboard: "ダッシュボード" - accounts: "アカウント" - notes: "投稿" - drive: "ドライブ" - instances: "インスタンス" - this-instance: "ワイã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹" - federated: "連åˆ" -admin/views/logs.vue: - levels: - info: "æƒ…å ±" - error: "エラー" -admin/views/abuse.vue: - details: "ã‚‚ã£ã¨" - remove-report: "削除" -admin/views/instance.vue: - instance: "インスタンス" - instance-name: "インスタンスå" - instance-description: "インスタンスã®ç´¹ä»‹" - host: "ホスト" - banner-url: "ãƒãƒŠãƒ¼ç”»åƒURL" - languages: "インスタンスã®å¯¾è±¡è¨€èªž" - languages-desc: "スペースã§åŒºåˆ‡ã£ã¦è¤‡æ•°è¨å®šã§ãã‚‹ã§ã€‚" - maintainer-config: "管ç†è€…æƒ…å ±" - maintainer-name: "管ç†è€…å" - maintainer-email: "管ç†è€…ã®é€£çµ¡å…ˆ" - drive-config: "ドライブã®è¨å®š" - object-storage-endpoint: "エンドãƒã‚¤ãƒ³ãƒˆ" - cache-remote-files: "リモートã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã‚ャッシュã™ã‚‹" - local-drive-capacity-mb: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" - remote-drive-capacity-mb: "リモートユーザーã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" - mb: "メガãƒã‚¤ãƒˆå˜ä½" - recaptcha-config: "reCAPTCHAã®è¨å®š" - recaptcha-info: "reCAPTCHAを有効ã«ã™ã‚‹ã«ã¯reCAPTCHAトークンãŒè¦ã‚‹ã§ã€‚https://www.google.com/recaptcha/intro/ ã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã—ã¦ãªã€‚" - enable-recaptcha: "reCAPTCHAを有効ã«ã™ã‚‹" - recaptcha-preview: "試ã—ã¦ã¿ã‚‹" - twitter-integration-config: "Twitter連æºã®è¨å®š" - twitter-integration-info: "コールãƒãƒƒã‚¯URL㯠{url} ã«è¨å®šã—ã¦ã‚„。" - enable-twitter-integration: "Twitter連æºã‚’有効ã«ã™ã‚‹" - twitter-integration-consumer-key: "Consumer key" - twitter-integration-consumer-secret: "Consumer secret" - github-integration-config: "GitHub連æºã®è¨å®š" - github-integration-info: "コールãƒãƒƒã‚¯URL㯠{url} ã«è¨å®šã—ã¦ã‚„。" - enable-github-integration: "GitHub連æºã‚’使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-config: "Discord連æºã®è¨å®š" - discord-integration-info: "コールãƒãƒƒã‚¯URL㯠{url} ã«è¨å®šã—ã¦ã‚„。" - enable-discord-integration: "Discord連æºã‚’有効ã«ã™ã‚‹" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - proxy-account-config: "プãƒã‚シアカウントã®è¨å®š" - proxy-account-info: "プãƒã‚シアカウントã¯ã€ä»£ã‚ã‚Šã«ãƒ•ã‚©ãƒãƒ¼ã—ã¦ãれるアカウントや。例ãˆã°ã€551ã«è±šã¾ã‚“ãŒç„¡ã„ã¨ãã‚„ã£ãŸã‚Šã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’アカウントã«å…¥ã‚ŒãŸã¨ãã€ãƒªã‚¹ãƒˆã«å…¥ã‚Œã‚‰ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒèª°ã‹ã‚‰ã‚‚フォãƒãƒ¼ã•ã‚Œã¦ãªã„ã¨å¯‚ã—ã„やん。寂ã—ã„ã—ã€ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティもé…é”ã•ã‚Œã¸ã‚“ã‹ã‚‰ã€ãƒ—ãƒã‚シアカウントãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ãれるã§ã€‚ãˆãˆã‚„ã¤ã‚„ん…" - proxy-account-username: "プãƒã‚シアカウントã®ãƒ¦ãƒ¼ã‚¶ãƒ¼å" - proxy-account-username-desc: "プãƒã‚ã‚·ã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを指定ã—ã¦ã‚„" - proxy-account-warn: "アカウント作るんã¯ã‚ã‚“ãŸãŒã‚„ã£ã¦ã‚„。ã‚ã‚“ãŸã®ãŠã‹ã‚“ã‚‚Misskeyã‚‚ã‚„ã£ã¦ãã‚Œã¸ã‚“ã§ã€‚" - max-note-text-length: "投稿ã®æœ€å¤§æ–‡å—æ•°" - disable-registration: "ユーザー登録ã®å—付をæ¢ã‚ã‚‹" - disable-local-timeline: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを使ãˆã‚“よã†ã«ã™ã‚‹" - invite: "æ¥ã¦ã‚„" - save: "ä¿å˜" - saved: "ä¿å˜ã—ãŸã§ï¼" - email-config: "メールサーãƒãƒ¼ã®è¨å®š" - email-config-info: "メールアドレス確èªã‚„パスワードリセットã®éš›ã«ä½¿ã†ã§ã€‚" - enable-email: "メールé…信を有効ã«ã™ã‚‹" - email: "メールアドレス" - smtp-secure: "SMTP接続ã«æš—黙的ãªSSL/TLSを使用ã™ã‚‹" - smtp-secure-info: "STARTTLS使用時ã¯ã‚ªãƒ•ã«ã—ã¾ã™ã€‚" - smtp-host: "SMTPホスト" - smtp-port: "SMTPãƒãƒ¼ãƒˆ" - smtp-user: "SMTPユーザー" - smtp-pass: "SMTPパスワード" -admin/views/charts.vue: - title: "ãƒãƒ£ãƒ¼ãƒˆ" - per-day: "1æ—¥ã”ã¨" - per-hour: "1時間ã”ã¨" - federation: "フェデレーション" - notes: "投稿" - users: "ユーザー" - drive: "ドライブ" - network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" - charts: - federation-instances: "インスタンスã®å¢—減" - federation-instances-total: "インスタンスã®ç©ç®—" - notes: "投稿ã®å¢—減(çµ±åˆ)" - local-notes: "投稿ã®å¢—減 (ãƒãƒ¼ã‚«ãƒ«)" - remote-notes: "投稿ã®å¢—減 (リモート)" - notes-total: "投稿ã®ç©ç®—" - users: "ユーザーã®å¢—減" - users-total: "ユーザーã®ç©ç®—" - drive: "ドライブ使用é‡ã®å¢—減" - drive-total: "ドライブ使用é‡ã®ç©ç®—" - drive-files: "ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•°ã®å¢—減" - drive-files-total: "ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«æ•°ã®ç©ç®—" - network-requests: "リクエスト" - network-time: "å¿œç”時間" - network-usage: "通信é‡" -admin/views/drive.vue: - operation: "æ“作" - lookup: "照会" - origin: - local: "ãƒãƒ¼ã‚«ãƒ«" - delete: "削除" - mark-as-sensitive: "見ãŸã‚‰ã‚ã‹ã‚“æ„Ÿã˜ã«ã—ã¨ã" - unmark-as-sensitive: "ã‚„ã£ã±è¦‹ã›ãŸã‚‹ã‚" -admin/views/users.vue: - operation: "æ“作" - username-or-userid: "ユーザーåã¾ãŸã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼ID" - user-not-found: "ユーザーãŒè¦‹ã¤ã‹ã‚‰ã¸ã‚“ï¼" - lookup: "照会" - reset-password: "パスワードをリセット" - password-updated: "パスワードã¯ç¾åœ¨ã€Œ{password} ã€ã‚„ã§" - suspend: "å‡çµ" - username: "ユーザーå" - host: "ホスト" - users: - title: "ユーザー" - state: - all: "ã™ã¹ã¦" - moderator: "モデレーター" - origin: - local: "ãƒãƒ¼ã‚«ãƒ«" -admin/views/moderators.vue: - logs: - moderator: "モデレーター" - type: "æ“作" - info: "æƒ…å ±" -admin/views/emoji.vue: - add-emoji: - add: "増やã™" - emojis: - remove: "削除" -admin/views/announcements.vue: - announcements: "知ã£ã¨ãã‚„" - save: "ä¿å˜" - remove: "削除" - add: "増やã™" - saved: "ä¿å˜ã—ãŸã§ï¼" -admin/views/federation.vue: - instance: "インスタンス" - host: "ホスト" - notes: "投稿" - users: "ユーザー" - following: "フォãƒãƒ¼ã—ã¨ã‚‹" - followers: "フォãƒãƒ¯ãƒ¼" - status: "ステータス" - block: "ブãƒãƒƒã‚¯" - lookup: "照会" - instances: "連åˆ" - states: - all: "ã™ã¹ã¦" - blocked: "ブãƒãƒƒã‚¯" - charts: "ãƒãƒ£ãƒ¼ãƒˆ" - chart-srcs: - requests: "リクエスト" - users: "ユーザーã®å¢—減" - users-total: "ユーザーã®ç©ç®—" - notes-total: "投稿ã®ç©ç®—" - drive-usage: "ドライブ使用é‡ã®å¢—減" - drive-usage-total: "ドライブ使用é‡ã®ç©ç®—" - chart-spans: - hour: "1時間ã”ã¨" - day: "1æ—¥ã”ã¨" - blocked-hosts: "ブãƒãƒƒã‚¯" - save: "ä¿å˜" -desktop/views/pages/welcome.vue: - about: "ã‚‚ã†ã¡ã‚‡ã„……" - timeline: "タイムライン" - announcements: "知ã£ã¨ãã‚„" - photos: "最近ã®ç”»åƒ" - powered-by-misskey: "<b>Misskey</b>ã®ãŠã‹ã’ã‚„" - info: "æƒ…å ±" -desktop/views/pages/drive.vue: - title: "ドライブ" -desktop/views/pages/note.vue: - prev: "å‰ã®ã‚„ã¤" - next: "次ã®ã‚„ã¤" -desktop/views/pages/selectdrive.vue: - title: "ファイルをé¸æŠžã—ã¦ã‚„" - ok: "決定" - cancel: "ã‚„ã‚ã¨ãã‚" - upload: "PCã‹ã‚‰ãƒ‰ãƒ©ã‚¤ãƒ–ã«ãƒ•ã‚¡ã‚¤ãƒ«ä¸Šã’ã‚‹" -desktop/views/pages/search.vue: - not-available: "検索機能ã¯ä½¿ãˆã¸ã‚“ã‚。管ç†è€…ãŒãã†è¨€ã†ã¨ã‚‹ã€‚" -desktop/views/pages/user-list.users.vue: - users: "ユーザー" - add-user: "ユーザー増やã™" - username: "ユーザーå" -desktop/views/pages/user/user.followers-you-know.vue: - title: "知ã£ã¨ã‚‹ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - loading: "èªã¿è¾¼ã‚“ã©ã‚Šã¾ã™" - no-users: "フォãƒãƒ¯ãƒ¼å…¨å“¡çŸ¥ã‚‰ã‚“ã‚" -desktop/views/pages/user/user.friends.vue: - title: "よã†è©±ã™ãƒ„レ" - loading: "èªã¿è¾¼ã‚“ã©ã‚Šã¾ã™" - no-users: "よã†è©±ã™ãƒ„レã¯å±…らん" -desktop/views/pages/user/user.photos.vue: - title: "写真" - loading: "èªã¿è¾¼ã‚“ã©ã‚Šã¾ã™" - no-photos: "写真ã¯ã‚らã¸ã‚“ã§" -desktop/views/pages/user/user.header.vue: - posts: "投稿" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - is-bot: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯Botã‚„" - years-old: "{age}æ³" - year: "å¹´" - month: "月" - day: "æ—¥" - follows-you: "フォãƒãƒ¼ã•ã‚Œã¨ã‚‹ã§" -desktop/views/pages/user/user.timeline.vue: - default: "投稿" - with-replies: "投稿ã¨è¿”ä¿¡" - with-media: "メディア" -desktop/views/widgets/notifications.vue: - title: "通知" -desktop/views/widgets/polls.vue: - title: "アンケート" - refresh: "他を見る" - nothing: "ã‚らã¸ã‚“ï¼" -desktop/views/widgets/post-form.vue: - title: "投稿" - note: "投稿" -desktop/views/widgets/profile.vue: - update-banner: "クリックã—ã¦ãƒãƒŠãƒ¼ç·¨é›†" - update-avatar: "クリックã—ã¦ã‚¢ãƒã‚¿ãƒ¼ç·¨é›†" -desktop/views/widgets/trends.vue: - title: "æµè¡Œ" - refresh: "他を見る" - nothing: "ã‚らã¸ã‚“ï¼" -desktop/views/widgets/users.vue: - title: "ãŠã™ã™ã‚ユーザー" - refresh: "他を見る" - no-one: "ãŠã‚‰ã‚“ï¼" -mobile/views/components/drive.vue: - used: "使ã†ã¨ã‚‹" - folder-count: "フォルダ" - count-separator: "ã€" - file-count: "ファイル" - nothing-in-drive: "ドライブã«ã¯ä½•ã‚‚ã‚らã¸ã‚“ã§ã€‚" - folder-is-empty: "ã“ã®ãƒ•ã‚©ãƒ«ãƒ€ä½•ã‚‚ãªã„ã‚" - folder-name: "フォルダーå" - url-prompt: "ã“ã®URLã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã—ãŸã„ãã‚“" - uploading: "アップãƒãƒ¼ãƒ‰ã‚’リクエストã—ãŸã§ã€‚アップãƒãƒ¼ãƒ‰ãŒå®Œäº†ã™ã‚‹ã¾ã§æ™‚é–“ãŒã‹ã‹ã‚‹ã‹ã‚‚分ã‹ã‚‰ã‚“ã€çŸ¥ã‚‰ã‚“ã‘ã©ã€‚" -mobile/views/components/drive-file-chooser.vue: - select-file: "ファイルé¸ã‚“ã§ã‚„" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "フォルダé¸ã‚“ã§ã‚„" -mobile/views/components/drive.file.vue: - nsfw: "ã¡ã‚‡ã£ã¨è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" -mobile/views/components/drive.file-detail.vue: - download: "ダウンãƒãƒ¼ãƒ‰" - rename: "åå‰ã‚’変ãˆã‚‹ã§" - move: "移動" - hash: "ãƒãƒƒã‚·ãƒ¥(md5)" - exif: "EXIF" - nsfw: "ã¡ã‚‡ã£ã¨è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - mark-as-sensitive: "見ãŸã‚‰ã‚ã‹ã‚“æ„Ÿã˜ã«ã—ã¨ã" - unmark-as-sensitive: "ã‚„ã£ã±è¦‹ã›ãŸã‚‹ã‚" -mobile/views/components/media-video.vue: - sensitive: "ã¡ã‚‡ã£ã¨è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - click-to-show: "押ã—ã¦ã¿ã€è¦‹ã›ãŸã‚‹ã‚" -common/views/components/follow-button.vue: - following: "フォãƒãƒ¼ã—ã¨ã‚‹" - follow: "フォãƒãƒ¼" - request-pending: "フォãƒãƒ¼è¨±ã—ã¦ãれるん待ã£ã¨ã‚‹" - follow-processing: "今フォãƒãƒ¼å‡¦ç†ã‚„ã£ã¨ã‚‹â€¥" - follow-request: "フォãƒãƒ¼ã•ã›ã¦ã‚„ï¼è¨€ã†ã¦ã¿ã‚‹" -mobile/views/components/note.vue: - private: "ã“ã®æŠ•ç¨¿ã¯è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - deleted: "ã“ã®æŠ•ç¨¿ãªã‚“ã‹ç„¡ããªã£ã¦ã‚‚ã†ãŸã‚" - location: "ã“ã“ãŠã‚‹ã§:" -mobile/views/components/note-detail.vue: - reply: "è¿”ã™" - reaction: "リアクション" - private: "ã“ã®æŠ•ç¨¿ã¯è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - deleted: "ã“ã®æŠ•ç¨¿ãªã‚“ã‹ç„¡ããªã£ã¦ã‚‚ã†ãŸã‚" - location: "ã“ã“ãŠã‚‹ã§:" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/notifications.vue: - empty: "ã‚らã¸ã‚“ï¼" -mobile/views/components/sub-note-content.vue: - private: "ã“ã®æŠ•ç¨¿ã¯è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" - deleted: "ã“ã®æŠ•ç¨¿ãªã‚“ã‹ç„¡ããªã£ã¦ã‚‚ã†ãŸã‚" - media-count: "{}ã¤ã®ãƒ¡ãƒ‡ã‚£ã‚¢" - poll: "アンケート" -mobile/views/components/ui.header.vue: - welcome-back: "ãŠã‹ãˆã‚Šã€" - adjective: "ã¯ã‚“" -mobile/views/components/ui.nav.vue: - timeline: "タイムライン" - notifications: "通知" - follow-requests: "フォãƒãƒ¼è¨±ã—ã¦ãれやï¼è¨€ã†ã¦ã¿ã‚‹" - search: "検索" - user-lists: "リスト" - widgets: "ウィジェット" - game: "ゲーム" - admin: "管ç†" - about: "Misskeyã£ã¦ãªã‚“や?" -mobile/views/pages/drive.vue: - contextmenu: - upload: "ファイル上ã’ã‚‹" - create-folder: "フォルダー作る" -mobile/views/pages/signup.vue: - lets-start: "📦 始ã‚よã†ã‚„" -mobile/views/pages/followers.vue: - followers-of: "{name}ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼" -mobile/views/pages/following.vue: - following-of: "{name}ã®ãƒ•ã‚©ãƒãƒ¼" -mobile/views/pages/home.vue: - home: "ホーム" - local: "ãƒãƒ¼ã‚«ãƒ«" - global: "ã‚°ãƒãƒ¼ãƒãƒ«" - mentions: "ã‚ã‚“ãŸå®›ã¦" - messages: "ダイレクト投稿" -mobile/views/pages/tag.vue: - no-posts-found: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ{q}ã€ãŒä»˜ã‘られãŸæŠ•ç¨¿ã¯ã‚らã¸ã‚“ã‹ã£ãŸã€‚" -mobile/views/pages/widgets.vue: - dashboard: "ダッシュボード" - add-widget: "増やã™" - customization-tips: "カスタマイズã®ãƒ’ント" -mobile/views/pages/widgets/activity.vue: - activity: "ã‚„ã£ã¨ã‚‹ã“ã¨" -mobile/views/pages/share.vue: - share-with: "{name}ã§å…±æœ‰" -mobile/views/pages/note.vue: - title: "投稿" - prev: "å‰ã®ã‚„ã¤" - next: "次ã®ã‚„ã¤" -mobile/views/pages/games/reversi.vue: - reversi: "リãƒãƒ¼ã‚·" -mobile/views/pages/search.vue: - search: "探ã™" - not-found: "ワイã¯ã€Œ{q}ã€ãªã‚“ã¦æŠ•ç¨¿çŸ¥ã‚‰ã‚“ã‚ã€ç„¡ã„ã‚“ã¡ã‚ƒã†ï¼ŸçŸ¥ã‚‰ã‚“ã‘ã©ã€‚" -mobile/views/pages/selectdrive.vue: - select-file: "ファイルé¸ã‚“ã§ã‚„" -mobile/views/pages/notifications.vue: - notifications: "通知" -mobile/views/pages/settings.vue: - signed-in-as: "ã‚ã‚“ãŸã¯æ©‹ã®ä¸‹ã§æ‹¾ã£ãŸ{}ã‚„ï¼" -mobile/views/pages/user.vue: - follows-you: "フォãƒãƒ¼ã•ã‚Œã¨ã‚‹ã§" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - notes: "投稿" - overview: "ã“ã‚“ãªã‚„ã¤" - timeline: "タイムライン" - media: "メディア" - years-old: "{age}æ³" -mobile/views/pages/user/home.vue: - recent-notes: "最近儲ã‹ã‚Šã¾ã£ã‹ï¼Ÿ" - images: "ç”»åƒ" - activity: "ã‚„ã£ã¨ã‚‹ã“ã¨" - keywords: "ã‚ーワード" - domains: "よã出るドメイン" - frequently-replied-users: "よã†è©±ã™ãƒ„レ" - followers-you-know: "知ã£ã¨ã‚‹ãƒ•ã‚©ãƒãƒ¯ãƒ¼" - last-used-at: "最後ã„ã¤æ¥ãŸï¼Ÿ" -mobile/views/pages/user/home.photos.vue: - no-photos: "写真ã¯ã‚らã¸ã‚“ã§" -deck: - widgets: "ウィジェット" - home: "ホーム" - local: "ãƒãƒ¼ã‚«ãƒ«" - hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - global: "ã‚°ãƒãƒ¼ãƒãƒ«" - mentions: "ã‚ã‚“ãŸå®›ã¦" - direct: "ダイレクト投稿" - notifications: "通知" - list: "リスト" - swap-left: "å·¦ã«ç§»å‹•ã‚„ï¼" - swap-right: "å³ã«ç§»å‹•ã‚„ï¼" - swap-up: "上ã«ç§»å‹•ã‚„ï¼" - swap-down: "下ã«ç§»å‹•ã‚„ï¼" - remove: "カラムã«ã•ã„ãªã‚‰" - add-column: "カラム増やã™" - rename: "åå‰ã‚’変ãˆã‚‹ã§" - stack-left: "å·¦ã«é‡ãã‚“ã§ï¼" - pop-right: "å³ã«å‡ºã™ã§ï¼" -deck/deck.tl-column.vue: - is-media-only: "メディア投稿ã ã‘ã‚„" - edit: "オプション" -deck/deck.user-column.vue: - follows-you: "フォãƒãƒ¼ã•ã‚Œã¨ã‚‹ã§" - posts: "投稿" - following: "フォãƒãƒ¼" - followers: "フォãƒãƒ¯ãƒ¼" - images: "ç”»åƒ" - activity: "アクティビティ" - timeline: "タイムライン" - pinned-notes: "ピン留ã‚ã—ã¯ã£ãŸæŠ•ç¨¿" -docs: - edit-this-page-on-github: "é–“é•ã„や改善点を見ã¤ã‘ã¾ã—ãŸã‹ï¼Ÿ" - edit-this-page-on-github-link: "ã“ã®ãƒšãƒ¼ã‚¸ã‚’GitHubã§ç·¨é›†" -dev/views/index.vue: - manage-apps: "アプリã®ç®¡ç†" -dev/views/apps.vue: - manage-apps: "アプリを管ç†" - create-app: "アプリ作る" - app-missing: "アプリã‚らã¸ã‚“" -dev/views/new-app.vue: - create-app: "アプリケーション作る" - app-name: "アプリケーションã®åå‰" - app-name-desc: "ã‚ã‚“ãŸã®ã‚¢ãƒ—リã®åå‰ã€‚" - app-overview: "ã“ã®ã‚¢ãƒ—リã©ã‚“ãªã‚“?" - callback-url: "コールãƒãƒƒã‚¯URL (ç„¡ãã¦ã‚‚ãˆãˆã§)" - callback-url-desc: "ユーザーãŒèªè¨¼ãƒ•ã‚©ãƒ¼ãƒ ã§èªè¨¼ã—ãŸå¾Œã©ã“ã«é€£ã‚Œã¦ãã‹ã‚’è¨å®šã§ãã‚‹ã§" - authority: "権é™" - authority-desc: "ã“ã“ã«ãƒã‚§ãƒƒã‚¯ã—ãŸæ©Ÿèƒ½ã—ã‹APIã‹ã‚‰ã‚¢ã‚¯ã‚»ã‚¹ã§ãã²ã‚“ã‹ã‚‰æ°—ãƒã¤ã‘ã¦ãª" - authority-warning: "アプリ作ã£ãŸå¾Œã§ã‚‚変ãˆã‚Œã‚‹ã‘ã©ã€æ–°ã—ã„ã‚„ã¤è¿½åŠ ã—ãŸã‚‰ãん時関連付ã„ã¦ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚ーã¯å…¨éƒ¨ã»ã‹ã•ã‚Œã‚‹ã§ã€‚" -pages: - pin-this-page: "ピン留ã‚" - unpin-this-page: "ピン留ã‚ã‚„ã‚ã‚‹" - like: "ãˆãˆã‚„ã‚“" - blocks: - image: "ç”»åƒ" - post: "投稿フォーム" - script: - categories: - random: "ã„ã‚“ã˜ã‚ƒã‚“ã»ã„" - list: "リスト" - blocks: - _join: - arg1: "リスト" - random: "ã„ã‚“ã˜ã‚ƒã‚“ã»ã„" - _randomPick: - arg1: "リスト" - _dailyRandomPick: - arg1: "リスト" - _seedRandomPick: - arg2: "リスト" - _pick: - arg1: "リスト" - _listLen: - arg1: "リスト" - types: - array: "リスト" -room: - translate: "移動" - save: "ä¿å˜" - saved: "ä¿å˜ã—ãŸã§ï¼" - furnitures: - moon: "月" - bin: "ゴミ箱" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml deleted file mode 100644 index bfbe224075bdfad7c097826ee8027c1f7cb90314..0000000000000000000000000000000000000000 --- a/locales/ko-KR.yml +++ /dev/null @@ -1,2175 +0,0 @@ ---- -meta: - lang: "í•œêµì–´" -common: - misskey: "ì—°í•©ìš°ì£¼ì˜ â" - about-title: "ì—°í•©ìš°ì£¼ì˜ â." - about: "Misskey를 발견해주셔서 ê°ì‚¬í•©ë‹ˆë‹¤! Misskey는 지구ì—ì„œ 태어난 <b>분산 마ì´í¬ë¡œ 블로그 SNS</b> 입니다. Fediverse (다양한 SNSê°€ 함께하는 우주) ì†ì— ì¡´ìž¬í•˜ê³ ìžˆì–´ì„œ, 다른 SNS와 서로 ì—°ê²°ë˜ì–´ 있습니다. 번잡한 ë„ì‹œì—ì„œ 벗어나 새로운 ì¸í„°ë„·ì— ë¹ ì ¸ë³´ì§€ ì•Šìœ¼ì‹œê² ì–´ìš”?" - intro: - title: "Misskey란?" - about: "Misskey는 오픈소스 <b>분산형 마ì´í¬ë¡œë¸”로그 SNS</b>입니다. ë‹¤ì–‘í•˜ê³ í넓게 커스터마ì´ì§•í• 수 있는 UI, ê¸€ì— ëŒ€í•œ 리액션, 파ì¼ì„ ê´€ë¦¬í• ìˆ˜ 있는 ë“œë¼ì´ë¸Œ ë“±ì˜ ì„ ì§„ì ì¸ ê¸°ëŠ¥ì„ ê°–ì¶”ê³ ìžˆìŠµë‹ˆë‹¤. ë”하여 Fediverseë¼ê³ 부르는 네트워í¬ì— ì—°ê²°í• ìˆ˜ 있어 다른 SNSì™€ë„ ì£¼ê³ ë°›ì„ ìˆ˜ 있습니다. 예를 들ìžë©´, ë‹¹ì‹ ì´ ë¬´ì–¸ê°€ë¥¼ 게시하면, 해당 ê²Œì‹œë¬¼ì€ Misskey ë¿ë§Œ ì•„ë‹ˆë¼ ë‹¤ë¥¸ SNSì—ë„ ì „í•´ì§‘ë‹ˆë‹¤. ì‚´ì§ ì–´ë–¤ 행성ì—ì„œ 다른 행성으로 ì „íŒŒë¥¼ ë°œì‹ í•˜ê³ ìžˆëŠ” ëª¨ìŠµì„ ìƒìƒí•´ì£¼ì„¸ìš”." - features: "특징" - rich-contents: "글" - rich-contents-desc: "ìžì‹ ì˜ ìƒê°, í™”ì œì˜ ì‚¬ê±´, 모ë‘와 ê³µìœ í•˜ê³ ì‹¶ì€ ê²ƒì„ ì˜¬ë ¤ì£¼ì„¸ìš”. 필요한 경우 다양한 스타ì¼ì„ 사용하여 ê¸€ì„ ìž¥ì‹í•˜ê±°ë‚˜ 마ìŒì— 드는 ì´ë¯¸ì§€, ì˜ìƒ ë“±ì˜ íŒŒì¼ì´ë‚˜ 투표를 올리는 ê²ƒë„ ê°€ëŠ¥í•©ë‹ˆë‹¤." - reaction: "리액션" - reaction-desc: "ë‹¹ì‹ ì˜ ê°ì •ì„ ì „í•˜ëŠ” 가장 쉬운 방법입니다. Misskey는 다른 사용ìžì˜ ê¸€ì— ë‹¤ì–‘í•œ ë¦¬ì•¡ì…˜ì„ ë¶™ì´ëŠ” ê²ƒì´ ê°€ëŠ¥í•©ë‹ˆë‹¤. í•œ 번 Misskeyì˜ ë¦¬ì•¡ì…˜ ê¸°ëŠ¥ì„ ê²½í—˜í•´ë²„ë¦¬ë©´, ë”는 \"좋아요\" ë°–ì— ê°ì •í‘œí˜„ì´ ì—†ëŠ” SNS로는 ëŒì•„ê°ˆ 수 없게 ë˜ì–´ë²„립니다." - ui: "ì¸í„°íŽ˜ì´ìŠ¤" - ui-desc: "ì–´ë–¤ í˜•íƒœì˜ UIê°€ 편한가는 사람마다 다 다릅니다. 그래서 Misskey는 ìžìœ ë„ê°€ ë†’ì€ UI를 ë§Œë“¤ê³ ìžˆìŠµë‹ˆë‹¤. ë ˆì´ì•„웃ì´ë‚˜ ë””ìžì¸ì„ 변경하거나, 커스터마ì´ì§• 가능한 다양한 ìœ„ì ¯ì„ ë°°ì¹˜í•˜ê±°ë‚˜ 하여 ìžì‹ ë§Œì˜ í™ˆì„ ë§Œë“¤ì–´ì£¼ì„¸ìš”." - drive: "ë“œë¼ì´ë¸Œ" - drive-desc: "ì´ì „ì— ì˜¬ë ¸ë˜ ì 있는 ì´ë¯¸ì§€ë¥¼ 다시 ì˜¬ë¦¬ê³ ì‹¶ì„ ë•Œê°€ 있지 ì•Šìœ¼ì‹ ê°€ìš”? 아니면 ì—…ë¡œë“œí–ˆë˜ íŒŒì¼ì„ í´ë”ë¡œ ì •ë¦¬í•˜ê³ ì‹¶ì§€ ì•Šìœ¼ì‹ ê°€ìš”? Misskeyì— ê¸°ë³¸ì 으로 ë‚´ìž¥ëœ ë“œë¼ì´ë¸Œ 기능으로 í•´ê²°ë©ë‹ˆë‹¤. íŒŒì¼ ê³µìœ ë„ ì‰½ìŠµë‹ˆë‹¤." - outro: "ì´ì™¸ì—ë„ Misskeyì—만 있는 ê¸°ëŠ¥ì´ ì•„ì§ë„ ë” ìžˆìœ¼ë‹ˆ 부디 여러분 ìžì‹ ì˜ ëˆˆìœ¼ë¡œ 확ì¸í•´ë³´ì‹œê¸° ë°”ëžë‹ˆë‹¤. Misskey는 분산형 SNSë¼ì„œ ì´ ì¸ìŠ¤í„´ìŠ¤ê°€ 마ìŒì— 들지 ì•Šìœ¼ì‹ ë‹¤ë©´ 다른 ì¸ìŠ¤í„´ìŠ¤ë¥¼ ì‹œë„해보실 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. 그럼, GLHF!" - application-authorization: "앱 연계" - close: "닫기" - do-not-copy-paste: "ì—¬ê¸°ì— ì½”ë“œë¥¼ ìž…ë ¥í•˜ê±°ë‚˜ 붙여넣지 마ì‹ì‹œì˜¤. ê³„ì •ì´ ë¬´ë‹¨ìœ¼ë¡œ 사용ë 수 있습니다." - load-more: "ë”보기" - enter-password: "비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - 2fa: "2단계 ì¸ì¦" - customize-home: "홈 커스터마ì´ì§•" - featured-notes: "하ì´ë¼ì´íŠ¸" - dark-mode: "ë‹¤í¬ ëª¨ë“œ" - signin: "로그ì¸" - signup: "ì‹ ê·œ 등ë¡" - signout: "로그아웃" - reload-to-apply-the-setting: "ì´ ì„¤ì •ì„ ì ìš©í•˜ë ¤ë©´ 페ì´ì§€ë¥¼ ìƒˆë¡œê³ ì¹¨í•´ì•¼ 합니다. 바로 ìƒˆë¡œê³ ì¹¨í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - fetching-as-ap-object: "ì—°í•©ì—ì„œ 조회 중" - unfollow-confirm: "{name} ë‹˜ì„ íŒ”ë¡œìš° í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - delete-confirm: "ì´ ê¸€ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - signin-required: "ë¡œê·¸ì¸ í•´ì£¼ì„¸ìš”" - notification-type: "ì•Œë¦¼ì˜ ì¢…ë¥˜" - notification-types: - all: "모ë‘" - pollVote: "투표" - follow: "팔로잉" - receiveFollowRequest: "팔로우 ìš”ì²" - reply: "답글 달기" - quote: "ì¸ìš©" - renote: "리노트" - mention: "멘션" - reaction: "리액션" - got-it: "ì•Œê² ìŠµë‹ˆë‹¤" - customization-tips: - title: "커스터마ì´ì§• ë„움ë§" - paragraph: "<p>홈 화면 커스터마ì´ì§•ì—서는 ìœ„ì ¯ì„ ì¶”ê°€/ì‚ì œí•˜ê±°ë‚˜ 드래그 & ë“œë¡í•˜ì—¬ ì •ë ¬í•˜ê±°ë‚˜ í• ìˆ˜ 있습니다.</p><p>ì¼ë¶€ ìœ„ì ¯ì€<strong><strong>오른쪽</strong>í´ë¦</strong>ì„ í•˜ì—¬ 보기를 ë³€ê²½í• ìˆ˜ 있습니다.</p><p>ìœ„ì ¯ì„ ì‚ì œí•˜ë ¤ë©´ í—¤ë”ì˜ <strong>\"휴지통\"</strong>ì´ë¼ê³ ì 혀있는 ì˜ì—ì— ì°½ì„ ëŒì–´ë„£ìŠµë‹ˆë‹¤.</p><p>커스터마ì´ì§•ì„ ì¢…ë£Œí•˜ë ¤ë©´ 오른쪽 ìƒë‹¨ì˜ \"완료\" ë²„íŠ¼ì„ í´ë¦í•´ì£¼ì„¸ìš”.</p>" - gotit: "Got it!" - notification: - file-uploaded: "파ì¼ì´ 업로드ë˜ì—ˆìŠµë‹ˆë‹¤" - message-from: "{}ë‹˜ì˜ ë©”ì‹œì§€:" - reversi-invited: "게임 초대가 있습니다" - reversi-invited-by: "{}님으로부터" - notified-by: "{}님으로부터" - reply-from: "{}님으로부터 답글:" - quoted-by: "{}ë‹˜ì´ ì¸ìš©:" - time: - unknown: "ì•Œ 수 없는 시간" - future: "미래" - just_now: "방금 ì „" - seconds_ago: "{}ì´ˆ ì „" - minutes_ago: "{}분 ì „" - hours_ago: "{}시간 ì „" - days_ago: "{}ì¼ ì „" - weeks_ago: "{}주 ì „" - months_ago: "{}개월 ì „" - years_ago: "{}ë…„ ì „" - month-and-day: "{month}ì›” {day}ì¼" - trash: "휴지통" - drive: "ë“œë¼ì´ë¸Œ" - pages: "페ì´ì§€" - messaging: "대화" - home: "홈" - deck: "ë±" - timeline: "타임ë¼ì¸" - explore: "발견하기" - following: "팔로우 중" - followers: "팔로워" - favorites: "ì¦ê²¨ì°¾ê¸°" - permissions: - "read:account": "ê³„ì • ì •ë³´ 보기" - "write:account": "ê³„ì • ì •ë³´ 변경" - "read:blocks": "차단 보기" - "write:blocks": "차단 ìˆ˜ì •" - "read:drive": "ë“œë¼ì´ë¸Œ 보기" - "write:drive": "ë“œë¼ì´ë¸Œ ìˆ˜ì •" - "read:favorites": "ì¦ê²¨ì°¾ê¸° 보기" - "write:favorites": "ì¦ê²¨ì°¾ê¸° ìˆ˜ì •" - "read:following": "팔로우 ì •ë³´ 보기" - "write:following": "팔로잉, 팔로우 ìˆ˜ì •" - "read:messaging": "대화 보기" - "write:messaging": "대화 ìˆ˜ì •" - "read:mutes": "뮤트 보기" - "write:mutes": "뮤트 ìˆ˜ì •" - "write:notes": "글 작성, ì‚ì œ" - "read:notifications": "알림 보기" - "write:notifications": "알림 ìˆ˜ì •" - "read:reactions": "리액션 보기" - "write:reactions": "리액션 ìˆ˜ì •" - "write:votes": "투표하기" - "read:pages": "페ì´ì§€ 보기" - "write:pages": "페ì´ì§€ 변경" - "read:page-likes": "페ì´ì§€ì˜ 좋아요 보기" - "write:page-likes": "페ì´ì§€ì˜ 좋아요 변경" - "read:user-groups": "ìœ ì € 그룹 보기" - "write:user-groups": "ìœ ì € ê·¸ë£¹ì„ ë³€ê²½" - empty-timeline-info: - follow-users-to-make-your-timeline: "사용ìžë¥¼ 팔로우하면 ê¸€ì´ íƒ€ìž„ë¼ì¸ì— 표시ë©ë‹ˆë‹¤." - explore: "ì‚¬ìš©ìž íƒìƒ‰" - post-form: - attach-location-information: "위치 ì •ë³´ë¥¼ 첨부합니다" - hide-contents: "ë‚´ìš© 숨기기" - reply-placeholder: "ì´ ê¸€ì— ë‹µê¸€..." - quote-placeholder: "ì´ ê¸€ì„ ì¸ìš©..." - option-quote-placeholder: "ì´ ê¸€ì„ ì¸ìš©... (옵션)" - quote-attached: "ì¸ìš©í•¨" - quote-question: "ì¸ìš©í•´ì„œ ìž‘ì„±í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - submit: "글쓰기" - reply: "답글 달기" - renote: "리노트" - posting: "게시중" - attach-media-from-local: "PCì—ì„œ 미디어 첨부" - attach-media-from-drive: "ë“œë¼ì´ë¸Œì—ì„œ 미디어 첨부" - insert-a-kao: "v('ω')v" - create-poll: "투표 만들기" - text-remain: "{}ë¬¸ìž ë‚¨ìŒ" - recent-tags: "최근" - local-only-message: "ì´ ê¸€ì€ ë¡œì»¬ì—만 공개ë˜ì–´ 있습니다" - click-to-tagging: "í´ë¦í•˜ì—¬ 태그 넣기" - visibility: "공개 범위" - geolocation-alert: "사용 중ì´ì‹ 장치ì—서는 위치 ì •ë³´ë¥¼ ì‚¬ìš©í• ìˆ˜ 없습니다" - error: "오류" - enter-username: "사용ìžëª…ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”" - specified-recipient: "ìˆ˜ì‹ ì¸" - add-visible-user: "ì‚¬ìš©ìž ì¶”ê°€" - cw-placeholder: "ë‚´ìš©ì— ëŒ€í•œ ì£¼ì„ (옵션)" - username-prompt: "사용ìžëª…ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”" - enter-file-name: "íŒŒì¼ ì´ë¦„ ìˆ˜ì •" - weekday-short: - sunday: "ì¼" - monday: "ì›”" - tuesday: "í™”" - wednesday: "수" - thursday: "목" - friday: "금" - saturday: "í† " - weekday: - sunday: "ì¼ìš”ì¼" - monday: "월요ì¼" - tuesday: "화요ì¼" - wednesday: "수요ì¼" - thursday: "목요ì¼" - friday: "금요ì¼" - saturday: "í† ìš”ì¼" - reactions: - like: "좋아요" - love: "ì£ ì•„" - laugh: "ã…‹ã…‹ã…‹ã…‹" - hmm: "으ìŒ...?" - surprise: "와우" - congrats: "축하합니다" - angry: "화남" - confused: "곤-란" - rip: "RIP" - pudding: "Pudding" - note-visibility: - public: "공개" - home: "홈" - home-desc: "홈 타임ë¼ì¸ì—만 공개" - followers: "팔로워" - followers-desc: "ìžì‹ ì˜ íŒ”ë¡œì›Œì—게만 공개" - specified: "다ì´ë ‰íŠ¸" - specified-desc: "ì§€ì •í•œ 사용ìžì—게만 공개" - local-public: "공개 (로컬 í•œì •)" - local-home: "홈 (로컬 í•œì •)" - local-followers: "팔로워 (로컬 í•œì •)" - note-placeholders: - a: "지금 ë¬´ì—‡ì„ í•˜ê³ ìžˆë‚˜ìš”?" - b: "무슨 ì¼ì´ ì¼ì–´ë‚˜ê³ 있나요?" - c: "ë¬´ì—‡ì„ ìƒê°í•˜ê³ 있나요?" - d: "ë§í•˜ê³ ì‹¶ì€ ê²Œ 있나요?" - e: "ì—¬ê¸°ì— ì 어주세요" - f: "작성해주시길 ê¸°ë‹¤ë¦¬ê³ ìžˆì–´ìš”..." - settings: "ì„¤ì •" - _settings: - profile: "프로필" - notification: "알림" - apps: "앱" - tags: "해시태그" - mute-and-block: "뮤트/차단" - blocking: "차단" - security: "보안" - signin: "ë¡œê·¸ì¸ ê¸°ë¡" - password: "비밀번호" - other: "기타" - appearance: "ë””ìžì¸" - behavior: "ë™ìž‘" - reactions: "리액션" - reactions-description: "리액션 ì„ íƒì°½ì— í‘œì‹œí• ë¦¬ì•¡ì…˜ì„ ì¤„ë°”ê¿ˆìœ¼ë¡œ 구분해 ì„¤ì •í•©ë‹ˆë‹¤." - fetch-on-scroll: "스í¬ë¡¤í•˜ì—¬ ìžë™ìœ¼ë¡œ 불러오기" - fetch-on-scroll-desc: "페ì´ì§€ë¥¼ 아래로 스í¬ë¡¤í•˜ì˜€ì„ ë•Œ ìžë™ìœ¼ë¡œ 추가 콘í…ì¸ ë¥¼ 불러옵니다." - note-visibility: "ê²Œì‹œë¬¼ì˜ ê³µê°œ 범위" - default-note-visibility: "기본 공개 범위" - remember-note-visibility: "ê¸€ì˜ ê³µê°œ 범위를 기억하기" - web-search-engine: "웹 검색엔진" - web-search-engine-desc: "예: https://www.google.com/?#q={{query}}" - paste: "붙여넣기" - pasted-file-name: "ë¶™ì—¬ë„£ì€ íŒŒì¼ì˜ ì´ë¦„ 템플릿" - pasted-file-name-desc: "예시: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" - paste-dialog: "붙여넣기 ì‹œ íŒŒì¼ ì´ë¦„ì„ íŽ¸ì§‘" - paste-dialog-desc: "붙여넣기 ì‹œ íŒŒì¼ ì´ë¦„ì„ íŽ¸ì§‘í• ìˆ˜ 있ë„ë¡ ëŒ€í™” ìƒìžë¥¼ 표시합니다." - keep-cw: "CW ìœ ì§€" - keep-cw-desc: "ê¸€ì— ë‹µê¸€ì„ ë‹¬ ë•Œ, ë‹µê¸€í• ê¸€ì— CWê°€ ì„¤ì •ë˜ì–´ 있는 경우 기본값으로 ë™ì¼í•œ CW를 ì„¤ì •í•˜ë„ë¡ í•©ë‹ˆë‹¤." - i-like-sushi: "ì €ëŠ” (푸딩보다 ì°¨ë¼ë¦¬) ì´ˆë°¥ì´ ì¢‹ì•„ìš”" - show-reversi-board-labels: "리버시 ë³´ë“œì˜ í–‰ê³¼ ì—´ ë ˆì´ë¸”ì„ í‘œì‹œ" - use-avatar-reversi-stones: "ë¦¬ë²„ì‹œì˜ ëŒë¡œ 아바타를 사용" - disable-animated-mfm: "ê¸€ì˜ ë¬¸ìž ì• ë‹ˆë©”ì´ì…˜ì„ 비활성화" - disable-showing-animated-images: "움ì§ì´ëŠ” ì´ë¯¸ì§€ë¥¼ ìžë™ìœ¼ë¡œ 재ìƒí•˜ì§€ ì•ŠìŒ" - enable-quick-notification-view: "ì•Œë¦¼ì˜ ë¹ ë¥¸ 보기를 사용합니다" - suggest-recent-hashtags: "최근 해시태그를 글 ìž‘ì„±ëž€ì— í‘œì‹œ" - always-show-nsfw: "í•ìƒ ì—´ëžŒì£¼ì˜ ë¯¸ë””ì–´ë¥¼ 표시" - always-mark-nsfw: "í•ìƒ 미디어를 열람주ì˜ë¡œ ì„¤ì •í•˜ì—¬ 게시" - show-full-acct: "사용ìžëª…ì˜ í˜¸ìŠ¤íŠ¸ë¥¼ 표시하지 않기" - show-via: "via 표시하기" - reduce-motion: "UIì˜ ì• ë‹ˆë©”ì´ì…˜ 줄ì´ê¸°" - this-setting-is-this-device-only: "ì´ ìž¥ì¹˜ë§Œ" - use-os-default-emojis: "ìš´ì˜ì²´ì œì˜ 기본 ì´ëª¨ì§€ 사용" - line-width: "ì„ ë‘께" - line-width-thin: "ì–‡ìŒ" - line-width-normal: "보통" - line-width-thick: "ë‘꺼움" - font-size: "글씨 í¬ê¸°" - font-size-x-small: "ìž‘ìŒ" - font-size-small: "조금 ìž‘ìŒ" - font-size-medium: "보통" - font-size-large: "조금 í¼" - font-size-x-large: "í¼" - deck-column-align: "ë±ì˜ 칼럼 위치" - deck-column-align-center: "가운ë°" - deck-column-align-left: "왼쪽" - deck-column-align-flexible: "í”Œë ‰ì„œë¸”" - deck-column-width: "ë±ì˜ 칼럼 í" - deck-column-width-narrow: "ì¢ìŒ" - deck-column-width-narrower: "조금 ì¢ìŒ" - deck-column-width-normal: "보통" - deck-column-width-wider: "조금 ë„“ìŒ" - deck-column-width-wide: "ë„“ìŒ" - use-shadow: "UIì— ê·¸ë¦¼ìž íš¨ê³¼ ì ìš©" - rounded-corners: "UIì˜ ëª¨ì„œë¦¬ë¥¼ 둥글게 ì„¤ì •" - circle-icons: "ì›í˜• 아바타를 사용" - contrasted-acct: "사용ìžëª…ì— ëŒ€ë¹„ 추가" - wallpaper: "ë°°ê²½" - choose-wallpaper: "ë°°ê²½ ì„¤ì •" - delete-wallpaper: "ë°°ê²½ ì œê±°" - post-form-on-timeline: "타임ë¼ì¸ ìƒë‹¨ì— 글 ìž‘ì„±ëž€ì„ í‘œì‹œ" - show-clock-on-header: "오른쪽 ìƒë‹¨ì— 시계 표시" - show-reply-target: "답글 ëŒ€ìƒ í‘œì‹œ" - timeline: "타임ë¼ì¸" - show-my-renotes: "ë‚´ 리노트를 타임ë¼ì¸ì— ë³´ì´ê¸°" - show-renoted-my-notes: "ë‚´ ê¸€ì´ ë¦¬ë…¸íŠ¸ë 경우 타임ë¼ì¸ì— ë³´ì´ê¸°" - show-local-renotes: "로컬 ê¸€ì˜ ë¦¬ë…¸íŠ¸ë¥¼ 타임ë¼ì¸ì— ë³´ì´ê¸°" - remain-deleted-note: "ì‚ì œëœ ê¸€ì„ ê³„ì† í‘œì‹œ" - sound: "소리" - enable-sounds: "소리 사용" - enable-sounds-desc: "글ì´ë‚˜ 메시지를 ì†¡ìˆ˜ì‹ í•˜ì˜€ì„ ë•Œ 소리를 재ìƒí•©ë‹ˆë‹¤. ì´ ì„¤ì •ì€ ë¸Œë¼ìš°ì €ì— ì €ìž¥ë©ë‹ˆë‹¤." - volume: "ìŒëŸ‰" - test: "테스트" - update: "Misskey Update" - version: "ë²„ì „:" - latest-version: "ìµœì‹ ë²„ì „:" - update-checking: "ì—…ë°ì´íŠ¸ í™•ì¸ ì¤‘" - do-update: "ì—…ë°ì´íŠ¸ 확ì¸" - update-settings: "ê³ ê¸‰ ì„¤ì •" - no-updates: "사용 가능한 ì—…ë°ì´íŠ¸ê°€ 없습니다" - no-updates-desc: "ì‚¬ìš©ì¤‘ì¸ Misskey는 ìµœì‹ ë²„ì „ìž…ë‹ˆë‹¤." - update-available: "새 ë²„ì „ì„ ì‚¬ìš©í• ìˆ˜ 있습니다" - update-available-desc: "페ì´ì§€ë¥¼ 다시 로드하면 ì—…ë°ì´íŠ¸ê°€ ì ìš©ë©ë‹ˆë‹¤." - advanced-settings: "ê³ ê¸‰ ì„¤ì •" - debug-mode: "디버그 모드를 사용하ë„ë¡ ì„¤ì •" - debug-mode-desc: "ì´ ì„¤ì •ì€ ë¸Œë¼ìš°ì €ì— ì €ìž¥ë©ë‹ˆë‹¤." - navbar-position: "내비게ì´ì…˜ 막대 위치" - navbar-position-top: "위" - navbar-position-left: "왼쪽" - navbar-position-right: "오른쪽" - i-am-under-limited-internet: "ì €ëŠ” í†µì‹ ëŒ€ì—íì´ ì œí•œë˜ì–´ 있습니다" - post-style: "글 표시 스타ì¼" - post-style-standard: "표준" - post-style-smart: "스마트" - notification-position: "알림 표시" - notification-position-bottom: "아래" - notification-position-top: "위" - disable-via-mobile: "작성하는 ê¸€ì— \"모바ì¼ì—ì„œ 작성함\" ì„ ë¶™ì´ì§€ ì•ŠìŒ" - load-raw-images: "첨부 ì´ë¯¸ì§€ë¥¼ ê³ í’ˆì§ˆë¡œ 표시" - load-remote-media: "ì›ê²© ì„œë²„ì˜ ë¯¸ë””ì–´ë¥¼ 표시" - sync: "ë™ê¸°í™”" - save: "ì €ìž¥" - saved: "ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" - preview: "미리보기" - home-profile: "홈 프로필" - deck-profile: "ë± í”„ë¡œí•„" - room: "룸" - _room: - graphicsQuality: "그래픽 품질" - _graphicsQuality: - ultra: "ìµœê³ " - high: "높ìŒ" - medium: "보통" - low: "ë‚®ìŒ" - cheep: "ìµœì €" - useOrthographicCamera: "í‰í–‰ 투시 ì¹´ë©”ë¼ë¥¼ 사용" - search: "검색" - delete: "ì‚ì œ" - loading: "로드 중" - ok: "ㅇㅇ" - cancel: "그만ë‘기" - update-available-title: "ì—…ë°ì´íŠ¸ê°€ 있습니다" - update-available: "Misskeyì˜ ìƒˆë¡œìš´ ë²„ì „ì´ ìžˆìŠµë‹ˆë‹¤ ({newer}. 현재 {current}ì„ ì‚¬ìš© 중). 페ì´ì§€ë¥¼ 다시 로드하면 ì—…ë°ì´íŠ¸ê°€ ì ìš©ë©ë‹ˆë‹¤." - my-token-regenerated: "ë‹¹ì‹ ì˜ í† í°ì´ ì—…ë°ì´íŠ¸ë˜ì—ˆìœ¼ë¯€ë¡œ 로그아웃합니다." - hide-password: "비밀번호 숨기기" - show-password: "비밀번호 표시" - enter-username: "사용ìžëª…ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”" - do-not-use-in-production: "ì´ê²ƒì€ 개발 빌드입니다. 프로ë•ì…˜ 환경ì—ì„œ 사용하지 마ì‹ì‹œì˜¤." - user-suspended: "ì´ ì‚¬ìš©ìžëŠ” ì •ì§€ëœ ìƒíƒœìž…니다." - is-remote-user: "ì´ ì‚¬ìš©ìž ì •ë³´ëŠ” ì •í™•í•˜ì§€ ì•Šì„ ìˆ˜ 있습니다." - is-remote-post: "ì´ ê¸€ ì •ë³´ëŠ” 복사본입니다." - view-on-remote: "ì •í™•í•œ ì •ë³´ 보기" - renoted-by: "{user} ë‹˜ì´ ë¦¬ë…¸íŠ¸" - no-notes: "ê¸€ì´ ì—†ìŠµë‹ˆë‹¤" - turn-on-darkmode: "ì–´ë‘ ì— ì‚¼ì¼œì ¸ë¼" - turn-off-darkmode: "ë¹›ì´ ìžˆìœ¼ë¼" - error: - title: "오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤" - retry: "다시 ì‹œë„" - reversi: - drawn: "무승부" - my-turn: "ë‹¹ì‹ ì˜ ì°¨ë¡€ìž…ë‹ˆë‹¤" - opponent-turn: "ìƒëŒ€ì˜ 차례입니다" - turn-of: "{name}ì˜ ì°¨ë¡€ìž…ë‹ˆë‹¤" - past-turn-of: "{name}ì˜ ì°¨ë¡€" - won: "{name}ì˜ ìŠ¹ë¦¬" - black: "í‘" - white: "ë°±" - total: "합계" - this-turn: "{count}í„´ 째" - widgets: - analog-clock: "ì•„ë‚ ë¡œê·¸ 시계" - profile: "프로필" - calendar: "ë‹¬ë ¥" - timemachine: "ë‹¬ë ¥(íƒ€ìž„ë¨¸ì‹ )" - activity: "활ë™" - rss: "RSS 리ë”" - memo: "스티커 메모" - trends: "íŠ¸ë Œë“œ" - photo-stream: "í¬í† 스트림" - posts-monitor: "글 차트" - slideshow: "슬ë¼ì´ë“œ 쇼" - version: "ë²„ì „" - broadcast: "브로드ìºìŠ¤íŠ¸" - notifications: "알림" - users: "추천 사용ìž" - polls: "투표" - post-form: "글 ìž…ë ¥ëž€" - server: "서버 ì •ë³´" - nav: "내비게ì´ì…˜" - tips: "íŒ" - hashtags: "해시태그" - queue: "í" - dev: "ì•±ì„ ë§Œë“œëŠ” ë° ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. 다시 ì‹œë„하시기 ë°”ëžë‹ˆë‹¤." - ai-chan-kawaii: "ì•„ì´ì¨© 귀여워" - you: "ë‹¹ì‹ " -auth/views/form.vue: - share-access: "<i>{name}</i>ê°€ ë‹¹ì‹ ì˜ ê³„ì •ì— ì—‘ì„¸ìŠ¤í•˜ë„ë¡ í—ˆìš©í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - permission-ask: "ì´ ì•±ì€ ë‹¤ìŒì˜ ê¶Œí•œì„ ìš”ì²í•©ë‹ˆë‹¤:" - cancel: "취소" - accept: "ì ‘ê·¼ 권한 허용" -auth/views/index.vue: - loading: "로드 중" - denied: "ì• í”Œë¦¬ì¼€ì´ì…˜ì˜ 연계를 취소하였습니다." - denied-paragraph: "ì´ ì•±ì´ ë‹¹ì‹ ì˜ ê³„ì •ì— ì•¡ì„¸ìŠ¤í• ìˆ˜ 없습니다." - already-authorized: "ì´ ì•±ì€ ì´ë¯¸ ì—°ê²°ë˜ì–´ 있습니다." - allowed: "ì• í”Œë¦¬ì¼€ì´ì…˜ì˜ ì—°ë™ì„ 허용하였습니다." - callback-url: "ì• í”Œë¦¬ì¼€ì´ì…˜ìœ¼ë¡œ ëŒì•„갑니다." - please-go-back: "ì• í”Œë¦¬ì¼€ì´ì…˜ìœ¼ë¡œ ëŒì•„가여 ì‹œë„하여 주ì‹ì‹œì˜¤." - error: "ì„¸ì…˜ì´ ì¡´ìž¬í•˜ì§€ 않습니다." - sign-in: "ë¡œê·¸ì¸ í•´ì£¼ì‹œê¸° ë°”ëžë‹ˆë‹¤" -common/views/pages/explore.vue: - pinned-users: "ê³ ì •ëœ ì‚¬ìš©ìž" - popular-users: "ì¸ê¸° 사용ìž" - recently-updated-users: "최근 게시한 사용ìž" - recently-registered-users: "ì‹ ê·œ 사용ìž" - recently-discovered-users: "최근 ë°œê²¬ëœ ìœ ì €" - popular-tags: "ì¸ê¸° 태그" - federated: "ì—°í•©" - explore: "{host}ì„(를) íƒìƒ‰" - explore-fediverse: "ì—°í•© 우주를 íƒìƒ‰" - users-info: "현재 {users} 사용ìžê°€ 등ë¡ë˜ì–´ 있습니다" -common/views/components/reactions-viewer.details.vue: - few-users: "{users}ë‹˜ì´ {reaction} 리액션" - many-users: "{users}님 외 {omitted}ëª…ì´ {reaction} 리액션" -common/views/components/url-preview.vue: - enable-player: "í”Œë ˆì´ì–´ 열기" - disable-player: "í”Œë ˆì´ì–´ 닫기" -common/views/components/user-list.vue: - no-users: "사용ìžê°€ 없습니다" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "{}ì„(를) ê¸°ë‹¤ë¦¬ê³ ìžˆìŠµë‹ˆë‹¤" - cancel: "취소" -common/views/components/games/reversi/reversi.game.vue: - surrender: "기권" - surrendered: "ê¸°ê¶Œì— ì˜í•´" - is-llotheo: "ëŒì´ ì ì€ ìª½ì´ ìŠ¹ë¦¬ (llotheo)" - looped-map: "루프 지ë„" - can-put-everywhere: "ì–´ë””ì—ë„ ë‘˜ 수 있는 모드" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "다른 Misskey 사용ìžì™€ 리버시로 대결하ìž" - invite: "초대" - rule: "게임 방법" - rule-desc: "리버시는 ìƒëŒ€ì™€ 번갈아가며 ëŒì„ íŒì— ë‘ê³ , ìƒëŒ€ì˜ ëŒì„ ìžì‹ ì˜ ëŒ ì‚¬ì´ì— ë‘ì–´ ìžì‹ ì˜ ìƒ‰ìœ¼ë¡œ 바꿔나가며, 최종ì 으로 남아있는 ëŒì´ ë§Žì€ ìª½ì´ ìŠ¹ë¦¬í•˜ëŠ” 보드게임입니다." - mode-invite: "초대" - mode-invite-desc: "ì§€ì •í•œ 사용ìžì™€ 대결하는 모드입니다." - invitations: "게임 초대가 있습니다!" - my-games: "ë‚´ 대êµ" - all-games: "모ë‘ì˜ ëŒ€êµ" - enter-username: "사용ìžëª…ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”" - game-state: - ended: "종료" - playing: "진행중" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "게임 ì„¤ì •" - choose-map: "맵 ì„ íƒ" - random: "ëžœë¤" - black-or-white: "ì„ ìˆ˜/후수" - black-is: "{}ê°€ í‘" - rules: "규칙" - is-llotheo: "ëŒì´ ì ì€ ì‚¬ëžŒì´ ìŠ¹ë¦¬ (llotheo)" - looped-map: "루프 지ë„" - can-put-everywhere: "어디나 ë†“ì„ ìˆ˜ 있ìŒ" - settings-of-the-bot: "Bot ì„¤ì •" - this-game-is-started-soon: "ê²Œìž„ì´ ëª‡ ì´ˆ í›„ì— ì‹œìž‘ë©ë‹ˆë‹¤" - waiting-for-other: "ìƒëŒ€ê°€ 준비가 완료ë 때까지 ê¸°ë‹¤ë¦¬ê³ ìžˆìŠµë‹ˆë‹¤" - waiting-for-me: "ë‹¹ì‹ ì˜ ì¤€ë¹„ 완료를 ê¸°ë‹¤ë¦¬ê³ ìžˆìŠµë‹ˆë‹¤" - waiting-for-both: "준비중" - cancel: "취소" - ready: "준비 완료" - cancel-ready: "준비 취소" -common/views/components/connect-failed.vue: - title: "ì„œë²„ì— ì—°ê²°í• ìˆ˜ 없습니다" - description: "ì¸í„°ë„· íšŒì„ ì— ë¬¸ì œê°€ 있거나, 서버가 다운ë˜ì—ˆê±°ë‚˜ ì ê²€ì¤‘ì¼ ê°€ëŠ¥ì„±ì´ ìžˆìŠµë‹ˆë‹¤. ìž ì‹œí›„ì— {다시 ì‹œë„}í•´ 주ì‹ì‹œì˜¤." - thanks: "í•ìƒ Misskey를 ì´ìš©í•´ì£¼ì…”ì„œ ê°ì‚¬í•©ë‹ˆë‹¤." - troubleshoot: "트러블슈팅" -common/views/components/connect-failed.troubleshooter.vue: - title: "트러블슈팅" - network: "ë„¤íŠ¸ì›Œí¬ ì—°ê²°" - checking-network: "ë„¤íŠ¸ì›Œí¬ ì—°ê²° 확ì¸ì¤‘" - internet: "ì¸í„°ë„· ì—°ê²°" - checking-internet: "ì¸í„°ë„· ì—°ê²°ì„ í™•ì¸ì¤‘" - server: "서버 ì—°ê²°" - checking-server: "서버 ì—°ê²°ì„ í™•ì¸ì¤‘" - finding: "ë¬¸ì œë¥¼ 확ì¸í•˜ê³ 있습니다" - no-network: "네트워í¬ì— ì—°ê²°ë˜ì–´ 있지 않습니다" - no-network-desc: "ì‚¬ìš©ì¤‘ì¸ PCì˜ ë„¤íŠ¸ì›Œí¬ ì—°ê²°ì´ ì •ìƒì ì¸ì§€ 확ì¸í•´ì£¼ì‹ì‹œì˜¤." - no-internet: "ì¸í„°ë„·ì— ì ‘ì†ë˜ì–´ 있지 않습니다" - no-internet-desc: "네트워í¬ì— ì—°ê²°ë˜ì–´ 있으나 ì¸í„°ë„·ì— ì—°ê²°ë˜ì–´ 있지 ì•Šì€ ê²ƒ 같습니다. 사용중ì´ì‹ PCì˜ ì¸í„°ë„· ì—°ê²°ì´ ì •ìƒì ì¸ì§€ 확ì¸í•˜ì—¬ 주ì‹ì‹œì˜¤." - no-server: "Misskey ì„œë²„ì— ì—°ê²°í• ìˆ˜ 없습니다" - no-server-desc: "사용중ì´ì‹ PCì˜ ì¸í„°ë„· ì—°ê²°ì´ ì •ìƒì ì´ë‚˜ Misskeyì˜ ì„œë²„ì— ì—°ê²°í• ìˆ˜ 없었습니다. 서버가 다운ë˜ì—ˆê±°ë‚˜ ì ê²€ì¤‘ì¼ ê°€ëŠ¥ì„±ì´ ìžˆìœ¼ë¯€ë¡œ, ìž ì‹œ í›„ì— ë‹¤ì‹œ ì‹œë„해주ì‹ì‹œì˜¤." - success: "Misskey ì„œë²„ì— ì—°ê²°í•˜ì˜€ìŠµë‹ˆë‹¤" - success-desc: "ì •ìƒì 으로 ì—°ê²° 가능한 것 같습니다. 페ì´ì§€ë¥¼ ìƒˆë¡œê³ ì¹¨í•˜ì—¬ 주ì‹ì‹œì˜¤." - flush: "ìºì‹œ ì‚ì œ" - set-version: "ë²„ì „ ì§€ì •" -common/views/components/media-banner.vue: - sensitive: "열람주ì˜" - click-to-show: "í´ë¦í•˜ì—¬ 표시" -common/views/components/theme.vue: - theme: "테마" - light-theme: "ë‹¤í¬ ëª¨ë“œê°€ ì•„ë‹ ë•Œ 사용하는 테마" - dark-theme: "ë‹¤í¬ ëª¨ë“œì¼ ë•Œ 사용하는 테마" - light-themes: "ë°ì€ 테마" - dark-themes: "ì–´ë‘ìš´ 테마" - install-a-theme: "테마 설치" - theme-code: "테마 코드" - install: "설치" - installed: "\"{}\"를 설치했습니다" - create-a-theme: "테마 만들기" - save-created-theme: "테마 ì €ìž¥" - primary-color: "기본 색" - secondary-color: "ë³´ì¡° 색" - text-color: "ê¸€ìž ìƒ‰ìƒ" - base-theme: "기본 테마" - base-theme-light: "ë°ìŒ" - base-theme-dark: "ì–´ë‘움" - find-more-theme: "ê·¸ 외 테마 찾아보기" - theme-name: "테마명" - preview-created-theme: "미리보기" - invalid-theme: "테마가 올바르지 않습니다." - already-installed: "ì´ë¯¸ 해당 테마가 설치ë˜ì–´ 있습니다." - saved: "ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" - manage-themes: "테마 관리" - builtin-themes: "표준 테마" - my-themes: "ë‚´ 테마" - installed-themes: "ì„¤ì¹˜ëœ í…Œë§ˆ" - select-theme: "테마를 ì„ íƒí•˜ì—¬ 주ì‹ì‹œì˜¤" - uninstall: "ì œê±°" - uninstalled: "\"{}\"ì„ ì œê±°í•˜ì˜€ìŠµë‹ˆë‹¤" - author: "작성ìž" - desc: "설명" - export: "내보내기" - import: "ê°€ì ¸ì˜¤ê¸°" - import-by-code: "ë˜ëŠ” 코드 붙여넣기" - theme-name-required: "í…Œë§ˆëª…ì€ í•„ìˆ˜ í•ëª©ìž…니다." -common/views/components/cw-button.vue: - hide: "숨기기" - show: "ë” ë³´ê¸°" - chars: "{count}문ìž" - files: "{count}파ì¼" - poll: "투표" -common/views/components/messaging.vue: - search-user: "ì‚¬ìš©ìž ì°¾ê¸°" - you: "ë‹¹ì‹ " - no-history: "기ë¡ì´ 없습니다" - user: "사용ìž" - group: "그룹" - start-with-user: "사용ìžì™€ 대화 시작" - start-with-group: "그룹과 대화 시작" - select-group: "ê·¸ë£¹ì„ ì„ íƒí•˜ì—¬ 주ì‹ì‹œì˜¤" -common/views/components/messaging-room.vue: - not-talked-user: "ì´ ì‚¬ìš©ìžì™€ì˜ 대화가 없습니다" - not-talked-group: "ì´ ê·¸ë£¹ê³¼ì˜ ëŒ€í™”ê°€ 없습니다" - no-history: "ì´ê²ƒë³´ë‹¤ ê³¼ê±°ì˜ ê¸°ë¡ì´ 없습니다" - new-message: "새 메시지가 있습니다" - only-one-file-attached: "ë©”ì‹œì§€ì— ì²¨ë¶€í• ìˆ˜ 있는 파ì¼ì€ 하나까지입니다" -common/views/components/messaging-room.form.vue: - input-message-here: "ì—¬ê¸°ì— ë©”ì‹œì§€ë¥¼ ìž…ë ¥í•˜ì„¸ìš”" - send: "ì „ì†¡" - attach-from-local: "PCì—ì„œ íŒŒì¼ ì²¨ë¶€" - attach-from-drive: "ë“œë¼ì´ë¸Œì—ì„œ íŒŒì¼ ì²¨ë¶€" - only-one-file-attached: "ë©”ì‹œì§€ì— ì²¨ë¶€í• ìˆ˜ 있는 파ì¼ì€ 하나까지입니다" -common/views/components/messaging-room.message.vue: - is-read: "ì½ìŒ" - deleted: "ì´ ë©”ì‹œì§€ëŠ” ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" -common/views/components/nav.vue: - about: "Misskeyì— ëŒ€í•˜ì—¬" - stats: "통계" - status: "ìƒíƒœ" - wiki: "위키" - donors: "기ì¦ìž" - repository: "ì €ìž¥ì†Œ" - develop: "개발ìž" - feedback: "피드백" - tos: "ì´ìš© 약관" -common/views/components/note-menu.vue: - mention: "멘션" - detail: "ìƒì„¸" - copy-content: "ë‚´ìš© 복사" - copy-link: "ë§í¬ 복사" - favorite: "ì´ ë…¸íŠ¸ ì¦ê²¨ì°¾ê¸°" - unfavorite: "ì¦ê²¨ì°¾ê¸°ì—ì„œ ì œê±°" - watch: "지켜보기" - unwatch: "지켜보기 í•´ì œ" - pin: "í”„ë¡œí•„ì— ê³ ì •" - unpin: "프로필ì—ì„œ ê³ ì • í•´ì œ" - delete: "ì‚ì œ" - delete-confirm: "ì´ ê¸€ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - delete-and-edit: "ì‚ì œ 후 편집" - delete-and-edit-confirm: "ì´ ê¸€ì„ ì‚ì œí•œ ë’¤ 다시 íŽ¸ì§‘í•˜ì‹œê² ìŠµë‹ˆê¹Œ? ì´ ê¸€ì— ëŒ€í•œ 리액션, 리노트, 답글 ë˜í•œ ëª¨ë‘ ì‚ì œë©ë‹ˆë‹¤." - remote: "글 ì›ë³¸ 보기" - pin-limit-exceeded: "ë” ì´ìƒ ê³ ì •í• ìˆ˜ 없습니다." -common/views/components/user-menu.vue: - mention: "멘션" - mute: "뮤트" - unmute: "뮤트 í•´ì œ" - mute-confirm: "ì´ ì‚¬ìš©ìžë¥¼ ë®¤íŠ¸í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - unmute-confirm: "ì´ ì‚¬ìš©ìžë¥¼ 뮤트 í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - block: "차단" - unblock: "차단 í•´ì œ" - block-confirm: "ì´ ì‚¬ìš©ìžë¥¼ ì°¨ë‹¨í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - unblock-confirm: "ì´ ì‚¬ìš©ìžë¥¼ 차단 í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - push-to-list: "ë¦¬ìŠ¤íŠ¸ì— ì¶”ê°€" - select-list: "리스트를 ì„ íƒí•˜ì—¬ 주ì‹ì‹œì˜¤" - report-abuse: "스팸 ì‹ ê³ " - report-abuse-detail: "ì–´ë–¤ 스팸 행위를 í•˜ê³ ìžˆìŠµë‹ˆê¹Œ?" - report-abuse-reported: "관리ìžì—게 ë³´ê³ ë˜ì—ˆìŠµë‹ˆë‹¤. 협조해주셔서 ê°ì‚¬í•©ë‹ˆë‹¤." - silence: "침묵" - unsilence: "침묵 í•´ì œ" - silence-confirm: "ì´ ì‚¬ìš©ìžë¥¼ ì¹¨ë¬µí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - unsilence-confirm: "ì´ ì‚¬ìš©ìžë¥¼ 침묵 í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - suspend: "ì •ì§€" - unsuspend: "ì •ì§€ í•´ì œ" - suspend-confirm: "ì´ ì‚¬ìš©ìžë¥¼ ì •ì§€í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - unsuspend-confirm: "ì´ ì‚¬ìš©ìžë¥¼ ì •ì§€ í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" -common/views/components/poll.vue: - vote-to: "\"{}\"ì— íˆ¬í‘œí•˜ê¸°" - vote-count: "{}í‘œ" - total-votes: "ì´ {}í‘œ" - vote: "투표하기" - show-result: "ê²°ê³¼ 보기" - voted: "투표함" - closed: "종료ë¨" - remaining-days: "종료까지 앞으로 {d}ì¼ {h}시간" - remaining-hours: "종료까지 앞으로 {h}시간 {m}분" - remaining-minutes: "종료까지 앞으로 {m}분 {s}ì´ˆ" - remaining-seconds: "종료까지 앞으로 {s}ì´ˆ" -common/views/components/poll-editor.vue: - no-only-one-choice: "투표ì—는 ì„ íƒì§€ê°€ 최소한 ë‘ ê°œ 필요합니다" - choice-n: "ì„ íƒì§€ {}" - remove: "ì´ ì„ íƒì§€ë¥¼ ì œê±°" - add: "+ì„ íƒì§€ 추가" - destroy: "투표 ì œê±°" - multiple: "복수 ì‘답 가능" - expiration: "기한" - infinite: "무기한" - at: "ì¼ì‹œ ì§€ì •" - after: "기간 ì§€ì •" - no-more: "ë” ì´ìƒ ì¶”ê°€í• ìˆ˜ 없습니다" - deadline-date: "기한" - deadline-time: "시간" - interval: "기간" - unit: "단위" - second: "ì´ˆ" - minute: "분" - hour: "시간" - day: "ì¼" -common/views/components/reaction-picker.vue: - choose-reaction: "리액션 ì„ íƒ" - input-reaction-placeholder: "ë˜ëŠ” ì´ëª¨ì§€ ìž…ë ¥" -common/views/components/emoji-picker.vue: - recent-emoji: "최근 사용한 ì´ëª¨ì§€" - custom-emoji: "커스텀 ì´ëª¨ì§€" - no-category: "ì¹´í…Œê³ ë¦¬ ì—†ìŒ" - people: "사람들" - animals-and-nature: "ë™ë¬¼ & ìžì—°" - food-and-drink: "ìŒì‹ & ìŒë£Œ" - activity: "활ë™" - travel-and-places: "장소" - objects: "사물" - symbols: "기호" - flags: "깃발" -common/views/components/settings/app-type.vue: - title: "모드" - intro: "ë°ìŠ¤í¬í†±ê³¼ ëª¨ë°”ì¼ ì¤‘ ì–´ë–¤ ë ˆì´ì•„ì›ƒì„ ì‚¬ìš©í• ì§€ ì§€ì •í• ìˆ˜ 있습니다." - choices: - auto: "ìžë™ìœ¼ë¡œ ì„ íƒ" - desktop: "ë°ìŠ¤í¬í†± ë ˆì´ì•„웃으로 ê³ ì •" - mobile: "ëª¨ë°”ì¼ ë ˆì´ì•„웃으로 ê³ ì •" - info: "변경사í•ì€ 페ì´ì§€ë¥¼ ìƒˆë¡œê³ ì¹¨í•œ ë’¤ì— ë°˜ì˜ë©ë‹ˆë‹¤." -common/views/components/signin.vue: - username: "사용ìžëª…" - password: "비밀번호" - token: "í† í°" - signing-in: "ë¡œê·¸ì¸ ì¤‘ìž…ë‹ˆë‹¤..." - or: "ë˜ëŠ”" - signin-with-twitter: "Twitterë¡œ 로그ì¸" - signin-with-github: "GitHub으로 로그ì¸" - signin-with-discord: "Discordë¡œ 로그ì¸" - login-failed: "로그ì¸í• 수 없습니다. 사용ìžëª…ê³¼ 비밀번호를 확ì¸í•˜ì—¬ 주ì‹ì‹œì˜¤." - tap-key: "보안 키를 í´ë¦í•˜ì—¬ 로그ì¸" - enter-2fa-code: "ì¸ì¦ 코드를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" -common/views/components/signup.vue: - invitation-code: "초대 코드" - invitation-info: "초대 코드가 ì—†ìœ¼ì‹ ë¶„ì€ <a href=\"{}\">관리ìž</a>ì—게 ì—°ë½í•˜ì‹œê¸° ë°”ëžë‹ˆë‹¤." - username: "사용ìžëª…" - checking: "확ì¸í•˜ëŠ” 중입니다..." - available: "사용 가능" - unavailable: "ì´ë¯¸ 사용중입니다" - error: "í†µì‹ ì˜¤ë¥˜" - invalid-format: "a~z, A~Z, 0-9, _를 ì‚¬ìš©í• ìˆ˜ 있습니다" - too-short: "1ìž ì´ìƒ 작성해야 합니다!" - too-long: "20ê¸€ìž ì´í•˜ë¡œ 작성하여 주ì‹ì‹œì˜¤" - password: "비밀번호" - password-placeholder: "8ê¸€ìž ì´ìƒì„ 권장합니다" - weak-password: "약한 비밀번호" - normal-password: "ì 당한 비밀번호" - strong-password: "ê°•í•œ 비밀번호" - retype: "다시 ìž…ë ¥" - retype-placeholder: "확ì¸ì„ 위해 다시 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - password-matched: "확ì¸ë˜ì—ˆìŠµë‹ˆë‹¤" - password-not-matched: "ì¼ì¹˜í•˜ì§€ 않습니다" - recaptcha: "ìžë™ 가입 방지" - agree-to: "{0}ì— ë™ì˜í•©ë‹ˆë‹¤." - tos: "ì´ìš© 약관" - create: "ê³„ì • 만들기" - some-error: "ì•Œ 수 없는 ì´ìœ ë¡œ ê³„ì • ë§Œë“¤ê¸°ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. 다시 한번 ì‹œë„í•´ 주세요." -common/views/components/special-message.vue: - new-year: "새해 ë³µ ë§Žì´ ë°›ìœ¼ì„¸ìš”!" - christmas: "메리 í¬ë¦¬ìŠ¤ë§ˆìŠ¤!" -common/views/components/stream-indicator.vue: - connecting: "연결중" - reconnecting: "다시 ì—°ê²° 중" - connected: "ì—°ê²° 완료" -common/views/components/notification-settings.vue: - title: "알림" - mark-as-read-all-notifications: "ëª¨ë“ ì•Œë¦¼ì„ ì½ì€ ìƒíƒœë¡œ 표시" - mark-as-read-all-unread-notes: "ëª¨ë“ ê¸€ì„ ì½ì€ ìƒíƒœë¡œ 표시" - mark-as-read-all-talk-messages: "ëª¨ë“ ëŒ€í™”ë¥¼ ì½ì€ ìƒíƒœë¡œ 표시" - auto-watch: "글 ìžë™ ê°ì‹œ" - auto-watch-desc: "리액션, 답글, ê²Œì‹œë¬¼ì— ëŒ€í•œ ì•Œë¦¼ì„ ìžë™ìœ¼ë¡œ ë°›ì„ ìˆ˜ 있ë„ë¡ í•©ë‹ˆë‹¤." -common/views/components/integration-settings.vue: - title: "서비스 연계" - connect: "ì ‘ì†" - disconnect: "ì—°ê²° ëŠê¸°" - connected-to: "ë‹¤ìŒ ê³„ì •ì— ì—°ê²°ë˜ì–´ 있습니다" -common/views/components/github-setting.vue: - description: "사용중ì´ì‹ Github ê³„ì •ì„ Misskey ê³„ì •ì— ì—°ê²°í•˜ë©´ í”„ë¡œí•„ì— Github ì •ë³´ê°€ 표시ë˜ê³ , Github를 사용하여 편리하게 로그ì¸í• 수 있습니다." - connected-to: "ë‹¤ìŒ Github ê³„ì •ì— ì—°ê²°ë˜ì–´ 있습니다" - detail: "ìžì„¸ížˆ..." - reconnect: "다시 ì—°ê²°" - connect: "GitHub와 ì—°ê²° 하기" - disconnect: "ì—°ê²° ëŠê¸°" -common/views/components/discord-setting.vue: - description: "사용중ì´ì‹ Discord ê³„ì •ì„ Misskey ê³„ì •ì— ì—°ê²°í•˜ë©´ í”„ë¡œí•„ì— Discord ì •ë³´ê°€ 표시ë˜ê³ , Discord를 사용하여 편리하게 로그ì¸í• 수 있습니다." - connected-to: "ë‹¤ìŒ Discord ê³„ì •ì— ì—°ê²°ë˜ì–´ 있습니다" - detail: "ìžì„¸ížˆ..." - reconnect: "다시 ì—°ê²°" - connect: "Discord와 연결하기" - disconnect: "ì—°ê²° ëŠê¸°" -common/views/components/uploader.vue: - waiting: "기다리는 중" -common/views/components/visibility-chooser.vue: - public: "공개" - home: "홈" - home-desc: "홈 타임ë¼ì¸ì—만 공개" - followers: "팔로워" - followers-desc: "ìžì‹ ì˜ íŒ”ë¡œì›Œì—게만 공개" - specified: "다ì´ë ‰íŠ¸" - specified-desc: "ì§€ì •í•œ 사용ìžì—게만 공개" - local-public: "공개 (로컬 í•œì •)" - local-public-desc: "ì›ê²©ì—는 공개하지 ì•ŠìŒ" - local-home: "홈 (로컬 í•œì •)" - local-followers: "팔로워 (로컬 í•œì •)" -common/views/components/trends.vue: - count: "{}ëª…ì´ ì–¸ê¸‰í•¨" - empty: "íŠ¸ë Œë“œ ì—†ìŒ" -common/views/components/language-settings.vue: - title: "표시 언어" - pick-language: "언어 ì„¤ì •" - recommended: "추천" - auto: "ìžë™" - specify-language: "언어 ì§€ì •" - info: "변경사í•ì€ 페ì´ì§€ë¥¼ ìƒˆë¡œê³ ì¹¨í•œ ë’¤ì— ë°˜ì˜ë©ë‹ˆë‹¤." -common/views/components/profile-editor.vue: - title: "프로필" - name: "ì´ë¦„" - account: "ê³„ì •" - location: "장소" - description: "ìžê¸°ì†Œê°œ" - you-can-include-hashtags: "í•´ì‹œ 태그를 í¬í•¨í• 수 있습니다." - language: "언어" - birthday: "ìƒì¼" - avatar: "아바타" - banner: "배너" - is-cat: "ì´ ê³„ì •ì€ Cat입니다" - is-bot: "ì´ ê³„ì •ì€ Bot입니다" - is-locked: "팔로우를 수ë™ìœ¼ë¡œ 승ì¸" - careful-bot: "Botì˜ íŒ”ë¡œìš°ë§Œ 수ë™ìœ¼ë¡œ 승ì¸" - auto-accept-followed: "íŒ”ë¡œìš°ì¤‘ì¸ ì‚¬ìš©ìžë¡œë¶€í„°ì˜ 팔로우를 ìžë™ìœ¼ë¡œ 승ì¸" - advanced: "기타" - privacy: "프ë¼ì´ë²„ì‹œ" - save: "ì €ìž¥" - saved: "í”„ë¡œí•„ì„ ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" - uploading: "업로드 중" - upload-failed: "ì—…ë¡œë“œì— ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤" - unable-to-process: "ìž‘ì—…ì„ ì™„ë£Œí• ìˆ˜ 없습니다" - avatar-not-an-image: "아바타로 ì§€ì •í•œ 파ì¼ì´ ì´ë¯¸ì§€ 형ì‹ì´ 아닙니다" - banner-not-an-image: "배너로 ì§€ì •í•œ 파ì¼ì´ ì´ë¯¸ì§€ 형ì‹ì´ 아닙니다" - email: "ë©”ì¼ ì„¤ì •" - email-address: "ë©”ì¼ ì£¼ì†Œ" - email-verified: "ë§¤ì¼ ì£¼ì†Œê°€ 확ì¸ë˜ì—ˆìŠµë‹ˆë‹¤" - email-not-verified: "ë©”ì¼ ì£¼ì†Œê°€ 확ì¸ë˜ì§€ 않았습니다. ë°›ì€ íŽ¸ì§€í•¨ì„ í™•ì¸í•˜ì—¬ 주시기 ë°”ëžë‹ˆë‹¤." - export: "내보내기" - import: "ê°€ì ¸ì˜¤ê¸°" - export-and-import: "내보내기와 ê°€ì ¸ì˜¤ê¸°" - export-targets: - all-notes: "ëª¨ë“ ê¸€ ë°ì´í„°" - following-list: "팔로잉" - mute-list: "뮤트" - blocking-list: "차단" - user-lists: "리스트" - export-requested: "내보내기를 ìš”ì²í•˜ì˜€ìŠµë‹ˆë‹¤. ì´ ìž‘ì—…ì€ ì‹œê°„ì´ ê±¸ë¦´ 수 있습니다. 내보내기가 완료ë˜ë©´ ë“œë¼ì´ë¸Œì— 파ì¼ì´ 추가ë©ë‹ˆë‹¤." - import-requested: "ê°€ì ¸ì˜¤ê¸°ë¥¼ ìš”ì²í•˜ì˜€ìŠµë‹ˆë‹¤. ì´ ìž‘ì—…ì—는 ì‹œê°„ì´ ê±¸ë¦´ 수 있습니다." - enter-password: "비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - danger-zone: "위험한 ì„¤ì •" - delete-account: "ê³„ì • ì‚ì œ" - account-deleted: "ê³„ì •ì´ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤. ë°ì´í„°ê°€ 사ë¼ì§ˆ 때까지 ì‹œê°„ì´ ê±¸ë¦´ 수 있습니다." - profile-metadata: "프로필 추가 ì •ë³´" - metadata-label: "ë¼ë²¨" - metadata-content: "ë‚´ìš©" -common/views/components/user-list-editor.vue: - users: "사용ìž" - rename: "리스트 ì´ë¦„ 바꾸기" - delete: "리스트 ì‚ì œ" - remove-user: "ì´ ë¦¬ìŠ¤íŠ¸ì—ì„œ ì œê±°" - delete-are-you-sure: "리스트 \"$1\"ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - deleted: "ì‚ì œí•˜ì˜€ìŠµë‹ˆë‹¤" - add-user: "ì‚¬ìš©ìž ì¶”ê°€" -common/views/components/user-group-editor.vue: - users: "멤버" - rename: "ê·¸ë£¹ëª…ì„ ë³€ê²½" - delete: "ê·¸ë£¹ì„ ì‚ì œ" - transfer: "ê·¸ë£¹ì„ ì–‘ë„" - transfer-are-you-sure: "그룹 「$1ã€ì„ 「@$2〠님ì—게 ì–‘ë„í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - transferred: "ê·¸ë£¹ì„ ì–‘ë„하였습니다" - remove-user: "ì´ ê·¸ë£¹ì—ì„œ ì‚ì œ" - delete-are-you-sure: "그룹 「$1ã€ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - deleted: "ì‚ì œí•˜ì˜€ìŠµë‹ˆë‹¤" - invite: "초대" - invited: "초대를 보냈습니다" -common/views/components/user-lists.vue: - user-lists: "리스트" - create-list: "리스트 만들기" - list-name: "리스트 ì´ë¦„" -common/views/components/user-groups.vue: - user-groups: "그룹" - create-group: "그룹 만들기" - group-name: "그룹명" - owned-groups: "ìžì‹ ì˜ ê·¸ë£¹" - joined-groups: "ì°¸ì—¬ì¤‘ì¸ ê·¸ë£¹" - invites: "초대" - accept-invite: "참여" - reject-invite: "거부" -common/views/widgets/broadcast.vue: - fetching: "확ì¸ì¤‘" - no-broadcasts: "공지사í•ì´ 없습니다" - have-a-nice-day: "ì¢‹ì€ í•˜ë£¨ ë˜ì„¸ìš”!" - next: "다ìŒ" - prev: "ì´ì „" -common/views/widgets/calendar.vue: - year: "{}ë…„" - month: "{}ì›”" - day: "{}ì¼" - today: "오늘:" - this-month: "ì´ë²ˆ 달:" - this-year: "올해:" -common/views/widgets/photo-stream.vue: - title: "í¬í† 스트림" - no-photos: "ì‚¬ì§„ì´ ì—†ìŠµë‹ˆë‹¤" -common/views/widgets/posts-monitor.vue: - title: "글 차트" - toggle: "보기 ì „í™˜" -common/views/widgets/hashtags.vue: - title: "해시태그" -common/views/widgets/server.vue: - title: "서버 ì •ë³´" - toggle: "보기 ì „í™˜" -common/views/widgets/memo.vue: - title: "스티커 메모" - memo: "ì—¬ê¸°ì— ì¨ì£¼ì„¸ìš”!" - save: "ì €ìž¥" -common/views/widgets/slideshow.vue: - folder-customize-mode: "í´ë”를 ì§€ì •í•˜ë ¤ë©´ 커스터마ì´ì§• 모드를 종료하여 주ì‹ì‹œì˜¤" - folder: "í´ë¦í•˜ì—¬ í´ë”를 ì§€ì •í•˜ì—¬ 주ì‹ì‹œì˜¤" - no-image: "ì´ í´ë”ì— ì‚¬ì§„ì´ ì—†ìŠµë‹ˆë‹¤" -common/views/widgets/tips.vue: - tips-line1: "<kbd>1</kbd>ë¡œ 타임ë¼ì¸ì„ 활성화(focus)í• ìˆ˜ 있습니다" - tips-line2: "<kbd>p</kbd> 혹ì€<kbd>n</kbd> 키로 글 작성 í¼ì„ ì—´ 수 있습니다" - tips-line3: "ê¸€ì— íŒŒì¼ì„ ëŒì–´ë„£ì„ 수 있습니다" - tips-line4: "글쓰기 ì˜ì—ì— í´ë¦½ë³´ë“œì— 있는 ì´ë¯¸ì§€ë¥¼ ë¶™ì—¬ë„£ì„ ìˆ˜ 있습니다" - tips-line5: "ë“œë¼ì´ë¸Œì— 파ì¼ì„ ëŒì–´ë„£ì–´ 업로드 하실 수 있습니다" - tips-line6: "ë“œë¼ì´ë¸Œì—ì„œ 파ì¼ì„ ëŒì–´ í´ë”를 ì´ë™í• 수 있습니다" - tips-line7: "ë“œë¼ì´ë¸Œì—ì„œ í´ë”를 ëŒì–´ í´ë”를 ì´ë™í• 수 있습니다" - tips-line8: "í™ˆì„ ì„¤ì •ì—ì„œ 커스터마ì´ì§• í• ìˆ˜ 있습니다" - tips-line9: "Misskey는 AGPLv3입니다" - tips-line10: "íƒ€ìž„ë¨¸ì‹ ìœ„ì ¯ì„ ì‚¬ìš©í•˜ë©´ 간단하게 ê³¼ê±°ì˜ íƒ€ìž„ë¼ì¸ìœ¼ë¡œ 거슬러올ë¼ê°ˆ 수 있습니다" - tips-line11: "ê¸€ì˜ \"...\" ì„ í´ë¦í•˜ì—¬ ê¸€ì„ ì‚¬ìš©ìžì˜ 페ì´ì§€ì— ê³ ì •í• ìˆ˜ 있습니다" - tips-line13: "ê¸€ì— ì²¨ë¶€ëœ íŒŒì¼ì€ ë“œë¼ì´ë¸Œì— ì €ìž¥ë©ë‹ˆë‹¤" - tips-line14: "í™ˆì˜ ì»¤ìŠ¤í„°ë§ˆì´ì§•ì—ì„œ ìœ„ì ¯ì„ ë§ˆìš°ìŠ¤ 오른쪽 í´ë¦ 하여 ë””ìžì¸ì„ ë³€ê²½í• ìˆ˜ 있습니다" - tips-line17: "\"**\" 으로 í…스트를 ê°ì‹¸ë©´ **강조표시**ë©ë‹ˆë‹¤." - tips-line19: "몇몇 ì°½ì€ ë¸Œë¼ìš°ì € 밖으로 ë¶„ë¦¬í• ìˆ˜ 있습니다" - tips-line20: "ë‹¬ë ¥ ìœ„ì ¯ì˜ í¼ì„¼íŠ¸ëŠ” ê²½ê³¼ëœ ë¹„ìœ¨ì„ ë‚˜íƒ€ëƒ…ë‹ˆë‹¤" - tips-line21: "API를 사용하여 botì˜ ê°œë°œ ë“±ì„ í• ìˆ˜ 있습니다" - tips-line23: "ì•„ì´ ê·€ì—¬ì›Œìš” ì•„ì´" - tips-line24: "Misskey는 2014ë…„ì— ì„œë¹„ìŠ¤ë¥¼ 시작했습니다" - tips-line25: "대ì‘하는 브ë¼ìš°ì €ì¸ 경우 Misskey를 열어놓지 ì•Šì•„ë„ ì•Œë¦¼ì„ ë°›ì„ ìˆ˜ 있습니다" -common/views/pages/not-found.vue: - page-not-found: "페ì´ì§€ë¥¼ ì°¾ì„ ìˆ˜ 없습니다" -common/views/pages/follow.vue: - signed-in-as: "{}으로 로그ì¸" - following: "팔로우 중" - follow: "팔로우" - request-pending: "팔로우 허가 대기중" - follow-processing: "팔로우 처리중" - follow-request: "팔로우 ìš”ì²" -common/views/pages/follow-requests.vue: - received-follow-requests: "팔로우 ìš”ì²" - accept: "승ì¸" - reject: "거부" -desktop: - banner-crop-title: "배너로 í‘œì‹œí• ë¶€ë¶„ì„ ì„ íƒ" - banner: "배너" - uploading-banner: "새로운 배너를 ì—…ë¡œë“œí•˜ê³ ìžˆìŠµë‹ˆë‹¤" - banner-updated: "배너를 ì—…ë°ì´íŠ¸ 했습니다" - choose-banner: "배너 ì´ë¯¸ì§€ë¥¼ ì„ íƒ" - avatar-crop-title: "아바타로 í‘œì‹œí• ë¶€ë¶„ì„ ì„ íƒ" - avatar: "아바타" - uploading-avatar: "새로운 아바타를 ì—…ë¡œë“œí•˜ê³ ìžˆìŠµë‹ˆë‹¤" - avatar-updated: "아바타가 변경ë˜ì—ˆìŠµë‹ˆë‹¤" - choose-avatar: "아바타 ì´ë¯¸ì§€ë¥¼ ì„ íƒ" - unable-to-process: "ìž‘ì—…ì„ ì™„ë£Œí• ìˆ˜ 없습니다" - invalid-filetype: "ì´ í˜•ì‹ì˜ 파ì¼ì€ 지ì›ë˜ì§€ 않습니다" -desktop/views/components/activity.chart.vue: - total: "ê²€ì€ìƒ‰ ... ì „ì²´" - notes: "파랑색 ... 노트" - replies: "빨강색 ... 답글" - renotes: "ì´ˆë¡ìƒ‰ ... 리노트" -desktop/views/components/activity.vue: - title: "활ë™" - toggle: "보기 ì „í™˜" -desktop/views/components/calendar.vue: - title: "{year}ë…„ {month}ì›”" - prev: "ì´ì „ 달" - next: "ë‹¤ìŒ ë‹¬" - go: "í´ë¦í•˜ì—¬ 시간ì—í–‰" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count} íŒŒì¼ ì„ íƒì¤‘" - upload: "PCì—ì„œ ë“œë¼ì´ë¸Œì— 파ì¼ì„ 업로드" - cancel: "취소" - ok: "확ì¸" - choose-prompt: "íŒŒì¼ ì„ íƒ" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "취소" - ok: "확ì¸" - choose-prompt: "í´ë” ì„ íƒ" -desktop/views/components/crop-window.vue: - skip: "ìžë¥´ê¸° 건너뛰기" - cancel: "취소" - ok: "확ì¸" -desktop/views/components/drive-window.vue: - used: "사용중" -desktop/views/components/drive.file.vue: - avatar: "아바타" - banner: "배너" - nsfw: "열람주ì˜" - contextmenu: - rename: "ì´ë¦„ 변경" - mark-as-sensitive: "열람주ì˜ë¡œ ì„¤ì •" - unmark-as-sensitive: "ì—´ëžŒì£¼ì˜ í•´ì œ" - copy-url: "URL 복사" - download: "다운로드" - else-files: "기타" - set-as-avatar: "아바타로 ì„¤ì •" - set-as-banner: "배너로 ì„¤ì •" - open-in-app: "앱ì—ì„œ 열기" - add-app: "앱 추가" - rename-file: "íŒŒì¼ ì´ë¦„ 변경" - input-new-file-name: "새 íŒŒì¼ ì´ë¦„ì„ ìž…ë ¥í•´ 주ì‹ì‹œì˜¤" - copied: "복사 완료" - copied-url-to-clipboard: "URLì„ í´ë¦½ë³´ë“œì— 복사하였습니다" -desktop/views/components/drive.folder.vue: - upload-folder: "기본 업로드 위치" - unable-to-process: "ìž‘ì—…ì„ ì™„ë£Œí• ìˆ˜ 없습니다" - circular-reference-detected: "ëŒ€ìƒ í´ë”ê°€ ì´ë™í• í´ë”ì˜ í•˜ìœ„ í´ë”입니다." - unhandled-error: "ì•Œ 수 없는 오류" - unable-to-delete: "ì‚ì œí• ìˆ˜ 없습니다" - has-child-files-or-folders: "ì´ í´ë”는 비어있지 않기 ë•Œë¬¸ì— ì‚ì œí• ìˆ˜ 없습니다." - contextmenu: - move-to-this-folder: "ì´ í´ë”ë¡œ ì´ë™" - show-in-new-window: "새 창으로 보기" - rename: "ì´ë¦„ 변경" - rename-folder: "í´ë” ì´ë¦„ 변경" - input-new-folder-name: "새 í´ë” ì´ë¦„ì„ ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - else-folders: "기타" - set-as-upload-folder: "기본 업로드 위치로 ì„¤ì •" -desktop/views/components/drive.vue: - search: "검색" - empty-draghover: "ëŒì–´ë†“ìœ¼ì‹ ê±° 맞나요? 괜찮아요, ì €ëŠ” 귀여우니까요" - empty-drive: "ë“œë¼ì´ë¸Œì— ì•„ë¬´ê²ƒë„ ì—†ìŠµë‹ˆë‹¤" - empty-drive-description: "오른쪽 í´ë¦í•˜ì—¬ \"íŒŒì¼ ì—…ë¡œë“œ\"를 ì„ íƒí•˜ê±°ë‚˜, 파ì¼ì„ ëŒì–´ë„£ì–´ ì—…ë¡œë“œí• ìˆ˜ 있습니다." - empty-folder: "ì´ í´ë”는 비어있습니다" - unable-to-process: "ìž‘ì—…ì„ ì™„ë£Œí• ìˆ˜ 없습니다" - circular-reference-detected: "ëŒ€ìƒ í´ë”ê°€ ì´ë™í• í´ë”ì˜ í•˜ìœ„ í´ë”입니다." - unhandled-error: "ì•Œ 수 없는 오류" - url-upload: "URL 업로드" - url-of-file: "업로드 í•˜ë ¤ëŠ” 파ì¼ì˜ URL" - url-upload-requested: "업로드를 ìš”ì²í–ˆìŠµë‹ˆë‹¤" - may-take-time: "업로드가 완료ë 때까지 ì‹œê°„ì´ ì†Œìš”ë 수 있습니다." - create-folder: "í´ë” 만들기" - folder-name: "í´ë” ì´ë¦„" - contextmenu: - create-folder: "í´ë” 만들기" - upload: "íŒŒì¼ ì—…ë¡œë“œ" - url-upload: "URLì—ì„œ 업로드" -desktop/views/components/media-video.vue: - sensitive: "열람주ì˜" - click-to-show: "í´ë¦í•˜ì—¬ 표시" -desktop/views/components/followers-window.vue: - followers: "{} ì˜ íŒ”ë¡œì›Œ" -desktop/views/components/followers.vue: - empty: "팔로워가 없는 것 같습니다." -desktop/views/components/following-window.vue: - following: "{} ì˜ íŒ”ë¡œìš°" -desktop/views/components/following.vue: - empty: "íŒ”ë¡œìš°ì¤‘ì¸ ì‚¬ìš©ìžê°€ 없는 것 같습니다." -desktop/views/components/game-window.vue: - game: "리버시" -desktop/views/components/home.vue: - done: "완료" - add-widget: "ìœ„ì ¯ 추가:" - add: "추가" -desktop/views/input-dialog.vue: - cancel: "취소" - ok: "확ì¸" -desktop/views/components/note-detail.vue: - private: "ì´ ê¸€ì€ ë¹„ê³µê°œìž…ë‹ˆë‹¤" - deleted: "ì´ ê¸€ì€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" - location: "위치 ì •ë³´" - renote: "리노트" - add-reaction: "리액션 추가" - undo-reaction: "리액션 취소" -desktop/views/components/note.vue: - reply: "답글 달기" - renote: "리노트" - add-reaction: "리액션 추가" - undo-reaction: "리액션 취소" - detail: "ìƒì„¸" - private: "ì´ ê¸€ì€ ë¹„ê³µê°œìž…ë‹ˆë‹¤" - deleted: "ì´ ê¸€ì€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" -desktop/views/components/notes.vue: - error: "불러오지 못했습니다" - retry: "재시ë„" -desktop/views/components/notifications.vue: - empty: "비었습니다!" -desktop/views/components/post-form.vue: - posted: "게시하였습니다!" - replied: "ë‹µê¸€ì„ ë‹¬ì•˜ìŠµë‹ˆë‹¤!" - reposted: "리노트 하였습니다!" - note-failed: "ê²Œì‹œì— ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤" - reply-failed: "ë‹µê¸€ì„ ë‹¬ì§€ 못했습니다" - renote-failed: "ë¦¬ë…¸íŠ¸ì— ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤" -desktop/views/components/post-form-window.vue: - note: "새 글" - reply: "답글 달기" - attaches: "첨부: {} 미디어" - uploading-media: "{}ê°œì˜ ë¯¸ë””ì–´ë¥¼ 업로드 중" -desktop/views/components/progress-dialog.vue: - waiting: "대기중" -desktop/views/components/renote-form.vue: - quote: "ì¸ìš©í•˜ê¸°..." - cancel: "취소" - renote: "리노트" - renote-home: "리노트 (홈)" - reposting: "작업중입니다..." - success: "리노트 하였습니다!" - failure: "ë¦¬ë…¸íŠ¸ì— ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤" -desktop/views/components/renote-form-window.vue: - title: "ì´ ê¸€ì„ ë¦¬ë…¸íŠ¸í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" -desktop/views/pages/user-following-or-followers.vue: - following: "{user}ì˜ íŒ”ë¡œìž‰" - followers: "{user}ì˜ íŒ”ë¡œì›Œ" -desktop/views/components/settings.2fa.vue: - intro: "2단계 ì¸ì¦ì„ ì„¤ì •í•˜ë©´ ë¡œê·¸ì¸ í•˜ë ¤ë©´ 비밀번호 외ì—ë„ ë¯¸ë¦¬ ë“±ë¡ í•´ë†“ì€ ë¬¼ë¦¬ì 장치 (예를 들면 ë‹¹ì‹ ì˜ ìŠ¤ë§ˆíŠ¸ í° ë“±) ë„ í•„ìš”í•˜ê²Œ ë˜ì–´ 보안 ìˆ˜ì¤€ì„ ë³´ë‹¤ í–¥ìƒì‹œí‚µë‹ˆë‹¤." - detail: "ìžì„¸ížˆ..." - url: "https://www.google.com/intl/ko/landing/2step/" - caution: "등ë¡í•œ 장치를 분실한 경우 Misskeyì— ë¡œê·¸ì¸í• 수 없게 ë˜ë¯€ë¡œ 주ì˜í•˜ì—¬ 주ì‹ì‹œì˜¤." - register: "장치 등ë¡" - already-registered: "ì´ë¯¸ ì„¤ì •ì´ ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤." - unregister: "ì„¤ì • í•´ì œ" - unregistered: "2단계 ì¸ì¦ì´ 비활성화ë˜ì—ˆìŠµë‹ˆë‹¤." - enter-password: "비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - authenticator: "ë¨¼ì €, ê°€ì§€ê³ ê³„ì‹ ìž¥ì¹˜ì— Google Authenticator를 설치해야 합니다:" - howtoinstall: "설치 ë°©ë²•ì€ ì—¬ê¸°ì— ìžˆìŠµë‹ˆë‹¤" - token: "í† í°" - scan: "다ìŒìœ¼ë¡œ 표시ë˜ì–´ 있는 QR 코드를 스캔합니다:" - done: "사용중ì´ì‹ ìž¥ì¹˜ì— í‘œì‹œëœ í† í°ì„ ìž…ë ¥í•´ì£¼ì‹œë©´ 마무리ë©ë‹ˆë‹¤:" - submit: "완료" - success: "ì„¤ì •ì´ ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤!" - failed: "ì„¤ì •ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. í† í°ì´ 잘못ë˜ì—ˆëŠ”지 확ì¸í•´ì£¼ì‹ì‹œì˜¤." - info: "ë‹¤ìŒ ë¡œê·¸ì¸ë¶€í„°ëŠ” ì´ì™€ ë™ì¼í•˜ê²Œ ë¹„ë°€ë²ˆí˜¸ì— ë”í•´ ìž¥ì¹˜ì— í‘œì‹œëœ í† í°ì„ ìž…ë ¥í•©ë‹ˆë‹¤." - totp-header: "ì¸ì¦ 앱" - security-key-header: "보안 키" - security-key: "ë³´ì•ˆì„ ê°•í™”í•˜ë ¤ë©´ FIDO2를 지ì›í•˜ëŠ” 하드웨어 보안 키를 사용하여 ê³„ì •ì— ë¡œê·¸ì¸í• 수 있습니다. ë¡œê·¸ì¸ ì‹œ 등ë¡í•˜ì˜€ë˜ 보안 키 ë˜ëŠ” ì¸ì¦ ì•±ì´ í•„ìš”í•˜ê²Œ ë©ë‹ˆë‹¤." - last-used: "마지막 사용:" - activate-key: "í´ë¦í•˜ì—¬ 보안 키를 활성화하여 주ì‹ì‹œì˜¤" - security-key-name: "키 ì´ë¦„" - register-security-key: "키 ë“±ë¡ ì™„ë£Œ" - something-went-wrong: "으악! 키를 등ë¡í•˜ëŠ” ë„중 ë¬¸ì œê°€ ë°œìƒí•˜ì˜€ìŠµë‹ˆë‹¤:" - key-unregistered: "키가 등ë¡ë˜ì–´ 있지 않습니다" - use-password-less-login: "비밀번호 없는 ë¡œê·¸ì¸ ì‚¬ìš©" -common/views/components/media-image.vue: - sensitive: "열람주ì˜" - click-to-show: "í´ë¦í•˜ì—¬ 보기" -common/views/components/api-settings.vue: - intro: "API를 ì‚¬ìš©í•˜ë ¤ë©´ ìœ„ì˜ í† í°ì„ \"i\" ë¼ëŠ” í‚¤ì˜ ê°’ìœ¼ë¡œ 매개변수를 추가하여 ìš”ì²í•©ë‹ˆë‹¤." - caution: "ê³„ì •ì„ ë¶€ì • ì‚¬ìš©í• ê°€ëŠ¥ì„±ì´ ìžˆìœ¼ë¯€ë¡œ, ì´ í† í°ì€ ì œ 3ìžì—게 ì•Œë ¤ì£¼ì§€ 마ì‹ì‹œì˜¤ (앱 ë“±ì— ë¶™ì—¬ë„£ì§€ 마ì‹ì‹œì˜¤)." - regeneration-of-token: "ë§Œì¼ ì´ í† í°ì´ ìœ ì¶œë˜ì—ˆê±°ë‚˜ 그럴 ê°€ëŠ¥ì„±ì´ ìžˆëŠ” 경우 í† í°ì„ 재ìƒì„±í• 수 있습니다." - regenerate-token: "í† í° ìž¬ìƒì„±" - token: "Token:" - enter-password: "비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - console: - title: "API 콘솔" - endpoint: "엔드í¬ì¸íŠ¸" - parameter: "매개변수" - credential-info: "\"i\" 패러미터는 ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤." - send: "ì „ì†¡" - sending: "ì‘ë‹µì„ ê¸°ë‹¤ë¦¬ëŠ” 중" - response: "ê²°ê³¼" -desktop/views/components/settings.apps.vue: - no-apps: "ì—°ê²°ëœ ì• í”Œë¦¬ì¼€ì´ì…˜ì´ 없습니다" -common/views/components/drive-settings.vue: - max: "최대 용량" - in-use: "사용중" - stats: "통계" - default-upload-folder: "기본 업로드 í´ë” 위치" - default-upload-folder-name: "í´ë”" - change-default-upload-folder: "í´ë” 변경" -common/views/components/mute-and-block.vue: - mute-and-block: "뮤트 ë° ì°¨ë‹¨" - mute: "뮤트" - block: "차단" - no-muted-users: "뮤트한 사용ìžê°€ 없습니다" - no-blocked-users: "차단한 사용ìžê°€ 없습니다" - word-mute: "단어 뮤트" - muted-words: "ë®¤íŠ¸ëœ í‚¤ì›Œë“œ" - muted-words-description: "공백으로 구분하는 경우 ANDë¡œ ì§€ì •ë˜ë©°, 줄바꿈으로 구분하는 경우 ORë¡œ ì§€ì •ë©ë‹ˆë‹¤" - unmute-confirm: "ì´ ì‚¬ìš©ìžë¥¼ 뮤트 í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - unblock-confirm: "ì´ ì‚¬ìš©ìžë¥¼ 차단 í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - save: "ì €ìž¥" -common/views/components/password-settings.vue: - reset: "비밀번호 변경" - enter-current-password: "현재 비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - enter-new-password: "새 비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - enter-new-password-again: "다시 í•œ 번 새 비밀번호를 ìž…ë ¥í•˜ì—¬ 주ì‹ì‹œì˜¤" - not-match: "새 비밀번호가 ì¼ì¹˜í•˜ì§€ 않습니다" - changed: "비밀번호를 변경하였습니다" - failed: "비밀번호 ë³€ê²½ì„ ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤." -common/views/components/post-form-attaches.vue: - attach-cancel: "첨부 취소" - mark-as-sensitive: "열람주ì˜ë¡œ ì„¤ì •" - unmark-as-sensitive: "ì—´ëžŒì£¼ì˜ í•´ì œ" -desktop/views/components/sub-note-content.vue: - private: "ì´ ê¸€ì€ ë¹„ê³µê°œìž…ë‹ˆë‹¤" - deleted: "ì´ ê¸€ì€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" - media-count: "{}ê°œì˜ ë¯¸ë””ì–´" - poll: "투표" -desktop/views/components/settings.tags.vue: - title: "태그" - query: "쿼리 (ìƒëžµ 가능)" - add: "추가" - save: "ì €ìž¥" -desktop/views/components/timeline.vue: - home: "홈" - local: "로컬" - hybrid: "소셜" - global: "글로벌" - mentions: "ë°›ì€ ë©˜ì…˜" - messages: "다ì´ë ‰íŠ¸ 게시글" - list: "리스트" - hashtag: "해시태그" - add-tag-timeline: "해시태그 추가" - add-list: "리스트 추가" - list-name: "리스트 ì´ë¦„" -desktop/views/components/ui.header.vue: - welcome-back: "ëŒì•„ì˜¤ì‹ ê±¸ 환ì˜í•©ë‹ˆë‹¤." - adjective: "님" -desktop/views/components/ui.header.account.vue: - profile: "프로필" - lists: "리스트" - groups: "그룹" - follow-requests: "팔로우 ìš”ì²" - admin: "관리" - room: "룸" -desktop/views/components/ui.header.nav.vue: - game: "게임" -desktop/views/components/ui.header.notifications.vue: - title: "알림" -desktop/views/components/ui.header.post.vue: - post: "새 글" -desktop/views/components/ui.header.search.vue: - placeholder: "검색" -desktop/views/components/user-preview.vue: - notes: "글" - following: "팔로잉" - followers: "팔로워" -desktop/views/components/users-list.vue: - all: "모ë‘" - iknow: "아는 사람" - fetching: "불러오는 중입니다" -desktop/views/components/users-list-item.vue: - followed: "ë‹¹ì‹ ì„ íŒ”ë¡œìš°í•©ë‹ˆë‹¤" -desktop/views/components/window.vue: - popout: "íŒì•„웃" - close: "닫기" -admin/views/index.vue: - dashboard: "대시보드" - instance: "ì¸ìŠ¤í„´ìŠ¤" - emoji: "커스텀 ì´ëª¨ì§€" - moderators: "모ë”ë ˆì´í„°" - users: "사용ìž" - federation: "ì—°í•©" - announcements: "공지사í•" - abuse: "스팸 ì‹ ê³ " - queue: "ìž‘ì—… 대기열" - logs: "로그" - db: "ë°ì´í„°ë² ì´ìŠ¤" - back-to-misskey: "Misskeyë¡œ ëŒì•„가기" -admin/views/db.vue: - tables: "í…Œì´ë¸”" - vacuum: "ì²ì†Œ" - vacuum-info: "ë°ì´í„°ë² ì´ìŠ¤ë¥¼ ì²ì†Œí•©ë‹ˆë‹¤. ë°ì´í„°ëŠ” ê·¸ëŒ€ë¡œì¸ ì±„ë¡œ ë°ì´í„°ì˜ ì‚¬ìš©ëŸ‰ì„ ì¤„ìž…ë‹ˆë‹¤. ì¼ë°˜ì 으로 ì´ ìž‘ì—…ì€ ìžë™ìœ¼ë¡œ ì •ê¸°ì 으로 수행ë©ë‹ˆë‹¤." - vacuum-exclamation: "ì²ì†Œë¥¼ 수행하면 í•œë™ì•ˆ ë°ì´í„°ë² ì´ìŠ¤ì˜ 부하가 ë†’ì•„ì ¸ ì‚¬ìš©ìž ì¡°ìž‘ì„ ì²˜ë¦¬í•˜ì§€ 못 í• ìˆ˜ 있습니다." -admin/views/dashboard.vue: - dashboard: "대시보드" - accounts: "ê³„ì •" - notes: "글" - drive: "ë“œë¼ì´ë¸Œ" - instances: "ì¸ìŠ¤í„´ìŠ¤" - this-instance: "ì´ ì¸ìŠ¤í„´ìŠ¤" - federated: "ì—°í•©" -admin/views/queue.vue: - title: "í" - remove-all-jobs: "ëª¨ë“ ìž‘ì—… ì œê±°" - jobs: "ìž‘ì—…" - queue: "í" - domains: - deliver: "ì „ì†¡" - inbox: "ìˆ˜ì‹ " - db: "ë°ì´í„°ë² ì´ìŠ¤" - objectStorage: "오브ì 트 ìŠ¤í† ë¦¬ì§€" - state: "ìƒíƒœ" - states: - active: "처리중" - delayed: "지연ë¨" - waiting: "ëŒ€ê¸°ì—´ì— ìžˆìŒ" - result-is-truncated: "결과는 ìƒëžµë˜ì—ˆìŠµë‹ˆë‹¤" - other-queues: "기타 í" -admin/views/logs.vue: - logs: "로그" - domain: "ë„ë©”ì¸" - level: "수준" - levels: - all: "ì „ì²´" - info: "ì •ë³´" - success: "성공" - warning: "ê²½ê³ " - error: "오류" - debug: "디버그" - delete-all: "ëª¨ë‘ ì‚ì œ" -admin/views/abuse.vue: - title: "스팸 ì‹ ê³ " - target: "대ìƒ" - reporter: "ì‹ ê³ ìž" - details: "ìƒì„¸" - remove-report: "ì‚ì œ" -admin/views/instance.vue: - instance: "ì¸ìŠ¤í„´ìŠ¤" - instance-name: "ì¸ìŠ¤í„´ìŠ¤ ì´ë¦„" - instance-description: "ì¸ìŠ¤í„´ìŠ¤ì˜ 소개" - host: "관리ìž" - icon-url: "ì•„ì´ì½˜ URL" - logo-url: "ë¡œê³ URL" - banner-url: "배너 ì´ë¯¸ì§€ URL" - error-image-url: "오류 ì´ë¯¸ì§€ URL" - languages: "ì¸ìŠ¤í„´ìŠ¤ì˜ ëŒ€ìƒ ì–¸ì–´" - languages-desc: "공백으로 구분하여 여러 ê°œ ì„¤ì •í• ìˆ˜ 있습니다." - tos-url: "ì´ìš©ì•½ê´€ URL" - repository-url: "ì €ìž¥ì†Œ URL" - feedback-url: "피드백 URL" - maintainer-config: "ê´€ë¦¬ìž ì •ë³´" - maintainer-name: "ê´€ë¦¬ìž ì´ë¦„" - maintainer-email: "ê´€ë¦¬ìž ì—°ë½ì²˜" - advanced-config: "ê·¸ 외 ì„¤ì •" - note-and-tl: "글과 타임ë¼ì¸" - drive-config: "ë“œë¼ì´ë¸Œ ì„¤ì •" - use-object-storage: "오브ì 트 ìŠ¤í† ë¦¬ì§€ë¥¼ 사용" - object-storage-base-url: "URL" - object-storage-bucket: "버킷 ì´ë¦„" - object-storage-prefix: "프리픽스" - object-storage-endpoint: "엔드í¬ì¸íŠ¸" - object-storage-region: "ë¦¬ì „" - object-storage-port: "í¬íŠ¸" - object-storage-access-key: "액세스 키" - object-storage-secret-key: "ì‹œí¬ë¦¿ 키" - object-storage-use-ssl: "SSL 사용" - object-storage-s3-info: "Amazon S3를 오브ì 트 ìŠ¤í† ë¦¬ì§€ë¡œ 사용하는 ê²½ìš°ì˜ ã€Œì—”ë“œí¬ì¸íŠ¸ã€ì™€ ã€Œë¦¬ì „ã€ì˜ ì„¤ì •ê°’ì— ëŒ€í•´ì„œëŠ” {0}ì„ í™•ì¸í•˜ì—¬ 주ì‹ì‹œì˜¤." - object-storage-s3-info-here: "ì´ê³³" - object-storage-gcs-info: "Google Cloud Storage를 오브ì 트 ìŠ¤í† ë¦¬ì§€ë¡œ 사용하는 경우, 「엔드í¬ì¸íŠ¸ã€ëŠ” storage.googleapis.com 으로 ì„¤ì •í•˜ê³ , ã€Œë¦¬ì „ã€ ëž€ì€ ë¹„ì›ë‹ˆë‹¤." - cache-remote-files: "ì›ê²© 파ì¼ì„ ìºì‹œ" - cache-remote-files-desc: "ì´ ì„¤ì •ì„ í•´ì§€í•˜ë©´ ì›ê²© 파ì¼ì„ ìºì‹œí•˜ì§€ ì•Šê³ í•´ë‹¹ 파ì¼ì„ ì§ì ‘ ë§í¬í•˜ê²Œ ë©ë‹ˆë‹¤. ê·¸ì— ë”°ë¼ ì„œë²„ì˜ ì €ìž¥ ê³µê°„ì„ ì ˆì•½í• ìˆ˜ 있지만, 프ë¼ì´ë²„ì‹œ ì„¤ì •ì—ì„œ ì§ì ‘ ë§í¬ë¥¼ 무효로 ì„¤ì •í•œ 사용ìžì—게는 파ì¼ì´ ë³´ì´ì§€ 않거나, ì¸ë„¤ì¼ì´ ìƒì„±ë˜ì§€ 않기 ë•Œë¬¸ì— í†µì‹ ëŸ‰ì´ ì¦ê°€í•©ë‹ˆë‹¤. ë³´í†µì€ ì´ ì„¤ì •ì„ ì‚¬ìš©í•˜ê±°ë‚˜ ì•„ëž˜ì˜ ì›ê²© íŒŒì¼ í”„ë¡ì‹œë¥¼ ì„¤ì •í•˜ëŠ” ê²ƒì„ ì¶”ì²œí•©ë‹ˆë‹¤." - proxy-remote-files: "ì›ê²© íŒŒì¼ í”„ë¡ì‹œ" - proxy-remote-files-desc: "ì´ ì„¤ì •ì„ ì‚¬ìš©í•˜ë©´, ì €ìž¥ë˜ì§€ 않았거나 용량 초과로 ì‚ì œëœ ì›ê²© 파ì¼ì„ 로컬ì—ì„œ 프ë¡ì‹œí•˜ì—¬ ì¸ë„¤ì¼ì„ ìƒì„±í•˜ê²Œ ë©ë‹ˆë‹¤." - local-drive-capacity-mb: "로컬 ì‚¬ìš©ìž í•œ 명당 ë“œë¼ì´ë¸Œ 용량" - remote-drive-capacity-mb: "ì›ê²© ì‚¬ìš©ìž í•œ 명당 ë“œë¼ì´ë¸Œ 용량" - mb: "메가바ì´íŠ¸ 단위" - recaptcha-config: "reCAPCHA ì„¤ì •" - recaptcha-info: "reCAPCHA를 사용하ë„ë¡ ì„¤ì •í•˜ëŠ” 경우 reCAPCHA í† í°ì„ 확보해야 합니다. https://www.google.com/recaptcha/intro/ ì— ì ‘ì†í•˜ì—¬ í† í°ì„ ê°€ì ¸ì™€ì£¼ì‹ì‹œì˜¤." - recaptcha-info2: "v3는지ì›í•˜ì§€ 않습니다. v2를 사용하여 주ì‹ì‹œì˜¤." - enable-recaptcha: "reCAPCHA 활성화" - recaptcha-site-key: "사ì´íŠ¸ 키" - recaptcha-secret-key: "ì‹œí¬ë¦¿ 키" - recaptcha-preview: "미리보기" - hidden-tags: "숨긴 해시태그" - hidden-tags-info: "집계ì—ì„œ ì œì™¸í• í•´ì‹œíƒœê·¸ë¥¼ 줄 바꿈으로 구분하여 ê¸°ìˆ í•©ë‹ˆë‹¤." - external-service-integration-config: "외부 서비스 연계" - twitter-integration-config: "Twitter ì—°ë™ ì„¤ì •" - twitter-integration-info: "콜백 URLì€ {url} ë¡œ ì„¤ì •ë©ë‹ˆë‹¤." - enable-twitter-integration: "트위터 ì—°ë™ í™œì„±í™”" - twitter-integration-consumer-key: "Consumer key" - twitter-integration-consumer-secret: "Consumer secret" - github-integration-config: "Github ì—°ë™ ì„¤ì •" - github-integration-info: "콜백 URLì€ {url} ë¡œ ì„¤ì •ë©ë‹ˆë‹¤." - enable-github-integration: "Github ì—°ë™ í™œì„±í™”" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-config: "Discord ì—°ë™ ì„¤ì •" - discord-integration-info: "콜백 URLì€ {url} ë¡œ ì„¤ì •ë©ë‹ˆë‹¤." - enable-discord-integration: "Discord ì—°ë™ í™œì„±í™”" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - proxy-account-config: "프ë¡ì‹œ ê³„ì • ì„¤ì •" - proxy-account-info: "프ë¡ì‹œ ê³„ì •ì€ íŠ¹ì • ì¡°ê±´ì—ì„œ 사용ìžì˜ ì›ê²© íŒ”ë¡œìž‰ì„ ëŒ€í–‰í•˜ëŠ” ê³„ì •ìž…ë‹ˆë‹¤. 예를 들면, 사용ìžê°€ ì›ê²© 사용ìžë¥¼ ë¦¬ìŠ¤íŠ¸ì— ë„£ì—ˆì„ ë•Œ ë¦¬ìŠ¤íŠ¸ì— ì‚½ìž…ëœ ì‚¬ìš©ìžë¥¼ ì•„ë¬´ë„ íŒ”ë¡œìš°í•˜ì§€ 않으면 해당 사용ìžì˜ 액티비티가 ì´ ì„œë²„ë¡œ ì „ì†¡ë˜ì§€ 않습니다. 그런 경우 ëŒ€ì‹ í”„ë¡ì‹œ ê³„ì •ì´ í•´ë‹¹ 사용ìžë¥¼ 팔로우하ë„ë¡ í•©ë‹ˆë‹¤." - proxy-account-username: "프ë¡ì‹œ ê³„ì • 사용ìžëª…" - proxy-account-username-desc: "프ë¡ì‹œë¡œ ì‚¬ìš©í• ì‚¬ìš©ìžì˜ 사용ìžëª…ì„ ì§€ì •í•˜ì—¬ 주ì‹ì‹œì˜¤." - proxy-account-warn: "프ë¡ì‹œ ê³„ì •ì€ ìžë™ìœ¼ë¡œ ìƒì„±ë˜ì§€ 않으므로 해당 사용ìžëª…ì˜ ê³„ì •ì„ ë¯¸ë¦¬ ìƒì„±í•´ë‘¬ì•¼ 합니다." - max-note-text-length: "ê¸€ì˜ ìµœëŒ€ 문ìžìˆ˜" - disable-registration: "ì‚¬ìš©ìž ë“±ë¡ ë¹„í™œì„±í™”" - disable-local-timeline: "로컬 타임ë¼ì¸ 비활성화" - disable-global-timeline: "글로벌 타임ë¼ì¸ 비활성화" - disabling-timelines-info: "ì´ íƒ€ìž„ë¼ì¸ë“¤ì„ ë¹„í™œì„±í™”í•´ë„ ê´€ë¦¬ìž ë° ëª¨ë”ë ˆì´í„°ëŠ” ê³„ì† ì‚¬ìš©í• ìˆ˜ 있습니다." - enable-emoji-reaction: "ë¦¬ì•¡ì…˜ì— ì´ëª¨ì§€ë¥¼ ì‚¬ìš©í• ìˆ˜ 있게 함" - use-star-for-reaction-fallback: "ì•Œ 수 없는 ë¦¬ì•¡ì…˜ì„ starë¡œ 대체하여 사용" - invite: "초대" - save: "ì €ìž¥" - saved: "ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" - pinned-users: "ê³ ì •ëœ ì‚¬ìš©ìž" - pinned-users-info: "ê³ ì •í•´ë‘ê³ ì‹¶ì€ ì‚¬ìš©ìžë¥¼ 줄바꿈으로 구분하여 ê¸°ìˆ í•©ë‹ˆë‹¤." - email-config: "ë©”ì¼ ì„œë²„ ì„¤ì •" - email-config-info: "ë©”ì¼ ì£¼ì†Œ í™•ì¸ í˜¹ì€ ë¹„ë°€ë²ˆí˜¸ ìž¬ì„¤ì •ì— ì‚¬ìš© ë©ë‹ˆë‹¤." - enable-email: "ë©”ì¼ ë°œì‹ í™œì„±í™”" - email: "ë©”ì¼ ì£¼ì†Œ" - smtp-secure: "SMTP ì—°ê²°ì— ì•”ì‹œì 으로 SSL/TLS를 사용" - smtp-secure-info: "STARTTLS를 사용 ì‹œ ON으로 합니다." - smtp-host: "SMTP 호스트" - smtp-port: "SMTP í¬íŠ¸" - smtp-auth: "SMTP ì¸ì¦ 수행" - smtp-user: "SMTP 사용ìž" - smtp-pass: "SMTP 비밀번호" - test-email: "테스트" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "ServiceWorker 사용" - serviceworker-info: "í‘¸ì‹œì•Œë¦¼ì„ ìˆ˜í–‰í•˜ë ¤ë©´ 사용해야 합니다." - vapid-publickey: "VAPID 공개키" - vapid-privatekey: "VAPID ê°œì¸í‚¤" - vapid-info: "ServiceWorker를 사용하는 경우 VAPID 키 ìŒì„ ìƒì„±í•´ì•¼ 합니다. ì…¸ì—ì„œ 다ìŒê³¼ ê°™ì´ í•©ë‹ˆë‹¤:" -admin/views/charts.vue: - title: "차트" - per-day: "1ì¼ë§ˆë‹¤" - per-hour: "1시간마다" - federation: "ì—°í•©" - notes: "글" - users: "사용ìž" - drive: "ë“œë¼ì´ë¸Œ" - network: "네트워í¬" - charts: - federation-instances: "ì¸ìŠ¤í„´ìŠ¤ 수 ì¦ê°" - federation-instances-total: "ì¸ìŠ¤í„´ìŠ¤ 수 누계" - notes: "글 ì¦ê° (통합)" - local-notes: "글 ì¦ê° (로컬)" - remote-notes: "글 ì¦ê° (ì›ê²©)" - notes-total: "글 누ì " - users: "ì‚¬ìš©ìž ì¦ê°" - users-total: "ì‚¬ìš©ìž ëˆ„ì " - active-users: "활성 ì‚¬ìš©ìž ìˆ˜" - drive: "ë“œë¼ì´ë¸Œ 사용량 ì¦ê°" - drive-total: "ë“œë¼ì´ë¸Œ 사용량 누ì " - drive-files: "ë“œë¼ì´ë¸Œ íŒŒì¼ ìˆ˜ ì¦ê°" - drive-files-total: "ë“œë¼ì´ë¸Œ íŒŒì¼ ìˆ˜ 누ì " - network-requests: "ìš”ì²" - network-time: "ì‘답시간" - network-usage: "í†µì‹ ëŸ‰" -admin/views/drive.vue: - operation: "ìž‘ì—…" - fileid-or-url: "íŒŒì¼ ID ë˜ëŠ” íŒŒì¼ URL" - file-not-found: "파ì¼ì„ ì°¾ì„ ìˆ˜ 없습니다" - lookup: "조회" - sort: - title: "ì •ë ¬" - createdAtAsc: "업로드 ë‚ ì§œ 오랜 순" - createdAtDesc: "업로드 ë‚ ì§œ ìµœì‹ ìˆœ" - sizeAsc: "í¬ê¸°ê°€ ìž‘ì€ ìˆœ" - sizeDesc: "í¬ê¸°ê°€ í° ìˆœ" - origin: - title: "출처" - combined: "로컬 + 리모트" - local: "로컬" - remote: "리모트" - delete: "ì‚ì œ" - deleted: "ì‚ì œí•˜ì˜€ìŠµë‹ˆë‹¤" - mark-as-sensitive: "열람주ì˜ë¡œ ì„¤ì •" - unmark-as-sensitive: "ì—´ëžŒì£¼ì˜ í•´ì œ" - marked-as-sensitive: "열람주ì˜ë¡œ ì„¤ì •í•˜ì˜€ìŠµë‹ˆë‹¤" - unmarked-as-sensitive: "열람주ì˜ë¥¼ ì œê±°í•˜ì˜€ìŠµë‹ˆë‹¤" - clean-remote-files: "리모트 íŒŒì¼ ìºì‹œë¥¼ ì‚ì œ" - clean-remote-files-are-you-sure: "ì •ë§ ëª¨ë“ ë¦¬ëª¨íŠ¸ 파ì¼ì˜ ìºì‹œë¥¼ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - clean-up: "ì²ì†Œ" -admin/views/users.vue: - operation: "ìž‘ì—…" - username-or-userid: "사용ìžëª… í˜¹ì€ ì‚¬ìš©ìž ID" - user-not-found: "사용ìžë¥¼ ì°¾ì„ ìˆ˜ 없습니다" - lookup: "조회" - reset-password: "비밀번호 ìž¬ì„¤ì •" - reset-password-confirm: "비밀번호를 ìž¬ì„¤ì •í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - password-updated: "비밀번호는 현재 \"{password}\" 입니다" - suspend: "ì •ì§€" - suspend-confirm: "ì •ì§€í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - suspended: "ì •ì§€í•˜ì˜€ìŠµë‹ˆë‹¤" - unsuspend: "ì •ì§€ í•´ì œ" - unsuspend-confirm: "ì •ì§€ë¥¼ í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - unsuspended: "ì •ì§€ë¥¼ í•´ì œí•˜ì˜€ìŠµë‹ˆë‹¤" - make-silence: "침묵" - silence-confirm: "침묵으로 ì„¤ì •í•©ë‹ˆê¹Œ?" - unmake-silence: "침묵 í•´ì œ" - unsilence-confirm: "침묵 í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - update-remote-user: "ì›ê²© ì‚¬ìš©ìž ì •ë³´ ê°±ì‹ " - remote-user-updated: "ì›ê²© ì‚¬ìš©ìž ì •ë³´ë¥¼ ê°±ì‹ í•˜ì˜€ìŠµë‹ˆë‹¤" - delete-all-files: "ëª¨ë“ íŒŒì¼ ì‚ì œ" - delete-all-files-confirm: "ëª¨ë“ íŒŒì¼ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - username: "사용ìžëª…" - host: "호스트" - users: - title: "사용ìž" - sort: - title: "ì •ë ¬" - createdAtAsc: "등ë¡ì¼ì´ ì˜¤ëž˜ëœ ìˆœ" - createdAtDesc: "등ë¡ì¼ì´ ìµœì‹ ì¸ ìˆœ" - updatedAtAsc: "ìˆ˜ì •ì¼ì´ ì˜¤ëž˜ëœ ìˆœ" - updatedAtDesc: "ìˆ˜ì •ì¼ì´ ìµœì‹ ì¸ ìˆœ" - state: - title: "ìƒíƒœ" - all: "모ë‘" - available: "ì´ìš© 가능" - admin: "관리ìž" - moderator: "모ë”ë ˆì´í„°" - adminOrModerator: "관리ìž+모ë”ë ˆì´í„°" - silenced: "침묵ë¨" - suspended: "ì •ì§€ë¨" - origin: - title: "위치 (오리진)" - combined: "로컬 + ì›ê²©" - local: "로컬" - remote: "ì›ê²©" - createdAt: "ë“±ë¡ ë‚ ì§œ" - updatedAt: "ìˆ˜ì •í•œ ë‚ ì§œ" -admin/views/moderators.vue: - add-moderator: - title: "모ë”ë ˆì´í„° 등ë¡" - add: "등ë¡" - added: "모ë”ë ˆì´í„°ë¥¼ 등ë¡í•˜ì˜€ìŠµë‹ˆë‹¤" - remove: "í•´ì œ" - removed: "모ë”ë ˆì´í„° 등ë¡ì„ í•´ì œí–ˆìŠµë‹ˆë‹¤" - logs: - title: "로그" - moderator: "모ë”ë ˆì´í„°" - type: "ìž‘ì—…" - at: "ì¼ì‹œ" - info: "ì •ë³´" -admin/views/emoji.vue: - add-emoji: - title: "ì´ëª¨ì§€ 등ë¡" - name: "ì´ëª¨ì§€ ì´ë¦„" - name-desc: "a~z 0~9 _ ì˜ ë¬¸ìžë¥¼ ì‚¬ìš©í• ìˆ˜ 있습니다." - category: "ì¹´í…Œê³ ë¦¬" - aliases: "별ì¹" - aliases-desc: "공백으로 구분하여 여러 ê°œ ì„¤ì •í• ìˆ˜ 있습니다." - url: "ì´ëª¨ì§€ ì´ë¯¸ì§€ URL" - add: "추가" - info: "50KB ë¯¸ë§Œì˜ PNG ì´ë¯¸ì§€ë¥¼ 추천합니다." - added: "ì´ëª¨ì§€ë¥¼ 등ë¡í•˜ì˜€ìŠµë‹ˆë‹¤" - emojis: - title: "ì´ëª¨ì§€ 목ë¡" - update: "ì—…ë°ì´íŠ¸" - remove: "ì‚ì œ" - updated: "ì—…ë°ì´íŠ¸ 하였습니다" - remove-emoji: - are-you-sure: "\"$1\" ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - removed: "ì‚ì œí•˜ì˜€ìŠµë‹ˆë‹¤" -admin/views/announcements.vue: - announcements: "공지사í•" - save: "ì €ìž¥" - remove: "ì‚ì œ" - add: "추가" - title: "ì œëª©" - text: "ë‚´ìš©" - saved: "ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" - _remove: - are-you-sure: "\"$1\" ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - removed: "ì‚ì œí•˜ì˜€ìŠµë‹ˆë‹¤" -admin/views/hashtags.vue: - hided-tags: "Hidden Tags" -admin/views/federation.vue: - instance: "ì¸ìŠ¤í„´ìŠ¤" - host: "호스트" - notes: "글" - users: "사용ìž" - following: "팔로우 중" - followers: "팔로워" - caught-at: "ë“±ë¡ ë‚ ì§œ" - status: "ìƒíƒœ" - latest-request-sent-at: "마지막으로 ìš”ì²ì„ ì „ì†¡í•œ 시간" - latest-request-received-at: "마지막으로 ìš”ì²ì„ ë°›ì€ ì‹œê°„" - remove-all-following: "ëª¨ë“ íŒ”ë¡œìž‰ í•´ì œ" - remove-all-following-info: "{host}(으)로부터 ëª¨ë“ íŒ”ë¡œìž‰ì„ í•´ì œí•©ë‹ˆë‹¤. 해당 ì¸ìŠ¤í„´ìŠ¤ê°€ ë” ì´ìƒ 존재하지 않게 ëœ ê²½ìš° ë“±ì— ì‹¤í–‰í•˜ì‹ì‹œì˜¤." - delete-all-files: "파ì¼ì„ ëª¨ë‘ ì‚ì œ" - block: "차단" - marked-as-closed: "íì‡„ëœ ê²ƒìœ¼ë¡œ 표시" - lookup: "조회" - instances: "ì—°í•©" - instance-not-registered: "해당 ì¸ìŠ¤í„´ìŠ¤ê°€ 등ë¡ë˜ì–´ 있지 않습니다" - sort: "ì •ë ¬" - sorts: - caughtAtAsc: "등ë¡ì¼ì´ ì˜¤ëž˜ëœ ìˆœ" - caughtAtDesc: "등ë¡ì¼ì´ ìµœì‹ ì¸ ìˆœ" - lastCommunicatedAtAsc: "마지막으로 ìš”ì²ì„ ì£¼ê³ ë°›ì€ ì¼ì‹œê°€ ì˜¤ëž˜ëœ ìˆœ" - lastCommunicatedAtDesc: "마지막으로 ìš”ì²ì„ ì£¼ê³ ë°›ì€ ì¼ì‹œê°€ ë¹ ë¥¸ 순" - notesAsc: "ê¸€ì´ ì ì€ ìˆœ" - notesDesc: "ê¸€ì´ ë§Žì€ ìˆœ" - usersAsc: "사용ìžê°€ ì ì€ ìˆœ" - usersDesc: "사용ìžê°€ ë§Žì€ ìˆœ" - followingAsc: "íŒ”ë¡œìž‰ì´ ì ì€ ìˆœ" - followingDesc: "íŒ”ë¡œìž‰ì´ ë§Žì€ ìˆœ" - followersAsc: "팔로워가 ì ì€ ìˆœ" - followersDesc: "팔로워가 ë§Žì€ ìˆœ" - driveUsageAsc: "ë“œë¼ì´ë¸Œ ì‚¬ìš©ëŸ‰ì´ ì ì€ ìˆœ" - driveUsageDesc: "ë“œë¼ì´ë¸Œ ì‚¬ìš©ëŸ‰ì´ ë§Žì€ ìˆœ" - driveFilesAsc: "ë“œë¼ì´ë¸Œ íŒŒì¼ ìˆ˜ê°€ ì ì€ ìˆœ" - driveFilesDesc: "ë“œë¼ì´ë¸Œ íŒŒì¼ ìˆ˜ê°€ ë§Žì€ ìˆœ" - state: "ìƒíƒœ" - states: - all: "모ë‘" - blocked: "차단ë¨" - not-responding: "ì‘답 ì—†ìŒ" - marked-as-closed: "íì‡„ëœ ê²ƒìœ¼ë¡œ 표시ë¨" - result-is-truncated: "ìƒìœ„ {n}개를 í‘œì‹œí•˜ê³ ìžˆìŠµë‹ˆë‹¤." - charts: "차트" - chart-srcs: - requests: "ìš”ì²" - users: "ì‚¬ìš©ìž ì¦ê°" - users-total: "ì‚¬ìš©ìž ëˆ„ì " - notes: "글 ì¦ê°" - notes-total: "글 누ì " - ff: "팔로잉/팔로워 ì¦ê°" - ff-total: "팔로잉/팔로워 누ì " - drive-usage: "ë“œë¼ì´ë¸Œ 사용량 ì¦ê°" - drive-usage-total: "ë“œë¼ì´ë¸Œ 사용량 누ì " - drive-files: "ë“œë¼ì´ë¸Œ íŒŒì¼ ìˆ˜ ì¦ê°" - drive-files-total: "ë“œë¼ì´ë¸Œ íŒŒì¼ ìˆ˜ 누ì " - chart-spans: - hour: "1시간마다" - day: "1ì¼ë§ˆë‹¤" - blocked-hosts: "차단" - blocked-hosts-info: "ì°¨ë‹¨í• í˜¸ìŠ¤íŠ¸ë¥¼ 줄바꿈으로 구분하여 ê¸°ìˆ í•©ë‹ˆë‹¤." - save: "ì €ìž¥" -desktop/views/pages/welcome.vue: - about: "ìžì„¸ížˆ..." - timeline: "타임ë¼ì¸" - announcements: "공지사í•" - photos: "최근 ì´ë¯¸ì§€" - powered-by-misskey: "Powered by <b>Misskey</b>." - info: "ì •ë³´" -desktop/views/pages/drive.vue: - title: "Misskey Drive" -desktop/views/pages/note.vue: - prev: "ì´ì „ 글" - next: "ë‹¤ìŒ ê¸€" -desktop/views/pages/selectdrive.vue: - title: "파ì¼ì„ ì„ íƒí•˜ì—¬ 주ì‹ì‹œì˜¤" - ok: "확ì¸" - cancel: "취소" - upload: "PCì—ì„œ ë“œë¼ì´ë¸Œì— 파ì¼ì„ 업로드" -desktop/views/pages/search.vue: - not-available: "검색 ê¸°ëŠ¥ì€ ì¸ìŠ¤í„´ìŠ¤ ì„¤ì •ì—ì„œ 비활성화ë˜ì–´ 있습니다." - not-found: "\"{q}\" 와 ì¼ì¹˜í•˜ëŠ” ê¸€ì„ ì°¾ì„ ìˆ˜ 없습니다." -desktop/views/pages/tag.vue: - no-posts-found: "해시태그 \"{q}\"ê°€ ë¶™ì€ ê¸€ì„ ì°¾ì„ ìˆ˜ 없습니다." -desktop/views/pages/user-list.users.vue: - users: "사용ìž" - add-user: "ì‚¬ìš©ìž ì¶”ê°€" - username: "사용ìžëª…" -desktop/views/pages/user/user.followers-you-know.vue: - title: "아는 ì‚¬ëžŒì˜ íŒ”ë¡œì›Œ" - loading: "로드 중" - no-users: "아는 ì‚¬ëžŒì˜ íŒ”ë¡œì›Œê°€ 없습니다." -desktop/views/pages/user/user.friends.vue: - title: "ìžì£¼ 언급ë˜ëŠ” 사용ìž" - loading: "로드 중" - no-users: "ìžì£¼ 언급ë˜ëŠ” 사용ìžê°€ 없습니다" -desktop/views/pages/user/user.photos.vue: - title: "사진" - loading: "로드 중" - no-photos: "ì‚¬ì§„ì´ ì—†ìŠµë‹ˆë‹¤" -desktop/views/pages/user/user.header.vue: - posts: "글" - following: "팔로잉" - followers: "팔로워" - is-bot: "ì´ ê³„ì •ì€ Bot입니다" - no-description: "ìžê¸°ì†Œê°œê°€ 없습니다" - years-old: "{age}세" - year: "ë…„" - month: "ì›”" - day: "ì¼" - follows-you: "ë‹¹ì‹ ì„ íŒ”ë¡œìš°í•©ë‹ˆë‹¤" -desktop/views/pages/user/user.timeline.vue: - default: "글" - with-replies: "글과 답글" - with-media: "미디어" - my-posts: "ë‚´ 글" -desktop/views/widgets/notifications.vue: - title: "알림" -desktop/views/widgets/polls.vue: - title: "투표" - refresh: "ìƒˆë¡œê³ ì¹¨" - nothing: "없습니다!" -desktop/views/widgets/post-form.vue: - title: "글쓰기" - note: "글쓰기" - something-happened: "ì•Œ 수 없는 ë¬¸ì œë¡œ ê¸€ì„ ê²Œì‹œí• ìˆ˜ 없습니다." -desktop/views/widgets/profile.vue: - update-banner: "í´ë¦í•˜ì—¬ 배너 변경" - update-avatar: "í´ë¦í•˜ì—¬ 아바타 변경" -desktop/views/widgets/trends.vue: - title: "íŠ¸ë Œë“œ" - refresh: "ìƒˆë¡œê³ ì¹¨" - nothing: "없습니다!" -desktop/views/widgets/users.vue: - title: "추천 사용ìž" - refresh: "ìƒˆë¡œê³ ì¹¨" - no-one: "없습니다!" -mobile/views/components/drive.vue: - used: "사용중" - folder-count: "í´ë”" - count-separator: ", " - file-count: "파ì¼" - nothing-in-drive: "ë“œë¼ì´ë¸Œì— ì•„ë¬´ê²ƒë„ ì—†ìŠµë‹ˆë‹¤" - folder-is-empty: "í´ë”ê°€ 비어있습니다" - folder-name: "í´ë” ì´ë¦„" - here-is-root: "현재 경로는 루트 경로로 í´ë”ê°€ 아닙니다." - url-prompt: "업로드 í•˜ë ¤ëŠ” 파ì¼ì˜ URL" - uploading: "업로드를 ìš”ì²í•˜ì˜€ìŠµë‹ˆë‹¤. 업로드가 완료ë 때까지 ì‹œê°„ì´ ì†Œìš”ë 수 있습니다." - folder-name-cannot-empty: "í´ë” ì´ë¦„ì€ ë¹„ì›Œë‘˜ 수 없습니다." -mobile/views/components/drive-file-chooser.vue: - select-file: "íŒŒì¼ ì„ íƒ" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "í´ë” ì„ íƒ" -mobile/views/components/drive.file.vue: - nsfw: "열람주ì˜" -mobile/views/components/drive.file-detail.vue: - download: "다운로드" - rename: "ì´ë¦„ 변경" - move: "ì´ë™" - hash: "í•´ì‹œ (md5)" - exif: "EXIF" - nsfw: "열람주ì˜" - mark-as-sensitive: "열람주ì˜ë¡œ ì„¤ì •" - unmark-as-sensitive: "ì—´ëžŒì£¼ì˜ í•´ì œ" -mobile/views/components/media-video.vue: - sensitive: "열람주ì˜" - click-to-show: "í´ë¦í•˜ì—¬ 표시" -common/views/components/follow-button.vue: - following: "팔로우 중" - follow: "팔로우" - request-pending: "팔로우 허가 대기중" - follow-processing: "팔로우 처리중" - follow-request: "팔로우 ìš”ì²" -mobile/views/components/note.vue: - private: "ì´ ê¸€ì€ ë¹„ê³µê°œìž…ë‹ˆë‹¤" - deleted: "ì´ ê¸€ì€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" - location: "위치 ì •ë³´" -mobile/views/components/note-detail.vue: - reply: "답글 달기" - reaction: "리액션" - private: "ì´ ê¸€ì€ ë¹„ê³µê°œìž…ë‹ˆë‹¤" - deleted: "ì´ ê¸€ì€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" - location: "위치 ì •ë³´" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "cat" -mobile/views/components/notifications.vue: - empty: "없습니다!" -mobile/views/components/sub-note-content.vue: - private: "ì´ ê¸€ì€ ë¹„ê³µê°œìž…ë‹ˆë‹¤" - deleted: "ì´ ê¸€ì€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" - media-count: "{}ê°œì˜ ë¯¸ë””ì–´" - poll: "투표" -mobile/views/components/ui.header.vue: - welcome-back: "ëŒì•„ì˜¤ì‹ ê±¸ 환ì˜í•©ë‹ˆë‹¤." - adjective: "님" -mobile/views/components/ui.nav.vue: - timeline: "타임ë¼ì¸" - notifications: "알림" - follow-requests: "팔로우 ìš”ì²" - search: "검색" - user-lists: "리스트" - user-groups: "그룹" - widgets: "ìœ„ì ¯" - game: "게임" - admin: "관리" - about: "Misskeyì— ëŒ€í•˜ì—¬" -mobile/views/pages/drive.vue: - contextmenu: - upload: "íŒŒì¼ ì—…ë¡œë“œ" - url-upload: "파ì¼ì„ URL로부터 업로드" - create-folder: "í´ë” 만들기" - rename-folder: "í´ë” ì´ë¦„ 바꾸기" - move-folder: "ì´ í´ë”를 ì´ë™" - delete-folder: "ì´ í´ë”를 ì‚ì œ" -mobile/views/pages/signup.vue: - lets-start: "📦 ì´ì œ ì‹œìž‘í•´ë„ ë©ë‹ˆë‹¤" -mobile/views/pages/followers.vue: - followers-of: "{name}ì˜ íŒ”ë¡œì›Œ" -mobile/views/pages/following.vue: - following-of: "{name}ì˜ íŒ”ë¡œìž‰" -mobile/views/pages/home.vue: - home: "홈" - local: "로컬" - hybrid: "소셜" - global: "글로벌" - mentions: "ë°›ì€ ë©˜ì…˜" - messages: "다ì´ë ‰íŠ¸ 게시글" -mobile/views/pages/tag.vue: - no-posts-found: "해시태그 \"{q}\"ê°€ ë¶™ì€ ê¸€ì„ ì°¾ì„ ìˆ˜ 없습니다." -mobile/views/pages/widgets.vue: - dashboard: "대시보드" - widgets-hints: "ìœ„ì ¯ì„ ì¶”ê°€ / ì œê±°í•˜ê±°ë‚˜ ì •ë ¬í• ìˆ˜ 있습니다. ìœ„ì ¯ì„ ì´ë™í•˜ë ¤ë©´ ì°½í‹€ì˜ \"☰\" ì•„ì´ì½˜ì„ 드래그합니다. ìœ„ì ¯ì„ ì‚ì œí•˜ë ¤ë©´ \"X\" ì•„ì´ì½˜ì„ íƒ í•©ë‹ˆë‹¤. 몇몇 ìœ„ì ¯ì€ íƒí•˜ë©´ 표시형ì‹ì„ 바꿀 수 있습니다." - add-widget: "추가" - customization-tips: "커스터마ì´ì§• ë„움ë§" -mobile/views/pages/widgets/activity.vue: - activity: "활ë™" -mobile/views/pages/share.vue: - share-with: "{name}(으)ë¡œ ê³µìœ " -mobile/views/pages/note.vue: - title: "글" - prev: "ì´ì „ 글" - next: "ë‹¤ìŒ ê¸€" -mobile/views/pages/games/reversi.vue: - reversi: "리버시" -mobile/views/pages/search.vue: - search: "검색" - not-found: "\"{q}\" 와 ì¼ì¹˜í•˜ëŠ” ê¸€ì„ ì°¾ì„ ìˆ˜ 없습니다." -mobile/views/pages/selectdrive.vue: - select-file: "íŒŒì¼ ì„ íƒ" -mobile/views/pages/notifications.vue: - notifications: "알림" -mobile/views/pages/settings.vue: - signed-in-as: "{}(으)ë¡œ 로그ì¸" -mobile/views/pages/user.vue: - follows-you: "ë‹¹ì‹ ì„ íŒ”ë¡œìš°í•©ë‹ˆë‹¤" - following: "팔로잉" - followers: "팔로워" - notes: "글" - overview: "요약" - timeline: "타임ë¼ì¸" - media: "미디어" - years-old: "{age}세" -mobile/views/pages/user/home.vue: - recent-notes: "최근 글" - images: "ì´ë¯¸ì§€" - activity: "활ë™" - keywords: "키워드" - domains: "ìžì£¼ ë³´ì´ëŠ” ë„ë©”ì¸" - frequently-replied-users: "ìžì£¼ 언급ë˜ëŠ” 사용ìž" - followers-you-know: "아는 ì‚¬ëžŒì˜ íŒ”ë¡œì›Œ" - last-used-at: "마지막 로그ì¸" -mobile/views/pages/user/home.photos.vue: - no-photos: "ì‚¬ì§„ì´ ì—†ìŠµë‹ˆë‹¤" -deck: - widgets: "ìœ„ì ¯" - home: "홈" - local: "로컬" - hybrid: "소셜" - hashtag: "해시태그" - global: "글로벌" - mentions: "ë°›ì€ ë©˜ì…˜" - direct: "다ì´ë ‰íŠ¸ 게시글" - notifications: "알림" - list: "리스트" - select-list: "리스트를 ì„ íƒí•˜ì—¬ 주ì‹ì‹œì˜¤" - swap-left: "왼쪽으로 ì´ë™" - swap-right: "오른쪽으로 ì´ë™" - swap-up: "위로 ì´ë™" - swap-down: "아래로 ì´ë™" - remove: "칼럼 ì œê±°" - add-column: "칼럼 추가" - rename: "ì´ë¦„ 변경" - stack-left: "ì™¼ìª½ì— ìŒ“ê¸°" - pop-right: "오른쪽으로 빼기" - disabled-timeline: - title: "ë¹„í™œì„±í™”ëœ íƒ€ìž„ë¼ì¸" - description: "서버 ìš´ì˜ìžì— ì˜í•´ ì´ íƒ€ìž„ë¼ì¸ì´ ì‚¬ìš©í• ìˆ˜ ì—†ë„ë¡ ì„¤ì •ë˜ì–´ 있습니다." -deck/deck.tl-column.vue: - is-media-only: "미디어가 달린 글만" - edit: "옵션" -deck/deck.user-column.vue: - follows-you: "ë‹¹ì‹ ì„ íŒ”ë¡œìš°í•©ë‹ˆë‹¤" - posts: "글" - following: "팔로잉" - followers: "팔로워" - images: "ì´ë¯¸ì§€" - activity: "활ë™" - timeline: "타임ë¼ì¸" - pinned-notes: "ê³ ì •í•´ë†“ì€ ê¸€" - pinned-page: "ê³ ì •í•´ë†“ì€ íŽ˜ì´ì§€" -docs: - edit-this-page-on-github: "틀린 ì ì´ë‚˜ ê°œì„ í• ì ì„ ì°¾ìœ¼ì…¨ë‚˜ìš”?" - edit-this-page-on-github-link: "ì´ íŽ˜ì´ì§€ë¥¼ GitHubì—ì„œ 편집" -dev/views/index.vue: - manage-apps: "앱 관리" -dev/views/apps.vue: - manage-apps: "앱 관리" - create-app: "앱 ìƒì„±" - app-missing: "앱 ì—†ìŒ" -dev/views/new-app.vue: - new-app: "새 ì• í”Œë¦¬ì¼€ì´ì…˜" - new-app-info: "ì• í”Œë¦¬ì¼€ì´ì…˜ì€ APIì—ì„œë„ ìƒì„±í• 수 있습니다. (app/create)" - create-app: "ì• í”Œë¦¬ì¼€ì´ì…˜ ìƒì„±" - app-name: "ì• í”Œë¦¬ì¼€ì´ì…˜ ì´ë¦„" - app-name-placeholder: "ex) Misskey for iOS" - app-name-desc: "ì•±ì˜ ì´ë¦„." - app-overview: "앱 개요" - app-overview-placeholder: "ex) Misskey iOS í´ë¼ì´ì–¸íŠ¸." - app-overview-desc: "ì• í”Œë¦¬ì¼€ì´ì…˜ì— 대한 간단한 설명ì´ë‚˜ 소개" - callback-url: "콜백 URL (옵션)" - callback-url-placeholder: "ex) https://your.app.example.com/callback.php" - callback-url-desc: "사용ìžê°€ ì¸ì¦ í¼ì—ì„œ ì¸ì¦í•œ ë’¤ 리다ì´ë ‰íŠ¸í• URLì„ ì„¤ì •í•©ë‹ˆë‹¤." - authority: "권한" - authority-desc: "ì´ê³³ì—ì„œ ìš”ì²í•œ ê¶Œí•œì— í•œì •í•˜ì—¬ APIë¡œ ì•¡ì„¸ìŠ¤í• ìˆ˜ 있습니다." - authority-warning: "ì•±ì„ ìƒì„±í•œ ë’¤ì—ë„ ë³€ê²½í• ìˆ˜ 있지만, 새로운 ê¶Œí•œì„ ì„¤ì •í•˜ëŠ” 경우 ê·¸ ì‹œì 부터 ì˜ˆì „ì— ë°œê¸‰ë°›ì•˜ë˜ ìœ ì € 키는 ëª¨ë‘ ë¬´íš¨í™”ë©ë‹ˆë‹¤." -pages: - new-page: "페ì´ì§€ 만들기" - edit-page: "페ì´ì§€ ìˆ˜ì •" - read-page: "소스 표시중" - page-created: "페ì´ì§€ë¥¼ 만들었습니다" - page-updated: "페ì´ì§€ë¥¼ ìˆ˜ì •í–ˆìŠµë‹ˆë‹¤" - name-already-exists: "ì§€ì •í•œ 페ì´ì§€ URLì€ ì´ë¯¸ 존재합니다" - title-invalid-name: "ìœ íš¨í•˜ì§€ ì•Šì€ íŽ˜ì´ì§€ URL입니다" - text-invalid-name: "비어있지 ì•Šì€ì§€ 확ì¸í•´ì£¼ì„¸ìš”" - are-you-sure-delete: "ì´ íŽ˜ì´ì§€ë¥¼ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" - page-deleted: "페ì´ì§€ê°€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤" - edit-this-page: "ì´ íŽ˜ì´ì§€ë¥¼ 편집" - pin-this-page: "í”„ë¡œí•„ì— ê³ ì •" - unpin-this-page: "프로필ì—ì„œ ê³ ì • í•´ì œ" - view-source: "소스 보기" - view-page: "페ì´ì§€ 보기" - like: "좋아요" - unlike: "좋아요 í•´ì œ" - liked-pages: "좋아요한 페ì´ì§€" - my-pages: "ë‚´ 페ì´ì§€" - inspector: "ì¸ìŠ¤íŽ™í„°" - content: "페ì´ì§€ 블ë¡" - variables: "변수" - variables-info: "변수를 사용하면 ë™ì ì¸ íŽ˜ì´ì§€ë¥¼ 만들 수 있습니다. í…ìŠ¤íŠ¸ì— <b>{ 변수명 }</b>ì„ ì 으면 ê·¸ ìœ„ì¹˜ì— ë³€ìˆ˜ì˜ ê°’ì„ ì§‘ì–´ë„£ìŠµë‹ˆë‹¤. 예를 들ìžë©´ <b>Hello { thing } world!</b> ë¼ëŠ” í…스트가 ìžˆì„ ë•Œ, 변수(thing)ì˜ ê°’ì´ <b>ai</b>ì¸ ê²½ìš° í…스트는 <b>Hello ai world!</b>ê°€ ë©ë‹ˆë‹¤." - variables-info2: "ë³€ìˆ˜ì˜ í‰ê°€(ê°’ì„ ì‚°ì¶œí•´ë‚´ëŠ” 것)는 위ì—서부터 아래로 진행ë˜ë¯€ë¡œ ì–´ë–¤ ë³€ìˆ˜ì˜ ë‚´ë¶€ì—ì„œ ìžì‹ 보다 ì•„ëž˜ì— ìžˆëŠ” 변수를 ì°¸ì¡°í• ìˆ˜ëŠ” 없습니다. 예를 들ìžë©´ 위ì—서부터 <b>A, B, C</b>ì˜ 3ê°œì˜ ë³€ìˆ˜ê°€ ì •ì˜ë˜ì–´ ìžˆì„ ë•Œ, <b>C</b>ì˜ ë‚´ë¶€ì— <b>A</b>나 <b>B</b>를 ì°¸ì¡°í• ìˆ˜ëŠ” 있지만, <b>A</b>ì˜ ë‚´ë¶€ì—ì„œ <b>B</b>나 <b>C</b>를 ì°¸ì¡°í• ìˆ˜ëŠ” 없습니다." - variables-info3: "사용ìžë¡œë¶€í„° ìž…ë ¥ì„ ë°›ìœ¼ë ¤ë©´, 페ì´ì§€ì— ã€Œì‚¬ìš©ìž ìž…ë ¥ã€ ë¸”ë¡ì„ ì‚½ìž…í•˜ê³ ã€Œë³€ìˆ˜ëª…ã€ì— ìž…ë ¥ë°›ì€ ê°’ì„ ì €ìž¥í•˜ê³ ì‹¶ì€ ë³€ìˆ˜ëª…ì„ ì„¤ì •í•©ë‹ˆë‹¤ (변수는 ìžë™ìœ¼ë¡œ ìƒì„±ë©ë‹ˆë‹¤). ê·¸ 변수를 사용하여 ì‚¬ìš©ìž ìž…ë ¥ì— ë”°ë¼ ë™ìž‘í• ìˆ˜ 있습니다." - variables-info4: "함수를 사용하면 반복ë˜ëŠ” ìž‘ì—…ì„ ì†ì‰½ê²Œ ì²˜ë¦¬í• ìˆ˜ 있습니다. 함수를 ë§Œë“œì‹œë ¤ë©´ ã€Œí•¨ìˆ˜ã€ íƒ€ìž…ì˜ ë³€ìˆ˜ë¥¼ 만ë“니다. 함수ì—ì„œ 슬롯(ì¸ìˆ˜)를 ë°›ë„ë¡ ì„¤ì •í•˜ë©´, 함수를 ì‚¬ìš©í• ë•Œ ìŠ¬ë¡¯ì— ìž…ë ¥ëœ ê°’ì„ í•¨ìˆ˜ 안ì—ì„œ ë³€ìˆ˜ë¡œì¨ ì´ìš©í• 수 있게 ë©ë‹ˆë‹¤. ë˜í•œ, AiScript 표준ì—는 함수를 ì¸ìˆ˜ë¡œ 받는 함수(ê³ ì°¨í•¨ìˆ˜)ë„ ì¡´ìž¬í•©ë‹ˆë‹¤. 함수를 미리 ì •ì˜í•˜ëŠ” 것 외ì—, ì´ì™€ ê°™ì€ ê³ ì°¨í•¨ìˆ˜ë¥¼ 즉ì„으로 ì„¤ì •í• ìˆ˜ 있습니다." - more-details: "ìžì„¸í•œ 설명" - title: "ì œëª©" - url: "페ì´ì§€ URL" - summary: "페ì´ì§€ 요약" - align-center: "ê°€ìš´ë° ì •ë ¬" - hide-title-when-pinned: "í”„ë¡œí•„ì— ê³ ì •í•´ë†“ì€ ê²½ìš° 타ì´í‹€ì„ 표시하지 ì•ŠìŒ" - font: "글꼴" - fontSerif: "세리프" - fontSansSerif: "ì‚° 세리프" - set-eye-catching-image: "ì•„ì´ìºì¹˜ ì´ë¯¸ì§€ë¥¼ ì„¤ì •" - remove-eye-catching-image: "ì•„ì´ìºì¹˜ ì´ë¯¸ì§€ë¥¼ ì‚ì œ" - choose-block: "ë¸”ë¡ ì¶”ê°€" - select-type: "종류 ì„ íƒ" - enter-variable-name: "ë³€ìˆ˜ëª…ì„ ì„¤ì •í•´ì£¼ì‹ì‹œì˜¤" - the-variable-name-is-already-used: "ê·¸ ë³€ìˆ˜ëª…ì€ ì´ë¯¸ 사용중입니다" - content-blocks: "콘í…ì¸ " - input-blocks: "ìž…ë ¥" - special-blocks: "특수" - post-from-post-form: "ì´ ë‚´ìš©ì„ ì˜¬ë¦¬ê¸°" - posted-from-post-form: "게시하였습니다" - blocks: - text: "í…스트" - textarea: "í…스트 ì˜ì—" - section: "섹션" - image: "ì´ë¯¸ì§€" - button: "버튼" - if: "만약" - _if: - variable: "변수" - post: "글 ìž…ë ¥ëž€" - _post: - text: "ë‚´ìš©" - textInput: "í…스트 ìž…ë ¥" - _textInput: - name: "변수명" - text: "ì œëª©" - default: "기본값" - textareaInput: "여러 줄 í…스트 ìž…ë ¥" - _textareaInput: - name: "변수명" - text: "ì œëª©" - default: "기본값" - numberInput: "수치 ìž…ë ¥" - _numberInput: - name: "변수명" - text: "ì œëª©" - default: "기본값" - switch: "스위치" - _switch: - name: "변수명" - text: "ì œëª©" - default: "기본값" - counter: "ì¹´ìš´í„°" - _counter: - name: "변수명" - text: "ì œëª©" - inc: "ì¦ê°€ì¹˜" - _button: - text: "ì œëª©" - colored: "색ìƒ" - action: "ë²„íŠ¼ì„ ëˆŒë €ì„ ë•Œì˜ ë™ìž‘" - _action: - dialog: "대화ìƒìžë¥¼ 표시" - _dialog: - content: "ë‚´ìš©" - resetRandom: "난수를 초기화" - pushEvent: "ì´ë²¤íŠ¸ 보내기" - _pushEvent: - event: "ì´ë²¤íŠ¸ ì´ë¦„" - message: "ëˆŒë €ì„ ë•Œ í‘œì‹œí• ë©”ì‹œì§€" - variable: "보낼 변수" - no-variable: "ì—†ìŒ" - radioButton: "ì„ íƒì§€" - _radioButton: - name: "변수명" - title: "ì œëª©" - values: "줄바꿈으로 êµ¬ë¶„ëœ ì„ íƒì§€" - default: "기본값" - script: - categories: - flow: "í름 ì œì–´" - logical: "논리 ì—°ì‚°" - operation: "계산" - comparison: "비êµ" - random: "ëžœë¤" - value: "ê°’" - fn: "함수" - text: "í…스트 ì¡°ìž‘" - convert: "변환" - list: "리스트" - blocks: - text: "í…스트" - multiLineText: "í…스트 (여러줄)" - textList: "í…스트 목ë¡" - _textList: - info: "ê°ê°ì„ 줄 바꿈으로 구분해주ì‹ì‹œì˜¤" - strLen: "í…ìŠ¤íŠ¸ì˜ ê¸¸ì´" - _strLen: - arg1: "í…스트" - strPick: "ë¬¸ìž ì¶”ì¶œ" - _strPick: - arg1: "í…스트" - arg2: "ë¬¸ìž ìœ„ì¹˜" - strReplace: "í…스트 치환" - _strReplace: - arg1: "í…스트" - arg2: "치환 ì „" - arg3: "치환 후" - strReverse: "í…스트 뒤집기" - _strReverse: - arg1: "í…스트" - join: "í…스트 ì ‘í•©" - _join: - arg1: "리스트" - arg2: "구분ìž" - add: "+ ë”하기" - _add: - arg1: "A" - arg2: "B" - subtract: "- 빼기" - _subtract: - arg1: "A" - arg2: "B" - multiply: "× 곱하기" - _multiply: - arg1: "A" - arg2: "B" - divide: "÷ 나누기" - _divide: - arg1: "A" - arg2: "B" - mod: "÷ 나눈 나머지" - _mod: - arg1: "A" - arg2: "B" - round: "소수ì ì„ ë°˜ì˜¬ë¦¼" - _round: - arg1: "수치" - eq: "A와 Bê°€ ë™ì¼" - _eq: - arg1: "A" - arg2: "B" - notEq: "A와 Bê°€ 다름" - _notEq: - arg1: "A" - arg2: "B" - and: "A ê·¸ë¦¬ê³ B" - _and: - arg1: "A" - arg2: "B" - or: "A í˜¹ì€ B" - _or: - arg1: "A" - arg2: "B" - lt: "< Aê°€ B보다 ìž‘ìŒ" - _lt: - arg1: "A" - arg2: "B" - gt: "> Aê°€ B보다 í¼" - _gt: - arg1: "A" - arg2: "B" - ltEq: "<= Aê°€ B보다 작거나 ê°™ìŒ" - _ltEq: - arg1: "A" - arg2: "B" - gtEq: ">= Aê°€ B보다 í¬ê±°ë‚˜ ê°™ìŒ" - _gtEq: - arg1: "A" - arg2: "B" - if: "분기" - _if: - arg1: "만약" - arg2: "그러면" - arg3: "ê·¸ë ‡ì§€ 않으면" - not: "ë¶€ì •" - _not: - arg1: "ë¶€ì •" - random: "ëžœë¤" - _random: - arg1: "í™•ë¥ " - rannum: "난수" - _rannum: - arg1: "최소" - arg2: "최대" - randomPick: "목ë¡ì—ì„œ ìž„ì˜ë¡œ ì„ íƒ" - _randomPick: - arg1: "리스트" - dailyRandom: "ëžœë¤ (하루ë™ì•ˆ ê²°ê³¼ ìœ ì§€)" - _dailyRandom: - arg1: "í™•ë¥ " - dailyRannum: "난수 (하루ë™ì•ˆ ê²°ê³¼ ìœ ì§€)" - _dailyRannum: - arg1: "최소" - arg2: "최대" - dailyRandomPick: "목ë¡ì—ì„œ ìž„ì˜ë¡œ ì„ íƒ (하루ë™ì•ˆ ê²°ê³¼ ìœ ì§€)" - _dailyRandomPick: - arg1: "리스트" - seedRandom: "무작위 (시드)" - _seedRandom: - arg1: "시드" - arg2: "í™•ë¥ " - seedRannum: "난수 (시드)" - _seedRannum: - arg1: "시드" - arg2: "최소" - arg3: "최대" - seedRandomPick: "목ë¡ì—ì„œ 무작위로 ì„ íƒ (시드)" - _seedRandomPick: - arg1: "시드" - arg2: "리스트" - DRPWPM: "í™•ë¥ í˜• 목ë¡ì—ì„œ ìž„ì˜ë¡œ ì„ íƒ (하루ë™ì•ˆ ê²°ê³¼ ìœ ì§€)" - _DRPWPM: - arg1: "í…스트 목ë¡" - pick: "목ë¡ì—ì„œ ì„ íƒ" - _pick: - arg1: "리스트" - arg2: "위치" - listLen: "ë¦¬ìŠ¤íŠ¸ì˜ ê¸¸ì´ ê°€ì ¸ì˜¤ê¸°" - _listLen: - arg1: "리스트" - number: "수치" - stringToNumber: "í…스트를 수치로" - _stringToNumber: - arg1: "í…스트" - numberToString: "수치를 í…스트로" - _numberToString: - arg1: "수치" - splitStrByLine: "í…스트를 í–‰ 단위로 ë¶„í• " - _splitStrByLine: - arg1: "í…스트" - ref: "변수" - fn: "함수" - _fn: - slots: "슬롯" - slots-info: "ê° ìŠ¬ë¡¯ì„ ì¤„ 바꿈으로 구분하여 주ì‹ì‹œì˜¤" - arg1: "ì¶œë ¥" - for: "반복" - _for: - arg1: "횟수" - arg2: "처리" - typeError: "슬롯 {slot}ì€ \"{expect}\"를 ì‚¬ìš©í• ìˆ˜ 있지만 \"{actual}ì´ ë“¤ì–´ìžˆìŠµë‹ˆë‹¤!" - thereIsEmptySlot: "슬롯 {slot}ì´(ê°€) 비었습니다!" - types: - string: "í…스트" - number: "수치" - boolean: "플래그" - array: "리스트" - stringArray: "í…스트 목ë¡" - emptySlot: "빈 슬롯" - enviromentVariables: "환경 변수" - pageVariables: "페ì´ì§€ 요소" - argVariables: "ìž…ë ¥ 슬롯" -room: - add-furniture: "가구를 배치" - translate: "ì´ë™" - rotate: "íšŒì „" - exit: "ì„ íƒ í•´ì œ" - remove: "치우기" - save: "ì €ìž¥" - saved: "ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" - clear: "ëª¨ë‘ ì¹˜ìš°ê¸°" - clear-confirm: "ì •ë§ ë°© ì•ˆì˜ ëª¨ë“ ê°€êµ¬ë¥¼ ì¹˜ìš°ì‹œê² ìŠµë‹ˆê¹Œ?" - leave-confirm: "ì €ìž¥ë˜ì§€ ì•Šì€ ë³€ê²½ 사í•ì´ 있습니다. ì •ë§ ë‚˜ê°€ì‹œê² ìŠµë‹ˆê¹Œ?" - chooseImage: "ì´ë¯¸ì§€ ì„ íƒ" - room-type: "룸 종류" - carpet-color: "바닥 색ìƒ" - rooms: - default: "기본" - washitsu: "ì¼ë³¸ì‹" - furnitures: - milk: "ìš°ìœ íŒ©" - bed: "침대" - low-table: "ë‚®ì€ í…Œì´ë¸”" - desk: "ì±…ìƒ" - chair: "ì˜ìž" - chair2: "ì˜ìž 2" - fan: "환기구" - pc: "컴퓨터" - plant: "관엽ì‹ë¬¼" - plant2: "관엽ì‹ë¬¼ 2" - eraser: "지우개" - pencil: "ì—°í•„" - pudding: "푸딩" - cardboard-box: "골íŒì§€ ìƒìž" - cardboard-box2: "골íŒì§€ ìƒìž 2" - cardboard-box3: "골íŒì§€ ìƒìž 3" - book: "ì±…" - book2: "ì±… 2" - piano: "피아노" - facial-tissue: "휴지 ìƒìž" - server: "서버" - moon: "달" - corkboard: "게시íŒ" - mousepad: "마우스 패드" - monitor: "모니터" - keyboard: "키보드" - carpet-stripe: "카페트 (줄무늬)" - mat: "매트" - color-box: "책장" - wall-clock: "ë²½ê±¸ì´ ì‹œê³„" - photoframe: "ì•¡ìž" - cube: "í브" - tv: "TV" - pinguin: "íŽê·„" - rubik-cube: "루빅스 í브" - poster-h: "í¬ìŠ¤í„° (가로)" - poster-v: "í¬ìŠ¤í„° (세로)" - sofa: "소파" - spiral: "ë‚˜ì„ í˜• 계단" - bin: "휴지통" - cup-noodle: "컵ë¼ë©´" - holo-display: "홀로그램" - energy-drink: "ì—너지 ë“œë§í¬" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml deleted file mode 100644 index c76e98229dd1a7578e96d517fea390de2ac09f25..0000000000000000000000000000000000000000 --- a/locales/nl-NL.yml +++ /dev/null @@ -1,701 +0,0 @@ ---- -meta: - lang: "Nederlands" -common: - misskey: "Deel alles met anderen die ook Misskey gebruiken." - about-title: "Een ster van het fediverse" - about: "Bedankt voor het ontdekken van Misskey. Misskey is een <b>gedecentraliseerd microblogging platform</b> geboren op aarde. Omdat het bestaat binnen het Fediverse (een georganiseerd universum van verschillende sociale mediaplatformen), staat het verbonden met andere sociale medieplatformen. Neem een pauze van de stedelijke drukte, en duik in het nieuwe intenet?" - intro: - title: "Wat is Misskey?" - about: "Misskey is een open source <b>gedecentraliseerd blogplatform</b>. Het heeft een gesofisticeerde, volledig aanpasbare gebruikersinterface, uitgebreide reactiemogelijkheden voor posts, gratis geïntegreerd bestandsoplagbeheer en andere geavanceerde mogelijkheden. Daarnaast staat Misskey verbonden aan een netwerksysteem genaam het \"Fediverse\", hiermee kunnen we communiceren met andere gebruikers op andere SNSs. Dit betekent dat wanneer je iets post het niet enkel verstuurd wordt naar andere Misskey gebruikers, maar ook naar gebruikers op Mastodon en Pleroma. Stel je voor dat een planeet een radiosignaal verzendt naar een andere planeet als manier van communiceren." - features: "Kenmerken" - rich-contents: "Bericht" - rich-contents-desc: "Post jouw idee, hot topic, wat je ook maar wil delen. Maak jouw teksten aantrekkelijk met je favoriete foto's, verzend bestanden, zelfs video's, of maak een poll. Dit zijn enkele van de mogelijkheden die Misskey aanbiedt!" - reaction: "Reactie" - reaction-desc: "Dé makkelijkste manier om jouw gevoelens uit te drukken. Met Misskey kan je verschillende reacties toevoegen aan jouw posts. Andere SNSs hebben enkel maar een \"vind ik leuk\" reactie." - ui: "Interface" - ui-desc: "Niet één UI past nij iedereen. Daarom heeft Misskey een uitgebreide keuze om de UI naar jouw hand te zetten. Je kan jouw nieuwe thuis zo origineel maken als je zelf wil door jouw tijdslijn aan te passen door widgets te verplaatsen en hun look te veranderen. Zo maak je van Misskey jouw eigen stek." - drive: "Drive" - drive-desc: "Wil je een foto posten die je reeds het geüpload? Wens je georganiseerde map met zelfgekozen naam maken voor al jouw bestanden? De beste oplossing voor jou is Misskey Drive. Dit maakt het supermakkelijk om jouw bestanden online te delen." - application-authorization: "Geauthoriseerde applicaties" - close: "Sluiten" - do-not-copy-paste: "Gelieve de code hier niet in te geven of te plakken. De account kan gecompromiseerd zijn." - load-more: "Laad meer resultaten" - enter-password: "Voer het wachtwoord in" - 2fa: "Tweestapsverificatie" - customize-home: "Layout aanpassen" - featured-notes: "Uitgelicht" - dark-mode: "Donker thema" - signin: "Aanmelden" - signup: "Registreren" - signout: "Afmelden" - reload-to-apply-the-setting: "Herlaad de pagina om je aanpassingen te bekijken. Wil je de pagina nu herladen?" - fetching-as-ap-object: "Verzenden naar Fediverse" - unfollow-confirm: "Wil stoppen met {name} te volgen?" - delete-confirm: "Ben je zeker dat je dit bericht wil verwijderen?" - signin-required: "Gelieve in te loggen" - notification-type: "Notificatietype" - notification-types: - all: "Alle" - pollVote: "Stemmen" - follow: "Volgend" - receiveFollowRequest: "Volgverzoeken" - reply: "Beantwoorden" - quote: "Bron" - mention: "Vermeldingen" - reaction: "Reactie" - got-it: "Ik snap het!" - customization-tips: - title: "Aanpassingstips" - gotit: "Ik snap het!" - notification: - file-uploaded: "Je bestand is geüpload" - message-from: "Bericht van {}:" - reversi-invited: "Uitgenodigd voor spel" - notified-by: "Bemerkt door: {}" - reply-from: "Antwoord van: {}" - quoted-by: "Geciteerd door: {}" - time: - unknown: "onbekend" - future: "toekomstig" - just_now: "zojuist" - seconds_ago: "{}s geleden" - minutes_ago: "{}m geleden" - hours_ago: "{}u geleden" - days_ago: "{}d geleden" - weeks_ago: "{}week/weken geleden" - months_ago: "{}maand(en) geleden" - years_ago: "{}jaar geleden" - month-and-day: "{day} {month}" - trash: "Prullenbak" - drive: "Drive" - pages: "Pagina's" - messaging: "Gesprekken" - home: "Startpagina" - deck: "Deck" - timeline: "Tijdlijn" - followers: "Volgers" - favorites: "Deze notitie toevoegen aan favorieten" - permissions: - "write:votes": "Stemmen" - post-form: - submit: "Bericht" - reply: "Beantwoorden" - add-visible-user: "Gebruiker toevoegen" - weekday-short: - sunday: "Z" - monday: "M" - tuesday: "D" - wednesday: "W" - thursday: "D" - friday: "V" - saturday: "Z" - reactions: - like: "Leuk" - love: "Geweldig" - laugh: "Grappig" - hmm: "Eh...?" - surprise: "Wauw" - congrats: "Gefeliciteerd!" - angry: "Boos" - confused: "Verward" - pudding: "Pudding" - note-visibility: - home: "Startpagina" - followers: "Volgers" - _settings: - profile: "Je profiel" - notification: "Meldingen" - password: "Wachtwoord" - reactions: "Reactie" - deck-column-align-center: "Centreren" - deck-column-align-left: "Links" - timeline: "Tijdlijn" - navbar-position-left: "Links" - search: "Zoeken" - delete: "Verwijderen" - loading: "Bezig met laden" - update-available: "Er is een nieuwe versie van Misskey beschikbaar: {newer} (de huidige versie is {current}). Herlaad de pagina om de update toe te passen." - my-token-regenerated: "Je sleutel is gegenereerd; je wordt nu uitgelogd." - widgets: - profile: "Je profiel" - activity: "Activiteit" - trends: "Populair" - photo-stream: "Fotostream" - notifications: "Meldingen" - users: "Aanbevolen gebruikers" - server: "Serverinformatie" - you: "Jij" -auth/views/form.vue: - cancel: "Annuleren" -auth/views/index.vue: - loading: "Bezig met laden" -common/views/components/games/reversi/reversi.vue: - matching: - cancel: "Annuleren" -common/views/components/games/reversi/reversi.room.vue: - cancel: "Annuleren" -common/views/components/connect-failed.vue: - title: "Verbinden met server mislukt" - description: "Er is een probleem met je internetverbinding, de server ligt plat of er wordt aan gewerkt. {Probeer} het later opnieuw." - thanks: "Bedankt voor het gebruiken van Misskey." - troubleshoot: "Probleemoplossing" -common/views/components/connect-failed.troubleshooter.vue: - title: "Probleemoplossing" - network: "Netwerkverbinding" - checking-network: "Bezig met controleren van netwerkverbinding" - internet: "Internetverbinding" - checking-internet: "Bezig met controleren van internetverbinding" - server: "Serververbinding" - checking-server: "Bezig met controleren van serververbinding" - finding: "Bezig met vaststellen van probleem" - no-network: "Er is geen internetverbinding" - no-network-desc: "Zorg ervoor dat je verbonden bent met een netwerk." - no-internet: "Er is geen internetverbinding" - no-internet-desc: "Zorg ervoor dat je verbonden bent met het internet." - no-server: "Verbinden met Misskey-server mislukt" - no-server-desc: "De netwerkverbinding van je computer is goed, maar er kan geen verbinding worden gemaakt met de Misskey-server. Het kan dat de server plat ligt of dat eraan wordt gewerkt. Probeer het later opnieuw." - success: "Verbonden met de Misskey-server" - success-desc: "Het verbinden lijkt te lukken. Herlaad de pagina." - flush: "Cache leegmaken" - set-version: "Versie opgeven" -common/views/components/theme.vue: - desc: "Omschrijving" -common/views/components/messaging.vue: - search-user: "Gebruiker zoeken" - you: "Jij" - no-history: "Geen geschiedenis" - user: "Gebruiker" -common/views/components/messaging-room.vue: - no-history: "Er is geen verdere geschiedenis" - new-message: "Nieuw bericht" -common/views/components/messaging-room.form.vue: - input-message-here: "Voer hier je bericht in" - send: "Versturen" - attach-from-local: "Bestanden bijvoegen van je computer" - attach-from-drive: "Bestanden bijvoegen van je Drive" -common/views/components/messaging-room.message.vue: - is-read: "Gelezen" - deleted: "Dit bericht is verwijderd" -common/views/components/nav.vue: - about: "Over" - stats: "Statistieken" - status: "Status" - donors: "Donateurs" - repository: "Broncode" - develop: "Ontwikkelaars" - feedback: "Feedback" -common/views/components/note-menu.vue: - favorite: "Deze notitie toevoegen aan favorieten" - pin: "Vastmaken aan profielpagina" - delete: "Verwijderen" - remote: "Origineel tonen" -common/views/components/poll.vue: - vote-to: "Stemmen op '{}'" - vote-count: "{} stemmen" - vote: "Stemmen" - show-result: "Resultaten tonen" - voted: "Gestemd" -common/views/components/poll-editor.vue: - no-only-one-choice: "Je moet twee of meer keuzes invoeren." - choice-n: "Keuze {}" - remove: "Deze keuze verwijderen" - add: "+ Keuze toevoegen" - destroy: "Deze peiling vernietigen" - day: "Z" -common/views/components/reaction-picker.vue: - choose-reaction: "Kies een reactie" -common/views/components/emoji-picker.vue: - activity: "Activiteit" -common/views/components/signin.vue: - username: "Gebruikersnaam" - password: "Wachtwoord" - token: "Sleutel" - signing-in: "Bezig met inloggen..." -common/views/components/signup.vue: - username: "Gebruikersnaam" - checking: "Bezig met controleren..." - available: "Beschikbaar" - unavailable: "Niet beschikbaar" - error: "Netwerkfout" - invalid-format: "Gebruik alleen letters, cijfers en -." - too-short: "Voer minimaal 1 teken in!" - too-long: "Voer maximaal 20 tekens in." - password: "Wachtwoord" - password-placeholder: "Wij raden aan meer dan 8 tekens te gebruiken." - weak-password: "Zwak" - normal-password: "'t Ken net" - strong-password: "Sterk" - retype: "Opnieuw invoeren" - retype-placeholder: "Wachtwoord bevestigen" - password-matched: "Oké" - password-not-matched: "Komt niet overeen" - recaptcha: "Verifiëren" - create: "Account creëren" - some-error: "Het creëren van een account is mislukt. Probeer het opnieuw." -common/views/components/special-message.vue: - new-year: "Gelukkig nieuwjaar!" - christmas: "Fijne kerstdagen!" -common/views/components/stream-indicator.vue: - connecting: "Bezig met verbinden" - reconnecting: "Bezig met herverbinden" - connected: "Verbonden" -common/views/components/notification-settings.vue: - title: "Meldingen" -common/views/components/github-setting.vue: - detail: "Details bekijken..." -common/views/components/discord-setting.vue: - detail: "Details bekijken..." -common/views/components/uploader.vue: - waiting: "Bezig met wachten" -common/views/components/visibility-chooser.vue: - home: "Startpagina" - followers: "Volgers" -common/views/components/profile-editor.vue: - title: "Je profiel" - name: "Naam" - avatar: "Gebruikersafbeelding" - banner: "Omslagfoto" - unable-to-process: "De operatie kan niet worden voltooid." - export-targets: - following-list: "Volgend" - user-lists: "Lijsten" - enter-password: "Voer het wachtwoord in" -common/views/components/user-list-editor.vue: - users: "Gebruiker" - add-user: "Gebruiker toevoegen" -common/views/components/user-lists.vue: - user-lists: "Lijsten" -common/views/widgets/broadcast.vue: - fetching: "Bezig met ophalen" - no-broadcasts: "Geen uitzendingen" - have-a-nice-day: "Fijne dag!" - next: "Volgende" -common/views/widgets/photo-stream.vue: - title: "Fotostream" - no-photos: "Geen foto's" -common/views/widgets/posts-monitor.vue: - toggle: "Schakelen tussen weergaven" -common/views/widgets/server.vue: - title: "Serverinformatie" - toggle: "Schakelen tussen weergaven" -common/views/pages/follow.vue: - signed-in-as: "Ingelogd als {}" - follow: "Volgend" -desktop: - banner: "Omslagfoto" - avatar: "Gebruikersafbeelding" - unable-to-process: "De operatie kan niet worden voltooid." -desktop/views/components/activity.chart.vue: - total: "Zwart ... totaal" - notes: "Blauw ... notities" - replies: "Rood ... antwoorden" - renotes: "Groen ... gedeelde notities" -desktop/views/components/activity.vue: - title: "Activiteit" - toggle: "Schakelen tussen weergaven" -desktop/views/components/calendar.vue: - prev: "Vorige maand" - next: "Volgende maand" - go: "Klik om te navigeren" -desktop/views/components/choose-file-from-drive-window.vue: - upload: "Bestanden uploaden van je computer" - cancel: "Annuleren" - ok: "Oké" - choose-prompt: "Kies een bestand" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Annuleren" - ok: "Oké" - choose-prompt: "Kies een map" -desktop/views/components/crop-window.vue: - skip: "Bijsnijden overslaan" - cancel: "Annuleren" - ok: "Oké" -desktop/views/components/drive-window.vue: - used: "gebruikt" -desktop/views/components/drive.file.vue: - avatar: "Gebruikersafbeelding" - banner: "Omslagfoto" - contextmenu: - rename: "Naam wijzigen" - copy-url: "URL kopiëren" - download: "Downloaden" - set-as-avatar: "Instellen als gebruikersafbeelding" - set-as-banner: "Instellen als omslagfoto" - open-in-app: "Openen in app" - add-app: "App toevoegen" - rename-file: "Bestandsnaam wijzigen" - input-new-file-name: "Voer een nieuwe naam in" - copied: "Gekopieerd" - copied-url-to-clipboard: "URL gekopieerd naar klembord" -desktop/views/components/drive.folder.vue: - unable-to-process: "De operatie kan niet worden voltooid." - circular-reference-detected: "De bestemmingsmap is een submap van de map die je wilt verplaatsen." - unhandled-error: "Onbekende fout" - contextmenu: - move-to-this-folder: "Verplaatsen naar deze map" - show-in-new-window: "Openen in nieuw venster" - rename: "Naam wijzigen" - rename-folder: "Mapnaam wijzigen" - input-new-folder-name: "Voer een nieuwe naam in" -desktop/views/components/drive.vue: - search: "Zoeken" - empty-draghover: "Welkom!" - empty-drive: "Je schijf is leeg" - empty-drive-description: "Je kunt ook uploaden door te klikken met de rechtermuisknop en te kiezen voor \"Bestand uploaden\" of door een bestand naar dit venster te slepen." - empty-folder: "Deze map is leeg" - unable-to-process: "De operatie kan niet worden voltooid." - circular-reference-detected: "De bestemmingsmap is een submap van de te verplaatsen map." - unhandled-error: "Onbekende fout" - url-upload: "Uploaden via URL" - url-of-file: "URL van het te uploaden bestand" - url-upload-requested: "Uploadverzoek" - may-take-time: "Het kan even duren voordat het uploaden voltooid is." - create-folder: "Map creëren" - folder-name: "Mapnaam" - contextmenu: - create-folder: "Map creëren" - upload: "Bestand uploaden" - url-upload: "Uploaden via URL" -desktop/views/components/followers-window.vue: - followers: "Volgers van {}" -desktop/views/components/followers.vue: - empty: "Het lijkt erop dat je geen volgers hebt." -desktop/views/components/following-window.vue: - following: "Volgend {}" -desktop/views/components/following.vue: - empty: "Je volgt niemand." -desktop/views/components/game-window.vue: - game: "Othello" -desktop/views/components/home.vue: - done: "Versturen" - add-widget: "Widget toevoegen:" - add: "Toevoegen" -desktop/views/input-dialog.vue: - cancel: "Annuleren" - ok: "Oké" -desktop/views/components/note-detail.vue: - private: "(dit bericht is privé)" - location: "Locatie" - add-reaction: "Reactie" -desktop/views/components/note.vue: - reply: "Beantwoorden" - add-reaction: "Reactie" - private: "(dit bericht is privé)" -desktop/views/components/notes.vue: - error: "Laden mislukt." - retry: "Opnieuw proberen" -desktop/views/components/notifications.vue: - empty: "Geen meldingen" -desktop/views/components/post-form.vue: - posted: "Geplaatst!" - replied: "Beantwoord!" - reposted: "Hergeplaatst!" - note-failed: "Noteren mislukt" - reply-failed: "Beantwoorden mislukt" - renote-failed: "Renote mislukt" -desktop/views/components/post-form-window.vue: - note: "Nieuwe notitie" - reply: "Beantwoorden" - attaches: "{} media bijgevoegd" - uploading-media: "Bezig met uploaden van media {}" -desktop/views/components/progress-dialog.vue: - waiting: "Bezig met wachten" -desktop/views/components/renote-form.vue: - quote: "Citeren..." - cancel: "Annuleren" - reposting: "Bezig met herplaatsen..." - success: "Hergeplaatst!" - failure: "Renote mislukt" -desktop/views/components/renote-form-window.vue: - title: "Weet je zeker dat je deze notitie wilt renoten?" -desktop/views/components/settings.2fa.vue: - intro: "Als je verificatie in twee stappen instelt, dan heb je niet alleen een wachtwoord nodig bij het inloggen, maar ook een geregistreerd fysiek apparaat (zoals je smartphone). Dit verhoogt de veiligheid. " - detail: "Details bekijken..." - url: "https://www.google.com/landing/2step/" - caution: "Als je geen toegang meer hebt tot je apparaat, dan kun je niet meer verbinden met Misskey!" - register: "Apparaat registreren" - already-registered: "Er is al een apparaat geregistreerd" - unregister: "Uitschakelen" - unregistered: "Authenticatie in twee stappen is uitgeschakeld." - enter-password: "Voer het wachtwoord in" - authenticator: "Installeer eerst Google Authenticator op je apparaat:" - howtoinstall: "Hoe installeer ik dit?" - token: "Sleutel" - scan: "Scan daarna de QR-code:" - done: "Voer de op je apparaat getoonde sleutel in:" - submit: "Versturen" - success: "Instellen voltooid!" - failed: "Instellen mislukt. Zorg ervoor dat de sleutel juist is." - info: "Vanaf nu moet je ook de op je apparaat getoonde sleutel tonen bij het inloggen op Misskey." -common/views/components/api-settings.vue: - enter-password: "Voer het wachtwoord in" - console: - parameter: "Parameters" - send: "Versturen" -common/views/components/drive-settings.vue: - in-use: "gebruikt" - stats: "Statistieken" - default-upload-folder-name: "Map(pen)" -desktop/views/components/sub-note-content.vue: - private: "(dit bericht is privé)" - poll: "Peilingen" -desktop/views/components/settings.tags.vue: - add: "Toevoegen" -desktop/views/components/timeline.vue: - home: "Startpagina" - local: "Lokaal" - global: "Algemeen" - list: "Lijsten" -desktop/views/components/ui.header.account.vue: - profile: "Je profiel" - lists: "Lijsten" -desktop/views/components/ui.header.nav.vue: - game: "Othello spelen" -desktop/views/components/ui.header.notifications.vue: - title: "Meldingen" -desktop/views/components/ui.header.post.vue: - post: "Nieuw bericht opstellen" -desktop/views/components/ui.header.search.vue: - placeholder: "Zoeken" -desktop/views/components/user-preview.vue: - notes: "Berichten" - following: "Volgend" - followers: "Volgers" -desktop/views/components/users-list.vue: - all: "Alle" - iknow: "die ik ken" - fetching: "Bezig met laden…" -desktop/views/components/users-list-item.vue: - followed: "Volgt jou" -desktop/views/components/window.vue: - popout: "Uitvouwen" - close: "Sluiten" -admin/views/index.vue: - users: "Gebruiker" -admin/views/dashboard.vue: - notes: "Bericht" - drive: "Drive" -admin/views/abuse.vue: - remove-report: "Verwijderen" -admin/views/charts.vue: - notes: "Bericht" - users: "Gebruiker" - drive: "Drive" -admin/views/drive.vue: - origin: - local: "Lokaal" - delete: "Verwijderen" -admin/views/users.vue: - username: "Gebruikersnaam" - users: - title: "Gebruiker" - state: - all: "Alle" - origin: - local: "Lokaal" -admin/views/emoji.vue: - add-emoji: - add: "Toevoegen" - emojis: - remove: "Verwijderen" -admin/views/announcements.vue: - remove: "Verwijderen" - add: "Toevoegen" -admin/views/federation.vue: - notes: "Bericht" - users: "Gebruiker" - followers: "Volgers" - status: "Status" - states: - all: "Alle" -desktop/views/pages/welcome.vue: - timeline: "Tijdlijn" -desktop/views/pages/note.vue: - prev: "Vorige notitie" - next: "Volgende notitie" -desktop/views/pages/selectdrive.vue: - title: "Bestand(en) kiezen" - ok: "Oké" - cancel: "Annuleren" - upload: "Bestanden uploaden van je PC" -desktop/views/pages/user-list.users.vue: - users: "Gebruiker" - add-user: "Gebruiker toevoegen" - username: "Gebruikersnaam" -desktop/views/pages/user/user.followers-you-know.vue: - title: "Volgers die je kent" - loading: "Bezig met laden" - no-users: "Geen gebruikers" -desktop/views/pages/user/user.friends.vue: - title: "Frequent beantwoord" - loading: "Bezig met laden" - no-users: "Geen gebruikers" -desktop/views/pages/user/user.photos.vue: - title: "Foto's" - loading: "Bezig met laden" - no-photos: "Geen foto's" -desktop/views/pages/user/user.header.vue: - posts: "Bericht" - following: "Volgend" - followers: "Volgers" - month: "M" - day: "Z" - follows-you: "Volgt jou" -desktop/views/pages/user/user.timeline.vue: - default: "Berichten" - with-replies: "Berichten en antwoorden" - with-media: "Media" -desktop/views/widgets/notifications.vue: - title: "Meldingen" -desktop/views/widgets/polls.vue: - title: "Peilingen" - refresh: "Anderen tonen" - nothing: "Niks" -desktop/views/widgets/post-form.vue: - title: "Bericht" - note: "Bericht" -desktop/views/widgets/profile.vue: - update-banner: "Klik om je omslagfoto te wijzigen" - update-avatar: "Klik om je gebruikersafbeelding te wijzigen" -desktop/views/widgets/trends.vue: - title: "Populair" - refresh: "Anderen tonen" - nothing: "Niks" -desktop/views/widgets/users.vue: - title: "Aanbevolen gebruikers" - refresh: "Anderen tonen" - no-one: "Niemand" -mobile/views/components/drive.vue: - used: "gebruikt" - folder-count: "Map(pen)" - count-separator: ", " - file-count: "Bestand(en)" - nothing-in-drive: "Niks" - folder-is-empty: "Deze map is leeg" - folder-name: "Mapnaam" - url-prompt: "URL van het te uploaden bestand" -mobile/views/components/drive-file-chooser.vue: - select-file: "Kies een bestand" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "Kies een map" -mobile/views/components/drive.file-detail.vue: - download: "Downloaden" - rename: "Naam wijzigen" - move: "Verplaatsen" - hash: "Hash (md5)" -common/views/components/follow-button.vue: - follow: "Volgend" -mobile/views/components/note.vue: - private: "(dit bericht is privé)" - location: "Locatie" -mobile/views/components/note-detail.vue: - reply: "Beantwoorden" - reaction: "Reactie" - private: "(dit bericht is privé)" - location: "Locatie" -mobile/views/components/notifications.vue: - empty: "Geen meldingen" -mobile/views/components/sub-note-content.vue: - private: "(dit bericht is privé)" - media-count: "{} media" - poll: "Peiling" -mobile/views/components/ui.nav.vue: - timeline: "Tijdlijn" - notifications: "Meldingen" - search: "Zoeken" - user-lists: "Lijsten" - game: "Othello spelen" - about: "Over Misskey" -mobile/views/pages/drive.vue: - contextmenu: - upload: "Bestand uploaden" - create-folder: "Map creëren" -mobile/views/pages/home.vue: - home: "Startpagina" - local: "Lokaal" - global: "Algemeen" -mobile/views/pages/widgets.vue: - add-widget: "Toevoegen" - customization-tips: "Aanpassingstips" -mobile/views/pages/widgets/activity.vue: - activity: "Activiteit" -mobile/views/pages/note.vue: - title: "Bericht" - prev: "Vorige notitie" - next: "Volgende notitie" -mobile/views/pages/games/reversi.vue: - reversi: "Othello" -mobile/views/pages/search.vue: - search: "Zoeken" -mobile/views/pages/selectdrive.vue: - select-file: "Kies een bestand" -mobile/views/pages/notifications.vue: - notifications: "Meldingen" -mobile/views/pages/settings.vue: - signed-in-as: "Ingelogd als {}" -mobile/views/pages/user.vue: - follows-you: "Volgt jou" - following: "Volgend" - followers: "Volgers" - notes: "Berichten" - overview: "Overzicht" - timeline: "Tijdlijn" - media: "Media" -mobile/views/pages/user/home.vue: - recent-notes: "Recente notities" - images: "Afbeeldingen" - activity: "Activiteit" - keywords: "Sleutelwoorden" - domains: "Domeinnamen" - frequently-replied-users: "Frequent beantwoord" - followers-you-know: "Volgers die je kent" - last-used-at: "Laatst actief" -mobile/views/pages/user/home.photos.vue: - no-photos: "Geen foto's" -deck: - home: "Startpagina" - local: "Lokaal" - global: "Algemeen" - notifications: "Meldingen" - list: "Lijsten" - rename: "Naam wijzigen" -deck/deck.user-column.vue: - follows-you: "Volgt jou" - posts: "Bericht" - following: "Volgend" - followers: "Volgers" - images: "Afbeeldingen" - activity: "Activiteit" - timeline: "Tijdlijn" -docs: - edit-this-page-on-github: "Heb je een fout ontdekt of wil je bijdragen aan de documentatie? " - edit-this-page-on-github-link: "Bewerk deze pagina op GitHub!" -pages: - pin-this-page: "Vastmaken aan profielpagina" - like: "Leuk" - blocks: - image: "Afbeeldingen" - script: - categories: - list: "Lijsten" - blocks: - _join: - arg1: "Lijsten" - _randomPick: - arg1: "Lijsten" - _dailyRandomPick: - arg1: "Lijsten" - _seedRandomPick: - arg2: "Lijsten" - _pick: - arg1: "Lijsten" - _listLen: - arg1: "Lijsten" - types: - array: "Lijsten" -room: - translate: "Verplaatsen" - furnitures: - moon: "Maan" - bin: "Prullenbak" diff --git a/locales/no-NO.yml b/locales/no-NO.yml deleted file mode 100644 index 2af76bf7d94b276c08a7438af6755db983dded2f..0000000000000000000000000000000000000000 --- a/locales/no-NO.yml +++ /dev/null @@ -1,528 +0,0 @@ ---- -meta: - lang: "Norsk BokmÃ¥l" -common: - misskey: "En â av fediverse" - about-title: "En â av fediverse" - about: "Takk for at du fant Misskey. Misskey er en <b>desentralisert mikroblogging platform</b> født pÃ¥ jorden. Siden den eksisterer sammen med Fediverset (Et univers hvor forskjellige sosiale media-plattformer blir organisert), sÃ¥ blir den gjensidig tilknyttet med andre sosiale media-plattformer. Hvorfor ikke ta en pause fra kjas og mas fra storbyen og hoppe inn i en ny type internett?" - intro: - title: "Hva er Misskey?" - features: "Funksjoner" - rich-contents: "Innlegg" - drive: "Disk" - close: "Lukk" - notification-types: - all: "Alle" - follow: "Følger" - reply: "Svar" - got-it: "Skjønner!" - notification: - file-uploaded: "Filen ble lastet opp!" - message-from: "Melding fra {}:" - reversi-invited: "Invitert til et spill" - reversi-invited-by: "Invitert av {}:" - notified-by: "Invitert av {}:" - reply-from: "Svar fra {}:" - quoted-by: "Sitert av {}:" - time: - unknown: "ukjent" - future: "fremtidig" - just_now: "akkurat nÃ¥" - seconds_ago: "{} sekunder siden" - minutes_ago: "{} minutter siden" - hours_ago: "{} t siden" - days_ago: "{} d siden" - weeks_ago: "{} uke(r) siden" - months_ago: "{} mÃ¥ned(er) siden" - years_ago: "{} Ã¥r siden" - month-and-day: "{day}/{month}" - trash: "Papirkurv" - drive: "Disk" - home: "Hjem" - followers: "Følgere" - favorites: "Merket som favoritt" - permissions: - "write:votes": "Stem" - post-form: - submit: "Innlegg" - reply: "Svar" - error: "Feil" - weekday-short: - sunday: "S" - monday: "M" - tuesday: "T" - wednesday: "O" - thursday: "T" - friday: "F" - saturday: "L" - weekday: - sunday: "Søndag" - monday: "Mandag" - tuesday: "Tirsdag" - wednesday: "Onsdag" - thursday: "Torsdag" - friday: "Fredag" - saturday: "Lørdag" - reactions: - like: "Lik" - love: "Elsk" - laugh: "Le" - hmm: "Hmm…?" - surprise: "Wow" - congrats: "Gratulerer!" - angry: "Sint" - confused: "Forvirret" - rip: "RIP" - pudding: "Pudding" - note-visibility: - public: "Offentlig" - home: "Hjem" - followers: "Følgere" - specified: "Direkte" - _settings: - notification: "Notifikasjon" - password: "Passord" - save: "Lagre" - search: "Søk" - delete: "Slett" - loading: "Laster inn..." - update-available: "En ny versjon av Misskey er nÃ¥ tilgjengelig ({newer}, nÃ¥værende versjon er {current}). Last inn siden igjen for at oppdateringen skal tre i kraft." - my-token-regenerated: "Ditt synbol har blitt generert. Du vil nÃ¥ bli utlogget." - reversi: - black: "Sort" - white: "Hvit" - total: "Totalt" - widgets: - calendar: "Kalender" - memo: "Notis" - trends: "Populært nÃ¥" - version: "Versjon" - notifications: "Notifikasjon" - tips: "Tips" - you: "Du" -auth/views/form.vue: - cancel: "Avbryt" -auth/views/index.vue: - loading: "Laster inn..." -common/views/components/games/reversi/reversi.vue: - matching: - cancel: "Avbryt" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Gi opp" -common/views/components/games/reversi/reversi.index.vue: - invite: "Inviter" - rule: "Slik spiller du" - mode-invite: "Inviter" - game-state: - ended: "Ferdig" - playing: "PÃ¥gÃ¥r" -common/views/components/games/reversi/reversi.room.vue: - random: "Tilfeldig" - black-is: "Sort er {}" - rules: "Regler" - waiting-for-both: "Venter pÃ¥ deg" - cancel: "Avbryt" - ready: "Klar" - cancel-ready: "Avbryt \"Klar\"" -common/views/components/connect-failed.vue: - title: "Kunne ikke koble til tjeneren." - description: "Det er enten et problem med internettilknytningen din, eller sÃ¥ har tjeneren blitt tatt ned for vedlikehold. {Prøv igjen} senere." -common/views/components/media-banner.vue: - sensitive: "Sensitivt innhold" -common/views/components/theme.vue: - text-color: "Tekstfarge" - base-theme-dark: "Mørk" - theme-name: "Tema navn" - author: "Forfatter" - desc: "Beskrivelse" -common/views/components/cw-button.vue: - hide: "Skjul" -common/views/components/messaging.vue: - you: "Du" - user: "Bruker" -common/views/components/messaging-room.form.vue: - send: "Send" -common/views/components/messaging-room.message.vue: - is-read: "Lest" -common/views/components/nav.vue: - stats: "Statistikk" - status: "Status" - wiki: "Wiki" - donors: "Donatorer" - repository: "Kodelager" - develop: "Utviklere" -common/views/components/note-menu.vue: - detail: "Detaljer" - favorite: "Merket som favoritt" - pin: "Fest til profilen din" - delete: "Slett" -common/views/components/poll.vue: - vote-count: "{} stemmer" - vote: "Stem" - show-result: "Vis resultater" - voted: "Stemt" -common/views/components/poll-editor.vue: - choice-n: "Valg {}" - day: "S" -common/views/components/signin.vue: - username: "Brukernavn" - password: "Passord" - token: "Token" - or: "Eller" -common/views/components/signup.vue: - username: "Brukernavn" - error: "Nettverksfeil" - password: "Passord" - retype: "Gjenta" - recaptcha: "Captcha" -common/views/components/stream-indicator.vue: - connecting: "Tilkobler" - reconnecting: "Kobler til pÃ¥ nytt" - connected: "Tilkoblet" -common/views/components/notification-settings.vue: - title: "Notifikasjon" -common/views/components/github-setting.vue: - detail: "Detaljer..." -common/views/components/discord-setting.vue: - detail: "Detaljer..." -common/views/components/uploader.vue: - waiting: "Venter" -common/views/components/visibility-chooser.vue: - public: "Offentlig" - home: "Hjem" - followers: "Følgere" - specified: "Direkte" -common/views/components/profile-editor.vue: - name: "Navn" - avatar: "Avatar" - banner: "Banner" - save: "Lagre" - export-targets: - following-list: "Følger" - user-lists: "Lister" -common/views/components/user-list-editor.vue: - users: "Bruker" -common/views/components/user-group-editor.vue: - invite: "Inviter" -common/views/components/user-lists.vue: - user-lists: "Lister" - list-name: "Liste navn" -common/views/components/user-groups.vue: - invites: "Inviter" -common/views/widgets/broadcast.vue: - fetching: "Henter" - next: "Neste" -common/views/widgets/calendar.vue: - year: "Ã…r {}" - month: "MÃ¥ned {}" - day: "Dag {}" - today: "I dag:" - this-month: "Denne mÃ¥neden:" - this-year: "Dette Ã¥ret:" -common/views/widgets/memo.vue: - title: "Notis" - save: "Lagre" -common/views/pages/follow.vue: - follow: "Følg" -desktop: - banner: "Banner" - avatar: "Avatar" -desktop/views/components/calendar.vue: - prev: "Forrige mÃ¥ned" - next: "Neste mÃ¥ned" -desktop/views/components/choose-file-from-drive-window.vue: - cancel: "Avbryt" - ok: "Ok" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Avbryt" - ok: "Ok" -desktop/views/components/crop-window.vue: - cancel: "Avbryt" - ok: "Ok" -desktop/views/components/drive-window.vue: - used: "brukt" -desktop/views/components/drive.file.vue: - avatar: "Avatar" - banner: "Banner" - nsfw: "NSFW" - contextmenu: - rename: "Endre navn" - copied: "Kopiert" -desktop/views/components/drive.folder.vue: - contextmenu: - rename: "Endre navn" -desktop/views/components/drive.vue: - search: "Søk" -desktop/views/components/media-video.vue: - sensitive: "Innholdet er NSFW" -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "Fullført" - add: "Legg til" -desktop/views/input-dialog.vue: - cancel: "Avbryt" - ok: "Ok" -desktop/views/components/note-detail.vue: - location: "Lokasjon" -desktop/views/components/note.vue: - reply: "Svar" - detail: "Detaljer" -desktop/views/components/notes.vue: - retry: "Prøv pÃ¥ nytt" -desktop/views/components/post-form-window.vue: - note: "Nytt innlegg" - reply: "Svar" -desktop/views/components/progress-dialog.vue: - waiting: "Venter" -desktop/views/components/renote-form.vue: - cancel: "Avbryt" -desktop/views/components/settings.2fa.vue: - detail: "Detaljer..." - unregister: "Avregistrer" - token: "Token" - submit: "Send" -common/views/components/media-image.vue: - sensitive: "Innholdet er NSFW" -common/views/components/api-settings.vue: - console: - parameter: "Parametere" - send: "Send" -common/views/components/drive-settings.vue: - in-use: "brukt" - stats: "Statistikk" - default-upload-folder-name: "Mappe(r)" -common/views/components/mute-and-block.vue: - save: "Lagre" -desktop/views/components/settings.tags.vue: - add: "Legg til" - save: "Lagre" -desktop/views/components/timeline.vue: - home: "Hjem" - local: "Lokalt" - global: "Globalt" - list: "Lister" - list-name: "Liste navn" -desktop/views/components/ui.header.vue: - adjective: "-san" -desktop/views/components/ui.header.account.vue: - lists: "Lister" - admin: "Admin" -desktop/views/components/ui.header.nav.vue: - game: "Spill" -desktop/views/components/ui.header.notifications.vue: - title: "Notifikasjon" -desktop/views/components/ui.header.post.vue: - post: "Skriv nytt innlegg" -desktop/views/components/ui.header.search.vue: - placeholder: "Søk" -desktop/views/components/user-preview.vue: - notes: "Innlegg" - following: "Følger" - followers: "Følgere" -desktop/views/components/users-list.vue: - all: "Alle" - iknow: "Du kjenner" -desktop/views/components/window.vue: - close: "Lukk" -admin/views/index.vue: - users: "Bruker" - announcements: "Kunngjøringer" -admin/views/dashboard.vue: - notes: "Innlegg" - drive: "Disk" -admin/views/logs.vue: - levels: - info: "Informasjon" - error: "Feil" -admin/views/abuse.vue: - details: "Detaljer" - remove-report: "Slett" -admin/views/instance.vue: - invite: "Inviter" - save: "Lagre" -admin/views/charts.vue: - notes: "Innlegg" - users: "Bruker" - drive: "Disk" -admin/views/drive.vue: - origin: - local: "Lokalt" - delete: "Slett" -admin/views/users.vue: - username: "Brukernavn" - users: - title: "Bruker" - state: - all: "Alle" - origin: - local: "Lokalt" -admin/views/moderators.vue: - logs: - info: "Informasjon" -admin/views/emoji.vue: - add-emoji: - add: "Legg til" - emojis: - remove: "Slett" -admin/views/announcements.vue: - announcements: "Kunngjøringer" - save: "Lagre" - remove: "Slett" - add: "Legg til" -admin/views/federation.vue: - notes: "Innlegg" - users: "Bruker" - followers: "Følgere" - status: "Status" - states: - all: "Alle" - save: "Lagre" -desktop/views/pages/welcome.vue: - announcements: "Kunngjøringer" - info: "Informasjon" -desktop/views/pages/note.vue: - prev: "Forrige innlegg" - next: "Neste innlegg" -desktop/views/pages/selectdrive.vue: - ok: "Ok" - cancel: "Avbryt" -desktop/views/pages/user-list.users.vue: - users: "Bruker" - username: "Brukernavn" -desktop/views/pages/user/user.followers-you-know.vue: - loading: "Laster inn" -desktop/views/pages/user/user.friends.vue: - loading: "Laster inn" -desktop/views/pages/user/user.photos.vue: - title: "Bilder" - loading: "Laster inn" -desktop/views/pages/user/user.header.vue: - posts: "Innlegg" - following: "Følger" - followers: "Følgere" - month: "M" - day: "S" -desktop/views/pages/user/user.timeline.vue: - default: "Innlegg" - with-replies: "Innlegg og svar" - with-media: "Media" -desktop/views/widgets/notifications.vue: - title: "Notifikasjon" -desktop/views/widgets/polls.vue: - refresh: "Oppdater" -desktop/views/widgets/post-form.vue: - title: "Innlegg" - note: "Innlegg" -desktop/views/widgets/trends.vue: - title: "Populært nÃ¥" - refresh: "Oppdater" -desktop/views/widgets/users.vue: - refresh: "Oppdater" - no-one: "Ingen" -mobile/views/components/drive.vue: - used: "brukt" - folder-count: "Mappe(r)" - count-separator: "," - file-count: "Fil(er)" -mobile/views/components/drive.file.vue: - nsfw: "NSFW" -mobile/views/components/drive.file-detail.vue: - rename: "Endre navn" - move: "Flytt" - exif: "EXIF" - nsfw: "NSFW" -mobile/views/components/media-video.vue: - sensitive: "Innholdet er NSFW" -common/views/components/follow-button.vue: - follow: "Følger" -mobile/views/components/note.vue: - location: "Lokasjon" -mobile/views/components/note-detail.vue: - reply: "Svar" - location: "Lokasjon" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "katt" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "katt" -mobile/views/components/ui.header.vue: - adjective: "Mr." -mobile/views/components/ui.nav.vue: - notifications: "Notifikasjon" - search: "Søk" - user-lists: "Lister" - game: "Spill" - admin: "Admin" -mobile/views/pages/home.vue: - home: "Hjem" - local: "Lokalt" - global: "Globalt" -mobile/views/pages/widgets.vue: - add-widget: "Legg til" -mobile/views/pages/note.vue: - title: "Innlegg" - prev: "Forrige innlegg" - next: "Neste innlegg" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "Søk" -mobile/views/pages/notifications.vue: - notifications: "Notifikasjon" -mobile/views/pages/user.vue: - following: "Følger" - followers: "Følgere" - notes: "Innlegg" - overview: "Oversikt" - media: "Media" -mobile/views/pages/user/home.vue: - recent-notes: "Nylige innlegg" - images: "Bilder" - keywords: "Nøkkelord" -deck: - home: "Hjem" - local: "Lokalt" - global: "Globalt" - notifications: "Notifikasjon" - list: "Lister" - rename: "Endre navn" -deck/deck.user-column.vue: - posts: "Innlegg" - following: "Følger" - followers: "Følgere" - images: "Bilder" -pages: - pin-this-page: "Fest til profilen din" - like: "Lik" - blocks: - image: "Bilder" - script: - categories: - random: "Tilfeldig" - list: "Lister" - blocks: - _join: - arg1: "Lister" - random: "Tilfeldig" - _randomPick: - arg1: "Lister" - _dailyRandomPick: - arg1: "Lister" - _seedRandomPick: - arg2: "Lister" - _pick: - arg1: "Lister" - _listLen: - arg1: "Lister" - types: - array: "Lister" -room: - translate: "Flytt" - save: "Lagre" - furnitures: - moon: "MÃ¥ne" - bin: "Papirkurv" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml deleted file mode 100644 index 3600fedd264c94e6f58459921b2dd082303cc1e3..0000000000000000000000000000000000000000 --- a/locales/pl-PL.yml +++ /dev/null @@ -1,1268 +0,0 @@ ---- -meta: - lang: "jÄ™zyk polski" -common: - misskey: "â Fediwersum" - about-title: "â Fediwersum" - about: "DziÄ™kujemy za znalezienie Misskey. Misskey jest <b>zdecentralizowanÄ… platformÄ… mikroblogowÄ…</b> powstaÅ‚Ä… na Ziemi. Ponieważ dziaÅ‚a ona w Fediwersum (uniwersum, w którego skÅ‚ad wchodzi wiele sieci spoÅ‚ecznoÅ›ciowych), jest ona poÅ‚Ä…czona z innymi platformami spoÅ‚ecznoÅ›ciowymi. Spróbujesz odpocząć od zatÅ‚oczoneo miasta i zanurzyć siÄ™ w nowym Internecie?" - intro: - title: "Czym jest Misskey?" - features: "Funkcje" - rich-contents: "Wpis" - rich-contents-desc: "Po prostu opublikuj swój pomysÅ‚, gorÄ…ce tematy i wszystko, co chcesz udostÄ™pnić. Możesz ozdobić swoje sÅ‚owa, doÅ‚Ä…czyć swoje ulubione zdjÄ™cia, wysÅ‚ać pliki, w tym filmy i utworzyć ankietÄ™ - to sÄ… rzeczy, które możesz zrobić w Misskey!" - reaction: "Reakcja" - ui: "Interfejs" - drive: "Dysk" - drive-desc: "Chcesz opublikować zdjÄ™cie, które już przesÅ‚aÅ‚eÅ›? Chcesz uporzÄ…dkować, nazwać i utworzyć folder dla przesÅ‚anych plików? Dysk Misskey to najlepsze rozwiÄ…zanie dla Ciebie. Bardzo Å‚atwo udostÄ™pniać swoje pliki online." - application-authorization: "Współpraca aplikacji" - close: "Zamknij" - load-more: "ZaÅ‚aduj wiÄ™cej" - enter-password: "Wprowadź HasÅ‚o" - 2fa: "Uwierzytelnienie dwuetapowe" - customize-home: "Dostosuj stronÄ™ głównÄ…" - featured-notes: "Wyróżnienia" - dark-mode: "Tryb ciemny" - signin: "Zaloguj siÄ™" - signup: "Rejestracja" - signout: "Wyloguj siÄ™" - delete-confirm: "Czy na pewno chcesz usunąć ten wpis?" - notification-type: "Typy powiadomieÅ„" - notification-types: - all: "Wszyscy" - pollVote: "GÅ‚osy" - follow: "Åšledzeni" - receiveFollowRequest: "ProÅ›by o Å›ledzenie" - reply: "Odpowiedzi" - quote: "Cytat" - renote: "UdostÄ™pnij" - mention: "Wzmianki" - reaction: "Reakcje" - got-it: "Rozumiem!" - customization-tips: - title: "Wskazówki o dostosowywaniu" - gotit: "Rozumiem!" - notification: - file-uploaded: "WysÅ‚ano plik!" - message-from: "Wiadomość od {}:" - reversi-invited: "Zaproszono do gry" - reversi-invited-by: "Zaproszono przez {}:" - notified-by: "Powiadomiono przez {}:" - reply-from: "Odpowiedź od {}:" - quoted-by: "Zacytowano przez {}:" - time: - unknown: "nieznany" - future: "w przyszÅ‚oÅ›ci" - just_now: "teraz" - seconds_ago: "{} sek. temu" - minutes_ago: "{} min. temu" - hours_ago: "{} godz. temu" - days_ago: "{} dni temu" - weeks_ago: "{} tyg. temu" - months_ago: "{} mies. temu" - years_ago: "{} lat temu" - month-and-day: "{month}-{day}" - trash: "Kosz" - drive: "Dysk" - pages: "Strony" - messaging: "Rozmowy" - home: "Strona główna" - deck: "Tablice" - timeline: "OÅ› czasu" - explore: "Znajdź" - following: "Åšledzisz" - followers: "ÅšledzÄ…cy" - favorites: "Moje ulubione" - permissions: - "read:drive": "WyÅ›wietl dysk" - "read:messaging": "Zobacz konwersacjÄ™" - "write:votes": "ZagÅ‚osuj" - empty-timeline-info: - explore: "Poznaj" - post-form: - hide-contents: "Ukryj zawartość" - reply-placeholder: "Odpowiedź na ten wpis…" - quote-placeholder: "Zacytuj ten wpis…" - quote-attached: "Z cytatem" - submit: "Wpis" - reply: "Odpowiedz" - renote: "UdostÄ™pnij" - posting: "WysyÅ‚anie" - attach-media-from-local: "ZaÅ‚Ä…cz zawartość multimedialnÄ… z komputera" - attach-media-from-drive: "ZaÅ‚Ä…cz zawartość multimedialnÄ… z dysku" - create-poll: "Utwórz ankietÄ™" - text-remain: "pozostaÅ‚e znaki: {}" - recent-tags: "Ostatnie" - visibility: "Widoczność" - error: "BÅ‚Ä…d" - enter-username: "Wprowadź nazwÄ™ użytkownika" - specified-recipient: "Adresat" - add-visible-user: "Dodaj użytkownika" - username-prompt: "Wprowadź nazwÄ™ użytkownika" - enter-file-name: "Wprowadź nazwÄ™ pliku" - weekday-short: - sunday: "N" - monday: "Pn" - tuesday: "W" - wednesday: "Åš" - thursday: "C" - friday: "P" - saturday: "S" - weekday: - sunday: "Niedziela" - monday: "PoniedziaÅ‚ek" - tuesday: "Wtorek" - wednesday: "Åšroda" - thursday: "Czwartek" - friday: "PiÄ…tek" - saturday: "Sobota" - reactions: - like: "LubiÄ™" - love: "Kocham" - laugh: "Åšmieszne" - hmm: "Hmm…?" - surprise: "Wow" - congrats: "GratulujÄ™!" - angry: "WÅ›ciekÅ‚y" - confused: "Zmieszany" - rip: "RIP" - pudding: "Pudding" - note-visibility: - public: "Publiczny" - home: "Lokalny" - home-desc: "Widoczny tylko na tej instancji" - followers: "Dla Å›ledzÄ…cych" - followers-desc: "Widoczny tylko dla osób, które CiÄ™ Å›ledzÄ…" - specified: "BezpoÅ›redni" - specified-desc: "Tylko dla okreÅ›lonych użytkowników" - local-public: "Publiczny (tylko lokalnie)" - local-followers: "Dla Å›ledzÄ…cych (tylko lokalnie)" - note-placeholders: - a: "Co robisz?" - b: "Co siÄ™ wydarzyÅ‚o?" - c: "Co Ci chodzi po gÅ‚owie?" - d: "Czy masz coÅ› do powiedzenia?" - e: "Napisz coÅ› tutaj!" - f: "Czekamy, aż coÅ› napiszesz." - settings: "Ustawienia" - _settings: - profile: "Profil" - notification: "Powiadomienia" - apps: "Aplikacje" - tags: "Hashtagi" - mute-and-block: "Wycisz / Zablokuj" - blocking: "Blokowanie" - security: "Zabezpieczenia" - signin: "Historia logowania" - password: "HasÅ‚o" - other: "Inne" - appearance: "WyglÄ…d" - behavior: "Zachowanie" - reactions: "Reakcja" - fetch-on-scroll: "Automatycznie Å‚aduj po przeciÄ…gniÄ™ciu w dół" - note-visibility: "Widoczność wpisów" - remember-note-visibility: "ZapamiÄ™taj widoczność wpisów" - web-search-engine: "Wyszukiwarka internetowa" - web-search-engine-desc: "Wzór: https://www.google.com/?#q={{query}}" - paste: "Wklej" - line-width: "SzerokoÅ›ci linii" - line-width-thin: "Cienka" - line-width-normal: "Normalna" - line-width-thick: "Gruba" - font-size: "Rozmiar tekstu" - font-size-x-small: "MaÅ‚e" - font-size-medium: "Normalna" - font-size-large: "TrochÄ™ duży" - font-size-x-large: "Duży" - deck-column-align-center: "Po Å›rodku" - deck-column-align-left: "Z lewej" - deck-column-align-flexible: "Elastyczne" - deck-column-width: "Szerokość kolumn w talii" - deck-column-width-narrow: "WÄ…ska" - deck-column-width-narrower: "TrochÄ™ wÄ…ska" - deck-column-width-normal: "Normalna" - deck-column-width-wider: "TrochÄ™ szerokie" - deck-column-width-wide: "Szeroka" - wallpaper: "Tapeta" - choose-wallpaper: "Wybierz tapetÄ™" - timeline: "OÅ› czasu" - sound: "DźwiÄ™k" - volume: "GÅ‚oÅ›ność" - test: "Test" - update: "Aktualizacja Misskey" - version: "Wersja:" - do-update: "Sprawdź dostÄ™pność nowych aktualizacji" - navbar-position-left: "Z lewej" - save: "Zapisz" - saved: "Zapisano" - preview: "Pokaż podglÄ…d" - search: "Szukaj" - delete: "UsuÅ„" - loading: "Åadowanie" - ok: "Możesz OK" - cancel: "Anuluj" - update-available-title: "Aktualizacja jest dostÄ™pna" - update-available: "Nowa wersja Misskey jest dostÄ™pna ({newer}, obecna to {current}). OdÅ›wież stronÄ™, aby zastosować aktualizacjÄ™." - my-token-regenerated: "Twój token zostaÅ‚ wygenerowany. Zostaniesz wylogowany." - hide-password: "Ukryj hasÅ‚o" - show-password: "Pokaż hasÅ‚o" - enter-username: "Wprowadź nazwÄ™ użytkownika" - view-on-remote: "Dla dopeÅ‚nienia, zobacz to zdalnie." - renoted-by: "{user} udostÄ™pniÅ‚(a)" - error: - title: "CoÅ› poszÅ‚o nie tak" - retry: "Ponów próbÄ™" - reversi: - drawn: "Remis" - my-turn: "Twoja kolej" - opponent-turn: "Kolej na przeciwnika" - won: "{name} wygraÅ‚(a)" - black: "Czarny" - white: "BiaÅ‚y" - total: "ÅÄ…cznie" - widgets: - analog-clock: "Zegar analogowy" - profile: "Profil" - calendar: "Kalendarz" - timemachine: "Kalendarz (wehikuÅ‚ czasu)" - activity: "Aktywność" - rss: "Czytnik RSS" - memo: "Notatka" - trends: "Na czasie" - photo-stream: "Photostream" - posts-monitor: "Wykres wpisów" - slideshow: "Pokaz slajdów" - version: "Wersja" - broadcast: "Transmisja" - notifications: "Powiadomienia" - users: "Polecani użytkownicy" - polls: "Ankiety" - post-form: "Formularz tworzenia" - server: "Informacje o serwerze" - nav: "Nawigacja" - tips: "Wskazówki" - hashtags: "Hashtagi" - you: "Ty" -auth/views/form.vue: - permission-ask: "Ta aplikacja wymaga nastÄ™pujÄ…cych uprawnieÅ„:" - cancel: "Anuluj" - accept: "Przyznaj dostÄ™p." -auth/views/index.vue: - loading: "Åadowanie" - denied: "Odrzucono uwierzytelnianie aplikacji." - denied-paragraph: "Ta aplikacja nie uzyska dostÄ™pu do Twojego konta." - already-authorized: "Ta aplikacja zostaÅ‚a już uwierzytelniona." - callback-url: "Powracam do aplikacji." - please-go-back: "Wróć do aplikacji." - error: "Sesja nie istnieje." - sign-in: "ProszÄ™ zalogować siÄ™." -common/views/pages/explore.vue: - popular-users: "Popularni użytkownicy" - popular-tags: "Popularne tagi" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "Oczekiwanie na {}" - cancel: "Anuluj" -common/views/components/games/reversi/reversi.game.vue: - surrender: "Poddaj siÄ™" - surrendered: "Przez poddanie siÄ™" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey Reversi" - sub-title: "Zagraj w Reversi ze znajomymi!" - invite: "ZaproÅ›" - rule: "Jak grać" - mode-invite: "ZaproÅ›" - mode-invite-desc: "ZaproÅ› użytkownika do gry." - invitations: "OtrzymaÅ‚eÅ›(-aÅ›) zaproszenie!" - my-games: "Moje gry" - all-games: "Wszystkie gry" - enter-username: "Wprowadź nazwÄ™ użytkownika" - game-state: - ended: "ZakoÅ„czono" - playing: "W trakcie" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "Ustawienia gry" - choose-map: "Wybierz mapÄ™" - random: "Losowy" - black-or-white: "Czarny/biaÅ‚y" - rules: "Zasady" - settings-of-the-bot: "Ustawienia bota" - this-game-is-started-soon: "Gra rozpocznie siÄ™ wkrótce" - waiting-for-both: "Oczekiwanie na Ciebie" - cancel: "Anuluj" - ready: "Gotowy" - cancel-ready: "Cofnij „gotowyâ€" -common/views/components/connect-failed.vue: - title: "Nie udaÅ‚o siÄ™ poÅ‚Ä…czyć z serwerem" - description: "WystÄ…piÅ‚ problem z Twoim poÅ‚Ä…czeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce." - thanks: "DziÄ™kujemy za korzystanie z Misskey." - troubleshoot: "RozwiÄ…zywanie problemów" -common/views/components/connect-failed.troubleshooter.vue: - title: "RozwiÄ…zywanie problemów" - network: "PoÅ‚Ä…czenie z sieciÄ…" - checking-network: "Sprawdzanie poÅ‚Ä…czenia sieciowego" - internet: "PoÅ‚Ä…czenie z Internetem" - checking-internet: "Sprawdzanie poÅ‚Ä…czenia z Internetem" - server: "PoÅ‚Ä…czenie z serwerem" - checking-server: "Sprawdzanie poÅ‚Ä…czenia z serwerem" - finding: "Wyszukiwanie problemu" - no-network: "Brak poÅ‚Ä…czenia z sieciÄ…" - no-network-desc: "Upewnij siÄ™, że jesteÅ› poÅ‚Ä…czony z sieciÄ…." - no-internet: "Brak poÅ‚Ä…czenia z Internetem" - no-internet-desc: "Upewnij siÄ™, że jesteÅ› poÅ‚Ä…czony z Internetem." - no-server: "Nie udaÅ‚o siÄ™ poÅ‚Ä…czyć z serwerem" - no-server-desc: "PoÅ‚Ä…czenie sieciowe dziaÅ‚a, ale nie udaÅ‚o siÄ™ poÅ‚Ä…czyć z serwerem Misskey. Możliwe że serwer nie dziaÅ‚a lub trwajÄ… prace konserwacyjne, spróbuj ponownie później." - success: "PomyÅ›lnie poÅ‚Ä…czono z serwerem Misskey" - success-desc: "WyglÄ…da na to, że udaÅ‚o siÄ™ poÅ‚Ä…czyć. OdÅ›wież stronÄ™." - flush: "Wyczyść pamięć podrÄ™cznÄ…" - set-version: "OkreÅ›l wersjÄ™" -common/views/components/media-banner.vue: - sensitive: "NSFW" - click-to-show: "NaciÅ›nij aby wyÅ›wietlić" -common/views/components/theme.vue: - theme: "Motyw" - light-theme: "Motyw" - light-themes: "Jasny Motyw" - dark-themes: "Ciemny motyw" - install-a-theme: "Zainstaluj motyw" - theme-code: "Kod motywu" - install: "Zainstaluj" - installed: "\"{}\" zostaÅ‚ zainstalowany" - create-a-theme: "Stwórz motyw" - save-created-theme: "Zapisz motyw" - primary-color: "Kolor podstawowy" - secondary-color: "Kolor dodatkowy" - text-color: "Kolor tekstu" - base-theme: "Podstawowy motyw" - base-theme-light: "Jasny" - base-theme-dark: "Ciemny" - find-more-theme: "Odkryj wiÄ™cej motywów" - theme-name: "Nazwa motywu" - preview-created-theme: "Pokaż podglÄ…d" - invalid-theme: "NieprawidÅ‚owy motyw" - already-installed: "Ten motyw jest już zainstalowany" - saved: "Zapisano" - manage-themes: "ZarzÄ…dzanie motywami" - builtin-themes: "Standardowe motywy" - my-themes: "Moje motywy" - installed-themes: "Zainstalowane motywy" - select-theme: "Wybierz motyw" - uninstall: "Odinstaluj" - uninstalled: "\"{}\" zostaÅ‚ odinstalowany" - author: "Author" - desc: "Opis" - export: "Eksportuj" - import: "Importuj" - import-by-code: "lub wklej kod" - theme-name-required: "Nazwa motywu jest obowiÄ…zkowa." -common/views/components/cw-button.vue: - hide: "Ukryj" - show: "Pokaż wiÄ™cej" - chars: "{count} znaków" - files: "{count} plików" - poll: "Ankieta" -common/views/components/messaging.vue: - search-user: "Znajdź użytkownika" - you: "Ty" - no-history: "Brak historii" - user: "Użytkownicy" -common/views/components/messaging-room.vue: - no-history: "Brak dalszej historii" - new-message: "Nowa wiadomość" -common/views/components/messaging-room.form.vue: - input-message-here: "Wprowadź wiadomość tutaj" - send: "WyÅ›lij" - attach-from-local: "ZaÅ‚Ä…cz pliki z komputera" - attach-from-drive: "ZaÅ‚Ä…cz pliki z dysku" -common/views/components/messaging-room.message.vue: - is-read: "Przeczytano" - deleted: "Wiadomość zostaÅ‚a usuniÄ™ta" -common/views/components/nav.vue: - about: "O stronie" - stats: "Statystyki" - status: "Stan" - wiki: "Wiki" - donors: "Sponsorzy" - repository: "Repozytorium" - develop: "Autorzy" - feedback: "Podziel siÄ™ opiniÄ…" -common/views/components/note-menu.vue: - mention: "Wspomnij" - detail: "Szczegóły" - copy-content: "Skopiuj zawartość" - copy-link: "Skopiuj adres" - favorite: "Dodaj do ulubionych" - unfavorite: "UsuÅ„ z ulubionych" - pin: "Przypnij do profilu" - unpin: "Odepnij" - delete: "UsuÅ„" - delete-confirm: "Czy na pewno chcesz usunąć ten wpis?" - remote: "Pokaż oryginaÅ‚" -common/views/components/user-menu.vue: - mention: "Wspomnij" - mute: "Wycisz" - unmute: "Cofnij wyciszenie" - block: "Zablokuj" - unblock: "Odblokuj" - push-to-list: "Dodaj do listy" - select-list: "Wybierz listÄ™" - report-abuse: "ZgÅ‚oÅ› nadużycie" -common/views/components/poll.vue: - vote-to: "ZagÅ‚osuj na '{}'" - vote-count: "{} gÅ‚osów" - vote: "ZagÅ‚osuj" - show-result: "Pokaż wyniki" - voted: "ZagÅ‚osowano" -common/views/components/poll-editor.vue: - no-only-one-choice: "Musisz wprowadzić przynajmniej dwie opcje." - choice-n: "Opcja {}" - remove: "UsuÅ„ tÄ… opcjÄ™" - add: "+ Dodaj opcjÄ™" - destroy: "UsuÅ„ tÄ™ ankietÄ™" - day: "N" -common/views/components/reaction-picker.vue: - choose-reaction: "Wybierz reakcjÄ™" -common/views/components/emoji-picker.vue: - custom-emoji: "Niestandardowe Emoji" - people: "Ludzie" - animals-and-nature: "ZwierzÄ™ta i Natura" - food-and-drink: "Å»ywność i napoje" - activity: "Aktywność" - travel-and-places: "Podróże i Miejsca" - objects: "Rzeczy" - symbols: "Symbole" - flags: "Flagi" -common/views/components/settings/app-type.vue: - info: "Musisz odÅ›wieżyć stronÄ™, aby zmiany zostaÅ‚y uwzglÄ™dnione." -common/views/components/signin.vue: - username: "Nazwa użytkownika" - password: "HasÅ‚o" - token: "Token" - signing-in: "Logowanie…" - or: "lub" - signin-with-twitter: "Zaloguj siÄ™ za pomocÄ… Twittera" - signin-with-github: "Zaloguj siÄ™ za pomocÄ…Â GitHuba" - signin-with-discord: "Zaloguj siÄ™ za pomocÄ…Â Discorda" - login-failed: "Logowanie nie powiodÅ‚o siÄ™. Upewnij siÄ™, że podaÅ‚eÅ› prawidÅ‚owÄ… nazwÄ™ użytkownika i hasÅ‚o." -common/views/components/signup.vue: - invitation-code: "Kod zaproszenia" - username: "Nazwa użytkownika" - checking: "Sprawdzanie…" - available: "DostÄ™pna" - unavailable: "NiedostÄ™pna" - error: "BÅ‚Ä…d sieci" - invalid-format: "Może zawierać litery, cyfry i myÅ›lniki." - too-short: "Wprowadź przynajmniej jeden znak" - too-long: "Nazwa nie może zawierać wiÄ™cej niż 20 znaków" - password: "HasÅ‚o" - password-placeholder: "Zalecamy korzystanie z hasÅ‚a zawierajÄ…cego przynajmniej 8 znaków." - weak-password: "SÅ‚abe" - normal-password: "Åšrednie" - strong-password: "Silne" - retype: "Powtórz hasÅ‚o" - retype-placeholder: "Potwierdź hasÅ‚o" - password-matched: "OK" - password-not-matched: "HasÅ‚a nie zgadzajÄ… siÄ™" - recaptcha: "Weryfikacja" - create: "Utwórz konto" - some-error: "Nie udaÅ‚o siÄ™ utworzyć konta. Spróbuj ponownie." -common/views/components/special-message.vue: - new-year: "Szczęśliwego nowego roku!" - christmas: "WesoÅ‚ych Å›wiÄ…t!" -common/views/components/stream-indicator.vue: - connecting: "ÅÄ…czenie" - reconnecting: "Ponowne Å‚Ä…czenie" - connected: "PoÅ‚Ä…czono" -common/views/components/notification-settings.vue: - title: "Powiadomienia" - mark-as-read-all-notifications: "Oznacz wszystkie powiadomienia jako przeczytane" - mark-as-read-all-unread-notes: "Oznacz wszystkie wpisy jako przeczytane" - mark-as-read-all-talk-messages: "Oznacz wszystkie rozmowy jako przeczytane" - auto-watch: "Automatycznie nasÅ‚uchuj wpisów" - auto-watch-desc: "Automatycznie otrzymuj powiadomienia o wpisach, w których zareagowaÅ‚eÅ›(-aÅ›) lub odpowiedziaÅ‚eÅ›(-aÅ›)." -common/views/components/integration-settings.vue: - connect: "PoÅ‚Ä…cz" - disconnect: "RozÅ‚Ä…cz" - connected-to: "Jesteś poÅ‚Ä…czony(-a) z nastÄ™pujÄ…cym kontem" -common/views/components/github-setting.vue: - detail: "WiÄ™cej..." - reconnect: "PoÅ‚Ä…cz ponownie" - disconnect: "RozÅ‚Ä…cz" -common/views/components/discord-setting.vue: - detail: "Szczegóły…" - reconnect: "PoÅ‚Ä…cz ponownie" - disconnect: "RozÅ‚Ä…cz" -common/views/components/uploader.vue: - waiting: "Oczekiwanie" -common/views/components/visibility-chooser.vue: - public: "Publiczny" - home: "Lokalny" - home-desc: "Widoczny tylko na tej instancji" - followers: "Dla Å›ledzÄ…cych" - followers-desc: "Widoczny tylko dla osób, które CiÄ™ Å›ledzÄ…" - specified: "BezpoÅ›redni" - specified-desc: "Tylko dla okreÅ›lonych użytkowników" - local-public: "Publiczny (tylko lokalnie)" - local-followers: "Dla Å›ledzÄ…cych (tylko lokalnie)" -common/views/components/trends.vue: - empty: "Brak popularnych hashtagów" -common/views/components/language-settings.vue: - title: "JÄ™zyk" - pick-language: "Wybierz jÄ™zyk" - recommended: "Zalecane" - auto: "Automatyczny" - specify-language: "Wybierz jÄ™zyk" - info: "Musisz odÅ›wieżyć stronÄ™, aby zmiany zostaÅ‚y uwzglÄ™dnione." -common/views/components/profile-editor.vue: - title: "Twój profil" - name: "Nazwa" - account: "Konto" - location: "Lokalizacja" - description: "O mnie" - language: "JÄ™zyk" - birthday: "Data urodzenia" - avatar: "Awatar" - banner: "Baner" - is-cat: "To konto jest prowadzone przez kota" - is-bot: "To konto jest prowadzone przez bota" - is-locked: "ProÅ›by Å›ledzenia wymagajÄ… zatwierdzenia" - careful-bot: "ProÅ›by Å›ledzenia od botów wymagajÄ… zatwierdzenia" - auto-accept-followed: "Automatyczne zatwierdzaj Å›ledzenia od osób, które Å›ledzisz." - advanced: "Inne" - privacy: "Prywatność" - save: "Zapisz" - saved: "PomyÅ›lnie zaktualizowano profil" - uploading: "WysyÅ‚anie" - upload-failed: "WysyÅ‚anie nie powiodÅ‚o siÄ™" - unable-to-process: "Nie udaÅ‚o siÄ™ ukoÅ„czyć dziaÅ‚ania." - email: "Ustawienia e-mail" - email-address: "Adres e-mail" - email-verified: "Twój adres e-mail zostaÅ‚ zweryfikowany." - export: "Eksportuj" - import: "Importuj" - export-targets: - following-list: "Åšledzeni" - mute-list: "Wycisz" - blocking-list: "Zablokuj" - user-lists: "Listy" - enter-password: "Wprowadź hasÅ‚o" -common/views/components/user-list-editor.vue: - users: "Użytkownicy" - rename: "ZmieÅ„ nazwÄ™ listy" - delete: "UsuÅ„ listÄ™" - remove-user: "UsuÅ„ z tej listy" - delete-are-you-sure: "Usunąć listÄ™ \"$1\"?" - deleted: "UsuniÄ™to" - add-user: "Dodaj użytkownika" -common/views/components/user-group-editor.vue: - deleted: "UsuniÄ™to" - invite: "ZaproÅ›" -common/views/components/user-lists.vue: - user-lists: "Listy" - list-name: "Nazwa listy" -common/views/components/user-groups.vue: - invites: "ZaproÅ›" -common/views/widgets/broadcast.vue: - fetching: "Sprawdzanie" - no-broadcasts: "Brak transmisji" - have-a-nice-day: "MiÅ‚ego dnia!" - next: "Dalej" -common/views/widgets/calendar.vue: - year: "Rok {}" - month: "MiesiÄ…c {}" - day: "DzieÅ„ {}" - today: "Dzisiaj:" - this-month: "Ten miesiÄ…c:" - this-year: "Ten rok:" -common/views/widgets/photo-stream.vue: - title: "Photostream" - no-photos: "Brak zdjęć" -common/views/widgets/posts-monitor.vue: - title: "Wykres wpisów" - toggle: "PrzeÅ‚Ä…cz widok" -common/views/widgets/hashtags.vue: - title: "Hashtagi" -common/views/widgets/server.vue: - title: "Informacje o serwerze" - toggle: "PrzeÅ‚Ä…cz widok" -common/views/widgets/memo.vue: - title: "Notatka" - memo: "Napisz tutaj!" - save: "Zapisz" -common/views/widgets/slideshow.vue: - folder-customize-mode: "Aby okreÅ›lić katalog, opuść tryb dostosowywania" - folder: "NaciÅ›nij i wybierz folder" - no-image: "Brak obrazu w tym folderze" -common/views/widgets/tips.vue: - tips-line1: "Możesz przejść do osi czasu używajÄ…c <kbd>t</kbd>." - tips-line2: "Otwórz formularz nowego wpisu używajÄ…c <kbd>p</kbd> lub <kbd>n</kbd>." - tips-line3: "Możesz przeciÄ…gnąć i upuÅ›cić pliki w formularzu wpisu." - tips-line5: "Możesz wysÅ‚ać pliki przeciÄ…gajÄ…c i upuszczajÄ…c je w Dysku." - tips-line6: "Możesz przenieść katalog przeciÄ…gajÄ…c go w Dysku." - tips-line7: "Możesz przenieść katalog przeciÄ…gajÄ…c go w Dysku." - tips-line8: "Strona główna może zostać dostosowana w ustawieniach." - tips-line9: "Misskey jest dostÄ™pny na licencji AGPLv3." - tips-line11: "Możesz przypiąć wpis na stronie użytkownika klikajÄ…c na „…â€" - tips-line17: "Oznaczenie tekstu **w ten sposób** wyróżni go." - tips-line19: "Część okien może zostać odÅ‚Ä…czona z przeglÄ…darki." - tips-line21: "Możesz też używać API, aby tworzyć boty." - tips-line24: "Misskey zaczÄ…Å‚ dziaÅ‚ać w 2014." - tips-line25: "Możesz otrzymywać powiadomienia nawet jeżeli Misskey nie jest otwarty w obsÅ‚ugiwanej przeglÄ…darce." -common/views/pages/not-found.vue: - page-not-found: "Strona nie zostaÅ‚a znaleziona" -common/views/pages/follow.vue: - signed-in-as: "Zalogowany jako {}" - following: "Åšledzisz" - follow: "Åšledź" - request-pending: "Oczekiwanie na pozwolenie" - follow-processing: "Przetwarzanie Å›ledzenia" - follow-request: "PoproÅ› o Å›ledzenie" -common/views/pages/follow-requests.vue: - received-follow-requests: "ProÅ›by o Å›ledzenie" -desktop: - banner: "Baner" - uploading-banner: "WysyÅ‚anie baneru" - banner-updated: "Zmieniono baner" - choose-banner: "Wybierz baner" - avatar-crop-title: "Wybierz część obrazu, która zostanie użyta jako awatar" - avatar: "Awatar" - uploading-avatar: "WysyÅ‚anie awatara" - avatar-updated: "WysÅ‚ano awatar" - choose-avatar: "Wybierz awatar" - unable-to-process: "Nie udaÅ‚o siÄ™ ukoÅ„czyć dziaÅ‚ania." -desktop/views/components/activity.chart.vue: - total: "Czarny … ÅÄ…cznie" - notes: "Niebieski … Wpisy" - replies: "Czerwony … Odpowiedzi" - renotes: "Czerwony … UdostÄ™pnienia" -desktop/views/components/activity.vue: - title: "Aktywność" - toggle: "PrzeÅ‚Ä…cz widok" -desktop/views/components/calendar.vue: - title: "{year} / {month}" - prev: "Poprzedni miesiÄ…c" - next: "NastÄ™pny miesiÄ…c" - go: "NaciÅ›nij, aby przejść" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "Wybrano {count} Plik(ów)" - upload: "WyÅ›lij pliki z Twojego komputera" - cancel: "Anuluj" - ok: "OK" - choose-prompt: "Wybierz plik" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "Anuluj" - ok: "OK" - choose-prompt: "Wybierz katalog" -desktop/views/components/crop-window.vue: - skip: "PomiÅ„ przycinanie" - cancel: "Anuluj" - ok: "OK" -desktop/views/components/drive-window.vue: - used: "wykorzystane" -desktop/views/components/drive.file.vue: - avatar: "Awatar" - banner: "Baner" - nsfw: "NSFW" - contextmenu: - rename: "ZmieÅ„ nazwÄ™" - mark-as-sensitive: "Oznacz jako zawartość wrażliwÄ…" - unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwÄ…" - copy-url: "Skopiuj adres" - download: "Pobierz" - else-files: "Inne" - set-as-avatar: "Ustaw jako awatar" - set-as-banner: "Ustaw jako baner" - open-in-app: "Otwórz w aplikacji" - add-app: "Dodaj aplikacjÄ™" - rename-file: "ZmieÅ„ nazwÄ™ pliku" - input-new-file-name: "Wprowadź nowÄ… nazwÄ™" - copied: "Skopiowano" - copied-url-to-clipboard: "Skopiowano adres do schowka" -desktop/views/components/drive.folder.vue: - unable-to-process: "Nie udaÅ‚o siÄ™ ukoÅ„czyć dziaÅ‚ania." - circular-reference-detected: "Docelowy katalog znajduje siÄ™ w katalogu, który chcesz przenieść." - unhandled-error: "Nieznany bÅ‚Ä…d" - contextmenu: - move-to-this-folder: "PrzenieÅ› do tego katalogu" - show-in-new-window: "Otwórz w nowym oknie" - rename: "ZmieÅ„ nazwÄ™" - rename-folder: "ZmieÅ„ nazwÄ™ katalogu" - input-new-folder-name: "Wprowadź nowÄ… nazwÄ™" - else-folders: "Inne" -desktop/views/components/drive.vue: - search: "Szukaj" - empty-draghover: "PrzeciÄ…gnij tutaj!" - empty-drive: "Twój dysk jest pusty" - empty-drive-description: "Możesz wysÅ‚ać plik klikajÄ…c prawym przyciskiem myszy i wybierajÄ…c \"WyÅ›lij plik\" lub przeciÄ…gnąć plik i upuÅ›cić w tym oknie." - empty-folder: "Ten katalog jest posty" - unable-to-process: "Nie udaÅ‚o siÄ™ dokoÅ„czyć dziaÅ‚ania." - circular-reference-detected: "Ten katalog znajduje siÄ™ w katalogu, który chcesz przenieść." - unhandled-error: "Nieznany bÅ‚Ä…d" - url-upload: "WyÅ›lij z adresu" - url-of-file: "Adres URL pliku, który chcesz wysÅ‚ać" - url-upload-requested: "Zaplanowano wysyÅ‚anie" - may-take-time: "Może trochÄ™ potrwać, zanim wysyÅ‚anie zostanie ukoÅ„czone." - create-folder: "Utwórz katalog" - folder-name: "Nazwa katalogu" - contextmenu: - create-folder: "Utwórz katalog" - upload: "WyÅ›lij plik" - url-upload: "WyÅ›lij z adresu URL" -desktop/views/components/media-video.vue: - sensitive: "To jest zawartość NSFW" - click-to-show: "NaciÅ›nij aby wyÅ›wietlić" -desktop/views/components/followers-window.vue: - followers: "ÅšledzÄ…cy" -desktop/views/components/followers.vue: - empty: "WyglÄ…da na to, że nikt CiÄ™ nie Å›ledzi…" -desktop/views/components/following-window.vue: - following: "Åšledzeni przez {}" -desktop/views/components/following.vue: - empty: "Nikt CiÄ™ nie Å›ledzi." -desktop/views/components/game-window.vue: - game: "Reversi" -desktop/views/components/home.vue: - done: "ZakoÅ„cz" - add-widget: "Dodaj widżet:" - add: "Dodaj" -desktop/views/input-dialog.vue: - cancel: "Anuluj" - ok: "OK" -desktop/views/components/note-detail.vue: - private: "ten wpis jest prywatny" - deleted: "ten wpis zostaÅ‚ usuniÄ™ty" - location: "Informacje o lokalizacji" - renote: "UdostÄ™pnij" - add-reaction: "Dodaj reakcjÄ™" -desktop/views/components/note.vue: - reply: "Odpowiedz" - renote: "UdostÄ™pnij" - add-reaction: "Dodaj reakcjÄ™" - detail: "Szczegóły" - private: "Ten wpis jest prywatny" - deleted: "ten wpis zostaÅ‚ usuniÄ™ty" -desktop/views/components/notes.vue: - error: "Åadowanie nie powiodÅ‚o siÄ™." - retry: "Spróbuj ponownie" -desktop/views/components/notifications.vue: - empty: "Brak powiadomieÅ„" -desktop/views/components/post-form.vue: - posted: "Opublikowano!" - replied: "Odpowiedziano!" - reposted: "UdostÄ™pniono!" - note-failed: "Nie udaÅ‚o siÄ™ wysÅ‚ać" - reply-failed: "Nie udaÅ‚o siÄ™ odpowiedzieć" - renote-failed: "Nie udaÅ‚o siÄ™ udostÄ™pnić" -desktop/views/components/post-form-window.vue: - note: "Nowy wpis" - reply: "Odpowiedz" - attaches: "{} zaÅ‚Ä…czników multimedialnych" - uploading-media: "WysyÅ‚anie {} treÅ›ci multimedialnych" -desktop/views/components/progress-dialog.vue: - waiting: "Oczekiwanie" -desktop/views/components/renote-form.vue: - quote: "Cytuj…" - cancel: "Anuluj" - renote: "UdostÄ™pnij" - reposting: "UdostÄ™pnianie…" - success: "UdostÄ™pniono!" - failure: "Nie udaÅ‚o siÄ™ udostÄ™pnić" -desktop/views/components/renote-form-window.vue: - title: "Czy na pewno chcesz udostÄ™pnić ten wpis?" -desktop/views/components/settings.2fa.vue: - intro: "Jeżeli skonfigurujesz uwierzytelnianie dwuetapowe, aby zablokować siÄ™ bÄ™dziesz potrzebować (oprócz hasÅ‚a) kodu ze skonfigurowanego urzÄ…dzenia (np. smartfonu), co zwiÄ™kszy bezpieczeÅ„stwo." - detail: "Zobacz szczegóły…" - url: "https://www.google.com/landing/2step/" - caution: "Jeżeli stracisz dostÄ™p do urzÄ…dzenia, nie bÄ™dziesz mógÅ‚ logować siÄ™ do Misskey!" - register: "Zarejestruj urzÄ…dzenie" - already-registered: "UrzÄ…dzenie jest już zarejestrowane" - unregister: "WyÅ‚Ä…cz" - unregistered: "WyÅ‚Ä…czono uwierzytelnianie dwuetapowe." - enter-password: "Wprowadź hasÅ‚o" - authenticator: "Na poczÄ…tek musisz zainstalować Google Authenticator na swoim urzÄ…dzeniu:" - howtoinstall: "Jak zainstalować" - token: "Token" - scan: "Później, zeskanuje ten kod QR:" - done: "Wprowadź token wyÅ›wietlony na Twoim urzÄ…dzeniu:" - submit: "WyÅ›lij" - success: "PomyÅ›lnie ukoÅ„czono konfiguracjÄ™!" - failed: "Nie udaÅ‚o siÄ™ skonfigurować uwierzytelniania dwuetapowego, upewnij siÄ™ że wprowadziÅ‚eÅ› prawidÅ‚owy token." - info: "Od teraz, wprowadzaj token wyÅ›wietlany na urzÄ…dzeniu przy każdym logowaniu do Misskey." -common/views/components/media-image.vue: - sensitive: "To jest zawartość NSFW" - click-to-show: "NaciÅ›nij aby wyÅ›wietlić" -common/views/components/api-settings.vue: - intro: "Aby uzyskać dostÄ™p do API, ustaw ten token jako klucz 'i' parametrów żądaÅ„." - caution: "Nie pokazuj tego tokenu osobom trzecim (nie wprowadzaj go nigdzie indziej), aby konto nie trafiÅ‚o w niepowoÅ‚ane rÄ™ce." - regeneration-of-token: "W przypadku wycieku tokenu, możesz wygenerować nowy." - regenerate-token: "Wygeneruj nowy token" - token: "Token:" - enter-password: "Wprowadź hasÅ‚o" - console: - title: "Konsola API" - parameter: "Parametry" - send: "WyÅ›lij" -desktop/views/components/settings.apps.vue: - no-apps: "Brak zautoryzowanych aplikacji" -common/views/components/drive-settings.vue: - max: "Max" - in-use: "użyto" - stats: "Statystyki" - default-upload-folder-name: "Katalog(i)" -common/views/components/mute-and-block.vue: - mute-and-block: "Wycisz / Zablokuj" - mute: "Wycisz" - block: "Zablokuj" - no-muted-users: "Brak wyciszonych użytkowników" - no-blocked-users: "Brak zablokowanych użytkowników" - word-mute: "Wyciszenie sÅ‚owa" - muted-words: "Wyciszone sÅ‚owa kluczowe" - save: "Zapisz" -common/views/components/password-settings.vue: - reset: "ZmieÅ„ hasÅ‚o" - enter-current-password: "Wprowadź obecne hasÅ‚o" - enter-new-password: "Wprowadź nowe hasÅ‚o" - enter-new-password-again: "Wprowadź ponownie nowe hasÅ‚o" -common/views/components/post-form-attaches.vue: - mark-as-sensitive: "Oznacz jako zawartość wrażliwÄ…" - unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwÄ…" -desktop/views/components/sub-note-content.vue: - private: "ten wpis jest prywatny" - deleted: "ten wpis zostaÅ‚ usuniÄ™ty" - media-count: "{}zawartoÅ›ci multimedialnej" - poll: "Ankieta" -desktop/views/components/settings.tags.vue: - title: "Tagi" - query: "Zapytanie (opcjonalne)" - add: "Dodaj" - save: "Zapisz" -desktop/views/components/timeline.vue: - home: "Strona główna" - local: "Lokalne" - global: "Globalne" - mentions: "Wspomnienia" - messages: "BezpoÅ›rednie wpisy" - list: "Listy" - hashtag: "Hashtag" - add-tag-timeline: "Dodaj hashtag" - add-list: "Dodaj listÄ™" - list-name: "Nazwa listy" -desktop/views/components/ui.header.vue: - welcome-back: "Witaj ponownie," -desktop/views/components/ui.header.account.vue: - profile: "Twój profil" - lists: "Listy" - follow-requests: "ProÅ›by o Å›ledzenie" - admin: "Admin" -desktop/views/components/ui.header.nav.vue: - game: "Gra" -desktop/views/components/ui.header.notifications.vue: - title: "Powiadomienia" -desktop/views/components/ui.header.post.vue: - post: "Utwórz nowy wpis" -desktop/views/components/ui.header.search.vue: - placeholder: "Szukaj" -desktop/views/components/user-preview.vue: - notes: "Wpisy" - following: "Åšledzeni" - followers: "ÅšledzÄ…cy" -desktop/views/components/users-list.vue: - all: "Wszyscy" - iknow: "Znasz" - fetching: "Åadowanie…" -desktop/views/components/users-list-item.vue: - followed: "Obserwuje CiÄ™" -desktop/views/components/window.vue: - popout: "Pop-out" - close: "Zamknij" -admin/views/index.vue: - dashboard: "Kokpit" - instance: "Instancja" - emoji: "Emoji" - moderators: "Moderatorzy" - users: "Użytkownicy" - announcements: "OgÅ‚oszenia" -admin/views/dashboard.vue: - dashboard: "Kokpit" - accounts: "Konta" - notes: "Wpisy" - drive: "Dysk" - instances: "Instancja" -admin/views/logs.vue: - levels: - info: "Informacje" - error: "BÅ‚Ä…d" -admin/views/abuse.vue: - details: "Szczegóły" - remove-report: "UsuÅ„" -admin/views/instance.vue: - instance: "Instancja" - recaptcha-preview: "Pokaż podglÄ…d" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - invite: "ZaproÅ›" - save: "Zapisz" - saved: "Zapisano" - email: "Adres e-mail" - test-email: "Test" -admin/views/charts.vue: - notes: "Wpisy" - users: "Użytkownicy" - drive: "Dysk" - network: "Sieć" - charts: - network-requests: "Żądania" - network-time: "Czas reakcji" -admin/views/drive.vue: - sort: - title: "Sortuj" - origin: - title: "ŹródÅ‚o" - local: "Lokalne" - remote: "Zdalny" - delete: "UsuÅ„" - deleted: "UsuniÄ™to" - mark-as-sensitive: "Oznacz jako zawartość wrażliwÄ…" - unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwÄ…" -admin/views/users.vue: - user-not-found: "Nie znaleziono użytkownika" - username: "Nazwa użytkownika" - users: - title: "Użytkownicy" - sort: - title: "Sortuj" - state: - all: "Wszyscy" - moderator: "Moderatorzy" - origin: - title: "ŹródÅ‚o" - local: "Lokalny" - remote: "Zdalny" - createdAt: "Utworzono" -admin/views/moderators.vue: - add-moderator: - add: "Zarejestruj siÄ™" - logs: - moderator: "Moderatorzy" - info: "Informacje" -admin/views/emoji.vue: - add-emoji: - name: "Nazwa Emoji" - aliases: "Aliasy" - add: "Dodaj" - emojis: - update: "Aktualizuj" - remove: "UsuÅ„" - updated: "Zaktualizowano" - remove-emoji: - are-you-sure: "Usunąć \"$1\"?" - removed: "UsuniÄ™to" -admin/views/announcements.vue: - announcements: "OgÅ‚oszenia" - save: "Zapisz" - remove: "UsuÅ„" - add: "Dodaj" - title: "TytuÅ‚" - saved: "Zapisano" - _remove: - are-you-sure: "Usunąć \"$1\"?" - removed: "UsuniÄ™to" -admin/views/federation.vue: - instance: "Instancja" - notes: "Wpis" - users: "Użytkownicy" - following: "Åšledzisz" - followers: "ÅšledzÄ…cy" - caught-at: "Utworzono" - status: "Stan" - block: "Zablokuj" - sort: "Sortuj" - states: - all: "Wszyscy" - blocked: "Zablokuj" - chart-srcs: - requests: "Żądania" - blocked-hosts: "Zablokuj" - save: "Zapisz" -desktop/views/pages/welcome.vue: - about: "O Misskey" - timeline: "OÅ› czasu" - announcements: "OgÅ‚oszenia" - photos: "Ostatnie obrazy" - powered-by-misskey: "Oparto o <b>Misskey</b>." - info: "Informacje" -desktop/views/pages/drive.vue: - title: "Dysk Misskey" -desktop/views/pages/note.vue: - prev: "Poprzedni wpis" - next: "NastÄ™pny wpis" -desktop/views/pages/selectdrive.vue: - title: "Wybierz plik(i)" - ok: "OK" - cancel: "Anuluj" - upload: "WyÅ›lij pliki z Twojego komputera" -desktop/views/pages/user-list.users.vue: - users: "Użytkownicy" - add-user: "Dodaj użytkownika" - username: "Nazwa użytkownika" -desktop/views/pages/user/user.followers-you-know.vue: - title: "ÅšledzÄ…cy których znasz" - loading: "Åadowanie" - no-users: "Brak użytkowników" -desktop/views/pages/user/user.friends.vue: - title: "Najbardziej aktywni" - loading: "Åadowanie" - no-users: "Brak użytkowników" -desktop/views/pages/user/user.photos.vue: - title: "ZdjÄ™cia" - loading: "Åadowanie" - no-photos: "Brak zdjęć" -desktop/views/pages/user/user.header.vue: - posts: "Wpisy" - following: "Åšledzeni" - followers: "ÅšledzÄ…cy" - is-bot: "To konto jest botem" - years-old: "{age} lat" - year: "/" - month: "/" - day: "-" - follows-you: "Åšledzi CiÄ™" -desktop/views/pages/user/user.timeline.vue: - default: "Wpisy" - with-replies: "Wpisy i odpowiedzi" - with-media: "Multimedia" - my-posts: "Moje wpisy" -desktop/views/widgets/notifications.vue: - title: "Powiadomienia" -desktop/views/widgets/polls.vue: - title: "Ankiety" - refresh: "Pokaż inne" - nothing: "Pusto" -desktop/views/widgets/post-form.vue: - title: "Wpis" - note: "Wpis" -desktop/views/widgets/profile.vue: - update-banner: "NaciÅ›nij, aby zmienić baner" - update-avatar: "NaciÅ›nij, aby zmienić awatar" -desktop/views/widgets/trends.vue: - title: "Na czasie" - refresh: "Pokaż inne" - nothing: "Pusto" -desktop/views/widgets/users.vue: - title: "Polecani użytkownicy" - refresh: "Pokaż innych" - no-one: "Pusto" -mobile/views/components/drive.vue: - used: "użyto" - folder-count: "Katalog(i)" - count-separator: ", " - file-count: "Plik(i)" - nothing-in-drive: "Pusto" - folder-is-empty: "Ten katalog jest pusty" - folder-name: "Nazwa katalogu" - url-prompt: "Adres URL pliku, który chcesz wysÅ‚ać" - uploading: "RozpoczÄ™to wysyÅ‚anie. Może to trochÄ™ potrwać." -mobile/views/components/drive-file-chooser.vue: - select-file: "Wybierz plik" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "Wybierz katalog" -mobile/views/components/drive.file.vue: - nsfw: "NSFW" -mobile/views/components/drive.file-detail.vue: - download: "Pobierz" - rename: "ZmieÅ„ nazwÄ™" - move: "PrzenieÅ›" - hash: "Hash (md5)" - exif: "EXIF" - nsfw: "NSFW" - mark-as-sensitive: "Oznacz jako zawartość wrażliwÄ…" - unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwÄ…" -mobile/views/components/media-video.vue: - sensitive: "To jest zawartość NSFW" - click-to-show: "NaciÅ›nij aby wyÅ›wietlić" -common/views/components/follow-button.vue: - following: "Åšledzisz" - follow: "Åšledź" - request-pending: "Oczekiwanie na pozwolenie" - follow-processing: "Przetwarzanie" - follow-request: "PoproÅ› o Å›ledzenie" -mobile/views/components/note.vue: - private: "ten wpis jest prywatny" - deleted: "ten wpis zostaÅ‚ usuniÄ™ty" - location: "Informacje o lokalizacji" -mobile/views/components/note-detail.vue: - reply: "Odpowiedz" - reaction: "Reakcja" - private: "ten wpis jest prywatny" - deleted: "ten wpis zostaÅ‚ usuniÄ™ty" - location: "Informacje o lokalizacji" -mobile/views/components/note-preview.vue: - admin: "admin" - bot: "bot" - cat: "kot" -mobile/views/components/note-sub.vue: - admin: "admin" - bot: "bot" - cat: "kot" -mobile/views/components/notifications.vue: - empty: "Brak powiadomieÅ„" -mobile/views/components/sub-note-content.vue: - private: "ten wpis jest prywatny" - deleted: "ten wpis zostaÅ‚ usuniÄ™ty" - media-count: "{}zawartoÅ›ci multimedialnej" - poll: "Ankieta" -mobile/views/components/ui.header.vue: - welcome-back: "Witaj ponownie, " -mobile/views/components/ui.nav.vue: - timeline: "OÅ› czasu" - notifications: "Powiadomienia" - follow-requests: "ProÅ›by o Å›ledzenie" - search: "Szukaj" - user-lists: "Listy" - widgets: "Widżety" - game: "Gry" - admin: "Admin" - about: "O Misskey" -mobile/views/pages/drive.vue: - contextmenu: - upload: "WyÅ›lij plik" - create-folder: "Utwórz katalog" -mobile/views/pages/signup.vue: - lets-start: "Rozpocznijmy! 📦" -mobile/views/pages/home.vue: - home: "Strona główna" - local: "Lokalne" - global: "Globalne" - mentions: "Wspomnienia" - messages: "BezpoÅ›rednie wpisy" -mobile/views/pages/widgets.vue: - dashboard: "Kokpit" - add-widget: "Dodaj" - customization-tips: "Wskazówki o dostosowywaniu" -mobile/views/pages/widgets/activity.vue: - activity: "Aktywność" -mobile/views/pages/note.vue: - title: "Wpis" - prev: "Poprzedni wpis" - next: "NastÄ™pny wpis" -mobile/views/pages/games/reversi.vue: - reversi: "Reversi" -mobile/views/pages/search.vue: - search: "Szukaj" -mobile/views/pages/selectdrive.vue: - select-file: "Wybierz plik" -mobile/views/pages/notifications.vue: - notifications: "Powiadomienia" -mobile/views/pages/settings.vue: - signed-in-as: "Zalogowany jako {}" -mobile/views/pages/user.vue: - follows-you: "Åšledzi CiÄ™" - following: "Åšledzeni" - followers: "ÅšledzÄ…cy" - notes: "Wpisy" - overview: "PrzeglÄ…d" - timeline: "OÅ› czasu" - media: "Multimedia" - years-old: "{age} lat" -mobile/views/pages/user/home.vue: - recent-notes: "Ostatnie wpisy" - images: "ZdjÄ™cia" - activity: "Aktywność" - keywords: "SÅ‚owa kluczowe" - domains: "Domeny" - frequently-replied-users: "Najbardziej aktywni" - followers-you-know: "ÅšledzÄ…cy których znasz" - last-used-at: "Ostatnio aktywny" -mobile/views/pages/user/home.photos.vue: - no-photos: "Brak zdjęć" -deck: - widgets: "Widżety" - home: "Strona główna" - local: "Lokalne" - hashtag: "Hashtag" - global: "Globalne" - mentions: "Wspomnienia" - direct: "BezpoÅ›rednie wpisy" - notifications: "Powiadomienia" - list: "Listy" - select-list: "Wybierz listÄ™" - swap-left: "PrzesuÅ„ w lewo" - swap-right: "PrzesuÅ„ w prawo" - swap-up: "PrzenieÅ› w górÄ™" - remove: "UsuÅ„" - add-column: "Dodaj kolumnÄ™" - rename: "ZmieÅ„ nazwÄ™" - stack-left: "Przypnij do lewej" -deck/deck.tl-column.vue: - is-media-only: "Tylko wpisy z zawartoÅ›ciÄ… multimedialnÄ…" - edit: "Opcje" -deck/deck.user-column.vue: - follows-you: "Åšledzi CiÄ™" - posts: "Wpisy" - following: "Åšledzeni" - followers: "ÅšledzÄ…cy" - images: "ZdjÄ™cia" - activity: "Aktywność" - timeline: "OÅ› czasu" - pinned-notes: "PrzypiÄ™te posty" -docs: - edit-this-page-on-github: "ZnalazÅ‚eÅ› bÅ‚Ä…d lub chcesz pomóc w tworzeniu dokumentacji?" - edit-this-page-on-github-link: "Edytuj stronÄ™ na GitHubie!" -dev/views/index.vue: - manage-apps: "ZarzÄ…dzaj aplikacjami" -dev/views/apps.vue: - manage-apps: "ZarzÄ…dzaj aplikacjami" - app-missing: "Brak aplikacji" -dev/views/new-app.vue: - app-name: "Nazwa Aplikacji" - authority: "Uprawnienia" -pages: - pin-this-page: "Przypnij do profilu" - unpin-this-page: "Odepnij" - like: "LubiÄ™" - title: "TytuÅ‚" - blocks: - image: "ZdjÄ™cia" - post: "Formularz tworzenia" - _textInput: - text: "TytuÅ‚" - _textareaInput: - text: "TytuÅ‚" - _numberInput: - text: "TytuÅ‚" - _switch: - text: "TytuÅ‚" - _counter: - text: "TytuÅ‚" - _button: - text: "TytuÅ‚" - _radioButton: - title: "TytuÅ‚" - script: - categories: - random: "Losowy" - list: "Listy" - blocks: - _join: - arg1: "Listy" - random: "Losowy" - _randomPick: - arg1: "Listy" - _dailyRandomPick: - arg1: "Listy" - _seedRandomPick: - arg2: "Listy" - _pick: - arg1: "Listy" - _listLen: - arg1: "Listy" - types: - array: "Listy" -room: - translate: "PrzenieÅ›" - save: "Zapisz" - saved: "Zapisano" - furnitures: - moon: "Księżyc" - bin: "Kosz" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml deleted file mode 100644 index 772e1c198ca03a203c1180582055a30ea5d080c4..0000000000000000000000000000000000000000 --- a/locales/pt-PT.yml +++ /dev/null @@ -1,288 +0,0 @@ ---- -meta: - lang: "Português" -common: - misskey: "Uma â do fediverso" - about-title: "Uma â do fediverso." - about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mÃdia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?" - intro: - title: "O que é Misskey?" - about: "Misskey é um <b>serviço de microblog descentralizado</b>. Personalização sofisticada da interface, variedade de reações a posts, armazenamento de arquivos grátis com gerenciamento integrado e outras funções avançadas estão disponÃveis. Um sistema em rede chamado \"Fediverso\" permite que nos comuniquemos com usuários em outras redes sociais. Se você postar algo, por exemplo, seu post não será mandado apenas para o Misskey, mas também para o Mastodon. Apenas imagine que o planeta está enviando ondas de rádio para outros planetas para se comunicar." - features: "Recursos" - rich-contents: "Post" - rich-contents-desc: "Apenas poste suas ideias, temas do momento e qualquer coisa que você queira compartilhar. Você pode querer decorar suas palavras, anexar suas imagens favoritas, enviar arquivos, inclusive vÃdeos ou criar uma enquete. Essas são as coisas que você pode fazer em Misskey." - reaction: "Reações" - reaction-desc: "ã‚ãªãŸã®æ°—æŒã¡ã‚’ä¼ãˆã‚‹æœ€ã‚‚ç°¡å˜ãªæ–¹æ³•ã§ã™ã€‚Misskeyã¯ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ã«æ§˜ã€…ãªãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’付ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã„ã¡ã©Misskeyã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ©Ÿèƒ½ã‚’体験ã—ã¦ã—ã¾ã†ã¨ã€ã‚‚ã†ã€Œã„ã„ãã€ã®æ¦‚念ã—ã‹å˜åœ¨ã—ãªã„SNSã«ã¯æˆ»ã‚Œãªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“" - application-authorization: "Aplicativos autorizados" - close: "Fechar" - do-not-copy-paste: "Por favor, não digite ou copie o código aqui. A conta pode ser comprometida." - notification-types: - follow: "Seguindo" - got-it: "Entendi!" - customization-tips: - title: "Dicas de personalização" - gotit: "Entendi!" - notification: - file-uploaded: "Arquivo enviado!" - message-from: "Mensagem de {}:" - reversi-invited: "Convidado a jogar" - reversi-invited-by: "Convidado por {}:" - notified-by: "Notificado por {}:" - reply-from: "Resposta de {}:" - quoted-by: "Citado por {}:" - time: - unknown: "Desconhecido" - future: "futuro" - just_now: "agora" - seconds_ago: "{} sec atrás" - minutes_ago: "{} min atrás" - hours_ago: "{} h atrás" - days_ago: "{} d atrás" - weeks_ago: "{} sem atrás" - months_ago: "{} m atrás" - years_ago: "{} ano(s) atrás" - month-and-day: "{day}/{month}" - trash: "Lixo" - timeline: "Linha do tempo" - followers: "Seguidores" - post-form: - enter-username: "Digite o nome de usuário." - username-prompt: "Digite o nome de usuário." - weekday-short: - sunday: "Dom" - monday: "Seg" - tuesday: "Ter" - wednesday: "Qua" - thursday: "Qui" - friday: "Sex" - saturday: "Seb" - weekday: - sunday: "domingo" - monday: "segunda" - tuesday: "terça" - wednesday: "quarta" - thursday: "quinta" - friday: "sexta" - saturday: "sábado" - reactions: - like: "Curtir" - love: "Amei" - laugh: "Riso" - hmm: "Hmm...?" - surprise: "Uau" - congrats: "Parabéns!" - angry: "Raiva" - confused: "Confuso" - rip: "RIP" - pudding: "Pudim" - note-visibility: - followers: "Seguidores" - note-placeholders: - a: "O que está fazendo?" - b: "O que está acontecendo?" - c: "No que está pensando?" - d: "Quer postar algo?" - e: "Escreva aqui" - f: "Esperando você escrever." - _settings: - timeline: "Linha do tempo" - search: "Buscar" - delete: "Apagar" - loading: "Carregando" - update-available-title: "Atualização disponÃvel" - update-available: "Uma nova versão de Misskey está disponÃvel ({newer}). A versão atual é {current}. Recarregue a página para atualizar." - my-token-regenerated: "Seu token foi recriado, portanto você foi deslogado." - enter-username: "Digite o nome de usuário." - reversi: - drawn: "Empatado" - my-turn: "Seu turno" - opponent-turn: "Turno do oponente" - black: "Pretas" - white: "Brancas" - total: "Total" - widgets: - analog-clock: "Relógio analógico" - profile: "Perfil" - calendar: "Calendário" - timemachine: "Calendário (máquina do tempo)" - activity: "Atividade" - rss: "Leitor de RSS" - memo: "Nota adesiva" - trends: "Tendências" - posts-monitor: "Gráfico de publicações" - version: "Versão" - notifications: "Notificações" - users: "Usuário sugeridos" - polls: "Enquetes" - post-form: "Formulário de publicação" - server: "Informações do servidor" - nav: "Navegação" - tips: "Dicas" - hashtags: "Hashtags" - you: "Você" -auth/views/form.vue: - permission-ask: "Este aplicativo precisa das seguintes permissões:" - cancel: "Cancelar" - accept: "Permitir acesso" -auth/views/index.vue: - loading: "Carregando" - already-authorized: "Este aplicativo já foi autorizado" - allowed: "Aplicativos com acesso autorizado" - callback-url: "Voltando ao aplicativo" - please-go-back: "Por favor, volte ao aplicativo." - error: "A sessão não existe." - sign-in: "Por favor, entre." -common/views/components/games/reversi/reversi.index.vue: - invite: "Convidar" - rule: "Como jogar" - mode-invite: "Convidar" - mode-invite-desc: "Convidar um usuário para jogar" - invitations: "Você foi convidado!" - my-games: "Meu jogo" - all-games: "Todos os jogos" - enter-username: "Digite o nome de usuário." - game-state: - ended: "Terminado" -common/views/components/games/reversi/reversi.room.vue: - rules: "Regras" - cancel: "Cancelar" -common/views/components/connect-failed.troubleshooter.vue: - flush: "Limpar o cache" -common/views/components/theme.vue: - desc: "Descrição" -common/views/components/cw-button.vue: - poll: "Enquetes" -common/views/components/messaging.vue: - you: "Você" -common/views/components/note-menu.vue: - delete: "Apagar" -common/views/components/poll-editor.vue: - day: "Dom" -common/views/components/visibility-chooser.vue: - followers: "Seguidores" -common/views/components/profile-editor.vue: - name: "Nome" - export-targets: - following-list: "Seguindo" -common/views/components/user-group-editor.vue: - invite: "Convidar" -common/views/components/user-groups.vue: - invites: "Convidar" -common/views/widgets/posts-monitor.vue: - title: "Gráfico de publicações" -common/views/widgets/memo.vue: - title: "Nota adesiva" -common/views/pages/follow.vue: - follow: "Seguindo" -desktop/views/components/choose-file-from-drive-window.vue: - upload: "Envie arquivos do seu dispositivo" - ok: "OK" -desktop/views/components/choose-folder-from-drive-window.vue: - ok: "OK" -desktop/views/components/crop-window.vue: - ok: "OK" -desktop/views/input-dialog.vue: - ok: "OK" -common/views/components/api-settings.vue: - console: - parameter: "Parâmetros" -desktop/views/components/sub-note-content.vue: - poll: "Enquetes" -desktop/views/components/user-preview.vue: - following: "Seguindo" - followers: "Seguidores" -desktop/views/components/users-list-item.vue: - followed: "Te segue" -admin/views/abuse.vue: - remove-report: "Apagar" -admin/views/instance.vue: - invite: "Convidar" -admin/views/drive.vue: - delete: "Apagar" -admin/views/emoji.vue: - emojis: - remove: "Apagar" -admin/views/announcements.vue: - remove: "Apagar" -admin/views/federation.vue: - followers: "Seguidores" -desktop/views/pages/welcome.vue: - timeline: "Timeline" - powered-by-misskey: "Desenvolvido por <b>Misskey</b>." -desktop/views/pages/drive.vue: - title: "Drive Misskey" -desktop/views/pages/note.vue: - prev: "Nota anterior" - next: "Próxima nota" -desktop/views/pages/selectdrive.vue: - title: "Selecione um arquivo" - ok: "OK" - cancel: "Cancelar" - upload: "Envie arquivos do seu dispositivo" -desktop/views/pages/search.vue: - not-available: "A pesquisa está desligada nas configurações desta instância." -desktop/views/pages/user/user.followers-you-know.vue: - loading: "Carregando" -desktop/views/pages/user/user.friends.vue: - loading: "Carregando" -desktop/views/pages/user/user.photos.vue: - loading: "Carregando" -desktop/views/pages/user/user.header.vue: - following: "Seguindo" - followers: "Seguidores" - month: "Seg" - day: "Dom" - follows-you: "Te segue" -desktop/views/pages/user/user.timeline.vue: - with-media: "MÃdia" -desktop/views/widgets/polls.vue: - title: "Enquetes" -common/views/components/follow-button.vue: - follow: "Seguindo" -mobile/views/components/sub-note-content.vue: - poll: "Enquetes" -mobile/views/components/ui.nav.vue: - timeline: "Linha do tempo" -mobile/views/pages/widgets.vue: - customization-tips: "Dicas de personalização" -mobile/views/pages/note.vue: - prev: "Nota anterior" - next: "Próxima nota" -mobile/views/pages/search.vue: - search: "Pesquisar" -mobile/views/pages/user.vue: - follows-you: "Te segue" - following: "Seguindo" - followers: "Seguidores" - notes: "Posts" - timeline: "Linha do tempo" - media: "MÃdia" -mobile/views/pages/user/home.vue: - recent-notes: "Notas recentes" - images: "Imagens" - activity: "Atividade" - keywords: "Palavras chave" - domains: "DomÃnios" - followers-you-know: "Seguidores que você conhece" - last-used-at: "Ativo pela última vez" -mobile/views/pages/user/home.photos.vue: - no-photos: "Sem fotos" -deck/deck.user-column.vue: - follows-you: "Te segue" - following: "Seguindo" - followers: "Seguidores" - images: "Imagens" - timeline: "Linha do tempo" -docs: - edit-this-page-on-github-link: "Edite esta página no GitHub!" -dev/views/index.vue: - manage-apps: "Gerenciar aplicativos" -pages: - like: "Curtir" - blocks: - image: "Imagens" - post: "Formulário de publicação" -room: - furnitures: - moon: "Lua" - bin: "Lixo" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml deleted file mode 100644 index 3b665766a2412fc1668309c18e9f62918f2403ab..0000000000000000000000000000000000000000 --- a/locales/ru-RU.yml +++ /dev/null @@ -1,171 +0,0 @@ ---- -meta: - lang: "РуÑÑкий Ñзык" -common: - misskey: "Мы — â fediverse" - about-title: "Мы — â fediverse" - about: "СпаÑибо, что нашли Misskey. Misskey — Ñто <b>Ð´ÐµÑ†ÐµÐ½Ñ‚Ñ€Ð°Ð»Ð¸Ð·Ð¾Ð²Ð°Ð½Ð½Ð°Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ð° Ð´Ð»Ñ Ð¼Ð¸ÐºÑ€Ð¾Ð±Ð»Ð¾Ð³Ð³Ð¸Ð½Ð³Ð°</b> родом Ñ Ð¿Ð»Ð°Ð½ÐµÑ‚Ñ‹ ЗемлÑ. ПоÑкольку она ÑущеÑтвует внутри Fediverse (вÑеленной различных Ñоциальных платформ), она ÑвÑзана Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼Ð¸ платформами. Отдохните от шума большого города — и познакомьтеÑÑŒ Ñ Ð½Ð¾Ð²Ñ‹Ð¼ интернетом." - intro: - title: "Что такое Misskey?" - about: "Misskey - Ñто <b>децентрализованный ÑÐµÑ€Ð²Ð¸Ñ Ð¼Ð¸ÐºÑ€Ð¾Ð±Ð»Ð¾Ð³Ð¸Ð½Ð³Ð°</b> Ñ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¼ иÑходным кодом. Он имеет такие функции, как: навороченный, полноÑтью наÑтраиваемый пользовательÑкий интерфейÑ, множеÑтво реакций на поÑÑ‚Ñ‹, беÑплатное хранилище файлов Ñ Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð¾Ð¹ ÑиÑтемой ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸ ещё куча передовых фишек. Рещё ÑÐµÑ‚ÐµÐ²Ð°Ñ ÑиÑтема под названием “Fediverse†позволÑет нам общатьÑÑ Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñми других Ñоциальных Ñетей. Ðапример, еÑли Ñ‚Ñ‹ что-нибудь запоÑтишь, то твой поÑÑ‚ будет отоÑлан не только в Misskey, но ещё и mastodon. ПроÑто предÑтавь, что планета поÑылает микроволны на другую планету Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼ÑƒÐ½Ð¸ÐºÐ°Ñ†Ð¸Ð¸." - features: "ОÑобенноÑти" - rich-contents: "ПоÑÑ‚Ñ‹" - rich-contents-desc: "ПроÑто выложи Ñвою идею, актуальные темы и вÑÑ‘, что тебе хочетÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°Ñ‚ÑŒ миру. Ты можешь декорировать Ñвои Ñлова, прикреплÑÑ‚ÑŒ Ñвои любимые картинки, отправлÑÑ‚ÑŒ файлы Ñ Ñ„Ð¸Ð»ÑŒÐ¼Ð°Ð¼Ð¸ и Ñоздать голоÑование - Ñто те вещи, которые Ñ‚Ñ‹ можешь Ñделать Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Misskey!" - reaction: "Реакции" - reaction-desc: "ã‚ãªãŸã®æ°—æŒã¡ã‚’ä¼ãˆã‚‹æœ€ã‚‚ç°¡å˜ãªæ–¹æ³•ã§ã™ã€‚Misskeyã¯ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ã«æ§˜ã€…ãªãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’付ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã„ã¡ã©Misskeyã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ©Ÿèƒ½ã‚’体験ã—ã¦ã—ã¾ã†ã¨ã€ã‚‚ã†ã€Œã„ã„ãã€ã®æ¦‚念ã—ã‹å˜åœ¨ã—ãªã„SNSã«ã¯æˆ»ã‚Œãªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。" - ui: "ИнтерфейÑ" - ui-desc: "Ðет такого интерфейÑа, понравившегоÑÑ Ð²Ñем. ПоÑтому у Misskey имеетÑÑ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÑŒÑкий интерфейÑ, широко наÑтраиваемый под ваши вкуÑÑ‹. Создай Ñебе уникальную домашнюю Ñтраницу редактируÑ, подÑÑ‚Ñ€Ð°Ð¸Ð²Ð°Ñ Ð¾Ñ„Ð¾Ñ€Ð¼Ð»ÐµÐ½Ð¸Ðµ ленты и Ñ€Ð°Ð·Ð¼ÐµÑ‰Ð°Ñ Ð²Ð¸Ð´Ð¶ÐµÑ‚Ñ‹, которые тоже можно каÑтомизировать." - drive: "Хранилище файлов" - drive-desc: "Хотите запоÑтить картинку, которую уже отправлÑли ранее? ХочетÑÑ Ñортировать, переименовать и Ñоздать папку Ð´Ð»Ñ Ð²Ð°ÑˆÐ¸Ñ… выложенных файлов? Тогда Misskey Drive - Ñто лучшее решение Ð´Ð»Ñ Ð²Ð°Ñ. Очень лёгкий ÑпоÑоб делитьÑÑ Ñвоими файлами онлайн." - outro: "Попробуйте будущие, уникальные Ð´Ð»Ñ Misskey функции Ñвоими глазами! ЕÑли чувÑтвуете, что Ñто не в вашем вкуÑе, то попробуйте другие инÑтанции, ведь Misskey - Ñто Ð´ÐµÑ†ÐµÐ½Ñ‚Ñ€Ð°Ð»Ð¸Ð·Ð¾Ð²Ð°Ð½Ð½Ð°Ñ ÑÐ¾Ñ†Ð¸Ð°Ð»ÑŒÐ½Ð°Ñ Ñеть, так что Ñ‚Ñ‹ можешь Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью найти Ñебе товарищей. И наконец, GLHF!" - application-authorization: "ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹" - close: "Закрыть" - do-not-copy-paste: "ПожалуйÑта, не вводите и не вÑтавлÑйте Ñюда код. Ðккаунту может угрожать опаÑноÑÑ‚ÑŒ." - load-more: "Загрузить больше" - enter-password: "ПожалуйÑта, введите ваш пароль" - 2fa: "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" - customize-home: "ÐаÑтройка домашней Ñтраницы" - featured-notes: "Рекомендуемые" - dark-mode: "Ð¢Ñ‘Ð¼Ð½Ð°Ñ Ñ‚ÐµÐ¼Ð°" - signin: "Войти" - signup: "РегиÑтрациÑ" - signout: "Выйти" - reload-to-apply-the-setting: "Вам необходимо перезагрузить Ñтраницу, чтобы применить наÑтройки. Ð’Ñ‹ хотите перезагрузить ÑейчаÑ?" - customization-tips: - title: "Советы по наÑтройке" - gotit: "ПонÑтно!" - notification: - file-uploaded: "Файл отправлен!" - message-from: "Сообщение от {}:" - reversi-invited: "Приглашён в игру" - reversi-invited-by: "Был приглашён {}:" - notified-by: "Был приглашён {}:" - reply-from: "Ответ от {}:" - quoted-by: "Цитировано {}:" - time: - unknown: "неизвеÑтно" - future: "ÑейчаÑ" - just_now: "ÑейчаÑ" - seconds_ago: "{} Ñекунд назад" - minutes_ago: "{} минут назад" - hours_ago: "{} чаÑов назад" - days_ago: "{} дней назад" - weeks_ago: "{} недель назад" - months_ago: "{} меÑÑцев назад" - years_ago: "{} лет назад" - month-and-day: "{day}.{month}" - trash: "МуÑорное ведро" - drive: "Drive" - pages: "Страницы" - messaging: "Чат" - timeline: "Лента" - followers: "ПодпиÑчики" - favorites: "Избранное" - post-form: - reply: "Ответить" - create-poll: "Создать опроÑ" - weekday-short: - sunday: "Ð’Ñ" - monday: "Пн" - tuesday: "Ð’Ñ‚" - wednesday: "Ср" - thursday: "Чт" - friday: "Пт" - saturday: "Сб" - weekday: - sunday: "ВоÑкреÑенье" - monday: "Понедельник" - tuesday: "Вторник" - wednesday: "Среда" - thursday: "Четверг" - friday: "ПÑтница" - saturday: "Суббота" - reactions: - like: "ÐравитÑÑ" - laugh: "Ха-Ха" - rip: "RIP" - do-not-use-in-production: "Ðта Ñборка Ð´Ð»Ñ Ñ€Ð°Ð·Ñ€Ð°Ð±Ð¾Ñ‚Ñ‡Ð¸ÐºÐ¾Ð². Ðе иÑпользуйте в продакшне." - error: - title: "Что-то пошло не так :(" - retry: "Повторить" - reversi: - drawn: "ÐичьÑ" - my-turn: "Ваш ход" - opponent-turn: "Ход оппонента" - turn-of: "Ход {name}" - past-turn-of: "Ход {name}" - won: "{name} победил" - black: "Чёрный" - white: "Белый" - total: "Ð’Ñего" - this-turn: "Ход {count}" - widgets: - analog-clock: "Ðналоговые чаÑÑ‹" - profile: "Профиль" - calendar: "Календарь" - timemachine: "Календарь (машина времени)" - activity: "ÐктивноÑÑ‚ÑŒ" - rss: "Ридер RSS" - memo: "Заметка" - trends: "ПопулÑрное" - photo-stream: "Фотопоток" - slideshow: "Слайдшоу" - version: "ВерÑиÑ" - notifications: "УведомлениÑ" - users: "Рекомендованные пользователи" - polls: "ГолоÑованиÑ" - server: "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ Ñервере" - hashtags: "Ð¥Ñштеги" - dev: "Ðе удалоÑÑŒ Ñоздать приложение. ПожалуйÑта, попробуйте ещё раз." - ai-chan-kawaii: "Ai-chan kawaii!" -auth/views/form.vue: - share-access: "Ð’Ñ‹ разрешаете <i>{name}</i> получить доÑтуп к вашему аккаунту?" -common/views/components/games/reversi/reversi.index.vue: - game-state: - ended: "Завершено" - playing: "Ð’ процеÑÑе" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "ÐаÑтройки игры" - random: "Случайно" - black-or-white: "Чёрные/Белые" - black-is: "{} ходит чёрными" - rules: "Правила" - settings-of-the-bot: "ÐаÑтройки бота" - this-game-is-started-soon: "Игра вот-вот начнётÑÑ" - waiting-for-other: "Ожидание оппонента" - cancel: "Отмена" - ready: "Готов" -common/views/components/connect-failed.vue: - title: "Ðевозможно подключитьÑÑ Ðº Ñерверу" -common/views/components/cw-button.vue: - poll: "ГолоÑованиÑ" -common/views/components/poll-editor.vue: - day: "Ð’Ñ" -common/views/widgets/memo.vue: - title: "Заметка" -desktop/views/components/sub-note-content.vue: - poll: "ГолоÑованиÑ" -admin/views/dashboard.vue: - drive: "Хранилище файлов" -admin/views/charts.vue: - drive: "Хранилище файлов" -desktop/views/pages/user/user.header.vue: - month: "Пн" - day: "Ð’Ñ" -desktop/views/widgets/polls.vue: - title: "ГолоÑованиÑ" -mobile/views/components/sub-note-content.vue: - poll: "ГолоÑованиÑ" -mobile/views/pages/widgets.vue: - customization-tips: "Советы по наÑтройке" -pages: - like: "ÐравитÑÑ" - script: - categories: - random: "Случайно" - blocks: - random: "Случайно" -room: - furnitures: - moon: "Луна" - bin: "МуÑорное ведро" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml deleted file mode 100644 index 9e379c5c29f828495deafd877e46418d319244ca..0000000000000000000000000000000000000000 --- a/locales/zh-CN.yml +++ /dev/null @@ -1,2173 +0,0 @@ ---- -meta: - lang: "ä¸æ–‡(简体)" -common: - misskey: "è”邦宇宙ä¸çš„一颗â" - about-title: "è”邦宇宙ä¸çš„一颗â" - about: "éžå¸¸æ„Ÿè°¢æ‚¨æ‰¾åˆ°äº†Misskey。 Misskey是诞生于地çƒçš„<b>分布å¼å¾®åšSNS</b>ã€‚å› ä¸ºå¥¹å¤„äºŽè”邦宇宙(ç”±å„ç§SNS组æˆçš„宇宙)ä¸ï¼Œæ‰€ä»¥å¥¹ä¸Žå…¶ä»–SNS相互连接。为什么ä¸è¯•è¯•è¿œç¦»å–§åš£çš„城市,潜入这片新的网络海洋之ä¸å‘¢ï¼Ÿ" - intro: - title: "什么是 Misskey å‘¢?" - about: "Misskey是开æºçš„<b>分散å¼å¾®åšSNS</b>。丰富且å¯ä»¥é«˜åº¦å®šåˆ¶çš„Ui,对别人的帖å进行å„ç§å›žåº”,集æˆç®¡ç†ç³»ç»Ÿçš„网盘ç‰å…ˆè¿›åŠŸèƒ½ã€‚æ¤å¤–,称为“è”邦世界â€çš„网络系统使我们能够与其他SNSçš„ç”¨æˆ·è¿›è¡Œé€šä¿¡ã€‚æ¯”å¦‚ï¼Œå¦‚æžœä½ å‘布一个帖åï¼Œé‚£ä¹ˆä½ çš„å¸–åä¸ä»…会å‘é€ç»™Misskey,还会å‘é€åˆ°å…¶ä»–SNSå¹³å°ã€‚想象一下,æ£å¦‚一颗行星和å¦ä¸€é¢—行星通过电ç£æ³¢æ¥è¿›è¡Œé€šä¿¡ä¸€æ ·ã€‚" - features: "功能" - rich-contents: "å‘布" - rich-contents-desc: "请分享您的想法,çƒé—¨è¯é¢˜ï¼Œä»¥åŠä»»ä½•æ‚¨æƒ³ä¸Žå¤§å®¶åˆ†äº«çš„内容。如果有需è¦çš„è¯ï¼Œæ‚¨å¯ä»¥ä½¿ç”¨å„ç§è¯æ³•æ¥ä¿®é¥°æ–‡ç« ,å‘布问å·è°ƒæŸ¥ï¼Œæˆ–è€…æ·»åŠ å„ç§æ‚¨å–œæ¬¢çš„图åƒå’Œè§†é¢‘ç‰æ–‡ä»¶ã€‚" - reaction: "回应" - reaction-desc: "这是表达情绪的最简å•æ–¹æ³•ã€‚ Misskeyå…许您å‘其他帖åæ·»åŠ å„ç§ç±»åž‹çš„回应。 一旦体验过Misskey的回应功能,就å†ä¹Ÿä¸ä¼šæƒ³å›žåˆ°é‚£äº›åªæœ‰ç‚¹èµžåŠŸèƒ½çš„其他SNS上了。" - ui: "交互界é¢" - ui-desc: "世界上没有一个UIå¯ä»¥é€‚åˆæ¯ä¸€ä¸ªäºº. 所以, Misskey æ供一个å¯ä»¥é«˜åº¦å®šåˆ¶çš„UI交互界é¢. 您å¯ä»¥é€šè¿‡ç¼–辑, 调整布局, 放置å¯é€‰æ‹©çš„å°éƒ¨ä»¶æ¥è½»æ¾å®šåˆ¶æ‚¨çš„专属UIç•Œé¢ã€‚" - drive: "网盘" - drive-desc: "想è¦å‘å¸ƒä¸€å¼ æ‚¨å·²ç»ä¸Šä¼ 过的照片å—?想è¦ç®¡ç†æ–‡ä»¶æˆ–ä¸ºä¸Šä¼ çš„æ–‡ä»¶åˆ›å»ºæ–‡ä»¶å¤¹å—?Misskey的内置网盘将为您完美解决这些问题。简å•åœ°åˆ†äº«æ‚¨çš„文件。" - outro: "Misskey还有其他更多功能,请亲身体验一下å§ã€‚å› ä¸º Misskey 是一个分布å¼çš„ SNS,如果您感觉æŸä¸ªåŠŸèƒ½ä¸é€‚åˆè‡ªå·±ï¼Œè¯•è¯•å…¶ä»–çš„å§ã€‚ç¥æ‚¨çŽ©å¾—开心ï¼" - application-authorization: "应用程åºæŽˆæƒ" - close: "å…³é—" - do-not-copy-paste: "请ä¸è¦åœ¨è¿™é‡Œè¾“入或粘贴代ç 。您å¸æˆ·å¯èƒ½ä¼šå—到æŸå®³ã€‚" - load-more: "åŠ è½½æ›´å¤š" - enter-password: "请输入您的密ç " - 2fa: "åŒé‡èº«ä»½éªŒè¯" - customize-home: "自定义主页" - featured-notes: "高亮" - dark-mode: "黑暗模å¼" - signin: "登录" - signup: "注册" - signout: "退出" - reload-to-apply-the-setting: "å¿…é¡»é‡æ–°åŠ 载页é¢ä»¥åº”用æ¤è®¾ç½®ã€‚ 确实è¦ç«‹å³é‡æ–°åŠ è½½å—?" - fetching-as-ap-object: "è”åˆæŸ¥è¯¢" - unfollow-confirm: "å–消对{name}的关注?" - delete-confirm: "ç¡®å®šåˆ é™¤è¿™ä¸ªæŠ•ç¨¿å—?" - signin-required: "请先登录" - notification-type: "通知类型" - notification-types: - all: "所有" - pollVote: "投票" - follow: "关注ä¸" - receiveFollowRequest: "关注请求" - reply: "回å¤" - quote: "引用" - renote: "转推" - mention: "æåŠ" - reaction: "回应" - got-it: "知é“了" - customization-tips: - title: "自定义æ示" - paragraph: "<p>主页定制å…è®¸æ‚¨æ·»åŠ æˆ–åˆ é™¤, 拖放和é‡æ–°æŽ’列å°ç»„件.</p></p>您å¯ä»¥é€šè¿‡<strong><strong>å³é”®</strong>点击</strong>æŸäº›å°éƒ¨ä»¶æ¥æ›´æ”¹æ˜¾ç¤º</p><p>è‹¥è¦åˆ 除å°éƒ¨ä»¶, è¯·å°†å…¶æ‹–åˆ°æ ‡å¤´ä¸º<strong>「垃圾箱ã€</strong>的区域</p><p>如果您完æˆäº†å®šåˆ¶è¿‡ç¨‹,å•å‡»å³ä¸Šè§’的「完æˆã€</p>" - gotit: "明白了ï¼" - notification: - file-uploaded: "æ–‡ä»¶å·²ä¸Šä¼ " - message-from: "æ¥è‡ª{}的消æ¯ï¼š" - reversi-invited: "æ‚¨å·²è¢«é‚€è¯·åŠ å…¥ä¸€åœºæ¸¸æˆ" - reversi-invited-by: "æ¥è‡ª{}的邀请" - notified-by: "æ¥è‡ª{}的通知" - reply-from: "æ¥è‡ª{}的回å¤ï¼š" - quoted-by: "æ¥è‡ª{}的引用:" - time: - unknown: "未知" - future: "未æ¥" - just_now: "刚刚" - seconds_ago: "{}秒å‰" - minutes_ago: "{}分å‰" - hours_ago: "{}å°æ—¶å‰" - days_ago: "{}天å‰" - weeks_ago: "{}周å‰" - months_ago: "{}月å‰" - years_ago: "{}å¹´å‰" - month-and-day: "{month}月 {day}æ—¥" - trash: "垃圾箱" - drive: "网盘" - pages: "页é¢" - messaging: "èŠå¤©" - home: "首页" - deck: "Deck" - timeline: "时间线" - explore: "å‘现" - following: "æ£åœ¨å…³æ³¨" - followers: "关注者" - favorites: "最爱" - permissions: - "read:account": "查看账户信æ¯" - "write:account": "更改我的å¸æˆ·ä¿¡æ¯" - "read:blocks": "查看黑åå•" - "write:blocks": "编辑黑åå•" - "read:drive": "查看网盘" - "write:drive": "管ç†ç½‘盘文件" - "read:favorites": "查看收è—夹" - "write:favorites": "编辑收è—夹" - "read:following": "查看关注信æ¯" - "write:following": "关注/å–消关注" - "read:messaging": "查看对è¯" - "write:messaging": "对è¯æ“作" - "read:mutes": "查看å±è”½åˆ—表" - "write:mutes": "编辑å±è”½åˆ—表" - "write:notes": "åˆ›å»ºæˆ–åˆ é™¤å¸–å" - "read:notifications": "查看通知" - "write:notifications": "管ç†é€šçŸ¥" - "read:reactions": "查看回应" - "write:reactions": "回应æ“作" - "write:votes": "投票" - "read:pages": "查看页é¢" - "write:pages": "æ“作页é¢" - "read:page-likes": "查看喜欢的页é¢" - "write:page-likes": "æ“作喜欢的页é¢" - "read:user-groups": "查看用户组" - "write:user-groups": "æ“作用户组" - empty-timeline-info: - follow-users-to-make-your-timeline: "关注其他用户时,帖å将显示在时间线ä¸ã€‚" - explore: "查找用户" - post-form: - attach-location-information: "æ·»åŠ ä½ç½®ä¿¡æ¯" - hide-contents: "éšè—内容" - reply-placeholder: "回å¤æ¤è´´..." - quote-placeholder: "引用æ¤å¸–…" - option-quote-placeholder: "引用æ¤å¸–…(å¯é€‰)" - quote-attached: "已引用" - quote-question: "是å¦å°†å…¶ä½œä¸ºå¼•ç”¨é™„上?" - submit: "帖å" - reply: "回å¤" - renote: "转推" - posting: "å‘é€ä¸" - attach-media-from-local: "从PCä¸æ·»åŠ 媒体文件" - attach-media-from-drive: "从网盘ä¸æ·»åŠ 媒体文件" - insert-a-kao: "v('ω')v" - create-poll: "创建一个投票" - text-remain: "还剩{}个å—符" - recent-tags: "最近" - local-only-message: "è¿™ç¯‡æ–‡ç« åªä¼šåœ¨æœ¬åœ°å‘布" - click-to-tagging: "ç‚¹å‡»æ·»åŠ æ ‡ç¾" - visibility: "å¯è§æ€§" - geolocation-alert: "您的设备ä¸æ”¯æŒå®šä½æœåŠ¡" - error: "错误" - enter-username: "输入用户å" - specified-recipient: "收件人" - add-visible-user: "æ·»åŠ ç”¨æˆ·" - cw-placeholder: "评论帖å(å¯é€‰)" - username-prompt: "输入用户å" - enter-file-name: "编辑文件å" - weekday-short: - sunday: "æ—¥" - monday: "一" - tuesday: "二" - wednesday: "三" - thursday: "å››" - friday: "五" - saturday: "å…" - weekday: - sunday: "星期日" - monday: "星期一" - tuesday: "星期二 " - wednesday: "星期三" - thursday: "星期四" - friday: "星期五" - saturday: "星期å…" - reactions: - like: "赞" - love: "喜爱" - laugh: "笑" - hmm: "emmm...?" - surprise: "哇! " - congrats: "æå–œ" - angry: "生气" - confused: "困惑" - rip: "RIP" - pudding: "布ä¸" - note-visibility: - public: "公开" - home: "首页" - home-desc: "ä»…å‘é€è‡³é¦–页的时间线" - followers: "关注者" - followers-desc: "ä»…å‘é€è‡³ç²‰ä¸" - specified: "指定用户" - specified-desc: "ä»…å‘é€è‡³æŒ‡å®šç”¨æˆ·" - local-public: "公开(仅é™æœ¬åœ°ï¼‰" - local-home: "首页(仅é™æœ¬åœ°ï¼‰" - local-followers: "关注者(仅é™æœ¬åœ°ï¼‰" - note-placeholders: - a: "现在在åšä»€ä¹ˆï¼Ÿ" - b: "å‘生了什么?" - c: "ä½ æœ‰ä»€ä¹ˆæƒ³æ³•ï¼Ÿ" - d: "ä½ æƒ³è¦å‘布些什么å—?" - e: "请写下æ¥å§" - f: "ç‰å¾…您的å‘布..." - settings: "设置" - _settings: - profile: "个人资料" - notification: "通知" - apps: "应用程åº" - tags: "è¯é¢˜æ ‡ç¾" - mute-and-block: "å±è”½/拉黑" - blocking: "拉黑" - security: "安全性" - signin: "登录历å²" - password: "密ç " - other: "其他" - appearance: "设计" - behavior: "行为" - reactions: "回应" - reactions-description: "快速选择回应ä¸çš„自定义表情符å·ï¼Œä»¥æ¢è¡Œç¬¦åˆ†éš”。" - fetch-on-scroll: "å‘ä¸‹æ»šåŠ¨æ—¶è‡ªåŠ¨åŠ è½½" - fetch-on-scroll-desc: "å‘下滚动页é¢æ—¶ï¼Œå®ƒä¼šè‡ªåŠ¨æå–其他内容。" - note-visibility: "帖åå¯è§æ€§" - default-note-visibility: "默认å¯è§æ€§" - remember-note-visibility: "è®°ä½å¸–åå¯è§æ€§" - web-search-engine: "æœç´¢å¼•æ“Ž" - web-search-engine-desc: "例如: https://www.google.com/?#q={{query}}" - paste: "粘贴" - pasted-file-name: "粘贴的文件å模æ¿" - pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" - paste-dialog: "粘贴时编辑文件å" - paste-dialog-desc: "粘贴时显示编辑文件å的对è¯æ¡†" - keep-cw: "ä¿ç•™å†…容è¦å‘Š" - keep-cw-desc: "在回å¤å¸–å时,如果原帖设置了内容è¦å‘Šï¼Œé»˜è®¤æƒ…况下回帖也会设置相åŒçš„内容è¦å‘Šã€‚" - i-like-sushi: "相比于布ä¸æ¥è¯´, 我更喜欢寿å¸ã€‚" - show-reversi-board-labels: "在黑白棋ä¸æ˜¾ç¤ºè¡Œå’Œåˆ—表ç¾" - use-avatar-reversi-stones: "用头åƒä½œä¸ºé»‘白棋的棋å" - disable-animated-mfm: "在帖åä¸ç¦ç”¨åŠ¨ç”»æ–‡æœ¬" - disable-showing-animated-images: "ä¸æ’放动画" - enable-quick-notification-view: "å¯ç”¨é€šçŸ¥å¿«é€ŸæŸ¥çœ‹" - suggest-recent-hashtags: "在帖å表å•ä¸Šæ˜¾ç¤ºæœ€è¿‘æµè¡Œçš„å“ˆå¸Œæ ‡ç¾" - always-show-nsfw: "总是显示 NSFW 的内容" - always-mark-nsfw: "总是用 NSFW æ¥æ ‡è®°é™„件" - show-full-acct: "ä¸è¦ä»Žç”¨æˆ·åä¸å¿½ç•¥ä¸»æœºå" - show-via: "显示 via" - reduce-motion: "å‡å¼±UIä¸çš„动画效果" - this-setting-is-this-device-only: "设置仅在本设备ä¸ç”Ÿæ•ˆ" - use-os-default-emojis: "使用设备系统默认的表情符å·" - line-width: "线æ¡å®½åº¦" - line-width-thin: "细" - line-width-normal: "æ£å¸¸" - line-width-thick: "ç²—" - font-size: "æ–‡å—大å°" - font-size-x-small: "å°" - font-size-small: "较å°" - font-size-medium: "普通" - font-size-large: "较大" - font-size-x-large: "大" - deck-column-align: "列对é½è®¾ç½®" - deck-column-align-center: "ä¸å¤®" - deck-column-align-left: "å·¦" - deck-column-align-flexible: "å¯å˜" - deck-column-width: "Deck列宽" - deck-column-width-narrow: "窄" - deck-column-width-narrower: "很窄" - deck-column-width-normal: "普通" - deck-column-width-wider: "很宽" - deck-column-width-wide: "宽" - use-shadow: "在UIä¸ä½¿ç”¨é˜´å½±æ•ˆæžœ" - rounded-corners: "UIç•Œé¢åœ†è§’效果" - circle-icons: "使用圆形头åƒ" - contrasted-acct: "å¢žåŠ ç”¨æˆ·å的对比度" - wallpaper: "å£çº¸" - choose-wallpaper: "选择å£çº¸" - delete-wallpaper: "移除å£çº¸" - post-form-on-timeline: "在时间线顶部显示帖å表å•" - show-clock-on-header: "在å³ä¸Šè§’显示时钟" - show-reply-target: "显示回å¤ç›®æ ‡" - timeline: "时间线" - show-my-renotes: "在时间线上显示我的转推" - show-renoted-my-notes: "在时间线上显示我的帖å的转推" - show-local-renotes: "在时间线上显示本地帖å的转推" - remain-deleted-note: "继ç»æ˜¾ç¤ºå·²åˆ 除的帖å" - sound: "声音" - enable-sounds: "å¼€å¯å£°éŸ³" - enable-sounds-desc: "收到帖å/留言时æ’放声音。 æ¤è®¾ç½®å°†è¢«å˜å‚¨åœ¨æµè§ˆå™¨ä¸ã€‚" - volume: "音é‡" - test: "测试" - update: "Misskeyæ›´æ–°" - version: "版本:" - latest-version: "最新版本:" - update-checking: "æ£åœ¨æ£€æŸ¥æ›´æ–°" - do-update: "检查更新" - update-settings: "详细设置" - no-updates: "æ— å¯ç”¨æ›´æ–°" - no-updates-desc: "您所使用的 Misskey å·²ç»æ˜¯æœ€æ–°ç‰ˆæœ¬ã€‚" - update-available: "有å¯ç”¨çš„新版本" - update-available-desc: "é‡æ–°åŠ 载页é¢ä»¥åº”用更新。" - advanced-settings: "高级设置" - debug-mode: "å¯ç”¨è°ƒè¯•æ¨¡å¼" - debug-mode-desc: "æ¤è®¾ç½®å˜å‚¨åœ¨æµè§ˆå™¨ä¸ã€‚" - navbar-position: "导航æ ä½ç½®" - navbar-position-top: "顶部" - navbar-position-left: "左边" - navbar-position-right: "å³è¾¹" - i-am-under-limited-internet: "我的带宽有é™" - post-style: "å‘å¸–çš„å±•ç¤ºé£Žæ ¼" - post-style-standard: "æ ‡å‡†" - post-style-smart: "Smart" - notification-position: "通知形å¼" - notification-position-bottom: "底部" - notification-position-top: "顶部" - disable-via-mobile: "ä¸è¦å°†å¸–åæ ‡è®°ä¸ºâ€œæ¥è‡ªæ‰‹æœºâ€" - load-raw-images: "以原始质é‡æ˜¾ç¤ºé™„åŠ å›¾åƒ" - load-remote-media: "显示æ¥è‡ªè¿œç¨‹æœåŠ¡å™¨çš„媒体" - sync: "åŒæ¥" - save: "ä¿å˜" - saved: "å·²ä¿å˜" - preview: "预览" - home-profile: "定制首页数æ®" - deck-profile: "定制Deckæ•°æ®" - room: "房间" - _room: - graphicsQuality: "图形质é‡" - _graphicsQuality: - ultra: "最高" - high: "高" - medium: "ä¸" - low: "低" - cheep: "最低" - useOrthographicCamera: "使用æ£äº¤ç›¸æœº" - search: "æœç´¢" - delete: "åˆ é™¤" - loading: "æ£åœ¨åŠ è½½ä¸" - ok: "确定" - cancel: "å–消" - update-available-title: "有å¯ç”¨æ›´æ–°" - update-available: "æ–°çš„ Misskey 版本现已å‘布({newer}。目å‰ç‰ˆæœ¬{current}). 刷新页é¢ä»¥åº”用更新。" - my-token-regenerated: "您的 Token 已被é‡ç½®, 您将自动登出。" - hide-password: "éšè—密ç " - show-password: "显示密ç " - enter-username: "输入用户å" - do-not-use-in-production: "这是一个开å‘者测试版. 请勿在生产环境ä¸ä½¿ç”¨." - user-suspended: "该用户已被冻结。" - is-remote-user: "æ¤ç”¨æˆ·ä¿¡æ¯å¯èƒ½ä¸å‡†ç¡®ã€‚" - is-remote-post: "该投稿已被å¤åˆ¶." - view-on-remote: "查看准确的信æ¯" - renoted-by: "ç”± {user} 转推" - no-notes: "没有帖å" - turn-on-darkmode: "切æ¢æš—色主题" - turn-off-darkmode: "切æ¢äº®è‰²ä¸»é¢˜" - error: - title: "出现问题" - retry: "é‡è¯•" - reversi: - drawn: "平局" - my-turn: "è½®åˆ°ä½ äº†" - opponent-turn: "轮到对手了" - turn-of: "{name}的回åˆ" - past-turn-of: "轮到{name}的回åˆäº†" - won: "{name}获胜" - black: "黑" - white: "白" - total: "总计" - this-turn: "{count}回åˆ" - widgets: - analog-clock: "模拟时钟" - profile: "个人资料" - calendar: "日历" - timemachine: "日历 (时间机器)" - activity: "动æ€" - rss: "RSS阅读器" - memo: "便ç¾" - trends: "趋势" - photo-stream: "照片æµ" - posts-monitor: "投稿图表" - slideshow: "å¹»ç¯ç‰‡" - version: "版本" - broadcast: "广æ’" - notifications: "通知" - users: "推è用户" - polls: "调查问å·" - post-form: "投稿窗å£" - server: "æœåŠ¡å™¨ä¿¡æ¯" - nav: "导航" - tips: "æ示" - hashtags: "å“ˆå¸Œæ ‡ç¾" - queue: "队列" - dev: "构建应用程åºå¤±è´¥ï¼Œè¯·å†è¯•ä¸€æ¬¡ã€‚" - ai-chan-kawaii: "å°è“真å¯çˆ±" - you: "您" -auth/views/form.vue: - share-access: "您è¦å…许<i>{name}</i>æ¥è®¿é—®æ‚¨çš„账户å—?" - permission-ask: "这个应用程åºéœ€è¦ä»¥ä¸‹æƒé™:" - cancel: "å–消" - accept: "å…许访问。" -auth/views/index.vue: - loading: "æ£åœ¨åŠ è½½ä¸" - denied: "已拒ç»åº”用程åºæŽˆæƒã€‚" - denied-paragraph: "这个应用程åºå°†ä¸ä¼šè®¿é—®æ‚¨çš„账户" - already-authorized: "这个应用程åºå·²æŽˆæƒã€‚" - allowed: "å…许应用程åºæŽˆæƒã€‚" - callback-url: "回到应用程åºã€‚" - please-go-back: "请返回到应用程åº" - error: "会è¯ä¸å˜åœ¨ã€‚" - sign-in: "请登录。" -common/views/pages/explore.vue: - pinned-users: "已置顶用户" - popular-users: "çƒé—¨ç”¨æˆ·" - recently-updated-users: "活跃用户" - recently-registered-users: "新用户" - recently-discovered-users: "最近å‘现的用户" - popular-tags: "çƒé—¨æ ‡ç¾" - federated: "è”邦" - explore: "查找{host}" - explore-fediverse: "探索Fediverse" - users-info: "当å‰æœ‰{users}个注册用户" -common/views/components/reactions-viewer.details.vue: - few-users: "{users}作出了{reaction}的回应" - many-users: "{users}和其他{omitted}人åšå‡ºäº†{reaction}的回应" -common/views/components/url-preview.vue: - enable-player: "打开æ’放器" - disable-player: "å…³é—æ’放器" -common/views/components/user-list.vue: - no-users: "æ— ç”¨æˆ·" -common/views/components/games/reversi/reversi.vue: - matching: - waiting-for: "ç‰å¾… {}" - cancel: "å–消" -common/views/components/games/reversi/reversi.game.vue: - surrender: "认输" - surrendered: "已认输" - is-llotheo: "棋å较少一方获胜(LLoTheO规则)" - looped-map: "循环棋盘" - can-put-everywhere: "å¯ä»¥ä¸‹åœ¨ä»»æ„ä½ç½®" -common/views/components/games/reversi/reversi.index.vue: - title: "Misskey 黑白棋" - sub-title: "和其他人一起æ¥çŽ©Misskey黑白棋" - invite: "邀请" - rule: "游æˆè¯´æ˜Ž" - rule-desc: "黑白棋是一ç§æ£‹ç›˜æ¸¸æˆã€‚两人交替在棋盘上è½å,并将该棋åå’Œå¦ä¸€ä¸ªå·±æ–¹æ£‹å之间的对方棋å转æ¢æˆè‡ªå·±çš„颜色。最终ä¿ç•™æœ€å¤šæ£‹å的人获胜。" - mode-invite: "邀请" - mode-invite-desc: "邀请指定用户å‚åŠ æ¸¸æˆ" - invitations: "您收到了一则邀请!" - my-games: "我的游æˆ" - all-games: "所有游æˆ" - enter-username: "输入用户å" - game-state: - ended: "结æŸ" - playing: "游æˆè¿›è¡Œä¸" -common/views/components/games/reversi/reversi.room.vue: - settings-of-the-game: "游æˆè®¾ç½®" - choose-map: "棋盘选择" - random: "éšæœº" - black-or-white: "黑/白" - black-is: "{}是黑" - rules: "规则" - is-llotheo: "棋å较少一方获胜(LLoTheO规则)" - looped-map: "循环棋盘" - can-put-everywhere: "å¯ä»¥ä¸‹åœ¨ä»»æ„ä½ç½®" - settings-of-the-bot: "机器人设定" - this-game-is-started-soon: "游æˆå³å°†åœ¨æ•°ç§’åŽå¼€å§‹" - waiting-for-other: "ç‰å¾…对手准备" - waiting-for-me: "ç‰å¾…您的准备" - waiting-for-both: "准备ä¸" - cancel: "å–消" - ready: "准备完æˆ" - cancel-ready: "å–消准备" -common/views/components/connect-failed.vue: - title: "æ— æ³•è¿žæŽ¥åˆ°æœåŠ¡å™¨" - description: "您的网络连接å¯èƒ½å‡ºçŽ°äº†é—®é¢˜, 或是远程æœåŠ¡å™¨æš‚æ—¶ä¸å¯ç”¨. 请ç¨åŽ{é‡è¯•}." - thanks: "感谢您使用 Misskey" - troubleshoot: "故障排除" -common/views/components/connect-failed.troubleshooter.vue: - title: "æ£åœ¨æŽ’除故障" - network: "网络已连接" - checking-network: "æ£åœ¨æ£€æŸ¥ç½‘络连接" - internet: "网络连接" - checking-internet: "æ£åœ¨æ£€æŸ¥ç½‘络连接" - server: "已连接至æœåŠ¡å™¨" - checking-server: "æ£åœ¨æ£€æŸ¥ä¸ŽæœåŠ¡å™¨çš„连接" - finding: "æœç´¢é—®é¢˜" - no-network: "æ— ç½‘ç»œè¿žæŽ¥" - no-network-desc: "请确ä¿æ‚¨å·²è¿žæŽ¥è‡³äº’è”网" - no-internet: "æ— ç½‘ç»œè¿žæŽ¥" - no-internet-desc: "ç½‘ç»œå·²è¿žæŽ¥ï¼Œä½†æ— æ³•è¿žæŽ¥åˆ°Internet。 请确ä¿æ‚¨çš„PCçš„Internet连接æ£å¸¸ã€‚" - no-server: "æ— æ³•è¿žæŽ¥åˆ° Misskey æœåŠ¡å™¨" - no-server-desc: "您设备与互è”网的网络连接æ£å¸¸ï¼Œä½†æ˜¯æ— 法连接至 Misskey æœåŠ¡å™¨ã€‚è¿™å¯èƒ½æ˜¯æœåŠ¡å™¨æš‚æ—¶ä¸å¯ç”¨æˆ–æ£åœ¨ç»´æŠ¤ï¼Œè¯·ç¨åŽå†è¯•ã€‚" - success: "æˆåŠŸè¿žæŽ¥è‡³ Misskey æœåŠ¡å™¨" - success-desc: "看起æ¥æˆ‘们连接æ£å¸¸. 请刷新网页." - flush: "清除缓å˜" - set-version: "指定版本" -common/views/components/media-banner.vue: - sensitive: "阅读注æ„" - click-to-show: "点击以显示" -common/views/components/theme.vue: - theme: "主题" - light-theme: "亮色模å¼ä½¿ç”¨çš„主题" - dark-theme: "暗色模å¼ä½¿ç”¨çš„主题" - light-themes: "亮色主题" - dark-themes: "暗色主题" - install-a-theme: "安装一个主题" - theme-code: "主题代ç " - install: "安装" - installed: "\"{}\" 已安装" - create-a-theme: "创建一个主题" - save-created-theme: "ä¿å˜ä¸»é¢˜" - primary-color: "主è¦é¢œè‰²" - secondary-color: "次è¦é¢œè‰²" - text-color: "文本颜色" - base-theme: "基础主题" - base-theme-light: "亮" - base-theme-dark: "æš—" - find-more-theme: "获å–更多主题" - theme-name: "主题å称" - preview-created-theme: "预览" - invalid-theme: "æ— æ•ˆä¸»é¢˜" - already-installed: "这个主题已ç»è¢«å®‰è£…。" - saved: "å·²ä¿å˜" - manage-themes: "主题管ç†" - builtin-themes: "æ ‡å‡†ä¸»é¢˜" - my-themes: "我的主题" - installed-themes: "已安装的主题" - select-theme: "选择您的主题" - uninstall: "å¸è½½" - uninstalled: "\"{}\" 已被å¸è½½" - author: "作者" - desc: "æè¿°" - export: "导出" - import: "导入" - import-by-code: "或者粘贴代ç " - theme-name-required: "必须填写主题å称" -common/views/components/cw-button.vue: - hide: "éšè—" - show: "查看更多" - chars: "{count}个å—符" - files: "{count} 个文件" - poll: "调查问å·" -common/views/components/messaging.vue: - search-user: "查找用户" - you: "您" - no-history: "没有历å²è®°å½•" - user: "用户" - group: "群组" - start-with-user: "开始用户èŠå¤©" - start-with-group: "开始群组èŠå¤©" - select-group: "请选择群组" -common/views/components/messaging-room.vue: - not-talked-user: "没有用户的会è¯è®°å½•" - not-talked-group: "没有群组的会è¯è®°å½•" - no-history: "没有更多的历å²è®°å½•" - new-message: "æ–°ä¿¡æ¯" - only-one-file-attached: "åªèƒ½æ·»åŠ 一个附件" -common/views/components/messaging-room.form.vue: - input-message-here: "在æ¤é”®å…¥ä¿¡æ¯" - send: "å‘é€" - attach-from-local: "从电脑ä¸æ·»åŠ 文件" - attach-from-drive: "从网盘ä¸æ·»åŠ 文件" - only-one-file-attached: "åªèƒ½æ·»åŠ 一个附件" -common/views/components/messaging-room.message.vue: - is-read: "已读" - deleted: "æ¤æ¶ˆæ¯å·²è¢«åˆ 除" -common/views/components/nav.vue: - about: "关于 Misskey" - stats: "统计" - status: "状æ€" - wiki: "维基百科" - donors: "æèµ è€…" - repository: "æºç 库" - develop: "å¼€å‘人员" - feedback: "å馈" - tos: "æœåŠ¡æ¡æ¬¾" -common/views/components/note-menu.vue: - mention: "æ到" - detail: "详细信æ¯" - copy-content: "å¤åˆ¶å†…容" - copy-link: "å¤åˆ¶é“¾æŽ¥" - favorite: "收è—这个投稿" - unfavorite: "å–消收è—" - watch: "关注" - unwatch: "å–消关注" - pin: "置顶" - unpin: "å–消置顶" - delete: "åˆ é™¤" - delete-confirm: "ç¡®å®šåˆ é™¤è¿™ä¸ªæŠ•ç¨¿å—?" - delete-and-edit: "åˆ é™¤å’Œç¼–è¾‘" - delete-and-edit-confirm: "è¦åˆ 除æ¤å¸–并å†æ¬¡ç¼–辑å—?对æ¤å¸–的所有回应,转推和回å¤ä¹Ÿå°†è¢«åˆ 除。" - remote: "显示原始投稿" - pin-limit-exceeded: "æ— æ³•ç½®é¡¶æ›´å¤šäº†ã€‚" -common/views/components/user-menu.vue: - mention: "æ到" - mute: "å±è”½" - unmute: "解除å±è”½" - mute-confirm: "å±è”½æ¤ç”¨æˆ·ï¼Ÿ" - unmute-confirm: "å–消å±è”½ç”¨æˆ·ï¼Ÿ" - block: "拉黑" - unblock: "å–消拉黑" - block-confirm: "确定拉黑æ¤ç”¨æˆ·ï¼Ÿ" - unblock-confirm: "å–消拉黑æ¤ç”¨æˆ·ï¼Ÿ" - push-to-list: "æ·»åŠ è‡³åˆ—è¡¨" - select-list: "请选择一个列表" - report-abuse: "举报骚扰" - report-abuse-detail: "åšäº†ä»€ä¹ˆéªšæ‰°çš„行为?" - report-abuse-reported: "已报告给管ç†å‘˜ã€‚ éžå¸¸æ„Ÿè°¢ä½ çš„åˆä½œã€‚" - silence: "ç¦è¨€" - unsilence: "解除ç¦è¨€" - silence-confirm: "确认å±è”½æ¤ç”¨æˆ·ï¼Ÿ" - unsilence-confirm: "å–消å±è”½æ¤ç”¨æˆ·ï¼Ÿ" - suspend: "冻结" - unsuspend: "解除冻结" - suspend-confirm: "确认冻结æ¤ç”¨æˆ·ï¼Ÿ" - unsuspend-confirm: "确认解冻æ¤ç”¨æˆ·ï¼Ÿ" -common/views/components/poll.vue: - vote-to: "为\"{}\"投票" - vote-count: "{}票" - total-votes: "总票数{}" - vote: "投票" - show-result: "显示结果" - voted: "已投票" - closed: "已截æ¢" - remaining-days: "{d}天{h}å°æ—¶åŽæˆªæ¢" - remaining-hours: "{h}å°æ—¶{m}分åŽæˆªæ¢" - remaining-minutes: "{m}分{s}秒åŽæˆªæ¢" - remaining-seconds: "{s}秒åŽæˆªæ¢" -common/views/components/poll-editor.vue: - no-only-one-choice: "至少选择两个选项" - choice-n: "选择{}" - remove: "åˆ é™¤é€‰é¡¹" - add: "+æ·»åŠ ä¸€ä¸ªé€‰é¡¹" - destroy: "放弃投票" - multiple: "å…许多个投票" - expiration: "截æ¢æ—¶é—´" - infinite: "永久" - at: "指定日期" - after: "指定时间" - no-more: "最多åªèƒ½æ·»åŠ å个回ç”" - deadline-date: "日期" - deadline-time: "时间" - interval: "时长" - unit: "å•ä½" - second: "秒" - minute: "分" - hour: "å°æ—¶" - day: "æ—¥" -common/views/components/reaction-picker.vue: - choose-reaction: "选择回应" - input-reaction-placeholder: "表情符å·è¾“å…¥" -common/views/components/emoji-picker.vue: - recent-emoji: "最近使用的表情符å·" - custom-emoji: "自定义表情符å·" - no-category: "未分类" - people: "人" - animals-and-nature: "动物与自然" - food-and-drink: "食物与饮å“" - activity: "活动" - travel-and-places: "ä½ç½®" - objects: "物å“" - symbols: "符å·" - flags: "旗帜" -common/views/components/settings/app-type.vue: - title: "模å¼" - intro: "您å¯ä»¥æŒ‡å®šä½¿ç”¨æ¡Œé¢ç‰ˆæˆ–移动版。" - choices: - auto: "自动选择" - desktop: "固定为桌é¢ç‰ˆ" - mobile: "固定为移动版" - info: "更改将在刷新页é¢åŽç”Ÿæ•ˆã€‚" -common/views/components/signin.vue: - username: "用户å" - password: "密ç " - token: "Token (令牌)" - signing-in: "在弄了在弄了..." - or: "或者" - signin-with-twitter: "用 Twitter 登录" - signin-with-github: "用 GitHub 登录" - signin-with-discord: "用 Discord 登录" - login-failed: "登录失败。请检查用户å和密ç 。" - tap-key: "点击安全密钥登录" - enter-2fa-code: "输入验è¯ç " -common/views/components/signup.vue: - invitation-code: "邀请ç " - invitation-info: "如果您没有邀请ç ,请è”ç³»<a href=\"{}\">管ç†å‘˜</a>。" - username: "用户å" - checking: "æ£åœ¨ç¡®è®¤..." - available: "å¯ç”¨" - unavailable: "ä¸å¯ç”¨" - error: "网络错误" - invalid-format: "å¯ä½¿ç”¨å¤§å°å†™è‹±æ–‡å—æ¯ã€æ•°å—和下划线。" - too-short: "请至少输入1个å—符ï¼" - too-long: "请ä¸è¦è¶…过20个å—符" - password: "密ç " - password-placeholder: "推è使用8个å—符以上的密ç 。" - weak-password: "密ç 强度:弱" - normal-password: "密ç 强度:ä¸ç‰" - strong-password: "密ç 强度:强" - retype: "é‡æ–°è¾“å…¥" - retype-placeholder: "é‡æ–°è¾“入您的密ç " - password-matched: "确认" - password-not-matched: "密ç ä¸ä¸€è‡´" - recaptcha: "验è¯" - agree-to: "åŒæ„{0}" - tos: "æœåŠ¡æ¡æ¬¾" - create: "创建一个账户" - some-error: "由于æŸç§åŽŸå› ,创建å¸æˆ·å¤±è´¥ã€‚请å†è¯•ä¸€æ¬¡ã€‚" -common/views/components/special-message.vue: - new-year: "æ–°å¹´å¿«ä¹å“¦~" - christmas: "圣诞快ä¹ï¼" -common/views/components/stream-indicator.vue: - connecting: "连接ä¸" - reconnecting: "é‡æ–°è¿žæŽ¥ä¸" - connected: "已连接" -common/views/components/notification-settings.vue: - title: "通知" - mark-as-read-all-notifications: "å°†æ‰€æœ‰é€šçŸ¥æ ‡ä¸ºå·²è¯»" - mark-as-read-all-unread-notes: "将所有帖åæ ‡ä¸ºå·²è¯»" - mark-as-read-all-talk-messages: "将所有对è¯æ ‡ä¸ºå·²è¯»" - auto-watch: "自动查看帖å" - auto-watch-desc: "自动接收有关您åšå‡ºå›žåº”或回å¤çš„帖å的通知。" -common/views/components/integration-settings.vue: - title: "æœåŠ¡åˆä½œ" - connect: "连接" - disconnect: "æ–开连接" - connected-to: "您的账å·å·²è¿žæŽ¥ä»¥ä¸‹ç¤¾äº¤è´¦å·" -common/views/components/github-setting.vue: - description: "当您用GitHub连接Misskey账户åŽï¼Œæ‚¨å°†èƒ½å¤Ÿçœ‹åˆ°æœ‰å…³æ‚¨è‡ªå·±çš„ä¿¡æ¯ï¼Œå¹¶ä¸”您将能够使用GitHub登录。" - connected-to: "æ¤è´¦æˆ·å·²è¿žæŽ¥GitHub" - detail: "详细信æ¯..." - reconnect: "é‡æ–°è¿žæŽ¥" - connect: "连接您的GitHub账户" - disconnect: "未连接" -common/views/components/discord-setting.vue: - description: "当您用Discord连接Misskey账户åŽï¼Œæ‚¨å°†èƒ½å¤Ÿçœ‹åˆ°æœ‰å…³æ‚¨è‡ªå·±çš„ä¿¡æ¯ï¼Œå¹¶ä¸”您将能够使用Discord登录。" - connected-to: "æ¤è´¦æˆ·å·²è¿žæŽ¥Discord" - detail: "详细信æ¯..." - reconnect: "é‡æ–°è¿žæŽ¥" - connect: "连接您的Discord账户" - disconnect: "æ–开连接" -common/views/components/uploader.vue: - waiting: "ç‰å¾…ä¸" -common/views/components/visibility-chooser.vue: - public: "公开" - home: "首页" - home-desc: "ä»…å‘é€è‡³é¦–页" - followers: "关注者" - followers-desc: "ä»…å‘é€è‡³å…³æ³¨è€…" - specified: "直接" - specified-desc: "ä»…å‘é€è‡³æŒ‡å®šç”¨æˆ·" - local-public: "公开(仅é™æœ¬åœ°ï¼‰" - local-public-desc: "ä¸è¦å…¬å¼€å‘布" - local-home: "首页(仅é™æœ¬åœ°ï¼‰" - local-followers: "关注者(仅é™æœ¬åœ°ï¼‰" -common/views/components/trends.vue: - count: "{} 被æ到" - empty: "没有趋势" -common/views/components/language-settings.vue: - title: "显示è¯è¨€" - pick-language: "选择è¯è¨€" - recommended: "推è" - auto: "自动" - specify-language: "指定è¯è¨€" - info: "更改将在刷新页é¢åŽç”Ÿæ•ˆã€‚" -common/views/components/profile-editor.vue: - title: "个人资料" - name: "å称" - account: "账户" - location: "ä½ç½®" - description: "个人简介" - you-can-include-hashtags: "您å¯ä»¥åŒ…å«ä¸€ä¸ªå“ˆå¸Œæ ‡ç¾ã€‚" - language: "è¯è¨€" - birthday: "生日" - avatar: "头åƒ" - banner: "横幅背景" - is-cat: "这个账户是CAT" - is-bot: "这个账户是BOT" - is-locked: "关注者请求需è¦æ‰¹å‡†" - careful-bot: "BOT的关注者请求需è¦æ‰¹å‡†" - auto-accept-followed: "自动åŒæ„æ¥è‡ªæ‚¨å…³æ³¨çš„人的关注申请" - advanced: "其他" - privacy: "éšç§" - save: "ä¿å˜" - saved: "您的个人资料已ä¿å˜" - uploading: "æ£åœ¨ä¸Šä¼ " - upload-failed: "ä¸Šä¼ å¤±è´¥" - unable-to-process: "æ— æ³•å®Œæˆæ“作" - avatar-not-an-image: "选择的头åƒæ–‡ä»¶ä¸æ˜¯å›¾ç‰‡æ ¼å¼" - banner-not-an-image: "选择的横幅背景ä¸æ˜¯å›¾ç‰‡æ ¼å¼" - email: "邮件设置" - email-address: "电å邮件地å€" - email-verified: "电å邮件地å€å·²éªŒè¯" - email-not-verified: "邮件地å€å°šæœªéªŒè¯ã€‚ 请检查您的邮箱。" - export: "导出" - import: "导入" - export-and-import: "导出/导入" - export-targets: - all-notes: "所有å‘帖" - following-list: "关注列表" - mute-list: "å±è”½åˆ—表" - blocking-list: "黑åå•" - user-lists: "列表" - export-requested: "导出请求已æ交。å¯èƒ½éœ€è¦èŠ±ä¸€äº›æ—¶é—´ã€‚导出的文件将ä¿å˜åˆ°ç½‘盘ä¸ã€‚" - import-requested: "导入请求已æ交。这å¯èƒ½éœ€è¦èŠ±ä¸€ç‚¹æ—¶é—´ã€‚" - enter-password: "请输入您的密ç " - danger-zone: "å±é™©é€‰é¡¹" - delete-account: "åˆ é™¤å¸æˆ·" - account-deleted: "å¸æˆ·å·²è¢«åˆ 除。 æ•°æ®ä¼šåœ¨ä¸€æ®µæ—¶é—´ä¹‹åŽæ¸…除。" - profile-metadata: "个人资料补充信æ¯" - metadata-label: "æ ‡ç¾" - metadata-content: "内容" -common/views/components/user-list-editor.vue: - users: "用户" - rename: "é‡å‘½å列表" - delete: "åˆ é™¤åˆ—è¡¨" - remove-user: "从æ¤åˆ—表ä¸åˆ 除" - delete-are-you-sure: "åˆ é™¤åˆ—è¡¨â€œ$1â€ï¼Ÿ" - deleted: "å·²åˆ é™¤" - add-user: "æ·»åŠ ç”¨æˆ·" -common/views/components/user-group-editor.vue: - users: "æˆå‘˜" - rename: "更改群组å" - delete: "åˆ é™¤ç¾¤ç»„" - transfer: "群组转让" - transfer-are-you-sure: "将群组「$1ã€è½¬è®©ç»™ã€Œ@$2ã€å—?" - transferred: "群组已转让" - remove-user: "从本群组ä¸åˆ 除" - delete-are-you-sure: "确定è¦åˆ 除「$1ã€ç»„?" - deleted: "å·²åˆ é™¤" - invite: "邀请" - invited: "邀请已å‘é€" -common/views/components/user-lists.vue: - user-lists: "列表" - create-list: "创建列表" - list-name: "列表å称" -common/views/components/user-groups.vue: - user-groups: "群组" - create-group: "创建群组" - group-name: "群组å" - owned-groups: "我的群组" - joined-groups: "åŠ å…¥ç¾¤ç»„" - invites: "邀请" - accept-invite: "åŠ å…¥" - reject-invite: "æ‹’ç»" -common/views/widgets/broadcast.vue: - fetching: "确认ä¸" - no-broadcasts: "没有公告" - have-a-nice-day: "ç¥ä½ 有愉快的一天ï¼" - next: "下一个" - prev: "上一首" -common/views/widgets/calendar.vue: - year: "{}å¹´" - month: "{}月" - day: "{}æ—¥" - today: "今天:" - this-month: "本月:" - this-year: "今年:" -common/views/widgets/photo-stream.vue: - title: "图片轮æ’" - no-photos: "没有图片" -common/views/widgets/posts-monitor.vue: - title: "æŠ•ç¨¿è¡¨æ ¼" - toggle: "切æ¢è§†å›¾" -common/views/widgets/hashtags.vue: - title: "å“ˆå¸Œæ ‡ç¾" -common/views/widgets/server.vue: - title: "æœåŠ¡å™¨ä¿¡æ¯" - toggle: "切æ¢æ˜¾ç¤º" -common/views/widgets/memo.vue: - title: "便ç¾" - memo: "在这儿输入!" - save: "ä¿å˜" -common/views/widgets/slideshow.vue: - folder-customize-mode: "è¦æŒ‡å®šæ–‡ä»¶å¤¹ï¼Œè¯·é€€å‡ºè‡ªå®šä¹‰æ¨¡å¼" - folder: "请å•å‡»å¹¶æŒ‡å®šæ–‡ä»¶å¤¹" - no-image: "这个文件夹里没有图片" -common/views/widgets/tips.vue: - tips-line1: "您å¯ä»¥ç”¨<kbd>t</kbd>专注于时间轴" - tips-line2: "从 <kbd>p</kbd> 或者 <kbd>n</kbd>打开投稿表å•" - tips-line3: "您å¯ä»¥åœ¨æŠ•ç¨¿è¡¨å•ä¸Šæ‹–放文件。" - tips-line4: "您å¯ä»¥å°†å‰ªè´´æ¿ä¸çš„图åƒç²˜è´´åˆ°æ交表å•ä¸ã€‚" - tips-line5: "您å¯ä»¥é€šè¿‡å°†æ–‡ä»¶æ‹–放到网盘æ¥ä¸Šä¼ 文件。" - tips-line6: "您å¯ä»¥é€šè¿‡åœ¨ç½‘盘ä¸é€šè¿‡æ‹–动æ“作æ¥ç§»åŠ¨æ–‡ä»¶å¤¹" - tips-line7: "您å¯ä»¥é€šè¿‡åœ¨ç½‘盘ä¸é€šè¿‡æ‹–动æ“作æ¥ç§»åŠ¨æ–‡ä»¶å¤¹ã€‚" - tips-line8: "å¯ä»¥ä»Žè®¾ç½®ä¸å®šåˆ¶ä¸»é¡µã€‚" - tips-line9: "Misskey æ ¹æ® AGPLv3 获得许å¯ã€‚" - tips-line10: "使用Time Machine(时光机)å°éƒ¨ä»¶å¯ä»¥è½»æ¾è¿½æº¯åˆ°è¿‡åŽ»çš„时间轴。" - tips-line11: "您å¯ä»¥ç‚¹å‡»â€œ...â€å°†å¸–å置顶到用户页é¢" - tips-line13: "附在帖å上的所有文件都会ä¿å˜åˆ°ç½‘盘ä¸ã€‚" - tips-line14: "在自定义首页布局时,您å¯ä»¥å³é”®å•å‡»çª—å£å°éƒ¨ä»¶ä»¥æ›´æ”¹å…¶è®¾è®¡ã€‚" - tips-line17: "用“**â€å›´ç»•æ–‡æœ¬å°†çªå‡ºæ˜¾ç¤ºå®ƒã€‚" - tips-line19: "å¯ä»¥åœ¨æµè§ˆå™¨å¤–部分离多个窗å£ã€‚" - tips-line20: "日历å°éƒ¨ä»¶çš„百分比显示ç»è¿‡çš„时间百分比。" - tips-line21: "您也å¯ä»¥ä½¿ç”¨APIå¼€å‘机器人。" - tips-line23: "å°è“很å¯çˆ±" - tips-line24: "Misskey自2014年开始è¿è¥ã€‚" - tips-line25: "在与通知功能兼容的æµè§ˆå™¨ä¸ï¼Œæ‚¨å¯ä»¥åœ¨Misskey未打开的情况下接收通知" -common/views/pages/not-found.vue: - page-not-found: "您è¦æ‰¾çš„网页ä¸å˜åœ¨ã€‚" -common/views/pages/follow.vue: - signed-in-as: "用 {}登录" - following: "æ£åœ¨å…³æ³¨" - follow: "关注" - request-pending: "å‘é€å…³æ³¨ç”³è¯·" - follow-processing: "申请处ç†ä¸" - follow-request: "关注请求" -common/views/pages/follow-requests.vue: - received-follow-requests: "关注申请" - accept: "接å—" - reject: "æ‹’ç»" -desktop: - banner-crop-title: "è£å‰ªæ˜¾ç¤ºä¸ºèƒŒæ™¯çš„部分" - banner: "背景" - uploading-banner: "ä¸Šä¼ ä¸€ä¸ªæ–°çš„èƒŒæ™¯" - banner-updated: "æˆåŠŸä¸Šä¼ 背景" - choose-banner: "选择一个背景" - avatar-crop-title: "è£å‰ªæ˜¾ç¤ºä¸ºå¤´åƒçš„部分" - avatar: "头åƒ" - uploading-avatar: "ä¸Šä¼ ä¸€ä¸ªæ–°çš„å¤´åƒ" - avatar-updated: "æˆåŠŸä¸Šä¼ 头åƒ" - choose-avatar: "选择作为头åƒçš„图片" - unable-to-process: "æ— æ³•å®Œæˆæ“作" - invalid-filetype: "ä¸æŽ¥å—æ¤æ–‡ä»¶ç±»åž‹" -desktop/views/components/activity.chart.vue: - total: "黑 ... 总计" - notes: "è“ ... 投稿" - replies: "红 ... 回å¤" - renotes: "绿 ... 转推" -desktop/views/components/activity.vue: - title: "活动" - toggle: "切æ¢æ˜¾ç¤º" -desktop/views/components/calendar.vue: - title: "{year}å¹´{month}月" - prev: "上个月" - next: "下个月" - go: "点击按时间æµè§ˆ" -desktop/views/components/choose-file-from-drive-window.vue: - chosen-files: "{count}文件已被选择" - upload: "从设备ä¸ä¸Šä¼ 文件" - cancel: "å–消" - ok: "确定" - choose-prompt: "选择文件" -desktop/views/components/choose-folder-from-drive-window.vue: - cancel: "å–消" - ok: "确定" - choose-prompt: "选择一个文件夹" -desktop/views/components/crop-window.vue: - skip: "跳过è£å‰ª" - cancel: "å–消" - ok: "确定" -desktop/views/components/drive-window.vue: - used: "已使用" -desktop/views/components/drive.file.vue: - avatar: "头åƒ" - banner: "背景" - nsfw: "阅读注æ„" - contextmenu: - rename: "é‡å‘½å" - mark-as-sensitive: "æ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - unmark-as-sensitive: "å–æ¶ˆæ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - copy-url: "å¤åˆ¶é“¾æŽ¥" - download: "下载" - else-files: "其他" - set-as-avatar: "设为头åƒ" - set-as-banner: "设置为背景" - open-in-app: "在应用程åºä¸æ‰“å¼€" - add-app: "æ·»åŠ åº”ç”¨" - rename-file: "é‡å‘½å文件" - input-new-file-name: "请输入新文件å" - copied: "å·²å¤åˆ¶" - copied-url-to-clipboard: "å·²å¤åˆ¶é“¾æŽ¥åˆ°å‰ªè´´æ¿" -desktop/views/components/drive.folder.vue: - upload-folder: "é»˜è®¤ä¸Šä¼ æ–‡ä»¶å¤¹" - unable-to-process: "æ— æ³•å®Œæˆæ“作" - circular-reference-detected: "ç›®æ ‡æ–‡ä»¶å¤¹æ˜¯æ‚¨è¦ç§»åŠ¨çš„文件夹的å文件夹。" - unhandled-error: "未知错误" - unable-to-delete: "æ— æ³•åˆ é™¤" - has-child-files-or-folders: "æ¤æ–‡ä»¶å¤¹ä¸ä¸ºç©ºï¼Œæ— æ³•åˆ é™¤ã€‚" - contextmenu: - move-to-this-folder: "移动到æ¤æ–‡ä»¶å¤¹" - show-in-new-window: "在新窗å£æ‰“å¼€" - rename: "é‡å‘½å" - rename-folder: "é‡å‘½å文件夹" - input-new-folder-name: "请输入新文件å" - else-folders: "其他" - set-as-upload-folder: "è®¾ç½®ä¸ºé»˜è®¤ä¸Šä¼ æ–‡ä»¶å¤¹" -desktop/views/components/drive.vue: - search: "æœç´¢" - empty-draghover: "放在这里ï¼å› ä¸ºä½ çŸ¥é“我很å¯çˆ±ï¼Œå¯¹å—?" - empty-drive: "您的媒体å˜å‚¨æ˜¯ç©ºçš„" - empty-drive-description: "å³é”®å•å‡»ä»¥æ‰“å¼€èœå•ï¼Œæˆ–将文件拖放到æ¤å¤„ä»¥è¿›è¡Œä¸Šä¼ ã€‚" - empty-folder: "这个文件夹是空的" - unable-to-process: "æ“ä½œæ— æ³•å®Œæˆ" - circular-reference-detected: "ç›®æ ‡æ–‡ä»¶å¤¹æ˜¯æ‚¨è¦ç§»åŠ¨çš„文件夹的å文件夹。" - unhandled-error: "未知错误" - url-upload: "从网å€ä¸Šä¼ " - url-of-file: "è¦ä¸Šè½½çš„文件的URL" - url-upload-requested: "è¯·æ±‚ä¸Šä¼ " - may-take-time: "ä¸Šä¼ å®Œæˆå¯èƒ½éœ€è¦ä¸€äº›æ—¶é—´ã€‚" - create-folder: "创建一个文件夹" - folder-name: "文件夹å称" - contextmenu: - create-folder: "创建文件夹" - upload: "ä¸Šä¼ æ–‡ä»¶" - url-upload: "从URLä¸Šä¼ " -desktop/views/components/media-video.vue: - sensitive: "阅读注æ„" - click-to-show: "点击以显示" -desktop/views/components/followers-window.vue: - followers: "{} 的关注者" -desktop/views/components/followers.vue: - empty: "看起æ¥æ‚¨æ²¡æœ‰å…³æ³¨è€…。" -desktop/views/components/following-window.vue: - following: "æ£åœ¨å…³æ³¨ {}" -desktop/views/components/following.vue: - empty: "看起æ¥æ‚¨æ²¡æœ‰æ£åœ¨å…³æ³¨çš„用户..." -desktop/views/components/game-window.vue: - game: "游æˆ" -desktop/views/components/home.vue: - done: "完æˆ" - add-widget: "æ·»åŠ å°éƒ¨ä»¶:" - add: "æ·»åŠ " -desktop/views/input-dialog.vue: - cancel: "å–消" - ok: "确定" -desktop/views/components/note-detail.vue: - private: "ç§å¯†æŠ•ç¨¿" - deleted: "æŠ•ç¨¿å·²åˆ é™¤" - location: "ä½ç½®ä¿¡æ¯" - renote: "转推" - add-reaction: "回应" - undo-reaction: "å–消回应" -desktop/views/components/note.vue: - reply: "回å¤" - renote: "转推" - add-reaction: "回应" - undo-reaction: "å–消回应" - detail: "详细信æ¯" - private: "这个投稿是ç§å¯†çš„" - deleted: "æŠ•ç¨¿å·²åˆ é™¤" -desktop/views/components/notes.vue: - error: "åŠ è½½å¤±è´¥ã€‚" - retry: "é‡è¯•" -desktop/views/components/notifications.vue: - empty: "没有通知哦!" -desktop/views/components/post-form.vue: - posted: "å·²å‘é€æŠ•ç¨¿!" - replied: "已回å¤!" - reposted: "已转推!" - note-failed: "å‘帖失败" - reply-failed: "回å¤å¤±è´¥" - renote-failed: "转推失败" -desktop/views/components/post-form-window.vue: - note: "新建帖å" - reply: "回å¤" - attaches: "å·²æ·»åŠ {}媒体文件" - uploading-media: "æ£åœ¨ä¸Šä¼ {} 媒体文件" -desktop/views/components/progress-dialog.vue: - waiting: "ç‰å¾…ä¸" -desktop/views/components/renote-form.vue: - quote: "引用..." - cancel: "å–消" - renote: "转推" - renote-home: "转推(首页)" - reposting: "é‡æ–°å‘é€ä¸..." - success: "已转推ï¼" - failure: "转推失败" -desktop/views/components/renote-form-window.vue: - title: "您是å¦è¦è½¬æŽ¨ï¼Ÿ" -desktop/views/pages/user-following-or-followers.vue: - following: "{user}çš„æ£åœ¨å…³æ³¨" - followers: "{user}的关注者" -desktop/views/components/settings.2fa.vue: - intro: "如果设置了两æ¥éªŒè¯ï¼Œåˆ™ä¸ä»…需è¦åœ¨ç™»å½•æ—¶ä½¿ç”¨å¯†ç ,还需è¦éªŒè¯è®¾å¤‡ï¼ˆå¦‚智能手机),这将æ高安全性。" - detail: "详细信æ¯..." - url: "https://www.google.com/landing/2step/" - caution: "å¦‚æžœæ‚¨æ— æ³•è®¿é—®å·²æ³¨å†Œçš„è®¾å¤‡ï¼Œæ‚¨å°†æ— æ³•å†è¿žæŽ¥åˆ° Misskeyï¼" - register: "注册设备" - already-registered: "æ¤è®¾å¤‡å·²è¢«æ³¨å†Œ" - unregister: "解除注册" - unregistered: "两æ¥éªŒè¯å·²è¢«åœç”¨ã€‚" - enter-password: "请输入您的密ç " - authenticator: "首先,您需è¦åœ¨è®¾å¤‡ä¸Šå®‰è£… Google Authenticator:" - howtoinstall: "æ€Žæ ·å®‰è£…" - token: "令牌" - scan: "然åŽï¼Œæ‰«æ二维ç :" - done: "请输入显示在您设备上的密钥:" - submit: "æ交" - success: "设置完æˆ" - failed: "设置失败, 请确ä¿æ‚¨çš„密钥是æ£ç¡®çš„。" - info: "从下次登录Misskey时,您的设备上显示的令牌以åŠå¯†ç 也是必需的。" - totp-header: "èº«ä»½éªŒè¯ App" - security-key-header: "安全密钥" - security-key: "为了增强安全性,您å¯ä»¥ä½¿ç”¨æ”¯æŒFIDO2的硬件安全密钥登录您的å¸æˆ·ã€‚ 登录时,您将需è¦æ³¨å†Œå®‰å…¨å¯†é’¥æˆ–身份验è¯åº”用。" - last-used: "最åŽä½¿ç”¨ï¼š" - activate-key: "å•å‡»ä»¥æ¿€æ´»æ‚¨çš„安全密钥" - security-key-name: "密钥å称" - register-security-key: "安全密钥注册完æˆ" - something-went-wrong: "糟糕ï¼å®‰å…¨å¯†é’¥æ³¨å†Œå‡ºçŽ°é—®é¢˜ï¼š" - key-unregistered: "å®‰å…¨å¯†é’¥å·²è¢«åˆ é™¤ã€‚" - use-password-less-login: "使用å…密ç 登录" -common/views/components/media-image.vue: - sensitive: "阅读注æ„" - click-to-show: "点击查看" -common/views/components/api-settings.vue: - intro: "è¦è®¿é—®API,请将æ¤æ ‡è®°è®¾ç½®ä¸ºè¯·æ±‚å‚数的关键å—“iâ€ã€‚" - caution: "请勿将æ¤ä»¤ç‰Œè¾“入任何应用,也ä¸è¦å°†æ¤ä»¤ç‰Œå‘Šè¯‰å…¶ä»–人,å¦åˆ™æ‚¨çš„账户å¯èƒ½ä¼šå—到æŸå®³ã€‚" - regeneration-of-token: "如果您的令牌泄露,您å¯ä»¥é‡æ–°ç”Ÿæˆã€‚" - regenerate-token: "é‡æ–°ç”Ÿæˆä»¤ç‰Œ" - token: "令牌:" - enter-password: "请输入您的密ç " - console: - title: "API 控制å°" - endpoint: "端点" - parameter: "å‚æ•°" - credential-info: "æ¤æŽ§åˆ¶å°ä¸éœ€è¦å‚数“iâ€ã€‚" - send: "å‘é€" - sending: "ç‰å¾…回应" - response: "结果" -desktop/views/components/settings.apps.vue: - no-apps: "没有已连接的应用程åº" -common/views/components/drive-settings.vue: - max: "容é‡" - in-use: "已使用" - stats: "统计" - default-upload-folder: "é»˜è®¤ä¸Šä¼ æ–‡ä»¶å¤¹" - default-upload-folder-name: "文件夹" - change-default-upload-folder: "更改文件夹" -common/views/components/mute-and-block.vue: - mute-and-block: "å±è”½/拉黑" - mute: "å±è”½" - block: "拉黑" - no-muted-users: "æ— å±è”½ç”¨æˆ·" - no-blocked-users: "æ— æ‹‰é»‘çš„ç”¨æˆ·" - word-mute: "æ–‡å—å±è”½" - muted-words: "å±è”½å…³é”®å—" - muted-words-description: "ä½¿ç”¨ç©ºæ ¼åˆ†éš”ä¼šäº§ç”ŸAND规范,并且使用æ¢è¡Œç¬¦åˆ†éš”会产生OR规范" - unmute-confirm: "å–消å±è”½ç”¨æˆ·ï¼Ÿ" - unblock-confirm: "å–消拉黑æ¤ç”¨æˆ·ï¼Ÿ" - save: "ä¿å˜" -common/views/components/password-settings.vue: - reset: "更改密ç " - enter-current-password: "输入当å‰çš„密ç " - enter-new-password: "输入新密ç " - enter-new-password-again: "请å†æ¬¡è¾“入新密ç " - not-match: "新密ç ä¸åŒ¹é…" - changed: "密ç 已更改" - failed: "更改密ç 失败" -common/views/components/post-form-attaches.vue: - attach-cancel: "åˆ é™¤é™„ä»¶" - mark-as-sensitive: "æ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - unmark-as-sensitive: "å–æ¶ˆæ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" -desktop/views/components/sub-note-content.vue: - private: "这个帖å是ç§å¯†çš„" - deleted: "帖åå·²åˆ é™¤" - media-count: "é™„åŠ {}媒体" - poll: "投票" -desktop/views/components/settings.tags.vue: - title: "æ ‡ç¾" - query: "查询 (å¯é€‰)" - add: "æ·»åŠ " - save: "ä¿å˜" -desktop/views/components/timeline.vue: - home: "首页" - local: "本地" - hybrid: "社交" - global: "å…¨çƒ" - mentions: "æ到的" - messages: "直接å‘布" - list: "列表" - hashtag: "å“ˆå¸Œæ ‡ç¾" - add-tag-timeline: "æ·»åŠ å“ˆå¸Œæ ‡ç¾" - add-list: "æ·»åŠ åˆ—è¡¨" - list-name: "列表å称" -desktop/views/components/ui.header.vue: - welcome-back: "欢迎回æ¥ï¼" - adjective: "先生" -desktop/views/components/ui.header.account.vue: - profile: "个人资料" - lists: "列表" - groups: "群组" - follow-requests: "关注申请" - admin: "管ç†" - room: "房间" -desktop/views/components/ui.header.nav.vue: - game: "游æˆ" -desktop/views/components/ui.header.notifications.vue: - title: "通知" -desktop/views/components/ui.header.post.vue: - post: "撰写新帖å" -desktop/views/components/ui.header.search.vue: - placeholder: "æœç´¢" -desktop/views/components/user-preview.vue: - notes: "帖å" - following: "关注ä¸" - followers: "关注者" -desktop/views/components/users-list.vue: - all: "所有" - iknow: "ä½ æ‡‚çš„" - fetching: "æ£åœ¨åŠ è½½..." -desktop/views/components/users-list-item.vue: - followed: "关注您" -desktop/views/components/window.vue: - popout: "弹出" - close: "å…³é—" -admin/views/index.vue: - dashboard: "仪表盘" - instance: "实例" - emoji: "自定义Emoji" - moderators: "版主" - users: "用户" - federation: "è”邦" - announcements: "公告" - abuse: "举报垃圾信æ¯" - queue: "作业队列" - logs: "日志" - db: "æ•°æ®åº“" - back-to-misskey: "返回 Misskey" -admin/views/db.vue: - tables: "è¡¨æ ¼" - vacuum: "VACUUM" - vacuum-info: "清ç†æ•°æ®åº“。 ä¿æŒæ•°æ®å®Œæ•´å¹¶å‡å°‘ç£ç›˜ä½¿ç”¨é‡ã€‚ æ¤æ“作通常会自动定期执行。" - vacuum-exclamation: "è¿è¡ŒVACUUM之åŽï¼Œæ•°æ®åº“上的负载å¯èƒ½ä¼šæŒç»ä¸€æ®µæ—¶é—´ï¼Œå¹¶ä¸”å¯èƒ½ä¸å“应用户æ“作。" -admin/views/dashboard.vue: - dashboard: "Dashboard" - accounts: "账户" - notes: "帖å" - drive: "网盘" - instances: "实例" - this-instance: "æ¤å®žä¾‹" - federated: "è”åˆ" -admin/views/queue.vue: - title: "队列" - remove-all-jobs: "清除所有作业" - jobs: "任务" - queue: "队列" - domains: - deliver: "交付" - inbox: "收件箱" - db: "æ•°æ®åº“" - objectStorage: "对象å˜å‚¨" - state: "状æ€" - states: - active: "处ç†ä¸" - delayed: "已预订" - waiting: "队列ç‰å¾…ä¸" - result-is-truncated: "结果已çœç•¥" - other-queues: "其他队列" -admin/views/logs.vue: - logs: "日志" - domain: "域" - level: "级别" - levels: - all: "所有" - info: "ä¿¡æ¯" - success: "æˆåŠŸ" - warning: "è¦å‘Š" - error: "错误" - debug: "调试" - delete-all: "å…¨éƒ¨åˆ é™¤" -admin/views/abuse.vue: - title: "举报垃圾信æ¯" - target: "ç›®æ ‡" - reporter: "报告者" - details: "详情" - remove-report: "åˆ é™¤" -admin/views/instance.vue: - instance: "实例" - instance-name: "实例å称" - instance-description: "实例介ç»" - host: "主机å" - icon-url: "å›¾æ ‡URL" - logo-url: "Logo URL" - banner-url: "背景图片地å€" - error-image-url: "æ— æ•ˆçš„å›¾åƒURL" - languages: "实例è¯è¨€" - languages-desc: "您å¯ä»¥æ·»åŠ å¤šä¸ªï¼Œä»¥ç©ºæ ¼åˆ†éš”ã€‚" - tos-url: "æœåŠ¡æ¡æ¬¾URL" - repository-url: "æºç 库URL" - feedback-url: "å馈URL" - maintainer-config: "管ç†å‘˜ä¿¡æ¯" - maintainer-name: "管ç†å‘˜å称" - maintainer-email: "è”系管ç†å‘˜" - advanced-config: "其他设置" - note-and-tl: "帖å和时间线" - drive-config: "网盘设置" - use-object-storage: "使用对象å˜å‚¨" - object-storage-base-url: "URL" - object-storage-bucket: "å˜å‚¨ç©ºé—´å" - object-storage-prefix: "å‰ç¼€" - object-storage-endpoint: "端点" - object-storage-region: "区域" - object-storage-port: "端å£" - object-storage-access-key: "访问密钥" - object-storage-secret-key: "密钥" - object-storage-use-ssl: "使用 SSL" - object-storage-s3-info: "使用Amazon S3作为对象å˜å‚¨æ—¶ï¼Œè¯·ç¡®è®¤{0}相关“终端â€å’Œâ€œåŒºåŸŸâ€çš„设置。" - object-storage-s3-info-here: "这里" - object-storage-gcs-info: "å°†Google Cloud Storage用作对象å˜å‚¨æ—¶ï¼Œè¯·å°†â€œç»ˆç«¯â€è®¾ç½®ä¸ºstorage.googleapis.com,并将“区域â€ç•™ç©ºã€‚" - cache-remote-files: "远程文件缓å˜" - proxy-remote-files: "代ç†è¿œç¨‹æ–‡ä»¶" - local-drive-capacity-mb: "æ¯ä¸ªç”¨æˆ·çš„网盘空间" - remote-drive-capacity-mb: "æ¯ä¸ªè¿œç¨‹ç”¨æˆ·çš„网盘容é‡" - mb: "以兆å—节(Mbps)为å•ä½" - recaptcha-config: "reCAPTCHA设置" - recaptcha-info: "reCAPTCHA token是必è¦çš„. 请从 https://www.google.com/recaptcha/intro/ 获å–。\n请注æ„, 该功能在ä¸å›½å¤§é™†ä¸å¯ç”¨ã€‚" - recaptcha-info2: "ä¸æ”¯æŒv3。请使用v2。" - enable-recaptcha: "å¯ç”¨ reCAPTCHA\n(请注æ„, æ¤åŠŸèƒ½åœ¨ä¸å›½å¤§é™†ä¸å¯ç”¨. 如果å¯ç”¨, å¯èƒ½å¯¼è‡´æ— 法æ£å¸¸ä½¿ç”¨ç™»å½•æˆ–注册ç‰åŠŸèƒ½)" - recaptcha-site-key: "网站密钥" - recaptcha-secret-key: "密钥" - recaptcha-preview: "预览" - hidden-tags: "éšè—å“ˆå¸Œæ ‡ç¾" - hidden-tags-info: "使用æ¢è¡Œç¬¦åˆ†éš”è¦ä»Žé›†åˆä¸æŽ’é™¤çš„å“ˆå¸Œæ ‡ç¾ã€‚" - external-service-integration-config: "连接外部æœåŠ¡" - twitter-integration-config: "连接到Twitter的设置" - twitter-integration-info: "设置返回的URL{url}。" - enable-twitter-integration: "å¯ç”¨è¿žæŽ¥åˆ°Twitter" - twitter-integration-consumer-key: "Consumer key" - twitter-integration-consumer-secret: "Consumer Secret" - github-integration-config: "连接到GitHub设置" - github-integration-info: "设置返回的URL{url}。" - enable-github-integration: "å¯ç”¨è¿žæŽ¥åˆ°GitHub" - github-integration-client-id: "Client ID" - github-integration-client-secret: "Client Secret" - discord-integration-config: "设置 Discord Integration" - discord-integration-info: "设置返回的URL{url}。" - enable-discord-integration: "å¯ç”¨ Discord 连接" - discord-integration-client-id: "Client ID" - discord-integration-client-secret: "Client Secret" - proxy-account-config: "代ç†å¸æˆ·è®¾ç½®" - proxy-account-info: "如果æ¤å®žä¾‹ä¸æ²¡æœ‰äººè·Ÿéšä»–或她,则代ç†å¸æˆ·å¯ä»¥è·Ÿéšè¿œç¨‹ç”¨æˆ·è¿›è¡Œæ´»åŠ¨ã€‚ 当您将æ¤å®žä¾‹ä¸æ²¡æœ‰äººçš„è¿œç¨‹ç”¨æˆ·æ·»åŠ åˆ°åˆ—è¡¨ä¸æ—¶ï¼Œä¸ºäº†èŽ·å–他或她的数æ®ï¼Œä»£ç†è´¦æˆ·ä¼šè·Ÿéšä»–或她,而ä¸æ˜¯æ‚¨çš„è·Ÿéšè€…。" - proxy-account-username: "代ç†è´¦æˆ·ç”¨æˆ·å" - proxy-account-username-desc: "指定用作代ç†çš„账户的用户å。" - proxy-account-warn: "在进行æ¤æ“作之å‰ï¼Œæ‚¨å¿…须创建一个拥有æ¤ç”¨æˆ·å的账户。" - max-note-text-length: "最大帖åå—符数" - disable-registration: "åœç”¨æ–°ç”¨æˆ·æ³¨å†ŒåŠŸèƒ½" - disable-local-timeline: "åœç”¨æœ¬åœ°æ—¶é—´çº¿åŠŸèƒ½" - disable-global-timeline: "ç¦ç”¨å…¨å±€æ—¶é—´çº¿" - disabling-timelines-info: "å³ä½¿ç¦ç”¨æ—¶é—´çº¿ï¼Œç®¡ç†å‘˜å’Œç‰ˆä¸»ä»ç„¶å¯ç”¨ã€‚" - enable-emoji-reaction: "在回应上使用表情符å·" - use-star-for-reaction-fallback: "使用默认的staræ¥è¡¨ç¤ºæœªçŸ¥çš„回应" - invite: "邀请" - save: "ä¿å˜" - saved: "ä¿å˜å®Œæ¯•" - pinned-users: "置顶用户" - pinned-users-info: "æ述您è¦ç½®é¡¶çš„用户,以æ¢è¡Œç¬¦åˆ†éš”。" - email-config: "电å邮件æœåŠ¡å™¨è®¾ç½®" - email-config-info: "用于确认电å邮件和密ç é‡ç½®ç‰ã€‚" - enable-email: "å¯ç”¨ç”µå邮件é€é€’" - email: "电å邮件地å€" - smtp-secure: "在 SMTP 连接ä¸ä½¿ç”¨éšå¼ SSL / TLS" - smtp-secure-info: "ä½¿ç”¨æ—¶å…³é— STARTTLS。" - smtp-host: "SMTP æœåŠ¡å™¨åœ°å€ (主机å)" - smtp-port: "SMTP 端å£" - smtp-auth: "SMTP身份验è¯" - smtp-user: "SMTP 用户å" - smtp-pass: "SMTP 密ç " - test-email: "测试" - serviceworker-config: "ServiceWorker" - enable-serviceworker: "å¯ç”¨ServiceWorker" - serviceworker-info: "您需è¦å¯ç”¨æŽ¨é€é€šçŸ¥" - vapid-publickey: "VAPID公钥" - vapid-privatekey: "VAPIDç§é’¥" - vapid-info: "如果您想è¦å¯ç”¨ServiceWorker,那么您需è¦ç”ŸæˆVAPID秘钥。除éžæ‚¨å·²ç»åœ¨å…¶ä»–地方设置了全局node_modulesä½ç½®ï¼Œå¦åˆ™æ‚¨éœ€è¦å°†å…¶ä½œä¸ºroot用户è¿è¡Œï¼š" -admin/views/charts.vue: - title: "历å²è®°å½•" - per-day: "æ¯å¤©" - per-hour: "æ¯å°æ—¶" - federation: "è”åˆ" - notes: "投稿" - users: "用户" - drive: "网盘" - network: "网络" - charts: - federation-instances: "å®žä¾‹æ•°ï¼šå¢žåŠ /å‡å°‘" - federation-instances-total: "实例总数" - notes: "帖åæ•°é‡ï¼šå¢žåŠ /å‡å°‘(总和)" - local-notes: "帖åæ•°é‡ï¼šå¢žåŠ /å‡å°‘(Local)" - remote-notes: "帖åæ•°é‡ï¼šå¢žåŠ /å‡å°‘(远程)" - notes-total: "帖å总数" - users: "用户数é‡ï¼šå¢žåŠ /å‡å°‘" - users-total: "用户总数" - active-users: "活跃用户数" - drive: "å˜å‚¨å®¹é‡ï¼šå¢žåŠ /å‡å°‘" - drive-total: "网盘总使用é‡" - drive-files: "网盘文件数é‡å˜åŒ–" - drive-files-total: "网盘文件总数" - network-requests: "请求" - network-time: "å“应时间" - network-usage: "网络æµé‡" -admin/views/drive.vue: - operation: "æ“作" - fileid-or-url: "文件ID或文件URL" - file-not-found: "找ä¸åˆ°æ–‡ä»¶" - lookup: "查询" - sort: - title: "排åº" - createdAtAsc: "æŒ‰ä¸Šä¼ æ—¶é—´ï¼ˆå‡åºï¼‰" - createdAtDesc: "æŒ‰ä¸Šä¼ æ—¶é—´ï¼ˆé™åºï¼‰" - sizeAsc: "按大å°ï¼ˆå‡åºï¼‰" - sizeDesc: "按大å°ï¼ˆé™åºï¼‰" - origin: - title: "æºè‡ª" - combined: "本地+远程" - local: "本地" - remote: "远程" - delete: "åˆ é™¤" - deleted: "å·²åˆ é™¤" - mark-as-sensitive: "æ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - unmark-as-sensitive: "å–æ¶ˆæ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - marked-as-sensitive: "æ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - unmarked-as-sensitive: "å–æ¶ˆæ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - clean-remote-files: "åˆ é™¤è¿œç¨‹æ–‡ä»¶ç¼“å˜" - clean-remote-files-are-you-sure: "确定è¦åˆ 除所有远程文件缓å˜å—?" - clean-up: "清除缓å˜" -admin/views/users.vue: - operation: "æ“作" - username-or-userid: "用户å或用户ID" - user-not-found: "用户ä¸å˜åœ¨" - lookup: "订阅" - reset-password: "密ç é‡ç½®" - reset-password-confirm: "是å¦é‡ç½®å¯†ç ?" - password-updated: "密ç 为「{password}ã€" - suspend: "被冻结" - suspend-confirm: "是å¦å†»ç»“?" - suspended: "æˆåŠŸå†»ç»“用户" - unsuspend: "已解除冻结" - unsuspend-confirm: "是å¦è§£é™¤å†»ç»“?" - unsuspended: "å·²æˆåŠŸè§£é™¤ç”¨æˆ·å†»ç»“" - make-silence: "ç¦è¨€" - silence-confirm: "确认å±è”½ï¼Ÿ" - unmake-silence: "解除ç¦è¨€" - unsilence-confirm: "解除å±è”½ï¼Ÿ" - update-remote-user: "更新远程用户信æ¯" - remote-user-updated: "远程用户信æ¯å·²æ›´æ–°" - delete-all-files: "åˆ é™¤æ‰€æœ‰æ–‡ä»¶" - delete-all-files-confirm: "åˆ é™¤æ‰€æœ‰æ–‡ä»¶å—?" - username: "用户å" - host: "主机å" - users: - title: "用户" - sort: - title: "排åº" - createdAtAsc: "注册时间从旧到新" - createdAtDesc: "注册时间从新到旧" - updatedAtAsc: "更新时间从旧到新" - updatedAtDesc: "更新时间从新到旧" - state: - title: "状æ€" - all: "全部" - available: "å¯ç”¨" - admin: "管ç†å‘˜" - moderator: "版主" - adminOrModerator: "管ç†å‘˜+版主" - silenced: "å·²ç¦è¨€" - suspended: "已冻结" - origin: - title: "æºè‡ª" - combined: "本地+远程" - local: "本地" - remote: "远程" - createdAt: "注册日期" - updatedAt: "最åŽæ›´æ–°" -admin/views/moderators.vue: - add-moderator: - title: "注册版主" - add: "注册" - added: "已注册版主。" - remove: "å–消" - removed: "å–消注册版主" - logs: - title: "日志" - moderator: "版主" - type: "æ“作" - at: "日期和时间" - info: "ä¿¡æ¯" -admin/views/emoji.vue: - add-emoji: - title: "æ·»åŠ emoji" - name: "Emoji å称" - name-desc: "ä½ å¯ä»¥ä½¿ç”¨å—符a~z 0~9 _" - category: "类别" - aliases: "别å" - aliases-desc: "您å¯ä»¥æ·»åŠ å¤šä¸ªï¼Œä»¥ç©ºæ ¼åˆ†éš”ã€‚" - url: "emoji 地å€" - add: "æ·»åŠ " - info: "我们建议使用50KB以下的PNG图åƒã€‚" - added: "Emoji å·²æ·»åŠ " - emojis: - title: "表情符å·åˆ—表" - update: "æ›´æ–°" - remove: "移除" - updated: "已更新" - remove-emoji: - are-you-sure: "åˆ é™¤ã€Œ$1ã€?" - removed: "å·²åˆ é™¤" -admin/views/announcements.vue: - announcements: "公告" - save: "ä¿å˜" - remove: "移除" - add: "æ·»åŠ " - title: "æ ‡é¢˜" - text: "内容" - saved: "å·²ä¿å˜" - _remove: - are-you-sure: "åˆ é™¤ã€Œ$1ã€?" - removed: "å·²åˆ é™¤" -admin/views/hashtags.vue: - hided-tags: "éšè—æ ‡ç¾" -admin/views/federation.vue: - instance: "例" - host: "主机å" - notes: "帖å" - users: "用户" - following: "æ£åœ¨å…³æ³¨" - followers: "关注者" - caught-at: "注册日期" - status: "状æ€" - latest-request-sent-at: "上次å‘é€çš„请求" - latest-request-received-at: "上次收到的请求" - remove-all-following: "å–消所有关注" - remove-all-following-info: "å–消{host}的所有关注者。当实例ä¸å˜åœ¨æ—¶æ‰§è¡Œã€‚" - delete-all-files: "åˆ é™¤æ‰€æœ‰æ–‡ä»¶" - block: "拉黑" - marked-as-closed: "æ ‡è®°ä¸ºå·²å…³é—" - lookup: "查询" - instances: "è”邦" - instance-not-registered: "实例未注册" - sort: "排åº" - sorts: - caughtAtAsc: "注册时间从旧到新" - caughtAtDesc: "注册时间从新到旧" - lastCommunicatedAtAsc: "上次互动时间从旧到新" - lastCommunicatedAtDesc: "上次互动时间从新到旧" - notesAsc: "å‘帖数é‡ä»Žå°‘到多" - notesDesc: "å‘帖数é‡ä»Žå¤šåˆ°å°‘" - usersAsc: "用户数从少到多" - usersDesc: "用户数从多到少" - followingAsc: "关注数从少到多" - followingDesc: "关注数从多到少" - followersAsc: "粉ä¸æ•°ä»Žå°‘到多" - followersDesc: "粉ä¸æ•°ä»Žå¤šåˆ°å°‘" - driveUsageAsc: "网盘使用é‡ä»Žå°‘到多" - driveUsageDesc: "网盘使用é‡ä»Žå¤šåˆ°å°‘" - driveFilesAsc: "网盘文件数从少到多" - driveFilesDesc: "网盘文件数从多到少" - state: "状æ€" - states: - all: "所有" - blocked: "已拉黑" - not-responding: "没有å“应" - marked-as-closed: "å·²æ ‡è®°ä¸ºå·²å…³é—" - result-is-truncated: "显示最å‰é¢çš„{n}项。" - charts: "图表" - chart-srcs: - requests: "请求" - users: "用户数é‡å˜åŒ–" - users-total: "用户总数" - notes: "å‘帖数å˜åŒ–" - notes-total: "帖å总数" - ff: "关注/被关注数é‡å˜åŒ–" - ff-total: "关注/被关注总数" - drive-usage: "网盘使用é‡å˜åŒ–" - drive-usage-total: "网盘总使用é‡" - drive-files: "网盘文件数é‡å˜åŒ–" - drive-files-total: "网盘文件总数" - chart-spans: - hour: "æ¯å°æ—¶" - day: "æ¯å¤©" - blocked-hosts: "拉黑" - blocked-hosts-info: "æ述您è¦é˜»æ¢çš„主机,以æ¢è¡Œç¬¦åˆ†éš”。" - save: "ä¿å˜" -desktop/views/pages/welcome.vue: - about: "更多信æ¯..." - timeline: "时间线" - announcements: "公告" - photos: "最近图片" - powered-by-misskey: "Powered by <b>Misskey</b>." - info: "ä¿¡æ¯" -desktop/views/pages/drive.vue: - title: "Misskey 网盘" -desktop/views/pages/note.vue: - prev: "上一个帖å" - next: "下一个帖å" -desktop/views/pages/selectdrive.vue: - title: "选择文件" - ok: "确定" - cancel: "å–消" - upload: "ä»Žè®¾å¤‡ä¸Šä¼ æ–‡ä»¶" -desktop/views/pages/search.vue: - not-available: "在æ¤å®žä¾‹çš„设置ä¸å…³é—æœç´¢åŠŸèƒ½ã€‚" - not-found: "没有找到“{q}â€çš„帖å" -desktop/views/pages/tag.vue: - no-posts-found: "æ²¡æœ‰æ‰¾åˆ°å¸¦æœ‰å“ˆå¸Œæ ‡ç¾â€œ{q}â€çš„帖å" -desktop/views/pages/user-list.users.vue: - users: "用户" - add-user: "æ·»åŠ ç”¨æˆ·" - username: "用户å" -desktop/views/pages/user/user.followers-you-know.vue: - title: "您å¯èƒ½è®¤è¯†çš„关注者" - loading: "æ£åœ¨åŠ è½½ä¸" - no-users: "æ²¡æœ‰ä½ çŸ¥é“的关注者" -desktop/views/pages/user/user.friends.vue: - title: "活跃用户" - loading: "æ£åœ¨åŠ è½½ä¸" - no-users: "没有活跃用户" -desktop/views/pages/user/user.photos.vue: - title: "照片" - loading: "æ£åœ¨åŠ è½½ä¸" - no-photos: "没有图片" -desktop/views/pages/user/user.header.vue: - posts: "帖å" - following: "关注ä¸" - followers: "关注者" - is-bot: "这个账户是Bot" - no-description: "没有自我介ç»" - years-old: "{age}å²" - year: "å¹´" - month: "月" - day: "æ—¥" - follows-you: "关注您" -desktop/views/pages/user/user.timeline.vue: - default: "帖å" - with-replies: "帖å与回å¤" - with-media: "媒体" - my-posts: "我的帖å" -desktop/views/widgets/notifications.vue: - title: "通知" -desktop/views/widgets/polls.vue: - title: "投票" - refresh: "更多" - nothing: "没有投票哦!" -desktop/views/widgets/post-form.vue: - title: "帖å" - note: "帖å" - something-happened: "由于æŸç§åŽŸå› æ— æ³•å‘帖。" -desktop/views/widgets/profile.vue: - update-banner: "点击æ¥å‰ªè¾‘背景" - update-avatar: "点击æ¥å‰ªè¾‘头åƒ" -desktop/views/widgets/trends.vue: - title: "趋势" - refresh: "更多" - nothing: "没有趋势图哦!" -desktop/views/widgets/users.vue: - title: "推è用户" - refresh: "更多" - no-one: "没有任何推è用户ï¼" -mobile/views/components/drive.vue: - used: "已使用" - folder-count: "文件夹" - count-separator: "," - file-count: "文件" - nothing-in-drive: "网盘为空" - folder-is-empty: "这文件夹是空的" - folder-name: "文件夹å称" - here-is-root: "当å‰ä½ç½®ä¸ºæ ¹ç›®å½•ã€‚" - url-prompt: "è¦ä¸Šä¼ 的文件的URL" - uploading: "å·²è¯·æ±‚ä¸Šä¼ ã€‚ ä¸Šä¼ å®Œæˆå¯èƒ½éœ€è¦ä¸€æ®µæ—¶é—´ã€‚" - folder-name-cannot-empty: "文件夹åä¸èƒ½ä¸ºç©ºã€‚" -mobile/views/components/drive-file-chooser.vue: - select-file: "选择文件" -mobile/views/components/drive-folder-chooser.vue: - select-folder: "选择一个文件夹" -mobile/views/components/drive.file.vue: - nsfw: "阅读注æ„" -mobile/views/components/drive.file-detail.vue: - download: "下载" - rename: "é‡å‘½å" - move: "移动" - hash: "哈希(md5)" - exif: "EXIF" - nsfw: "阅读注æ„" - mark-as-sensitive: "æ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" - unmark-as-sensitive: "å–æ¶ˆæ ‡è®°ä¸ºâ€œæ•æ„Ÿâ€" -mobile/views/components/media-video.vue: - sensitive: "阅读注æ„" - click-to-show: "点击以显示" -common/views/components/follow-button.vue: - following: "æ£åœ¨å…³æ³¨" - follow: "关注" - request-pending: "å‘é€å…³æ³¨ç”³è¯·" - follow-processing: "申请处ç†ä¸" - follow-request: "关注申请" -mobile/views/components/note.vue: - private: "ç§å¯†å¸–å" - deleted: "帖åå·²åˆ é™¤" - location: "ä½ç½®ä¿¡æ¯" -mobile/views/components/note-detail.vue: - reply: "回å¤" - reaction: "回应" - private: "这个帖å是ç§å¯†çš„" - deleted: "帖åå·²åˆ é™¤" - location: "ä½ç½®ä¿¡æ¯" -mobile/views/components/note-preview.vue: - admin: "管ç†å‘˜" - bot: "bot" - cat: "cat" -mobile/views/components/note-sub.vue: - admin: "管ç†å‘˜" - bot: "bot" - cat: "cat" -mobile/views/components/notifications.vue: - empty: "没有通知哦!" -mobile/views/components/sub-note-content.vue: - private: "ç§å¯†å¸–å" - deleted: "帖åå·²åˆ é™¤" - media-count: "é™„åŠ {}媒体" - poll: "投票" -mobile/views/components/ui.header.vue: - welcome-back: "欢迎回æ¥ï¼" - adjective: "先生" -mobile/views/components/ui.nav.vue: - timeline: "时间线" - notifications: "通知" - follow-requests: "关注申请" - search: "æœç´¢" - user-lists: "列表" - user-groups: "群组" - widgets: "å°éƒ¨ä»¶" - game: "游æˆ" - admin: "管ç†" - about: "关于 Misskey" -mobile/views/pages/drive.vue: - contextmenu: - upload: "ä¸Šä¼ æ–‡ä»¶" - url-upload: "从URLä¸Šä¼ æ–‡ä»¶" - create-folder: "创建文件夹" - rename-folder: "é‡å‘½å文件夹" - move-folder: "移动æ¤æ–‡ä»¶å¤¹" - delete-folder: "åˆ é™¤æ¤æ–‡ä»¶å¤¹" -mobile/views/pages/signup.vue: - lets-start: "æ‚¨çš„è´¦æˆ·çŽ°å·²å‡†å¤‡å°±ç»ªï¼ ðŸ“¦" -mobile/views/pages/followers.vue: - followers-of: "{name}的关注者" -mobile/views/pages/following.vue: - following-of: "{name}çš„æ£åœ¨å…³æ³¨" -mobile/views/pages/home.vue: - home: "首页" - local: "Local" - hybrid: "社交" - global: "Global" - mentions: "Mentions" - messages: "直接å‘布" -mobile/views/pages/tag.vue: - no-posts-found: "æ²¡æœ‰æ‰¾åˆ°å¸¦æœ‰å“ˆå¸Œæ ‡ç¾â€œ{q}â€çš„帖å" -mobile/views/pages/widgets.vue: - dashboard: "仪表盘" - widgets-hints: "您å¯ä»¥æ·»åŠ /åˆ é™¤/é‡æ–°æŽ’列å°éƒ¨ä»¶ã€‚ è¦ç§»åŠ¨å°éƒ¨ä»¶ï¼Œè¯·æ‹–动“三â€ã€‚ 点击“×â€åˆ 除å°éƒ¨ä»¶ã€‚ æŸäº›å°éƒ¨ä»¶å¯ä»¥é€šè¿‡ç‚¹å‡»æ¥æ›´æ”¹æ˜¾ç¤ºã€‚" - add-widget: "æ·»åŠ " - customization-tips: "定制æ示" -mobile/views/pages/widgets/activity.vue: - activity: "活动" -mobile/views/pages/share.vue: - share-with: "共享{name}" -mobile/views/pages/note.vue: - title: "帖文" - prev: "上一个帖å" - next: "下一个帖å" -mobile/views/pages/games/reversi.vue: - reversi: "游æˆ" -mobile/views/pages/search.vue: - search: "æœç´¢" - not-found: "没有找到有关于“{q}â€çš„帖å" -mobile/views/pages/selectdrive.vue: - select-file: "选择文件" -mobile/views/pages/notifications.vue: - notifications: "通知" -mobile/views/pages/settings.vue: - signed-in-as: "以{}登录" -mobile/views/pages/user.vue: - follows-you: "关注您" - following: "关注ä¸" - followers: "关注者" - notes: "帖å" - overview: "概观" - timeline: "时间线" - media: "媒体" - years-old: "{age}å²" -mobile/views/pages/user/home.vue: - recent-notes: "最近的帖å" - images: "图片" - activity: "活动" - keywords: "关键å—" - domains: "域å" - frequently-replied-users: "活跃用户" - followers-you-know: "您å¯èƒ½è®¤è¯†çš„关注者" - last-used-at: "上次登录:" -mobile/views/pages/user/home.photos.vue: - no-photos: "没有图片" -deck: - widgets: "å°éƒ¨ä»¶" - home: "首页" - local: "Local" - hybrid: "社交" - hashtag: "å“ˆå¸Œæ ‡ç¾" - global: "Global" - mentions: "Mentions" - direct: "直接å‘布" - notifications: "通知" - list: "列表" - select-list: "请选择一个列表" - swap-left: "å‘左移动" - swap-right: "å‘å³ç§»åŠ¨" - swap-up: "å‘上移动" - swap-down: "å‘下移动" - remove: "移除" - add-column: "æ·»åŠ ä¸€åˆ—" - rename: "é‡å‘½å" - stack-left: "å‘左折å " - pop-right: "带到å³è¾¹" - disabled-timeline: - title: "ç¦ç”¨æ—¶é—´çº¿" - description: "æœåŠ¡å™¨ç®¡ç†å‘˜å·²ç¦ç”¨æ—¶é—´çº¿ã€‚" -deck/deck.tl-column.vue: - is-media-only: "åªæœ‰åª’体的帖å" - edit: "选项" -deck/deck.user-column.vue: - follows-you: "关注您" - posts: "帖å" - following: "关注ä¸" - followers: "关注者" - images: "图片" - activity: "活动" - timeline: "时间线" - pinned-notes: "置顶帖" - pinned-page: "已置顶的页é¢" -docs: - edit-this-page-on-github: "å‘现错误或想è¦ä¸ºæ–‡æ¡£åšå‡ºè´¡çŒ®ï¼Ÿ" - edit-this-page-on-github-link: "在GitHub上编辑这个页é¢ã€‚" -dev/views/index.vue: - manage-apps: "管ç†åº”用" -dev/views/apps.vue: - manage-apps: "管ç†åº”用" - create-app: "创建应用" - app-missing: "没有应用" -dev/views/new-app.vue: - new-app: "新应用" - new-app-info: "å¯ä»¥ä»Ž API ä¸åˆ›å»ºåº”用。 (app/create)" - create-app: "æ£åœ¨åˆ›å»ºåº”用" - app-name: "应用å称" - app-name-placeholder: "ex) iOS版Misskey" - app-name-desc: "您应用的å称" - app-overview: "应用摘è¦" - app-overview-placeholder: " ex) iOS版Misskey客户端." - app-overview-desc: "您的应用的简è¦è¯´æ˜Žæˆ–介ç»ã€‚" - callback-url: "回应URL (optional)" - callback-url-placeholder: "ex) https://your.app.example.com/callback.php" - callback-url-desc: "通过身份验è¯è¡¨å•å¯¹ç”¨æˆ·è¿›è¡Œèº«ä»½éªŒè¯åŽé‡å®šå‘到的URL。" - authority: "æƒé™" - authority-desc: "åªèƒ½é€šè¿‡API访问æ¤å¤„请求的功能。" - authority-warning: "您å¯ä»¥åœ¨åˆ›å»ºåº”用程åºåŽå¯¹å…¶è¿›è¡Œæ›´æ”¹ï¼Œä½†å¦‚果您授予ä¸åŒçš„æƒé™ï¼Œåˆ™å½“时关è”的所有用户密钥都将失效。" -pages: - new-page: "创建页é¢" - edit-page: "编辑页é¢" - read-page: "查看æº" - page-created: "页é¢å·²åˆ›å»º" - page-updated: "页é¢å·²æ›´æ–°" - name-already-exists: "该页é¢URLå·²å˜åœ¨" - title-invalid-name: "æ— æ•ˆçš„é¡µé¢URL" - text-invalid-name: "请确认该项ä¸ä¸ºç©º" - are-you-sure-delete: "是å¦åˆ 除æ¤é¡µé¢ï¼Ÿ" - page-deleted: "该页é¢å·²è¢«åˆ 除。" - edit-this-page: "编辑æ¤é¡µé¢" - pin-this-page: "置顶" - unpin-this-page: "å–消置顶" - view-source: "查看æºä»£ç " - view-page: "查看页é¢" - like: "赞" - unlike: "å–消赞" - liked-pages: "喜欢的页é¢" - my-pages: "个人页é¢" - inspector: "检查器" - content: "页é¢å†…容" - variables: "å˜é‡" - variables-info: "您å¯ä»¥ä½¿ç”¨å˜é‡åˆ›å»ºåŠ¨æ€é¡µé¢ã€‚在文本ä¸é€šè¿‡<b>{å˜é‡å}</b>的写法æ¥åµŒå…¥å˜é‡å€¼ã€‚例如在文本<b>Hello { thing } world!</b>ä¸ï¼Œå¦‚æžœå˜é‡(thing)的值为<b>ai</b>,那么该文本会æˆä¸º<b>Hello ai world!</b>。" - variables-info2: "å› ä¸ºå˜é‡çš„计算(计算å˜é‡å€¼)是从上到下执行的,所以ä¸èƒ½åœ¨å˜é‡ä¸å¼•ç”¨ä¸‹é¢çš„å˜é‡ã€‚例如从上到下ä¾æ¬¡å®šä¹‰äº†<b>A,B,C</b>3个å˜é‡ï¼Œé‚£ä¹ˆ<b>C</b>ä¸å¯ä»¥å¼•ç”¨<b>A</b>或<b>B</b>,但是<b>A</b>æ— æ³•å¼•ç”¨<b>B</b>或<b>C</b>。" - variables-info3: "为了接收æ¥è‡ªç”¨æˆ·çš„输入,页é¢ä¸Šè®¾æœ‰â€œç”¨æˆ·è¾“å…¥â€å—,在“å˜é‡å称â€ä¸è®¾ç½®è¦åœ¨å…¶ä¸ä¿å˜è¾“入值的å˜é‡å(å˜é‡ä¼šè‡ªåŠ¨åˆ›å»º)。您å¯ä»¥ä½¿ç”¨è¯¥å˜é‡æ‰§è¡Œæ“作以å“应用户输入。" - variables-info4: "通过使用函数,您å¯ä»¥å°†æ•°å€¼è®¡ç®—过程组åˆæˆå¯é‡ç”¨çš„å½¢å¼ã€‚è¦åˆ›å»ºå‡½æ•°ï¼Œéœ€è¦åˆ›å»ºä¸€ä¸ªâ€œå‡½æ•°â€ç±»åž‹çš„å˜é‡ã€‚ä½ å¯ä»¥å°†å‡½æ•°è®¾å®šä¸ºæ§½å‡½æ•°(å‚æ•°)çš„æ ¼å¼ï¼Œæ§½å‡½æ•°çš„值å¯ä½œä¸ºå‡½æ•°ä¸çš„å˜é‡ä½¿ç”¨ã€‚å¦å¤–,AiScriptæ ‡å‡†ä¸è¿˜æœ‰ä¸€äº›å‡½æ•°ä¼šå°†å‡½æ•°ä½œä¸ºå‚æ•°(称为高阶函数)。\n除了已ç»é¢„先定义的函数外,您也å¯ä»¥å°†å®ƒä»¬è®¾ç½®ä¸ºè¿™äº›é«˜é˜¶å‡½æ•°çš„槽函数。" - more-details: "详细说明" - title: "æ ‡é¢˜" - url: "页é¢URL" - summary: "页é¢æ‘˜è¦" - align-center: "å±…ä¸" - hide-title-when-pinned: "置顶时éšè—æ ‡é¢˜" - font: "å—体" - fontSerif: "衬线å—体" - fontSansSerif: "æ— è¡¬çº¿å—体" - set-eye-catching-image: "设置å°é¢å›¾ç‰‡" - remove-eye-catching-image: "åˆ é™¤å°é¢å›¾ç‰‡" - choose-block: "æ·»åŠ å—" - select-type: "类型选择" - enter-variable-name: "请确定å˜é‡å" - the-variable-name-is-already-used: "å˜é‡å已使用" - content-blocks: "内容" - input-blocks: "输入" - special-blocks: "特殊" - post-from-post-form: "å‘布æ¤å†…容" - posted-from-post-form: "å·²å‘布" - blocks: - text: "文本" - textarea: "文本区域" - section: "ç« èŠ‚" - image: "图片" - button: "按钮" - if: "判æ–" - _if: - variable: "å˜é‡" - post: "投稿窗å£" - _post: - text: "内容" - textInput: "文本输入" - _textInput: - name: "å˜é‡å" - text: "æ ‡é¢˜" - default: "默认值" - textareaInput: "多行文本输入" - _textareaInput: - name: "å˜é‡å" - text: "æ ‡é¢˜" - default: "默认值" - numberInput: "数值输入" - _numberInput: - name: "å˜é‡å" - text: "æ ‡é¢˜" - default: "默认值" - switch: "开关" - _switch: - name: "å˜é‡å" - text: "æ ‡é¢˜" - default: "默认值" - counter: "计数器" - _counter: - name: "å˜é‡å" - text: "æ ‡é¢˜" - inc: "å¢žåŠ å€¼" - _button: - text: "æ ‡é¢˜" - colored: "彩色" - action: "按下按钮时的行为" - _action: - dialog: "显示对è¯æ¡†" - _dialog: - content: "内容" - resetRandom: "éšæœºå€¼é‡ç½®" - pushEvent: "å‘é€äº‹ä»¶" - _pushEvent: - event: "事件å称" - message: "按下时显示的消æ¯" - variable: "å‘é€çš„å˜é‡" - no-variable: "空" - radioButton: "选择项" - _radioButton: - name: "å˜é‡å" - title: "æ ‡é¢˜" - values: "使用æ¢è¡ŒåŒºåˆ†çš„选择项" - default: "默认值" - script: - categories: - flow: "控制" - logical: "逻辑è¿ç®—" - operation: "计算" - comparison: "比较" - random: "éšæœº" - value: "值" - fn: "函数" - text: "文本æ“作" - convert: "转æ¢" - list: "列表" - blocks: - text: "文本" - multiLineText: "文本 (多行)" - textList: "文本列表" - _textList: - info: "情使用æ¢è¡Œç¬¦åˆ†éš”æ¯è¡Œ" - strLen: "文本长度" - _strLen: - arg1: "文本" - strPick: "å—符æå–" - _strPick: - arg1: "文本" - arg2: "å—符ä½ç½®" - strReplace: "文本替æ¢" - _strReplace: - arg1: "文本" - arg2: "替æ¢ä¹‹å‰" - arg3: "替æ¢ä¹‹åŽ" - strReverse: "文本åå‘" - _strReverse: - arg1: "文本" - join: "åˆå¹¶æ–‡æœ¬" - _join: - arg1: "列表" - arg2: "分隔符" - add: "+ åŠ " - _add: - arg1: "A" - arg2: "B" - subtract: "- å‡" - _subtract: - arg1: "A" - arg2: "B" - multiply: "× 乘" - _multiply: - arg1: "A" - arg2: "B" - divide: "÷ 除" - _divide: - arg1: "A" - arg2: "B" - mod: "÷ å–模" - _mod: - arg1: "A" - arg2: "B" - round: "å››èˆäº”å…¥" - _round: - arg1: "数值" - eq: "Aå’ŒB相ç‰" - _eq: - arg1: "A" - arg2: "B" - notEq: "Aå’ŒBä¸ç‰" - _notEq: - arg1: "A" - arg2: "B" - and: "Aå’ŒB" - _and: - arg1: "A" - arg2: "B" - or: "A或B" - _or: - arg1: "A" - arg2: "B" - lt: "< Aå°äºŽB" - _lt: - arg1: "A" - arg2: "B" - gt: "> A大于B" - _gt: - arg1: "A" - arg2: "B" - ltEq: "<= Aå°äºŽç‰äºŽB" - _ltEq: - arg1: "A" - arg2: "B" - gtEq: ">= A大于ç‰äºŽB" - _gtEq: - arg1: "A" - arg2: "B" - if: "分支" - _if: - arg1: "如果" - arg2: "çš„è¯" - arg3: "å¦åˆ™" - not: "å¦å®š" - _not: - arg1: "å¦å®š" - random: "éšæœº" - _random: - arg1: "概率" - rannum: "éšæœº" - _rannum: - arg1: "最å°" - arg2: "最大" - randomPick: "从列表ä¸éšæœºé€‰æ‹©" - _randomPick: - arg1: "列表" - dailyRandom: "éšæœº(æ¯ä¸ªç”¨æˆ·æ¯æ—¥)" - _dailyRandom: - arg1: "概率" - dailyRannum: "éšæœºæ•°(æ¯ä¸ªç”¨æˆ·æ¯æ—¥)" - _dailyRannum: - arg1: "最å°" - arg2: "最大" - dailyRandomPick: "从列表ä¸éšæœºé€‰æ‹©(æ¯ä¸ªç”¨æˆ·æ¯æ—¥)" - _dailyRandomPick: - arg1: "列表" - seedRandom: "éšæœº (ç§å)" - _seedRandom: - arg1: "ç§å" - arg2: "概率" - seedRannum: "éšæœºæ•°(ç§å)" - _seedRannum: - arg1: "ç§å" - arg2: "最å°" - arg3: "最大" - seedRandomPick: "从列表ä¸éšæœºé€‰æ‹© (ç§å)" - _seedRandomPick: - arg1: "ç§å" - arg2: "列表" - DRPWPM: "从概率列表ä¸éšæœºé€‰æ‹©(æ¯ç”¨æˆ·æ¯å¤©)" - _DRPWPM: - arg1: "文本列表" - pick: "从列表ä¸é€‰æ‹©" - _pick: - arg1: "列表" - arg2: "ä½ç½®" - listLen: "获å–列表长度" - _listLen: - arg1: "列表" - number: "数值" - stringToNumber: "文本到数å—" - _stringToNumber: - arg1: "文本" - numberToString: "æ•°å—到文本" - _numberToString: - arg1: "数值" - splitStrByLine: "将文本按行拆分" - _splitStrByLine: - arg1: "文本" - ref: "å˜é‡" - fn: "函数" - _fn: - slots: "槽函数" - slots-info: "请使用æ¢è¡Œç¬¦åˆ†éš”æ¯ä¸ªæ§½å‡½æ•°" - arg1: "输出" - for: "é‡å¤" - _for: - arg1: "次数" - arg2: "处ç†" - typeError: "槽函数{slot}需è¦ä¼ 入“{expect}â€ï¼Œä½†æ˜¯å®žé™…ä¼ å…¥ä¸ºâ€œ{actual}â€ï¼" - thereIsEmptySlot: "槽函数{slot}为空ï¼" - types: - string: "文本" - number: "数值" - boolean: "布尔值" - array: "列表" - stringArray: "文本列表" - emptySlot: "空白槽函数" - enviromentVariables: "环境å˜é‡" - pageVariables: "页é¢å…ƒç´ " - argVariables: "输入槽函数" -room: - add-furniture: "放置家具" - translate: "移动" - rotate: "旋转" - exit: "返回" - remove: "移除" - save: "ä¿å˜" - saved: "å·²ä¿å˜" - clear: "清ç†" - clear-confirm: "是å¦æ¸…除所有家具?" - leave-confirm: "有尚未ä¿å˜çš„修改。是å¦ç¦»å¼€ï¼Ÿ" - chooseImage: "选择图片" - room-type: "房间类型" - carpet-color: "地æ¿é¢œè‰²" - rooms: - default: "默认" - washitsu: "å’Œå¼æˆ¿é—´" - furnitures: - milk: "牛奶纸箱" - bed: "床" - low-table: "矮桌" - desk: "书桌" - chair: "椅å" - chair2: "椅å2" - fan: "æ¢æ°”扇" - pc: "电脑" - plant: "观å¶æ¤ç‰©" - plant2: "观å¶æ¤ç‰©2" - eraser: "橡皮擦" - pencil: "铅笔" - pudding: "布ä¸" - cardboard-box: "纸æ¿ç®±" - cardboard-box2: "纸æ¿ç®±2" - cardboard-box3: "纸æ¿ç®±3" - book: "书" - book2: "书2" - piano: "é’¢ç´" - facial-tissue: "纸巾盒" - server: "æœåŠ¡å™¨" - moon: "月çƒ" - corkboard: "软木æ¿" - mousepad: "é¼ æ ‡åž«" - monitor: "显示器" - keyboard: "键盘" - carpet-stripe: "地毯(æ¡çº¹)" - mat: "åž«å" - color-box: "收纳柜" - wall-clock: "挂钟" - photoframe: "相框" - cube: "立方体" - tv: "电视" - pinguin: "ä¼é¹…å›" - rubik-cube: "é”æ–¹" - poster-h: "海报(横å‘)" - poster-v: "海报(纵å‘)" - sofa: "æ²™å‘" - spiral: "螺旋楼梯" - bin: "垃圾箱" - cup-noodle: "æ¯é¢" - holo-display: "å…¨æ¯æ˜¾ç¤ºå™¨" - energy-drink: "能é‡é¥®æ–™" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml deleted file mode 100644 index a0138b465fd314a60c786a0e0c83c2eb812b2b9e..0000000000000000000000000000000000000000 --- a/locales/zh-TW.yml +++ /dev/null @@ -1,91 +0,0 @@ ---- -meta: - lang: "ä¸æ–‡ï¼ˆç¹ä½“)" -common: - intro: - title: "什麽是 Misskey å‘¢?" - rich-contents: "發佈" - reaction: "回應" - drive: "雲端硬碟" - close: "關閉" - enter-password: "請輸入密碼" - 2fa: "é›™é‡èº«ä»½é©—è‰" - dark-mode: "夜間模å¼" - signup: "註冊" - signout: "登出" - notification: - reversi-invited: "æ‚¨å·²è¢«é‚€è«‹åŠ å…¥å£¹å ´éŠæˆ²" - reversi-invited-by: "來自{}的邀請" - notified-by: "來自{}的邀請" - time: - future: "未來" - just_now: "剛剛" - drive: "雲端硬碟" - weekday: - sunday: "週日" - monday: "週一" - tuesday: "週二" - wednesday: "週三" - thursday: "週四" - friday: "週五" - saturday: "週å…" - reactions: - like: "è´Š" - love: "å–œæ¡" - congrats: "æå–œ" - _settings: - password: "密碼" - font-size: "å—體大å°" - font-size-x-small: "å°" - font-size-small: "較å°" - deck-column-width-wide: "寬" - timeline: "時間軸" -common/views/components/connect-failed.troubleshooter.vue: - flush: "清除快å–" -common/views/components/theme.vue: - light-themes: "淺色主題" - dark-themes: "深色主題" - install-a-theme: "安è£ä¸»é¡Œ" - save-created-theme: "ä¿å˜ä¸»é¡Œ" -common/views/components/signin.vue: - signin-with-twitter: "用 Twitter 帳號登入" - signin-with-github: "用 GitHub 帳號登入" - signin-with-discord: "用 Discord 帳號登入" - login-failed: "登錄失敗。 請檢查用戶å和密碼。" -common/views/components/signup.vue: - invitation-code: "邀請碼" - username: "用戶å" - available: "å¯ç”¨" - too-long: "è«‹ä¸è¦è¶…éŽ20個å—å…ƒ" - password: "密碼" - password-placeholder: "建è°è‡³å°‘8個å—å…ƒ" -common/views/components/stream-indicator.vue: - connecting: "æ£åœ¨é€£ç·š" - reconnecting: "æ£åœ¨é‡æ–°é€£ç·š" - connected: "已建立連線" -common/views/components/integration-settings.vue: - disconnect: "ä¸æ–·é€£ç·š" -common/views/components/github-setting.vue: - reconnect: "é‡æ–°é€£ç·š" - disconnect: "ä¸æ–·é€£ç·š" -common/views/components/discord-setting.vue: - reconnect: "é‡æ–°é€£ç·š" - disconnect: "ä¸æ–·é€£ç·š" -common/views/components/language-settings.vue: - recommended: "推薦" - auto: "自動" - specify-language: "指定語言" -common/views/components/profile-editor.vue: - title: "個人資料" - name: "å稱" - birthday: "生日:" - privacy: "éš±ç§" -admin/views/dashboard.vue: - drive: "雲端硬碟" -admin/views/charts.vue: - drive: "雲端硬碟" -pages: - like: "è´Š" -room: - furnitures: - moon: "月" diff --git a/migration/1579267006611-v12.ts b/migration/1579267006611-v12.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c15283fa4d547730924e7dd02a2906747cd714b --- /dev/null +++ b/migration/1579267006611-v12.ts @@ -0,0 +1,34 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v121579267006611 implements MigrationInterface { + name = 'v121579267006611' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`CREATE TABLE "announcement" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "text" character varying(8192) NOT NULL, "title" character varying(256) NOT NULL, "imageUrl" character varying(1024), CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_118ec703e596086fc4515acb39" ON "announcement" ("createdAt") `, undefined); + await queryRunner.query(`CREATE TABLE "announcement_read" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "announcementId" character varying(32) NOT NULL, CONSTRAINT "PK_4b90ad1f42681d97b2683890c5e" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_8288151386172b8109f7239ab2" ON "announcement_read" ("userId") `, undefined); + await queryRunner.query(`CREATE INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf" ON "announcement_read" ("announcementId") `, undefined); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_924fa71815cfa3941d003702a0" ON "announcement_read" ("userId", "announcementId") `, undefined); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVerified"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "announcements"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableEmojiReaction"`, undefined); + await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_8288151386172b8109f7239ab28" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe" FOREIGN KEY ("announcementId") REFERENCES "announcement"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe"`, undefined); + await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_8288151386172b8109f7239ab28"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableEmojiReaction" boolean NOT NULL DEFAULT true`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ADD "announcements" jsonb NOT NULL DEFAULT '[]'`, undefined); + await queryRunner.query(`ALTER TABLE "user" ADD "isVerified" boolean NOT NULL DEFAULT false`, undefined); + await queryRunner.query(`DROP INDEX "IDX_924fa71815cfa3941d003702a0"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_8288151386172b8109f7239ab2"`, undefined); + await queryRunner.query(`DROP TABLE "announcement_read"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_118ec703e596086fc4515acb39"`, undefined); + await queryRunner.query(`DROP TABLE "announcement"`, undefined); + } + +} diff --git a/migration/1579270193251-v12-2.ts b/migration/1579270193251-v12-2.ts new file mode 100644 index 0000000000000000000000000000000000000000..efad0cd56075fb54907a4ea121dbd79315424adb --- /dev/null +++ b/migration/1579270193251-v12-2.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1221579270193251 implements MigrationInterface { + name = 'v1221579270193251' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "announcement_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "announcement_read" DROP COLUMN "createdAt"`, undefined); + } + +} diff --git a/migration/1579282808087-v12-3.ts b/migration/1579282808087-v12-3.ts new file mode 100644 index 0000000000000000000000000000000000000000..a330caa978f01ed83ee2965fa05a69a03e60c4f8 --- /dev/null +++ b/migration/1579282808087-v12-3.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1231579282808087 implements MigrationInterface { + name = 'v1231579282808087' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "announcement" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "updatedAt"`, undefined); + } + +} diff --git a/migration/1579544426412-v12-4.ts b/migration/1579544426412-v12-4.ts new file mode 100644 index 0000000000000000000000000000000000000000..d35b25d045e1dd393f5ee5dc7026517d52df339b --- /dev/null +++ b/migration/1579544426412-v12-4.ts @@ -0,0 +1,16 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1241579544426412 implements MigrationInterface { + name = 'v1241579544426412' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "notification" ADD "followRequestId" character varying(32)`, undefined); + await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_bd7fab507621e635b32cd31892c" FOREIGN KEY ("followRequestId") REFERENCES "follow_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_bd7fab507621e635b32cd31892c"`, undefined); + await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "followRequestId"`, undefined); + } + +} diff --git a/migration/1579977526288-v12-5.ts b/migration/1579977526288-v12-5.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f824a676e1f6e2733ba99b65fa6d7f0f525375a --- /dev/null +++ b/migration/1579977526288-v12-5.ts @@ -0,0 +1,54 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1251579977526288 implements MigrationInterface { + name = 'v1251579977526288' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`CREATE TABLE "clip" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_f0685dac8d4dd056d7255670b75" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_2b5ec6c574d6802c94c80313fb" ON "clip" ("userId") `, undefined); + await queryRunner.query(`CREATE TABLE "clip_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_e94cda2f40a99b57e032a1a738b" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_a012eaf5c87c65da1deb5fdbfa" ON "clip_note" ("noteId") `, undefined); + await queryRunner.query(`CREATE INDEX "IDX_ebe99317bbbe9968a0c6f579ad" ON "clip_note" ("clipId") `, undefined); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fc0ec357d55a18646262fdfff" ON "clip_note" ("noteId", "clipId") `, undefined); + await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'list')`, undefined); + await queryRunner.query(`CREATE TABLE "antenna" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "src" "antenna_src_enum" NOT NULL, "userListId" character varying(32), "keywords" jsonb NOT NULL DEFAULT '[]', "withFile" boolean NOT NULL, "expression" character varying(2048), "notify" boolean NOT NULL, "hasNewNote" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_c170b99775e1dccca947c9f2d5f" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_6446c571a0e8d0f05f01c78909" ON "antenna" ("userId") `, undefined); + await queryRunner.query(`CREATE TABLE "antenna_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, CONSTRAINT "PK_fb28d94d0989a3872df19fd6ef8" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_bd0397be22147e17210940e125" ON "antenna_note" ("noteId") `, undefined); + await queryRunner.query(`CREATE INDEX "IDX_0d775946662d2575dfd2068a5f" ON "antenna_note" ("antennaId") `, undefined); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_335a0bf3f904406f9ef3dd51c2" ON "antenna_note" ("noteId", "antennaId") `, undefined); + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "geo"`, undefined); + await queryRunner.query(`ALTER TABLE "clip" ADD CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_6446c571a0e8d0f05f01c789096" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_0d775946662d2575dfd2068a5f5"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_bd0397be22147e17210940e125b"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_6446c571a0e8d0f05f01c789096"`, undefined); + await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf"`, undefined); + await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3"`, undefined); + await queryRunner.query(`ALTER TABLE "clip" DROP CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2"`, undefined); + await queryRunner.query(`ALTER TABLE "note" ADD "geo" jsonb`, undefined); + await queryRunner.query(`DROP INDEX "IDX_335a0bf3f904406f9ef3dd51c2"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_0d775946662d2575dfd2068a5f"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_bd0397be22147e17210940e125"`, undefined); + await queryRunner.query(`DROP TABLE "antenna_note"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_6446c571a0e8d0f05f01c78909"`, undefined); + await queryRunner.query(`DROP TABLE "antenna"`, undefined); + await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_6fc0ec357d55a18646262fdfff"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_ebe99317bbbe9968a0c6f579ad"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_a012eaf5c87c65da1deb5fdbfa"`, undefined); + await queryRunner.query(`DROP TABLE "clip_note"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_2b5ec6c574d6802c94c80313fb"`, undefined); + await queryRunner.query(`DROP TABLE "clip"`, undefined); + } + +} diff --git a/migration/1579993013959-v12-6.ts b/migration/1579993013959-v12-6.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fa4623c3e6f307a17a20dd472cb854b1d860c44 --- /dev/null +++ b/migration/1579993013959-v12-6.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1261579993013959 implements MigrationInterface { + name = 'v1261579993013959' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hasNewNote"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna_note" ADD "read" boolean NOT NULL DEFAULT false`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read") `, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`DROP INDEX "IDX_9937ea48d7ae97ffb4f3f063a4"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna_note" DROP COLUMN "read"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD "hasNewNote" boolean NOT NULL DEFAULT false`, undefined); + } + +} diff --git a/migration/1580069531114-v12-7.ts b/migration/1580069531114-v12-7.ts new file mode 100644 index 0000000000000000000000000000000000000000..227e7cceb6d99d54613dba77d334adfebbec76f7 --- /dev/null +++ b/migration/1580069531114-v12-7.ts @@ -0,0 +1,24 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1271580069531114 implements MigrationInterface { + name = 'v1271580069531114' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD "caseSensitive" boolean NOT NULL DEFAULT false`, undefined); + await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined); + await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined); + await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'list')`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined); + await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined); + await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO "antenna_src_enum"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "caseSensitive"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined); + } + +} diff --git a/migration/1580148575182-v12-8.ts b/migration/1580148575182-v12-8.ts new file mode 100644 index 0000000000000000000000000000000000000000..c63bdb4eb4f36a6507df8b6837ac95953b8ef224 --- /dev/null +++ b/migration/1580148575182-v12-8.ts @@ -0,0 +1,16 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1281580148575182 implements MigrationInterface { + name = 'v1281580148575182' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`, undefined); + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "appId"`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "note" ADD "appId" character varying(32)`, undefined); + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined); + } + +} diff --git a/migration/1580154400017-v12-9.ts b/migration/1580154400017-v12-9.ts new file mode 100644 index 0000000000000000000000000000000000000000..de06d26e49e9a7fd7e71f6f143f25f9ea4c980f3 --- /dev/null +++ b/migration/1580154400017-v12-9.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v1291580154400017 implements MigrationInterface { + name = 'v1291580154400017' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "antenna" ADD "withReplies" boolean NOT NULL DEFAULT false`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "withReplies"`, undefined); + } + +} diff --git a/migration/1580276619901-v12-10.ts b/migration/1580276619901-v12-10.ts new file mode 100644 index 0000000000000000000000000000000000000000..f48f42b4ac258d8c3c29847d3c42b1909c5ad074 --- /dev/null +++ b/migration/1580276619901-v12-10.ts @@ -0,0 +1,19 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class v12101580276619901 implements MigrationInterface { + name = 'v12101580276619901' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`TRUNCATE TABLE "notification"`, undefined); + await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined); + await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined); + await queryRunner.query(`ALTER TABLE "notification" ADD "type" "notification_type_enum" NOT NULL`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined); + await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined); + await queryRunner.query(`ALTER TABLE "notification" ADD "type" character varying(32) NOT NULL`, undefined); + } + +} diff --git a/package.json b/package.json index d5b85fc8ca7e0bed9970e3a4c71eb68022a539a2..3a6d1c81de4703ff97af69ea5f51c71e69e8eaeb 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", - "author": "syuilo <i@syuilo.com>", - "version": "11.37.1", - "codename": "daybreak", + "author": "syuilo <syuilotan@yahoo.co.jp>", + "version": "12.0.0-alpha.10", + "codename": "indigo", "repository": { "type": "git", "url": "https://github.com/syuilo/misskey.git" @@ -112,6 +112,7 @@ "cbor": "5.0.1", "chai": "4.2.0", "chalk": "3.0.0", + "chart.js": "2.9.3", "cli-highlight": "2.1.4", "commander": "4.1.0", "content-disposition": "0.5.3", @@ -125,15 +126,16 @@ "eslint-plugin-vue": "6.1.2", "eventemitter3": "4.0.0", "feed": "4.1.0", + "fibers": "4.0.2", "file-type": "13.0.1", "fluent-ffmpeg": "2.1.2", "gulp": "4.0.2", "gulp-clean-css": "4.2.0", + "gulp-dart-sass": "0.9.1", "gulp-mocha": "7.0.2", "gulp-rename": "2.0.0", "gulp-replace": "1.0.0", "gulp-sourcemaps": "2.6.5", - "gulp-stylus": "2.7.0", "gulp-terser": "1.2.0", "gulp-tslint": "8.1.4", "gulp-typescript": "5.0.1", @@ -177,6 +179,7 @@ "parse5": "5.1.1", "parsimmon": "1.13.0", "pg": "7.17.0", + "portal-vue": "2.1.7", "portscanner": "2.2.0", "postcss-loader": "3.0.0", "prismjs": "1.18.0", @@ -204,6 +207,8 @@ "rimraf": "3.0.0", "rndstr": "1.0.0", "s-age": "1.1.2", + "sass": "1.25.0", + "sass-loader": "8.0.1", "seedrandom": "3.0.5", "sharp": "0.23.4", "showdown": "1.9.1", @@ -211,8 +216,6 @@ "speakeasy": "2.0.0", "stringz": "2.0.0", "style-loader": "1.1.2", - "stylus": "0.54.7", - "stylus-loader": "3.0.2", "summaly": "2.3.1", "syslog-pro": "1.0.0", "systeminformation": "4.17.3", @@ -238,10 +241,10 @@ "vue-content-loading": "1.6.0", "vue-cropperjs": "4.0.1", "vue-i18n": "8.15.3", - "vue-js-modal": "1.3.31", "vue-json-pretty": "1.6.3", "vue-loader": "15.8.3", "vue-marquee-text-component": "1.1.1", + "vue-meta": "2.3.1", "vue-prism-component": "1.1.1", "vue-router": "3.1.3", "vue-sequential-entrance": "1.1.3", @@ -249,7 +252,6 @@ "vue-svg-inline-loader": "1.4.4", "vue-template-compiler": "2.6.11", "vuedraggable": "2.23.2", - "vuewordcloud": "18.7.11", "vuex": "3.1.2", "vuex-persistedstate": "2.7.0", "web-push": "3.4.3", diff --git a/src/@types/const.json.d.ts b/src/@types/const.json.d.ts deleted file mode 100644 index 40a96f2a2af7008c2b5a4b3d33a4d963bce4ce0f..0000000000000000000000000000000000000000 --- a/src/@types/const.json.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module '*/const.json' { - const copyright: string; -} diff --git a/src/boot/master.ts b/src/boot/master.ts index db063ef4b08931955b011c0028a32942c9c67726..d6d5ed438f2a81de2e090fdfd13b130779dc8097 100644 --- a/src/boot/master.ts +++ b/src/boot/master.ts @@ -77,7 +77,6 @@ export async function masterMain() { if (!program.noDaemons) { require('../daemons/server-stats').default(); - require('../daemons/notes-stats').default(); require('../daemons/queue-stats').default(); require('../daemons/janitor').default(); } diff --git a/src/client/app.vue b/src/client/app.vue new file mode 100644 index 0000000000000000000000000000000000000000..3e65880b0a37b30154f24a4509b54a821962f750 --- /dev/null +++ b/src/client/app.vue @@ -0,0 +1,1105 @@ +<template> +<div class="mk-app" v-hotkey.global="keymap"> + <header class="header"> + <div class="title" ref="title"> + <transition name="header" mode="out-in" appear> + <button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button> + </transition> + <transition name="header" mode="out-in" appear> + <div class="body" :key="pageKey"> + <div class="default"> + <portal-target name="avatar" slim/> + <h1 class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></h1> + </div> + <div class="custom"> + <portal-target name="header" slim/> + </div> + </div> + </transition> + </div> + <div class="sub"> + <fa :icon="faSearch"/> + <input type="search" class="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/> + <button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> + </div> + </header> + + <nav class="nav" ref="nav"> + <div> + <button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn"> + <mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/> + </button> + <router-link class="item" active-class="active" to="/" exact v-if="$store.getters.isSignedIn"> + <fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span> + </router-link> + <router-link class="item" active-class="active" to="/" exact v-else> + <fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span> + </router-link> + <router-link class="item" active-class="active" to="/featured"> + <fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span> + </router-link> + <router-link class="item" active-class="active" to="/explore"> + <fa :icon="faHashtag" fixed-width/><span class="text">{{ $t('explore') }}</span> + </router-link> + <button class="item _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn"> + <fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span> + <i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i> + </button> + <router-link class="item" active-class="active" to="/my/messaging" v-if="$store.getters.isSignedIn"> + <fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span> + <i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i> + </router-link> + <router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked"> + <fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span> + <i v-if="$store.state.i.pendingReceivedFollowRequestsCount"><fa :icon="faCircle"/></i> + </router-link> + <router-link class="item" active-class="active" to="/my/drive" v-if="$store.getters.isSignedIn"> + <fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span> + </router-link> + <router-link class="item" active-class="active" to="/announcements"> + <fa :icon="faBroadcastTower" fixed-width/><span class="text">{{ $t('announcements') }}</span> + <i v-if="$store.getters.isSignedIn && $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i> + </router-link> + <button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu"> + <fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span> + </button> + <button class="item _button" @click="search()"> + <fa :icon="faSearch" fixed-width/><span class="text">{{ $t('search') }}</span> + </button> + <button class="item _button" @click="more"> + <fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span> + <i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i> + </button> + </div> + </nav> + + <div class="contents"> + <main ref="main"> + <div class="content"> + <transition name="page" mode="out-in"> + <router-view></router-view> + </transition> + </div> + <div class="powerd-by" :class="{ visible: !$store.getters.isSignedIn }"> + <b><router-link to="/">{{ host }}</router-link></b> + <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small> + </div> + </main> + + <div class="widgets"> + <div ref="widgets" :class="{ edit: widgetsEditMode }"> + <template v-if="enableWidgets && $store.getters.isSignedIn"> + <template v-if="widgetsEditMode"> + <mk-button primary @click="addWidget" class="add"><fa :icon="faPlus"/></mk-button> + <x-draggable + :list="widgets" + handle=".handle" + animation="150" + class="sortable" + @sort="onWidgetSort" + > + <div v-for="widget in widgets" class="customize-container" :key="widget.id"> + <header> + <span class="handle"><fa :icon="faBars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> + </header> + <div @click="widgetFunc(widget.id)"> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="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"/> + </template> + <button ref="widgetsEditButton" v-if="widgetsEditMode" class="_button edit" @click="widgetsEditMode = false">{{ $t('exitEdit') }}</button> + <button ref="widgetsEditButton" v-else class="_button edit" @click="widgetsEditMode = true">{{ $t('editWidgets') }}</button> + </template> + </div> + </div> + </div> + + <div class="buttons"> + <button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.pendingReceivedFollowRequestsCount || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button> + <button v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></button> + <button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button> + <button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> + </div> + + <button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> + + <transition name="zoom-in-top"> + <x-notifications v-if="notificationsOpen" class="notifications" ref="notifications"/> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; +import { v4 as uuid } from 'uuid'; +import i18n from './i18n'; +import { host } from './config'; +import { search } from './scripts/search'; +import contains from './scripts/contains'; +import MkToast from './components/toast.vue'; + +export default Vue.extend({ + i18n, + + components: { + XNotifications: () => import('./components/notifications.vue').then(m => m.default), + MkButton: () => import('./components/ui/button.vue').then(m => m.default), + XDraggable: () => import('vuedraggable'), + }, + + data() { + return { + host: host, + pageKey: 0, + searching: false, + notificationsOpen: false, + accounts: [], + lists: [], + connection: null, + searchQuery: '', + searchWait: false, + widgetsEditMode: false, + enableWidgets: window.innerWidth >= 1100, + canBack: false, + faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer + }; + }, + + computed: { + keymap(): any { + return { + 'p': this.post, + 'n': this.post, + }; + }, + + widgets(): any[] { + return this.$store.state.settings.widgets; + } + }, + + watch:{ + $route(to, from) { + this.pageKey++; + this.notificationsOpen = false; + this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); + }, + + notificationsOpen(open) { + if (open) { + for (const el of Array.from(document.querySelectorAll('*'))) { + el.addEventListener('mousedown', this.onMousedown); + } + } else { + for (const el of Array.from(document.querySelectorAll('*'))) { + el.removeEventListener('mousedown', this.onMousedown); + } + } + } + }, + + created() { + if (this.$store.getters.isSignedIn) { + this.connection = this.$root.stream.useSharedConnection('main'); + this.connection.on('notification', this.onNotification); + + if (this.widgets.length === 0) { + this.$store.dispatch('settings/setWidgets', [{ + name: 'notifications', + id: 'a', data: {} + }]); + } + } + + this.$root.stream.on('_disconnected_', async () => { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + title: this.$t('disconnectedFromServer'), + text: this.$t('reloadConfirm'), + }); + if (!confirm.canceled) { + location.reload(); + } + }); + + setInterval(() => { + this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px'; + }, 1000); + + // https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width + if (this.enableWidgets) { + setInterval(() => { + const width = this.$refs.widgetsEditButton.offsetLeft + 300; + this.$refs.widgets.style.width = width + 'px'; + }, 1000); + } + }, + + methods: { + back() { + window.history.back(); + }, + + post() { + this.$root.post(); + }, + + search() { + if (this.searching) return; + + this.$root.dialog({ + title: this.$t('search'), + input: true + }).then(async ({ canceled, result: query }) => { + if (canceled || query == null || query == '') return; + + this.searching = true; + search(this, query).finally(() => { + this.searching = false; + }); + }); + }, + + searchKeypress(e) { + if (e.keyCode == 13) { + this.searchWait = true; + search(this, this.searchQuery).finally(() => { + this.searchWait = false; + this.searchQuery = ''; + }); + } + }, + + showNav(ev) { + this.$root.menu({ + items: [{ + text: this.$t('search'), + icon: faSearch, + action: this.search, + }, null, this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? { + text: this.$t('instance'), + icon: faServer, + action: () => this.oepnInstanceMenu(ev), + } : undefined, { + type: 'link', + text: this.$t('announcements'), + to: '/announcements', + icon: faBroadcastTower, + indicate: this.$store.state.i.hasUnreadAnnouncement, + }, { + type: 'link', + text: this.$t('featured'), + to: '/featured', + icon: faFireAlt, + }, { + type: 'link', + text: this.$t('explore'), + to: '/explore', + icon: faHashtag, + }, { + type: 'link', + text: this.$t('messaging'), + to: '/my/messaging', + icon: faComments, + indicate: this.$store.state.i.hasUnreadMessagingMessage, + }, this.$store.state.i.isLocked ? { + type: 'link', + text: this.$t('followRequests'), + to: '/my/follow-requests', + icon: faUserClock, + indicate: this.$store.state.i.pendingReceivedFollowRequestsCount > 0, + } : undefined, { + type: 'link', + text: this.$t('drive'), + to: '/my/drive', + icon: faCloud, + }, { + text: this.$t('more'), + icon: faEllipsisH, + action: () => this.more(ev), + indicate: this.$store.state.i.hasUnreadMentions || this.$store.state.i.hasUnreadSpecifiedNotes + }, null, { + type: 'user', + user: this.$store.state.i, + action: () => this.openAccountMenu(ev), + }], + direction: 'up', + align: 'left', + fixed: true, + width: 200, + source: ev.currentTarget || ev.target, + }); + }, + + async openAccountMenu(ev) { + const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id); + + const accountItems = accounts.map(account => ({ + type: 'user', + user: account, + action: () => { this.switchAccount(account) } + })); + + this.$root.menu({ + items: [...[{ + type: 'link', + text: this.$t('profile'), + to: `/@${ this.$store.state.i.username }`, + avatar: this.$store.state.i, + }, { + type: 'link', + text: this.$t('settings'), + to: '/my/settings', + icon: faCog, + }, null, { + type: 'item', + text: this.$t('addAcount'), + icon: faPlus, + action: () => { this.addAcount() }, + }], ...accountItems], + align: 'left', + fixed: true, + width: 240, + source: ev.currentTarget || ev.target, + }); + }, + + oepnInstanceMenu(ev) { + this.$root.menu({ + items: [{ + type: 'link', + text: this.$t('statistics'), + to: '/instance/stats', + icon: faChartBar, + }, { + type: 'link', + text: this.$t('customEmojis'), + to: '/instance/emojis', + icon: faLaugh, + }, { + type: 'link', + text: this.$t('users'), + to: '/instance/users', + icon: faUsers, + }, { + type: 'link', + text: this.$t('files'), + to: '/instance/files', + icon: faCloud, + }, { + type: 'link', + text: this.$t('monitor'), + to: '/instance/monitor', + icon: faTachometerAlt, + }, { + type: 'link', + text: this.$t('jobQueue'), + to: '/instance/queue', + icon: faExchangeAlt, + }, { + type: 'link', + text: this.$t('federation'), + to: '/instance/federation', + icon: faGlobe, + }, { + type: 'link', + text: this.$t('announcements'), + to: '/instance/announcements', + icon: faBroadcastTower, + }, null, { + type: 'link', + text: this.$t('general'), + to: '/instance', + icon: faCog, + }], + align: 'left', + fixed: true, + width: 200, + source: ev.currentTarget || ev.target, + }); + }, + + more(ev) { + this.$root.menu({ + items: [...(this.$store.getters.isSignedIn ? [{ + type: 'link', + text: this.$t('lists'), + to: '/my/lists', + icon: faListUl, + }, { + type: 'link', + text: this.$t('antennas'), + to: '/my/antennas', + icon: faSatellite, + }, { + type: 'link', + text: this.$t('mentions'), + to: '/my/mentions', + icon: faAt, + indicate: this.$store.state.i.hasUnreadMentions + }, { + type: 'link', + text: this.$t('directNotes'), + to: '/my/messages', + icon: faEnvelope, + indicate: this.$store.state.i.hasUnreadSpecifiedNotes + }, { + type: 'link', + text: this.$t('favorites'), + to: '/my/favorites', + icon: faStar, + }, { + type: 'link', + text: this.$t('pages'), + to: '/my/pages', + icon: faFileAlt, + }, { + type: 'link', + text: this.$t('games'), + to: '/games', + icon: faGamepad, + }, null] : []), { + type: 'link', + text: this.$t('about'), + to: '/about', + icon: faInfoCircle, + }], + align: 'left', + fixed: true, + width: 200, + source: ev.currentTarget || ev.target, + }); + }, + + async addAcount() { + this.$root.new(await import('./components/signin-dialog.vue').then(m => m.default)).$once('login', res => { + this.$store.dispatch('addAcount', res); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + }, + + async switchAccount(account) { + const token = this.$store.state.device.accounts.find(x => x.id === account.id).token; + this.$root.api('i', {}, token).then(i => { + this.$store.dispatch('switchAccount', { + ...i, + token: token + }); + location.reload(); + }); + }, + + onNotification(notification) { + // TODO: ユーザーãŒç”»é¢ã‚’見ã¦ãªã„ã¨æ€ã‚れるã¨ã(ブラウザやタブãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã˜ã‚ƒãªã„ãªã©)ã¯é€ä¿¡ã—ãªã„ + this.$root.stream.send('readNotification', { + id: notification.id + }); + + this.$root.new(MkToast, { + notification + }); + }, + + onMousedown(e) { + e.preventDefault(); + if (!contains(this.$refs.notifications.$el, e.target) && + !contains(this.$refs.notificationButton, e.target) && + !contains(this.$refs.notificationButton2, e.target) + ) this.notificationsOpen = false; + return false; + }, + + widgetFunc(id) { + const w = this.$refs[id][0]; + if (w.func) w.func(); + }, + + onWidgetSort() { + this.saveHome(); + }, + + addWidget(ev) { + const widgets = [ + 'memo', + 'notifications', + 'timeline', + 'calendar', + 'rss', + 'trends', + ]; + + this.$root.menu({ + items: widgets.map(widget => ({ + text: this.$t('_widgets.' + widget), + action: () => { + this.$store.dispatch('settings/addWidget', { + name: widget, + id: uuid(), + data: {} + }); + } + })), + source: ev.currentTarget || ev.target, + }); + }, + + removeWidget(widget) { + this.$store.dispatch('settings/removeWidget', widget); + }, + + saveHome() { + this.$store.dispatch('settings/setWidgets', this.widgets); + } + } +}); +</script> + +<style lang="scss" scoped> +@keyframes blink { + 0% { opacity: 1; } + 30% { opacity: 1; } + 90% { opacity: 0; } +} + +.header-enter-active, .header-leave-active { + transition: opacity 0.5s, transform 0.5s !important; +} +.header-enter { + opacity: 0; + transform: scale(0.9); +} +.header-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.page-enter-active, .page-leave-active { + transition: opacity 0.5s, transform 0.5s !important; +} +.page-enter { + opacity: 0; + transform: translateY(-32px); +} +.page-leave-to { + opacity: 0; + transform: translateY(32px); +} + +.mk-app { + $header-height: 60px; + $nav-width: 250px; + $nav-icon-only-width: 74px; + $main-width: 700px; + $ui-font-size: 1em; + $nav-icon-only-threshold: 1300px; + $nav-hide-threshold: 700px; + $side-hide-threshold: 1100px; + + min-height: 100vh; + box-sizing: border-box; + padding-top: $header-height; + + &, > .header > .body { + display: flex; + margin: 0 auto; + } + + > .header { + position: fixed; + z-index: 1000; + top: 0; + right: 0; + height: $header-height; + width: calc(100% - #{$nav-width}); + //background-color: var(--panel); + -webkit-backdrop-filter: blur(32px); + backdrop-filter: blur(32px); + background-color: var(--header); + border-bottom: solid 1px var(--divider); + + @media (max-width: $nav-icon-only-threshold) { + width: calc(100% - #{$nav-icon-only-width}); + } + + @media (max-width: $nav-hide-threshold) { + width: 100%; + } + + > .title { + position: relative; + line-height: $header-height; + height: $header-height; + max-width: $main-width; + text-align: center; + + > .back { + position: absolute; + z-index: 1; + top: 0; + left: 0; + height: $header-height; + width: $header-height; + } + + > .body { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: $header-height; + + > .default { + padding: 0 $header-height; + + > .avatar { + $size: 32px; + display: inline-block; + width: $size; + height: $size; + vertical-align: bottom; + margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0; + } + + > .title { + display: inline-block; + font-size: $ui-font-size; + margin: 0; + line-height: $header-height; + + > [data-icon] { + margin-right: 8px; + } + } + } + + > .custom { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } + } + } + + > .sub { + $post-button-size: 42px; + $post-button-margin: (($header-height - $post-button-size) / 2); + position: absolute; + top: 0; + right: 16px; + height: $header-height; + + @media (max-width: $side-hide-threshold) { + display: none; + } + + > [data-icon] { + position: absolute; + top: 0; + left: 16px; + height: $header-height; + pointer-events: none; + font-size: 16px; + } + + > .search { + $margin: 8px; + width: calc(100% - #{$post-button-size + $post-button-margin + $margin}); + box-sizing: border-box; + margin-right: $margin; + padding: 0 12px 0 42px; + font-size: 1rem; + line-height: 38px; + border: none; + border-radius: 38px; + color: var(--fg); + background: var(--bg); + + &:focus { + outline: none; + } + } + + > .post { + width: $post-button-size; + height: $post-button-size; + margin: $post-button-margin 0 $post-button-margin $post-button-margin; + border-radius: 100%; + font-size: 16px; + } + } + } + + > .nav { + $avatar-size: 32px; + $avatar-margin: ($header-height - $avatar-size) / 2; + + flex: 0 0 $nav-width; + width: $nav-width; + box-sizing: border-box; + + @media (max-width: $nav-icon-only-threshold) { + flex: 0 0 $nav-icon-only-width; + width: $nav-icon-only-width; + } + + @media (max-width: $nav-hide-threshold) { + display: none; + } + + > div { + position: fixed; + top: 0; + left: 0; + z-index: 1001; + width: $nav-width; + height: 100vh; + padding-top: 16px; + box-sizing: border-box; + background: var(--navBg); + border-right: solid 1px var(--divider); + + @media (max-width: $nav-icon-only-threshold) { + width: $nav-icon-only-width; + } + + > .item { + position: relative; + display: block; + padding-left: 32px; + font-size: $ui-font-size; + font-weight: bold; + line-height: 3.2rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + color: var(--navFg); + + &:not(.active) { + opacity: 0.85; + + &:hover { + opacity: 1; + + > [data-icon] { + opacity: 1; + } + } + + > [data-icon] { + opacity: 0.85; + } + } + + > [data-icon] { + width: ($header-height - ($avatar-margin * 2)); + } + + > [data-icon], + > .avatar { + margin-right: $avatar-margin; + } + + > .avatar { + width: $avatar-size; + height: $avatar-size; + vertical-align: middle; + } + + > i { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + } + + &:hover { + text-decoration: none; + } + + &.active { + color: var(--navActive); + } + + @media (max-width: $nav-icon-only-threshold) { + padding-left: 0; + width: 100%; + text-align: center; + font-size: $ui-font-size * 1.2; + line-height: 3.5rem; + + > [data-icon], + > .avatar { + margin-right: 0; + } + + > i { + left: 10px; + } + + > .text { + display: none; + } + } + } + } + } + + > .contents { + display: flex; + margin: 0 auto; + min-width: 0; + + > main { + width: $main-width; + min-width: $main-width; + + @media (max-width: $side-hide-threshold) { + min-width: 0; + } + + > .content { + padding: 16px; + box-sizing: border-box; + + @media (max-width: 500px) { + padding: 8px; + } + } + + > .powerd-by { + font-size: 14px; + text-align: center; + margin: 32px 0; + visibility: hidden; + + &.visible { + visibility: visible; + } + + &:not(.visible) { + @media (min-width: 850px) { + display: none; + } + } + + @media (max-width: 500px) { + margin-top: 16px; + } + + > small { + display: block; + margin-top: 8px; + opacity: 0.5; + + @media (max-width: 500px) { + margin-top: 4px; + } + } + } + } + + > .widgets { + box-sizing: border-box; + + @media (max-width: $side-hide-threshold) { + display: none; + } + + > div { + position: sticky; + top: calc(#{$header-height} + var(--margin)); + height: calc(100vh - #{$header-height} - var(--margin)); + + &.edit { + overflow: auto; + width: auto !important; + } + + &:not(.edit) { + display: inline-flex; + flex-wrap: wrap; + flex-direction: column; + place-content: flex-start; + } + + > * { + margin: 0 var(--margin) var(--margin) 0; + width: 300px; + } + + > .add { + margin: 0 auto; + } + + > .edit { + display: block; + font-size: 0.9em; + margin: 0 auto; + } + + .customize-container { + margin: 8px 0; + background: #fff; + + > header { + position: relative; + 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; + } + } + } + } + } + } + + > .post { + display: none; + position: fixed; + z-index: 1000; + bottom: 32px; + right: 32px; + width: 64px; + height: 64px; + border-radius: 100%; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); + font-size: 22px; + + @media (min-width: ($nav-hide-threshold + 1px)) { + display: block; + } + + @media (min-width: ($side-hide-threshold + 1px)) { + display: none; + } + } + + > .buttons { + position: fixed; + z-index: 1000; + bottom: 0; + padding: 0 32px 32px 32px; + display: flex; + width: 100%; + box-sizing: border-box; + background: linear-gradient(0deg, var(--bg), var(--bonzsgfz)); + + @media (max-width: 500px) { + padding: 0 16px 16px 16px; + } + + @media (min-width: ($nav-hide-threshold + 1px)) { + display: none; + } + + > .button { + position: relative; + padding: 0; + margin: auto; + width: 64px; + height: 64px; + border-radius: 100%; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + > * { + font-size: 22px; + } + + &:disabled { + cursor: default; + + > * { + opacity: 0.5; + } + } + + &:not(.post) { + background: var(--panel); + color: var(--fg); + + &:hover { + background: var(--pcncwizz); + } + + > i { + position: absolute; + top: 0; + left: 0; + color: var(--accent); + font-size: 16px; + animation: blink 1s infinite; + } + } + } + } + + > .notifications { + position: fixed; + top: 32px; + left: 0; + right: 0; + margin: 0 auto; + z-index: 10001; + width: 350px; + height: 400px; + background: var(--vocsgcxy); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + border-radius: 6px; + box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); + overflow: hidden; + + @media (max-width: 800px) { + width: 320px; + height: 350px; + } + + @media (max-width: 500px) { + width: 290px; + height: 310px; + } + } +} +</style> diff --git a/src/client/app/admin/assets/header-icon.svg b/src/client/app/admin/assets/header-icon.svg deleted file mode 100644 index d677d2d16304709f93cc3d9be631cf686d724697..0000000000000000000000000000000000000000 Binary files a/src/client/app/admin/assets/header-icon.svg and /dev/null differ diff --git a/src/client/app/admin/script.ts b/src/client/app/admin/script.ts deleted file mode 100644 index 3f2d6466ace03b499e6d11d64b6607fac256e0d2..0000000000000000000000000000000000000000 --- a/src/client/app/admin/script.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Admin - */ - -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; -import Index from './views/index.vue'; -import NotFound from '../common/views/pages/not-found.vue'; - -init(launch => { - document.title = 'Admin'; - - // Init router - const router = new VueRouter({ - mode: 'history', - base: '/admin/', - routes: [ - { path: '/:page', component: Index }, - { path: '/', redirect: '/dashboard' }, - { path: '*', component: NotFound } - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/client/app/admin/style.styl b/src/client/app/admin/style.styl deleted file mode 100644 index ae1a28226a53ad93b6e6a1f68e4a0f1a3250d09c..0000000000000000000000000000000000000000 --- a/src/client/app/admin/style.styl +++ /dev/null @@ -1,6 +0,0 @@ -@import "../app" -@import "../reset" - -html - height 100% - background var(--bg) diff --git a/src/client/app/admin/views/abuse.vue b/src/client/app/admin/views/abuse.vue deleted file mode 100644 index afa285debcd09cd2b3133cb0a243b86be8666b6f..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/abuse.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faExclamationCircle"/> {{ $t('title') }}</template> - <section class="fit-top"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div v-for="report in userReports" :key="report.id" class="haexwsjc"> - <ui-horizon-group inputs> - <ui-input :value="report.user | acct" type="text" readonly> - <span>{{ $t('target') }}</span> - </ui-input> - <ui-input :value="report.reporter | acct" type="text" readonly> - <span>{{ $t('reporter') }}</span> - </ui-input> - </ui-horizon-group> - <ui-textarea :value="report.comment" readonly> - <span>{{ $t('details') }}</span> - </ui-textarea> - <ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button> - </div> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/abuse.vue'), - - data() { - return { - limit: 10, - untilId: undefined, - userReports: [], - existMore: false, - faExclamationCircle - }; - }, - - mounted() { - this.fetchUserReports(); - }, - - methods: { - fetchUserReports() { - this.$root.api('admin/abuse-user-reports', { - untilId: this.untilId, - limit: this.limit + 1 - }).then(reports => { - if (reports.length == this.limit + 1) { - reports.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.userReports = this.userReports.concat(reports); - this.untilId = this.userReports[this.userReports.length - 1].id; - }); - }, - - removeReport(report) { - this.$root.api('admin/remove-abuse-user-report', { - reportId: report.id - }).then(() => { - this.userReports = this.userReports.filter(r => r.id != report.id); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.haexwsjc - padding-bottom 16px - border-bottom solid 1px var(--faceDivider) - -</style> diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue deleted file mode 100644 index f6c0540b3756e72fa1d891643a93ed76ef4d6292..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/announcements.vue +++ /dev/null @@ -1,91 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faBroadcastTower"/> {{ $t('announcements') }}</template> - <section v-for="(announcement, i) in announcements" class="fit-top"> - <ui-input v-model="announcement.title" @change="save"> - <span>{{ $t('title') }}</span> - </ui-input> - <ui-textarea v-model="announcement.text"> - <span>{{ $t('text') }}</span> - </ui-textarea> - <ui-input v-model="announcement.image"> - <span>{{ $t('image-url') }}</span> - </ui-input> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button> - <ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button> - </ui-horizon-group> - </section> - <section> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/announcements.vue'), - data() { - return { - announcements: [], - faBroadcastTower, faPlus - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - }); - }, - - methods: { - add() { - this.announcements.unshift({ - title: '', - text: '', - image: null - }); - }, - - remove(i) { - this.$root.dialog({ - type: 'warning', - text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.announcements = this.announcements.filter((_, j) => j !== i); - this.save(true); - this.$root.dialog({ - type: 'success', - text: this.$t('_remove.removed') - }); - }); - }, - - save(silent) { - this.$root.api('admin/update-meta', { - announcements: this.announcements - }).then(() => { - if (!silent) { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - } - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - } -}); -</script> diff --git a/src/client/app/admin/views/dashboard.ap-log.vue b/src/client/app/admin/views/dashboard.ap-log.vue deleted file mode 100644 index ee48ef15ea7c00bfdabcbdce794bf4fa83f6f5a2..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/dashboard.ap-log.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="hyhctythnmwihguaaapnbrbszsjqxpio"> - <table> - <thead> - <tr> - <th><fa :icon="faExchangeAlt"/> In/Out</th> - <th><fa :icon="faBolt"/> Activity</th> - <th><fa icon="server"/> Host</th> - <th><fa icon="user"/> Actor</th> - </tr> - </thead> - <tbody> - <tr v-for="log in logs" :key="log.id"> - <td :class="log.direction">{{ log.direction == 'in' ? '<' : '>' }} {{ log.direction }}</td> - <td>{{ log.activity }}</td> - <td>{{ log.host }}</td> - <td>@{{ log.actor }}</td> - </tr> - </tbody> - </table> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faExchangeAlt } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - data() { - return { - logs: [], - connection: null, - faBolt, faExchangeAlt - }; - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('apLog'); - this.connection.on('log', this.onLog); - this.connection.on('logs', this.onLogs); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 50 - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onLog(log) { - log.id = Math.random(); - this.logs.unshift(log); - if (this.logs.length > 50) this.logs.pop(); - }, - - onLogs(logs) { - for (const log of logs.reverse()) { - this.onLog(log) - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.hyhctythnmwihguaaapnbrbszsjqxpio - display block - padding 12px 16px 16px 16px - height 250px - overflow auto - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--adminDashboardCardBg) - border-radius 8px - - > table - width 100% - max-width 100% - overflow auto - border-spacing 0 - border-collapse collapse - color var(--adminDashboardCardFg) - font-size 14px - - thead - border-bottom solid 1px var(--adminDashboardCardDivider) - - tr - th - font-weight normal - text-align left - - tbody - tr - &:nth-child(odd) - background rgba(0, 0, 0, 0.025) - - th, td - padding 8px 16px - min-width 128px - - td.in - color #d26755 - - td.out - color #55bb83 - -</style> diff --git a/src/client/app/admin/views/dashboard.cpu-memory.vue b/src/client/app/admin/views/dashboard.cpu-memory.vue deleted file mode 100644 index a3951e76182d6974f7eeb6173aa38856030fb11f..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/dashboard.cpu-memory.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> -<div class="zyknedwtlthezamcjlolyusmipqmjgxz"> - <div> - <header> - <span><fa icon="microchip"/> CPU <span>{{ cpuP }}%</span></span> - <span v-if="meta">{{ meta.cpu.model }}</span> - </header> - <div ref="cpu"></div> - </div> - <div> - <header> - <span><fa icon="memory"/> MEM <span>{{ memP }}%</span></span> - <span v-if="meta"></span> - </header> - <div ref="mem"></div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - props: ['connection'], - - data() { - return { - stats: [], - cpuChart: null, - memChart: null, - cpuP: '', - memP: '', - meta: null - }; - }, - - watch: { - stats(stats) { - this.cpuChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: x.cpu_usage })) - }]); - this.memChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: (x.mem.used / x.mem.total) })) - }]); - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); - - const chartOpts = { - chart: { - type: 'area', - height: 200, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)' - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - enabled: false - }, - series: [{ - data: [] - }], - xaxis: { - type: 'numeric', - labels: { - show: false - }, - tooltip: { - enabled: false - } - }, - yaxis: { - show: false, - min: 0, - max: 1 - } - }; - - this.cpuChart = new ApexCharts(this.$refs.cpu, chartOpts); - this.memChart = new ApexCharts(this.$refs.mem, chartOpts); - - this.cpuChart.render(); - this.memChart.render(); - }, - - beforeDestroy() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - - this.cpuChart.destroy(); - this.memChart.destroy(); - }, - - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 200) this.stats.shift(); - - this.cpuP = (stats.cpu_usage * 100).toFixed(0); - this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.zyknedwtlthezamcjlolyusmipqmjgxz - display flex - - > div - display block - flex 1 - padding 20px 12px 0 12px - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--face) - border-radius 8px - - &:first-child - margin-right 16px - - > header - display flex - padding 0 8px - margin-bottom -16px - color var(--adminDashboardCardFg) - font-size 14px - - > span - &:last-child - margin-left auto - opacity 0.7 - - > span - opacity 0.7 - - > div - margin-bottom -10px - - @media (max-width 1000px) - display block - margin-bottom 26px - - > div - &:first-child - margin-right 0 - margin-bottom 26px - -</style> diff --git a/src/client/app/admin/views/dashboard.queue-charts.vue b/src/client/app/admin/views/dashboard.queue-charts.vue deleted file mode 100644 index d2d7811bffcd12adf18d44078ebbb49456b046c9..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/dashboard.queue-charts.vue +++ /dev/null @@ -1,196 +0,0 @@ -<template> -<div class="mzxlfysy"> - <div> - <header> - <span><fa :icon="faInbox"/> In</span> - <span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span> - </header> - <div ref="in"></div> - </div> - <div> - <header> - <span><fa :icon="faPaperPlane"/> Out</span> - <span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span> - </header> - <div ref="out"></div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faInbox } from '@fortawesome/free-solid-svg-icons'; -import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import ApexCharts from 'apexcharts'; - -const limit = 150; - -export default Vue.extend({ - data() { - return { - stats: [], - inChart: null, - outChart: null, - faInbox, faPaperPlane - }; - }, - - computed: { - latestStats(): any { - return this.stats[this.stats.length - 1]; - } - }, - - watch: { - stats(stats) { - this.inChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.inbox.active })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) - }]); - this.outChart.updateSeries([{ - data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.deliver.active })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting })) - }, { - data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) - }]); - } - }, - - mounted() { - const chartOpts = { - chart: { - type: 'area', - height: 200, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)' - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - enabled: false - }, - legend: { - show: false - }, - colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], - series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any, - xaxis: { - type: 'numeric', - labels: { - show: false - }, - tooltip: { - enabled: false - } - }, - yaxis: { - show: false, - min: 0, - } - }; - - this.inChart = new ApexCharts(this.$refs.in, chartOpts); - this.outChart = new ApexCharts(this.$refs.out, chartOpts); - - this.inChart.render(); - this.outChart.render(); - - const connection = this.$root.stream.useSharedConnection('queueStats'); - connection.on('stats', this.onStats); - connection.on('statsLog', this.onStatsLog); - connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: limit - }); - - this.$once('hook:beforeDestroy', () => { - connection.dispose(); - this.inChart.destroy(); - this.outChart.destroy(); - }); - }, - - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > limit) this.stats.shift(); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mzxlfysy - display flex - - > div - display block - flex 1 - padding 20px 12px 0 12px - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--face) - border-radius 8px - - &:first-child - margin-right 16px - - > header - display flex - padding 0 8px - margin-bottom -16px - color var(--adminDashboardCardFg) - font-size 14px - - > span - &:last-child - margin-left auto - opacity 0.7 - - > span - opacity 0.7 - - > div - margin-bottom -10px - - @media (max-width 1000px) - display block - margin-bottom 26px - - > div - &:first-child - margin-right 0 - margin-bottom 26px - -</style> diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue deleted file mode 100644 index 5ccfaa06ca593765149b80c71938487d79be6909..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/dashboard.vue +++ /dev/null @@ -1,286 +0,0 @@ -<template> -<div class="obdskegsannmntldydackcpzezagxqfy"> - <header v-if="meta"> - <p><b>Misskey</b><span>{{ meta.version }}</span></p> - <p><b>Machine</b><span>{{ meta.machine }}</span></p> - <p><b>OS</b><span>{{ meta.os }}</span></p> - <p><b>Node</b><span>{{ meta.node }}</span></p> - <p>{{ $t('@.ai-chan-kawaii') }}</p> - </header> - - <marquee-text v-if="instances.length > 0" class="instances" :repeat="10" :duration="60"> - <span v-for="instance in instances" class="instance"> - <b :style="{ background: instance.bg }">{{ instance.host }}</b>{{ instance.notesCount | number }} / {{ instance.usersCount | number }} - </span> - </marquee-text> - - <div v-if="stats" class="stats"> - <div> - <div> - <div><fa icon="user"/></div> - <div> - <span>{{ $t('accounts') }}</span> - <b>{{ stats.originalUsersCount | number }}</b> - </div> - </div> - <div> - <span><fa icon="home"/> {{ $t('this-instance') }}</span> - <span @click="setChartSrc('users')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - <div> - <div> - <div><fa icon="pencil-alt"/></div> - <div> - <span>{{ $t('notes') }}</span> - <b>{{ stats.originalNotesCount | number }}</b> - </div> - </div> - <div> - <span><fa icon="home"/> {{ $t('this-instance') }}</span> - <span @click="setChartSrc('notes')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - <div> - <div> - <div><fa :icon="faDatabase"/></div> - <div> - <span>{{ $t('drive') }}</span> - <b>{{ stats.driveUsageLocal | bytes }}</b> - </div> - </div> - <div> - <span><fa icon="home"/> {{ $t('this-instance') }}</span> - <span @click="setChartSrc('drive')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - <div> - <div> - <div><fa :icon="['far', 'hdd']"/></div> - <div> - <span>{{ $t('instances') }}</span> - <b>{{ stats.instances | number }}</b> - </div> - </div> - <div> - <span><fa icon="globe"/> {{ $t('federated') }}</span> - <span @click="setChartSrc('federation-instances-total')"><fa :icon="['far', 'chart-bar']"/></span> - </div> - </div> - </div> - - <div class="charts"> - <x-charts ref="charts"/> - </div> - - <div class="queue"> - <x-queue/> - </div> - - <div class="cpu-memory"> - <x-cpu-memory :connection="connection"/> - </div> - - <div class="ap"> - <x-ap-log/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import XCpuMemory from "./dashboard.cpu-memory.vue"; -import XQueue from "./dashboard.queue-charts.vue"; -import XCharts from "./dashboard.charts.vue"; -import XApLog from "./dashboard.ap-log.vue"; -import { faDatabase } from '@fortawesome/free-solid-svg-icons'; -import MarqueeText from 'vue-marquee-text-component'; -import randomColor from 'randomcolor'; - -export default Vue.extend({ - i18n: i18n('admin/views/dashboard.vue'), - - components: { - XCpuMemory, - XQueue, - XCharts, - XApLog, - MarqueeText - }, - - data() { - return { - stats: null, - connection: null, - meta: null, - instances: [], - faDatabase - }; - }, - - created() { - this.connection = this.$root.stream.useSharedConnection('serverStats'); - - this.updateStats(); - - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - - this.$root.api('federation/instances', { - sort: '+notes' - }).then(instances => { - for (const i of instances) { - i.bg = randomColor({ - seed: i.host, - luminosity: 'dark' - }); - } - this.instances = instances; - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - setChartSrc(src) { - this.$refs.charts.setSrc(src); - }, - - updateStats() { - this.$root.api('stats', {}, true).then(stats => { - this.stats = stats; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.obdskegsannmntldydackcpzezagxqfy - padding 16px - - @media (min-width 500px) - padding 16px - - > header - display flex - padding-bottom 16px - border-bottom solid 1px var(--adminDashboardHeaderBorder) - color var(--adminDashboardHeaderFg) - font-size 14px - white-space nowrap - - @media (max-width 1000px) - display none - - > p - display block - margin 0 32px 0 0 - overflow hidden - text-overflow ellipsis - - > b - &:after - content ':' - margin-right 8px - - &:last-child - margin-left auto - margin-right 0 - - > .instances - padding 16px - color var(--adminDashboardHeaderFg) - font-size 13px - - >>> .instance - margin 0 10px - - > b - padding 2px 6px - margin-right 4px - border-radius 4px - color #fff - - > .stats - display flex - justify-content space-between - margin-bottom 16px - - > div - flex 1 - margin-right 16px - color var(--adminDashboardCardFg) - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--adminDashboardCardBg) - border-radius 8px - - &:last-child - margin-right 0 - - > div:first-child - display flex - align-items center - text-align center - - &:last-child - margin-right 0 - - > div:first-child - padding 16px 24px - font-size 28px - - > div:last-child - flex 1 - padding 16px 32px 16px 0 - text-align right - - > span - font-size 70% - opacity 0.7 - - > b - display block - - > div:last-child - display flex - padding 6px 16px - border-top solid 1px var(--adminDashboardCardDivider) - - > span - font-size 70% - opacity 0.7 - - &:last-child - margin-left auto - cursor pointer - - @media (max-width 900px) - display grid - grid-template-columns 1fr 1fr - grid-template-rows 1fr 1fr - gap 16px - - > div - margin-right 0 - - @media (max-width 500px) - display block - - > div:not(:last-child) - margin-bottom 16px - - > .charts - margin-bottom 16px - - > .queue - margin-bottom 16px - - > .cpu-memory - margin-bottom 16px - -</style> diff --git a/src/client/app/admin/views/db.vue b/src/client/app/admin/views/db.vue deleted file mode 100644 index 9f87a749b6d22811619dbc01aea5caef430254ed..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/db.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faDatabase"/> {{ $t('tables') }}</template> - <section v-if="tables"> - <div v-for="table in Object.keys(tables)"><b>{{ table }}</b> {{ tables[table].count | number }} {{ tables[table].size | bytes }}</div> - </section> - <section> - <header><fa :icon="faBroom"/> {{ $t('vacuum') }}</header> - <ui-info>{{ $t('vacuum-info') }}</ui-info> - <ui-switch v-model="fullVacuum">FULL</ui-switch> - <ui-switch v-model="analyzeVacuum">ANALYZE</ui-switch> - <ui-button @click="vacuum()"><fa :icon="faBroom"/> {{ $t('vacuum') }}</ui-button> - <ui-info warn>{{ $t('vacuum-exclamation') }}</ui-info> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faDatabase, faBroom } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/db.vue'), - - data() { - return { - tables: null, - fullVacuum: true, - analyzeVacuum: true, - faDatabase, faBroom - }; - }, - - mounted() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('admin/get-table-stats').then(tables => { - this.tables = tables; - }); - }, - - vacuum() { - this.$root.api('admin/vacuum', { - full: this.fullVacuum, - analyze: this.analyzeVacuum, - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - } -}); -</script> diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue deleted file mode 100644 index 1152db2b919288ea9c1b2af40790b2b143b5258d..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/drive.vue +++ /dev/null @@ -1,292 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faTerminal"/> {{ $t('operation') }}</template> - <section class="fit-top"> - <ui-input v-model="target" type="text"> - <span>{{ $t('fileid-or-url') }}</span> - </ui-input> - <ui-horizon-group> - <ui-button @click="findAndToggleSensitive(true)"><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="findAndToggleSensitive(false)"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button> - </ui-horizon-group> - <ui-button @click="findAndDel()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - <ui-button @click="show()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - <ui-textarea v-if="file" :value="file | json5" readonly tall style="margin-top:16px;"></ui-textarea> - </section> - <section> - <ui-button @click="cleanUp()"><fa :icon="faTrashAlt"/> {{ $t('clean-up') }}</ui-button> - <ui-button @click="cleanRemoteFiles()"><fa :icon="faTrashAlt"/> {{ $t('clean-remote-files') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faCloud"/> {{ $t('@.drive') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="sort"> - <template #label>{{ $t('sort.title') }}</template> - <option value="-createdAt">{{ $t('sort.createdAtAsc') }}</option> - <option value="+createdAt">{{ $t('sort.createdAtDesc') }}</option> - <option value="-size">{{ $t('sort.sizeAsc') }}</option> - <option value="+size">{{ $t('sort.sizeDesc') }}</option> - </ui-select> - <ui-select v-model="origin"> - <template #label>{{ $t('origin.title') }}</template> - <option value="combined">{{ $t('origin.combined') }}</option> - <option value="local">{{ $t('origin.local') }}</option> - <option value="remote">{{ $t('origin.remote') }}</option> - </ui-select> - </ui-horizon-group> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="kidvdlkg" v-for="file in files"> - <div @click="file._open = !file._open"> - <div> - <x-file-thumbnail class="thumbnail" :file="file" fit="contain" @click="showFileMenu(file)"/> - </div> - <div> - <header> - <b>{{ file.name }}</b> - <span class="username">@{{ file.user | acct }}</span> - </header> - <div> - <div> - <span style="margin-right:16px;">{{ file.type }}</span> - <span>{{ file.size | bytes }}</span> - </div> - <div><mk-time :time="file.createdAt" mode="detail"/></div> - </div> - </div> - </div> - <div v-show="file._open"> - <ui-input readonly :value="file.url"></ui-input> - <ui-horizon-group> - <ui-button @click="toggleSensitive(file)" v-if="file.isSensitive"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button> - <ui-button @click="toggleSensitive(file)" v-else><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - </ui-horizon-group> - </div> - </div> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetch">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faCloud, faTerminal, faSearch } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import XFileThumbnail from '../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('admin/views/drive.vue'), - - components: { - XFileThumbnail - }, - - data() { - return { - file: null, - target: null, - sort: '+createdAt', - origin: 'combined', - limit: 10, - offset: 0, - files: [], - existMore: false, - faCloud, faTrashAlt, faEye, faEyeSlash, faTerminal, faSearch - }; - }, - - watch: { - sort() { - this.files = []; - this.offset = 0; - this.fetch(); - }, - - origin() { - this.files = []; - this.offset = 0; - this.fetch(); - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - async fetchFile() { - try { - return await this.$root.api('drive/files/show', this.target.startsWith('http') ? { url: this.target } : { fileId: this.target }); - } catch (e) { - if (e == 'file-not-found') { - this.$root.dialog({ - type: 'error', - text: this.$t('file-not-found') - }); - } else { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - } - } - }, - - fetch() { - this.$root.api('admin/drive/files', { - origin: this.origin, - sort: this.sort, - offset: this.offset, - limit: this.limit + 1 - }).then(files => { - if (files.length == this.limit + 1) { - files.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - for (const x of files) { - x._open = false; - } - this.files = this.files.concat(files); - this.offset += this.limit; - }); - }, - - async del(file: any) { - const process = async () => { - await this.$root.api('drive/files/delete', { fileId: file.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - toggleSensitive(file: any) { - this.$root.api('drive/files/update', { - fileId: file.id, - isSensitive: !file.isSensitive - }).then(() => { - file.isSensitive = !file.isSensitive; - }); - }, - - async show() { - const file = await this.fetchFile(); - this.$root.api('admin/drive/show-file', { fileId: file.id }).then(info => { - this.file = info; - }); - }, - - async findAndToggleSensitive(sensitive) { - const process = async () => { - const file = await this.fetchFile(); - await this.$root.api('drive/files/update', { - fileId: file.id, - isSensitive: sensitive - }); - this.$root.dialog({ - type: 'success', - text: sensitive ? this.$t('marked-as-sensitive') : this.$t('unmarked-as-sensitive') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - async findAndDel() { - const process = async () => { - const file = await this.fetchFile(); - await this.$root.api('drive/files/delete', { fileId: file.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - cleanRemoteFiles() { - this.$root.dialog({ - type: 'warning', - text: this.$t('clean-remote-files-are-you-sure'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('admin/drive/clean-remote-files'); - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - cleanUp() { - this.$root.api('admin/drive/cleanup'); - this.$root.dialog({ - type: 'success', - splash: true - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kidvdlkg - padding 16px 0 - border-top solid 1px var(--faceDivider) - - > div:first-child - display flex - cursor pointer - - > div:nth-child(1) - > .thumbnail - display flex - width 64px - height 64px - background-size cover - background-position center center - - > div:nth-child(2) - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - word-break break-word - - > .username - margin-left 8px - opacity 0.7 - -</style> diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue deleted file mode 100644 index 2925fcab57b17c35ab42fc6c6127bc49fc95240f..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/emoji.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa icon="plus"/> {{ $t('add-emoji.title') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-input v-model="name"> - <span>{{ $t('add-emoji.name') }}</span> - <template #desc>{{ $t('add-emoji.name-desc') }}</template> - </ui-input> - <ui-input v-model="category" :datalist="categoryList"> - <span>{{ $t('add-emoji.category') }}</span> - </ui-input> - <ui-input v-model="aliases"> - <span>{{ $t('add-emoji.aliases') }}</span> - <template #desc>{{ $t('add-emoji.aliases-desc') }}</template> - </ui-input> - </ui-horizon-group> - <ui-input v-model="url"> - <template #icon><fa icon="link"/></template> - <span>{{ $t('add-emoji.url') }}</span> - </ui-input> - <ui-info>{{ $t('add-emoji.info') }}</ui-info> - <ui-button @click="add">{{ $t('add-emoji.add') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faGrin"/> {{ $t('emojis.title') }}</template> - <section v-for="emoji in emojis" :key="emoji.name" class="oryfrbft"> - <div> - <img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/> - </div> - <div> - <ui-horizon-group> - <ui-input v-model="emoji.name"> - <span>{{ $t('add-emoji.name') }}</span> - </ui-input> - <ui-input v-model="emoji.category" :datalist="categoryList"> - <span>{{ $t('add-emoji.category') }}</span> - </ui-input> - <ui-input v-model="emoji.aliases"> - <span>{{ $t('add-emoji.aliases') }}</span> - </ui-input> - </ui-horizon-group> - <ui-input v-model="emoji.url"> - <template #icon><fa icon="link"/></template> - <span>{{ $t('add-emoji.url') }}</span> - </ui-input> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button> - <ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button> - </ui-horizon-group> - </div> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faGrin } from '@fortawesome/free-regular-svg-icons'; -import { unique } from '../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('admin/views/emoji.vue'), - data() { - return { - name: '', - category: '', - url: '', - aliases: '', - emojis: [], - faGrin - }; - }, - - mounted() { - this.fetchEmojis(); - }, - - computed: { - categoryList() { - return unique(this.emojis.map((x: any) => x.category || '').filter((x: string) => x !== '')); - } - }, - - methods: { - add() { - this.$root.api('admin/emoji/add', { - name: this.name, - category: this.category, - url: this.url, - aliases: this.aliases.split(' ').filter(x => x.length > 0) - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('add-emoji.added') - }); - this.fetchEmojis(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - fetchEmojis() { - this.$root.api('admin/emoji/list').then(emojis => { - for (const e of emojis) { - e.aliases = (e.aliases || []).join(' '); - } - this.emojis = emojis; - }); - }, - - updateEmoji(emoji) { - this.$root.api('admin/emoji/update', { - id: emoji.id, - name: emoji.name, - category: emoji.category, - url: emoji.url, - aliases: emoji.aliases.split(' ').filter(x => x.length > 0) - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('updated') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - removeEmoji(emoji) { - this.$root.dialog({ - type: 'warning', - text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('admin/emoji/remove', { - id: emoji.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('remove-emoji.removed') - }); - this.fetchEmojis(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.oryfrbft - @media (min-width 500px) - display flex - - > div:first-child - @media (max-width 500px) - padding-bottom 16px - - > img - vertical-align bottom - - > div:last-child - flex 1 - - @media (min-width 500px) - padding-left 16px - -</style> diff --git a/src/client/app/admin/views/federation.vue b/src/client/app/admin/views/federation.vue deleted file mode 100644 index b419cca1d7c149a7517873dead0c7893778ca803..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/federation.vue +++ /dev/null @@ -1,553 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faTerminal"/> {{ $t('instance') }}</template> - <section class="fit-top"> - <ui-input class="target" v-model="target" type="text" @enter="showInstance()"> - <span>{{ $t('host') }}</span> - <template #prefix><fa :icon="faServer"/></template> - </ui-input> - <ui-button @click="showInstance()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - - <div class="instance" v-if="instance"> - <ui-horizon-group inputs> - <ui-input :value="instance.host" type="text" readonly> - <span>{{ $t('host') }}</span> - <template #prefix><fa :icon="faServer"/></template> - </ui-input> - <ui-input :value="instance.caughtAt | date" type="text" readonly> - <span>{{ $t('caught-at') }}</span> - <template #prefix><fa :icon="faCrosshairs"/></template> - </ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input :value="instance.notesCount | number" type="text" readonly> - <span>{{ $t('notes') }}</span> - <template #prefix><fa :icon="faEnvelopeOpenText"/></template> - </ui-input> - <ui-input :value="instance.usersCount | number" type="text" readonly> - <span>{{ $t('users') }}</span> - <template #prefix><fa :icon="faUsers"/></template> - </ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input :value="instance.followingCount | number" type="text" readonly> - <span>{{ $t('following') }}</span> - <template #prefix><fa :icon="faCaretDown"/></template> - </ui-input> - <ui-input :value="instance.followersCount | number" type="text" readonly> - <span>{{ $t('followers') }}</span> - <template #prefix><fa :icon="faCaretUp"/></template> - </ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input :value="instance.latestRequestSentAt | date" type="text" readonly> - <span>{{ $t('latest-request-sent-at') }}</span> - <template #prefix><fa :icon="faPaperPlane"/></template> - </ui-input> - <ui-input :value="instance.latestStatus" type="text" readonly> - <span>{{ $t('status') }}</span> - <template #prefix><fa :icon="faTrafficLight"/></template> - </ui-input> - </ui-horizon-group> - <ui-input :value="instance.latestRequestReceivedAt | date" type="text" readonly> - <span>{{ $t('latest-request-received-at') }}</span> - <template #prefix><fa :icon="faInbox"/></template> - </ui-input> - <ui-switch v-model="instance.isMarkedAsClosed" @change="updateInstance()">{{ $t('marked-as-closed') }}</ui-switch> - <details> - <summary>{{ $t('charts') }}</summary> - <ui-horizon-group inputs> - <ui-select v-model="chartSrc"> - <option value="requests">{{ $t('chart-srcs.requests') }}</option> - <option value="users">{{ $t('chart-srcs.users') }}</option> - <option value="users-total">{{ $t('chart-srcs.users-total') }}</option> - <option value="notes">{{ $t('chart-srcs.notes') }}</option> - <option value="notes-total">{{ $t('chart-srcs.notes-total') }}</option> - <option value="ff">{{ $t('chart-srcs.ff') }}</option> - <option value="ff-total">{{ $t('chart-srcs.ff-total') }}</option> - <option value="drive-usage">{{ $t('chart-srcs.drive-usage') }}</option> - <option value="drive-usage-total">{{ $t('chart-srcs.drive-usage-total') }}</option> - <option value="drive-files">{{ $t('chart-srcs.drive-files') }}</option> - <option value="drive-files-total">{{ $t('chart-srcs.drive-files-total') }}</option> - </ui-select> - <ui-select v-model="chartSpan"> - <option value="hour">{{ $t('chart-spans.hour') }}</option> - <option value="day">{{ $t('chart-spans.day') }}</option> - </ui-select> - </ui-horizon-group> - <div ref="chart"></div> - </details> - <details> - <summary>{{ $t('delete-all-files') }}</summary> - <ui-button @click="deleteAllFiles()" style="margin-top: 16px;"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button> - </details> - <details> - <summary>{{ $t('remove-all-following') }}</summary> - <ui-button @click="removeAllFollowing()" style="margin-top: 16px;"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button> - <ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info> - </details> - </div> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faServer"/> {{ $t('instances') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="sort"> - <template #label>{{ $t('sort') }}</template> - <option value="-caughtAt">{{ $t('sorts.caughtAtAsc') }}</option> - <option value="+caughtAt">{{ $t('sorts.caughtAtDesc') }}</option> - <option value="-lastCommunicatedAt">{{ $t('sorts.lastCommunicatedAtAsc') }}</option> - <option value="+lastCommunicatedAt">{{ $t('sorts.lastCommunicatedAtDesc') }}</option> - <option value="-notes">{{ $t('sorts.notesAsc') }}</option> - <option value="+notes">{{ $t('sorts.notesDesc') }}</option> - <option value="-users">{{ $t('sorts.usersAsc') }}</option> - <option value="+users">{{ $t('sorts.usersDesc') }}</option> - <option value="-following">{{ $t('sorts.followingAsc') }}</option> - <option value="+following">{{ $t('sorts.followingDesc') }}</option> - <option value="-followers">{{ $t('sorts.followersAsc') }}</option> - <option value="+followers">{{ $t('sorts.followersDesc') }}</option> - <option value="-driveUsage">{{ $t('sorts.driveUsageAsc') }}</option> - <option value="+driveUsage">{{ $t('sorts.driveUsageDesc') }}</option> - <option value="-driveFiles">{{ $t('sorts.driveFilesAsc') }}</option> - <option value="+driveFiles">{{ $t('sorts.driveFilesDesc') }}</option> - </ui-select> - <ui-select v-model="state"> - <template #label>{{ $t('state') }}</template> - <option value="all">{{ $t('states.all') }}</option> - <option value="blocked">{{ $t('states.blocked') }}</option> - <option value="notResponding">{{ $t('states.not-responding') }}</option> - <option value="markedAsClosed">{{ $t('states.marked-as-closed') }}</option> - </ui-select> - </ui-horizon-group> - - <div class="instances"> - <header> - <span>{{ $t('host') }}</span> - <span>{{ $t('notes') }}</span> - <span>{{ $t('users') }}</span> - <span>{{ $t('following') }}</span> - <span>{{ $t('followers') }}</span> - <span>{{ $t('status') }}</span> - </header> - <div v-for="instance in instances" :style="{ opacity: instance.isNotResponding ? 0.5 : 1 }"> - <a @click.prevent="showInstance(instance.host)" rel="nofollow noopener" target="_blank" :href="`https://${instance.host}`" :style="{ textDecoration: instance.isMarkedAsClosed ? 'line-through' : 'none' }">{{ instance.host }}</a> - <span>{{ instance.notesCount | number }}</span> - <span>{{ instance.usersCount | number }}</span> - <span>{{ instance.followingCount | number }}</span> - <span>{{ instance.followersCount | number }}</span> - <span>{{ instance.latestStatus }}</span> - </div> - </div> - - <ui-info v-if="instances.length == limit">{{ $t('result-is-truncated', { n: limit }) }}</ui-info> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faBan"/> {{ $t('blocked-hosts') }}</template> - <section class="fit-top"> - <ui-textarea v-model="blockedHosts"> - <template #desc>{{ $t('blocked-hosts-info') }}</template> - </ui-textarea> - <ui-button @click="saveBlockedHosts">{{ $t('save') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import { faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons'; -import ApexCharts from 'apexcharts'; -import * as tinycolor from 'tinycolor2'; - -const chartLimit = 90; -const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); -const negate = arr => arr.map(x => -x); - -export default Vue.extend({ - i18n: i18n('admin/views/federation.vue'), - - filters: { - date: v => v ? new Date(v).toLocaleString() : 'N/A' - }, - - data() { - return { - instance: null, - target: null, - sort: '+lastCommunicatedAt', - state: 'all', - limit: 100, - instances: [], - chart: null, - chartSrc: 'requests', - chartSpan: 'hour', - chartInstance: null, - blockedHosts: '', - faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox - }; - }, - - computed: { - data(): any { - if (this.chart == null) return null; - switch (this.chartSrc) { - case 'requests': return this.requestsChart(); - case 'users': return this.usersChart(false); - case 'users-total': return this.usersChart(true); - case 'notes': return this.notesChart(false); - case 'notes-total': return this.notesChart(true); - case 'ff': return this.ffChart(false); - case 'ff-total': return this.ffChart(true); - case 'drive-usage': return this.driveUsageChart(false); - case 'drive-usage-total': return this.driveUsageChart(true); - case 'drive-files': return this.driveFilesChart(false); - case 'drive-files-total': return this.driveFilesChart(true); - } - }, - - stats(): any[] { - const stats = - this.chartSpan == 'day' ? this.chart.perDay : - this.chartSpan == 'hour' ? this.chart.perHour : - null; - - return stats; - } - }, - - watch: { - sort() { - this.fetchInstances(); - }, - - state() { - this.fetchInstances(); - }, - - async instance() { - this.now = new Date(); - - const [perHour, perDay] = await Promise.all([ - this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), - this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), - ]); - - const chart = { - perHour: perHour, - perDay: perDay - }; - - this.chart = chart; - - this.renderChart(); - }, - - chartSrc() { - this.renderChart(); - }, - - chartSpan() { - this.renderChart(); - } - }, - - mounted() { - this.fetchInstances(); - - this.$root.getMeta().then(meta => { - this.blockedHosts = meta.blockedHosts.join('\n'); - }); - }, - - beforeDestroy() { - this.chartInstance.destroy(); - }, - - methods: { - showInstance(target?: string) { - this.$root.api('federation/show-instance', { - host: target || this.target - }).then(instance => { - if (instance == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('instance-not-registered') - }); - } else { - this.instance = instance; - this.target = ''; - } - }); - }, - - fetchInstances() { - this.instances = []; - this.$root.api('federation/instances', { - blocked: this.state === 'blocked' ? true : null, - notResponding: this.state === 'notResponding' ? true : null, - markedAsClosed: this.state === 'markedAsClosed' ? true : null, - sort: this.sort, - limit: this.limit - }).then(instances => { - this.instances = instances; - }); - }, - - removeAllFollowing() { - this.$root.api('admin/federation/remove-all-following', { - host: this.instance.host - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - deleteAllFiles() { - this.$root.api('admin/federation/delete-all-files', { - host: this.instance.host - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - updateInstance() { - this.$root.api('admin/federation/update-instance', { - host: this.instance.host, - isBlocked: this.instance.isBlocked || false, - isClosed: this.instance.isMarkedAsClosed || false - }); - }, - - setSrc(src) { - this.chartSrc = src; - }, - - renderChart() { - if (this.chartInstance) { - this.chartInstance.destroy(); - } - - this.chartInstance = new ApexCharts(this.$refs.chart, { - chart: { - type: 'area', - height: 300, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)' - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - theme: this.$store.state.device.darkmode ? 'dark' : 'light' - }, - legend: { - labels: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - }, - }, - xaxis: { - type: 'datetime', - labels: { - style: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - }, - axisBorder: { - color: 'rgba(0, 0, 0, 0.1)' - }, - axisTicks: { - color: 'rgba(0, 0, 0, 0.1)' - }, - }, - yaxis: { - labels: { - formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v), - style: { - color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - } - }, - series: this.data.series - }); - - this.chartInstance.render(); - }, - - getDate(i: number) { - const y = this.now.getFullYear(); - const m = this.now.getMonth(); - const d = this.now.getDate(); - const h = this.now.getHours(); - - return ( - this.chartSpan == 'day' ? new Date(y, m, d - i) : - this.chartSpan == 'hour' ? new Date(y, m, d, h - i) : - null - ); - }, - - format(arr) { - return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v })); - }, - - requestsChart(): any { - return { - series: [{ - name: 'Incoming', - data: this.format(this.stats.requests.received) - }, { - name: 'Outgoing (succeeded)', - data: this.format(this.stats.requests.succeeded) - }, { - name: 'Outgoing (failed)', - data: this.format(this.stats.requests.failed) - }] - }; - }, - - usersChart(total: boolean): any { - return { - series: [{ - name: 'Users', - type: 'area', - data: this.format(total - ? this.stats.users.total - : sum(this.stats.users.inc, negate(this.stats.users.dec)) - ) - }] - }; - }, - - notesChart(total: boolean): any { - return { - series: [{ - name: 'Notes', - type: 'area', - data: this.format(total - ? this.stats.notes.total - : sum(this.stats.notes.inc, negate(this.stats.notes.dec)) - ) - }] - }; - }, - - ffChart(total: boolean): any { - return { - series: [{ - name: 'Following', - type: 'area', - data: this.format(total - ? this.stats.following.total - : sum(this.stats.following.inc, negate(this.stats.following.dec)) - ) - }, { - name: 'Followers', - type: 'area', - data: this.format(total - ? this.stats.followers.total - : sum(this.stats.followers.inc, negate(this.stats.followers.dec)) - ) - }] - }; - }, - - driveUsageChart(total: boolean): any { - return { - bytes: true, - series: [{ - name: 'Drive usage', - type: 'area', - data: this.format(total - ? this.stats.drive.totalUsage - : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) - ) - }] - }; - }, - - driveFilesChart(total: boolean): any { - return { - series: [{ - name: 'Drive files', - type: 'area', - data: this.format(total - ? this.stats.drive.totalFiles - : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) - ) - }] - }; - }, - - saveBlockedHosts() { - this.$root.api('admin/update-meta', { - blockedHosts: this.blockedHosts ? this.blockedHosts.split('\n') : [] - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.target - margin-bottom 16px !important - -.instances - width 100% - - > header - display flex - - > * - color var(--text) - font-weight bold - - > div - display flex - - > * > * - flex 1 - overflow auto - - &:first-child - min-width 200px - -</style> diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue deleted file mode 100644 index 1b81185749657e507d5d0b44863dd96296a3ba2e..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/index.vue +++ /dev/null @@ -1,297 +0,0 @@ -<template> -<div class="mk-admin" :class="{ isMobile }"> - <header v-show="isMobile"> - <button class="nav" @click="navOpend = true"><fa icon="bars"/></button> - <span>MisskeyMyAdmin</span> - </header> - <div class="nav-backdrop" - v-if="navOpend && isMobile" - @click="navOpend = false" - @touchstart="navOpend = false" - ></div> - <nav v-show="navOpend"> - <div class="mi"> - <img svg-inline src="../assets/header-icon.svg"/> - </div> - <div class="me"> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <p class="name"><mk-user-name :user="$store.state.i"/></p> - </div> - <ul> - <li><router-link to="/dashboard" active-class="active"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</router-link></li> - <li><router-link to="/instance" active-class="active"><fa icon="cog" fixed-width/>{{ $t('instance') }}</router-link></li> - <li><router-link to="/queue" active-class="active"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</router-link></li> - <li><router-link to="/logs" active-class="active"><fa :icon="faStream" fixed-width/>{{ $t('logs') }}</router-link></li> - <li><router-link to="/db" active-class="active"><fa :icon="faDatabase" fixed-width/>{{ $t('db') }}</router-link></li> - <li><router-link to="/moderators" active-class="active"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</router-link></li> - <li><router-link to="/users" active-class="active"><fa icon="users" fixed-width/>{{ $t('users') }}</router-link></li> - <li><router-link to="/drive" active-class="active"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</router-link></li> - <li><router-link to="/federation" active-class="active"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</router-link></li> - <li><router-link to="/emoji" active-class="active"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</router-link></li> - <li><router-link to="/announcements" active-class="active"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</router-link></li> - <li><router-link to="/abuse" active-class="active"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</router-link></li> - </ul> - <div class="back-to-misskey"> - <a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a> - </div> - <div class="version"> - <small>Misskey {{ version }}</small> - </div> - </nav> - <main> - <div class="page"> - <div v-if="page == 'dashboard'"><x-dashboard/></div> - <div v-if="page == 'instance'"><x-instance/></div> - <div v-if="page == 'queue'"><x-queue/></div> - <div v-if="page == 'logs'"><x-logs/></div> - <div v-if="page == 'db'"><x-db/></div> - <div v-if="page == 'moderators'"><x-moderators/></div> - <div v-if="page == 'users'"><x-users/></div> - <div v-if="page == 'emoji'"><x-emoji/></div> - <div v-if="page == 'announcements'"><x-announcements/></div> - <div v-if="page == 'drive'"><x-drive/></div> - <div v-if="page == 'federation'"><x-federation/></div> - <div v-if="page == 'abuse'"><x-abuse/></div> - </div> - </main> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { version } from '../../config'; -import XDashboard from './dashboard.vue'; -import XInstance from './instance.vue'; -import XQueue from './queue.vue'; -import XLogs from './logs.vue'; -import XDb from './db.vue'; -import XModerators from './moderators.vue'; -import XEmoji from './emoji.vue'; -import XAnnouncements from './announcements.vue'; -import XUsers from './users.vue'; -import XDrive from './drive.vue'; -import XAbuse from './abuse.vue'; -import XFederation from './federation.vue'; - -import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks, faStream, faDatabase } from '@fortawesome/free-solid-svg-icons'; -import { faGrin } from '@fortawesome/free-regular-svg-icons'; - -// Detect the user agent -const ua = navigator.userAgent.toLowerCase(); -const isMobile = /mobile|iphone|ipad|android/.test(ua); - -export default Vue.extend({ - i18n: i18n('admin/views/index.vue'), - components: { - XDashboard, - XInstance, - XQueue, - XLogs, - XDb, - XModerators, - XEmoji, - XAnnouncements, - XUsers, - XDrive, - XAbuse, - XFederation, - }, - provide: { - isMobile - }, - data() { - return { - version, - isMobile, - navOpend: !isMobile, - faGrin, - faArrowLeft, - faHeadset, - faGlobe, - faExclamationCircle, - faTasks, - faStream, - faDatabase, - }; - }, - computed: { - page() { - return this.$route.params.page; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-admin - $headerHeight = 48px - - display flex - height 100% - - > header - position fixed - top 0 - z-index 10000 - width 100% - color var(--mobileHeaderFg) - background-color var(--mobileHeaderBg) - box-shadow 0 1px 0 rgba(#000, 0.075) - - &, * - user-select none - - > span - display block - line-height $headerHeight - text-align center - - > .nav - display block - position absolute - top 0 - left 0 - z-index 10001 - padding 0 - width $headerHeight - font-size 1.4em - line-height $headerHeight - border-right solid 1px rgba(#000, 0.1) - - > [data-icon] - transition all 0.2s ease - - > nav - position fixed - z-index 20001 - top 0 - left 0 - width 250px - height 100vh - overflow auto - background #333 - color #fff - - > .mi - text-align center - - > svg - width 24px - height 82px - vertical-align top - fill #fff - opacity 0.7 - - > .me - display flex - margin 0 16px 16px 16px - padding 16px 0 - align-items center - border-top solid 1px #555 - border-bottom solid 1px #555 - - > .avatar - height 48px - border-radius 100% - vertical-align middle - - > .name - margin 0 16px - padding 0 - color #fff - overflow hidden - text-overflow ellipsis - white-space nowrap - font-size 15px - - > .back-to-misskey - margin 16px 16px 0 16px - padding 0 - border-top solid 1px #555 - - > a - display block - padding 16px 4px - color inherit - text-decoration none - color #eee - font-size 15px - - &:hover - color #fff - - > [data-icon] - margin-right 6px - - > .version - margin 0 16px 16px 16px - padding-top 16px - border-top solid 1px #555 - text-align center - - > small - opacity 0.7 - - > ul - margin 0 - padding 0 - list-style none - font-size 15px - - > li > a - display block - padding 10px 16px - margin 0 - user-select none - color #eee - transition margin-left 0.2s ease - - &:hover - color #fff - - > [data-icon] - margin-right 6px - - &.active - margin-left 8px - color var(--primary) !important - - &:after - content "" - display block - position absolute - top 0 - right 0 - bottom 0 - margin auto 0 - height 0 - border-top solid 16px transparent - border-right solid 16px var(--bg) - border-bottom solid 16px transparent - border-left solid 16px transparent - - > .nav-backdrop - position fixed - top 0 - left 0 - z-index 20000 - width 100% - height 100% - background var(--mobileNavBackdrop) - - > main - width 100% - padding 0 0 0 250px - - > .page - max-width 1150px - - @media (min-width 500px) - padding 16px - - &.isMobile - > main - padding $headerHeight 0 0 0 - -</style> diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue deleted file mode 100644 index ebc554f955c99b44f8f31997255c3e50cf989437..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/instance.vue +++ /dev/null @@ -1,523 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa icon="cog"/> {{ $t('instance') }}</template> - <section class="fit-top"> - <ui-input :value="host" readonly>{{ $t('host') }}</ui-input> - <ui-input v-model="name">{{ $t('instance-name') }}</ui-input> - <ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea> - <ui-input v-model="iconUrl"><template #icon><fa icon="link"/></template>{{ $t('icon-url') }}</ui-input> - <ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input> - <ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input> - <ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input> - <details> - <summary>{{ $t('advanced-config') }}</summary> - <ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input> - <ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input> - <ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input> - <ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input> - </details> - </section> - <section class="fit-bottom"> - <header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header> - <ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input> - <ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input> - </section> - <section> - <ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch> - <ui-button v-if="disableRegistration" @click="invite">{{ $t('invite') }}</ui-button> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faPencilAlt"/> {{ $t('note-and-tl') }}</template> - <section class="fit-top fit-bottom"> - <ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input> - </section> - <section> - <ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch> - <ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch> - <ui-info>{{ $t('disabling-timelines-info') }}</ui-info> - </section> - <section> - <ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch> - <ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa icon="cloud"/> {{ $t('drive-config') }}</template> - <section> - <ui-switch v-model="useObjectStorage">{{ $t('use-object-storage') }}</ui-switch> - <template v-if="useObjectStorage"> - <ui-info> - <i18n path="object-storage-s3-info"> - <a href="https://docs.aws.amazon.com/general/latest/gr/rande.html" target="_blank">{{ $t('object-storage-s3-info-here') }}</a> - </i18n> - </ui-info> - <ui-info>{{ $t('object-storage-gcs-info') }}</ui-info> - <ui-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('object-storage-base-url') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('object-storage-bucket') }}</ui-input> - <ui-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('object-storage-prefix') }}</ui-input> - </ui-horizon-group> - <ui-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('object-storage-endpoint') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('object-storage-region') }}</ui-input> - <ui-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">{{ $t('object-storage-port') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group inputs> - <ui-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-access-key') }}</ui-input> - <ui-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-secret-key') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('object-storage-use-ssl') }}</ui-switch> - </template> - </section> - <section> - <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch> - <ui-switch v-model="proxyRemoteFiles">{{ $t('proxy-remote-files') }}<template #desc>{{ $t('proxy-remote-files-desc') }}</template></ui-switch> - </section> - <section class="fit-top fit-bottom"> - <ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> - <ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faThumbtack"/> {{ $t('pinned-users') }}</template> - <section class="fit-top"> - <ui-textarea v-model="pinnedUsers"> - <template #desc>{{ $t('pinned-users-info') }}</template> - </ui-textarea> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</template> - <section> - <ui-info>{{ $t('proxy-account-info') }}</ui-info> - <ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input> - <ui-info warn>{{ $t('proxy-account-warn') }}</ui-info> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="farEnvelope"/> {{ $t('email-config') }}</template> - <section> - <ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch> - <template v-if="enableEmail"> - <ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input> - <ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch> - <ui-horizon-group inputs> - <ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input> - <ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch> - <ui-button @click="testEmail()">{{ $t('test-email') }}</ui-button> - </template> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</template> - <section> - <ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch> - <template v-if="enableServiceWorker"> - <ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info> - <ui-horizon-group inputs class="fit-bottom"> - <ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input> - <ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input> - </ui-horizon-group> - </template> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</template> - <section :class="enableRecaptcha ? 'fit-bottom' : ''"> - <ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch> - <template v-if="enableRecaptcha"> - <ui-info>{{ $t('recaptcha-info') }}</ui-info> - <ui-info warn>{{ $t('recaptcha-info2') }}</ui-info> - <ui-horizon-group inputs> - <ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input> - <ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input> - </ui-horizon-group> - </template> - </section> - <section v-if="enableRecaptcha && recaptchaSiteKey"> - <header>{{ $t('recaptcha-preview') }}</header> - <div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faShieldAlt"/> {{ $t('external-service-integration-config') }}</template> - <section> - <header><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</header> - <ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch> - <template v-if="enableTwitterIntegration"> - <ui-horizon-group> - <ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input> - <ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info> - </template> - </section> - <section> - <header><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</header> - <ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch> - <template v-if="enableGithubIntegration"> - <ui-horizon-group> - <ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input> - <ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info> - </template> - </section> - <section> - <header><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</header> - <ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch> - <template v-if="enableDiscordIntegration"> - <ui-horizon-group> - <ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input> - <ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info> - </template> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <details> - <summary style="color:var(--text);">{{ $t('advanced-config') }}</summary> - - <ui-card> - <template #title><fa :icon="faHashtag"/> {{ $t('hidden-tags') }}</template> - <section class="fit-top"> - <ui-textarea v-model="hiddenTags"> - <template #desc>{{ $t('hidden-tags-info') }}</template> - </ui-textarea> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title>summaly Proxy</template> - <section class="fit-top fit-bottom"> - <ui-input v-model="summalyProxy">URL</ui-input> - </section> - <section> - <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </section> - </ui-card> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { url, host } from '../../config'; -import { toUnicode } from 'punycode'; -import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons'; -import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/instance.vue'), - - data() { - return { - url, - host: toUnicode(host), - maintainerName: null, - maintainerEmail: null, - ToSUrl: null, - repositoryUrl: "https://github.com/syuilo/misskey", - feedbackUrl: null, - disableRegistration: false, - disableLocalTimeline: false, - disableGlobalTimeline: false, - enableEmojiReaction: true, - useStarForReactionFallback: false, - mascotImageUrl: null, - bannerUrl: null, - errorImageUrl: null, - iconUrl: null, - name: null, - description: null, - languages: null, - cacheRemoteFiles: false, - proxyRemoteFiles: false, - localDriveCapacityMb: null, - remoteDriveCapacityMb: null, - maxNoteTextLength: null, - enableRecaptcha: false, - recaptchaSiteKey: null, - recaptchaSecretKey: null, - enableTwitterIntegration: false, - twitterConsumerKey: null, - twitterConsumerSecret: null, - enableGithubIntegration: false, - githubClientId: null, - githubClientSecret: null, - enableDiscordIntegration: false, - discordClientId: null, - discordClientSecret: null, - proxyAccount: null, - summalyProxy: null, - enableEmail: false, - email: null, - smtpSecure: false, - smtpHost: null, - smtpPort: null, - smtpUser: null, - smtpPass: null, - smtpAuth: false, - enableServiceWorker: false, - swPublicKey: null, - swPrivateKey: null, - pinnedUsers: '', - hiddenTags: '', - useObjectStorage: false, - objectStorageBaseUrl: null, - objectStorageBucket: null, - objectStoragePrefix: null, - objectStorageEndpoint: null, - objectStorageRegion: null, - objectStoragePort: null, - objectStorageAccessKey: null, - objectStorageSecretKey: null, - objectStorageUseSSL: false, - faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag - }; - }, - - created() { - this.$root.getMeta(true).then(meta => { - this.maintainerName = meta.maintainerName; - this.maintainerEmail = meta.maintainerEmail; - this.ToSUrl = meta.ToSUrl; - this.repositoryUrl = meta.repositoryUrl; - this.feedbackUrl = meta.feedbackUrl; - this.disableRegistration = meta.disableRegistration; - this.disableLocalTimeline = meta.disableLocalTimeline; - this.disableGlobalTimeline = meta.disableGlobalTimeline; - this.enableEmojiReaction = meta.enableEmojiReaction; - this.useStarForReactionFallback = meta.useStarForReactionFallback; - this.mascotImageUrl = meta.mascotImageUrl; - this.bannerUrl = meta.bannerUrl; - this.errorImageUrl = meta.errorImageUrl; - this.iconUrl = meta.iconUrl; - this.name = meta.name; - this.description = meta.description; - this.languages = meta.langs.join(' '); - this.cacheRemoteFiles = meta.cacheRemoteFiles; - this.proxyRemoteFiles = meta.proxyRemoteFiles; - this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; - this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; - this.maxNoteTextLength = meta.maxNoteTextLength; - this.enableRecaptcha = meta.enableRecaptcha; - this.recaptchaSiteKey = meta.recaptchaSiteKey; - this.recaptchaSecretKey = meta.recaptchaSecretKey; - this.proxyAccount = meta.proxyAccount; - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.twitterConsumerKey = meta.twitterConsumerKey; - this.twitterConsumerSecret = meta.twitterConsumerSecret; - this.enableGithubIntegration = meta.enableGithubIntegration; - this.githubClientId = meta.githubClientId; - this.githubClientSecret = meta.githubClientSecret; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - this.discordClientId = meta.discordClientId; - this.discordClientSecret = meta.discordClientSecret; - this.summalyProxy = meta.summalyProxy; - this.enableEmail = meta.enableEmail; - this.email = meta.email; - this.smtpSecure = meta.smtpSecure; - this.smtpHost = meta.smtpHost; - this.smtpPort = meta.smtpPort; - this.smtpUser = meta.smtpUser; - this.smtpPass = meta.smtpPass; - this.smtpAuth = meta.smtpUser != null && meta.smtpUser !== ''; - this.enableServiceWorker = meta.enableServiceWorker; - this.swPublicKey = meta.swPublickey; - this.swPrivateKey = meta.swPrivateKey; - this.pinnedUsers = meta.pinnedUsers.join('\n'); - this.hiddenTags = meta.hiddenTags.join('\n'); - this.useObjectStorage = meta.useObjectStorage; - this.objectStorageBaseUrl = meta.objectStorageBaseUrl; - this.objectStorageBucket = meta.objectStorageBucket; - this.objectStoragePrefix = meta.objectStoragePrefix; - this.objectStorageEndpoint = meta.objectStorageEndpoint; - this.objectStorageRegion = meta.objectStorageRegion; - this.objectStoragePort = meta.objectStoragePort; - this.objectStorageAccessKey = meta.objectStorageAccessKey; - this.objectStorageSecretKey = meta.objectStorageSecretKey; - this.objectStorageUseSSL = meta.objectStorageUseSSL; - }); - }, - - mounted() { - const renderRecaptchaPreview = () => { - if (!(window as any).grecaptcha) return; - if (!this.$refs.recaptcha) return; - if (!this.recaptchaSiteKey) return; - (window as any).grecaptcha.render(this.$refs.recaptcha, { - sitekey: this.recaptchaSiteKey - }); - }; - - window.onRecaotchaLoad = () => { - renderRecaptchaPreview(); - }; - - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad'); - head.appendChild(script); - - this.$watch('enableRecaptcha', () => { - renderRecaptchaPreview(); - }); - - this.$watch('recaptchaSiteKey', () => { - renderRecaptchaPreview(); - }); - }, - - methods: { - invite() { - this.$root.api('admin/invite').then(x => { - this.$root.dialog({ - type: 'info', - text: x.code - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async testEmail() { - this.$root.api('admin/send-email', { - to: this.maintainerEmail, - subject: 'Test email', - text: 'Yo' - }).then(x => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - updateMeta() { - this.$root.api('admin/update-meta', { - maintainerName: this.maintainerName, - maintainerEmail: this.maintainerEmail, - ToSUrl: this.ToSUrl, - repositoryUrl: this.repositoryUrl, - feedbackUrl: this.feedbackUrl, - disableRegistration: this.disableRegistration, - disableLocalTimeline: this.disableLocalTimeline, - disableGlobalTimeline: this.disableGlobalTimeline, - enableEmojiReaction: this.enableEmojiReaction, - useStarForReactionFallback: this.useStarForReactionFallback, - mascotImageUrl: this.mascotImageUrl, - bannerUrl: this.bannerUrl, - errorImageUrl: this.errorImageUrl, - iconUrl: this.iconUrl, - name: this.name, - description: this.description, - langs: this.languages ? this.languages.split(' ') : [], - cacheRemoteFiles: this.cacheRemoteFiles, - proxyRemoteFiles: this.proxyRemoteFiles, - localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), - remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), - maxNoteTextLength: parseInt(this.maxNoteTextLength, 10), - enableRecaptcha: this.enableRecaptcha, - recaptchaSiteKey: this.recaptchaSiteKey, - recaptchaSecretKey: this.recaptchaSecretKey, - proxyAccount: this.proxyAccount, - enableTwitterIntegration: this.enableTwitterIntegration, - twitterConsumerKey: this.twitterConsumerKey, - twitterConsumerSecret: this.twitterConsumerSecret, - enableGithubIntegration: this.enableGithubIntegration, - githubClientId: this.githubClientId, - githubClientSecret: this.githubClientSecret, - enableDiscordIntegration: this.enableDiscordIntegration, - discordClientId: this.discordClientId, - discordClientSecret: this.discordClientSecret, - summalyProxy: this.summalyProxy, - enableEmail: this.enableEmail, - email: this.email, - smtpSecure: this.smtpSecure, - smtpHost: this.smtpHost, - smtpPort: parseInt(this.smtpPort, 10), - smtpUser: this.smtpAuth ? this.smtpUser : '', - smtpPass: this.smtpAuth ? this.smtpPass : '', - enableServiceWorker: this.enableServiceWorker, - swPublicKey: this.swPublicKey, - swPrivateKey: this.swPrivateKey, - pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [], - hiddenTags: this.hiddenTags ? this.hiddenTags.split('\n') : [], - useObjectStorage: this.useObjectStorage, - objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null, - objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null, - objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null, - objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null, - objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null, - objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null, - objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, - objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, - objectStorageUseSSL: this.objectStorageUseSSL, - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - } -}); -</script> diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue deleted file mode 100644 index cb54318187e0a53529b7d034345c040e6f3342e4..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/logs.vue +++ /dev/null @@ -1,119 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faStream"/> {{ $t('logs') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-input v-model="domain" :debounce="true"> - <span>{{ $t('domain') }}</span> - </ui-input> - <ui-select v-model="level"> - <template #label>{{ $t('level') }}</template> - <option value="all">{{ $t('levels.all') }}</option> - <option value="info">{{ $t('levels.info') }}</option> - <option value="success">{{ $t('levels.success') }}</option> - <option value="warning">{{ $t('levels.warning') }}</option> - <option value="error">{{ $t('levels.error') }}</option> - <option value="debug">{{ $t('levels.debug') }}</option> - </ui-select> - </ui-horizon-group> - - <div class="nqjzuvev"> - <code v-for="log in logs" :key="log.id" :class="log.level"> - <details> - <summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> - <vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> - </details> - </code> - </div> - - <ui-button @click="deleteAll()">{{ $t('delete-all') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faStream } from '@fortawesome/free-solid-svg-icons'; -import VueJsonPretty from 'vue-json-pretty'; - -export default Vue.extend({ - i18n: i18n('admin/views/logs.vue'), - - components: { - VueJsonPretty - }, - - data() { - return { - logs: [], - level: 'all', - domain: '', - faStream - }; - }, - - watch: { - level() { - this.logs = []; - this.fetch(); - }, - - domain() { - this.logs = []; - this.fetch(); - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('admin/logs', { - level: this.level === 'all' ? null : this.level, - domain: this.domain === '' ? null : this.domain, - limit: 100 - }).then(logs => { - this.logs = logs.reverse(); - }); - }, - - deleteAll() { - this.$root.api('admin/delete-logs').then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nqjzuvev - padding 8px - background #000 - color #fff - font-size 14px - - > code - display block - - &.error - color #f00 - - &.warning - color #ff0 - - &.success - color #0f0 - - &.debug - opacity 0.7 - -</style> diff --git a/src/client/app/admin/views/moderators.vue b/src/client/app/admin/views/moderators.vue deleted file mode 100644 index 8ceab02d97af51b120d93f202244f2e0fa19b50c..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/moderators.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa icon="plus"/> {{ $t('add-moderator.title') }}</template> - <section class="fit-top"> - <ui-input v-model="username" type="text"> - <template #prefix>@</template> - </ui-input> - <ui-horizon-group> - <ui-button @click="add" :disabled="changing">{{ $t('add-moderator.add') }}</ui-button> - <ui-button @click="remove" :disabled="changing">{{ $t('add-moderator.remove') }}</ui-button> - </ui-horizon-group> - </section> - </ui-card> - - <ui-card> - <template #title>{{ $t('logs.title') }}</template> - <section class="fit-top"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div v-for="log in logs" :key="log.id" class=""> - <ui-horizon-group inputs> - <ui-input :value="log.user | acct" type="text" readonly> - <span>{{ $t('logs.moderator') }}</span> - </ui-input> - <ui-input :value="log.type" type="text" readonly> - <span>{{ $t('logs.type') }}</span> - </ui-input> - <ui-input :value="log.createdAt | date" type="text" readonly> - <span>{{ $t('logs.at') }}</span> - </ui-input> - </ui-horizon-group> - <ui-textarea :value="JSON.stringify(log.info, null, 4)" readonly> - <span>{{ $t('logs.info') }}</span> - </ui-textarea> - </div> - </sequential-entrance> - <ui-button v-if="existMoreLogs" @click="fetchLogs">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import parseAcct from "../../../../misc/acct/parse"; - -export default Vue.extend({ - i18n: i18n('admin/views/moderators.vue'), - - data() { - return { - username: '', - changing: false, - logs: [], - untilLogId: null, - existMoreLogs: false - }; - }, - - created() { - this.fetchLogs(); - }, - - methods: { - async add() { - this.changing = true; - - const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.username)); - await this.$root.api('admin/moderators/add', { userId: user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('add-moderator.added') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.changing = false; - }, - - async remove() { - this.changing = true; - - const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.username)); - await this.$root.api('admin/moderators/remove', { userId: user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('add-moderator.removed') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.changing = false; - }, - - fetchLogs() { - this.$root.api('admin/show-moderation-logs', { - untilId: this.untilId, - limit: 10 + 1 - }).then(logs => { - if (logs.length == 10 + 1) { - logs.pop(); - this.existMoreLogs = true; - } else { - this.existMoreLogs = false; - } - this.logs = this.logs.concat(logs); - this.untilLogId = this.logs[this.logs.length - 1].id; - }); - }, - } -}); -</script> diff --git a/src/client/app/admin/views/queue.chart.vue b/src/client/app/admin/views/queue.chart.vue deleted file mode 100644 index ff29aa8392c90e341dbd0b357802589cf689f396..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/queue.chart.vue +++ /dev/null @@ -1,181 +0,0 @@ -<template> -<div> - <ui-info warn v-if="latestStats && latestStats.waiting > 0">The queue is jammed.</ui-info> - <ui-horizon-group inputs v-if="latestStats" class="fit-bottom"> - <ui-input :value="latestStats.activeSincePrevTick | number" type="text" readonly> - <span>Process</span> - <template #prefix><fa :icon="fasPlayCircle"/></template> - <template #suffix>jobs/tick</template> - </ui-input> - <ui-input :value="latestStats.active | number" type="text" readonly> - <span>Active</span> - <template #prefix><fa :icon="farPlayCircle"/></template> - <template #suffix>jobs</template> - </ui-input> - <ui-input :value="latestStats.waiting | number" type="text" readonly> - <span>Waiting</span> - <template #prefix><fa :icon="faStopCircle"/></template> - <template #suffix>jobs</template> - </ui-input> - <ui-input :value="latestStats.delayed | number" type="text" readonly> - <span>Delayed</span> - <template #prefix><fa :icon="faStopwatch"/></template> - <template #suffix>jobs</template> - </ui-input> - </ui-horizon-group> - <div ref="chart" class="wptihjuy"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import ApexCharts from 'apexcharts'; -import * as tinycolor from 'tinycolor2'; -import { faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons'; -import { faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/queue.vue'), - - props: { - type: { - type: String, - required: true - }, - connection: { - required: true - }, - limit: { - type: Number, - required: true - } - }, - - data() { - return { - stats: [], - chart: null, - faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle - }; - }, - - computed: { - latestStats(): any { - return this.stats.length > 0 ? this.stats[this.stats.length - 1][this.type] : null; - } - }, - - watch: { - stats(stats) { - this.chart.updateSeries([{ - name: 'Process', - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x[this.type].activeSincePrevTick })) - }, { - name: 'Active', - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x[this.type].active })) - }, { - name: 'Waiting', - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x[this.type].waiting })) - }, { - name: 'Delayed', - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x[this.type].delayed })) - }]); - }, - }, - - mounted() { - this.chart = new ApexCharts(this.$refs.chart, { - chart: { - id: this.type, - group: 'queue', - type: 'area', - height: 200, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)', - xaxis: { - lines: { - show: true, - } - }, - }, - stroke: { - curve: 'straight', - width: 2 - }, - tooltip: { - enabled: false - }, - legend: { - labels: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - }, - }, - series: [] as any, - colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], - xaxis: { - type: 'numeric', - labels: { - show: false - }, - tooltip: { - enabled: false - } - }, - yaxis: { - show: false, - min: 0, - } - }); - - this.chart.render(); - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - - this.$once('hook:beforeDestroy', () => { - if (this.chart) this.chart.destroy(); - }); - }, - - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > this.limit) this.stats.shift(); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wptihjuy - min-height 200px !important - margin -8px - -</style> diff --git a/src/client/app/admin/views/queue.vue b/src/client/app/admin/views/queue.vue deleted file mode 100644 index 9aa740c68c2b26449b8e546367dc745220d77291..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/queue.vue +++ /dev/null @@ -1,159 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template> - <section> - <header><fa :icon="faPaperPlane"/> {{ $t('domains.deliver') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="deliver"/> - </section> - <section> - <header><fa :icon="faInbox"/> {{ $t('domains.inbox') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="inbox"/> - </section> - <section> - <details> - <summary>{{ $t('other-queues') }}</summary> - <section> - <header><fa :icon="faDatabase"/> {{ $t('domains.db') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="db"/> - </section> - <ui-hr/> - <section> - <header><fa :icon="faCloud"/> {{ $t('domains.objectStorage') }}</header> - <x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="objectStorage"/> - </section> - </details> - </section> - <section> - <ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="domain"> - <template #label>{{ $t('queue') }}</template> - <option value="deliver">{{ $t('domains.deliver') }}</option> - <option value="inbox">{{ $t('domains.inbox') }}</option> - <option value="db">{{ $t('domains.db') }}</option> - <option value="objectStorage">{{ $t('domains.objectStorage') }}</option> - </ui-select> - <ui-select v-model="state"> - <template #label>{{ $t('state') }}</template> - <option value="active">{{ $t('states.active') }}</option> - <option value="waiting">{{ $t('states.waiting') }}</option> - <option value="delayed">{{ $t('states.delayed') }}</option> - </ui-select> - </ui-horizon-group> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="xvvuvgsv" v-for="job in jobs" :key="job.id"> - <b>{{ job.id }}</b> - <template v-if="domain === 'deliver'"> - <span>{{ job.data.to }}</span> - </template> - <template v-if="domain === 'inbox'"> - <span>{{ job.data.activity.id }}</span> - </template> - <span>{{ `(${job.attempts}/${job.maxAttempts}, ${Math.floor((jobsFetched - job.timestamp) / 1000 / 60)}min)` }}</span> - </div> - </sequential-entrance> - <ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faTasks, faInbox, faDatabase, faCloud } from '@fortawesome/free-solid-svg-icons'; -import { faPaperPlane, faChartBar } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../i18n'; -import XChart from './queue.chart.vue'; - -export default Vue.extend({ - i18n: i18n('admin/views/queue.vue'), - - components: { - XChart - }, - - data() { - return { - connection: null, - chartLimit: 200, - jobs: [], - jobsLimit: 50, - jobsFetched: Date.now(), - domain: 'deliver', - state: 'delayed', - faTasks, faPaperPlane, faInbox, faChartBar, faDatabase, faCloud - }; - }, - - watch: { - domain() { - this.jobs = []; - this.fetchJobs(); - }, - - state() { - this.jobs = []; - this.fetchJobs(); - }, - }, - - mounted() { - this.fetchJobs(); - - this.connection = this.$root.stream.useSharedConnection('queueStats'); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: this.chartLimit - }); - - this.$once('hook:beforeDestroy', () => { - this.connection.dispose(); - }); - }, - - methods: { - async removeAllJobs() { - const process = async () => { - await this.$root.api('admin/queue/clear'); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - fetchJobs() { - this.$root.api('admin/queue/jobs', { - domain: this.domain, - state: this.state, - limit: this.jobsLimit - }).then(jobs => { - this.jobsFetched = Date.now(), - this.jobs = jobs; - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.xvvuvgsv - margin-left -6px - > b, span - margin 0 6px - -</style> diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue deleted file mode 100644 index 9c3db2d6c2a8838b0423ad6b21bb278bcce312f9..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/users.user.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<div class="kofvwchc"> - <div> - <a :href="user | userPage(null, true)"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div @click="click(user.id)"> - <header> - <b><mk-user-name :user="user"/></b> - <span class="username">@{{ user | acct }}</span> - <span class="is-admin" v-if="user.isAdmin">admin</span> - <span class="is-moderator" v-if="user.isModerator">moderator</span> - <span class="is-silenced" v-if="user.isSilenced" :title="$t('@.silenced-user')"><fa :icon="faMicrophoneSlash"/></span> - <span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span> - </header> - <div> - <span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span> - </div> - <div> - <span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('admin/views/users.vue'), - props: ['user', 'click'], - data() { - return { - faSnowflake, faMicrophoneSlash - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.kofvwchc - display flex - padding 16px - border-top solid 1px var(--faceDivider) - - > div:first-child - > a - > .avatar - width 64px - height 64px - - > div:last-child - flex 1 - cursor pointer - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - > .username - margin-left 8px - opacity 0.7 - - > .is-admin - > .is-moderator - flex-shrink 0 - align-self center - margin 0 0 0 .5em - padding 1px 6px - font-size 80% - border-radius 3px - background var(--noteHeaderAdminBg) - color var(--noteHeaderAdminFg) - - > .is-silenced - > .is-suspended - margin 0 0 0 .5em - color #4dabf7 - - &:hover - color var(--primaryForeground) - background var(--primary) - text-decoration none - border-radius 3px - - &:active - color var(--primaryForeground) - background var(--primaryDarken10) - border-radius 3px -</style> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue deleted file mode 100644 index 920bfc381e9bc7d6621b07fa3fdf4fd70f97fd6a..0000000000000000000000000000000000000000 --- a/src/client/app/admin/views/users.vue +++ /dev/null @@ -1,366 +0,0 @@ -<template> -<div> - <ui-card> - <template #title><fa :icon="faTerminal"/> {{ $t('operation') }}</template> - <section class="fit-top"> - <ui-input class="target" v-model="target" type="text" @enter="showUser"> - <span>{{ $t('username-or-userid') }}</span> - </ui-input> - <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - - <div ref="user" class="user" v-if="user" :key="user.id"> - <x-user :user="user"/> - <div class="actions"> - <ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button> - <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> - <ui-horizon-group> - <ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button> - <ui-button @click="unsilenceUser">{{ $t('unmake-silence') }}</ui-button> - </ui-horizon-group> - <ui-horizon-group> - <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> - <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> - </ui-horizon-group> - <ui-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button> - <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> - </div> - </div> - </section> - </ui-card> - - <ui-card> - <template #title><fa :icon="faUsers"/> {{ $t('users.title') }}</template> - <section class="fit-top"> - <ui-horizon-group inputs> - <ui-select v-model="sort"> - <template #label>{{ $t('users.sort.title') }}</template> - <option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option> - <option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option> - <option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option> - <option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option> - </ui-select> - <ui-select v-model="state"> - <template #label>{{ $t('users.state.title') }}</template> - <option value="all">{{ $t('users.state.all') }}</option> - <option value="available">{{ $t('users.state.available') }}</option> - <option value="admin">{{ $t('users.state.admin') }}</option> - <option value="moderator">{{ $t('users.state.moderator') }}</option> - <option value="silenced">{{ $t('users.state.silenced') }}</option> - <option value="suspended">{{ $t('users.state.suspended') }}</option> - </ui-select> - <ui-select v-model="origin"> - <template #label>{{ $t('users.origin.title') }}</template> - <option value="combined">{{ $t('users.origin.combined') }}</option> - <option value="local">{{ $t('users.origin.local') }}</option> - <option value="remote">{{ $t('users.origin.remote') }}</option> - </ui-select> - </ui-horizon-group> - <ui-horizon-group searchboxes> - <ui-input v-model="searchUsername" type="text" spellcheck="false" @input="fetchUsers(true)"> - <span>{{ $t('username') }}</span> - </ui-input> - <ui-input v-model="searchHost" type="text" spellcheck="false" @input="fetchUsers(true)" :disabled="origin === 'local'"> - <span>{{ $t('host') }}</span> - </ui-input> - </ui-horizon-group> - <sequential-entrance animation="entranceFromTop" delay="25"> - <x-user v-for="user in users" :key="user.id" :user='user' :click="showUserOnClick"/> - </sequential-entrance> - <ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import parseAcct from "../../../../misc/acct/parse"; -import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import XUser from './users.user.vue'; - -export default Vue.extend({ - i18n: i18n('admin/views/users.vue'), - components: { - XUser - }, - data() { - return { - user: null, - target: null, - suspending: false, - unsuspending: false, - sort: '+createdAt', - state: 'all', - origin: 'local', - searchUsername: '', - searchHost: '', - limit: 10, - offset: 0, - users: [], - existMore: false, - faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt - }; - }, - - watch: { - sort() { - this.users = []; - this.offset = 0; - this.fetchUsers(); - }, - - state() { - this.users = []; - this.offset = 0; - this.fetchUsers(); - }, - - origin() { - if (this.origin === 'local') this.searchHost = ''; - this.users = []; - this.offset = 0; - this.fetchUsers(); - } - }, - - mounted() { - this.fetchUsers(); - }, - - methods: { - /** テã‚ストエリアã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’解決ã™ã‚‹ */ - fetchUser() { - return new Promise((res) => { - const usernamePromise = this.$root.api('users/show', parseAcct(this.target)); - const idPromise = this.$root.api('users/show', { userId: this.target }); - - let _notFound = false; - const notFound = () => { - if (_notFound) { - this.$root.dialog({ - type: 'error', - text: this.$t('user-not-found') - }); - } else { - _notFound = true; - } - }; - - usernamePromise.then(res).catch(e => { - if (e == 'user not found') { - notFound(); - } - }); - idPromise.then(res).catch(e => { - notFound(); - }); - }); - }, - - /** テã‚ストエリアã‹ã‚‰å‡¦ç†å¯¾è±¡ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¨å®šã™ã‚‹ */ - async showUser() { - this.user = null; - const user = await this.fetchUser(); - this.$root.api('admin/show-user', { userId: user.id }).then(info => { - this.user = info; - }); - this.target = ''; - }, - - async showUserOnClick(userId: string) { - this.$root.api('admin/show-user', { userId: userId }).then(info => { - this.user = info; - this.$nextTick(() => { - this.$refs.user.scrollIntoView(); - }); - }); - }, - - /** 処ç†å¯¾è±¡ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æƒ…å ±ã‚’æ›´æ–°ã™ã‚‹ */ - async refreshUser() { - this.$root.api('admin/show-user', { userId: this.user.id }).then(info => { - this.user = info; - }); - }, - - async resetPassword() { - if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; - - this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => { - this.$root.dialog({ - type: 'success', - text: this.$t('password-updated', { password: res.password }) - }); - }); - }, - - async silenceUser() { - if (!await this.getConfirmed(this.$t('silence-confirm'))) return; - - const process = async () => { - await this.$root.api('admin/silence-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.refreshUser(); - }, - - async unsilenceUser() { - if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return; - - const process = async () => { - await this.$root.api('admin/unsilence-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.refreshUser(); - }, - - async suspendUser() { - if (!await this.getConfirmed(this.$t('suspend-confirm'))) return; - - this.suspending = true; - - const process = async () => { - await this.$root.api('admin/suspend-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('suspended') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.suspending = false; - - this.refreshUser(); - }, - - async unsuspendUser() { - if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return; - - this.unsuspending = true; - - const process = async () => { - await this.$root.api('admin/unsuspend-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - text: this.$t('unsuspended') - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - - this.unsuspending = false; - - this.refreshUser(); - }, - - async updateRemoteUser() { - this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => { - this.$root.dialog({ - type: 'success', - text: this.$t('remote-user-updated') - }); - }); - - this.refreshUser(); - }, - - async deleteAllFiles() { - if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return; - - const process = async () => { - await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }; - - await process().catch(e => { - this.$root.dialog({ - type: 'error', - text: e.toString() - }); - }); - }, - - async getConfirmed(text: string): Promise<Boolean> { - const confirm = await this.$root.dialog({ - type: 'warning', - showCancelButton: true, - title: 'confirm', - text, - }); - - return !confirm.canceled; - }, - - fetchUsers(truncate?: boolean) { - if (truncate) this.offset = 0; - this.$root.api('admin/show-users', { - state: this.state, - origin: this.origin, - sort: this.sort, - offset: this.offset, - limit: this.limit + 1, - username: this.searchUsername, - hostname: this.searchHost - }).then(users => { - if (users.length == this.limit + 1) { - users.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.users = truncate ? users : this.users.concat(users); - this.offset += this.limit; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.target - margin-bottom 16px !important - -.user - margin-top 32px - - > .actions - margin-left 80px -</style> diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl deleted file mode 100644 index 6c4d5b8b6fa93873fbdff9e624f5a42399aed1b2..0000000000000000000000000000000000000000 --- a/src/client/app/animation.styl +++ /dev/null @@ -1,47 +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); -} - -.entranceFromTop { - animation-duration: 0.5s; - animation-name: entranceFromTop; -} - -@keyframes entranceFromTop { - from { - opacity: 0; - transform: translateY(-64px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes jump { - 0% { transform: translateY(0); } - 25% { transform: translateY(-16px); } - 50% { transform: translateY(0); } - 75% { transform: translateY(-8px); } - 100% { transform: translateY(0); } -} - -@keyframes blink { - 0% { opacity: 1; } - 30% { opacity: 1; } - 90% { opacity: 0; } -} diff --git a/src/client/app/app.styl b/src/client/app/app.styl deleted file mode 100644 index 6389aa0a875f6c8158cb0191fef555ca6ff22b5f..0000000000000000000000000000000000000000 --- a/src/client/app/app.styl +++ /dev/null @@ -1,84 +0,0 @@ -@import "../style" -@import "../animation" - -html - &.progress - &, * - cursor progress !important - -html - // iOSã®ãŸã‚ - overflow auto - -body - overflow-wrap break-word - -#nprogress - pointer-events none - - position absolute - z-index 65536 - - .bar - background var(--primary) - - position fixed - z-index 65537 - top 0 - left 0 - - width 100% - height 2px - - /* Fancy blur effect */ - .peg - display block - position absolute - right 0 - width 100px - height 100% - box-shadow 0 0 10px var(--primary), 0 0 5px var(--primary) - 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 var(--primary) - border-left-color var(--primary) - 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 - -pre - display block - - > code - display block - overflow auto - tab-size 2 - -[data-icon] - display inline-block diff --git a/src/client/app/app.vue b/src/client/app/app.vue deleted file mode 100644 index e639c9f9ac6b400baefd9ab10ab8b177ae9b8930..0000000000000000000000000000000000000000 --- a/src/client/app/app.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<router-view id="app" v-hotkey.global="keymap"></router-view> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { url, lang } from './config'; - -export default Vue.extend({ - computed: { - keymap(): any { - return { - 'h|slash': this.help, - 'd': this.dark - }; - } - }, - - methods: { - help() { - window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank'); - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - } - } -}); -</script> diff --git a/src/client/app/auth/assets/icon.svg b/src/client/app/auth/assets/icon.svg deleted file mode 100644 index 36f5d3e40478767d5e9d111a10411b28ccdd8baa..0000000000000000000000000000000000000000 Binary files a/src/client/app/auth/assets/icon.svg and /dev/null differ diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts deleted file mode 100644 index 91bb24b1085e4fcc9f1f75dd541d60e8d70b1727..0000000000000000000000000000000000000000 --- a/src/client/app/auth/script.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Authorize Form - */ - -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; -import Index from './views/index.vue'; -import NotFound from '../common/views/pages/not-found.vue'; - -/** - * init - */ -init(launch => { - // Init router - const router = new VueRouter({ - mode: 'history', - base: '/auth/', - routes: [ - { path: '/:token', component: Index }, - { path: '*', component: NotFound } - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/client/app/auth/style.styl b/src/client/app/auth/style.styl deleted file mode 100644 index bd25e1b572fcf36cb2208394c3c277b7a4f4fba0..0000000000000000000000000000000000000000 --- a/src/client/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/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue deleted file mode 100644 index 064dbf388705af251e42a69612fe166a082d2011..0000000000000000000000000000000000000000 --- a/src/client/app/auth/views/form.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<div class="form"> - <header> - <h1 v-html="$t('share-access', { name })"></h1> - <img :src="app.iconUrl"/> - </header> - <div class="app"> - <section> - <h2>{{ app.name }}</h2> - <p class="id">{{ app.id }}</p> - <p class="description">{{ app.description }}</p> - </section> - <section> - <h2>{{ $t('permission-ask') }}</h2> - <ul> - <template v-for="p in app.permission"> - <li :key="p">{{ $t(`@.permissions.${p}`) }}</li> - </template> - </ul> - </section> - </div> - <div class="action"> - <button @click="cancel">{{ $t('cancel') }}</button> - <button @click="accept">{{ $t('accept') }}</button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; - -export default Vue.extend({ - i18n: i18n('auth/views/form.vue'), - props: ['session'], - computed: { - name(): string { - const el = document.createElement('div'); - el.textContent = this.app.name - return el.innerHTML; - }, - app(): any { - return this.session.app; - } - }, - methods: { - cancel() { - this.$root.api('auth/deny', { - token: this.session.token - }).then(() => { - this.$emit('denied'); - }); - }, - - accept() { - this.$root.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(#000, 0.1) - - > .app - padding 44px 16px 0 16px - color #555 - background #eee - box-shadow 0 2px 2px rgba(#000, 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/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue deleted file mode 100644 index ad9b1e4e35b1da9154f9aaebc65e50945e4c0961..0000000000000000000000000000000000000000 --- a/src/client/app/auth/views/index.vue +++ /dev/null @@ -1,153 +0,0 @@ -<template> -<div class="index"> - <main v-if="$store.getters.isSignedIn"> - <p class="fetching" v-if="fetching">{{ $t('loading') }}<mk-ellipsis/></p> - <x-form - class="form" - ref="form" - v-if="state == 'waiting'" - :session="session" - @denied="state = 'denied'" - @accepted="accepted" - /> - <div class="denied" v-if="state == 'denied'"> - <h1>{{ $t('denied') }}</h1> - <p>{{ $t('denied-paragraph') }}</p> - </div> - <div class="accepted" v-if="state == 'accepted'"> - <h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1> - <p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p> - <p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p> - </div> - <div class="error" v-if="state == 'fetch-session-error'"> - <p>{{ $t('error') }}</p> - </div> - </main> - <main class="signin" v-if="!$store.getters.isSignedIn"> - <h1>{{ $t('sign-in') }}</h1> - <mk-signin/> - </main> - <footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import XForm from './form.vue'; - -export default Vue.extend({ - i18n: i18n('auth/views/index.vue'), - components: { - XForm - }, - data() { - return { - state: null, - session: null, - fetching: true - }; - }, - computed: { - token(): string { - return this.$route.params.token; - } - }, - mounted() { - if (!this.$store.getters.isSignedIn) return; - - // Fetch session - this.$root.api('auth/session/show', { - token: this.token - }).then(session => { - this.session = session; - this.fetching = false; - - // æ—¢ã«é€£æºã—ã¦ã„ãŸå ´åˆ - if (this.session.app.isAuthorized) { - this.$root.api('auth/accept', { - token: this.session.token - }).then(() => { - this.accepted(); - }); - } else { - this.state = 'waiting'; - } - }).catch(error => { - this.state = 'fetch-session-error'; - this.fetching = false; - }); - }, - 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 0 4px 16px rgba(#000, 0.2) - - > .fetching - margin 0 - padding 32px - color #555 - - > div:not(.form) - 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 32px - height 32px - margin 16px auto - -</style> diff --git a/src/client/app/boot.js b/src/client/app/boot.js deleted file mode 100644 index 64d46298830b1be30373d280fda2f871a3702564..0000000000000000000000000000000000000000 --- a/src/client/app/boot.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * MISSKEY BOOT LOADER - * (ENTRY POINT) - */ - -'use strict'; - -(async function() { - // ã‚ャッシュ削除è¦æ±‚ãŒã‚ã‚Œã°å¾“ㆠ- if (localStorage.getItem('shouldFlush') == 'true') { - refresh(); - return; - } - - const langs = LANGS; - - //#region Apply theme - const theme = localStorage.getItem('theme'); - if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - } - //#endregion - - //#region Load settings - let settings = null; - const vuex = localStorage.getItem('vuex'); - if (vuex) { - settings = JSON.parse(vuex); - } - //#endregion - - // Get the current url information - const url = new URL(location.href); - - //#region Detect app name - let app = null; - - if (`${url.pathname}/`.startsWith('/docs/')) app = 'docs'; - if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev'; - if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth'; - if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin'; - //#endregion - - // Script version - const ver = localStorage.getItem('v') || VERSION; - - //#region Detect the user language - let lang = null; - - if (langs.includes(navigator.language)) { - lang = navigator.language; - } else { - lang = langs.find(x => x.split('-')[0] == navigator.language); - - if (lang == null) { - // Fallback - lang = 'en-US'; - } - } - - if (settings && settings.device.lang && - langs.includes(settings.device.lang)) - { - lang = settings.device.lang; - } - - localStorage.setItem('lang', lang); - //#endregion - - //#region Fetch locale data - const cachedLocale = localStorage.getItem('locale'); - const localeKey = localStorage.getItem('localeKey'); - let localeData = null; - - if (cachedLocale == null || localeKey != `${ver}.${lang}`) { - const locale = await fetch(`/assets/locales/${lang}.json?ver=${ver}`) - .then(response => response.json()); - localeData = locale; - - localStorage.setItem('locale', JSON.stringify(locale)); - localStorage.setItem('localeKey', `${ver}.${lang}`); - } else { - localeData = JSON.parse(cachedLocale); - } - //#endregion - - // Detect the user agent - const ua = navigator.userAgent.toLowerCase(); - let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576; - if (settings && settings.device.appTypeForce) { - if (settings.device.appTypeForce === 'mobile') { - isMobile = true; - } else if (settings.device.appTypeForce === 'desktop') { - isMobile = false; - } - } - - // Get the <head> element - const head = document.getElementsByTagName('head')[0]; - - // If mobile, insert the viewport meta tag - if (isMobile) { - const viewport = document.getElementsByName("viewport").item(0); - viewport.content = `${viewport.content},minimum-scale=1,maximum-scale=1,user-scalable=no`; - head.appendChild(viewport); - } - - // Switch desktop or mobile version - if (app == null) { - app = isMobile ? 'mobile' : 'desktop'; - } - - // 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.src = `/assets/${app}.${ver}.js`; - script.async = true; - script.defer = true; - head.appendChild(script); - - // 3秒経ã£ã¦ã‚‚スクリプトãŒãƒãƒ¼ãƒ‰ã•ã‚Œãªã„å ´åˆã¯ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒå¤ã㦠- // 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) { - localStorage.setItem('v', meta.version); - - alert( - localeData.common._settings["update-available"] + - '\n' + - localeData.common._settings["update-available-desc"] - ); - refresh(); - } - }, 3000); - - function refresh() { - localStorage.setItem('shouldFlush', 'false'); - - localStorage.removeItem('locale'); - - // Clear cache (service worker) - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - for (const registration of registrations) registration.unregister(); - }); - } catch (e) { - console.error(e); - } - - // Force reload - location.reload(true); - } -})(); diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts deleted file mode 100644 index d487915766ee07c5f823a1bc9cdfa29e3c16e896..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/check-for-update.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { version as current } from '../../config'; - -export default async function($root: any, force = false, silent = false) { - const meta = await $root.getMeta(force); - const newer = meta.version; - - if (newer != current) { - localStorage.setItem('should-refresh', 'true'); - localStorage.setItem('v', newer); - - // Clear cache (service worker) - try { - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - } - - const registrations = await navigator.serviceWorker.getRegistrations(); - for (const registration of registrations) { - registration.unregister(); - } - } catch (e) { - console.error(e); - } - - /*if (!silent) { - $root.dialog({ - title: $root.$t('@.update-available-title'), - text: $root.$t('@.update-available', { newer, current }) - }); - }*/ - - return newer; - } else { - return null; - } -} diff --git a/src/client/app/common/scripts/format-uptime.ts b/src/client/app/common/scripts/format-uptime.ts deleted file mode 100644 index 6550e4cc398bd4c6705024dd1dc310e5cdc9f7ed..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/format-uptime.ts +++ /dev/null @@ -1,25 +0,0 @@ - -/** - * Format like the uptime command - */ -export default function(sec) { - if (!sec) return sec; - - const day = Math.floor(sec / 86400); - const tod = sec % 86400; - - // Days part in string: 2 days, 1 day, null - const d = day >= 2 ? `${day} days` : day >= 1 ? `${day} day` : null; - - // Time part in string: 1 sec, 1 min, 1:01 - const t - = tod < 60 ? `${Math.floor(tod)} sec` - : tod < 3600 ? `${Math.floor(tod / 60)} min` - : `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, '0')}`; - - let str = ''; - if (d) str += `${d}, `; - str += t; - - return str; -} diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts deleted file mode 100644 index 19f2bdb064f0f97253c0a4a0832be6ac3935eaa5..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/get-face.ts +++ /dev/null @@ -1,11 +0,0 @@ -const faces = [ - '(=^・・^=)', - 'v(\'ω\')v', - 'ðŸ¡( \'-\' 🡠)フグパï¾ï¾!!!!', - '✌ï¸(´・_ï½¥`)✌ï¸', - '(。>ï¹<。)', - '(Δ・x・Δ)', - '(コ`・ï¾ãƒ»Â´ï½¹)' -]; - -export default () => faces[Math.floor(Math.random() * faces.length)]; diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts deleted file mode 100644 index 84e134cc320bc75e1bf4637af33ef0f883852cc9..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/note-mixin.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { parse } from '../../../../mfm/parse'; -import { sum, unique } from '../../../../prelude/array'; -import shouldMuteNote from './should-mute-note'; -import MkNoteMenu from '../views/components/note-menu.vue'; -import MkReactionPicker from '../views/components/reaction-picker.vue'; -import pleaseLogin from './please-login'; -import i18n from '../../i18n'; - -function focus(el, fn) { - const target = fn(el); - if (target) { - if (target.hasAttribute('tabindex')) { - target.focus(); - } else { - focus(target, fn); - } - } -} - -type Opts = { - mobile?: boolean; -}; - -export default (opts: Opts = {}) => ({ - i18n: i18n(), - - data() { - return { - showContent: false, - hideThisNote: false, - openingMenu: false - }; - }, - - computed: { - keymap(): any { - return { - 'r': () => this.reply(true), - 'e|a|plus': () => this.react(true), - 'q': () => this.renote(true), - 'f|b': this.favorite, - 'delete|ctrl+d': this.del, - 'ctrl+q': this.renoteDirectly, - 'up|k|shift+tab': this.focusBefore, - 'down|j|tab': this.focusAfter, - //'esc': this.blur, - 'm|o': () => this.menu(true), - 's': this.toggleShowContent, - '1': () => this.reactDirectly('like'), - '2': () => this.reactDirectly('love'), - '3': () => this.reactDirectly('laugh'), - '4': () => this.reactDirectly('hmm'), - '5': () => this.reactDirectly('surprise'), - '6': () => this.reactDirectly('congrats'), - '7': () => this.reactDirectly('angry'), - '8': () => this.reactDirectly('confused'), - '9': () => this.reactDirectly('rip'), - '0': () => this.reactDirectly('pudding'), - }; - }, - - isRenote(): boolean { - return (this.note.renote && - this.note.text == null && - this.note.fileIds.length == 0 && - this.note.poll == null); - }, - - appearNote(): any { - return this.isRenote ? this.note.renote : this.note; - }, - - isMyNote(): boolean { - return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId); - }, - - reactionsCount(): number { - return this.appearNote.reactions - ? sum(Object.values(this.appearNote.reactions)) - : 0; - }, - - title(): string { - return ''; - }, - - urls(): string[] { - if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: å†å¸°çš„ã«URLè¦ç´ ãŒãªã„ã‹èª¿ã¹ã‚‹ - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); - } else { - return null; - } - } - }, - - created() { - this.hideThisNote = shouldMuteNote(this.$store.state.i, this.$store.state.settings, this.appearNote); - }, - - methods: { - reply(viaKeyboard = false) { - pleaseLogin(this.$root); - this.$root.$post({ - reply: this.appearNote, - animation: !viaKeyboard, - cb: () => { - this.focus(); - } - }); - }, - - renote(viaKeyboard = false) { - pleaseLogin(this.$root); - this.$root.$post({ - renote: this.appearNote, - animation: !viaKeyboard, - cb: () => { - this.focus(); - } - }); - }, - - renoteDirectly() { - (this as any).api('notes/create', { - renoteId: this.appearNote.id - }); - }, - - react(viaKeyboard = false) { - pleaseLogin(this.$root); - this.blur(); - const w = this.$root.new(MkReactionPicker, { - source: this.$refs.reactButton, - showFocus: viaKeyboard, - animation: !viaKeyboard - }); - w.$once('chosen', reaction => { - this.$root.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }).then(() => { - w.close(); - }); - }); - w.$once('closed', this.focus); - }, - - reactDirectly(reaction) { - this.$root.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, - - undoReact(note) { - const oldReaction = note.myReaction; - if (!oldReaction) return; - this.$root.api('notes/reactions/delete', { - noteId: note.id - }); - }, - - favorite() { - pleaseLogin(this.$root); - this.$root.api('notes/favorites/create', { - noteId: this.appearNote.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.delete-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('notes/delete', { - noteId: this.appearNote.id - }); - }); - }, - - menu(viaKeyboard = false) { - if (this.openingMenu) return; - this.openingMenu = true; - const w = this.$root.new(MkNoteMenu, { - source: this.$refs.menuButton, - note: this.appearNote, - animation: !viaKeyboard - }).$once('closed', () => { - this.openingMenu = false; - this.focus(); - }); - this.$once('hook:beforeDestroy', () => { - w.destroyDom(); - }); - }, - - toggleShowContent() { - this.showContent = !this.showContent; - }, - - focus() { - this.$el.focus(); - }, - - blur() { - this.$el.blur(); - }, - - focusBefore() { - focus(this.$el, e => e.previousElementSibling); - }, - - focusAfter() { - focus(this.$el, e => e.nextElementSibling); - } - } -}); diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts deleted file mode 100644 index 5b31a9f9d087a37d3c65bd78fa662e9ed9cbdb49..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/note-subscriber.ts +++ /dev/null @@ -1,149 +0,0 @@ -import Vue from 'vue'; - -export default prop => ({ - data() { - return { - connection: null - }; - }, - - computed: { - $_ns_note_(): any { - return this[prop]; - }, - - $_ns_isRenote(): boolean { - return (this.$_ns_note_.renote != null && - this.$_ns_note_.text == null && - this.$_ns_note_.fileIds.length == 0 && - this.$_ns_note_.poll == null); - }, - - $_ns_target(): any { - return this.$_ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_; - }, - }, - - created() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream; - } - }, - - mounted() { - this.capture(true); - - if (this.$store.getters.isSignedIn) { - this.connection.on('_connected_', this.onStreamConnected); - } - }, - - beforeDestroy() { - this.decapture(true); - - if (this.$store.getters.isSignedIn) { - this.connection.off('_connected_', this.onStreamConnected); - } - }, - - methods: { - capture(withHandler = false) { - if (this.$store.getters.isSignedIn) { - const data = { - id: this.$_ns_target.id - } as any; - - if ( - (this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) || - (this.$_ns_target.mentions || []).includes(this.$store.state.i.id) - ) { - data.read = true; - } - - this.connection.send('sn', data); - if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); - } - }, - - decapture(withHandler = false) { - if (this.$store.getters.isSignedIn) { - this.connection.send('un', { - id: this.$_ns_target.id - }); - if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); - } - }, - - onStreamConnected() { - this.capture(); - }, - - onStreamNoteUpdated(data) { - const { type, id, body } = data; - - if (id !== this.$_ns_target.id) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - if (this.$_ns_target.reactions == null) { - Vue.set(this.$_ns_target, 'reactions', {}); - } - - if (this.$_ns_target.reactions[reaction] == null) { - Vue.set(this.$_ns_target.reactions, reaction, 0); - } - - // Increment the count - this.$_ns_target.reactions[reaction]++; - - if (body.userId == this.$store.state.i.id) { - Vue.set(this.$_ns_target, 'myReaction', reaction); - } - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - if (this.$_ns_target.reactions == null) { - return; - } - - if (this.$_ns_target.reactions[reaction] == null) { - return; - } - - // Decrement the count - if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--; - - if (body.userId == this.$store.state.i.id) { - Vue.set(this.$_ns_target, 'myReaction', null); - } - break; - } - - case 'pollVoted': { - const choice = body.choice; - this.$_ns_target.poll.choices[choice].votes++; - if (body.userId == this.$store.state.i.id) { - Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true); - } - break; - } - - case 'deleted': { - Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt); - Vue.set(this.$_ns_target, 'renote', null); - this.$_ns_target.text = null; - this.$_ns_target.fileIds = []; - this.$_ns_target.poll = null; - this.$_ns_target.geo = null; - this.$_ns_target.cw = null; - break; - } - } - }, - } -}); diff --git a/src/client/app/common/scripts/room/furniture.ts b/src/client/app/common/scripts/room/furniture.ts deleted file mode 100644 index 7734e32668699547a00291fef0d828190f1ed209..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/room/furniture.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type RoomInfo = { - roomType: string; - carpetColor: string; - furnitures: Furniture[]; -}; - -export type Furniture = { - id: string; // åŒã˜å®¶å…·ãŒè¤‡æ•°ã‚ã‚‹å ´åˆã«ãã‚Œãžã‚Œã‚’è˜åˆ¥ã™ã‚‹ãŸã‚ã®IDã§ã‚ã‚Šã€å®¶å…·IDã§ã¯ãªã„ - type: string; // ã“ã£ã¡ãŒå®¶å…·ID(chairã¨ã‹) - position: { - x: number; - y: number; - z: number; - }; - rotation: { - x: number; - y: number; - z: number; - }; - props?: Record<string, any>; -}; diff --git a/src/client/app/common/scripts/room/furnitures.json5 b/src/client/app/common/scripts/room/furnitures.json5 deleted file mode 100644 index 7c1a90a3f93b38f277389663e445e4123766e298..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/room/furnitures.json5 +++ /dev/null @@ -1,397 +0,0 @@ -// 家具メタデータ - -// 家具ã«ã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè¨å®šã§ãるプãƒãƒ‘ティをè¨å®šå¯èƒ½ã§ã™: -// -// props: { -// <propname>: <proptype> -// } -// -// proptype一覧: -// * image ... ç”»åƒé¸æŠžãƒ€ã‚¤ã‚¢ãƒã‚°ã‚’出ã—ã€ãã®ç”»åƒã®URLãŒæ ¼ç´ã•ã‚Œã¾ã™ -// * color ... 色é¸æŠžã‚³ãƒ³ãƒˆãƒãƒ¼ãƒ«ã‚’出ã—ã€é¸æŠžã•ã‚ŒãŸè‰²ãŒæ ¼ç´ã•ã‚Œã¾ã™ - -// 家具ã«ã‚«ã‚¹ã‚¿ãƒ テクスãƒãƒ£ã‚’é©ç”¨ã§ãるよã†ã«ã™ã‚‹ã«ã¯ã€textureプãƒãƒ‘ティã«ä»¥ä¸‹ã®è¿½åŠ ã®æƒ…å ±ã‚’å«ã‚ã¾ã™: -// 便宜上ãã®UVã®ã©ã®éƒ¨åˆ†ã«ã‚«ã‚¹ã‚¿ãƒ テクスãƒãƒ£ã‚’貼りåˆã‚ã›ã‚‹ã‹ã®ã‚¨ãƒªã‚¢ã‚’テクスãƒãƒ£ã‚¨ãƒªã‚¢ã¨å‘¼ã³ã¾ã™ã€‚ -// UVã¯1024*1024ã ã¨ä»®å®šã—ã¾ã™ã€‚ -// -// <key>: { -// prop: <プãƒãƒ‘ティå>, -// uv: { -// x: <テクスãƒãƒ£ã‚¨ãƒªã‚¢X座標>, -// y: <テクスãƒãƒ£ã‚¨ãƒªã‚¢Y座標>, -// width: <テクスãƒãƒ£ã‚¨ãƒªã‚¢ã®å¹…>, -// height: <テクスãƒãƒ£ã‚¨ãƒªã‚¢ã®é«˜ã•>, -// }, -// } -// -// <key>ã«ã¯ã€ã‚«ã‚¹ã‚¿ãƒ テクスãƒãƒ£ã‚’é©ç”¨ã—ãŸã„メッシュåを指定ã—ã¾ã™ -// <プãƒãƒ‘ティå>ã«ã¯ã€ã‚«ã‚¹ã‚¿ãƒ テクスãƒãƒ£ã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ç”»åƒã‚’æ ¼ç´ã™ã‚‹ãƒ—ãƒãƒ‘ティ(å‰è¿°)åを指定ã—ã¾ã™ - -// 家具ã«ã‚«ã‚¹ã‚¿ãƒ カラーをé©ç”¨ã§ãるよã†ã«ã™ã‚‹ã«ã¯ã€colorプãƒãƒ‘ティã«ä»¥ä¸‹ã®è¿½åŠ ã®æƒ…å ±ã‚’å«ã‚ã¾ã™: -// -// <key>: <プãƒãƒ‘ティå> -// -// <key>ã«ã¯ã€ã‚«ã‚¹ã‚¿ãƒ カラーをé©ç”¨ã—ãŸã„マテリアルåを指定ã—ã¾ã™ -// <プãƒãƒ‘ティå>ã«ã¯ã€ã‚«ã‚¹ã‚¿ãƒ カラーã¨ã—ã¦ä½¿ç”¨ã™ã‚‹è‰²ã‚’æ ¼ç´ã™ã‚‹ãƒ—ãƒãƒ‘ティ(å‰è¿°)åを指定ã—ã¾ã™ - -[ - { - id: "milk", - place: "floor" - }, - { - id: "bed", - place: "floor" - }, - { - id: "low-table", - place: "floor", - props: { - color: 'color' - }, - color: { - Table: 'color' - } - }, - { - id: "desk", - place: "floor", - props: { - color: 'color' - }, - color: { - Board: 'color' - } - }, - { - id: "chair", - place: "floor", - props: { - color: 'color' - }, - color: { - Chair: 'color' - } - }, - { - id: "chair2", - place: "floor", - props: { - color1: 'color', - color2: 'color' - }, - color: { - Cushion: 'color1', - Leg: 'color2' - } - }, - { - id: "fan", - place: "wall" - }, - { - id: "pc", - place: "floor" - }, - { - id: "plant", - place: "floor" - }, - { - id: "plant2", - place: "floor" - }, - { - id: "eraser", - place: "floor" - }, - { - id: "pencil", - place: "floor" - }, - { - id: "pudding", - place: "floor" - }, - { - id: "cardboard-box", - place: "floor" - }, - { - id: "cardboard-box2", - place: "floor" - }, - { - id: "cardboard-box3", - place: "floor" - }, - { - id: "book", - place: "floor", - props: { - color: 'color' - }, - color: { - Cover: 'color' - } - }, - { - id: "book2", - place: "floor" - }, - { - id: "piano", - place: "floor" - }, - { - id: "facial-tissue", - place: "floor" - }, - { - id: "server", - place: "floor" - }, - { - id: "moon", - place: "floor" - }, - { - id: "corkboard", - place: "wall" - }, - { - id: "mousepad", - place: "floor", - props: { - color: 'color' - }, - color: { - Pad: 'color' - } - }, - { - id: "monitor", - place: "floor", - props: { - screen: 'image' - }, - texture: { - Screen: { - prop: 'screen', - uv: { - x: 0, - y: 434, - width: 1024, - height: 588, - }, - }, - }, - }, - { - id: "tv", - place: "floor", - props: { - screen: 'image' - }, - texture: { - Screen: { - prop: 'screen', - uv: { - x: 0, - y: 434, - width: 1024, - height: 588, - }, - }, - }, - }, - { - id: "keyboard", - place: "floor" - }, - { - id: "carpet-stripe", - place: "floor", - props: { - color1: 'color', - color2: 'color' - }, - color: { - CarpetAreaA: 'color1', - CarpetAreaB: 'color2' - }, - }, - { - id: "mat", - place: "floor", - props: { - color: 'color' - }, - color: { - Mat: 'color' - } - }, - { - id: "color-box", - place: "floor", - props: { - color: 'color' - }, - color: { - main: 'color' - } - }, - { - id: "wall-clock", - place: "wall" - }, - { - id: "cube", - place: "floor", - props: { - color: 'color' - }, - color: { - Cube: 'color' - } - }, - { - id: "photoframe", - place: "wall", - props: { - photo: 'image', - color: 'color' - }, - texture: { - Photo: { - prop: 'photo', - uv: { - x: 0, - y: 342, - width: 1024, - height: 683, - }, - }, - }, - color: { - Frame: 'color' - } - }, - { - id: "pinguin", - place: "floor", - props: { - body: 'color', - belly: 'color' - }, - color: { - Body: 'body', - Belly: 'belly', - } - }, - { - id: "rubik-cube", - place: "floor", - }, - { - id: "poster-h", - place: "wall", - props: { - picture: 'image' - }, - texture: { - Poster: { - prop: 'picture', - uv: { - x: 0, - y: 277, - width: 1024, - height: 745, - }, - }, - }, - }, - { - id: "poster-v", - place: "wall", - props: { - picture: 'image' - }, - texture: { - Poster: { - prop: 'picture', - uv: { - x: 0, - y: 0, - width: 745, - height: 1024, - }, - }, - }, - }, - { - id: "sofa", - place: "floor", - props: { - color: 'color' - }, - color: { - Sofa: 'color' - } - }, - { - id: "spiral", - place: "floor", - props: { - color: 'color' - }, - color: { - Step: 'color' - } - }, - { - id: "bin", - place: "floor", - props: { - color: 'color' - }, - color: { - Bin: 'color' - } - }, - { - id: "cup-noodle", - place: "floor" - }, - { - id: "holo-display", - place: "floor", - props: { - image: 'image' - }, - texture: { - Image_Front: { - prop: 'image', - uv: { - x: 0, - y: 0, - width: 1024, - height: 1024, - }, - }, - Image_Back: { - prop: 'image', - uv: { - x: 0, - y: 0, - width: 1024, - height: 1024, - }, - }, - }, - }, - { - id: 'energy-drink', - place: "floor", - } -] diff --git a/src/client/app/common/scripts/room/room.ts b/src/client/app/common/scripts/room/room.ts deleted file mode 100644 index c2a989c784af85043dcc564d4b3083e02131380f..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/room/room.ts +++ /dev/null @@ -1,776 +0,0 @@ -import autobind from 'autobind-decorator'; -import { v4 as uuid } from 'uuid'; -import * as THREE from 'three'; -import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; -import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; -import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; -import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js'; -import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js'; -import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; -import { Furniture, RoomInfo } from './furniture'; -import { query as urlQuery } from '../../../../../prelude/url'; -const furnitureDefs = require('./furnitures.json5'); - -THREE.ImageUtils.crossOrigin = ''; - -type Options = { - graphicsQuality: Room['graphicsQuality']; - onChangeSelect: Room['onChangeSelect']; - useOrthographicCamera: boolean; -}; - -/** - * MisskeyRoom Core Engine - */ -export class Room { - private clock: THREE.Clock; - private scene: THREE.Scene; - private renderer: THREE.WebGLRenderer; - private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera; - private controls: OrbitControls; - private composer: EffectComposer; - private mixers: THREE.AnimationMixer[] = []; - private furnitureControl: TransformControls; - private roomInfo: RoomInfo; - private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra'; - private roomObj: THREE.Object3D; - private objects: THREE.Object3D[] = []; - private selectedObject: THREE.Object3D = null; - private onChangeSelect: Function; - private isTransformMode = false; - private renderFrameRequestId: number; - - private get canvas(): HTMLCanvasElement { - return this.renderer.domElement; - } - - private get furnitures(): Furniture[] { - return this.roomInfo.furnitures; - } - - private set furnitures(furnitures: Furniture[]) { - this.roomInfo.furnitures = furnitures; - } - - private get enableShadow() { - return this.graphicsQuality != 'cheep'; - } - - private get usePostFXs() { - return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low'; - } - - private get shadowQuality() { - return ( - this.graphicsQuality === 'ultra' ? 16384 : - this.graphicsQuality === 'high' ? 8192 : - this.graphicsQuality === 'medium' ? 4096 : - this.graphicsQuality === 'low' ? 1024 : - 0); // cheep - } - - constructor(user, isMyRoom, roomInfo: RoomInfo, container, options: Options) { - this.roomInfo = roomInfo; - this.graphicsQuality = options.graphicsQuality; - this.onChangeSelect = options.onChangeSelect; - - this.clock = new THREE.Clock(true); - - //#region Init a scene - this.scene = new THREE.Scene(); - - const width = window.innerWidth; - const height = window.innerHeight; - - //#region Init a renderer - this.renderer = new THREE.WebGLRenderer({ - antialias: false, - stencil: false, - alpha: false, - powerPreference: - this.graphicsQuality === 'ultra' ? 'high-performance' : - this.graphicsQuality === 'high' ? 'high-performance' : - this.graphicsQuality === 'medium' ? 'default' : - this.graphicsQuality === 'low' ? 'low-power' : - 'low-power' // cheep - }); - - this.renderer.setPixelRatio(window.devicePixelRatio); - this.renderer.setSize(width, height); - this.renderer.autoClear = false; - this.renderer.setClearColor(new THREE.Color(0x051f2d)); - this.renderer.shadowMap.enabled = this.enableShadow; - this.renderer.shadowMap.type = - this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap : - this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap : - this.graphicsQuality === 'medium' ? THREE.PCFShadowMap : - this.graphicsQuality === 'low' ? THREE.BasicShadowMap : - THREE.BasicShadowMap; // cheep - - container.appendChild(this.canvas); - //#endregion - - //#region Init a camera - this.camera = options.useOrthographicCamera - ? new THREE.OrthographicCamera( - width / - 2, width / 2, height / 2, height / - 2, -10, 10) - : new THREE.PerspectiveCamera(45, width / height); - - if (options.useOrthographicCamera) { - this.camera.position.x = 2; - this.camera.position.y = 2; - this.camera.position.z = 2; - this.camera.zoom = 100; - this.camera.updateProjectionMatrix(); - } else { - this.camera.position.x = 5; - this.camera.position.y = 2; - this.camera.position.z = 5; - } - - this.scene.add(this.camera); - //#endregion - - //#region AmbientLight - const ambientLight = new THREE.AmbientLight(0xffffff, 1); - this.scene.add(ambientLight); - //#endregion - - if (this.graphicsQuality !== 'cheep') { - //#region Room light - const roomLight = new THREE.SpotLight(0xffffff, 0.1); - - roomLight.position.set(0, 8, 0); - roomLight.castShadow = this.enableShadow; - roomLight.shadow.bias = -0.0001; - roomLight.shadow.mapSize.width = this.shadowQuality; - roomLight.shadow.mapSize.height = this.shadowQuality; - roomLight.shadow.camera.near = 0.1; - roomLight.shadow.camera.far = 9; - roomLight.shadow.camera.fov = 45; - - this.scene.add(roomLight); - //#endregion - } - - //#region Out light - const outLight1 = new THREE.SpotLight(0xffffff, 0.4); - outLight1.position.set(9, 3, -2); - outLight1.castShadow = this.enableShadow; - outLight1.shadow.bias = -0.001; // アクãƒã€ã‚¢ãƒ¼ãƒãƒ•ã‚¡ã‚¯ãƒˆå¯¾ç– ãã®ä»£ã‚りピーターパンãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ - outLight1.shadow.mapSize.width = this.shadowQuality; - outLight1.shadow.mapSize.height = this.shadowQuality; - outLight1.shadow.camera.near = 6; - outLight1.shadow.camera.far = 15; - outLight1.shadow.camera.fov = 45; - this.scene.add(outLight1); - - const outLight2 = new THREE.SpotLight(0xffffff, 0.2); - outLight2.position.set(-2, 3, 9); - outLight2.castShadow = false; - outLight2.shadow.bias = -0.001; // アクãƒã€ã‚¢ãƒ¼ãƒãƒ•ã‚¡ã‚¯ãƒˆå¯¾ç– ãã®ä»£ã‚りピーターパンãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ - outLight2.shadow.camera.near = 6; - outLight2.shadow.camera.far = 15; - outLight2.shadow.camera.fov = 45; - this.scene.add(outLight2); - //#endregion - - //#region Init a controller - this.controls = new OrbitControls(this.camera, this.canvas); - - this.controls.target.set(0, 1, 0); - this.controls.enableZoom = true; - this.controls.enablePan = isMyRoom; - this.controls.minPolarAngle = 0; - this.controls.maxPolarAngle = Math.PI / 2; - this.controls.minAzimuthAngle = 0; - this.controls.maxAzimuthAngle = Math.PI / 2; - this.controls.enableDamping = true; - this.controls.dampingFactor = 0.2; - this.controls.mouseButtons.LEFT = 1; - this.controls.mouseButtons.MIDDLE = 2; - this.controls.mouseButtons.RIGHT = 0; - //#endregion - - //#region POST FXs - if (!this.usePostFXs) { - this.composer = null; - } else { - const renderTarget = new THREE.WebGLRenderTarget(width, height, { - minFilter: THREE.LinearFilter, - magFilter: THREE.LinearFilter, - format: THREE.RGBFormat, - stencilBuffer: false, - }); - - const fxaa = new ShaderPass(FXAAShader); - fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height); - fxaa.renderToScreen = true; - - this.composer = new EffectComposer(this.renderer, renderTarget); - this.composer.addPass(new RenderPass(this.scene, this.camera)); - if (this.graphicsQuality === 'ultra') { - this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512)); - } - this.composer.addPass(fxaa); - } - //#endregion - //#endregion - - //#region Label - //#region Avatar - const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`; - - const textureLoader = new THREE.TextureLoader(); - textureLoader.crossOrigin = 'anonymous'; - - const iconTexture = textureLoader.load(avatarUrl); - iconTexture.wrapS = THREE.RepeatWrapping; - iconTexture.wrapT = THREE.RepeatWrapping; - iconTexture.anisotropy = 16; - - const avatarMaterial = new THREE.MeshBasicMaterial({ - map: iconTexture, - side: THREE.DoubleSide, - alphaTest: 0.5 - }); - - const iconGeometry = new THREE.PlaneGeometry(1, 1); - - const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial); - avatarObject.position.set(-3, 2.5, 2); - avatarObject.rotation.y = Math.PI / 2; - avatarObject.castShadow = false; - - this.scene.add(avatarObject); - //#endregion - - //#region Username - const name = user.username; - - new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => { - const nameGeometry = new THREE.TextGeometry(name, { - size: 0.5, - height: 0, - curveSegments: 8, - font: font, - bevelThickness: 0, - bevelSize: 0, - bevelEnabled: false - }); - - const nameMaterial = new THREE.MeshLambertMaterial({ - color: 0xffffff - }); - - const nameObject = new THREE.Mesh(nameGeometry, nameMaterial); - nameObject.position.set(-3, 2.25, 1.25); - nameObject.rotation.y = Math.PI / 2; - nameObject.castShadow = false; - - this.scene.add(nameObject); - }); - //#endregion - //#endregion - - //#region Interaction - if (isMyRoom) { - this.furnitureControl = new TransformControls(this.camera, this.canvas); - this.scene.add(this.furnitureControl); - - // Hover highlight - this.canvas.onmousemove = this.onmousemove; - - // Click - this.canvas.onmousedown = this.onmousedown; - } - //#endregion - - //#region Init room - this.loadRoom(); - //#endregion - - //#region Load furnitures - for (const furniture of this.furnitures) { - this.loadFurniture(furniture).then(obj => { - this.scene.add(obj.scene); - this.objects.push(obj.scene); - }); - } - //#endregion - - // Start render - if (this.usePostFXs) { - this.renderWithPostFXs(); - } else { - this.renderWithoutPostFXs(); - } - } - - @autobind - private renderWithoutPostFXs() { - this.renderFrameRequestId = - window.requestAnimationFrame(this.renderWithoutPostFXs); - - // Update animations - const clock = this.clock.getDelta(); - for (const mixer of this.mixers) { - mixer.update(clock); - } - - this.controls.update(); - this.renderer.render(this.scene, this.camera); - } - - @autobind - private renderWithPostFXs() { - this.renderFrameRequestId = - window.requestAnimationFrame(this.renderWithPostFXs); - - // Update animations - const clock = this.clock.getDelta(); - for (const mixer of this.mixers) { - mixer.update(clock); - } - - this.controls.update(); - this.renderer.clear(); - this.composer.render(); - } - - @autobind - private loadRoom() { - const type = this.roomInfo.roomType; - new GLTFLoader().load(`/assets/room/rooms/${type}/${type}.glb`, gltf => { - gltf.scene.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - - child.receiveShadow = this.enableShadow; - - child.material = new THREE.MeshLambertMaterial({ - color: (child.material as THREE.MeshStandardMaterial).color, - map: (child.material as THREE.MeshStandardMaterial).map, - name: (child.material as THREE.MeshStandardMaterial).name, - }); - - // 異方性フィルタリング - if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') { - (child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshLambertMaterial).map.anisotropy = 8; - } - }); - - gltf.scene.position.set(0, 0, 0); - - this.scene.add(gltf.scene); - this.roomObj = gltf.scene; - if (this.roomInfo.roomType === 'default') { - this.applyCarpetColor(); - } - }); - } - - @autobind - private loadFurniture(furniture: Furniture) { - const def = furnitureDefs.find(d => d.id === furniture.type); - return new Promise<GLTF>((res, rej) => { - const loader = new GLTFLoader(); - loader.load(`/assets/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => { - const model = gltf.scene; - - // Load animation - if (gltf.animations.length > 0) { - const mixer = new THREE.AnimationMixer(model); - this.mixers.push(mixer); - for (const clip of gltf.animations) { - mixer.clipAction(clip).play(); - } - } - - model.name = furniture.id; - model.position.x = furniture.position.x; - model.position.y = furniture.position.y; - model.position.z = furniture.position.z; - model.rotation.x = furniture.rotation.x; - model.rotation.y = furniture.rotation.y; - model.rotation.z = furniture.rotation.z; - - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - child.castShadow = this.enableShadow; - child.receiveShadow = this.enableShadow; - (child.material as THREE.MeshStandardMaterial).metalness = 0; - - // 異方性フィルタリング - if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') { - (child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshStandardMaterial).map.anisotropy = 8; - } - }); - - if (def.color) { // カスタムカラー - this.applyCustomColor(model); - } - - if (def.texture) { // カスタムテクスãƒãƒ£ - this.applyCustomTexture(model); - } - - res(gltf); - }, null, rej); - }); - } - - @autobind - private applyCarpetColor() { - this.roomObj.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - if (child.material && - (child.material as THREE.MeshStandardMaterial).name && - (child.material as THREE.MeshStandardMaterial).name === 'Carpet' - ) { - const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16); - (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex); - } - }); - } - - @autobind - private applyCustomColor(model: THREE.Object3D) { - const furniture = this.furnitures.find(furniture => furniture.id === model.name); - const def = furnitureDefs.find(d => d.id === furniture.type); - if (def.color == null) return; - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - for (const t of Object.keys(def.color)) { - if (!child.material || - !(child.material as THREE.MeshStandardMaterial).name || - (child.material as THREE.MeshStandardMaterial).name !== t - ) continue; - - const prop = def.color[t]; - const val = furniture.props ? furniture.props[prop] : undefined; - - if (val == null) continue; - - const colorHex = parseInt(val.substr(1), 16); - (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex); - } - }); - } - - @autobind - private applyCustomTexture(model: THREE.Object3D) { - const furniture = this.furnitures.find(furniture => furniture.id === model.name); - const def = furnitureDefs.find(d => d.id === furniture.type); - if (def.texture == null) return; - - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - for (const t of Object.keys(def.texture)) { - if (child.name !== t) continue; - - const prop = def.texture[t].prop; - const val = furniture.props ? furniture.props[prop] : undefined; - - if (val == null) continue; - - const canvas = document.createElement('canvas'); - canvas.height = 1024; - canvas.width = 1024; - - child.material = new THREE.MeshLambertMaterial({ - emissive: 0x111111, - side: THREE.DoubleSide, - alphaTest: 0.5, - }); - - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.onload = () => { - const uvInfo = def.texture[t].uv; - - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, - 0, 0, img.width, img.height, - uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height); - - const texture = new THREE.Texture(canvas); - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.anisotropy = 16; - texture.flipY = false; - - (child.material as THREE.MeshLambertMaterial).map = texture; - (child.material as THREE.MeshLambertMaterial).needsUpdate = true; - (child.material as THREE.MeshLambertMaterial).map.needsUpdate = true; - }; - img.src = val; - } - }); - } - - @autobind - private onmousemove(ev: MouseEvent) { - if (this.isTransformMode) return; - - const rect = (ev.target as HTMLElement).getBoundingClientRect(); - const x = (((ev.clientX * window.devicePixelRatio) - rect.left) / this.canvas.width) * 2 - 1; - const y = -(((ev.clientY * window.devicePixelRatio) - rect.top) / this.canvas.height) * 2 + 1; - const pos = new THREE.Vector2(x, y); - - this.camera.updateMatrixWorld(); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(pos, this.camera); - - const intersects = raycaster.intersectObjects(this.objects, true); - - for (const object of this.objects) { - if (this.isSelectedObject(object)) continue; - object.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000); - } - }); - } - - if (intersects.length > 0) { - const intersected = this.getRoot(intersects[0].object); - if (this.isSelectedObject(intersected)) return; - intersected.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919); - } - }); - } - } - - @autobind - private onmousedown(ev: MouseEvent) { - if (this.isTransformMode) return; - if (ev.target !== this.canvas || ev.button !== 0) return; - - const rect = (ev.target as HTMLElement).getBoundingClientRect(); - const x = (((ev.clientX * window.devicePixelRatio) - rect.left) / this.canvas.width) * 2 - 1; - const y = -(((ev.clientY * window.devicePixelRatio) - rect.top) / this.canvas.height) * 2 + 1; - const pos = new THREE.Vector2(x, y); - - this.camera.updateMatrixWorld(); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(pos, this.camera); - - const intersects = raycaster.intersectObjects(this.objects, true); - - for (const object of this.objects) { - object.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000); - } - }); - } - - if (intersects.length > 0) { - const selectedObj = this.getRoot(intersects[0].object); - this.selectFurniture(selectedObj); - } else { - this.selectedObject = null; - this.onChangeSelect(null); - } - } - - @autobind - private getRoot(obj: THREE.Object3D): THREE.Object3D { - let found = false; - let x = obj.parent; - while (!found) { - if (x.parent.parent == null) { - found = true; - } else { - x = x.parent; - } - } - return x; - } - - @autobind - private isSelectedObject(obj: THREE.Object3D): boolean { - if (this.selectedObject == null) { - return false; - } else { - return obj.name === this.selectedObject.name; - } - } - - @autobind - private selectFurniture(obj: THREE.Object3D) { - this.selectedObject = obj; - this.onChangeSelect(obj); - obj.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000); - } - }); - } - - /** - * 家具ã®ç§»å‹•/回転モードã«ã—ã¾ã™ - * @param type 移動ã‹å›žè»¢ã‹ - */ - @autobind - public enterTransformMode(type: 'translate' | 'rotate') { - this.isTransformMode = true; - this.furnitureControl.setMode(type); - this.furnitureControl.attach(this.selectedObject); - } - - /** - * 家具ã®ç§»å‹•/回転モードを終了ã—ã¾ã™ - */ - @autobind - public exitTransformMode() { - this.isTransformMode = false; - this.furnitureControl.detach(); - } - - /** - * 家具プãƒãƒ‘ティを更新ã—ã¾ã™ - * @param key プãƒãƒ‘ティå - * @param value 値 - */ - @autobind - public updateProp(key: string, value: any) { - const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name); - if (furniture.props == null) furniture.props = {}; - furniture.props[key] = value; - this.applyCustomColor(this.selectedObject); - this.applyCustomTexture(this.selectedObject); - } - - /** - * 部屋ã«å®¶å…·ã‚’è¿½åŠ ã—ã¾ã™ - * @param type 家具ã®ç¨®é¡ž - */ - @autobind - public addFurniture(type: string) { - const furniture = { - id: uuid(), - type: type, - position: { - x: 0, - y: 0, - z: 0, - }, - rotation: { - x: 0, - y: 0, - z: 0, - }, - }; - - this.furnitures.push(furniture); - - this.loadFurniture(furniture).then(obj => { - this.scene.add(obj.scene); - this.objects.push(obj.scene); - }); - } - - /** - * ç¾åœ¨é¸æŠžã•ã‚Œã¦ã„る家具を部屋ã‹ã‚‰å‰Šé™¤ã—ã¾ã™ - */ - @autobind - public removeFurniture() { - this.exitTransformMode(); - const obj = this.selectedObject; - this.scene.remove(obj); - this.objects = this.objects.filter(object => object.name !== obj.name); - this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name); - this.selectedObject = null; - this.onChangeSelect(null); - } - - /** - * å…¨ã¦ã®å®¶å…·ã‚’部屋ã‹ã‚‰å‰Šé™¤ã—ã¾ã™ - */ - @autobind - public removeAllFurnitures() { - this.exitTransformMode(); - for (const obj of this.objects) { - this.scene.remove(obj); - } - this.objects = []; - this.furnitures = []; - this.selectedObject = null; - this.onChangeSelect(null); - } - - /** - * 部屋ã®åºŠã®è‰²ã‚’変更ã—ã¾ã™ - * @param color 色 - */ - @autobind - public updateCarpetColor(color: string) { - this.roomInfo.carpetColor = color; - this.applyCarpetColor(); - } - - /** - * 部屋ã®ç¨®é¡žã‚’変更ã—ã¾ã™ - * @param type 種類 - */ - @autobind - public changeRoomType(type: string) { - this.roomInfo.roomType = type; - this.scene.remove(this.roomObj); - this.loadRoom(); - } - - /** - * 部屋データをå–å¾—ã—ã¾ã™ - */ - @autobind - public getRoomInfo() { - for (const obj of this.objects) { - const furniture = this.furnitures.find(f => f.id === obj.name); - furniture.position.x = obj.position.x; - furniture.position.y = obj.position.y; - furniture.position.z = obj.position.z; - furniture.rotation.x = obj.rotation.x; - furniture.rotation.y = obj.rotation.y; - furniture.rotation.z = obj.rotation.z; - } - - return this.roomInfo; - } - - /** - * é¸æŠžã•ã‚Œã¦ã„る家具をå–å¾—ã—ã¾ã™ - */ - @autobind - public getSelectedObject() { - return this.selectedObject; - } - - @autobind - public findFurnitureById(id: string) { - return this.furnitures.find(furniture => furniture.id === id); - } - - /** - * レンダリングを終了ã—ã¾ã™ - */ - @autobind - public destroy() { - // Stop render loop - window.cancelAnimationFrame(this.renderFrameRequestId); - - this.controls.dispose(); - this.scene.dispose(); - } -} diff --git a/src/client/app/common/scripts/should-mute-note.ts b/src/client/app/common/scripts/should-mute-note.ts deleted file mode 100644 index 8fd78886287d9fe3f7b94c17872ce310f0273494..0000000000000000000000000000000000000000 --- a/src/client/app/common/scripts/should-mute-note.ts +++ /dev/null @@ -1,19 +0,0 @@ -export default function(me, settings, note) { - const isMyNote = me && (note.userId == me.id); - const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; - - const includesMutedWords = (text: string) => - text - ? settings.mutedWords.some(q => q.length > 0 && !q.some(word => - word.startsWith('/') && word.endsWith('/') ? !(new RegExp(word.substr(1, word.length - 2)).test(text)) : !text.includes(word))) - : false; - - return ( - (!isMyNote && note.reply && includesMutedWords(note.reply.text)) || - (!isMyNote && note.renote && includesMutedWords(note.renote.text)) || - (!settings.showMyRenotes && isMyNote && isPureRenote) || - (!settings.showRenotedMyNotes && isPureRenote && note.renote.userId == me.id) || - (!settings.showLocalRenotes && isPureRenote && note.renote.user.host == null) || - (!isMyNote && includesMutedWords(note.text)) - ); -} diff --git a/src/client/app/common/size.ts b/src/client/app/common/size.ts deleted file mode 100644 index 6abb3057470e68f6604110ef008fbe5905cfcce9..0000000000000000000000000000000000000000 --- a/src/client/app/common/size.ts +++ /dev/null @@ -1,18 +0,0 @@ -export default { - install(Vue) { - Vue.directive('size', { - inserted(el, binding) { - const query = binding.value; - const width = el.clientWidth; - for (const q of query) { - if (q.lt && (width <= q.lt)) { - el.classList.add(q.class); - } - if (q.gt && (width >= q.gt)) { - el.classList.add(q.class); - } - } - } - }); - } -}; diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue deleted file mode 100644 index e802000833f80e75e63fafd9ccab334905a8bc88..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/acct.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<span class="mk-acct" v-once> - <span class="name">@{{ user.username }}</span> - <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span> - <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { host } from '../../../config'; -import { toUnicode } from 'punycode'; -export default Vue.extend({ - props: ['user', 'detail'], - data() { - return { - host: toUnicode(host) - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-acct - > .host.fade - opacity 0.5 - - > .locked - opacity 0.8 - margin-left 0.5em -</style> diff --git a/src/client/app/common/views/components/analog-clock.vue b/src/client/app/common/views/components/analog-clock.vue deleted file mode 100644 index 5eb7ffd153c4f0fbe20b6123e7601e87a3ae7f89..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/analog-clock.vue +++ /dev/null @@ -1,140 +0,0 @@ -<template> -<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none"> - <circle v-for="angle, i in graduations" - :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))" - :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))" - :r="i % 5 == 0 ? 0.125 : 0.05" - :fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/> - - <line - :x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))" - :y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))" - :x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))" - :y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))" - :stroke="sHandColor" - stroke-width="0.05"/> - <line - :x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))" - :y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))" - :x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))" - :y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))" - :stroke="mHandColor" - stroke-width="0.1"/> - <line - :x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))" - :y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))" - :x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))" - :y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))" - :stroke="hHandColor" - stroke-width="0.1"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as tinycolor from 'tinycolor2'; - -export default Vue.extend({ - props: { - dark: { - type: Boolean, - default: false - }, - smooth: { - type: Boolean, - default: false - } - }, - - data() { - return { - now: new Date(), - enabled: true, - - graduationsPadding: 0.5, - handsPadding: 1, - handsTailLength: 0.7, - hHandLengthRatio: 0.75, - mHandLengthRatio: 1, - sHandLengthRatio: 1 - }; - }, - - computed: { - majorGraduationColor(): string { - return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; - }, - minorGraduationColor(): string { - return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - }, - - sHandColor(): string { - return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; - }, - mHandColor(): string { - return this.dark ? '#fff' : '#777'; - }, - hHandColor(): string { - return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString(); - }, - - ms(): number { - return this.now.getMilliseconds() * this.smooth; - }, - s(): number { - return this.now.getSeconds(); - }, - m(): number { - return this.now.getMinutes(); - }, - h(): number { - return this.now.getHours(); - }, - - hAngle(): number { - return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6; - }, - mAngle(): number { - return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30; - }, - sAngle(): number { - return Math.PI * (this.s + this.ms / 1000) / 30; - }, - - graduations(): any { - const angles = []; - for (let i = 0; i < 60; i++) { - const angle = Math.PI * i / 30; - angles.push(angle); - } - - return angles; - } - }, - - mounted() { - const update = () => { - if (this.enabled) { - this.tick(); - requestAnimationFrame(update); - } - }; - update(); - }, - - beforeDestroy() { - this.enabled = false; - }, - - methods: { - tick() { - this.now = new Date(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-analog-clock - display block -</style> diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue deleted file mode 100644 index cd02c6957d58e71c59b67002c189a903766c258f..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/avatar.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> - <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick" v-once> - <span class="inner" :style="icon"></span> - </span> - <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick" v-once> - <span class="inner" :style="icon"></span> - </span> - <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id" v-once> - <span class="inner" :style="icon"></span> - </router-link> - <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview" v-once> - <span class="inner" :style="icon"></span> - </router-link> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - props: { - user: { - type: Object, - required: true - }, - target: { - required: false, - default: null - }, - disableLink: { - required: false, - default: false - }, - disablePreview: { - required: false, - default: false - } - }, - computed: { - lightmode(): boolean { - return this.$store.state.device.lightmode; - }, - cat(): boolean { - return this.user.isCat && this.$store.state.settings.circleIcons; - }, - style(): any { - return { - borderRadius: this.$store.state.settings.circleIcons ? '100%' : null - }; - }, - url(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.user.avatarUrl) - : this.user.avatarUrl; - }, - icon(): any { - return { - backgroundColor: this.user.avatarColor, - backgroundImage: this.lightmode ? null : `url(${this.url})`, - borderRadius: this.$store.state.settings.circleIcons ? '100%' : null - }; - } - }, - mounted() { - if (this.user.avatarColor) { - this.$el.style.color = this.user.avatarColor; - } - }, - methods: { - onClick(e) { - this.$emit('click', e); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-avatar - display inline-block - vertical-align bottom - flex-shrink 0 - - &:not(.cat) - overflow hidden - border-radius 8px - - &.cat::before, - &.cat::after - background #df548f - border solid 4px currentColor - box-sizing border-box - content '' - display inline-block - height 50% - width 50% - - &.cat::before - border-radius 0 75% 75% - transform rotate(37.5deg) skew(30deg) - - &.cat::after - border-radius 75% 0 75% 75% - transform rotate(-37.5deg) skew(-30deg) - - .inner - background-position center center - background-size cover - bottom 0 - left 0 - position absolute - right 0 - top 0 - transition border-radius 1s ease - z-index 1 - -</style> diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue deleted file mode 100644 index 19b8c3e974f8d73d84a2d4a08d86b6f2ddb4bde7..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<div class="troubleshooter"> - <div class="body"> - <h1><fa icon="wrench"/>{{ $t('title') }}</h1> - <div> - <p :data-wip="network == null"> - <template v-if="network != null"> - <template v-if="network"><fa icon="check"/></template> - <template v-if="!network"><fa icon="times"/></template> - </template> - {{ network == null ? this.$t('checking-network') : this.$t('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 icon="check"/></template> - <template v-if="!internet"><fa icon="times"/></template> - </template> - {{ internet == null ? this.$t('checking-internet') : this.$t('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 icon="check"/></template> - <template v-if="!server"><fa icon="times"/></template> - </template> - {{ server == null ? this.$t('checking-server') : this.$t('server') }}<mk-ellipsis v-if="server == null"/> - </p> - </div> - <p v-if="!end">{{ $t('finding') }}<mk-ellipsis/></p> - <p v-if="network === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-network') }}</b><br>{{ $t('no-network-desc') }}</p> - <p v-if="internet === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-internet') }}</b><br>{{ $t('no-internet-desc') }}</p> - <p v-if="server === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-server') }}</b><br>{{ $t('no-server-desc') }}</p> - <p v-if="server === true" class="success"><b><fa icon="info-circle"/>{{ $t('success') }}</b><br>{{ $t('success-desc') }}</p> - </div> - <footer> - <a href="/assets/flush.html">{{ $t('flush') }}</a> | <a href="/assets/version.html">{{ $t('set-version') }}</a> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { apiUrl } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/connect-failed.troubleshooter.vue'), - 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 - margin-top 1em - - > .body - width 100% - max-width 500px - margin 0 auto - 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-icon] - 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-icon] - 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-icon] - margin-right 0.25em - - &.success - > b - color #39adad - - &:not(.success) - > b - color #ad4339 - -</style> diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue deleted file mode 100644 index a364304a63c3b91bcb9a379131ec5cd72c58a0ad..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/connect-failed.vue +++ /dev/null @@ -1,105 +0,0 @@ -<template> -<div class="mk-connect-failed"> - <img src="/assets/error.jpg" onerror="this.src='https://raw.githubusercontent.com/syuilo/misskey/develop/src/client/assets/error.jpg';" alt=""/> - <h1>{{ $t('title') }}</h1> - <p class="text"> - <span>{{ this.$t('description').substr(0, this.$t('description').indexOf('{')) }}</span> - <a @click="reload">{{ this.$t('description').match(/\{(.+?)\}/)[1] }}</a> - <span>{{ this.$t('description').substr(this.$t('description').indexOf('}') + 1) }}</span> - </p> - <button v-if="!troubleshooting" @click="troubleshooting = true">{{ $t('troubleshoot') }}</button> - <x-troubleshooter v-if="troubleshooting"/> - <p class="thanks">{{ $t('thanks') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XTroubleshooter from './connect-failed.troubleshooter.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/connect-failed.vue'), - components: { - XTroubleshooter - }, - data() { - return { - troubleshooting: false - }; - }, - mounted() { - document.title = 'Oops!'; - document.documentElement.style.setProperty('background', '#f8f8f8', 'important'); - }, - methods: { - reload() { - location.reload(true); - } - } -}); -</script> - -<style lang="stylus" scoped> - - -.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 var(--primaryForeground) - background var(--primary) - - &:focus - outline solid 3px var(--primaryAlpha03) - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - - > .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/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue deleted file mode 100644 index 098aa021d113677c7c372d883ae3773e2b66f03b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/cw-button.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle"> - <b>{{ value ? this.$t('hide') : this.$t('show') }}</b> - <span v-if="!value">{{ this.label }}</span> -</button> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { length } from 'stringz'; -import { concat } from '../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('common/views/components/cw-button.vue'), - - props: { - value: { - type: Boolean, - required: true - }, - note: { - type: Object, - required: true - } - }, - - computed: { - label(): string { - return concat([ - this.note.text ? [this.$t('chars', { count: length(this.note.text) })] : [], - this.note.files && this.note.files.length !== 0 ? [this.$t('files', { count: this.note.files.length }) ] : [], - this.note.poll != null ? [this.$t('poll')] : [] - ] as string[][]).join(' / '); - } - }, - - methods: { - length, - - toggle() { - this.$emit('input', !this.value); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nrvgflfuaxwgkxoynpnumyookecqrrvh - display inline-block - padding 4px 8px - font-size 0.7em - color var(--cwButtonFg) - background var(--cwButtonBg) - border-radius 2px - cursor pointer - user-select none - - &:hover - background var(--cwButtonHoverBg) - - > span - margin-left 4px - - &:before - content '(' - &:after - content ')' - -</style> diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue deleted file mode 100644 index 27449030075fa01f4d5508066a4d70fb07b859ce..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/dialog.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<ui-modal - ref="modal" - class="modal" - :class="{ splash }" - :close-anime-duration="300" - :close-on-bg-click="false" - @bg-click="onBgClick" - @before-close="onBeforeClose"> - <div class="main" ref="main" :class="{ round: $store.state.device.roundedCorners }"> - <template v-if="type == 'signin'"> - <mk-signin/> - </template> - <template v-else> - <div class="icon" v-if="icon"> - <fa :icon="icon"/> - </div> - <div class="icon" v-else-if="!input && !select && !user" :class="type"> - <fa icon="check" v-if="type === 'success'"/> - <fa :icon="faTimesCircle" v-if="type === 'error'"/> - <fa icon="exclamation-triangle" v-if="type === 'warning'"/> - <fa icon="info-circle" v-if="type === 'info'"/> - <fa :icon="faQuestionCircle" v-if="type === 'question'"/> - <fa icon="spinner" pulse v-if="type === 'waiting'"/> - </div> - <header v-if="title" v-html="title"></header> - <header v-if="title == null && user">{{ $t('@.enter-username') }}</header> - <div class="body" v-if="text" v-html="text"></div> - <ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input> - <ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input> - <ui-select v-if="select" v-model="selectedValue" autofocus> - <template v-if="select.items"> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> - </template> - <template v-else> - <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label"> - <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> - </optgroup> - </template> - </ui-select> - <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)"> - <ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button> - <ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button> - </ui-horizon-group> - </template> - </div> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; -import parseAcct from "../../../../../misc/acct/parse"; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - type: { - type: String, - required: false, - default: 'info' - }, - title: { - type: String, - required: false - }, - text: { - type: String, - required: false - }, - input: { - required: false - }, - select: { - required: false - }, - user: { - required: false - }, - icon: { - required: false - }, - showOkButton: { - type: Boolean, - default: true - }, - showCancelButton: { - type: Boolean, - default: false - }, - cancelableByBgClick: { - type: Boolean, - default: true - }, - splash: { - type: Boolean, - default: false - } - }, - - data() { - return { - inputValue: this.input && this.input.default ? this.input.default : null, - userInputValue: null, - selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null, - canOk: true, - faTimesCircle, faQuestionCircle - }; - }, - - watch: { - userInputValue() { - if (this.user) { - this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => { - this.canOk = u != null; - }).catch(() => { - this.canOk = false; - }); - } - } - }, - - mounted() { - if (this.user) this.canOk = false; - - this.$nextTick(() => { - anime({ - targets: this.$refs.main, - opacity: 1, - scale: [1.2, 1], - duration: 300, - easing: 'cubicBezier(0, 0.5, 0.5, 1)' - }); - - if (this.splash) { - setTimeout(() => { - this.close(); - }, 1000); - } - }); - }, - - methods: { - async ok() { - if (!this.canOk) return; - if (!this.showOkButton) return; - - if (this.user) { - const user = await this.$root.api('users/show', parseAcct(this.userInputValue)); - if (user) { - this.$emit('ok', user); - this.close(); - } - } else { - const result = - this.input ? this.inputValue : - this.select ? this.selectedValue : - true; - this.$emit('ok', result); - this.close(); - } - }, - - cancel() { - this.$emit('cancel'); - this.close(); - }, - - onBgClick() { - if (this.cancelableByBgClick) this.cancel(); - } - - close() { - this.$refs.modal.close(); - }, - - onBeforeClose() { - this.$el.style.pointerEvents = 'none'; - (this.$refs.main as any).style.pointerEvents = 'none'; - - anime({ - targets: this.$refs.main, - opacity: 0, - scale: 0.8, - duration: 300, - easing: 'cubicBezier(0, 0.5, 0.5, 1)', - }); - }, - - onInputKeydown(e) { - if (e.which == 13) { // Enter - e.preventDefault(); - e.stopPropagation(); - this.ok(); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.modal - display flex - align-items center - justify-content center - - &.splash - > .main - min-width 0 - width initial - -.main - display block - position fixed - margin auto - padding 32px - min-width 320px - max-width 480px - width calc(100% - 32px) - text-align center - background var(--face) - color var(--faceText) - opacity 0 - - &.round - border-radius 8px - - > .icon - font-size 32px - - &.success - color #85da5a - - &.error - color #ec4137 - - &.warning - color #ecb637 - - > * - display block - margin 0 auto - - & + header - margin-top 16px - - > header - margin 0 0 8px 0 - font-weight bold - font-size 20px - - & + .body - margin-top 8px - - > .body - margin 16px 0 0 0 - - > .buttons - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/dummy.vue b/src/client/app/common/views/components/dummy.vue deleted file mode 100644 index 5634efc509b15d7c286b950d81291460407d17ae..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/dummy.vue +++ /dev/null @@ -1,11 +0,0 @@ -<template> -<div> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ -}); -</script> diff --git a/src/client/app/common/views/components/ellipsis.vue b/src/client/app/common/views/components/ellipsis.vue deleted file mode 100644 index 07349902dee4de47418032a114eec0487a6d6717..0000000000000000000000000000000000000000 --- a/src/client/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/client/app/common/views/components/emoji-picker.vue b/src/client/app/common/views/components/emoji-picker.vue deleted file mode 100644 index abae69e28ab2f2f86ee5bc93c27d2de7812c9afe..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/emoji-picker.vue +++ /dev/null @@ -1,243 +0,0 @@ -<template> -<div class="prlncendiewqqkrevzeruhndoakghvtx"> - <header> - <button v-for="category in categories" - :title="category.text" - @click="go(category)" - :class="{ active: category.isActive }" - :key="category.text" - > - <fa :icon="category.icon" fixed-width/> - </button> - </header> - <div class="emojis"> - <template v-if="categories[0].isActive"> - <header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recent-emoji') }}</header> - <div class="list"> - <button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])" - :title="emoji.name" - @click="chosen(emoji)" - :key="i" - > - <mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/> - <img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> - </button> - </div> - </template> - - <header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header> - <template v-if="categories.find(x => x.isActive).name"> - <div class="list"> - <button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)" - :title="emoji.name" - @click="chosen(emoji)" - :key="emoji.name" - > - <mk-emoji :emoji="emoji.char"/> - </button> - </div> - </template> - <template v-else> - <div v-for="(key, i) in Object.keys(customEmojis)" :key="i"> - <header class="sub">{{ key || $t('no-category') }}</header> - <div class="list"> - <button v-for="emoji in customEmojis[key]" - :title="emoji.name" - @click="chosen(emoji)" - :key="emoji.name" - > - <img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> - </button> - </div> - </div> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { emojilist } from '../../../../../misc/emojilist'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; -import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory } from '@fortawesome/free-solid-svg-icons'; -import { faHeart, faFlag } from '@fortawesome/free-regular-svg-icons'; -import { groupByX } from '../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('common/views/components/emoji-picker.vue'), - - data() { - return { - emojilist, - getStaticImageUrl, - customEmojis: {}, - faGlobe, faHistory, - categories: [{ - text: this.$t('custom-emoji'), - icon: faAsterisk, - isActive: true - }, { - name: 'people', - text: this.$t('people'), - icon: ['far', 'laugh'], - isActive: false - }, { - name: 'animals_and_nature', - text: this.$t('animals-and-nature'), - icon: faLeaf, - isActive: false - }, { - name: 'food_and_drink', - text: this.$t('food-and-drink'), - icon: faUtensils, - isActive: false - }, { - name: 'activity', - text: this.$t('activity'), - icon: faFutbol, - isActive: false - }, { - name: 'travel_and_places', - text: this.$t('travel-and-places'), - icon: faCity, - isActive: false - }, { - name: 'objects', - text: this.$t('objects'), - icon: faDice, - isActive: false - }, { - name: 'symbols', - text: this.$t('symbols'), - icon: faHeart, - isActive: false - }, { - name: 'flags', - text: this.$t('flags'), - icon: faFlag, - isActive: false - }] - } - }, - - created() { - let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || []; - local = groupByX(local, (x: any) => x.category || ''); - this.customEmojis = local; - - if (this.$store.state.device.activeEmojiCategoryName) { - this.goCategory(this.$store.state.device.activeEmojiCategoryName); - } - }, - - methods: { - go(category: any) { - this.goCategory(category.name); - }, - - goCategory(name: string) { - let matched = false; - for (const c of this.categories) { - c.isActive = c.name === name; - if (c.isActive) { - matched = true; - this.$store.commit('device/set', { key: 'activeEmojiCategoryName', value: c.name }); - } - } - if (!matched) { - this.categories[0].isActive = true; - } - }, - - chosen(emoji: any) { - const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`; - - let recents = this.$store.state.device.recentEmojis || []; - recents = recents.filter((e: any) => getKey(e) !== getKey(emoji)); - recents.unshift(emoji) - this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) }); - - this.$emit('chosen', getKey(emoji)); - } - } -}); -</script> - -<style lang="stylus" scoped> -.prlncendiewqqkrevzeruhndoakghvtx - width 350px - background var(--face) - - > header - display flex - - > button - flex 1 - padding 10px 0 - font-size 16px - color var(--text) - transition color 0.2s ease - - &:hover - color var(--textHighlighted) - transition color 0s - - &.active - color var(--primary) - transition color 0s - - > .emojis - height 300px - overflow-y auto - overflow-x hidden - - > header.category - position sticky - top 0 - left 0 - z-index 1 - padding 8px - background var(--faceHeader) - color var(--text) - font-size 12px - - >>> header.sub - padding 4px 8px - color var(--text) - font-size 12px - - >>> div.list - display grid - grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr - gap 4px - padding 8px - - > button - padding 0 - width 100% - - &:before - content '' - display block - width 1px - height 0 - padding-bottom 100% - - &:hover - > * - transform scale(1.2) - transition transform 0s - - > * - position absolute - top 0 - left 0 - width 100% - height 100% - object-fit contain - font-size 28px - transition transform 0.2s ease - pointer-events none - -</style> diff --git a/src/client/app/common/views/components/error.vue b/src/client/app/common/views/components/error.vue deleted file mode 100644 index 0462a6efda4cb4d169d30772fdcfe1dfb9701bad..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/error.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<div class="wjqjnyhzogztorhrdgcpqlkxhkmuetgj"> - <p><fa icon="exclamation-triangle"/> {{ $t('@.error.title') }}</p> - <ui-button @click="() => $emit('retry')">{{ $t('@.error.retry') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n() -}); -</script> - -<style lang="stylus" scoped> -.wjqjnyhzogztorhrdgcpqlkxhkmuetgj - max-width 350px - margin 0 auto - padding 32px - text-align center - color var(--text) - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/common/views/components/file-type-icon.vue b/src/client/app/common/views/components/file-type-icon.vue deleted file mode 100644 index 3a9fe768d1769239512136bf95116bb74a2854c0..0000000000000000000000000000000000000000 --- a/src/client/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 icon="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/client/app/common/views/components/follow-button.vue b/src/client/app/common/views/components/follow-button.vue deleted file mode 100644 index 074a0c05b6ae04889e368e972bc774dee40730e4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/follow-button.vue +++ /dev/null @@ -1,209 +0,0 @@ -<template> -<button class="wfliddvnhxvyusikowhxozkyxyenqxqr" - :class="{ wait, block, inline, mini, transparent, active: isFollowing || hasPendingFollowRequestFromYou }" - @click="onClick" - :disabled="wait" - :inline="inline" -> - <template v-if="!wait"> - <fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template> - </template> - <template v-else><fa icon="spinner" pulse fixed-width/></template> -</button> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/follow-button.vue'), - - props: { - user: { - type: Object, - required: true - }, - block: { - type: Boolean, - required: false, - default: false - }, - inline: { - type: Boolean, - required: false, - default: false - }, - mini: { - type: Boolean, - required: false, - default: false - }, - transparent: { - type: Boolean, - required: false, - default: true - }, - }, - - data() { - return { - isFollowing: this.user.isFollowing, - hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou, - wait: false, - connection: null - }; - }, - - computed: { - iconAndText(): any[] { - return ( - (this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] : - (this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['spinner', this.$t('follow-processing')] : - (this.isFollowing) ? ['minus', this.$t('following')] : - (!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] : - (!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] : - [] - ); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('follow', this.onFollowChange); - this.connection.on('unfollow', this.onFollowChange); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onFollowChange(user) { - if (user.id == this.user.id) { - this.isFollowing = user.isFollowing; - this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; - } - }, - - async onClick() { - this.wait = true; - - try { - if (this.isFollowing) { - const { canceled } = await this.$root.dialog({ - type: 'warning', - text: this.$t('@.unfollow-confirm', { name: this.user.name || this.user.username }), - showCancelButton: true - }); - - if (canceled) return; - - await this.$root.api('following/delete', { - userId: this.user.id - }); - } else { - if (this.hasPendingFollowRequestFromYou) { - await this.$root.api('following/requests/cancel', { - userId: this.user.id - }); - } else if (this.user.isLocked) { - await this.$root.api('following/create', { - userId: this.user.id - }); - this.hasPendingFollowRequestFromYou = true; - } else { - await this.$root.api('following/create', { - userId: this.user.id - }); - this.hasPendingFollowRequestFromYou = true; - } - } - } catch (e) { - console.error(e); - } finally { - this.wait = false; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.wfliddvnhxvyusikowhxozkyxyenqxqr - display block - user-select none - cursor pointer - padding 0 16px - margin 0 - min-width 100px - line-height 36px - font-size 14px - font-weight bold - color var(--primary) - background transparent - outline none - border solid 1px var(--primary) - border-radius 36px - - &:not(.transparent) - background #fff - - &.inline - display inline-block - - &.mini - padding 0 - min-width 0 - width 32px - height 32px - font-size 16px - border-radius 4px - line-height 32px - - &:focus - &:after - border-radius 8px - - &.block - width 100% - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 36px - - &:hover - background var(--primaryAlpha01) - - &:active - background var(--primaryAlpha02) - - &.active - color var(--primaryForeground) - background var(--primary) - - &:hover - background var(--primaryLighten10) - border-color var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - border-color var(--primaryDarken10) - - &.wait - cursor wait !important - opacity 0.7 - - * - pointer-events none - -</style> diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue deleted file mode 100644 index 328e3ca7b09e125ac26030165bc4abd258e84596..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/forkit.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<a class="a" :href="repositoryUrl" rel="noopener" target="_blank" title="View source on GitHub"> - <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> - -<script lang="ts"> -import Vue from 'vue' -export default Vue.extend({ - data() { - return { - repositoryUrl: 'https://github.com/syuilo/misskey' - }; - } -}); -</script> - -<style lang="stylus" scoped> -.a - display block - - > svg - display block - //fill #151513 - //color #fff - fill var(--primary) - color var(--primaryForeground) - - .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/client/app/common/views/components/frac.vue b/src/client/app/common/views/components/frac.vue deleted file mode 100644 index 1840bd28fe7af736f0adca5005f391a87e4e1a29..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/frac.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<span class="mk-frac"><span>{{ pad }}</span><span>{{ value }} / {{ total }}</span></span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - value: { - type: Number, - required: true, - }, - total: { - type: Number, - required: true, - }, - }, - computed: { - pad(this: { - value: number; - total: number; - length(value: number): number; - }) { - return '0'.repeat(this.length(this.total) - this.length(this.value)); - }, - }, - methods: { - length(value: number) { - const string = value.toString(); - - return string.includes('e') ? -~string.substr(string.indexOf('e')) : string.length; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-frac - -webkit-font-feature-settings 'tnum' - -moz-font-feature-settings 'tnum' - font-feature-settings 'tnum' - font-variant-numeric tabular-nums - - > :first-child - visibility hidden -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue deleted file mode 100644 index a7c918aa7188c7894ece12700cb561e46fe14352..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ /dev/null @@ -1,473 +0,0 @@ -<template> -<div class="xqnhankfuuilcwvhgsopeqncafzsquya"> - <button class="go-index" v-if="selfNav" @click="goIndex"><fa icon="arrow-left"/></button> - <header><b><router-link :to="blackUser | userPage"><mk-user-name :user="blackUser"/></router-link></b>({{ $t('@.reversi.black') }}) vs <b><router-link :to="whiteUser | userPage"><mk-user-name :user="whiteUser"/></router-link></b>({{ $t('@.reversi.white') }})</header> - - <div style="overflow: hidden; line-height: 28px;"> - <p class="turn" v-if="!iAmPlayer && !game.isEnded"> - <mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/> - <mk-ellipsis/> - </p> - <p class="turn" v-if="logPos != logs.length"> - <mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/> - </p> - <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p> - <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p> - <p class="result" v-if="game.isEnded && logPos == logs.length"> - <template v-if="game.winner"> - <mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :plain="true" :custom-emojis="game.winner.emojis"/> - <span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span> - </template> - <template v-else>{{ $t('@.reversi.drawn') }}</template> - </p> - </div> - - <div class="board"> - <div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - <div class="flex"> - <div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - <div class="cells" :style="cellsStyle"> - <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="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> - <template v-if="$store.state.settings.gamesReversiUseAvatarStones"> - <img v-if="stone === true" :src="blackUser.avatarUrl" alt="black"> - <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white"> - </template> - <template v-else> - <fa v-if="stone === true" :icon="fasCircle"/> - <fa v-if="stone === false" :icon="farCircle"/> - </template> - </div> - </div> - <div class="labels-y" v-if="this.$store.state.settings.gamesReversiShowBoardLabels"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - </div> - <div class="labels-x" v-if="this.$store.state.settings.gamesReversiShowBoardLabels"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - </div> - - <p class="status"><b>{{ $t('@.reversi.this-turn', { count: logPos }) }}</b> {{ $t('@.reversi.black') }}:{{ o.blackCount }} {{ $t('@.reversi.white') }}:{{ o.whiteCount }} {{ $t('@.reversi.total') }}:{{ o.blackCount + o.whiteCount }}</p> - - <div class="actions" v-if="!game.isEnded && iAmPlayer"> - <form-button @click="surrender">{{ $t('surrender') }}</form-button> - </div> - - <div class="player" v-if="game.isEnded"> - <span>{{ logPos }} / {{ logs.length }}</span> - <ui-horizon-group> - <ui-button @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></ui-button> - <ui-button @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></ui-button> - <ui-button @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></ui-button> - <ui-button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></ui-button> - </ui-horizon-group> - </div> - - <div class="info"> - <p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p> - <p v-if="game.loopedBoard">{{ $t('looped-map') }}</p> - <p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import * as CRC32 from 'crc-32'; -import Reversi, { Color } from '../../../../../../../games/reversi/core'; -import { url } from '../../../../../config'; -import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'; -import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons'; -import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.game.vue'), - props: { - initGame: { - type: Object, - require: true - }, - connection: { - type: Object, - require: true - }, - selfNav: { - type: Boolean, - require: true - } - }, - - data() { - return { - game: null, - o: null as Reversi, - logs: [], - logPos: 0, - pollingClock: null, - faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle - }; - }, - - computed: { - iAmPlayer(): boolean { - if (!this.$store.getters.isSignedIn) return false; - return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id; - }, - - myColor(): Color { - if (!this.iAmPlayer) return null; - if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true; - if (this.game.user2Id == this.$store.state.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.iAmPlayer) return false; - if (this.turnUser == null) return false; - return this.turnUser.id == this.$store.state.i.id; - }, - - cellsStyle(): any { - return { - 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`, - 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)` - }; - } - }, - - watch: { - logPos(v) { - if (!this.game.isEnded) return; - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - for (const log of this.logs.slice(0, v)) { - this.o.put(log.color, log.pos); - } - this.$forceUpdate(); - } - }, - - created() { - this.game = this.initGame; - - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - - for (const log of this.game.logs) { - 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(() => { - if (this.game.isEnded) return; - const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join('')); - this.connection.send('check', { - crc32: crc32 - }); - }, 3000); - } - }, - - mounted() { - this.connection.on('set', this.onSet); - this.connection.on('rescue', this.onRescue); - this.connection.on('ended', this.onEnded); - }, - - beforeDestroy() { - this.connection.off('set', this.onSet); - this.connection.off('rescue', this.onRescue); - this.connection.off('ended', this.onEnded); - - 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.$store.state.device.enableSounds) { - const sound = new Audio(`${url}/assets/reversi-put-me.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - } - - this.connection.send('set', { - pos: 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.$store.state.device.enableSounds && x.color != this.myColor) { - const sound = new Audio(`${url}/assets/reversi-put-you.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - } - }, - - onEnded(x) { - this.game = x.game; - }, - - 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 Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - - for (const log of this.game.logs) { - this.o.put(log.color, log.pos, true); - } - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - this.checkEnd(); - this.$forceUpdate(); - }, - - surrender() { - this.$root.api('games/reversi/games/surrender', { - gameId: this.game.id - }); - }, - - goIndex() { - this.$emit('go-index'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.xqnhankfuuilcwvhgsopeqncafzsquya - text-align center - - > .go-index - position absolute - top 0 - left 0 - z-index 1 - width 42px - height 42px - - > header - padding 8px - border-bottom dashed 1px var(--reversiGameHeaderLine) - - a - color inherit - - > .board - width calc(100% - 16px) - max-width 500px - margin 0 auto - - $label-size = 16px - $gap = 4px - - > .labels-x - height $label-size - padding 0 $label-size - display flex - - > * - flex 1 - display flex - align-items center - justify-content center - font-size 12px - - &:first-child - margin-left -($gap / 2) - - &:last-child - margin-right -($gap / 2) - - > .flex - display flex - - > .labels-y - width $label-size - display flex - flex-direction column - - > * - flex 1 - display flex - align-items center - justify-content center - font-size 12px - - &:first-child - margin-top -($gap / 2) - - &:last-child - margin-bottom -($gap / 2) - - > .cells - flex 1 - display grid - grid-gap $gap - - > div - background transparent - border-radius 6px - overflow hidden - - * - pointer-events none - user-select none - - &.empty - border solid 2px var(--reversiGameEmptyCell) - - &.empty.can - background var(--reversiGameEmptyCell) - - &.empty.myTurn - border-color var(--reversiGameEmptyCellMyTurn) - - &.can - background var(--reversiGameEmptyCellCanPut) - cursor pointer - - &:hover - border-color var(--primaryDarken10) - background var(--primary) - - &:active - background var(--primaryDarken10) - - &.prev - box-shadow 0 0 0 4px var(--primaryAlpha07) - - &.isEnded - border-color var(--reversiGameEmptyCellMyTurn) - - &.none - border-color transparent !important - - > svg - display block - width 100% - height 100% - - > 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 - - > .actions - padding-bottom 16px - - > .player - padding 0 16px 32px 16px - margin 0 auto - max-width 500px - - > span - display inline-block - margin 0 8px - min-width 70px - -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue deleted file mode 100644 index 40993895024a39b061a0e50a6da03aa029a72cf4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div> - <x-room v-if="!g.isStarted" :game="g" :connection="connection"/> - <x-game v-else :init-game="g" :connection="connection" :self-nav="selfNav" @go-index="goIndex"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import XGame from './reversi.game.vue'; -import XRoom from './reversi.room.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.gameroom.vue'), - components: { - XGame, - XRoom - }, - props: { - game: { - type: Object, - required: true - }, - selfNav: { - type: Boolean, - require: true - } - }, - data() { - return { - connection: null, - g: null - }; - }, - created() { - this.g = this.game; - this.connection = this.$root.stream.connectToChannel('gamesReversiGame', { - gameId: this.game.id - }); - this.connection.on('started', this.onStarted); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - onStarted(game) { - Object.assign(this.g, game); - this.$forceUpdate(); - }, - goIndex() { - this.$emit('go-index'); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue deleted file mode 100644 index 94e1d9a7e3970cace4915d6694e5528a00b308a6..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ /dev/null @@ -1,245 +0,0 @@ -<template> -<div class="phgnkghfpyvkrvwiajkiuoxyrdaqpzcx"> - <h1>{{ $t('title') }}</h1> - <p>{{ $t('sub-title') }}</p> - <div class="play"> - <form-button primary round @click="match">{{ $t('invite') }}</form-button> - <details> - <summary>{{ $t('rule') }}</summary> - <div> - <p>{{ $t('rule-desc') }}</p> - <dl> - <dt><b>{{ $t('mode-invite') }}</b></dt> - <dd>{{ $t('mode-invite-desc') }}</dd> - </dl> - </div> - </details> - </div> - <section v-if="invitations.length > 0"> - <h2>{{ $t('invitations') }}</h2> - <div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)"> - <mk-avatar class="avatar" :user="i.parent"/> - <span class="name"><b><mk-user-name :user="i.parent"/></b></span> - <span class="username">@{{ i.parent.username }}</span> - <mk-time :time="i.createdAt"/> - </div> - </section> - <section v-if="myGames.length > 0"> - <h2>{{ $t('my-games') }}</h2> - <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/games/reversi/${g.id}`"> - <mk-avatar class="avatar" :user="g.user1"/> - <mk-avatar class="avatar" :user="g.user2"/> - <span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span> - <span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span> - <mk-time :time="g.createdAt" /> - </a> - </section> - <section v-if="games.length > 0"> - <h2>{{ $t('all-games') }}</h2> - <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/games/reversi/${g.id}`"> - <mk-avatar class="avatar" :user="g.user1"/> - <mk-avatar class="avatar" :user="g.user2"/> - <span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span> - <span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span> - <mk-time :time="g.createdAt" /> - </a> - </section> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.index.vue'), - data() { - return { - games: [], - gamesFetching: true, - gamesMoreFetching: false, - myGames: [], - matching: null, - invitations: [], - connection: null - }; - }, - - mounted() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('gamesReversi'); - - this.connection.on('invited', this.onInvited); - - this.$root.api('games/reversi/games', { - my: true - }).then(games => { - this.myGames = games; - }); - - this.$root.api('games/reversi/invitations').then(invitations => { - this.invitations = this.invitations.concat(invitations); - }); - } - - this.$root.api('games/reversi/games').then(games => { - this.games = games; - this.gamesFetching = false; - }); - }, - - beforeDestroy() { - if (this.connection) { - this.connection.dispose(); - } - }, - - methods: { - go(game) { - this.$emit('go', game); - }, - - async match() { - const { result: user } = await this.$root.dialog({ - title: this.$t('enter-username'), - user: { - local: true - } - }); - if (user == null) return; - this.$root.api('games/reversi/match', { - userId: user.id - }).then(res => { - if (res == null) { - this.$emit('matching', user); - } else { - this.$emit('go', res); - } - }); - }, - - accept(invitation) { - this.$root.api('games/reversi/match', { - userId: invitation.parent.id - }).then(game => { - if (game) { - this.$emit('go', game); - } - }); - }, - - onInvited(invite) { - this.invitations.unshift(invite); - } - } -}); -</script> - -<style lang="stylus" scoped> -.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx - > h1 - margin 0 - padding 24px - font-size 24px - text-align center - font-weight normal - color #fff - background linear-gradient(to bottom, var(--reversiBannerGradientStart), var(--reversiBannerGradientEnd)) - - & + p - margin 0 - padding 12px - margin-bottom 12px - text-align center - font-size 14px - border-bottom solid 1px var(--faceDivider) - - > .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 var(--reversiDescBg) - border-radius 8px - - > section - margin 0 auto - padding 0 16px 16px 16px - max-width 500px - border-top solid 1px var(--faceDivider) - - > h2 - margin 0 - padding 16px 0 8px 0 - font-size 16px - font-weight bold - - .invitation - margin 8px 0 - padding 8px - color var(--text) - background var(--face) - box-shadow 0 2px 16px var(--reversiListItemShadow) - border-radius 6px - cursor pointer - - * - pointer-events none - user-select none - - &:focus - border-color var(--primary) - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .avatar - width 32px - height 32px - border-radius 100% - - > span - margin 0 8px - line-height 32px - - .game - display block - margin 8px 0 - padding 8px - color var(--text) - background var(--face) - box-shadow 0 2px 16px var(--reversiListItemShadow) - border-radius 6px - cursor pointer - - * - pointer-events none - user-select none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .avatar - width 32px - height 32px - border-radius 100% - - > span - margin 0 8px - line-height 32px - -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue deleted file mode 100644 index c1657f49e5a905c29ff103fa36a73aaa706004bb..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ /dev/null @@ -1,355 +0,0 @@ -<template> -<div class="urbixznjwwuukfsckrwzwsqzsxornqij"> - <header><b><mk-user-name :user="game.user1"/></b> vs <b><mk-user-name :user="game.user2"/></b></header> - - <div> - <p>{{ $t('settings-of-the-game') }}</p> - - <div class="card map"> - <header> - <select v-model="mapName" :placeholder="$t('choose-map')" @change="onMapChange"> - <option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/> - <option :label="$t('random')" :value="null"/> - <optgroup v-for="c in mapCategories" :key="c" :label="c"> - <option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option> - </optgroup> - </select> - </header> - - <div> - <div class="random" v-if="game.map == null"><fa icon="dice"/></div> - <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.map.join('')" - :data-none="x == ' '" - @click="onPixelClick(i, x)"> - <fa v-if="x == 'b'" :icon="fasCircle"/> - <fa v-if="x == 'w'" :icon="farCircle"/> - </div> - </div> - </div> - </div> - - <div class="card"> - <header> - <span>{{ $t('black-or-white') }}</span> - </header> - - <div> - <form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio> - <form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> - <form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> - </div> - </div> - - <div class="card"> - <header> - <span>{{ $t('rules') }}</span> - </header> - - <div> - <ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch> - <ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch> - <ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch> - </div> - </div> - - <div class="card form" v-if="form"> - <header> - <span>{{ $t('settings-of-the-bot') }}</span> - </header> - - <div> - <template v-for="item in form"> - <ui-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</ui-switch> - - <div class="card" v-if="item.type == 'radio'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <form-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @change="onChangeForm(item)">{{ r.label }}</form-radio> - </div> - </div> - - <div class="card" v-if="item.type == 'slider'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/> - </div> - </div> - - <div class="card" v-if="item.type == 'textbox'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input v-model="item.value" @change="onChangeForm(item)"/> - </div> - </div> - </template> - </div> - </div> - </div> - - <footer> - <p class="status"> - <template v-if="isAccepted && isOpAccepted">{{ $t('this-game-is-started-soon') }}<mk-ellipsis/></template> - <template v-if="isAccepted && !isOpAccepted">{{ $t('waiting-for-other') }}<mk-ellipsis/></template> - <template v-if="!isAccepted && isOpAccepted">{{ $t('waiting-for-me') }}</template> - <template v-if="!isAccepted && !isOpAccepted">{{ $t('waiting-for-both') }}<mk-ellipsis/></template> - </p> - - <div class="actions"> - <form-button @click="exit">{{ $t('cancel') }}</form-button> - <form-button primary @click="accept" v-if="!isAccepted">{{ $t('ready') }}</form-button> - <form-button primary @click="cancel" v-if="isAccepted">{{ $t('cancel-ready') }}</form-button> - </div> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import * as maps from '../../../../../../../games/reversi/maps'; -import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons'; -import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.room.vue'), - props: ['game', 'connection'], - - data() { - return { - o: null, - isLlotheo: false, - mapName: maps.eighteight.name, - maps: maps, - form: null, - messages: [], - fasCircle, farCircle - }; - }, - - computed: { - mapCategories(): string[] { - const categories = Object.values(maps).map(x => x.category); - return categories.filter((item, pos) => categories.indexOf(item) == pos); - }, - isAccepted(): boolean { - if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true; - if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true; - return false; - }, - isOpAccepted(): boolean { - if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true; - if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true; - return false; - } - }, - - created() { - this.connection.on('changeAccepts', this.onChangeAccepts); - this.connection.on('updateSettings', this.onUpdateSettings); - this.connection.on('initForm', this.onInitForm); - this.connection.on('message', this.onMessage); - - if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1; - if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2; - }, - - beforeDestroy() { - this.connection.off('changeAccepts', this.onChangeAccepts); - this.connection.off('updateSettings', this.onUpdateSettings); - this.connection.off('initForm', this.onInitForm); - this.connection.off('message', this.onMessage); - }, - - methods: { - exit() { - - }, - - accept() { - this.connection.send('accept', {}); - }, - - cancel() { - this.connection.send('cancelAccept', {}); - }, - - onChangeAccepts(accepts) { - this.game.user1Accepted = accepts.user1; - this.game.user2Accepted = accepts.user2; - this.$forceUpdate(); - }, - - updateSettings(key: string) { - this.connection.send('updateSettings', { - key: key, - value: this.game[key] - }); - }, - - onUpdateSettings({ key, value }) { - this.game[key] = value; - if (this.game.map == null) { - this.mapName = null; - } else { - const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join('')); - this.mapName = found ? found.name : '-Custom-'; - } - }, - - onInitForm(x) { - if (x.userId == this.$store.state.i.id) return; - this.form = x.form; - }, - - onMessage(x) { - if (x.userId == this.$store.state.i.id) return; - this.messages.unshift(x.message); - }, - - onChangeForm(item) { - this.connection.send('updateForm', { - id: item.id, - value: item.value - }); - }, - - onMapChange() { - if (this.mapName == null) { - this.game.map = null; - } else { - this.game.map = Object.values(maps).find(x => x.name == this.mapName).data; - } - this.$forceUpdate(); - this.updateSettings('map'); - }, - - onPixelClick(pos, pixel) { - const x = pos % this.game.map[0].length; - const y = Math.floor(pos / this.game.map[0].length); - const newPixel = - pixel == ' ' ? '-' : - pixel == '-' ? 'b' : - pixel == 'b' ? 'w' : - ' '; - const line = this.game.map[y].split(''); - line[x] = newPixel; - this.$set(this.game.map, y, line.join('')); - this.$forceUpdate(); - this.updateSettings('map'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.urbixznjwwuukfsckrwzwsqzsxornqij - text-align center - background var(--bg) - - > header - padding 8px - border-bottom dashed 1px #c4cdd4 - - > div - padding 0 16px - - > .card - margin 0 auto 16px auto - - &.map - > header - > select - width 100% - padding 12px 14px - background var(--face) - border 1px solid var(--reversiMapSelectBorder) - border-radius 4px - color var(--text) - cursor pointer - transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) - -webkit-appearance none - -moz-appearance none - appearance none - - &:hover - border-color var(--reversiMapSelectHoverBorder) - - &:focus - &:active - border-color var(--primary) - - > div - > .random - padding 32px 0 - font-size 64px - color var(--text) - opacity 0.7 - - > .board - display grid - grid-gap 4px - width 300px - height 300px - margin 0 auto - color var(--text) - - > div - background transparent - border solid 2px var(--faceDivider) - border-radius 6px - overflow hidden - cursor pointer - - * - pointer-events none - user-select none - width 100% - height 100% - - &[data-none] - border-color transparent - - &.form - > div - > .card + .card - margin-top 16px - - input[type='range'] - width 100% - - .card - max-width 400px - border-radius 4px - background var(--face) - color var(--text) - box-shadow 0 2px 12px 0 var(--reversiRoomFormShadow) - - > header - padding 18px 20px - border-bottom 1px solid var(--faceDivider) - - > div - padding 20px - color var(--text) - - > footer - position sticky - bottom 0 - padding 16px - background var(--reversiRoomFooterBg) - border-top solid 1px var(--faceDivider) - - > .status - margin 0 0 16px 0 - -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue deleted file mode 100644 index d33471a04981bbeb1a3fd6178247cc323c468125..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ /dev/null @@ -1,175 +0,0 @@ -<template> -<div class="vchtoekanapleubgzioubdtmlkribzfd"> - <div v-if="game"> - <x-gameroom :game="game" :self-nav="selfNav" @go-index="goIndex"/> - </div> - <div class="matching" v-else-if="matching"> - <h1>{{ this.$t('matching.waiting-for').split('{}')[0] }}<b><mk-user-name :user="matching"/></b>{{ this.$t('matching.waiting-for').split('{}')[1] }}<mk-ellipsis/></h1> - <div class="cancel"> - <form-button round @click="cancel">{{ $t('matching.cancel') }}</form-button> - </div> - </div> - <div v-else-if="gameId"> - ... - </div> - <div class="index" v-else> - <x-index @go="nav" @matching="onMatching"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../../i18n'; -import XGameroom from './reversi.gameroom.vue'; -import XIndex from './reversi.index.vue'; -import Progress from '../../../../scripts/loading'; - -export default Vue.extend({ - i18n: i18n('common/views/components/games/reversi/reversi.vue'), - components: { - XGameroom, - XIndex - }, - - props: { - gameId: { - type: String, - required: false - }, - selfNav: { - type: Boolean, - require: false, - default: true - } - }, - - data() { - return { - game: null, - matching: null, - connection: null, - pingClock: null - }; - }, - - watch: { - game() { - this.$emit('gamed', this.game); - }, - - gameId() { - this.fetch(); - } - }, - - mounted() { - this.fetch(); - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('gamesReversi'); - - this.connection.on('matched', this.onMatched); - - this.pingClock = setInterval(() => { - if (this.matching) { - this.connection.send('ping', { - id: this.matching.id - }); - } - }, 3000); - } - }, - - beforeDestroy() { - if (this.connection) { - this.connection.dispose(); - clearInterval(this.pingClock); - } - }, - - methods: { - fetch() { - if (this.gameId == null) { - this.game = null; - } else { - Progress.start(); - this.$root.api('games/reversi/games/show', { - gameId: this.gameId - }).then(game => { - this.game = game; - Progress.done(); - }); - } - }, - - async nav(game, actualNav = true) { - if (this.selfNav) { - // å—ã‘å–ã£ãŸã‚²ãƒ¼ãƒ æƒ…å ±ãŒçœç•¥ã•ã‚ŒãŸã‚‚ã®ãªã‚‰å®Œå…¨ãªæƒ…å ±ã‚’å–å¾—ã™ã‚‹ - if (game != null && game.map == null) { - game = await this.$root.api('games/reversi/games/show', { - gameId: game.id - }); - } - - this.game = game; - } else { - this.$emit('nav', game, actualNav); - } - }, - - onMatching(user) { - this.matching = user; - }, - - cancel() { - this.matching = null; - this.$root.api('games/reversi/match/cancel'); - }, - - accept(invitation) { - this.$root.api('games/reversi/match', { - userId: invitation.parent.id - }).then(game => { - if (game) { - this.matching = null; - - this.nav(game); - } - }); - }, - - onMatched(game) { - this.matching = null; - this.game = game; - this.nav(game, false); - }, - - goIndex() { - this.nav(null); - } - } -}); -</script> - -<style lang="stylus" scoped> -.vchtoekanapleubgzioubdtmlkribzfd - color var(--text) - background var(--bg) - - > .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 - -</style> diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue deleted file mode 100644 index 1e881473995c238598a9a4adc37797d3af0b5181..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/google.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<div class="mk-google"> - <input type="search" v-model="query" :placeholder="q"> - <button @click="search"><fa icon="search"/> {{ $t('@.search') }}</button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: ['q'], - data() { - return { - query: null - }; - }, - mounted() { - this.query = this.q; - }, - methods: { - search() { - const engine = this.$store.state.settings.webSearchEngine || - 'https://www.google.com/?#q={{query}}'; - const url = engine.replace('{{query}}', this.query) - window.open(url, '_blank'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-google - display flex - margin 8px 0 - - > input - flex-shrink 1 - padding 10px - width 100% - height 40px - font-size 16px - color var(--googleSearchFg) - background var(--googleSearchBg) - border solid 1px var(--googleSearchBorder) - border-radius 4px 0 0 4px - - &:hover - border-color var(--googleSearchHoverBorder) - - > button - flex-shrink 0 - padding 0 16px - border solid 1px var(--googleSearchBorder) - border-left none - border-radius 0 4px 4px 0 - - &:hover - background-color var(--googleSearchHoverButton) - - &:active - box-shadow 0 2px 4px rgba(#000, 0.15) inset - -</style> diff --git a/src/client/app/common/views/components/image-viewer.vue b/src/client/app/common/views/components/image-viewer.vue deleted file mode 100644 index 63b5e28d000822a3e95b3b7f7595003b9ebbb8cd..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/image-viewer.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<ui-modal ref="modal" v-hotkey.global="keymap"> - <img :src="image.url" :alt="image.name" :title="image.name" @click="close" /> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['image'], - computed: { - keymap(): any { - return { - 'esc': this.close, - }; - } - }, - methods: { - close() { - (this.$refs.modal as any).close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -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 - image-orientation from-image - -</style> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts deleted file mode 100644 index 88cd4931d42359a42341d2252c07cd6bbbf23de2..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import Vue from 'vue'; - -import dummy from './dummy.vue'; -import userName from './user-name.vue'; -import followButton from './follow-button.vue'; -import error from './error.vue'; -import noteSkeleton from './note-skeleton.vue'; -import instance from './instance.vue'; -import cwButton from './cw-button.vue'; -import tagCloud from './tag-cloud.vue'; -import trends from './trends.vue'; -import analogClock from './analog-clock.vue'; -import menu from './menu.vue'; -import noteHeader from './note-header.vue'; -import renote from './renote.vue'; -import signin from './signin.vue'; -import signup from './signup.vue'; -import forkit from './forkit.vue'; -import acct from './acct.vue'; -import avatar from './avatar.vue'; -import nav from './nav.vue'; -import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue'; -import poll from './poll.vue'; -import reactionIcon from './reaction-icon.vue'; -import reactionsViewer from './reactions-viewer.vue'; -import time from './time.vue'; -import mediaList from './media-list.vue'; -import uploader from './uploader.vue'; -import streamIndicator from './stream-indicator.vue'; -import ellipsis from './ellipsis.vue'; -import urlPreview from './url-preview.vue'; -import fileTypeIcon from './file-type-icon.vue'; -import emoji from './emoji.vue'; -import welcomeTimeline from './welcome-timeline.vue'; -import userList from './user-list.vue'; -import frac from './frac.vue'; -import uiInput from './ui/input.vue'; -import uiButton from './ui/button.vue'; -import uiHorizonGroup from './ui/horizon-group.vue'; -import uiCard from './ui/card.vue'; -import uiForm from './ui/form.vue'; -import uiTextarea from './ui/textarea.vue'; -import uiSwitch from './ui/switch.vue'; -import uiRadio from './ui/radio.vue'; -import uiSelect from './ui/select.vue'; -import uiInfo from './ui/info.vue'; -import uiMargin from './ui/margin.vue'; -import uiHr from './ui/hr.vue'; -import uiPagination from './ui/pagination.vue'; -import uiModal from './ui/modal.vue'; -import formButton from './ui/form/button.vue'; -import formRadio from './ui/form/radio.vue'; - -Vue.component('mfm', misskeyFlavoredMarkdown); -Vue.component('mk-dummy', dummy); -Vue.component('mk-user-name', userName); -Vue.component('mk-follow-button', followButton); -Vue.component('mk-error', error); -Vue.component('mk-note-skeleton', noteSkeleton); -Vue.component('mk-instance', instance); -Vue.component('mk-cw-button', cwButton); -Vue.component('mk-tag-cloud', tagCloud); -Vue.component('mk-trends', trends); -Vue.component('mk-analog-clock', analogClock); -Vue.component('mk-menu', menu); -Vue.component('mk-note-header', noteHeader); -Vue.component('mk-renote', renote); -Vue.component('mk-signin', signin); -Vue.component('mk-signup', signup); -Vue.component('mk-forkit', forkit); -Vue.component('mk-acct', acct); -Vue.component('mk-avatar', avatar); -Vue.component('mk-nav', nav); -Vue.component('mk-poll', poll); -Vue.component('mk-reaction-icon', reactionIcon); -Vue.component('mk-reactions-viewer', reactionsViewer); -Vue.component('mk-time', time); -Vue.component('mk-media-list', mediaList); -Vue.component('mk-uploader', uploader); -Vue.component('mk-stream-indicator', streamIndicator); -Vue.component('mk-ellipsis', ellipsis); -Vue.component('mk-url-preview', urlPreview); -Vue.component('mk-file-type-icon', fileTypeIcon); -Vue.component('mk-emoji', emoji); -Vue.component('mk-welcome-timeline', welcomeTimeline); -Vue.component('mk-user-list', userList); -Vue.component('mk-frac', frac); -Vue.component('ui-input', uiInput); -Vue.component('ui-button', uiButton); -Vue.component('ui-horizon-group', uiHorizonGroup); -Vue.component('ui-card', uiCard); -Vue.component('ui-form', uiForm); -Vue.component('ui-textarea', uiTextarea); -Vue.component('ui-switch', uiSwitch); -Vue.component('ui-radio', uiRadio); -Vue.component('ui-select', uiSelect); -Vue.component('ui-info', uiInfo); -Vue.component('ui-margin', uiMargin); -Vue.component('ui-hr', uiHr); -Vue.component('ui-pagination', uiPagination); -Vue.component('ui-modal', uiModal); -Vue.component('form-button', formButton); -Vue.component('form-radio', formRadio); diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue deleted file mode 100644 index 497e4976f59b80f48aa25487f1530fd57ad13cd1..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/instance.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta"> - <div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div> - - <h1>{{ meta.name || 'Misskey' }}</h1> - <p v-html="meta.description || this.$t('@.about')"></p> - <router-link to="/">{{ $t('start') }}</router-link> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/instance.vue'), - data() { - return { - meta: null - } - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.nhasjydimbopojusarffqjyktglcuxjy - color var(--text) - background var(--face) - text-align center - - > .banner - height 100px - background-position center - background-size cover - - > h1 - margin 16px - font-size 16px - - > p - margin 16px - font-size 14px - - > a - display block - padding-bottom 16px - -</style> diff --git a/src/client/app/common/views/components/integrations.integration.vue b/src/client/app/common/views/components/integrations.integration.vue deleted file mode 100644 index 51995843b1f8003b88a91553e3a37599defb0510..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/integrations.integration.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<a class="zxrjzpcj" :href="url" :class="service" rel="noopener" target="_blank"> - <fa :icon="icon" size="lg" fixed-width /><span>{{ text }}</span> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['url', 'text', 'icon', 'service'] -}); -</script> - -<style lang="stylus" scoped> -.zxrjzpcj - display inline-block - padding 6px 8px 6px 6px - margin-top 4px - margin-bottom 4px - border-radius 32px - white-space nowrap - - &:hover - text-decoration none - - &.twitter - color #fff - background #1da1f3 - - &:hover - background #0c87cf - - &.github - color #fff - background #171515 - - &:hover - background #000 - - &.discord - color #fff - background #7289da - - &:hover - background #4968ce - -</style> diff --git a/src/client/app/common/views/components/integrations.vue b/src/client/app/common/views/components/integrations.vue deleted file mode 100644 index 7a341a14fdada878737a13a7f44592c4913eaf03..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/integrations.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<div class="nbogcrmo" :v-if="user.twitter || user.github || user.discord"> - <x-integration v-if="user.twitter" service="twitter" :url="`https://twitter.com/${user.twitter.screenName}`" :text="user.twitter.screenName" :icon="['fab', 'twitter']"/> - <x-integration v-if="user.github" service="github" :url="`https://github.com/${user.github.login}`" :text="user.github.login" :icon="['fab', 'github']"/> - <x-integration v-if="user.discord" service="discord" :url="`https://discordapp.com/users/${user.discord.id}`" :text="`${user.discord.username}#${user.discord.discriminator}`" :icon="['fab', 'discord']"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XIntegration from './integrations.integration.vue'; - -export default Vue.extend({ - components: { - XIntegration - }, - props: ['user'] -}); -</script> - -<style lang="stylus" scoped> -.nbogcrmo - > * - margin-right 10px - -</style> diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue deleted file mode 100644 index b8b164aed0eff3fc309bb0f76fa1199e16497ee3..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/media-image.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> - <div> - <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> -</div> -<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else - :href="image.url" - :style="style" - :title="image.name" - @click.prevent="onClick" -> - <div v-if="image.type === 'image/gif'">GIF</div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import ImageViewer from './image-viewer.vue'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - i18n: i18n('common/views/components/media-image.vue'), - props: { - image: { - type: Object, - required: true - }, - raw: { - default: false - } - }, - data() { - return { - hide: true - }; - }, - computed: { - style(): any { - let url = `url(${ - this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.image.thumbnailUrl) - : this.image.thumbnailUrl - })`; - - if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) { - url = null; - } else if (this.raw || this.$store.state.device.loadRawImages) { - url = `url(${this.image.url})`; - } - - return { - 'background-color': this.image.properties.avgColor || 'transparent', - 'background-image': url - }; - } - }, - methods: { - onClick() { - const viewer = this.$root.new(ImageViewer, { - image: this.image - }); - this.$once('hook:beforeDestroy', () => { - viewer.close(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gqnyydlzavusgskkfvwvjiattxdzsqlf - display block - cursor zoom-in - overflow hidden - width 100% - height 100% - background-position center - background-size contain - background-repeat no-repeat - - > div - background-color var(--text) - border-radius 6px - color var(--secondary) - display inline-block - font-size 14px - font-weight bold - left 12px - opacity .5 - padding 0 6px - text-align center - top 12px - pointer-events none - -.qjewsnkgzzxlxtzncydssfbgjibiehcy - display flex - justify-content center - align-items center - background #111 - color #fff - - > div - display table-cell - text-align center - font-size 12px - - > * - display block - -</style> diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue deleted file mode 100644 index bfbc9366d3475c87a71427c6b2761fca2cfc661a..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/media-list.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div class="mk-media-list"> - <template v-for="media in mediaList.filter(media => !previewable(media))"> - <x-banner :media="media" :key="media.id"/> - </template> - <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container"> - <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid"> - <template v-for="media in mediaList"> - <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> - <x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> - </template> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XBanner from './media-banner.vue'; -import XImage from './media-image.vue'; - -export default Vue.extend({ - components: { - XBanner, - XImage - }, - props: { - mediaList: { - required: true - }, - raw: { - default: false - } - }, - mounted() { - //#region for Safari bug - if (this.$refs.grid) { - this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` - : this.$store.state.device.inDeckMode ? '128px' : this.$root.isMobile ? '173px' : '287px'; - } - //#endregion - }, - methods: { - previewable(file) { - return (file.type.startsWith('video') || file.type.startsWith('image')) && file.thumbnailUrl; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-media-list - > .gird-container - width 100% - margin-top 4px - - &:before - content '' - display block - padding-top 56.25% // 16:9 - - > div - position absolute - top 0 - right 0 - bottom 0 - left 0 - display grid - grid-gap 4px - - > * - overflow hidden - border-radius 4px - - &[data-count="1"] - grid-template-rows 1fr - - &[data-count="2"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr - - &[data-count="3"] - grid-template-columns 1fr 0.5fr - grid-template-rows 1fr 1fr - - > *:nth-child(1) - grid-row 1 / 3 - - > *:nth-child(3) - grid-column 2 / 3 - grid-row 2 / 3 - - &[data-count="4"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr 1fr - - > *: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/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue deleted file mode 100644 index 68fa0f5e62cff8bdef6bce2b92162a3a0730b063..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/menu.vue +++ /dev/null @@ -1,196 +0,0 @@ -<template> -<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv" :class="{ isMobile: $root.isMobile }"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ bubble }" ref="popover"> - <template v-for="item, i in items"> - <div v-if="item === null"></div> - <button v-if="item" @click="clicked(item.action)" :tabindex="i"> - <fa v-if="item.icon" :icon="item.icon"/>{{ item.text }} - </button> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: { - source: { - required: true - }, - items: { - type: Array, - required: true - } - }, - data() { - return { - bubble: !this.$root.isMobile - }; - }, - mounted() { - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - let left; - let top; - - if (this.$root.isMobile) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset; - this.bubble = false; - } - - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset; - this.bubble = false; - } - - if (top < 0) { - top = 0; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - }, - methods: { - clicked(fn) { - fn(); - this.close(); - }, - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: 200, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: 200, - easing: 'easeInBack', - complete: () => { - this.$emit('closed'); - this.destroyDom(); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.onchrpzrvnoruiaenfcqvccjfuupzzwv - $bg-color = var(--popupBg) - - position initial - - &.isMobile - > .popover - > button - font-size 15px - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background var(--modalBackdrop) - opacity 0 - - > .popover - position absolute - z-index 10001 - padding 8px 0 - background $bg-color - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - $balloon-size = 16px - - &.bubble - margin-top $balloon-size - transform-origin center -($balloon-size) - - &:before - &:after - content "" - display block - position absolute - pointer-events none - - &:before - top -($balloon-size * 2) - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size $bg-color - - > button - display block - padding 8px 16px - width 100% - color var(--popupFg) - white-space nowrap - - &:hover - color var(--primaryForeground) - background var(--primary) - text-decoration none - - &:active - color var(--primaryForeground) - background var(--primaryDarken10) - - > [data-icon] - margin-right 4px - - > div - margin 8px 0 - height var(--lineWidth) - background var(--faceDivider) - -</style> diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue deleted file mode 100644 index 1ab6359415e19ea3bef1056c8a42f8e72702a928..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ /dev/null @@ -1,279 +0,0 @@ -<template> -<div class="message" :data-is-me="isMe"> - <mk-avatar class="avatar" :user="message.user" target="_blank"/> - <div class="content"> - <div class="balloon" :data-no-text="message.text == null"> - <button class="delete-button" v-if="isMe" :title="$t('@.delete')" @click="del"> - <img src="/assets/desktop/remove.png" alt="Delete"/> - </button> - <div class="content" v-if="!message.isDeleted"> - <mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> - <div class="file" v-if="message.file"> - <a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name"> - <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name" - :style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/> - <p v-else>{{ message.file.name }}</p> - </a> - </div> - </div> - <div class="content" v-else> - <p class="is-deleted">{{ $t('deleted') }}</p> - </div> - </div> - <div></div> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <footer> - <template v-if="isGroup"> - <span class="read" v-if="message.reads.length > 0">{{ $t('is-read') }} {{ message.reads.length }}</span> - </template> - <template v-else> - <span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span> - </template> - <mk-time :time="message.createdAt"/> - <template v-if="message.is_edited"><fa icon="pencil-alt"/></template> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { parse } from '../../../../../mfm/parse'; -import { unique } from '../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('common/views/components/messaging-room.message.vue'), - props: { - message: { - required: true - }, - isGroup: { - required: false - } - }, - computed: { - isMe(): boolean { - return this.message.userId == this.$store.state.i.id; - }, - urls(): string[] { - if (this.message.text) { - const ast = parse(this.message.text); - return unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - } else { - return null; - } - } - }, - methods: { - del() { - this.$root.api('messaging/messages/delete', { - messageId: this.message.id - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.message - $me-balloon-color = var(--primary) - - padding 10px 12px 10px 12px - background-color transparent - - > .avatar - display block - position absolute - top 10px - width 54px - height 54px - border-radius 8px - transition all 0.1s ease - - > .content - - > .balloon - display flex - align-items center - 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 - - > .content - max-width 100% - - > .is-deleted - display block - margin 0 - padding 0 - overflow hidden - overflow-wrap break-word - font-size 1em - color rgba(#000, 0.5) - - > .text - display block - margin 0 - padding 8px 16px - overflow hidden - overflow-wrap break-word - word-break break-word - font-size 1em - color rgba(#000, 0.8) - - & + .file - > a - border-radius 0 0 16px 16px - - > .file - > a - display block - max-width 100% - border-radius 16px - overflow hidden - text-decoration none - - &:hover - text-decoration none - - > p - background #ccc - - > * - display block - margin 0 - width 100% - max-height 512px - object-fit contain - - > 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 var(--messagingRoomMessageInfo) - - > .read - margin 0 8px - - > [data-icon] - margin-left 4px - - &:not([data-is-me]) - > .avatar - left 12px - - > .content - padding-left 66px - - > .balloon - $color = var(--messagingRoomMessageBg) - float left - background $color - - &[data-no-text] - background transparent - - &:not([data-no-text]):before - left -14px - border-top solid 8px transparent - border-right solid 8px $color - border-bottom solid 8px transparent - border-left solid 8px transparent - - > .content - > .text - color var(--messagingRoomMessageFg) - - > footer - text-align left - - &[data-is-me] - > .avatar - 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(#fff, 0.5) - - > .text >>> - &, * - color #fff !important - - > footer - text-align right - - > .read - user-select none - - &[data-is-deleted] - > .balloon - opacity 0.5 - -</style> diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue deleted file mode 100644 index 52f55e4333f9a9c3c943118f7cc23437fc42137e..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/messaging.vue +++ /dev/null @@ -1,500 +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"><i><fa icon="search"/></i></label> - <input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" :placeholder="$t('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" - > - <mk-avatar class="avatar" :user="user" :key="user.id"/> - <span class="name"><mk-user-name :user="user" :key="user.id"/></span> - <span class="username">@{{ user | acct }}</span> - </li> - </ol> - </div> - </div> - <div class="history" v-if="messages.length > 0"> - <a v-for="message in messages" - class="user" - :href="message.groupId ? `/i/messaging/group/${message.groupId}` : `/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" - :data-is-me="isMe(message)" - :data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead" - @click.prevent="message.groupId ? navigateGroup(message.group) : navigate(isMe(message) ? message.recipient : message.user)" - :key="message.id" - > - <div> - <mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> - <header v-if="message.groupId"> - <span class="name">{{ message.group.name }}</span> - <mk-time :time="message.createdAt"/> - </header> - <header v-else> - <span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> - <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> - <mk-time :time="message.createdAt"/> - </header> - <div class="body"> - <p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> - </div> - </div> - </a> - </div> - <p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <ui-margin> - <ui-button @click="startUser()"><fa :icon="faUser"/> {{ $t('start-with-user') }}</ui-button> - <ui-button @click="startGroup()"><fa :icon="faUsers"/> {{ $t('start-with-group') }}</ui-button> - </ui-margin> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faUser, faUsers } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n('common/views/components/messaging.vue'), - props: { - compact: { - type: Boolean, - default: false - }, - headerTop: { - type: Number, - default: 0 - } - }, - data() { - return { - fetching: true, - moreFetching: false, - messages: [], - q: null, - result: [], - connection: null, - faUser, faUsers - }; - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('messagingIndex'); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - - this.$root.api('messaging/history', { group: false }).then(userMessages => { - this.$root.api('messaging/history', { group: true }).then(groupMessages => { - const messages = userMessages.concat(groupMessages); - messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - this.messages = messages; - this.fetching = false; - }); - }); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - getAcct, - isMe(message) { - return message.userId == this.$store.state.i.id; - }, - onMessage(message) { - if (message.recipientId) { - 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); - } else if (message.groupId) { - this.messages = this.messages.filter(m => m.groupId !== message.groupId); - this.messages.unshift(message); - } - }, - onRead(ids) { - for (const id of ids) { - const found = this.messages.find(m => m.id == id); - if (found) { - if (found.recipientId) { - found.isRead = true; - } else if (found.groupId) { - found.reads.push(this.$store.state.i.id); - } - } - } - }, - search() { - if (this.q == '') { - this.result = []; - return; - } - this.$root.api('users/search', { - query: this.q, - localOnly: false, - limit: 10, - detail: false - }).then(users => { - this.result = users.filter(user => user.id != this.$store.state.i.id); - }); - }, - navigate(user) { - this.$emit('navigate', user); - }, - navigateGroup(group) { - this.$emit('navigateGroup', group); - }, - 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; - } - }, - async startUser() { - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - this.navigate(user); - }, - async startGroup() { - const groups1 = await this.$root.api('users/groups/owned'); - const groups2 = await this.$root.api('users/groups/joined'); - const { canceled, result: group } = await this.$root.dialog({ - type: null, - title: this.$t('select-group'), - select: { - items: groups1.concat(groups2).map(group => ({ - value: group, text: group.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - this.navigateGroup(group); - } - } -}); -</script> - -<style lang="stylus" scoped> -.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% - box-shadow 0 0 2px rgba(#000, 0.2) - - > .form - background rgba(0, 0, 0, 0.02) - - > label - display block - position absolute - top 0 - left 8px - z-index 1 - height 100% - width 38px - pointer-events none - - > i - display block - position absolute - top 0 - right 0 - bottom 0 - left 0 - width 1em - line-height 48px - margin auto - color #555 - - > input - margin 0 - padding 0 0 0 42px - width 100% - font-size 1em - line-height 48px - color var(--faceText) - outline none - background transparent - border none - border-radius 5px - box-shadow none - - > .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(#000, 0.8) - text-decoration none - transition none - cursor pointer - - &:hover - &:focus - color #fff - background var(--primary) - - .name - color #fff - - .username - color #fff - - &:active - color #fff - background var(--primaryDarken10) - - .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(#000, 0.8) - - .username - font-weight normal - color rgba(#000, 0.3) - - > .history - > a - display block - text-decoration none - background var(--face) - border-bottom solid 1px var(--faceDivider) - - * - pointer-events none - user-select none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - .avatar - filter saturate(200%) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - &[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 var(--noteHeaderName) - font-weight bold - transition all 0.1s ease - - > .username - margin 0 8px - color var(--noteHeaderAcct) - - > .mk-time - margin 0 0 0 auto - color var(--noteHeaderInfo) - 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 var(--faceText) - - .me - opacity 0.7 - - > .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 var(--text) - - > [data-icon] - 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/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue deleted file mode 100644 index 40c444242c49890031d4a3b4e715763aa4535ec8..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/misskey-flavored-markdown.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MfmCore from './mfm'; - -export default Vue.extend({ - components: { - MfmCore - } -}); -</script> - -<style lang="stylus" scoped> -.havbbuyv - white-space pre-wrap - - &.nowrap - white-space pre - word-wrap normal // https://codeday.me/jp/qa/20190424/690106.html - - >>> .title - display block - margin-bottom 4px - padding 4px - font-size 90% - text-align center - background var(--mfmTitleBg) - border-radius 4px - - >>> .quote - display block - margin 8px - padding 6px 0 6px 12px - color var(--mfmQuote) - border-left solid 3px var(--mfmQuoteLine) - - >>> pre code - font-size 80% - -</style> diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue deleted file mode 100644 index 41b65604dec21ca52044d5ca5a6c7ae2cddcf483..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/nav.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<span class="mk-nav"> - <a :href="aboutUrl">{{ $t('about') }}</a> - <template v-if="ToSUrl !== null"> - <i>・</i> - <a :href="ToSUrl" target="_blank">{{ $t('tos') }}</a> - </template> - <i>・</i> - <a :href="repositoryUrl" rel="noopener" target="_blank">{{ $t('repository') }}</a> - <i>・</i> - <a :href="feedbackUrl" rel="noopener" target="_blank">{{ $t('feedback') }}</a> - <i>・</i> - <a href="/dev" target="_blank">{{ $t('develop') }}</a> -</span> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { lang } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/nav.vue'), - data() { - return { - aboutUrl: `/docs/${lang}/about`, - repositoryUrl: 'https://github.com/syuilo/misskey', - feedbackUrl: 'https://github.com/syuilo/misskey/issues/new', - ToSUrl: null - } - }, - - mounted() { - this.$root.getMeta(true).then(meta => { - this.repositoryUrl = meta.repositoryUrl; - this.feedbackUrl = meta.feedbackUrl; - this.ToSUrl = meta.ToSUrl; - }) - } -}); -</script> - -<style lang="stylus" scoped> -.mk-nav - a - color inherit -</style> diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue deleted file mode 100644 index a72863e1dda16ed5b6a7ee5ffc9fd296e5043a00..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/note-header.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"> - <mk-user-name :user="note.user"/> - </router-link> - <span class="is-admin" v-if="note.user.isAdmin">admin</span> - <span class="is-bot" v-if="note.user.isBot">bot</span> - <span class="is-cat" v-if="note.user.isCat">cat</span> - <span class="username"><mk-acct :user="note.user"/></span> - <div class="info"> - <span class="app" v-if="note.app && !mini && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span> - <span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span> - <router-link class="created-at" :to="note | notePage"> - <mk-time :time="note.createdAt"/> - </router-link> - <span class="visibility" v-if="note.visibility != 'public'"> - <fa v-if="note.visibility == 'home'" icon="home"/> - <fa v-if="note.visibility == 'followers'" icon="unlock"/> - <fa v-if="note.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span> - </div> -</header> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - note: { - type: Object, - required: true - }, - mini: { - type: Boolean, - required: false, - default: false - } - } -}); -</script> - -<style lang="stylus" scoped> -.bvonvjxbwzaiskogyhbwgyxvcgserpmu - display flex - align-items baseline - white-space nowrap - - > .avatar - flex-shrink 0 - margin-right 8px - width 20px - height 20px - border-radius 100% - - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - color var(--noteHeaderName) - font-size 1em - font-weight bold - text-decoration none - text-overflow ellipsis - - &:hover - text-decoration underline - - > .is-admin - > .is-bot - > .is-cat - flex-shrink 0 - align-self center - margin 0 .5em 0 0 - padding 1px 6px - font-size 80% - color var(--noteHeaderBadgeFg) - background var(--noteHeaderBadgeBg) - border-radius 3px - - &.is-admin - background var(--noteHeaderAdminBg) - color var(--noteHeaderAdminFg) - - > .username - margin 0 .5em 0 0 - overflow hidden - text-overflow ellipsis - color var(--noteHeaderAcct) - flex-shrink 2147483647 - - > .info - margin-left auto - font-size 0.9em - - > * - color var(--noteHeaderInfo) - - > .mobile - margin-right 8px - - > .app - margin-right 8px - padding-right 8px - border-right solid 1px var(--faceDivider) - - > .visibility - margin-left 8px - - > .localOnly - margin-left 4px - -</style> diff --git a/src/client/app/common/views/components/note-skeleton.vue b/src/client/app/common/views/components/note-skeleton.vue deleted file mode 100644 index a2e09e3222740901b36b8e5e9d612242e487ce89..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/note-skeleton.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<div> - <vue-content-loading v-if="width" :width="width" :height="100" :primary="primary" :secondary="secondary"> - <circle cx="30" cy="30" r="30" /> - <rect x="75" y="13" rx="4" ry="4" :width="150 + r1" height="15" /> - <rect x="75" y="39" rx="4" ry="4" :width="260 + r2" height="10" /> - <rect x="75" y="59" rx="4" ry="4" :width="230 + r3" height="10" /> - </vue-content-loading> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import VueContentLoading from 'vue-content-loading'; -import * as tinycolor from 'tinycolor2'; - -export default Vue.extend({ - components: { - VueContentLoading, - }, - - data() { - return { - width: 0, - r1: (Math.random() * 100) - 50, - r2: (Math.random() * 100) - 50, - r3: (Math.random() * 100) - 50 - }; - }, - - computed: { - text(): tinycolor.Instance { - const text = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')); - return text; - }, - - primary(): string { - return '#' + this.text.clone().toHex(); - }, - - secondary(): string { - return '#' + this.text.clone().darken(20).toHex(); - } - }, - - mounted() { - let width = this.$el.clientWidth; - if (width < 400) width = 400; - this.width = width; - } -}); -</script> diff --git a/src/client/app/common/views/components/page-preview.vue b/src/client/app/common/views/components/page-preview.vue deleted file mode 100644 index e3e73bd08f7e16a9deb3a15cf489809bc88e5c82..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/page-preview.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1"> - <div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div> - <article> - <header> - <h1 :title="page.title">{{ page.title }}</h1> - </header> - <p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> - <footer> - <img class="icon" :src="page.user.avatarUrl"/> - <p>{{ page.user | userName }}</p> - </footer> - </article> -</router-link> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - page: { - type: Object, - required: true - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.vhpxefrj - display block - overflow hidden - width 100% - border solid var(--lineWidth) var(--urlPreviewBorder) - border-radius 4px - overflow hidden - - &:hover - text-decoration none - border-color var(--urlPreviewBorderHover) - - > .thumbnail - position absolute - width 100px - height 100% - background-position center - background-size cover - display flex - justify-content center - align-items center - - > button - font-size 3.5em - opacity: 0.7 - - &:hover - font-size 4em - opacity 0.9 - - & + article - left 100px - width calc(100% - 100px) - - > article - padding 16px - - > header - margin-bottom 8px - - > h1 - margin 0 - font-size 1em - color var(--urlPreviewTitle) - - > p - margin 0 - color var(--urlPreviewText) - 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 var(--urlPreviewInfo) - font-size 0.8em - line-height 16px - vertical-align top - - @media (max-width 700px) - > .thumbnail - position relative - width 100% - height 100px - - & + article - left 0 - width 100% - - @media (max-width 550px) - font-size 12px - - > .thumbnail - height 80px - - > article - padding 12px - - @media (max-width 500px) - font-size 10px - - > .thumbnail - height 70px - - > article - padding 8px - - > header - margin-bottom 4px - - > footer - margin-top 4px - - > img - width 12px - height 12px - -</style> diff --git a/src/client/app/common/views/components/page/page.post.vue b/src/client/app/common/views/components/page/page.post.vue deleted file mode 100644 index cb695e21e983f1d3ae1f0d8a6dfedd67c5e3d2ff..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/page/page.post.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="ngbfujlo"> - <ui-textarea class="textarea" :value="text" readonly></ui-textarea> - <ui-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('pages'), - - props: { - value: { - required: true - }, - script: { - required: true - } - }, - - data() { - return { - text: this.script.interpolate(this.value.text), - posted: false, - posting: false, - }; - }, - - created() { - this.$watch('script.vars', () => { - this.text = this.script.interpolate(this.value.text); - }, { deep: true }); - }, - - methods: { - post() { - this.posting = true; - this.$root.api('notes/create', { - text: this.text, - }).then(() => { - this.posted = true; - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ngbfujlo - padding 0 32px 32px 32px - border solid 2px var(--pageBlockBorder) - border-radius 6px - - @media (max-width 600px) - padding 0 16px 16px 16px - - > .textarea - margin-top 16px - margin-bottom 16px - -</style> diff --git a/src/client/app/common/views/components/particle.vue b/src/client/app/common/views/components/particle.vue deleted file mode 100644 index 33c118f00085e9810387850f2a9bd3375d14d1fd..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/particle.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<div class="vswabwbm" :style="{ top: `${y - 50}px`, left: `${x - 50}px` }" :class="{ active }"></div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - x: { - type: Number, - required: true - }, - y: { - type: Number, - required: true - } - }, - data() { - return { - active: false - } - }, - mounted() { - setTimeout(() => { - this.active = true; - }, 1); - - setTimeout(() => { - this.destroyDom(); - }, 1000); - } -}); -</script> - -<style lang="stylus" scoped> -.vswabwbm - pointer-events none - position fixed - z-index 1000000 - width 100px - height 100px - background url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABqUAAABkCAYAAAAPKjqIAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA25pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1ZmMyNTFlNy02ZmI3LTg3NDMtYWFkNy1kZWQ2ZWY1NzIzYWUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDIyMEQ0QjBFNTE2MTFFNkFGREZCRkYzMDQ2QkI0RDciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDIyMEQ0QUZFNTE2MTFFNkFGREZCRkYzMDQ2QkI0RDciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MUU1NkMyNUZFNTAzMTFFNkI1RjJFOTE0NTRGREQ2MDgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MUU1NkMyNjBFNTAzMTFFNkI1RjJFOTE0NTRGREQ2MDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5nGnsGAABRHklEQVR42uydB3xUVfbH731TMiUNkkACCQRQOrYkoq4gYEVFZUXsfQUbCuta1nV33f/adQVFRVwrdnFlEV2wgQYVIYmutABSQnqAEFJmMsnMvPt/Z5LJDggkwMy8Mr/v5zO8kmHeuee8W94975zLhRAMAAAAAAAAAAAAAAAAAAAAgEgiQQUAAAAAAAAAAAAAAAAAAAAg0sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4pj1KjjnHNYDAAAAAAAAAAAAAAAAAEBM8sela9NO2tnSeOGlOR61ZRFCdOl7vKtf1BpwSgEAAAAAAAAAAAAcOkXP1mYJbjuT+fg3uTMcW6ARdfnwnWVnZfOEycX9j/rX1SOTF0Mj6vL61vwJybVsakU6y78ta/QT0Ii6vFix/Bqv5L/VLKSNmd85bpxwSa4PWlHXHk0m9kac8K/sk58wRguOkFhvrxpM1o9MptbKzO+cg9S2R1d9TWaYDgAAAAAAAAAAAJGi4LnaVMlrf0kwNohzsaiyd/EDmFRU0R4zm4bZHWwNYzRxJFjBLNcpedOdP0Az6jB7wdahSQnyZ3XKfnrjrhvfXLnnXDim1OO5dfOT4j22f/ucjPVsZONf35pffF3/0YugGfXwC/H3FKs7S9k9sTaHfahsYQ8VIQchYybWwk0jS0/claecWg6tqIewyqNazV4zY7zPD2lxCcopXTgJ4ZQCAAAAAAAAAGAoCme6znP3trxI+/Yq74t5dzofhlbUQ2q1/8OeKF/Ufjgko3rIJmX7KjSjlkH4RW0OqfZDxsg2cEqpRG9L5WmMpXccm7auGaNs4JRSiYqaIdb0dHfHsSeOd4NW1EU2yVXKhpxSrFVybYJG1MUim15oMbGRgUipVakF0Ii6uOp3PpySlEZOwrV/GDd8p17khlMKAAAAAAAAAI6Ql1e0XttrpzyG9tNKXXfn3Z6yC1pRh4XvF9laZMsik7/tuLWH5e+rZjcuOXFaQhG0oxKc9drr0M8GQikqmkOIbXudkFgFtKIeWSVD5jcNrvuzj7EMOnb2dX0ArajHo+OG73y+LP+PvUtNdzOrf2XGGifsoTIZovziGnefK2Vf84pp/c7eCI2oy829R81TNvOgCW1w+7BL6pXNPXqTG04pAAAAAAAAADgC3ly5Z3xCLXul1SwFjhvSEwYom9HQjDp0K+1lZpl7n5P8pj7KBk4plRBm8a6yOT147DfJ86EV9ajqveGD9Moh/SSZj2FcFFX2Lp4DragHvcRQ8FztMRvyTHlNdTvW3XLKOaXQirq0ryOFtaQ0wqReV1XAHgAYC97Vxac0JzjnsB4AAAAAAIhJyAniEY7naL/F7v+i76Z1t2N9luiQ/2RVPG1H353RFDz3n489r7WapatDv9d9XW1y6HdAZCic6R7ATc2tOXeklIWe/+rD1nybl51K+34Tq7DvaDwe0WuRZ9Xsxhza7i8qrWimaywzseGyXyzNmxG/DtqKTl9BW6xPpA1erFh+DW3bowwA7AFgD83yUsniQbSdkj0ekWkagLIA0PbCS3M0v15UV31NcEoBAAAAAIAOKAWZ4OwvbQNF9n+/O9n6BrSirj1S69grtL+rG7uR7EFOkS2npjSEfs/P2PWwVeQpnO26oSXV8jLtx+3y/i53mvPVfe1EdG9iVaMnW7Ogscjbw2EXAXu4m9hVudPj3wl9eO+9Y/AVSjvmZMzz732dViAC9pjpOs+RIAKLz7sb+YTcGc5PoRX1mL1g69DBCXLA+behURo2bWL/9dCKuny4flUgqeikoSeaoA31mVOZHxg33dJr9LXQhvrACQKAMeiqrwnp+wAAAAAAQIDn19TlmJrYayFrn782e8HWgmzf7k2lRw25V0hSFpflsj6bix9HVE7kIefT7hBHBzk9Fr5f9P6aobZBjn3ibzwO/8nKBk6pCOPuaflbcJ0i2lc2AadUz/LVb5viho+haKkEN/t+zRDXDGgr8nAfP5+1N1hcSJOUTYdTqv1N0lehpShiZj33uw9U4aSKpB1Ng+uqgvvQiPo02TwXQQvaAc4obQFnFACxNmwEAAAAAABAIa7JOXzfc/b0zLztjow7/Nw0NXDCZGLbBw6jCJCbobHIsm5EY/cMOWWvc712Zsez9VvX7eg7YptZsH7B8za3aQU0Fnm8Ettm8rPewf3g+XYn7fXtHxAlBGdvKpuLQvaBilT02PBOr+rBGbRf3asY6Zf2w9dXrUyrn2BujEb6HUpXuWh+Ia2lxsbefhReJNkHevGj5Js9J2SflvxjtFKtXtd/9CJofv8svuyn/jubbJlJV7tX6SE9ldF5La90Im2vL+izANpQF4r89r5nu8ntlX6+5pMh+dCI+v34Llf8ZbDHkYP0fQAAAABQlTlLNvWRe/YOrMUi1VS8ecs5A7G4s0pQqp/49My1oeeaqsuHywMyf7X2x53HWDEYiwKhaxVZffKb515gCzg9KKqNSdKT1kZ739Y476u3He98GNqKPLR+0Y4B0gO037PE9RekhFOfgudqU2mL9aJ+zS8PLR3OnNbHA/2ro+m1AVPP+RBaUQdygKSWb/7a7LSdQMf+2urTh8ydsAyaUa9u1GxI/Yn2vbK3xCNLZ49/7/it0Iw6kAMkNcH3UfvhGnbL7hMQka8erxxb9lfOxV9pXwj+txt/zvobtKIe8/K2vefzmibTvjnTNQaOEJXbq6kF33HGT6T9lDOYHW3Vr0H6PgAAAABoHpqk+alX95WM8fTAiV59b39u3fzBtw+7pB7aiT603sSbK/ec1+A0PUbHiS7/fXTumdUt1R02ahtqVkNb0YGcUKtmNz5XMMbHbhvRrSh4vn1/HDQUXXJnOLYwREMdkMA6TrJ4zm9puT7Olbhqs2XXXyddMfbzSF4Tzqj9s2h+oVnsEB9aRWtgjQ7mTTj3l4eWjjj6gXFroZ3ok1L+y1Sz035CRy9qS36SmhRoRh08laYbg/sWyZLdI20PRYX8A5pRBxPzXxpyOML8LxNF+MFJqBIWq2+Iz9u27JnExRXKBk4pjeCvcaZAC+rCGaP1GU8UTKyacEkeHFJHAJxSAAAAAFCN7aPto/Z2dvD0pKYzT1F2FkM70YMiDYSw9jX5pET2PfMIxu5vG3RLnqKZrrE/+H3P+UyWh4Lfd7r4fdBa9DhxWkIRtHBgyBHS2s39JufsRHOyWMO97r9eeMo50JkK+J3N1/s9POC0a3E2nJjFrIsXzS+Mw1uk0adXVb8Mq7RhUOg5Ka11sLKBUwrEPJKFl+/VdnkZXrZREUem+ztWbw86ptb4LvaXsvegF9X6cmZ6X9kEInOYhWHMrzJpA/bcv3NLMvO2movT7qlZxC6BTtSk2zhpWv2bjjf7JDcUQxtHBtL3AQAAiAnojeGM8qF9ORN9lJ7PofSASYE/CF6v9ChuwXhpVeb67Zg4iywFM5uGcTMfyf1soKL7gYotBn13unlo6Hd+85VvvWKTjYqNNgkT2yR8YmXejPh10F746kLP6kHHSj4p/VDufUqzaEnKHnvcylYv1RvZLFfXpG/8OZbrzHPr5ieZk9MvtLWIum4Fzi+wBoI6fFGw8DF3c+q9wWNvi1wat8c+AO159Cl8t/BlipIKPSdVD+6JaCZ12vrBVeYKi+Tq0VE39rQgUkolaA2KHimti4Pp+zx7mq8+5oXRb0Mz6kCR+vGbN79U3+jIS01u+qRkrPNu9BnqQin8EjIbM1OdTe+NeWvkTmhE/TpC22ittwYAMA5d9TXBKQUAAGGGIg641zaSm1h/4WdDeFsUSILS3ia3t197lE2jYKJa+U6x8p2twuJZiQmb8FL0bG2WkONGcVk6W3CWq/SM9LawqZP/5lcMtJELVigk+TMutSzHeiFHRiCVU9WQ8YpOr1bqwGjl1K9SDpQPtvi3Z7bZpm8583er9Jj+e6KNBY8zNwTyR9QqdSefFrKvyChejMn/Q4fWwlGGiIOVtqckXE6+gJOR8WylZdvQntYsZviw8q3eNXKf761NPJOOPUn+gj75CWNwb0affxfkf8KbTeeFnvMetS1zUq+rKqCdKNeLd5adlWW1dkS6Ugq/464dcTI0ow5YU0pbBByFGxsG70k+thrjfgAAAAAYETilAIghaNJ34NLKk2Vb0gmyV2R6/VJPi8nf02QxBSJB/F5/vddvqrGY5BpKVSB56n/cNK7XCkychQ+amJU4v1rI7HR2+PnhC7nEvpKFeBNRIYcHvdHlsCVO5j5+udK/nRGm/uZLYRbvuj0NH+BNsa4TcAr6bX9kIpD6IdQRFXAuBaKgmFjNBa/ym+UGyWciZy2Tzf7kFadZPwlN6RfimOr4DcbZB9zkeTRWnYazt302SjLbT5Z9zSum9Tt7+cG+u2p2Yw5FRVVlFn8WqbeA2yIRh5xN0VOxkmrutfKvn/A1me4KPedPYNff3HvUPLQA0WXfSClhYrtMlXEZeOtdHbbMXTKpLjH1nIAtavvdh8l3AAAAAAAAYgM4pQAwOJQCIi2l5RrZK5/LmfiNciruEH+iRTD+nWSR/rOzNm4eQuQPnUAEyI7BVzAvu4WFf6HiQmZhcyp6bHgHzsPOoeg05rdN5X42g+0nEidM1AoTm8lMnrmYYDsw5IxiPvtDgokrWTAyjfP1QhLvMFn8uzOH6x+Xrk1LTx24I/ScSfjnjlzqn80kfhGX+RXKKCeY7s/PGX+bmZsfiCXn1PNl+fc0W/jjwWO7V9x7W9boJ37ViLRHRkXSGbUvQedULEROwSmlHSiNYs+K9JctTmmMzy27LU7Xb7GmFAAAAAAAAABEFzilADAoxVMXjfWZEh7gTIwLa6PB+FKzv/GhIXMnLIOWD05g0rV6yDXMyx5UDrMifLkyZmEPVqUXz8Mb3/u3Ra+qIbcIH39MuYsdUeqB3Nws7qvMKJ4Dm+xti/TKIfdyP7+/3RZ+pa/+WObiibzpzh8O5beeX1e/1Oe3jw0ed292n3v1yOSOdFAFs1wnSYLfo4xhLmABxxd3C5N4pLpX8eNGtwk5w385zd2873lbbXXy7cMuqe9oo8qHnK1mWtBgGtNoOsSibovvl+Ts6O5cFXquW+L2PkgZBwAAAAAAAAAgFoFTCgCDQc4oYUt+Uvb6ciJ5HcliLuKePXfDObV/AmmwvNKLLPyRUZ1RKFvkm2MlLVaXbeEzzQuJmol2R7ReNvuvgU3aI3KEeC9YL5Q+eoHM5D8fbhpKipbK7NZvipCkrCRPy8JQh1QogbSZTPq7MpaZGBRFufhlRo7QId2kDK3bse/52vXdejw6bvjOQKSa33ZUzgynJtrwopmusczk2WzUSDZKo2iOs//OJss1Xl/zK1Oyx29E6wwAAAAAAAAAIBaBUwqoQvlxl1hom/nf+V5oIzxQmr5EZ+szVpPv8mhet9VvfrfBZb0Taf3a+F8UCPsbC6Ykiz5+YWJ/jYVokM4oetY1TfjYTBVt0WETbmYzcu5wzo5VWxTOarqCydI/26Ojyhhnt+TOcH4aVRlmus5jgs1hgchF7maSfFPu9Ph3jKrz2VVf/9DCTSODx3HCv3JaxpiT2h21e7TmlCOnJa0Vtq8Dd81LRffbXM2/8Tjt321NYk8jVam67PrPM7lse+pdbs5LrAM2zUo/88EaaEU9di9+IIFJBbf56rOPd2dn/CX7xAfh8FTbJp+dfV9rNznX3tQwM2ncyu+gkUObeAj3c3zJqgcH2dh3D0ueYW/3GD1rASyh/rPSbxL/9Afa7372Z49BI+qzI3/6xG1DMweNTP0D7KEBPqx8q3eWtfrq6q/GzsKYVxus3PXUfQU1fecEs00A9Z9Fvj4upQpZJ7QDZWk5nPYKTikQUUqHTEr1ZeaeLjjLVSyRq9xF/VnbOi7O9q+4GC1mz9hW5W+FXLBCc3nhV32KP8Q6LIcARUfJ5oS3lXqaoVI9q5J8jVfGetQUpaGSvPZ3FTucoZH270vZ0nx5LK5rRJ1iZsWQuYKJazTVJzE+r7x38dRYecCgh6qdrM8jsszGDNzQq0+31RltqfoszVPUTBen1NOXglFTwsQeyLvT+bCR9P5SyeJBVtk5sFVybfJbnff7uDzILKSN/rqaO07+/NwTZItnjVbbhTb72EYEI7jIIcW81oeCf2/p3vJq7uW5v9NVe/T9kkDkslSR+rPeXxSo/uLBnp6KPh0PgLaklm/SJ95yul7kp8lQufeuYzML83YYJSpvxwdT31c2wShQ1mPyXKue7DF4Y8Pg9UcnbTZKv1i/dORvTL6W/ODxsvqX4/RS7/OfrIrPaCnOPvqBcWvD+btHOo9wpM/zjcuO+4l72TG03+o/P7n7+Ica9/c9emnypz/dK7RiL3rhUDq2T/PouzOaInmdQ7FPOOZWyAHiSdr8Ee1bd+SmH+jFBnK4m+K+OKbZP36zFl5+IHs0Xmiqi9T9oZbTNjD22vHntXb3zmGpv6Qf0B7UXo9OuS3wopMWnO1kD6O+FPvNjlmnOpgvf7svPetgk+7kcN+enZzW8M2pP6jdbtH9YeSXYckp1fenptcO1hZRm7UmJ/XYEUW7fj5QPwNbhG+uR5KEr7NyUl3a6UvdBueVNqA2i7ahL9DBKQXCDg3oW3rmXMcFu0K5a8Yclt2UcYbg7J24mqLXEU11cNbdmX+X8Pqf0kR9s5j+MOyZ0f+IRTtQKizhty9RLUXcgRvB9dzUfI5RU2IdaFLFaU5aEB7nIHcrXWVt+35KONajImehy1c/MdKTDFpg30idbvVpX9w4aPBZWpCt4BnXn7ifBZwdXGKPy2bPU8xvmxo4lvl7ek3t93xZ/j1WF3+0Y6BnaxoaTBVHKfJcvoYCrd97bXU4MY8cU/99Y80KUxPruIdsfv/Oo+84rqde7PFFwcLH3M2p9wZsYfd/elHe6PP1XKfpzcSmHQl7rf2Wfd0NZr3IX/hu4ct+S8v1tJ9a23jpgKnnfKj3drYzpxRNTpzs/O44n6WuTEtRbSTXoB1irVW0Dmrl1o3FqdJxRnBM7euU2nPP0bZ9n2XoodxRUvV/gYN0+3taiN6hdjelZOMmS5Iz3etqvjsc4/lwzx8c7nN99aozP4zfs3OisLDV9TOOPnF/z5YU3WblNYEXVFpFzz+pHcFDE+5mZq+k/Uaff9D4947fGs7fD4dtDtseXzzYkyV993xgLLb90cv2N6lI33Ga/70k6Ex028ZMUrOeLL7sp/6+HUlb6hr5+9cU9LtMS3Y4Elvs1SZ59gw9mI6pHrXGxV1M+/HV3j+qWUfmnV882lfu/Nqc6RpzzSdD8sP9+0dil2jNP5Jzd0cfT8C52+xIW5fX4+/D1bLHa3mlE03Mf2k464beIIfUlhy+grGkocpIYH3lsoty1RrTUP1Ii/eUh7vf0BvkSJRk9gjtd+bgjSSHG0FkNILO9kCfzsyjT+sx/dtDaW8lBkAnkDNqy1mP39XSI2czE+ylw3VIBW5M+r/Kb9Bv0W8G0/2BvVlz6zczteKQCthNkYVkijU7UMop4bN9pzmHVFsrP5RkC6zlEwNQp38EDqlaimRiFnajbJFz3f6GhNzfO5y5v3f2afs4nHSO/kbfCXxX+T+H8aBzBslIshpN/zTJSGvn0Oe5dfOTQh1SgQFIYnWiVmQNREdJ4kpG6S5ldi9vtW0gJ1XAUSXESorY0aMNQh1SBEVKBSZ+ZrlO0oNDiiAZSVaS2Wdt2Wu9MVeSaaue2qOgQyowUdFsOo+i2PRcx8mxEXoc3xw3P7j/y0NLh1NkG320WH9IpqBDiqhLTD3HEA1vuv294K45qeQvoX+iid0RrtUeciRShFvpi+88qhWxe1X1yyCHVKDdUrZDf6k/KvxDIHHInyOFIgjIqdGUnLbAb44bva8DhCauHCVVa1ibI3Eiq25+P5ASU2UoQoocUgF7ZEvjwqH3SNnzkNutKcmXky1cvovO2Z9DiupJ0CEVKL+yT85FNe3R7LMmROaxIHy2OdzfIud4+olfTKLPgd5yt8orrg86pAi7/+u/aOHR25Hp/k5rdgjH79Fb6wd1SCl1JOiQIprSLar2JdmnJf+Y2K/xzqSr3asi0V9o4Xc6Q7atu7Kjfrh3Dgs4e1Xi+oI+CyLtkIp2X37IjUNO6rFtDqnASGBo8tjlqvXr5KiNdYcUIcn1VwX341xNp6glBxxSbTTU2zqyejk2Jww7ZHtCheBgUPq4gDOKCXKQ9AnjT/eh36TfpmtA0yEd363fzORMnq41uUimWHJMBSbehKA0U1kaFjOLZNTrJHtXIYdIZuWQdw7VIUWRS8qNe35VVnF6zu8d1+ZOc75Ka9rsb/KeztHf6Dv0Xfo/9H8Dv3FoA+szSFaS2Uj6Lzm16dsWuyOfPjyp52pri2WvyQZKIaclmQPrSUn8EmXXz9pSywZJ4b64s4xiG3JKS5KnQk/ReSQrySxqhrzsj2crAw8UzLOptSH7dlorruhp9xu01XIdorQSwsT2SpPo3zCgWc/3Ek0mesuzj5M8ticDn+ziKXSeHFKebmmrA6kWlU8S3/otRV5oSfbqntv2hB7HNXJDPLDTRKLcLSOLPvu+ud66ZeBe40TZ5rlbzYmrUCiC2+sV/wnIya0bKYVfuH77SCalwjGhRXagCff9pbiSRdqvHNOS6T+qp32mlH0UIcXTWhYzV+u9R6L7SHOo1yBHFNniQJGCniSWvO+5FvPJPdS0B00o9hy863j6hGtyMVK20WtGnUO1x4Sl/Y+ZtGDY7CPVVST1FanfL9hzPr2EVxpyqlTtMSLZIhyTvZG0SSR/e3v/swuD+xQp1W4jQ3EkTqZoO6j2LBtVSBFSbUf16ymlIgPqjvvre/+NbFHSal8g/5i3yMhl1bLDtoP/5i6QJXY/fSrLjnvjUP870veB/UKTQUP/+SW9KfOHKF3yqfU3nfFHo+dI7Qwtpew7YN2LgVR+9BZ874rBy5XdXJ2IXFjRe8Moo76tUTTL9RhFvByKPoTEpuVNd4Zl0EhRHVxmsw/lfqC0cTnTnfcZQf8UHUXOqNBzx67NYMVH7/K1xnnNccK/Mq6l+dpgKjktEZrKr2NwJ7GTw3VvRJPXyr9+wtdkuit4bLGKi4Z+enwlOVP1eF+tmt2Y4/E0bazPrvBQ30/OqNC14ihikRzEWpX/9a35E5KrTK/Svlwn/vLb80+dE66HDy2NhSktXtzuuBtCz7V4+10VcPxqqZ94tjbLaiu5+pdezXUml/21cPWHh/OcFA277Jrz9rtN9pZL9jo5tHRYaC53LdikMmNbVTjG9pF4Xg23ncgpKNVV7Z1SOd1+qRZS+GlR/9GyTei6U5Tmz+s5f7Saa4Lo0TbhtAdFFFpsn+QHbUKRblpYxyhW6wdB0YPN8YkzaN/e1DAT9lDfJpTCb9vQzEGdrXUE20Rn3EVzoyP6fjJgzfbzt8T6fCXQRz3Rgr8Ea0qBw6Z84MXxrdl5i44kTd9h2ZSxr60lBRMyN/2rKRb1vvrW/Csl5n9LD7LKzHTVMS+Mftuotiic6XqBCXaLvhpFNid3hvNWA9riPMUWn3RRCW5mEdMo2ikissx23cC8fHaX15/i7HzFJp/q3Qb7c0oNXZX3+rkX2K6nVH63D7ukXsvyF810fxGMstO7s5BsYbE6+kneppUnLBh9lN7vL6rfVAaK9uSttl8tak1pNWNhjbYjfQCJ5Jh4f04ph7lishHWbIrkw2CkbbNl7pJJprjKjvR+tqSWb9In3nI6bKGujWgikdL20T6lXFR7/SK92CCSdiEnCJMKbqP9VulkTPBqpC0jJy5FgOh9glfv9QP9BexiFPtA99G3E3Su/Tqipo3glAKHhVoOqQ67xqhjihZZzeq+h8KC43QickvZ7uShRsxpWzjLPZHJ4iNdCi/x3+ZOdywwii3aJ6o3sL3Trx2IMsHF+LwZ8esiKtPMpmFc8MWsa2kda4XVMzjv9pRderfFnMr8N1wSD0SxJLm61fQobMnWS2ReIPKxckhR+9pwtcLEZgbWndJz3ZjlOqm6d3Gh3idz6M3D9Iohuczs2bwfp5Tf7W9INrpTSusOkI70fe1QqsVqd2auEe0SyWeiSNiGHFNJcu3Fbs5L4vuuf8RI0R+RtgeeK7VnA9hF+7aBPVA/YAvYReu2gf7VsRH0HlvPIOEsE5xSoIPy4y6xtPbI+Vwth1SHbckxtaPorP0tWGtU1t+5vFD2+nL0JLNkMRcNfWZUrpHsQOtkOEyJJaxrThAtUuv2N2QbZbLwECLWCoXVMz5azp92Zxk5pjq//w0SwVY00zW2KcG/lPYdjdLwSDv/wm8v+/a9ItwkcaXW0o91FXKyZewYOEyvafv2hdL4VfXYtC6zYsjc0PR9Ro3+1OPDB61dFucsvpT26+p7PQuHFJ5djGAP2EebdoBNtG0X2EQbdQP20J4tYBft2Ab6V88+0D2eQQ61XBJuARCkpUfOP9V2SAVuXkUGkiVW9E5p+/TmkCJIZpLdSLZwmBOfYPp1SBEp7WXQPTRR3SWHFOfro+mQIuhadE26dhcatFsCZdH7YInx55wNJhbfZHpcTw6pwEDHaxvxq5SLgp+qV1v0rhx8ulEcUgSVhcpUmbX+RmZhN5IzirZVmcV34OFDG7+fO8OxZcSUnEfoYzSHVDQXB1Z9IWLUD01eF3aATfSkE9hEWzqIdXtotfyoJ+rqAPpXT0fQvTb1pWW7wCkFAmw98zF6A1ZLC4pf2y6ToaHIHBOXn9Sr/CQ7lcEItqA3wZlgU/Tfs7EpgbLoHJPP1JV1GGq5qfkcNdLj0TXp2iRDmMqi3bpBKS3bUt+VubwND+nvZvJsVv71h57igjv1aAuKkhJMlERywNrZJzLNliiRZW6m9eAoOoq2Rl1IGA8fsWcL2EY/uoFttKcH2ASgbqCO6LHcsdx24aWG2LYPdK9NPWnVLnBKAVY6ZFKq4OwVzVVSRSaSzci6Tyn/ZarSOGTouCHNoDIYpFu4S/nHZICCmNrLolsoski5t87o9IucXZtzR0qZWnIGrs07d+ZTWXQdLSWL+wPlMLG5eoySCNjJQg5n7u6wCQs42XQHRRRFIlLtUBxOkXBOUZmobHj4wENhrOgGttGuTmAbgDqCOgJQF1BX9F9mtFWRf4bTwrVQT/RdJyS9V6hwfGIdb1buU8pGi2+NO9tlMyT0xjvzyXfpviBKGQJl0TG05owhoqQ6Gkg2JVAmnWLymjpN28UZn5c7w/mp2rKSDCRLOMqkyboxy3USa1s7q7a5teEZvd5TgQic3zucwupJY23RbbntZdMVnDG3Vgan4R4/RaJsAGj5gQzPIAD1A3VFrzrAZDtkAwAAoL/2Wmv9BSKlYpztgy7sx7SVtm9frm2X0XD0+8Z1sZ6jpEIatQwqi57LIMm2y5kxoqSCmNrLpDsoHaTSTU7q5Gu1srVZMw7ddlkOmsaPyqTHVJdcsGvadtgHRlhLJpDqUSnLXmXTCZSWszKzeLmWBqXhHNRS2YyQelRrDwCYqAKoH6g3QN+UDrrAAS0AoM/2OZb6E7zUANtA/+BQgVMqxvH1PfmvkFEdOJNvQFk00kn79DU5beQyOSwJFyjSH/Thm0vsZTXWkToQgfWlFJk6sYijrWz6YdH8QjMTbHJAes7mGaZuBMuilC1QRv1IPjic6yyF6yEhXL/TVjYxGA+CeCCMJV3ANgCALjQUNigBAP32mejrAZ5DgJZ0oSWbwCkVw5QPvJje2p+kA1EntctqGL6+amUaZ2KcUcpDZaEy6VH2omdrs1hbejKjkdteNn3dS0LqpE3ibtns0VxazzaZuPvIyqYt0iuGUL1IUT5ledOdPxilYrSXhdYiS2kvo07aWeOnt0MKPxCLD8Wx/rAOu6DMsMfBydr48W7YBGWFPQDuP9QNgDpiNHnglIphWvvmnce0uZbUvjjbZTUM3RNbzjLa/aTXMgk5bpRhOz4dlk3pHEcf7O+cs8+0FCUVJBAtpch2JGXTHJyd3rbhyyI9IIr2mo8dZWovo9ah1I+yxbNGq4PQcP0elVGPaS4BAACASFA28MLu0AIAAAAtPPPpXQ6gPeCUiuUGirPxkFUdOBOGi8zRa5m4LJ1t1Dqut7IVzGwaxtoicw7SFsgfared6lS2lPYy6mOAIPMxgXJJ8meRGpx2NkCNlHMqWKZgGbWO3ZI4XIvO2HBDZaSy4mEQD4MgRp5FNH5fot4A9ZFN0AEA+m+PjdyfIJIQAHC4wCkV24yFrKp1i8cYcDiiyzIJJoYadoCos7Jxzo7ttEzmls81q+8uyNaVMmpo8D6obct+VvvBIPyRPW1lCpZR83VDHNxZq4WHsnD9bjjLCkCk7/tYkQ8AoBoeqAAAAIDex5axPNZFquoDA6dUjFI6ZFKqsumjI5H7tMtsCCTOhhjtntJvmfhQ49Z0fZVNcN7v4MXh67UcLRKQTZHxiMqoEdpTqNGaZP7qzA0btTD4Ceegqb1Mfioj0sUBAAAAQJMjeQGnFAAAAACMCZxSMYpr9LUjILM6fH3VyjQhRIbR7ikqE5VNTzIXPFebqkjuMG5NF462MuqkQ/LzgQd9MGdso9bL0JmMnZVRK9jNCX3bdysnXJLrM1rNaC9T5T5lBQAAAADQDL03feyFFgAAAABgROCUilHitq7rAZnVodlnTTDqfaW3snGvPcnodV1XZeTsoPePYKJa60XoVMZOyqgdW/CgnDVh1c8RRjuFOcS8Zp+yarhqMHesjE9iqawAAADAwYdjHEoAAAAAgCGBUypGERJLhMzqcNTwOsNG5uitbLLZn2z0uq6zMiYc/MGcNWi9AF2QURdOqeC6PpzzPUatG8Gy6WENI5mz5lgZn8RSWQEAAAAAAAAAgFgETqlYRfBEyKwOvjJXmlFvK72VzeSTEo1e1Y1URpmzRsgYJTi3uhL9bE+WOW7h+0U2Q1cSpaxaEuePS9emPb+mLsfwetc4sxdsHUq2gCbU5cPKt3o/t25+EjShLovmF5phBwAAAAAAAEC4MEMFMQoXDUzoUGYjVLos505/pUEbFKVsepLXb5YbJK+xffNURqOURRLajzIiGYUBdP3DWH62nwd8IqMYG75t9oKtp0+b2H+9MbtDoZlosJdXtF7rcrLXfcpNVDJkePXza+rOv21EtyIMWqIHOaIye/T9kpvdx2QxJ3v+J9cDtx3vfBiaiS7kALGbR8xv9DWf4VRa/hcrll93c+9R86CZ6PP61vwJjUk7FnZX6sO7lfN+rK2zn377sEvqoZno882OWafWxTveUXZ7eHd7P7WtGHmpEdd91APkqDUPLp67J0Uca5Ybt9fU97gB9UI98p+sii/P3PxnX2NrliNeen3SFWM/h1bUtceO73dfT/uWyzz/vPDSHA+0oq49tnzgPTN9QO3P4987fis0oi704qF1gdQLtgAAkVIxC5dZA2RWh81ruxl2vQy9lU3ymfYYva7rrIwHjTISQvspPLsgo+YjqWhS3s9NU0Na33TeP+MOo9UNIUQgtaVW0sWR3skhFap3xs2vBuqxYPaYGZiqXNbeKQP+Tg6pDivYmx6iqCmMHKOLMyntT5K9+YzgcZw//nWKmoJmogtNvFuTmhZ2tJtxqSeQbaAZdWh3SGVRlbB0t/y2ecS6W6AVdSDdt0jSDfY6U46lPvm3A2TP09CKepBDinsa77ZYWi7ztjQvmbNkU5+wzkPoYG0vLcm4ZXP5Z009mp+mT91SeTbuUJXt8ba3SHjZv6o2pGxefNlP/aER9SAH4e5H034mW8zL2/YeNKIuVB+WXVH0C31QN1R69ocKYhNL2cqNkFkdnDnpO4x6X+mtbMLSbPg3GvVURsFE9UEfthg/WvMPhJ3I2FkZtUCvesevUpZxc+tAA1aPnm1GEZpwFGam9f7VBIpPjjum7b5hhl2L8Nd1RN2y7u9eT+7VvS9GjtHFI0k9fzUOLElNh2aiy/ah25xdsQ2IPOQgZG0OqQ76pu50QjPqkOCVjgs9burWkAytqIc5UR4Xepy2u2IwtKLq89CJIeM6vNijIl9ftTJNsUHH82n1lpRjoRX1KPlmzwlBe/i9puOhEXXp3b3xZItkyaaPq8V6HjQSfeCUitWBgixthszqMPrujCbOeZXh7imlTFQ2Pcmcd3vKLmVTa+CqXtteRl0gJFZx0L8LMUjzZehExs7KqAUoTZ9Zalkdei6uwf6GkSoGvaXmSvRnVeT+lxVe+uM9s7d9Nkr9+/9LpY/b22lpNjUvi0BbHak+IKy/R+nbVImMaTV/FXooS76G9a7yQq3pW69ydFlewfeyg89r2cHK0tZhBB9dKB0Zb9n1Y+g5u4d9CM1EH0rT593t/SjkVMvPTcMWxEo7oTXZHD1rX9urD2+K/zrW2m8tyWUWplc7unGvVNqjYvD3aDVUHdW+HtxL2OF4z8j1QOvyj3lr5E6zxf9BQAbGfuk3qOpb3J/q0e1a1/dBeyT0a3wOGlGX3Q1xnzuc9fOTEnfnpzqbELmmyjOfEFCCzju4w2XLWY9tVzZ9dCJu6YDP7zPMm8prbv36K87EOEMNPRlfOuKFMafrTe6ime4vlHbwDIO2bV/mzHCcqRd5C2e5JzJZfHTQMpk9fXLuSCnT5L30bG2W8NlKD/olif82d7pjgdZtQenKsuIyA46pigzvX8O1pk44xhzh6LMLZrlOWjFxzfLWOG/H2pq167v1eHTccFXXxXtz5Z7xu+32Vyl1HzkG5V92Xk5OwsKZrvNyZzg/DVt7HYGxX7jGUlTWwsk/pphamwMTGh6bdRXfvfPsaK3VQdEIpUcNuZdZ5MmMy7VMlu8+0nW9tDDWPhL7kE5s2RUP7epdf0ZyLf+5df3Q26KxPsSLFcuvsTSlXdWasKPK1Op6ZEr2+I16t0O47FP9xYM9y5xZFwX2K459JZLrCpGDmFL2UYQUOaSu6z96kZbbIrVsQzYxe7tlpZ57Z2GkZKF1KFqOXXsTRUhVbxr+xYWnnBP2NQe1ahMtPq8v/H5JTkv3mlN6dq/76bQe0yM60atFu2jNJmQPb0lcSvWxu1ZGasyA+tF1gqmwwrlujp7nLtW2EUVMkYMqluqFHupJpNGSbWJ13t1ozyDhLrOZgViG3sC+VkeyGgi+mhnMKdVWJh12EEz8omwM6ZRqL5ueBO70HhLcRk62VzUpfptsR1xGtSmc1XQF28bnMeY10XHvMN5FNOg5kkFZuAZNv/T8cWJr3N5joEHZu09SNovU1P3VI5MXK5sMmnjsXTVkPDP1PKngudodspDDmvbxSO0QycGs4KzW1NrcYQebp/XE1sSetMbZE9GwQfvk/sPtnw4ouo62eosIDge0dorfb77XXmdiLRLLcfYvp0mN+yJ93Zt7j1LaITYPw/VfU2bv8amfSScE6kh2Rb9I2qN9cveeSD0IG+FBfdd/nsltktgqFsdY1ZczX8w4Y8atkZCn3RmM9Vm6wNZlT99T7fU/lG4xPdB/7O8j0n+0OwWLoO3OIec2byy9Py05tXzy4GtnR9AeoItQNPqz2xvS80++NSJ6C6czKlag8X+kXvqJlENKD315uJ9VAADhA+n7Yhgu2GLIqg5mf8PHRrufdFsmzr8wbiXXV9lyZzi2KJuDRkFxH79cs+ruXLay9jJqG5k/pvxr6iiXzK8wWtXos+P47H3PtUquTVqRL6V60BvlXHxULrNXKvy21ZLPtIci8Yw+LqEy/nRRft2+5x1cTlVTrifm7nh8hTO+nj5Pzqm7UW8Pwkd6/W6NyXulUfTZGtLULM8Vr1XcfdM/d64+//WdM9vX2Yk5/JL9hOB+ckuFqi/WnPp67cVkkz8uXZvGYpQGu39cSIU7R01ZyA5HYgstTtwdjkzkkArd6n9Iz3UtT19r7Vmyh/2+pnrX0+Sggh3Ul2lmWc2cXj7vipdKFg+C/tWXe/SKF3L+2O/rJtoyoAnG/vD0I3q3h5HSiFPfoVd7GDUNLznSj1QGOKViGOvOIkqTVaoDUUvbZTUMm8b1WqFsWgxUpJb2MukOYW5ermz8Bqzi/vay6avDZvygUZFCiLGFM90DtCY3yUSyHUnZNIRjb6Wz7HBO/B7u4Cccg7nny/Lveb48f/uPF6397bFrM5jdJ0rjhH9lvJ9de6SpwcIFOWZ2MT455FSGkMRlzG87SouD47AOspUykh0oZV/o6SZvi2oRbJTKUvjtfwgey8L8kp4eRsJxXbc3bn7osc/mWqKWPa56Y9tYqzfxMa+wDUtttd3xuKvfhbH4QFiXYP7fejZS3D/Vkp0cUs1Cmr+ROR7/vKz367H6oC45GxbGMVFC+82S/KKa9vi8PrGKPnOWbNJLivaIQBFSoVu12qtLPl9WeMHyV29Uq63QSpu1vTXl8/SjbB9LNvZ0tNLx/qo/3/bZKBoHHumYVktt1pHIktLabWGNxf9URkGaKi/MUaTW4awdqrs1Mrso7+TEtM09Zf4EbfXY5hpxTUK/sP1Lr/YwIn3X93P9pnl0KTShHcIR2Yn0fTFM5n/ne7eclUOL7P1B46J+QLIarfL+OHX5R1aT73IjlKfVb/4oGutLRIK821N2Fc10LzPaulLK4GtZrlI2vcktuPiACXbNQb5iUr51l7K9VWOS38VCoosOWDY92MDEZnI/C75Z7FfOODLKh5yt7H8axvvzkNI8hONB5/Wt+RN2WfjjbaMfL/t5eBVL9LjOmpKpDWdUp3bZ11mogh0i/QAaLCOtIUUp+yhCihxS0/qdrTsHu6HGLKecU7Tw+yW5tHZK3O6e30845UrVUiSViMTuod7ZPsLTP5r3fCTq4OEwfsTVNyo2eeFY8w9N2Sc+qFobRvrf2N40CZkNidU6QjZY+H7R0JHdFyWln/5gjVpyJDP51Ob2/berU/LYYb58qKV6crh1pD1l3xNqyt6Uve0Kp8uSw5r73aIcvhLL/Ui7I+piNWUwx9l/V9q4+xo5t5lepFx+pPel2nXkSMdgH4+6IXBPqvXGnJfJC81y43a17wuN1ZH71bLHu5XzCi1MunBSr6sqYI02KLVlvkrXpufWcK3hqXZ7Fa7nxfYU61Ff/5migSRJ+CK5fmssg0ipGMdSVkgTdC4Ni+hql9Fw2Nmef6Is2kBw+TWj3V96LVNVZvFnyqb24IVjU7QULRWQRZGpk6/VtpdN8+Td6XxYSOxkJokrGWeBqBAu9orcCdsAtbNBale+0/XO5NcTprIlfqTW9J9zR0pZpszmhFYLZvLM5SbP6uC6Rlp4UAi3Q4rKRmUMrDuR2HOqZGLDWrhprdoOqWkT+6/fZq7viAySuG9KtHWtheuRY4rWAVF7zY677SWf2libPSzcs+7mXdKcWB3Dky3UdEgRffvWva48TS4WjJU4uHy3XupGJOShl7PSz1TPIUWM6FPzCGtI+Cyj3j77Xue2hVppP4xyXxwqZzdn/t3l9L5ck1A81Sg60bNNTK2uR/rGp1ybXZiyQu+6MML6OI2taVdyKekOI5dfT3YKh0PKSOkt1SalyLHYCHowQltF47twOKSMMNaNiBxaX5AOFSXybDnrsSeZdqOlnhrw+X13G1X36277ulKpgxk6rz9Vw54f00vPZaDJUIcpsYT6f4PcWrVuf0P26LszmvQofOFM1wtMsFs6ue++zJnhOFML8hbNdH/RaaQdZ3NyZzhv1Z8tyOEmNisFcAtrc988HUbfBQlESjn4XmvfxTW7R2s1CqdgZtMwifEeLl9DQbAuF810jc2Z4YzYS5RdGRNGasxEZavMLF5ecmrTty3c1OEstHvFvbdljX5CbXtQKqxSa2vzo+OG74yGnrU4tqV1agZl7z6J1l/TSrpLLdsAzx/6swlsoy2bwA7asg3soQ1bwA7a7DdgJ23aBPUF/QbsoI5dulpOREoBFldS8DemzbWlSttlMyx+Id2NMqgPTfhSyjLDdHRKWfTqkAp0kibPo6yTdb7ICVT0rGua2rKSDF1I/ehvL5PuyJ3h2EIOQErhJ/lsf9BzvaAUCN3q076wtlh89KF1pLScFi5vRvw6ckBFsy4HI9MO9okkJbm1J4c6pNqEYrdpwR63nDOwNBwOqWg8CETi98mp279fZY2XexZyYSqmdTn03B4YKdqA1uSgtVIoyhA2MY4csa4L2EFb+oE9tKEb2EEf+ohlOxllzTWj2iZaOonmtYC+gVMKsMxN/2pq7TfsOq3JRTKRbEbWfffvZ39AkUY67tiqqAyGMIbJM5eiQQww3HC3lUW/UPoyZRjzdmffEz7+WMEs10lqyUnXJhk6tYhSFiqTXu0hc/HngL5lPq3gudpUvZaDZD/20z6/OeVfI8wnLxgx6ubeo+bprm7McC5T856PZF2KZARYrDwYRvIBsNEu/yn02OrzPAYniPrX/WDDG9PMdbw8y70rP0uWShd+vyQHNlH/+iWrHhxU8P0rUxf98uIli+YXmmETfd0HsA3soaX+Nxq/j3qA+mI0HcAO6ukGuo+NsW64kPRsyHB9AGND5k5YpmhVQ2/B8z+0yWRsMv873yv5Gq/Uq/wkO5XBCLagtGTCJB7RezmoDHpOsdZRDs7+r3MnoXBwmX2ixvpSdE26NsnQSVvmbiuLjuvGdOcPHdFSrfZ/6La9Csiu3DNKWahMui2HYHaj9YXBMtE6D3HCv3Lvas6eN/qDyZGORaMxnpX80q9SDad3a4mH/tW7Fjk74rzxD3VUFcmTKCyOvxnBJnq+LjkGq319i/1MmpO2M+598+DiubCJuteiRcqrvpz5QtVXs7ZWL5jzVfUXD/Y0Uh+ix98nm5Djduuyp+8hJy76dPX7KHrRRK9Rt4i0hU1gB23oCLqPjbFuWJ8xcTuAIAM+v5cmG9/QgChvtMsSE5DzrdVvfldvcpPMRnMcVvcqflxpqdfruHdbHyiDAaC0cV10EqYwIVaumt0YtbfDA9dSrsm6sAYZlYHKond7CMZuZpStk4lraO0fvclPMpPsbWUIlEW3UESRHm1wMNsEo6RoEVm+e+fZtI6UUxbzUt3iAi2sJxWth4RDfVCI5mSVg8nvhx57bNZVR7ogdiw9HEbi92WZm8kRFXquuXdJOuyh7vWsCTv3Wj+yW6Pver1HS0XLJpG6xoge39zRwqWbWxjP9iS2jmVNPd8xWv+hN5ufmJL/tLdp9+xqr/+hyj3ONUZyFIb2z4ejQzVenn6pZPEgsbN0F9u+dilt6TgWxlFavvYFy1+98bLlz7TSZ/SKF3L0Wg+Mck1y1v5m5T8+po9e7RHJ+1aN+kd2GPvD04/QmreoH9q4HtmEXjo5lP8DpxTYi/U3nfE7pq5j6o12GWKKBpf1Tj2l8SNZSWaj2YEmRAUXN7JO1jPSKH6SncpgFHtUpm/4RxedhCmSV/qscJZ7YqRlomvQtVgXHFIke6AMBiCwtpTEnqJ9IdgbekrjR7KSzAGTKGUwgpNQZmJH/pNVuo9SoTJQWULP3T7sknpyRN3Sa/S1tA5YrI0HtBrpn7oi/v5Ws+0+ckaZLdanKotTJxhV91r/zSAXXprjiZPlV0PPmRrEG0ayhx6vY/Yk7mQGJVL3c6TbNLufZ4cei8SWfrCNevYgtsm+vZ73y5xZFxm53pQOviipfNAFeWWDLhimfFKVj4U+yrlE5W9WtbP4eKvX33CwY7RT0b0WTeo6WOOcjod8qfmvsIe61/lXY9m9dZydSx8926OzZ49o/L9wQI6oXabmlTWSuCff+dkrGOuqfx1yEPbyeVe81a3h20P5f3BKgb2gCe0Bn993nbL7lAqXf4qubaRJ9a4y5q2RO9vT+LXoQNwWkpVkNqItKK2XMDHdDTRIZj2nJNsfNOEmmDy5i2t9pTBZfFQ40/VCJCbr6Tfpt+karCsOKUrbp8hOZTCKPcozNjyobAqVT5bktb+rhze/SUaSlWQm2dvLoP92akb8Ooc58TS9l4PKQGXB6Esf40NyFk7LGHPS9Zlj7nl03HDDT7wf7gNcNB/SW9cPva3V5LuDnFMma+PkyYOvnW1EW+jpt+t7N842yc0/Bo9NTL7FaM824dJdtOqJybr9i70HzWIJ2i/12i3C4nCuDj3ut6epyMh9aN+NC+uzNi0qkASjZ5T7lM+jQogRyrkG5W+tasvXJ8VZGHqclpxajnZKvbZLksRefYZV8LVGsIcefzvW0OMyOOQoRN1Qv360cjE8MOayrz7mUNLAcqUzhOHAftl65mOXCs7I6+yM8KVcXLAb+39x3/uxrvPVt+ZfKTH/W1qWUWamq455YfTbRrdF0Uz3R0r7OFEPsirt4YKcGY7fGtUWhbNdNzAvO5Q3YMqYhT1YlV4870gngsipkVE95Brl+g+yNsdGF5+22Y2505yvGs4WtH5XMHUhZ3NyZzhv1ba8rheYYLcou7VKRRlphCip0HuzV/mQUcHUd/prY11jKzOLl8fiiygAgMMjHM+t0XiGDKQuydo5bKTrh/L0Mx+sgV3Uf47fkT99or+175nNJlGybtfop2Op79k+6EJat9HXd+NCzawDTOtIxTUk3VluizuRSXH/zDvlxrkMqMrzP/zj8R6ZtqN2lHs29yk77U9GrCNH0oeokZqMInLIIXVxQtbjlEkAdlDPJjTJ/m5T6Zu0b5Ltf8s/+dYitBrqQikua611FxrFHpHwzUSzjlCb1b118M2yVPLDx6NueKWr5YFTChyU0kEXZHj7nvKosntthC7xhmX793/ss/HjKmi7jXV35t8lvP6nNFnvLKY/DHtmdEys90UTCpmVQxYpbeQZGm8LvyzvVTzBSBE5+6NolusxIbN7D/G/lQkTmytxz7ycO1LKDul6z9ZmycJ2DfezqexQnFEskCLu8ZzpzvsMawtan0kweuvYpOj3gbw7nQ9rUc6CZ1x/Uuz3kLLrV4YMZ+rVeXPQMlIaRW9cT71FGxXMbBrGLC01eben7EKvDwCIxoM7nh0BAAAAfffn6MsB6oQ+6gmcUiCsFE9dNDZu27q/KHfLmLDYj7GvW/oN+78hcycsg3Z/TXvEFEWGxGlEpBaZmW6MhQipUChlm8OUSPdorkZFLHT7G8aOvjujKRbsERL1cjidxnrOxSJhYpuEn63ngu8UlubAG2fca08SXKRxExvK/WygEHyC0osOPczGTfPRQ2GxxaymK5jMA+2BFp1wezkxJXFl7vT4d4xbL9wDaKuXKDC9yQsAAAAAAAAAAESTQ/HXaM1HAqcUiAhlgy4a3tr3JIqamqx8+hzify9VPh9Yt//wRtbGf6+FNg8OOQJlc8LbSh3NULmuVdEaUrHqQCTHlNOctEBrEVMUIeXy1U+MFYdUkMOMmIqOTQweIbUv7WkVX1J2TZzxeT+MFZuY1Xe68Fk3marKHrnlnIGl4brWLw8tHS6ltQ6Wd1o3HP3AuLUHra+mpOcFE9coh35mYVOMmEZxX1bNbswxidYdhxoRGPX6+2xtlp9be5w4LQEpLwAAAAAAAAAAAIMBpxSIOGUDLzy6td/JY5hgA3l7JIlyN/UP2Iexre3HhcrBJuu2FV9nbVr4C7R2aCy+7Kf+fXs2fSB7fTlqXF+ymIu218RPHv/e8Vtj2Q6Uyq935eCnDztKJ+wNIJtT0WvD742esu9AtKdl+xuj9cO1gV+Y2F+1msYukhTOdJ3HBP+gfLA5bntmqD1Edf+Na7LCkY9+y9wlk3q66uZ39D0b48ftz0lOKeE4kz5oi3LjbmWEMzl3hvPTmKkXbeX3aDUCiSKkBJNteks1CAAAAAAAAAAAgK4BpxQABoEWs+//rftO4fXThHe00vm1cIvpT1tPdTyDRej/R+Es90Qmi38quykqiVDLJH5T7nTHgli3Rfu6Rm+wQ1zvKQKUKd3RtUZcr6irUJTOuty4ggYn26tjNnNX7m0juh1xRMyuOW+/a2uWLgsee+zye6m3XHl58Jicxr2qB9/F/fx+ZfjjIJvIFnliLEbjBNZCk22986Y7f9CSXAWzXCdJkqdC65FcAAAAAAAAAAAAOHy66muSoCoAtA05hYY9M/ofuxocWYJJs5RTLRG8XAtdg65F14RDam/IGSSsnsGUqiza16Zr0rXhkGqDnECKPk5Qwxb72OSEWHZIEeT8cTn8L+17fuTX1mPD8ftuzksOdLzmpaL7h9aYtrdFzgmHxyQ+cPsbhsZqejhy+lRmbPgvOdDphQa15SEZSBaSCQ4pAAAAAAAAAAAAEIiUAkBnfH3VyrSURM/9EheXhmu9KVo3Shb8/doG2yNj3hq5E1runMCb/4L/PdJrTdHaUTIXf9Za5IOWCERNMf5cW9q2qHRA6zkTt8e6MyqUOUs29ZEzMxb55Lhj6LhvOfPXdWt78WX4Ku+fm1sbnjnc9c92L34gQS4Z8pKt2Xyqx+77dpv/nGlc2C5IEr88muEr7hH83rb4gX8aMSXnkVi3g6/ZHJ/t272pV/mQUTITO9RKl0fpBCXGe6CeAAAAAAAAAAAAsQHS9wFgcOgN9KO+rBrlMyVeYLJIow513SlaL8rvlZeb/Q0fbz4jYzmiog4PSl1m8pruUFrSSe2pw8LRwrmVFu5Dv8X/bKxGfBxOfcioGDqByeJ+1r7GXQQoZBJ/pKr3+kWoL/sn4JzqnZbmE87C0PO/+dIXuKeFJH/GpZblhxo1Q2nphBw3isvS2cG6lmr9L0ttLe34zr5p/WKN535uftHPTVNp3yy1rJbKqyYMW5Ow22FJOlOYm5fn3Z6yK5zX++WhpcPjUquv9PLuReu7p/47WCcKnqtN5T77KLe3/ovDdUQCAAAAAAAAAABAf8ApBUCMQRFUPZ07hntNSb04E4FJea9f6klbi0muCTQMjBda/PWVNa4eaxERFV7yn6yKd1gSLmibNBdj2aGvdVTGGV9Gk/Zub+PHmMw9fMhRKPmkG5UbfjI78vW/ahlnH8hm+RU4CLvGyytar3U52euh545b5WHOBlNoJ75e6cU3KqOQTUxiFYIzF/PzusDfTKIbF8zJZNabCT5QGaUM2k8UXOFg9kk6Y3Jm8EQsO6XeXLln/G674z+h50zCP/f2Y+03037QUcSl5sJwpNEjh1SGo2ZNqO63e8+5R8j2XNnkL023LJpE53e2nv1CLKbtIyd5eUJir8zGhkotOLBp3TXaXnhpjgctFAAAAAAAAACASAGnFAAAqAhFdjC/7ShmYsNpcl1pahO54M5AA82FS2nCGmgynvnZWmbybMZ6K5EhkELMxMc1C3Gqzc/7K73H0ANHtHG38rf1HpPYauf8W9kvlqqV+kzPPL+mLmfvSClRffI3redzWTpHkvkYpS6ccuhRhdyt1JnvZUl8zWX+Xu4Mx5Zd/3km17YhfUGbY0oqr3KnjT/6gXFrY1LnP7n+5DNZHgo9R9FSmes2jSwbPHQWN/lOtlkqatzrrdNPKekZeFlBtnjWHG70VOmL7zza3c3vCz1XbD0rzfXDZt79hJ1Voef3+HOSY8nJTve/iHMvlWRzoiz5GniLY9xtI7p1OLQpmtA+sPrYeFvpj5N6XVURzmvTyykJJx5lC+1PZr249aJ4ufpftO/o5n/qistH3YtWSl1Ofb324j7C0//k5JZPp03svx4aUR9yJCMCGgAAAAAAgCMHTikAAABgPwSiRrz2JMHkQPQAZ5JHWJrrw53eLJahaKmWhJbfMy7XMlm+O3RSnqI2MnYMHCb5+SDO+DFM5hmKERKUPyW0f6WRCeUjiSrBxGrZJDZW9di0bn9RHuXHXWJpnnTLIPuHczZm/ne+N1b1/WtHYFukFJflMm5v6nBWCb9t2W3DksbRPkV3Os2Jecoo0KGMqNy0/lSzr3H7gRxI9H27OaEvrRPV1/bvKbZm6bL//VUqj//9pVlb5i6Z5HL53gv9f06n+bIBU8/5MGZssbbxZ252H9Ohc59j9W3DE46l/dnbPhsVb+rxTfBv3ctr8i485ZywRGCuuzP/Ln/fhsdp386TP9uQabuQJtlffuF7f+j3mjPSR8SKI4TamqpT3ZP9ski3+F0Lp2SP37jv33f/pvH/bFLpuHrR79mbe4+aF2mZrnit4u4Ef9NjweNzE5qciGD7Hx9WvtU7zbyr32k9pn8bjeuRM+pB94CFTGbjM7jvmU+uS5sBK6jL7AVbh35Wn3BTMm/6+K1r+2FNQgAAAAAAnQGnFAAAAABAjNCWNlE8poyQ0s2m5mWCfT6Rs7MWcJNnbOj3bh3c84ADqEAaUnNSz4DDlvOE9hFlIzlu3b76mqDDavfiBxLkkiEvtTmmpPIaZ9IMcjwVT1001juIfxH6m3a347hYiWCjCe6yEVm/co5mrSmzkINoTmX+GzZfwtUdf5Dqn7o+c8w94bhu37LqvRwb5Ayktb5qdra2hJ6PFacU6WTHMfH/kezNZwTP7esEfL4s/55ujv85iJoa+ZB9HVdHAkXFtZSYT4jL9v14yzkDAwvgnf/6zpm9fbvvCH5neIolZpyEL5UsHtQipB59VqUW7M8R982OWafWxTu+VHbjvLu9H12SedvFkZbpqje2jS2Wk78KHp/Vt6Lno+OGI701a3PaWoeuf572t0i2398+7JL6aFz3hNfqtiqdVLZgrOTH67v1hyX2blNKra3N0bxHqY6YW1K/tLH6JS9OyTwPVlCfJ+fU3Timxboub7rzB2gDAACAFumqr0mCqgAAAAAA9M3vTra+cecxcRnZxWvsFA0VmEBsNX+11+DQbzvoW+fkdKLUiJS2kiY7Ah9ln86FRlB1H/9QI63f1TRiY/qyrAH9gpFQQ+ZOWJbsaHgy+D3aj6WUiuR42lfHdBxMC2aT29Z3DOIW0q5wXfdA57v3kW8PHlvNze/HigOkKm/ngFCHFFGZ7bgj9NjB5dTQ456VPD5c1//wnWVnWbbu2kapE2lLx4E6wZs+Dn6Hy84lsWKP18q/fiI+QRSnJPq/Kf+NayNFRO37HZvkO1XZxNG+pbslLxpyZWW51pLzI1BXle0ptZ66WLAH6f+DDW9MW/j9kpwDThKcUDChRZJuoE8Pk/uaaMnGJVaMHv3XUARZctXmkmO2l9Y8t25+UrSuK8vWwDrFfh6XBSv8D3Lavj614J+v5ZVOjOZ1yTEpC/NLKyy+F2GFvaEUxmpct3Cm64Vvn2l6HxbQBrSEQsEs10nQBAD6AE4pAAAAAACDEBqBkLlpwz/klsS5lEIujtd9IZXVXBfOa6Wf+WDNvg6RPjdf8UdaR4o+tB9zA2tFx+SIovWkaBuqc1f9zoflZvuXPq9lh8fc+KZoqJkbruvy+Iy/BPcpfd/a5LRPaP+35586Z8vgxHRv/9R+10w5/YpYrhv7OgUTpPJneMuuH2m/viX+zXClUiTcTfJ1+zumdGTd+3vTXRbrGed1q74wFvROk7dxdvcfgsfd7E1ZjXLmnft+7+emYQuUTWA9tAx3wwvhloOi1Ka+vNF/0z93rqYJfjpHESdn960YaefyJbSNhXWlKCLNnOzeYu3T8qw4rrRw0S8vXrK/71VZ+q4O7vfsXvdTtOQ7K6viOrLHTRk7x8RC/SAH4eI1b74yv/z5fx3IFmpyqaN6dqul4b5ma9OdsWAPirKllwjoQ/sH+p51gdRL6XmvYyfU/CGa8lHUrcR9U2RT7YMY8f4PckhtXZtR8+HEddOifnEuvrVJ/DNYYW9eObbsrwerQ5HCz609JInlwQK/btvUujald4cFtDUu15I8SN8HAAAAAACAzvnloaXDabthUOKGWJhc74zQdInkCBSmstPCmZ7vYLzz7vLH3XWmjslKRzf/U1dcPureWLQDRXV0T3fuFYHU0uzYb+pKmjTZPnSbM9yp4igFmdPb+mXHc6TsjNlUZOT8sHS3/DZ43LzHVnR5r2tyD2S79G4t8ZN6XVURbjlofbWNzPE4Rag5uHz3t9el/CsW7fFu5bxCe52pI2Jth7P74AO1U+RMNdt9TcF0oOHm1NdrAykz73VuWxirfci8l756x2JpCayZ6fXGvXewlznmnV88Oi3eUz7+veO3YgQSGcjJ1NSj+WnBxKr0evPVB9M1Ra31G1T17Zi3RiIFa4RYfNlP/SuLU6+WrGL19QV9FnRmu7grmudgPBp5m3SlDVp9a/6Vx7ww+m1oTBuQY8joa8piTSkAAAAAAABAzPL61vwJnjjebfvGboujuQ4Lrc9WkrT+JcG8p3Bm+T67fuiU0BSYsQY5CJPimjrWUwv3+l2dAafU/zgUp1SkoMmY/3P3dwePY3n9qH+ve3uvyRiTtXHyhKNvnh9tOSiSsEqY26KhJLa46NpuMVc/yCne1LSjNfRc713HJ6rRdpMDcvWuhPe8wjZsmN835e5bur0Sa/agdqJuqWj83xnx+nVz825SSx5ay0tI3oEJffc8HynHsNbrx66He6xTGqyj6dic6RpzzSdD8tWSh8ZZdnNCX0ozHqtjq3l5297zeU2TzRb/B9cU9LsMo351IUds/x67T66pj38ALyt03Sllxq0DAAAAAAAAMBrX9R+9SI3rtk9iXgELtJH5nePG2hz2oaN1W7aUaPpoSnb4I28OxqWO2uWf1qUvEZLrHDpuivM+Fau24FLSHYy5KbURrRFU1su8e3q0ZZAkgTfn2/Em7fnIUp/c4SRsccZ/r4YclcJ8YccrvzIbH4u2oIiON15aUmq1yH3ouNUrlar1MsFn9Qk3pQrbMNpfZzK/pGxizimlpXZi1otbL2qRFTv4zcy7Je5s5dRxsWaPpJLetp3Me3Tw2F/jTFFLFnJIOUyJ65lgvYpmuZ7Kme68L9bs0Zay0jSZ9skxpejkd2q+/FT0rGuaKWnrFf7d/e/PmeFcFov2MDP/025XEuuZFAj2V/UZoOC52tTuloIxFQ3HLtH6S3FYUwoAAAAAAAAAQESgyV5yEE4efO3sSKSC68r1KTKK1vM6N6HJSWt7xaotSP+mT0/o79vjyOSLcgae1mP6t2rYg9aNogipYPq+WLVHTX2PG1pNvjuc7vjHeX1Krhr1g+jFfQs7DiS2OFbt0ZA28FxK20cf2ldLjkTRWhnct3BPTEaCUDshVThPp9R99Ml2yferJYtXdEthMQ5NbAvB/8YZ+4UicwZMtnyhliw2W/wgarZoX8js9Fi0B6WpJDvQPm3VdjzYHeIZq7ffSFP3rY/Eoj0aLzTVeWVvCe1v3dF9hdryWBIqF+1KSXjfkVkxS+u6Q/o+AAAAAAAAAAAAgBiEUnM97up3Ie3fbS/51OhrXWgdSl33vjt9WgO39jovvW5mLKaL01r9KN7d52GbL+7sk2TpxhOnJRRBK+pSONP1AhMsj3H2YO4M56fQiPr2iLNuvblF7v+73GnOV2O1nUhY6O+mhXXtCt8tfNlvabnesdv25xFTclRxFHbZ10Rf1OMHAAAAAAAAAAAAAAAAAAAAtKXwU/P6XfXtcDh4AAAAAAAAAAAAAAAAAAAAQKTBmlIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACLO/wswAA1Niv+YaMCdAAAAAElFTkSuQmCC") no-repeat; - background-size initial - background-position 0 0 - transition background-position 1s steps(25) - transition-duration 0s - - &.active - transition-duration 1s - background-position -2500px 0 - -</style> diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue deleted file mode 100644 index 51c73003d1b544ccd9a7b7d6de85606d83a24864..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/poll-editor.vue +++ /dev/null @@ -1,235 +0,0 @@ -<template> -<div class="zmdxowus"> - <p class="caution" v-if="choices.length < 2"> - <fa icon="exclamation-triangle"/>{{ $t('no-only-one-choice') }} - </p> - <ul ref="choices"> - <li v-for="(choice, i) in choices"> - <input :value="choice" @input="onInput(i, $event)" :placeholder="$t('choice-n').replace('{}', i + 1)"> - <button @click="remove(i)" :title="$t('remove')"> - <fa icon="times"/> - </button> - </li> - </ul> - <button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button> - <button class="add" v-else disabled>{{ $t('no-more') }}</button> - <button class="destroy" @click="destroy" :title="$t('destroy')"> - <fa icon="times"/> - </button> - <section> - <ui-switch v-model="multiple">{{ $t('multiple') }}</ui-switch> - <div> - <ui-select v-model="expiration"> - <template #label>{{ $t('expiration') }}</template> - <option value="infinite">{{ $t('infinite') }}</option> - <option value="at">{{ $t('at') }}</option> - <option value="after">{{ $t('after') }}</option> - </ui-select> - <section v-if="expiration === 'at'"> - <ui-input v-model="atDate" type="date"> - <template #title>{{ $t('deadline-date') }}</template> - </ui-input> - <ui-input v-model="atTime" type="time"> - <template #title>{{ $t('deadline-time') }}</template> - </ui-input> - </section> - <section v-if="expiration === 'after'"> - <ui-input v-model="after" type="number"> - <template #title>{{ $t('interval') }}</template> - </ui-input> - <ui-select v-model="unit"> - <template #title>{{ $t('unit') }}</template> - <option value="second">{{ $t('second') }}</option> - <option value="minute">{{ $t('minute') }}</option> - <option value="hour">{{ $t('hour') }}</option> - <option value="day">{{ $t('day') }}</option> - </ui-select> - </section> - </div> - </section> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { erase } from '../../../../../prelude/array'; -import { addTimespan } from '../../../../../prelude/time'; -import { formatDateTimeString } from '../../../../../misc/format-time-string'; - -export default Vue.extend({ - i18n: i18n('common/views/components/poll-editor.vue'), - data() { - return { - choices: ['', ''], - multiple: false, - expiration: 'infinite', - atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'), - atTime: '00:00', - after: 0, - unit: 'second' - }; - }, - 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() { - const at = () => { - return new Date(`${this.atDate} ${this.atTime}`).getTime(); - }; - - const after = () => { - let base = parseInt(this.after); - switch (this.unit) { - case 'day': base *= 24; - case 'hour': base *= 60; - case 'minute': base *= 60; - case 'second': return base *= 1000; - default: return null; - } - }; - - return { - choices: erase('', this.choices), - multiple: this.multiple, - ...( - this.expiration === 'at' ? { expiresAt: at() } : - this.expiration === 'after' ? { expiredAfter: after() } : {}) - }; - }, - - set(data) { - if (data.choices.length == 0) return; - this.choices = data.choices; - if (data.choices.length == 1) this.choices = this.choices.concat(''); - this.multiple = data.multiple; - if (data.expiresAt) { - this.expiration = 'at'; - this.atDate = this.atTime = data.expiresAt; - } else if (typeof data.expiredAfter === 'number') { - this.expiration = 'after'; - this.after = data.expiredAfter; - } else { - this.expiration = 'infinite'; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.zmdxowus - padding 8px - - > .caution - margin 0 0 8px 0 - font-size 0.8em - color #f00 - - > [data-icon] - 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 - color var(--inputText) - background var(--pollEditorInputBg) - border solid 1px var(--primaryAlpha01) - border-radius 4px - - &:hover - border-color var(--primaryAlpha02) - - &:focus - border-color var(--primaryAlpha05) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .add - margin 8px 0 0 0 - vertical-align top - color var(--primary) - z-index 1 - - > .destroy - position absolute - top 0 - right 0 - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > section - margin 16px 0 -16px 0 - - > div - margin 0 8px - - &:last-child - flex 1 0 auto - - > section - align-items center - display flex - margin -32px 0 0 - - > :first-child - margin-right 16px - - > .ui-input - flex 1 0 auto -</style> diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue deleted file mode 100644 index bd5eeaf832d3340d8404cd938e7f527f1f717e44..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/poll.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<div class="mk-poll" :data-done="closed || isVoted"> - <ul> - <li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> - <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> - <span> - <template v-if="choice.isVoted"><fa icon="check"/></template> - <mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> - <span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span> - </span> - </li> - </ul> - <p> - <span>{{ $t('total-votes').replace('{}', total) }}</span> - <span> · </span> - <a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a> - <span v-if="isVoted">{{ $t('voted') }}</span> - <span v-else-if="closed">{{ $t('closed') }}</span> - <span v-if="remaining > 0"> · {{ timer }}</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { sum } from '../../../../../prelude/array'; -export default Vue.extend({ - i18n: i18n('common/views/components/poll.vue'), - props: ['note'], - data() { - return { - remaining: -1, - showResult: false - }; - }, - computed: { - poll(): any { - return this.note.poll; - }, - total(): number { - return sum(this.poll.choices.map(x => x.votes)); - }, - closed(): boolean { - return !this.remaining; - }, - timer(): string { - return this.$t( - this.remaining > 86400 ? 'remaining-days' : - this.remaining > 3600 ? 'remaining-hours' : - this.remaining > 60 ? 'remaining-minutes' : 'remaining-seconds') - .replace('{s}', Math.floor(this.remaining % 60)) - .replace('{m}', Math.floor(this.remaining / 60) % 60) - .replace('{h}', Math.floor(this.remaining / 3600) % 24) - .replace('{d}', Math.floor(this.remaining / 86400)); - }, - isVoted(): boolean { - return !this.poll.multiple && this.poll.choices.some(c => c.isVoted); - } - }, - created() { - this.showResult = this.isVoted; - - if (this.note.poll.expiresAt) { - const update = () => { - if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000)) - requestAnimationFrame(update); - else - this.showResult = true; - }; - - update(); - } - }, - methods: { - toggleShowResult() { - this.showResult = !this.showResult; - }, - vote(id) { - if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return; - this.$root.api('notes/polls/vote', { - noteId: this.note.id, - choice: id - }).then(() => { - if (!this.showResult) this.showResult = !this.poll.multiple; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-poll - > ul - display block - margin 0 - padding 0 - list-style none - - > li - display block - margin 4px 0 - padding 4px 8px - width 100% - color var(--pollChoiceText) - border solid 1px var(--pollChoiceBorder) - border-radius 4px - overflow hidden - cursor pointer - - &:hover - background rgba(#000, 0.05) - - &:active - background rgba(#000, 0.1) - - > .backdrop - position absolute - top 0 - left 0 - height 100% - background var(--primary) - transition width 1s ease - - > span - > [data-icon] - margin-right 4px - - > .votes - margin-left 4px - - > p - color var(--text) - - a - color inherit - - &[data-done] - > ul > li - cursor default - - &:hover - background transparent - - &:active - background transparent - -</style> diff --git a/src/client/app/common/views/components/post-form-attaches.vue b/src/client/app/common/views/components/post-form-attaches.vue deleted file mode 100644 index e051b6a808a578696670d716d816bd4fbbad42ab..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/post-form-attaches.vue +++ /dev/null @@ -1,139 +0,0 @@ -<template> -<div class="skeikyzd" v-show="files.length != 0"> - <x-draggable class="files" :list="files" animation="150"> - <div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)"> - <x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/> - <img class="remove" @click.stop="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/> - <div class="sensitive" v-if="file.isSensitive"> - <fa class="icon" :icon="faExclamationTriangle"/> - </div> - </div> - </x-draggable> - <p class="remain">{{ 4 - files.length }}/4</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import XMenu from '../../../common/views/components/menu.vue'; -import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import XFileThumbnail from './drive-file-thumbnail.vue' - -export default Vue.extend({ - i18n: i18n('common/views/components/post-form-attaches.vue'), - - components: { - XDraggable, - XFileThumbnail - }, - - props: { - files: { - type: Array, - required: true - }, - detachMediaFn: { - type: Function, - required: false - } - }, - - data() { - return { - faExclamationTriangle - }; - }, - - methods: { - detachMedia(id) { - if (this.detachMediaFn) this.detachMediaFn(id) - else if (this.$parent.detachMedia) this.$parent.detachMedia(id) - }, - toggleSensitive(file) { - this.$root.api('drive/files/update', { - fileId: file.id, - isSensitive: !file.isSensitive - }).then(() => { - file.isSensitive = !file.isSensitive; - }); - }, - showFileMenu(file, ev: MouseEvent) { - this.$root.new(XMenu, { - items: [{ - type: 'item', - text: file.isSensitive ? this.$t('unmark-as-sensitive') : this.$t('mark-as-sensitive'), - icon: file.isSensitive ? faEyeSlash : faEye, - action: () => { this.toggleSensitive(file) } - }, { - type: 'item', - text: this.$t('attach-cancel'), - icon: faTimesCircle, - action: () => { this.detachMedia(file.id) } - }], - source: ev.currentTarget || ev.target - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.skeikyzd - padding 4px - - > .files - display flex - flex-wrap wrap - - > div - width 64px - height 64px - margin 4px - cursor move - - &:hover > .remove - display block - - > .thumbnail - width 100% - height 100% - z-index 1 - color var(--text) - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - z-index 1000 - - > .sensitive - display flex - position absolute - width 64px - height 64px - top 0 - left 0 - z-index 2 - background rgba(17, 17, 17, .7) - color #fff - - > .icon - margin auto - - > .remain - display block - position absolute - top 8px - right 8px - margin 0 - padding 0 - color var(--primaryAlpha04) - -</style> diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue deleted file mode 100644 index afe51d783359944a3a37f5d2c1d2f70f379a2f19..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/reaction-icon.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<mk-emoji :emoji="str.startsWith(':') ? null : str" :name="str.startsWith(':') ? str.substr(1, str.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n(), - props: { - reaction: { - type: String, - required: true - }, - }, - data() { - return { - customEmojis: [] - }; - }, - created() { - this.$root.getMeta().then(meta => { - if (meta && meta.emojis) this.customEmojis = meta.emojis; - }); - }, - computed: { - str(): any { - switch (this.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 'rip': return '😇'; - case 'pudding': return (this.$store.getters.isSignedIn && this.$store.state.settings.iLikeSushi) ? 'ðŸ£' : 'ðŸ®'; - case 'star': return 'â'; - default: return this.reaction; - } - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-reaction-icon - img - vertical-align middle - width 1em - height 1em -</style> diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue deleted file mode 100644 index f363fe9779b8d10bea499a183f54560f39b4f7e9..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/reaction-picker.vue +++ /dev/null @@ -1,323 +0,0 @@ -<template> -<div class="rdfaahpb" v-hotkey.global="keymap"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover"> - <p v-if="!$root.isMobile">{{ title }}</p> - <div class="buttons" ref="buttons" :class="{ showFocus }"> - <button v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" @mouseover="onMouseover" @mouseout="onMouseout" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction" v-particle><mk-reaction-icon :reaction="reaction"/></button> - </div> - <div v-if="enableEmojiReaction" class="text"> - <input v-model="text" :placeholder="$t('input-reaction-placeholder')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; -import { emojiRegex } from '../../../../../misc/emoji-regex'; - -export default Vue.extend({ - i18n: i18n('common/views/components/reaction-picker.vue'), - props: { - source: { - required: true - }, - - reactions: { - required: false - }, - - showFocus: { - type: Boolean, - required: false, - default: false - }, - - animation: { - type: Boolean, - required: false, - default: true - } - }, - - data() { - return { - rs: this.reactions || this.$store.state.settings.reactions, - title: this.$t('choose-reaction'), - text: null, - enableEmojiReaction: false, - focus: null - }; - }, - - computed: { - keymap(): any { - return { - 'esc': this.close, - 'enter|space|plus': this.choose, - 'up|k': this.focusUp, - 'left|h|shift+tab': this.focusLeft, - 'right|l|tab': this.focusRight, - 'down|j': this.focusDown, - '1': () => this.react('like'), - '2': () => this.react('love'), - '3': () => this.react('laugh'), - '4': () => this.react('hmm'), - '5': () => this.react('surprise'), - '6': () => this.react('congrats'), - '7': () => this.react('angry'), - '8': () => this.react('confused'), - '9': () => this.react('rip'), - '0': () => this.react('pudding'), - }; - } - }, - - watch: { - focus(i) { - this.$refs.buttons.children[i].focus(); - - if (this.showFocus) { - this.title = this.$refs.buttons.children[i].title; - } - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.enableEmojiReaction = meta.enableEmojiReaction; - }); - - this.$nextTick(() => { - this.focus = 0; - - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - if (this.$root.isMobile) { - 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: this.animation ? 100 : 0, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: this.animation ? 500 : 0 - }); - }); - }, - - methods: { - react(reaction) { - this.$emit('chosen', reaction); - }, - - reactText() { - if (!this.text) return; - this.react(this.text); - }, - - tryReactText() { - if (!this.text) return; - if (!this.text.match(emojiRegex)) return; - this.reactText(); - }, - - onMouseover(e) { - this.title = e.target.title; - }, - - onMouseout(e) { - this.title = this.$t('choose-reaction'); - }, - - close() { - (this.$refs.backdrop as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.backdrop, - opacity: 0, - duration: this.animation ? 200 : 0, - easing: 'linear' - }); - - (this.$refs.popover as any).style.pointerEvents = 'none'; - anime({ - targets: this.$refs.popover, - opacity: 0, - scale: 0.5, - duration: this.animation ? 200 : 0, - easing: 'easeInBack', - complete: () => { - this.$emit('closed'); - this.destroyDom(); - } - }); - }, - - focusUp() { - this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5); - }, - - focusDown() { - this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5); - }, - - focusRight() { - this.focus = this.focus == 9 ? 0 : (this.focus + 1); - }, - - focusLeft() { - this.focus = this.focus == 0 ? 9 : (this.focus - 1); - }, - - choose() { - this.$refs.buttons.childNodes[this.focus].click(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.rdfaahpb - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background var(--modalBackdrop) - opacity 0 - - > .popover - $bgcolor = var(--popupBg) - position absolute - z-index 10001 - background $bgcolor - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - &.isMobile - > div - width 280px - - > button - width 50px - height 50px - font-size 28px - border-radius 4px - - &:not(.isMobile) - $arrow-size = 16px - - margin-top $arrow-size - transform-origin center -($arrow-size) - - &:before - content "" - display block - position absolute - top -($arrow-size * 2) - left s('calc(50% - %s)', $arrow-size) - border-top solid $arrow-size transparent - border-left solid $arrow-size transparent - border-right solid $arrow-size transparent - border-bottom solid $arrow-size $bgcolor - - > p - display block - margin 0 - padding 8px 10px - font-size 14px - color var(--popupFg) - border-bottom solid var(--lineWidth) var(--faceDivider) - line-height 20px - - > .buttons - padding 4px 4px 8px 4px - width 216px - text-align center - - &.showFocus - > button:focus - z-index 1 - - &:after - content "" - pointer-events none - position absolute - top 0 - right 0 - bottom 0 - left 0 - border 2px solid var(--primaryAlpha03) - border-radius 4px - - > button - padding 0 - width 40px - height 40px - font-size 24px - border-radius 2px - - > * - height 1em - - &:hover - background var(--reactionPickerButtonHoverBg) - - &:active - background var(--primary) - box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15) - - > .text - width 216px - padding 0 8px 8px 8px - - > input - width 100% - padding 10px - margin 0 - text-align center - font-size 16px - color var(--desktopPostFormTextareaFg) - background var(--desktopPostFormTextareaBg) - outline none - border solid 1px var(--primaryAlpha01) - border-radius 4px - transition border-color .2s ease - - &:hover - border-color var(--primaryAlpha02) - transition border-color .1s ease - - &:focus - border-color var(--primaryAlpha05) - transition border-color 0s ease - -</style> diff --git a/src/client/app/common/views/components/reactions-viewer.details.vue b/src/client/app/common/views/components/reactions-viewer.details.vue deleted file mode 100644 index 778b9368967dc031df1b348a091514eab3ad73d9..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/reactions-viewer.details.vue +++ /dev/null @@ -1,122 +0,0 @@ -<template> -<transition name="zoom-in-top"> - <div class="buebdbiu" ref="popover" v-if="show"> - <i18n path="few-users" v-if="users.length <= 10"> - <span slot="users"> - <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> - <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> - <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> - </b> - </span> - <mk-reaction-icon slot="reaction" :reaction="reaction" ref="icon" /> - </i18n> - <i18n path="many-users" v-if="10 < users.length"> - <span slot="users"> - <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> - <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> - <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> - </b> - </span> - <span slot="omitted">{{ count - 10 }}</span> - <mk-reaction-icon slot="reaction" :reaction="reaction" ref="icon" /> - </i18n> - </div> -</transition> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/reactions-viewer.details.vue'), - props: { - reaction: { - type: String, - required: true, - }, - users: { - type: Array, - required: true, - }, - count: { - type: Number, - required: true, - }, - source: { - required: true, - } - }, - data() { - return { - show: false - }; - }, - mounted() { - this.show = true; - - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - if (this.source == null) { - this.destroyDom(); - return; - } - const rect = this.source.getBoundingClientRect(); - - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - popover.style.left = (x - 28) + 'px'; - popover.style.top = (y + 16) + 'px'; - }); - } - methods: { - close() { - this.show = false; - setTimeout(this.destroyDom, 300); - } - } -}) -</script> - -<style lang="stylus" scoped> -.buebdbiu - $bgcolor = var(--popupBg) - z-index 10000 - display block - position absolute - max-width 240px - font-size 0.8em - padding 6px 8px - background $bgcolor - text-align center - color var(--text) - border-radius 4px - box-shadow 0 var(--lineWidth) 4px rgba(#000, 0.25) - pointer-events none - transform-origin center -16px - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - left 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(#000, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - left 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px $bgcolor - border-left solid 14px transparent -</style> diff --git a/src/client/app/common/views/components/renote.vue b/src/client/app/common/views/components/renote.vue deleted file mode 100644 index 58a0a26593e649067d6fda7836db7ac364d5e672..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/renote.vue +++ /dev/null @@ -1,104 +0,0 @@ -<template> -<div class="puqkfets" :class="{ mini: narrow }"> - <mk-avatar class="avatar" :user="note.user"/> - <fa icon="retweet"/> - <i18n path="@.renoted-by" tag="span"> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user"> - <mk-user-name :user="note.user"/> - </router-link> - </i18n> - <div class="info"> - <span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span> - <mk-time :time="note.createdAt"/> - <span class="visibility" v-if="note.visibility != 'public'"> - <fa v-if="note.visibility == 'home'" icon="home"/> - <fa v-if="note.visibility == 'followers'" icon="unlock"/> - <fa v-if="note.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - props: { - note: { - type: Object, - required: true - } - }, - inject: { - narrow: { - default: false - } - }, -}); -</script> - -<style lang="stylus" scoped> -.puqkfets - display flex - align-items center - padding 8px 16px - line-height 28px - white-space pre - color var(--renoteText) - background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%) - - &:not(.mini) - padding 8px 16px - - @media (min-width 500px) - padding 8px 16px - - @media (min-width 600px) - padding 16px 32px 8px 32px - - > .avatar - flex-shrink 0 - display inline-block - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px - - > [data-icon] - margin-right 4px - - > span - overflow hidden - flex-shrink 1 - text-overflow ellipsis - white-space nowrap - - > .name - font-weight bold - - > .info - margin-left auto - font-size 0.9em - - > .mobile - margin-right 8px - - > .mk-time - flex-shrink 0 - - > .visibility - margin-left 8px - - [data-icon] - margin-right 0 - - > .localOnly - margin-left 4px - - [data-icon] - margin-right 0 - -</style> diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue deleted file mode 100644 index 813a91b5c02890a1b9470729e5bf76f45d6ac5e4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/2fa.vue +++ /dev/null @@ -1,259 +0,0 @@ -<template> -<div class="2fa totp-section"> - <p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p> - <ui-info warn>{{ $t('caution') }}</ui-info> - <p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p> - <template v-if="$store.state.i.twoFactorEnabled"> - <h2 class="heading">{{ $t('totp-header') }}</h2> - <p>{{ $t('already-registered') }}</p> - <ui-button @click="unregister">{{ $t('unregister') }}</ui-button> - - <template v-if="supportsCredentials"> - <hr class="totp-method-sep"> - - <h2 class="heading">{{ $t('security-key-header') }}</h2> - <p>{{ $t('security-key') }}</p> - <div class="key-list"> - <div class="key" v-for="key in $store.state.i.securityKeysList"> - <h3> - {{ key.name }} - </h3> - <div class="last-used"> - {{ $t('last-used') }} - <mk-time :time="key.lastUsed"/> - </div> - <ui-button @click="unregisterKey(key)"> - {{ $t('unregister') }} - </ui-button> - </div> - </div> - - <ui-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0"> - {{ $t('use-password-less-login') }} - </ui-switch> - - <ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info> - <ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button> - - <ol v-if="registration && !registration.error"> - <li v-if="registration.stage >= 0"> - {{ $t('activate-key') }} - <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" /> - </li> - <li v-if="registration.stage >= 1"> - <ui-form :disabled="registration.stage != 1 || registration.saving"> - <ui-input v-model="keyName" :max="30"> - <span>{{ $t('security-key-name') }}</span> - </ui-input> - <ui-button @click="registerKey" :disabled="this.keyName.length == 0"> - {{ $t('register-security-key') }} - </ui-button> - <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" /> - </ui-form> - </li> - </ol> - </template> - </template> - <div v-if="data && !$store.state.i.twoFactorEnabled"> - <ol> - <li>{{ $t('authenticator') }}<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank">{{ $t('howtoinstall') }}</a></li> - <li>{{ $t('scan') }}<br><img :src="data.qr"></li> - <li>{{ $t('done') }}<br> - <ui-input v-model="token">{{ $t('token') }}</ui-input> - <ui-button primary @click="submit">{{ $t('submit') }}</ui-button> - </li> - </ol> - <ui-info>{{ $t('info') }}</ui-info> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { hostname } from '../../../../config'; -import { hexifyAB } from '../../../scripts/2fa'; - -function stringifyAB(buffer) { - return String.fromCharCode.apply(null, new Uint8Array(buffer)); -} - -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings.2fa.vue'), - data() { - return { - data: null, - supportsCredentials: !!navigator.credentials, - usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin, - registration: null, - keyName: '', - token: null - }; - }, - methods: { - register() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/2fa/register', { - password: password - }).then(data => { - this.data = data; - }); - }); - }, - - unregister() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/2fa/unregister', { - password: password - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - this.$notify(this.$t('unregistered')); - this.$store.state.i.twoFactorEnabled = false; - }); - }); - }, - - submit() { - this.$root.api('i/2fa/done', { - token: this.token - }).then(() => { - this.$notify(this.$t('success')); - this.$store.state.i.twoFactorEnabled = true; - }).catch(() => { - this.$notify(this.$t('failed')); - }); - }, - - registerKey() { - this.registration.saving = true; - this.$root.api('i/2fa/key-done', { - password: this.registration.password, - name: this.keyName, - challengeId: this.registration.challengeId, - // we convert each 16 bits to a string to serialise - clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON), - attestationObject: hexifyAB(this.registration.credential.response.attestationObject) - }).then(key => { - this.registration = null; - key.lastUsed = new Date(); - this.$notify(this.$t('success')); - }) - }, - - unregisterKey(key) { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - return this.$root.api('i/2fa/remove-key', { - password, - credentialId: key.id - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - this.$notify(this.$t('key-unregistered')); - }); - }); - }, - - addSecurityKey() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/2fa/register-key', { - password - }).then(registration => { - this.registration = { - password, - challengeId: registration.challengeId, - stage: 0, - publicKeyOptions: { - challenge: Buffer.from( - registration.challenge - .replace(/\-/g, "+") - .replace(/_/g, "/"), - 'base64' - ), - rp: { - id: hostname, - name: 'Misskey' - }, - user: { - id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)), - name: this.$store.state.i.username, - displayName: this.$store.state.i.name, - }, - pubKeyCredParams: [{alg: -7, type: 'public-key'}], - timeout: 60000, - attestation: 'direct' - }, - saving: true - }; - return navigator.credentials.create({ - publicKey: this.registration.publicKeyOptions - }); - }).then(credential => { - this.registration.credential = credential; - this.registration.saving = false; - this.registration.stage = 1; - }).catch(err => { - console.warn('Error while registering?', err); - this.registration.error = err.message; - this.registration.stage = -1; - }); - }); - }, - updatePasswordLessLogin() { - this.$root.api('i/2fa/password-less', { - value: !!this.usePasswordLessLogin - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.totp-section - .totp-method-sep - margin 1.5em 0 1em - border none - border-top solid var(--lineWidth) var(--faceDivider) - - h2.heading - margin 0 - - .key - padding 1em - margin 0.5em 0 - background #161616 - border-radius 6px - - h3 - margin-top 0 - margin-bottom .3em - - .last-used - margin-bottom .5em -</style> diff --git a/src/client/app/common/views/components/settings/api.vue b/src/client/app/common/views/components/settings/api.vue deleted file mode 100644 index 184fa069fb3c68989307ebef0ec252163b810e8a..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/api.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="key"/> API</template> - - <section class="fit-top"> - <ui-input :value="$store.state.i.token" readonly> - <span>{{ $t('token') }}</span> - </ui-input> - <p>{{ $t('intro') }}</p> - <ui-info warn>{{ $t('caution') }}</ui-info> - <p>{{ $t('regeneration-of-token') }}</p> - <ui-button @click="regenerateToken"><fa icon="sync-alt"/> {{ $t('regenerate-token') }}</ui-button> - </section> - - <section> - <header><fa icon="terminal"/> {{ $t('console.title') }}</header> - <ui-input v-model="endpoint" :datalist="endpoints" @change="onEndpointChange()"> - <span>{{ $t('console.endpoint') }}</span> - </ui-input> - <ui-textarea v-model="body"> - <span>{{ $t('console.parameter') }} (JSON or JSON5)</span> - <template #desc>{{ $t('console.credential-info') }}</template> - </ui-textarea> - <ui-button @click="send" :disabled="sending"> - <template v-if="sending">{{ $t('console.sending') }}</template> - <template v-else><fa icon="paper-plane"/> {{ $t('console.send') }}</template> - </ui-button> - <ui-textarea v-if="res" v-model="res" readonly tall> - <span>{{ $t('console.response') }}</span> - </ui-textarea> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as JSON5 from 'json5'; - -export default Vue.extend({ - i18n: i18n('common/views/components/api-settings.vue'), - - data() { - return { - endpoint: '', - body: '{}', - res: null, - sending: false, - endpoints: [] - }; - }, - - created() { - this.$root.api('endpoints').then(endpoints => { - this.endpoints = endpoints; - }); - }, - - methods: { - regenerateToken() { - this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/regenerate_token', { - password: password - }); - }); - }, - - send() { - this.sending = true; - this.$root.api(this.endpoint, JSON5.parse(this.body)).then(res => { - this.sending = false; - this.res = JSON5.stringify(res, null, 2); - }, err => { - this.sending = false; - this.res = JSON5.stringify(err, null, 2); - }); - }, - - onEndpointChange() { - this.$root.api('endpoint', { endpoint: this.endpoint }).then(endpoint => { - const body = {}; - for (const p of endpoint.params) { - body[p.name] = - p.type === 'String' ? '' : - p.type === 'Number' ? 0 : - p.type === 'Boolean' ? false : - p.type === 'Array' ? [] : - p.type === 'Object' ? {} : - null; - } - this.body = JSON5.stringify(body, null, 2); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue deleted file mode 100644 index d163f1e746d7d6cf0fa53873a5c7ebcbc5ed72c4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/app-type.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<ui-card> - <template #title><fa :icon="faMobileAlt"/> {{ $t('title') }}</template> - - <section class="fit-top"> - <p>{{ $t('intro') }}</p> - <ui-select v-model="appTypeForce" :placeholder="$t('intro')"> - <option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option> - </ui-select> - <ui-info warn>{{ $t('info') }}</ui-info> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { faMobileAlt } from '@fortawesome/free-solid-svg-icons' - -export default Vue.extend({ - i18n: i18n('common/views/components/settings/app-type.vue'), - - data() { - return { - faMobileAlt - }; - }, - - computed: { - appTypeForce: { - get() { return this.$store.state.device.appTypeForce; }, - set(value) { - this.$store.commit('device/set', { key: 'appTypeForce', value }); - this.reload(); - } - }, - }, - - methods: { - reload() { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.reload-to-apply-the-setting'), - showCancelButton: true - }).then(({ canceled }) => { - if (!canceled) { - location.reload(); - } - }); - }, - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/apps.vue b/src/client/app/common/views/components/settings/apps.vue deleted file mode 100644 index c5beaa1fe217ad60823dff780ad273e9e5eac76b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/apps.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<div class="root"> - <ui-info v-if="!fetching && apps.length == 0">{{ $t('no-apps') }}</ui-info> - <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'; -import i18n from '../../../../i18n'; -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings.apps.vue'), - data() { - return { - fetching: true, - apps: [] - }; - }, - mounted() { - this.$root.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/client/app/common/views/components/settings/drive.vue b/src/client/app/common/views/components/settings/drive.vue deleted file mode 100644 index da028e85efe12838b0832f6964c1d2a8d63c62a1..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/drive.vue +++ /dev/null @@ -1,209 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="cloud"/> {{ $t('@.drive') }}</template> - - <section v-if="!fetching" class="juakhbxthdewydyreaphkepoxgxvfogn"> - <div class="meter"><div :style="meterStyle"></div></div> - <p>{{ $t('max') }}: <b>{{ capacity | bytes }}</b> {{ $t('in-use') }}: <b>{{ usage | bytes }}</b></p> - </section> - - <section> - <header>{{ $t('stats') }}</header> - <div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div> - </section> - - <section> - <header>{{ $t('default-upload-folder') }}</header> - <ui-input v-model="uploadFolderName" readonly>{{ $t('default-upload-folder-name') }}</ui-input> - <ui-button @click="chooseUploadFolder()">{{ $t('change-default-upload-folder') }}</ui-button> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as tinycolor from 'tinycolor2'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - i18n: i18n('common/views/components/drive-settings.vue'), - data() { - return { - fetching: true, - usage: null, - capacity: null, - uploadFolderName: null - }; - }, - - computed: { - meterStyle(): any { - return { - width: `${this.usage / this.capacity * 100}%`, - background: tinycolor({ - h: 180 - (this.usage / this.capacity * 180), - s: 0.7, - l: 0.5 - }) - }; - }, - - uploadFolder: { - get() { return this.$store.state.settings.uploadFolder; }, - set(value) { this.$store.dispatch('settings/set', { key: 'uploadFolder', value }); } - }, - }, - - mounted() { - if (this.uploadFolder == null) { - this.uploadFolderName = this.$t('@._settings.root'); - } else { - this.$root.api('drive/folders/show', { - folderId: this.uploadFolder - }).then(folder => { - this.uploadFolderName = folder.name; - }); - } - - this.$root.api('drive').then(info => { - this.capacity = info.capacity; - this.usage = info.usage; - this.fetching = false; - - this.$nextTick(() => { - this.renderChart(); - }); - }); - }, - - methods: { - renderChart() { - this.$root.api('charts/user/drive', { - userId: this.$store.state.i.id, - span: 'day', - limit: 21 - }).then(stats => { - const addition = []; - const deletion = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - - for (let i = 0; i < 21; i++) { - const x = new Date(y, m, d - i); - addition.push([ - x, - stats.incSize[i] - ]); - deletion.push([ - x, - -stats.decSize[i] - ]); - } - - const chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'bar', - stacked: true, - height: 150, - zoom: { - enabled: false - }, - toolbar: { - show: false - } - }, - plotOptions: { - bar: { - columnWidth: '80%' - } - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)', - xaxis: { - lines: { - show: true, - } - }, - }, - tooltip: { - shared: true, - intersect: false - }, - dataLabels: { - enabled: false - }, - legend: { - show: false - }, - series: [{ - name: 'Additions', - data: addition - }, { - name: 'Deletions', - data: deletion - }], - xaxis: { - type: 'datetime', - labels: { - style: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - }, - axisBorder: { - color: 'rgba(0, 0, 0, 0.1)' - }, - axisTicks: { - color: 'rgba(0, 0, 0, 0.1)' - }, - crosshairs: { - width: 1, - opacity: 1 - } - }, - yaxis: { - labels: { - formatter: v => Vue.filter('bytes')(v, 0), - style: { - color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } - } - } - }); - - chart.render(); - }); - }, - - chooseUploadFolder() { - this.$chooseDriveFolder().then(folder => { - this.uploadFolder = folder ? folder.id : null; - this.uploadFolderName = folder ? folder.name : this.$t('@._settings.root'); - }) - } - } -}); -</script> - -<style lang="stylus" scoped> -.juakhbxthdewydyreaphkepoxgxvfogn - > .meter - $size = 12px - - margin-bottom 16px - background rgba(0, 0, 0, 0.1) - border-radius ($size / 2) - overflow hidden - - > div - height $size - border-radius ($size / 2) - - > p - margin 0 - -</style> diff --git a/src/client/app/common/views/components/settings/integration.vue b/src/client/app/common/views/components/settings/integration.vue deleted file mode 100644 index 71ad8b4509e5b7e00040ea6ce305db7e98e5ee35..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/integration.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<ui-card v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration"> - <template #title><fa icon="share-alt"/> {{ $t('title') }}</template> - - <section v-if="enableTwitterIntegration"> - <header><fa :icon="['fab', 'twitter']"/> Twitter</header> - <p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> - <ui-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnect') }}</ui-button> - <ui-button v-else @click="connectTwitter">{{ $t('connect') }}</ui-button> - </section> - - <section v-if="enableDiscordIntegration"> - <header><fa :icon="['fab', 'discord']"/> Discord</header> - <p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p> - <ui-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnect') }}</ui-button> - <ui-button v-else @click="connectDiscord">{{ $t('connect') }}</ui-button> - </section> - - <section v-if="enableGithubIntegration"> - <header><fa :icon="['fab', 'github']"/> GitHub</header> - <p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.github.login }}</a></p> - <ui-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnect') }}</ui-button> - <ui-button v-else @click="connectGithub">{{ $t('connect') }}</ui-button> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { apiUrl } from '../../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/integration-settings.vue'), - - data() { - return { - apiUrl, - twitterForm: null, - discordForm: null, - githubForm: null, - enableTwitterIntegration: false, - enableDiscordIntegration: false, - enableGithubIntegration: false, - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - this.enableGithubIntegration = meta.enableGithubIntegration; - }); - }, - - mounted() { - if (!document.cookie.match(/i=(\w+)/)) { - document.cookie = `i=${this.$store.state.i.token}; path=/;` + - ` domain=${document.location.hostname}; max-age=31536000;` + - (document.location.protocol.startsWith('https') ? ' secure' : ''); - } - this.$watch('$store.state.i', () => { - if (this.$store.state.i.twitter) { - if (this.twitterForm) this.twitterForm.close(); - } - if (this.$store.state.i.discord) { - if (this.discordForm) this.discordForm.close(); - } - if (this.$store.state.i.github) { - if (this.githubForm) this.githubForm.close(); - } - }, { - deep: true - }); - }, - - methods: { - connectTwitter() { - this.twitterForm = window.open(apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570, width=520'); - }, - - disconnectTwitter() { - window.open(apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570, width=520'); - }, - - connectDiscord() { - this.discordForm = window.open(apiUrl + '/connect/discord', - 'discord_connect_window', - 'height=570, width=520'); - }, - - disconnectDiscord() { - window.open(apiUrl + '/disconnect/discord', - 'discord_disconnect_window', - 'height=570, width=520'); - }, - - connectGithub() { - this.githubForm = window.open(apiUrl + '/connect/github', - 'github_connect_window', - 'height=570, width=520'); - }, - - disconnectGithub() { - window.open(apiUrl + '/disconnect/github', - 'github_disconnect_window', - 'height=570, width=520'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/settings/language.vue b/src/client/app/common/views/components/settings/language.vue deleted file mode 100644 index f81775f09bcb9c16480b778e54ae197100f56344..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/language.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="language"/> {{ $t('title') }}</template> - - <section class="fit-top"> - <ui-select v-model="lang" :placeholder="$t('pick-language')"> - <optgroup :label="$t('recommended')"> - <option value="">{{ $t('auto') }}</option> - </optgroup> - - <optgroup :label="$t('specify-language')"> - <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> - </optgroup> - </ui-select> - <ui-info>Current: <i>{{ currentLanguage }}</i></ui-info> - <ui-info warn>{{ $t('info') }}</ui-info> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { langs } from '../../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/language-settings.vue'), - - data() { - return { - langs, - currentLanguage: 'Unknown', - }; - }, - - computed: { - lang: { - get() { return this.$store.state.device.lang; }, - set(value) { this.$store.commit('device/set', { key: 'lang', value }); } - }, - }, - - created() { - try { - const locale = JSON.parse(localStorage.getItem('locale') || "{}"); - const localeKey = localStorage.getItem('localeKey'); - this.currentLanguage = `${locale.meta.lang} (${localeKey})`; - } catch { } - }, - - methods: { - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/mute-and-block.user.vue b/src/client/app/common/views/components/settings/mute-and-block.user.vue deleted file mode 100644 index 29ef1f7a6753fd3af9ee8616ff797746c456d9b3..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/mute-and-block.user.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<div class="muteblockuser"> - <div class="avatar-link"> - <a :href="user | userPage(null, true)"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div class="text"> - <div><mk-user-name :user="user"/></div> - <div class="username">@{{ user | acct }}</div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/mute-and-block.user.vue'), - props: ['user'], -}); -</script> - -<style lang="stylus" scoped> -.muteblockuser - display flex - padding 16px - - > .avatar-link - > a - > .avatar - width 40px - height 40px - - > .text - color var(--text) - margin-left 16px -</style> diff --git a/src/client/app/common/views/components/settings/mute-and-block.vue b/src/client/app/common/views/components/settings/mute-and-block.vue deleted file mode 100644 index 8ff58041688dfe94e27edb0a3c2d0c3a42997732..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/mute-and-block.vue +++ /dev/null @@ -1,181 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="ban"/> {{ $t('mute-and-block') }}</template> - - <section> - <header>{{ $t('mute') }}</header> - <ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info> - <div class="users" v-if="mute.length != 0"> - <div class="user" v-for="user in mute" :key="user.id"> - <x-user :user="user"/> - <span @click="unmute(user)"> - <fa icon="times"/> - </span> - </div> - <ui-button v-if="this.muteCursor != null" @click="updateMute()">{{ $t('@.load-more') }}</ui-button> - </div> - </section> - - <section> - <header>{{ $t('block') }}</header> - <ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info> - <div class="users" v-if="block.length != 0"> - <div class="user" v-for="user in block" :key="user.id"> - <x-user :user="user"/> - <span @click="unblock(user)"> - <fa icon="times"/> - </span> - </div> - <ui-button v-if="this.blockCursor != null" @click="updateBlock()">{{ $t('@.load-more') }}</ui-button> - </div> - </section> - - <section> - <header>{{ $t('word-mute') }}</header> - <ui-textarea v-model="mutedWords"> - {{ $t('muted-words') }}<template #desc>{{ $t('muted-words-description') }}</template> - </ui-textarea> - <ui-button @click="save">{{ $t('save') }}</ui-button> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XUser from './mute-and-block.user.vue'; - -const fetchLimit = 30; - -export default Vue.extend({ - i18n: i18n('common/views/components/mute-and-block.vue'), - - components: { - XUser - }, - - data() { - return { - muteFetching: true, - blockFetching: true, - mute: [], - block: [], - muteCursor: undefined, - blockCursor: undefined, - mutedWords: '' - }; - }, - - computed: { - _mutedWords: { - get() { return this.$store.state.settings.mutedWords; }, - set(value) { this.$store.dispatch('settings/set', { key: 'mutedWords', value }); } - }, - }, - - mounted() { - this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n'); - - this.updateMute(); - this.updateBlock(); - }, - - methods: { - save() { - this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != '')); - }, - - unmute(user) { - this.$root.dialog({ - type: 'warning', - text: this.$t('unmute-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('mute/delete', { - userId: user.id - }).then(() => { - this.muteCursor = undefined; - this.updateMute(); - }); - }); - }, - - unblock(user) { - this.$root.dialog({ - type: 'warning', - text: this.$t('unblock-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('blocking/delete', { - userId: user.id - }).then(() => { - this.updateBlock(); - }); - }); - }, - - updateMute() { - this.muteFetching = true; - this.$root.api('mute/list', { - limit: fetchLimit + 1, - untilId: this.muteCursor, - }).then((items: Object[]) => { - const past = this.muteCursor ? this.mute : []; - - if (items.length === fetchLimit + 1) { - items.pop() - this.muteCursor = items[items.length - 1].id; - } else { - this.muteCursor = undefined; - } - - this.mute = past.concat(items.map(x => x.mutee)); - this.muteFetching = false; - }); - }, - - updateBlock() { - this.blockFetching = true; - this.$root.api('blocking/list', { - limit: fetchLimit + 1, - untilId: this.blockCursor, - }).then((items: Object[]) => { - const past = this.blockCursor ? this.block : []; - - if (items.length === fetchLimit + 1) { - items.pop() - this.blockCursor = items[items.length - 1].id; - } else { - this.blockCursor = undefined; - } - - this.block = past.concat(items.map(x => x.blockee)); - this.blockFetching = false; - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> - .users - > .user - display flex - align-items center - justify-content flex-end - border-radius 6px - - &:hover - background-color var(--primary) - - > span - margin-left auto - cursor pointer - padding 16px - - > button - margin-top 16px -</style> - diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue deleted file mode 100644 index 2554fe633137dcd589f264cdbf08059f049424ea..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/notification.vue +++ /dev/null @@ -1,44 +0,0 @@ -<template> -<ui-card> - <template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template> - <section> - <ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch"> - {{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template> - </ui-switch> - <section> - <ui-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</ui-button> - <ui-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</ui-button> - <ui-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</ui-button> - </section> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/notification-settings.vue'), - - methods: { - onChangeAutoWatch(v) { - this.$root.api('i/update', { - autoWatch: v - }); - }, - - readAllUnreadNotes() { - this.$root.api('i/read_all_unread_notes'); - }, - - readAllMessagingMessages() { - this.$root.api('i/read_all_messaging_messages'); - }, - - readAllNotifications() { - this.$root.api('notifications/mark_all_as_read'); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/password.vue b/src/client/app/common/views/components/settings/password.vue deleted file mode 100644 index c867561518db6867c191c5fb7b380a712dbac262..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/password.vue +++ /dev/null @@ -1,63 +0,0 @@ -<template> -<div> - <ui-button @click="reset">{{ $t('reset') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/components/password-settings.vue'), - methods: { - async reset() { - const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({ - title: this.$t('enter-current-password'), - input: { - type: 'password' - } - }); - if (canceled1) return; - - const { canceled: canceled2, result: newPassword } = await this.$root.dialog({ - title: this.$t('enter-new-password'), - input: { - type: 'password' - } - }); - if (canceled2) return; - - const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({ - title: this.$t('enter-new-password-again'), - input: { - type: 'password' - } - }); - if (canceled3) return; - - if (newPassword !== newPassword2) { - this.$root.dialog({ - title: null, - text: this.$t('not-match') - }); - return; - } - this.$root.api('i/change_password', { - currentPassword, - newPassword - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('changed') - }); - }).catch(() => { - this.$root.dialog({ - type: 'error', - text: this.$t('failed') - }); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue deleted file mode 100644 index 0c291f90296d7dea76974dc8d476160286528a59..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/profile.vue +++ /dev/null @@ -1,442 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="user"/> {{ $t('title') }}</template> - - <section class="esokaraujimuwfttfzgocmutcihewscl"> - <div class="header" :style="bannerStyle"> - <mk-avatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true"/> - </div> - - <ui-form :disabled="saving"> - <ui-input v-model="name" :max="30"> - <span>{{ $t('name') }}</span> - </ui-input> - - <ui-input v-model="username" readonly> - <span>{{ $t('account') }}</span> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </ui-input> - - <ui-input v-model="location"> - <span>{{ $t('location') }}</span> - <template #prefix><fa icon="map-marker-alt"/></template> - </ui-input> - - <ui-input v-model="birthday" type="date"> - <template #title>{{ $t('birthday') }}</template> - <template #prefix><fa icon="birthday-cake"/></template> - </ui-input> - - <ui-textarea v-model="description" :max="500"> - <span>{{ $t('description') }}</span> - <template #desc>{{ $t('you-can-include-hashtags') }}</template> - </ui-textarea> - - <ui-select v-model="lang"> - <template #label>{{ $t('language') }}</template> - <template #icon><fa icon="language"/></template> - <option v-for="lang in unique(Object.values(langmap).map(x => x.nativeName)).map(name => Object.keys(langmap).find(k => langmap[k].nativeName == name))" :value="lang" :key="lang">{{ langmap[lang].nativeName }}</option> - </ui-select> - - <ui-input type="file" @change="onAvatarChange"> - <span>{{ $t('avatar') }}</span> - <template #icon><fa icon="image"/></template> - <template #desc v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></template> - </ui-input> - - <ui-input type="file" @change="onBannerChange"> - <span>{{ $t('banner') }}</span> - <template #icon><fa icon="image"/></template> - <template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template> - </ui-input> - - <div class="fields"> - <header>{{ $t('profile-metadata') }}</header> - <ui-horizon-group> - <ui-input v-model="fieldName0">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue0">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group> - <ui-input v-model="fieldName1">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue1">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group> - <ui-input v-model="fieldName2">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue2">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - <ui-horizon-group> - <ui-input v-model="fieldName3">{{ $t('metadata-label') }}</ui-input> - <ui-input v-model="fieldValue3">{{ $t('metadata-content') }}</ui-input> - </ui-horizon-group> - </div> - - <ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </ui-form> - </section> - - <section> - <header><fa :icon="faCogs"/> {{ $t('advanced') }}</header> - - <div> - <ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch> - <ui-switch v-model="isBot" @change="save(false)">{{ $t('is-bot') }}</ui-switch> - <ui-switch v-model="alwaysMarkNsfw">{{ $t('@._settings.always-mark-nsfw') }}</ui-switch> - </div> - </section> - - <section> - <header><fa :icon="faUnlockAlt"/> {{ $t('privacy') }}</header> - - <div> - <ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch> - <ui-switch v-model="carefulBot" :disabled="isLocked" @change="save(false)">{{ $t('careful-bot') }}</ui-switch> - <ui-switch v-model="autoAcceptFollowed" :disabled="!isLocked && !carefulBot" @change="save(false)">{{ $t('auto-accept-followed') }}</ui-switch> - </div> - </section> - - <section v-if="enableEmail"> - <header><fa :icon="faEnvelope"/> {{ $t('email') }}</header> - - <div> - <template v-if="$store.state.i.email != null"> - <ui-info v-if="$store.state.i.emailVerified">{{ $t('email-verified') }}</ui-info> - <ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info> - </template> - <ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input> - <ui-button @click="updateEmail()" :disabled="email === $store.state.i.email"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - </div> - </section> - - <section> - <header><fa :icon="faBoxes"/> {{ $t('export-and-import') }}</header> - - <div> - <ui-select v-model="exportTarget"> - <option value="notes">{{ $t('export-targets.all-notes') }}</option> - <option value="following">{{ $t('export-targets.following-list') }}</option> - <option value="mute">{{ $t('export-targets.mute-list') }}</option> - <option value="blocking">{{ $t('export-targets.blocking-list') }}</option> - <option value="user-lists">{{ $t('export-targets.user-lists') }}</option> - </ui-select> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button> - <ui-button @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</ui-button> - </ui-horizon-group> - </div> - </section> - - <section> - <details> - <summary>{{ $t('danger-zone') }}</summary> - <ui-button @click="deleteAccount()">{{ $t('delete-account') }}</ui-button> - </details> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { apiUrl, host } from '../../../../config'; -import { toUnicode } from 'punycode'; -import langmap from 'langmap'; -import { unique } from '../../../../../../prelude/array'; -import { faDownload, faUpload, faUnlockAlt, faBoxes, faCogs } from '@fortawesome/free-solid-svg-icons'; -import { faSave, faEnvelope } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/profile-editor.vue'), - - data() { - return { - unique, - langmap, - host: toUnicode(host), - enableEmail: false, - email: null, - name: null, - username: null, - location: null, - description: null, - fieldName0: null, - fieldValue0: null, - fieldName1: null, - fieldValue1: null, - fieldName2: null, - fieldValue2: null, - fieldName3: null, - fieldValue3: null, - lang: null, - birthday: null, - avatarId: null, - bannerId: null, - isCat: false, - isBot: false, - isLocked: false, - carefulBot: false, - autoAcceptFollowed: false, - saving: false, - avatarUploading: false, - bannerUploading: false, - exportTarget: 'notes', - faDownload, faUpload, faSave, faEnvelope, faUnlockAlt, faBoxes, faCogs - }; - }, - - computed: { - alwaysMarkNsfw: { - get() { return this.$store.state.i.alwaysMarkNsfw; }, - set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); } - }, - - bannerStyle(): any { - if (this.$store.state.i.bannerUrl == null) return {}; - return { - backgroundColor: this.$store.state.i.bannerColor, - backgroundImage: `url(${ this.$store.state.i.bannerUrl })` - }; - }, - }, - - created() { - this.$root.getMeta().then(meta => { - this.enableEmail = meta.enableEmail; - }); - this.email = this.$store.state.i.email; - this.name = this.$store.state.i.name; - this.username = this.$store.state.i.username; - this.location = this.$store.state.i.location; - this.description = this.$store.state.i.description; - this.lang = this.$store.state.i.lang; - this.birthday = this.$store.state.i.birthday; - this.avatarId = this.$store.state.i.avatarId; - this.bannerId = this.$store.state.i.bannerId; - this.isCat = this.$store.state.i.isCat; - this.isBot = this.$store.state.i.isBot; - this.isLocked = this.$store.state.i.isLocked; - this.carefulBot = this.$store.state.i.carefulBot; - this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; - - this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null; - this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null; - this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null; - this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null; - this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null; - this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null; - this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null; - this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null; - }, - - methods: { - onAvatarChange([file]) { - this.avatarUploading = true; - - const data = new FormData(); - data.append('file', file); - data.append('i', this.$store.state.i.token); - - fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: data - }) - .then(response => response.json()) - .then(f => { - this.avatarId = f.id; - this.avatarUploading = false; - }) - .catch(e => { - this.avatarUploading = false; - alert('%18n:@upload-failed%'); - }); - }, - - onBannerChange([file]) { - this.bannerUploading = true; - - const data = new FormData(); - data.append('file', file); - data.append('i', this.$store.state.i.token); - - fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: data - }) - .then(response => response.json()) - .then(f => { - this.bannerId = f.id; - this.bannerUploading = false; - }) - .catch(e => { - this.bannerUploading = false; - alert('%18n:@upload-failed%'); - }); - }, - - save(notify) { - const fields = [ - { name: this.fieldName0, value: this.fieldValue0 }, - { name: this.fieldName1, value: this.fieldValue1 }, - { name: this.fieldName2, value: this.fieldValue2 }, - { name: this.fieldName3, value: this.fieldValue3 }, - ]; - - this.saving = true; - - this.$root.api('i/update', { - name: this.name || null, - location: this.location || null, - description: this.description || null, - lang: this.lang, - birthday: this.birthday || null, - avatarId: this.avatarId || undefined, - bannerId: this.bannerId || undefined, - fields, - isCat: !!this.isCat, - isBot: !!this.isBot, - isLocked: !!this.isLocked, - carefulBot: !!this.carefulBot, - autoAcceptFollowed: !!this.autoAcceptFollowed - }).then(i => { - this.saving = false; - this.$store.state.i.avatarId = i.avatarId; - this.$store.state.i.avatarUrl = i.avatarUrl; - this.$store.state.i.bannerId = i.bannerId; - this.$store.state.i.bannerUrl = i.bannerUrl; - - if (notify) { - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - } - }).catch(err => { - this.saving = false; - switch(err.id) { - case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191': - this.$root.dialog({ - type: 'error', - title: this.$t('unable-to-process'), - text: this.$t('avatar-not-an-image') - }); - break; - case '75aedb19-2afd-4e6d-87fc-67941256fa60': - this.$root.dialog({ - type: 'error', - title: this.$t('unable-to-process'), - text: this.$t('banner-not-an-image') - }); - break; - default: - this.$root.dialog({ - type: 'error', - text: this.$t('unable-to-process') - }); - } - }); - }, - - updateEmail() { - this.$root.dialog({ - title: this.$t('@.enter-password'), - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - this.$root.api('i/update_email', { - password: password, - email: this.email == '' ? null : this.email - }); - }); - }, - - doExport() { - this.$root.api( - this.exportTarget == 'notes' ? 'i/export-notes' : - this.exportTarget == 'following' ? 'i/export-following' : - this.exportTarget == 'mute' ? 'i/export-mute' : - this.exportTarget == 'blocking' ? 'i/export-blocking' : - this.exportTarget == 'user-lists' ? 'i/export-user-lists' : - null, {}).then(() => { - this.$root.dialog({ - type: 'info', - text: this.$t('export-requested') - }); - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }); - }, - - doImport() { - this.$chooseDriveFile().then(file => { - this.$root.api( - this.exportTarget == 'following' ? 'i/import-following' : - this.exportTarget == 'user-lists' ? 'i/import-user-lists' : - null, { - fileId: file.id - }).then(() => { - this.$root.dialog({ - type: 'info', - text: this.$t('import-requested') - }); - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }); - }); - }, - - async deleteAccount() { - const { canceled: canceled, result: password } = await this.$root.dialog({ - title: this.$t('enter-password'), - input: { - type: 'password' - } - }); - if (canceled) return; - - this.$root.api('i/delete-account', { - password - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('account-deleted') - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.esokaraujimuwfttfzgocmutcihewscl - > .header - height 150px - overflow hidden - background-size cover - background-position center - border-radius 4px - - > .avatar - position absolute - top 0 - bottom 0 - left 0 - right 0 - display block - width 72px - height 72px - margin auto - -.fields - > header - padding 8px 0px - font-weight bold - -</style> diff --git a/src/client/app/common/views/components/settings/settings.vue b/src/client/app/common/views/components/settings/settings.vue deleted file mode 100644 index 3a0ba561afc85badf93a50e2417cf950744e94be..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/settings.vue +++ /dev/null @@ -1,671 +0,0 @@ -<template> -<div class="nqfhvmnl"> - <template v-if="page == null || page == 'profile'"> - <x-profile/> - <x-integration/> - </template> - - <template v-if="page == null || page == 'appearance'"> - <x-theme/> - - <ui-card> - <template #title><fa icon="desktop"/> {{ $t('@._settings.appearance') }}</template> - - <section v-if="!$root.isMobile"> - <ui-switch v-model="showPostFormOnTopOfTl">{{ $t('@._settings.post-form-on-timeline') }}</ui-switch> - <ui-button @click="customizeHome">{{ $t('@.customize-home') }}</ui-button> - </section> - <section v-if="!$root.isMobile"> - <header>{{ $t('@._settings.wallpaper') }}</header> - <ui-horizon-group class="fit-bottom"> - <ui-button @click="updateWallpaper">{{ $t('@._settings.choose-wallpaper') }}</ui-button> - <ui-button @click="deleteWallpaper">{{ $t('@._settings.delete-wallpaper') }}</ui-button> - </ui-horizon-group> - </section> - <section v-if="!$root.isMobile"> - <header>{{ $t('@._settings.navbar-position') }}</header> - <ui-radio v-model="navbar" value="top">{{ $t('@._settings.navbar-position-top') }}</ui-radio> - <ui-radio v-model="navbar" value="left">{{ $t('@._settings.navbar-position-left') }}</ui-radio> - <ui-radio v-model="navbar" value="right">{{ $t('@._settings.navbar-position-right') }}</ui-radio> - </section> - <section> - <ui-switch v-model="useShadow">{{ $t('@._settings.use-shadow') }}</ui-switch> - <ui-switch v-model="roundedCorners">{{ $t('@._settings.rounded-corners') }}</ui-switch> - <ui-switch v-model="circleIcons">{{ $t('@._settings.circle-icons') }}</ui-switch> - <ui-switch v-model="reduceMotion">{{ $t('@._settings.reduce-motion') }}</ui-switch> - <ui-switch v-model="contrastedAcct">{{ $t('@._settings.contrasted-acct') }}</ui-switch> - <ui-switch v-model="showFullAcct">{{ $t('@._settings.show-full-acct') }}</ui-switch> - <ui-switch v-model="showVia">{{ $t('@._settings.show-via') }}</ui-switch> - <ui-switch v-model="useOsDefaultEmojis">{{ $t('@._settings.use-os-default-emojis') }}</ui-switch> - <ui-switch v-model="iLikeSushi">{{ $t('@._settings.i-like-sushi') }}</ui-switch> - </section> - <section> - <ui-switch v-model="suggestRecentHashtags">{{ $t('@._settings.suggest-recent-hashtags') }}</ui-switch> - <ui-switch v-model="showClockOnHeader" v-if="!$root.isMobile">{{ $t('@._settings.show-clock-on-header') }}</ui-switch> - <ui-switch v-model="alwaysShowNsfw">{{ $t('@._settings.always-show-nsfw') }}</ui-switch> - <ui-switch v-model="showReplyTarget">{{ $t('@._settings.show-reply-target') }}</ui-switch> - <ui-switch v-model="disableAnimatedMfm">{{ $t('@._settings.disable-animated-mfm') }}</ui-switch> - <ui-switch v-model="disableShowingAnimatedImages">{{ $t('@._settings.disable-showing-animated-images') }}</ui-switch> - <ui-switch v-model="remainDeletedNote">{{ $t('@._settings.remain-deleted-note') }}</ui-switch> - <ui-switch v-model="enableMobileQuickNotificationView">{{ $t('@._settings.enable-quick-notification-view') }}</ui-switch> - </section> - <section> - <header>{{ $t('@._settings.line-width') }}</header> - <ui-radio v-model="lineWidth" :value="0.5">{{ $t('@._settings.line-width-thin') }}</ui-radio> - <ui-radio v-model="lineWidth" :value="1">{{ $t('@._settings.line-width-normal') }}</ui-radio> - <ui-radio v-model="lineWidth" :value="2">{{ $t('@._settings.line-width-thick') }}</ui-radio> - </section> - <section> - <header>{{ $t('@._settings.font-size') }}</header> - <ui-radio v-model="fontSize" :value="-2">{{ $t('@._settings.font-size-x-small') }}</ui-radio> - <ui-radio v-model="fontSize" :value="-1">{{ $t('@._settings.font-size-small') }}</ui-radio> - <ui-radio v-model="fontSize" :value="0">{{ $t('@._settings.font-size-medium') }}</ui-radio> - <ui-radio v-model="fontSize" :value="1">{{ $t('@._settings.font-size-large') }}</ui-radio> - <ui-radio v-model="fontSize" :value="2">{{ $t('@._settings.font-size-x-large') }}</ui-radio> - </section> - <section v-if="$root.isMobile"> - <header>{{ $t('@._settings.post-style') }}</header> - <ui-radio v-model="postStyle" value="standard">{{ $t('@._settings.post-style-standard') }}</ui-radio> - <ui-radio v-model="postStyle" value="smart">{{ $t('@._settings.post-style-smart') }}</ui-radio> - </section> - <section v-if="$root.isMobile"> - <header>{{ $t('@._settings.notification-position') }}</header> - <ui-radio v-model="mobileNotificationPosition" value="bottom">{{ $t('@._settings.notification-position-bottom') }}</ui-radio> - <ui-radio v-model="mobileNotificationPosition" value="top">{{ $t('@._settings.notification-position-top') }}</ui-radio> - </section> - <section> - <header>{{ $t('@._settings.deck-column-align') }}</header> - <ui-radio v-model="deckColumnAlign" value="center">{{ $t('@._settings.deck-column-align-center') }}</ui-radio> - <ui-radio v-model="deckColumnAlign" value="left">{{ $t('@._settings.deck-column-align-left') }}</ui-radio> - <ui-radio v-model="deckColumnAlign" value="flexible">{{ $t('@._settings.deck-column-align-flexible') }}</ui-radio> - </section> - <section> - <header>{{ $t('@._settings.deck-column-width') }}</header> - <ui-radio v-model="deckColumnWidth" value="narrow">{{ $t('@._settings.deck-column-width-narrow') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="narrower">{{ $t('@._settings.deck-column-width-narrower') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="normal">{{ $t('@._settings.deck-column-width-normal') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="wider">{{ $t('@._settings.deck-column-width-wider') }}</ui-radio> - <ui-radio v-model="deckColumnWidth" value="wide">{{ $t('@._settings.deck-column-width-wide') }}</ui-radio> - </section> - <section> - <ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@._settings.show-reversi-board-labels') }}</ui-switch> - <ui-switch v-model="games_reversi_useAvatarStones">{{ $t('@._settings.use-avatar-reversi-stones') }}</ui-switch> - </section> - </ui-card> - </template> - - <template v-if="page == null || page == 'behavior'"> - <ui-card> - <template #title><fa icon="sliders-h"/> {{ $t('@._settings.behavior') }}</template> - - <section> - <ui-switch v-model="fetchOnScroll">{{ $t('@._settings.fetch-on-scroll') }} - <template #desc>{{ $t('@._settings.fetch-on-scroll-desc') }}</template> - </ui-switch> - <ui-switch v-model="keepCw">{{ $t('@._settings.keep-cw') }} - <template #desc>{{ $t('@._settings.keep-cw-desc') }}</template> - </ui-switch> - <ui-switch v-if="$root.isMobile" v-model="disableViaMobile">{{ $t('@._settings.disable-via-mobile') }}</ui-switch> - </section> - - <section> - <header>{{ $t('@._settings.reactions') }}</header> - <ui-textarea v-model="reactions"> - {{ $t('@._settings.reactions') }}<template #desc>{{ $t('@._settings.reactions-description') }}</template> - </ui-textarea> - <ui-horizon-group> - <ui-button @click="save('reactions', reactions.trim().split('\n'))" primary><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button> - <ui-button @click="previewReaction()" ref="reactionsPreviewButton"><fa :icon="faEye"/> {{ $t('@._settings.preview') }}</ui-button> - </ui-horizon-group> - </section> - - <section> - <header>{{ $t('@._settings.timeline') }}</header> - <ui-switch v-model="showMyRenotes">{{ $t('@._settings.show-my-renotes') }}</ui-switch> - <ui-switch v-model="showRenotedMyNotes">{{ $t('@._settings.show-renoted-my-notes') }}</ui-switch> - <ui-switch v-model="showLocalRenotes">{{ $t('@._settings.show-local-renotes') }}</ui-switch> - </section> - - <section> - <header>{{ $t('@._settings.note-visibility') }}</header> - <ui-switch v-model="rememberNoteVisibility">{{ $t('@._settings.remember-note-visibility') }}</ui-switch> - <section> - <header>{{ $t('@._settings.default-note-visibility') }}</header> - <ui-select v-model="defaultNoteVisibility"> - <option value="public">{{ $t('@.note-visibility.public') }}</option> - <option value="home">{{ $t('@.note-visibility.home') }}</option> - <option value="followers">{{ $t('@.note-visibility.followers') }}</option> - <option value="specified">{{ $t('@.note-visibility.specified') }}</option> - <option value="local-public">{{ $t('@.note-visibility.local-public') }}</option> - <option value="local-home">{{ $t('@.note-visibility.local-home') }}</option> - <option value="local-followers">{{ $t('@.note-visibility.local-followers') }}</option> - </ui-select> - </section> - </section> - - <section> - <header>{{ $t('@._settings.sync') }}</header> - <ui-input v-if="$root.isMobile" v-model="mobileHomeProfile" :datalist="Object.keys($store.state.settings.mobileHomeProfiles)">{{ $t('@._settings.home-profile') }}</ui-input> - <ui-input v-else v-model="homeProfile" :datalist="Object.keys($store.state.settings.homeProfiles)">{{ $t('@._settings.home-profile') }}</ui-input> - <ui-input v-model="deckProfile" :datalist="Object.keys($store.state.settings.deckProfiles)">{{ $t('@._settings.deck-profile') }}</ui-input> - </section> - - <section> - <header>{{ $t('@._settings.web-search-engine') }}</header> - <ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }} - <template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template> - </ui-input> - <ui-button @click="save('webSearchEngine', webSearchEngine)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button> - </section> - - <section v-if="!$root.isMobile"> - <header>{{ $t('@._settings.paste') }}</header> - <ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }} - <template v-if="pastedFileName === this.$store.state.settings.pastedFileName" #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template> - <template v-else #desc>{{ pastedFileNamePreview() }}</template> - </ui-input> - <ui-button @click="save('pastedFileName', pastedFileName)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button> - - <ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }} - <template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template> - </ui-switch> - </section> - - <section> - <header>{{ $t('@._settings.room') }}</header> - <ui-select v-model="roomGraphicsQuality"> - <template #label>{{ $t('@._settings._room.graphicsQuality') }}</template> - <option value="ultra">{{ $t('@._settings._room._graphicsQuality.ultra') }}</option> - <option value="high">{{ $t('@._settings._room._graphicsQuality.high') }}</option> - <option value="medium">{{ $t('@._settings._room._graphicsQuality.medium') }}</option> - <option value="low">{{ $t('@._settings._room._graphicsQuality.low') }}</option> - <option value="cheep">{{ $t('@._settings._room._graphicsQuality.cheep') }}</option> - </ui-select> - <ui-switch v-model="roomUseOrthographicCamera">{{ $t('@._settings._room.useOrthographicCamera') }}</ui-switch> - </section> - </ui-card> - - <ui-card> - <template #title><fa icon="volume-up"/> {{ $t('@._settings.sound') }}</template> - - <section> - <ui-switch v-model="enableSounds">{{ $t('@._settings.enable-sounds') }} - <template #desc>{{ $t('@._settings.enable-sounds-desc') }}</template> - </ui-switch> - <label>{{ $t('@._settings.volume') }}</label> - <input type="range" - v-model="soundVolume" - :disabled="!enableSounds" - max="1" - step="0.1" - /> - <ui-button @click="soundTest"><fa icon="volume-up"/> {{ $t('@._settings.test') }}</ui-button> - </section> - </ui-card> - - <x-language/> - <x-app-type/> - </template> - - <template v-if="page == null || page == 'notification'"> - <x-notification/> - </template> - - <template v-if="page == null || page == 'drive'"> - <x-drive/> - </template> - - <template v-if="page == null || page == 'hashtags'"> - <ui-card> - <template #title><fa icon="hashtag"/> {{ $t('@._settings.tags') }}</template> - <section> - <x-tags/> - </section> - </ui-card> - </template> - - <template v-if="page == null || page == 'muteAndBlock'"> - <x-mute-and-block/> - </template> - - <!-- - <template v-if="page == null || page == 'apps'"> - <ui-card> - <template #title><fa icon="puzzle-piece"/> {{ $t('@._settings.apps') }}</template> - <section> - <x-apps/> - </section> - </ui-card> - </template> - --> - - <template v-if="page == null || page == 'security'"> - <ui-card> - <template #title><fa icon="unlock-alt"/> {{ $t('@._settings.password') }}</template> - <section> - <x-password/> - </section> - </ui-card> - - <ui-card v-if="!$root.isMobile"> - <template #title><fa icon="mobile-alt"/> {{ $t('@.2fa') }}</template> - <section> - <x-2fa/> - </section> - </ui-card> - - <!-- - <ui-card> - <template #title><fa icon="sign-in-alt"/> {{ $t('@._settings.signin') }}</template> - <section> - <x-signins/> - </section> - </ui-card> - --> - </template> - - <template v-if="page == null || page == 'api'"> - <x-api/> - </template> - - <template v-if="page == null || page == 'other'"> - <ui-card> - <template #title><fa icon="sync-alt"/> {{ $t('@._settings.update') }}</template> - <section> - <p> - <span>{{ $t('@._settings.version') }} <i>{{ version }}</i></span> - <template v-if="latestVersion !== undefined"> - <br> - <span>{{ $t('@._settings.latest-version') }} <i>{{ latestVersion ? latestVersion : version }}</i></span> - </template> - </p> - <ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> - <template v-if="checkingForUpdate">{{ $t('@._settings.update-checking') }}<mk-ellipsis/></template> - <template v-else>{{ $t('@._settings.do-update') }}</template> - </ui-button> - </section> - </ui-card> - - <ui-card> - <template #title><fa icon="cogs"/> {{ $t('@._settings.advanced-settings') }}</template> - <section> - <ui-switch v-model="debug"> - {{ $t('@._settings.debug-mode') }}<template #desc>{{ $t('@._settings.debug-mode-desc') }}</template> - </ui-switch> - </section> - </ui-card> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import X2fa from './2fa.vue'; -import XApps from './apps.vue'; -import XSignins from './signins.vue'; -import XTags from './tags.vue'; -import XIntegration from './integration.vue'; -import XTheme from './theme.vue'; -import XDrive from './drive.vue'; -import XMuteAndBlock from './mute-and-block.vue'; -import XPassword from './password.vue'; -import XProfile from './profile.vue'; -import XApi from './api.vue'; -import XLanguage from './language.vue'; -import XAppType from './app-type.vue'; -import XNotification from './notification.vue'; -import MkReactionPicker from '../reaction-picker.vue'; - -import { url, version } from '../../../../config'; -import checkForUpdate from '../../../scripts/check-for-update'; -import { formatTimeString } from '../../../../../../misc/format-time-string'; -import { faSave, faEye } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n(), - components: { - X2fa, - XApps, - XSignins, - XTags, - XIntegration, - XTheme, - XDrive, - XMuteAndBlock, - XPassword, - XProfile, - XApi, - XLanguage, - XAppType, - XNotification, - }, - props: { - page: { - type: String, - required: false, - default: null - } - }, - data() { - return { - meta: null, - version, - reactions: this.$store.state.settings.reactions.join('\n'), - webSearchEngine: this.$store.state.settings.webSearchEngine, - pastedFileName : this.$store.state.settings.pastedFileName, - latestVersion: undefined, - checkingForUpdate: false, - faSave, faEye - }; - }, - computed: { - useOsDefaultEmojis: { - get() { return this.$store.state.device.useOsDefaultEmojis; }, - set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); } - }, - - reduceMotion: { - get() { return this.$store.state.device.reduceMotion; }, - set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); } - }, - - keepCw: { - get() { return this.$store.state.settings.keepCw; }, - set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); } - }, - - navbar: { - get() { return this.$store.state.device.navbar; }, - set(value) { this.$store.commit('device/set', { key: 'navbar', value }); } - }, - - deckColumnAlign: { - get() { return this.$store.state.device.deckColumnAlign; }, - set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); } - }, - - deckColumnWidth: { - get() { return this.$store.state.device.deckColumnWidth; }, - set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); } - }, - - enableSounds: { - get() { return this.$store.state.device.enableSounds; }, - set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } - }, - - soundVolume: { - get() { return this.$store.state.device.soundVolume; }, - set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); } - }, - - debug: { - get() { return this.$store.state.device.debug; }, - set(value) { this.$store.commit('device/set', { key: 'debug', value }); } - }, - - alwaysShowNsfw: { - get() { return this.$store.state.device.alwaysShowNsfw; }, - set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); } - }, - - postStyle: { - get() { return this.$store.state.device.postStyle; }, - set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); } - }, - - disableViaMobile: { - get() { return this.$store.state.settings.disableViaMobile; }, - set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); } - }, - - useShadow: { - get() { return this.$store.state.device.useShadow; }, - set(value) { this.$store.commit('device/set', { key: 'useShadow', value }); } - }, - - roundedCorners: { - get() { return this.$store.state.device.roundedCorners; }, - set(value) { this.$store.commit('device/set', { key: 'roundedCorners', value }); } - }, - - lineWidth: { - get() { return this.$store.state.device.lineWidth; }, - set(value) { this.$store.commit('device/set', { key: 'lineWidth', value }); } - }, - - fontSize: { - get() { return this.$store.state.device.fontSize; }, - set(value) { this.$store.commit('device/set', { key: 'fontSize', value }); } - }, - - fetchOnScroll: { - get() { return this.$store.state.settings.fetchOnScroll; }, - set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } - }, - - rememberNoteVisibility: { - get() { return this.$store.state.settings.rememberNoteVisibility; }, - set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); } - }, - - defaultNoteVisibility: { - get() { return this.$store.state.settings.defaultNoteVisibility; }, - set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); } - }, - - pasteDialog: { - get() { return this.$store.state.settings.pasteDialog; }, - set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); } - }, - - showReplyTarget: { - get() { return this.$store.state.settings.showReplyTarget; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); } - }, - - showMyRenotes: { - get() { return this.$store.state.settings.showMyRenotes; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); } - }, - - showRenotedMyNotes: { - get() { return this.$store.state.settings.showRenotedMyNotes; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); } - }, - - showLocalRenotes: { - get() { return this.$store.state.settings.showLocalRenotes; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); } - }, - - showPostFormOnTopOfTl: { - get() { return this.$store.state.settings.showPostFormOnTopOfTl; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); } - }, - - suggestRecentHashtags: { - get() { return this.$store.state.settings.suggestRecentHashtags; }, - set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); } - }, - - showClockOnHeader: { - get() { return this.$store.state.settings.showClockOnHeader; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); } - }, - - circleIcons: { - get() { return this.$store.state.settings.circleIcons; }, - set(value) { - this.$store.dispatch('settings/set', { key: 'circleIcons', value }); - this.reload(); - } - }, - - contrastedAcct: { - get() { return this.$store.state.settings.contrastedAcct; }, - set(value) { - this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); - this.reload(); - } - }, - - showFullAcct: { - get() { return this.$store.state.settings.showFullAcct; }, - set(value) { - this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); - this.reload(); - } - }, - - showVia: { - get() { return this.$store.state.settings.showVia; }, - set(value) { this.$store.dispatch('settings/set', { key: 'showVia', value }); } - }, - - iLikeSushi: { - get() { return this.$store.state.settings.iLikeSushi; }, - set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); } - }, - - roomUseOrthographicCamera: { - get() { return this.$store.state.device.roomUseOrthographicCamera; }, - set(value) { this.$store.commit('device/set', { key: 'roomUseOrthographicCamera', value }); } - }, - - roomGraphicsQuality: { - get() { return this.$store.state.device.roomGraphicsQuality; }, - set(value) { this.$store.commit('device/set', { key: 'roomGraphicsQuality', value }); } - }, - - games_reversi_showBoardLabels: { - get() { return this.$store.state.settings.gamesReversiShowBoardLabels; }, - set(value) { this.$store.dispatch('settings/set', { key: 'gamesReversiShowBoardLabels', value }); } - }, - - games_reversi_useAvatarStones: { - get() { return this.$store.state.settings.gamesReversiUseAvatarStones; }, - set(value) { this.$store.dispatch('settings/set', { key: 'gamesReversiUseAvatarStones', value }); } - }, - - disableAnimatedMfm: { - get() { return this.$store.state.settings.disableAnimatedMfm; }, - set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } - }, - - disableShowingAnimatedImages: { - get() { return this.$store.state.device.disableShowingAnimatedImages; }, - set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); } - }, - - remainDeletedNote: { - get() { return this.$store.state.settings.remainDeletedNote; }, - set(value) { this.$store.dispatch('settings/set', { key: 'remainDeletedNote', value }); } - }, - - mobileNotificationPosition: { - get() { return this.$store.state.device.mobileNotificationPosition; }, - set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); } - }, - - enableMobileQuickNotificationView: { - get() { return this.$store.state.device.enableMobileQuickNotificationView; }, - set(value) { this.$store.commit('device/set', { key: 'enableMobileQuickNotificationView', value }); } - }, - - homeProfile: { - get() { return this.$store.state.device.homeProfile; }, - set(value) { this.$store.commit('device/set', { key: 'homeProfile', value }); } - }, - - mobileHomeProfile: { - get() { return this.$store.state.device.mobileHomeProfile; }, - set(value) { this.$store.commit('device/set', { key: 'mobileHomeProfile', value }); } - }, - - deckProfile: { - get() { return this.$store.state.device.deckProfile; }, - set(value) { this.$store.commit('device/set', { key: 'deckProfile', value }); } - }, - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - }, - methods: { - reload() { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.reload-to-apply-the-setting'), - showCancelButton: true - }).then(({ canceled }) => { - if (!canceled) { - location.reload(); - } - }); - }, - save(key, value) { - this.$store.dispatch('settings/set', { - key, - value - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('@._settings.saved') - }) - }); - }, - customizeHome() { - location.href = '/?customize'; - }, - updateWallpaper() { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - this.$store.dispatch('settings/set', { key: 'wallpaper', value: file.url }); - }); - }, - deleteWallpaper() { - this.$store.dispatch('settings/set', { key: 'wallpaper', value: null }); - }, - checkForUpdate() { - this.checkingForUpdate = true; - checkForUpdate(this.$root, true, true).then(newer => { - this.checkingForUpdate = false; - this.latestVersion = newer; - if (newer == null) { - this.$root.dialog({ - title: this.$t('@._settings.no-updates'), - text: this.$t('@._settings.no-updates-desc') - }); - } else { - this.$root.dialog({ - title: this.$t('@._settings.update-available'), - text: this.$t('@._settings.update-available-desc') - }); - } - }); - }, - soundTest() { - const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = this.$store.state.device.soundVolume; - sound.play(); - }, - pastedFileNamePreview() { - return `${formatTimeString(new Date(), this.pastedFileName).replace(/{{number}}/g, `1`)}.png` - }, - previewReaction() { - const picker = this.$root.new(MkReactionPicker, { - source: this.$refs.reactionsPreviewButton.$el, - reactions: this.reactions.trim().split('\n'), - showFocus: false, - }); - picker.$once('chosen', reaction => { - picker.close(); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/components/settings/signins.vue b/src/client/app/common/views/components/settings/signins.vue deleted file mode 100644 index 048fa2fc5bbad7c6326a808bcd4987ff29083554..0000000000000000000000000000000000000000 --- a/src/client/app/common/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 icon="check"/></template> - <template v-else><fa icon="times"/></template> - <span class="ip">{{ signin.ip }}</span> - <mk-time :time="signin.createdAt"/> - </header> - <div class="headers" v-show="signin._show"> - <!-- TODO --> - </div> - </div> -</div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - data() { - return { - fetching: true, - signins: [], - connection: null - }; - }, - - mounted() { - this.$root.api('i/signin_history').then(signins => { - this.signins = signins; - this.fetching = false; - }); - - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('signin', this.onSignin); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - 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-icon] - 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/client/app/common/views/components/settings/tags.vue b/src/client/app/common/views/components/settings/tags.vue deleted file mode 100644 index 2e17f35e3ef9ea74b9f079dcf983dc3af9604d06..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/tags.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<div class="vfcitkilproprqtbnpoertpsziierwzi"> - <div v-for="timeline in timelines" class="timeline" :key="timeline.id"> - <ui-input v-model="timeline.title" @change="save"> - <span>{{ $t('title') }}</span> - </ui-input> - <ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" :pre="true" @input="onQueryChange(timeline, $event)"> - <span>{{ $t('query') }}</span> - </ui-textarea> - </div> - <ui-button class="add" @click="add">{{ $t('add') }}</ui-button> - <ui-button class="save" @click="save">{{ $t('save') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings.tags.vue'), - data() { - return { - timelines: this.$store.state.settings.tagTimelines - }; - }, - - methods: { - add() { - this.timelines.push({ - id: uuid(), - title: '', - query: '' - }); - }, - - save() { - const timelines = this.timelines - .filter(timeline => timeline.title) - .map(timeline => { - if (!(timeline.query && timeline.query[0] && timeline.query[0][0])) { - timeline.query = timeline.title.split('\n').map(tags => tags.split(' ')); - } - return timeline; - }); - - this.$store.dispatch('settings/set', { key: 'tagTimelines', value: timelines }); - }, - - onQueryChange(timeline, value) { - timeline.query = value.split('\n').map(tags => tags.split(' ')); - } - } -}); -</script> - -<style lang="stylus" scoped> -.vfcitkilproprqtbnpoertpsziierwzi - > .timeline - padding-bottom 16px - border-bottom solid 1px rgba(#000, 0.1) - - > .add - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue deleted file mode 100644 index d916a5750808ef775457bbe2a58f1b95ac6cb8ef..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/settings/theme.vue +++ /dev/null @@ -1,558 +0,0 @@ -<template> -<ui-card> - <template #title><fa icon="palette"/> {{ $t('theme') }}</template> - <section class="nicnklzforebnpfgasiypmpdaaglujqm fit-top"> - <div class="dark"> - <div class="toggleWrapper"> - <input type="checkbox" class="dn" id="dn" v-model="darkmode"/> - <label for="dn" class="toggle"> - <span class="toggle__handler"> - <span class="crater crater--1"></span> - <span class="crater crater--2"></span> - <span class="crater crater--3"></span> - </span> - <span class="star star--1"></span> - <span class="star star--2"></span> - <span class="star star--3"></span> - <span class="star star--4"></span> - <span class="star star--5"></span> - <span class="star star--6"></span> - </label> - </div> - </div> - - <label> - <ui-select v-model="light" :placeholder="$t('light-theme')"> - <template #label><fa :icon="faSun"/> {{ $t('light-theme') }}</template> - <optgroup :label="$t('light-themes')"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('dark-themes')"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </ui-select> - </label> - - <label> - <ui-select v-model="dark" :placeholder="$t('dark-theme')"> - <template #label><fa :icon="faMoon"/> {{ $t('dark-theme') }}</template> - <optgroup :label="$t('dark-themes')"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('light-themes')"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </ui-select> - </label> - - <a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank">{{ $t('find-more-theme') }}</a> - - <details class="creator"> - <summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary> - <div> - <span>{{ $t('base-theme') }}:</span> - <ui-radio v-model="myThemeBase" value="light">{{ $t('base-theme-light') }}</ui-radio> - <ui-radio v-model="myThemeBase" value="dark">{{ $t('base-theme-dark') }}</ui-radio> - </div> - <div> - <ui-input v-model="myThemeName"> - <span>{{ $t('theme-name') }}</span> - </ui-input> - <ui-textarea v-model="myThemeDesc"> - <span>{{ $t('desc') }}</span> - </ui-textarea> - </div> - <div> - <div style="padding-bottom:8px;">{{ $t('primary-color') }}:</div> - <color-picker v-model="myThemePrimary"/> - </div> - <div> - <div style="padding-bottom:8px;">{{ $t('secondary-color') }}:</div> - <color-picker v-model="myThemeSecondary"/> - </div> - <div> - <div style="padding-bottom:8px;">{{ $t('text-color') }}:</div> - <color-picker v-model="myThemeText"/> - </div> - <ui-button @click="preview()"><fa icon="eye"/> {{ $t('preview-created-theme') }}</ui-button> - <ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> {{ $t('save-created-theme') }}</ui-button> - </details> - - <details> - <summary><fa icon="download"/> {{ $t('install-a-theme') }}</summary> - <ui-button @click="import_()"><fa icon="file-import"/> {{ $t('import') }}</ui-button> - <input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/> - <p>{{ $t('import-by-code') }}:</p> - <ui-textarea v-model="installThemeCode"> - <span>{{ $t('theme-code') }}</span> - </ui-textarea> - <ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> {{ $t('install') }}</ui-button> - </details> - - <details> - <summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary> - <ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')"> - <optgroup :label="$t('builtin-themes')"> - <option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('my-themes')"> - <option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$t('installed-themes')"> - <option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </ui-select> - <template v-if="selectedTheme"> - <ui-input readonly :value="selectedTheme.author"> - <span>{{ $t('author') }}</span> - </ui-input> - <ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc"> - <span>{{ $t('desc') }}</span> - </ui-textarea> - <ui-textarea readonly tall :value="selectedThemeCode"> - <span>{{ $t('theme-code') }}</span> - </ui-textarea> - <ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button> - <ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button> - </template> - </details> - </section> -</ui-card> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme'; -import { Chrome } from 'vue-color'; -import { v4 as uuid } from 'uuid'; -import * as tinycolor from 'tinycolor2'; -import * as JSON5 from 'json5'; -import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/theme.vue'), - components: { - ColorPicker: Chrome - }, - - data() { - return { - builtinThemes: builtinThemes, - installThemeCode: null, - selectedThemeId: null, - myThemeBase: 'light', - myThemeName: '', - myThemeDesc: '', - myThemePrimary: lightTheme.vars.primary, - myThemeSecondary: lightTheme.vars.secondary, - myThemeText: lightTheme.vars.text, - faMoon, faSun - }; - }, - - computed: { - themes(): Theme[] { - return builtinThemes.concat(this.$store.state.device.themes); - }, - - darkThemes(): Theme[] { - return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark'); - }, - - lightThemes(): Theme[] { - return this.themes.filter(t => t.base == 'light' || t.kind == 'light'); - }, - - installedThemes(): Theme[] { - return this.$store.state.device.themes; - }, - - light: { - get() { return this.$store.state.device.lightTheme; }, - set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); } - }, - - dark: { - get() { return this.$store.state.device.darkTheme; }, - set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); } - }, - - selectedTheme() { - if (this.selectedThemeId == null) return null; - return this.themes.find(x => x.id == this.selectedThemeId); - }, - - selectedThemeCode() { - if (this.selectedTheme == null) return null; - return JSON5.stringify(this.selectedTheme, null, '\t'); - }, - - myTheme(): any { - return { - name: this.myThemeName, - author: this.$store.state.i.username, - desc: this.myThemeDesc, - base: this.myThemeBase, - vars: { - primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(), - secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(), - text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString() - } - }; - }, - - darkmode: { - get() { return this.$store.state.device.darkmode; }, - set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } - }, - }, - - watch: { - myThemeBase(v) { - const theme = v == 'light' ? lightTheme : darkTheme; - this.myThemePrimary = theme.vars.primary; - this.myThemeSecondary = theme.vars.secondary; - this.myThemeText = theme.vars.text; - } - }, - - methods: { - install(code) { - let theme; - - try { - theme = JSON5.parse(code); - } catch (e) { - this.$root.dialog({ - type: 'error', - text: this.$t('invalid-theme') - }); - return; - } - - if (theme.id == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('invalid-theme') - }); - return; - } - - if (this.$store.state.device.themes.some(t => t.id == theme.id)) { - this.$root.dialog({ - type: 'info', - text: this.$t('already-installed') - }); - return; - } - - const themes = this.$store.state.device.themes.concat(theme); - this.$store.commit('device/set', { - key: 'themes', value: themes - }); - - this.$root.dialog({ - type: 'success', - text: this.$t('installed').replace('{}', theme.name) - }); - }, - - uninstall() { - const theme = this.selectedTheme; - const themes = this.$store.state.device.themes.filter(t => t.id != theme.id); - this.$store.commit('device/set', { - key: 'themes', value: themes - }); - - this.$root.dialog({ - type: 'info', - text: this.$t('uninstalled').replace('{}', theme.name) - }); - }, - - import_() { - (this.$refs.file as any).click(); - }, - - export_() { - const blob = new Blob([this.selectedThemeCode], { - type: 'application/json5' - }); - this.$refs.export.$el.href = window.URL.createObjectURL(blob); - }, - - onUpdateImportFile() { - const f = (this.$refs.file as any).files[0]; - - const reader = new FileReader(); - - reader.onload = e => { - this.install(e.target.result); - }; - - reader.readAsText(f); - }, - - preview() { - applyTheme(this.myTheme, false); - }, - - gen() { - const theme = this.myTheme; - - if (theme.name == null || theme.name.trim() == '') { - this.$root.dialog({ - type: 'warning', - text: this.$t('theme-name-required') - }); - return; - } - - theme.id = uuid(); - - const themes = this.$store.state.device.themes.concat(theme); - this.$store.commit('device/set', { - key: 'themes', value: themes - }); - - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.nicnklzforebnpfgasiypmpdaaglujqm - > .dark - margin-top 48px - margin-bottom 110px - - .toggleWrapper { - position: absolute; - top: 50%; - left: 50%; - overflow: hidden; - padding: 0 200px; - transform: translate3d(-50%, -50%, 0); - - input { - position: absolute; - left: -99em; - } - } - - .toggle { - cursor: pointer; - display: inline-block; - position: relative; - width: 90px; - height: 50px; - background-color: #83D8FF; - border-radius: 90px - 6; - transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - - &:before { - content: 'Light'; - position: absolute; - left: -60px; - top: 15px; - font-size: 18px; - color: var(--primary); - } - - &:after { - content: 'Dark'; - position: absolute; - right: -58px; - top: 15px; - font-size: 18px; - color: var(--text); - } - } - - .toggle__handler { - display: inline-block; - position: relative; - z-index: 1; - top: 3px; - left: 3px; - width: 50px - 6; - height: 50px - 6; - background-color: #FFCF96; - border-radius: 50px; - box-shadow: 0 2px 6px rgba(0,0,0,.3); - transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; - transform: rotate(-45deg); - - .crater { - position: absolute; - background-color: #E8CDA5; - opacity: 0; - transition: opacity 200ms ease-in-out !important; - border-radius: 100%; - } - - .crater--1 { - top: 18px; - left: 10px; - width: 4px; - height: 4px; - } - - .crater--2 { - top: 28px; - left: 22px; - width: 6px; - height: 6px; - } - - .crater--3 { - top: 10px; - left: 25px; - width: 8px; - height: 8px; - } - } - - .star { - position: absolute; - background-color: #ffffff; - transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - border-radius: 50%; - } - - .star--1 { - top: 10px; - left: 35px; - z-index: 0; - width: 30px; - height: 3px; - } - - .star--2 { - top: 18px; - left: 28px; - z-index: 1; - width: 30px; - height: 3px; - } - - .star--3 { - top: 27px; - left: 40px; - z-index: 0; - width: 30px; - height: 3px; - } - - .star--4, - .star--5, - .star--6 { - opacity: 0; - transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - - .star--4 { - top: 16px; - left: 11px; - z-index: 0; - width: 2px; - height: 2px; - transform: translate3d(3px,0,0); - } - - .star--5 { - top: 32px; - left: 17px; - z-index: 0; - width: 3px; - height: 3px; - transform: translate3d(3px,0,0); - } - - .star--6 { - top: 36px; - left: 28px; - z-index: 0; - width: 2px; - height: 2px; - transform: translate3d(3px,0,0); - } - - input:checked { - + .toggle { - background-color: #749DD6; - - &:before { - color: var(--text); - } - - &:after { - color: var(--primary); - } - - .toggle__handler { - background-color: #FFE5B5; - transform: translate3d(40px, 0, 0) rotate(0); - - .crater { opacity: 1; } - } - - .star--1 { - width: 2px; - height: 2px; - } - - .star--2 { - width: 4px; - height: 4px; - transform: translate3d(-5px, 0, 0); - } - - .star--3 { - width: 2px; - height: 2px; - transform: translate3d(-7px, 0, 0); - } - - .star--4, - .star--5, - .star--6 { - opacity: 1; - transform: translate3d(0,0,0); - } - .star--4 { - transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - .star--5 { - transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - .star--6 { - transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - } - } - - > a - display block - margin-top -16px - margin-bottom 16px - - > details - border-top solid var(--lineWidth) var(--faceDivider) - - > summary - padding 16px 0 - - > *:last-child - margin-bottom 16px - - > .creator - > div - padding 16px 0 - border-bottom solid var(--lineWidth) var(--faceDivider) -</style> diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue deleted file mode 100644 index 8ab1cfcfeb30ca00c5d2ca9779f761b7fc6a2688..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/stream-indicator.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<div class="mk-stream-indicator"> - <p v-if="stream.state == 'initializing'"> - <fa icon="spinner" pulse/> - <span>{{ $t('connecting') }}<mk-ellipsis/></span> - </p> - <p v-if="stream.state == 'reconnecting'"> - <fa icon="spinner" pulse/> - <span>{{ $t('reconnecting') }}<mk-ellipsis/></span> - </p> - <p v-if="stream.state == 'connected'"> - <fa icon="check"/> - <span>{{ $t('connected') }}</span> - </p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; - -export default Vue.extend({ - i18n: i18n('common/views/components/stream-indicator.vue'), - computed: { - stream() { - return this.$root.stream; - } - }, - created() { - this.$root.stream.on('_connected_', this.onConnected); - this.$root.stream.on('_disconnected_', this.onDisconnected); - - this.$nextTick(() => { - if (this.stream.state == 'connected') { - this.$el.style.opacity = '0'; - } - }); - }, - beforeDestroy() { - this.$root.stream.off('_connected_', this.onConnected); - this.$root.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(#000, 0.8) - border-radius 4px - - > p - display block - margin 0 - - > [data-icon] - margin-right 0.25em - -</style> diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue deleted file mode 100644 index 3fa5e3b9d4ebf6e6767dcf5d2f0352081665d2dd..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/tag-cloud.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="jtivnzhfwquxpsfidertopbmwmchmnmo"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <p class="empty" v-else-if="tags.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> - <div v-else> - <vue-word-cloud - :words="tags.slice(0, 20).map(x => [x.tag, x.count])" - :color="color" - :spacing="1"> - <template slot-scope="{word, text, weight}"> - <div style="cursor: pointer;" :title="weight"> - {{ text }} - </div> - </template> - </vue-word-cloud> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as VueWordCloud from 'vuewordcloud'; - -export default Vue.extend({ - i18n: i18n('common/views/components/tag-cloud.vue'), - components: { - [VueWordCloud.name]: VueWordCloud - }, - data() { - return { - tags: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - fetch() { - this.$root.api('hashtags/trend').then(tags => { - this.tags = tags; - this.fetching = false; - }); - }, - color([, weight]) { - const peak = Math.max.apply(null, this.tags.map(x => x.count)); - const w = weight / peak; - - if (w > 0.9) { - return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69'; - } else if (w > 0.5) { - return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7'; - } else { - return this.$store.state.device.darkmode ? '#fff' : '#555'; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.jtivnzhfwquxpsfidertopbmwmchmnmo - height 100% - width 100% - - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - - > div - height 100% - width 100% - -</style> diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue deleted file mode 100644 index 536d55247cdd6721ff7cb815f678ca8308d9dfb4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/trends.vue +++ /dev/null @@ -1,100 +0,0 @@ -<template> -<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <transition-group v-else tag="div" name="chart"> - <div v-for="stat in stats" :key="stat.tag"> - <div class="tag"> - <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> - <p>{{ $t('count').replace('{}', stat.usersCount) }}</p> - </div> - <x-chart class="chart" :src="stat.chart"/> - </div> - </transition-group> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XChart from './trends.chart.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/trends.vue'), - components: { - XChart - }, - data() { - return { - stats: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - fetch() { - this.$root.api('hashtags/trend').then(stats => { - this.stats = stats; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.csqvmxybqbycalfhkxvyfrgbrdalkaoc - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - opacity 0.7 - - > [data-icon] - margin-right 4px - - > div - .chart-move - transition transform 1s ease - - > div - display flex - align-items center - padding 14px 16px - - &:not(:last-child) - border-bottom solid 1px var(--faceDivider) - - > .tag - flex 1 - overflow hidden - font-size 14px - color var(--text) - - > a - display block - width 100% - white-space nowrap - overflow hidden - text-overflow ellipsis - color inherit - - > p - margin 0 - font-size 75% - opacity 0.7 - - > .chart - height 30px - -</style> diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue deleted file mode 100644 index 59a5c858a724edeb35c4392f65d4da7e7b38bef1..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/button.vue +++ /dev/null @@ -1,224 +0,0 @@ -<template> -<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr" - :is="link ? 'a' : 'button'" - :class="{ inline, primary, wait, round: $store.state.device.roundedCorners }" - :type="type" - @click="$emit('click')" - @mousedown="onMousedown" -> - <div ref="ripples" class="ripples"></div> - <div class="content"> - <slot></slot> - </div> -</component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - inject: { - horizonGrouped: { - default: false - } - }, - props: { - type: { - type: String, - required: false - }, - primary: { - type: Boolean, - required: false, - default: false - }, - inline: { - type: Boolean, - required: false, - default(): boolean { - return this.horizonGrouped; - } - }, - link: { - type: Boolean, - required: false, - default: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - wait: { - type: Boolean, - required: false, - default: false - }, - }, - mounted() { - if (this.autofocus) { - this.$nextTick(() => { - this.$el.focus(); - }); - } - }, - methods: { - onMousedown(e: MouseEvent) { - function distance(p, q) { - return Math.hypot(p.x - q.x, p.y - q.y); - } - - function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) { - const origin = {x: circleCenterX, y: circleCenterY}; - const dist1 = distance({x: 0, y: 0}, origin); - const dist2 = distance({x: boxW, y: 0}, origin); - const dist3 = distance({x: 0, y: boxH}, origin); - const dist4 = distance({x: boxW, y: boxH }, origin); - return Math.max(dist1, dist2, dist3, dist4) * 2; - } - - const rect = e.target.getBoundingClientRect(); - - const ripple = document.createElement('div'); - ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px'; - ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px'; - - this.$refs.ripples.appendChild(ripple); - - const circleCenterX = e.clientX - rect.left; - const circleCenterY = e.clientY - rect.top; - - const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY); - - setTimeout(() => { - ripple.style.transform = 'scale(' + (scale / 2) + ')'; - }, 1); - setTimeout(() => { - ripple.style.transition = 'all 1s ease'; - ripple.style.opacity = '0'; - }, 1000); - setTimeout(() => { - if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple); - }, 2000); - } - } -}); -</script> - -<style lang="stylus" scoped> -.dmtdnykelhudezerjlfpbhgovrgnqqgr - display block - width 100% - margin 0 - padding 8px 10px - text-align center - font-weight normal - font-size 14px - line-height 24px - border none - outline none - box-shadow none - text-decoration none - user-select none - color var(--text) - background var(--buttonBg) - - &.round - border-radius 6px - - &:not(:disabled):hover - background var(--buttonHoverBg) - - &:not(:disabled):active - background var(--buttonActiveBg) - - &.primary - color var(--primaryForeground) - background var(--primary) - - &:not(:disabled):hover - background var(--primaryLighten5) - - &:not(:disabled):active - background var(--primaryDarken5) - - * - pointer-events none - user-select none - - &:disabled - opacity 0.7 - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - - &.round:focus:after - border-radius 10px - - &:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr - margin-top 16px - - &.inline - display inline-block - width auto - min-width 100px - - &.primary - font-weight bold - - &.wait - background linear-gradient( - 45deg, - var(--primaryDarken10) 25%, - var(--primary) 25%, - var(--primary) 50%, - var(--primaryDarken10) 50%, - var(--primaryDarken10) 75%, - var(--primary) 75%, - var(--primary) - ) - 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;} - - > .ripples - position absolute - z-index 0 - top 0 - left 0 - width 100% - height 100% - overflow hidden - - >>> div - position absolute - width 2px - height 2px - border-radius 100% - background rgba(0, 0, 0, 0.1) - opacity 1 - transform scale(1) - transition all 0.5s cubic-bezier(0, .5, .5, 1) - - &.round > .ripples - border-radius 6px - - &.primary > .ripples >>> div - background rgba(0, 0, 0, 0.15) - - > .content - z-index 1 - -</style> diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue deleted file mode 100644 index a83013f5d0105678af9e1e6dcce2eb3e7ef3103f..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/card.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<div class="ui-card" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header> - <slot name="title"></slot> - </header> - - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - provide() { - return { - isCardChild: true - }; - } -}); -</script> - -<style lang="stylus" scoped> -.ui-card - margin 16px - max-width 850px - color var(--faceText) - background var(--face) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > header - padding 16px - font-weight bold - font-size 20px - color var(--faceText) - - @media (min-width 500px) - padding 24px 32px - - > section - padding 20px 16px - border-top solid var(--lineWidth) var(--faceDivider) - - @media (min-width 500px) - padding 32px - - &.fit-top - padding-top 0 - - &.fit-bottom - padding-bottom 0 - - > header - margin-bottom 16px - font-weight bold - color var(--faceText) - - > section - margin 16px 0 - - > header - font-weight bold - color var(--text) - -</style> diff --git a/src/client/app/common/views/components/ui/form.vue b/src/client/app/common/views/components/ui/form.vue deleted file mode 100644 index 5c5bbd7256feba52b75d5c276c4cf3c4e206e472..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/form.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<div class="ui-form"> - <fieldset :disabled="disabled"> - <slot></slot> - </fieldset> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - disabled: { - type: Boolean, - required: false - } - } -}); -</script> - -<style lang="stylus" scoped> - - -.ui-form - > fieldset - margin 0 - padding 0 - border none - -</style> diff --git a/src/client/app/common/views/components/ui/form/button.vue b/src/client/app/common/views/components/ui/form/button.vue deleted file mode 100644 index 3fd7b476298a6c4a50e4e163e444f0290064a294..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/form/button.vue +++ /dev/null @@ -1,81 +0,0 @@ -<template> -<div class="nvemkhtwcnnpkdrwfcbzuwhfulejhmzg" :class="{ round, primary }"> - <button @click="$emit('click')"> - <slot></slot> - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - round: { - type: Boolean, - required: false, - default: false - }, - primary: { - type: Boolean, - required: false, - default: false - } - } -}); -</script> - -<style lang="stylus" scoped> -.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg - display inline-block - - & + .nvemkhtwcnnpkdrwfcbzuwhfulejhmzg - margin-left 12px - - > button - display inline-block - margin 0 - padding 12px 20px - font-size 14px - border 1px solid var(--formButtonBorder) - border-radius 4px - outline none - box-shadow none - color var(--text) - transition 0.1s - - * - pointer-events none - - &:hover - &:focus - color var(--primary) - background var(--formButtonHoverBg) - border-color var(--formButtonHoverBorder) - - &:active - color var(--primaryDarken20) - background var(--formButtonActiveBg) - border-color var(--primary) - transition all 0s - - &.primary - > button - border 1px solid var(--primary) - background var(--primary) - color var(--primaryForeground) - - &:hover - &:focus - background var(--primaryLighten20) - border-color var(--primaryLighten20) - - &:active - background var(--primaryDarken20) - border-color var(--primaryDarken20) - transition all 0s - - &.round - > button - border-radius 64px - -</style> diff --git a/src/client/app/common/views/components/ui/form/radio.vue b/src/client/app/common/views/components/ui/form/radio.vue deleted file mode 100644 index 396b2997e51ca8e19d2194588d33a58df2ce9d1e..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/form/radio.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<div - class="uywduthvrdnlpsvsjkqigicixgyfctto" - :class="{ disabled, checked }" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input type="radio" - :disabled="disabled" - > - <span class="button"> - <span></span> - </span> - <span class="label"><slot></slot></span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - model: { - prop: 'model', - event: 'change' - }, - props: { - model: { - required: false - }, - value: { - required: false - }, - disabled: { - type: Boolean, - default: false - } - }, - computed: { - checked(): boolean { - return this.model === this.value; - } - }, - methods: { - toggle() { - this.$emit('change', this.value); - } - } -}); -</script> - -<style lang="stylus" scoped> -.uywduthvrdnlpsvsjkqigicixgyfctto - display inline-flex - margin 0 16px 0 0 - cursor pointer - transition all 0.3s - - > * - user-select none - - &:hover - > .button - border solid 2px var(--inputLabel) - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - border-color var(--primary) - - &:after - background-color var(--primary) - transform scale(1) - opacity 1 - - > .label - color var(--primary) - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - > .button - display inline-block - flex-shrink 0 - width 20px - height 20px - background none - border solid 2px var(--radioBorder) - border-radius 100% - transition inherit - - &:after - content '' - display block - position absolute - top 3px - right 3px - bottom 3px - left 3px - border-radius 100% - opacity 0 - transform scale(0) - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - - > .label - margin-left 8px - display block - font-size 14px - line-height 20px - cursor pointer - -</style> diff --git a/src/client/app/common/views/components/ui/horizon-group.vue b/src/client/app/common/views/components/ui/horizon-group.vue deleted file mode 100644 index 33d0300101d8566ed82a5ebd5a16a4a95f830bee..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/horizon-group.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div class="vnxwkwuf" :class="{ inputs, noGrow }" :data-children-count="children"> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - provide: { - horizonGrouped: true - }, - props: { - inputs: { - type: Boolean, - required: false, - default: false - }, - noGrow: { - type: Boolean, - required: false, - default: false - } - }, - data() { - return { - children: 0 - }; - }, - mounted() { - this.$nextTick(() => { - this.children = this.$slots.default.length; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.vnxwkwuf - margin 16px 0 - - &.inputs - margin 32px 0 - - &.fit-top - margin-top 0 - - &.fit-bottom - margin-bottom 0 - - &:not(.noGrow) - display flex - - > * - flex 1 - min-width 0 !important - - > *:not(:last-child) - margin-right 16px !important - - &[data-children-count="3"] - @media (max-width 600px) - display block - - > * - display block - width 100% !important - margin 16px 0 !important - - &:first-child - margin-top 0 !important - - &:last-child - margin-bottom 0 !important - -</style> diff --git a/src/client/app/common/views/components/ui/info.vue b/src/client/app/common/views/components/ui/info.vue deleted file mode 100644 index 30fd8cb344e24b4c8ba147058b711848a3f065fd..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/info.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<div class="ymxyweixqwsxauxldgpvecjepnwxbylu" :class="{ warn }"> - <i v-if="warn"><fa icon="exclamation-triangle"/></i> - <i v-else><fa icon="info-circle"/></i> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - warn: { - type: Boolean, - required: false, - default: false - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.ymxyweixqwsxauxldgpvecjepnwxbylu - margin 16px 0 - padding 16px - font-size 90% - background var(--infoBg) - color var(--infoFg) - - &.warn - background var(--infoWarnBg) - color var(--infoWarnFg) - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > i - margin-right 4px - -</style> diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue deleted file mode 100644 index 1b339a9ae0052de4ed396668af67b4d7e037258f..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/input.vue +++ /dev/null @@ -1,503 +0,0 @@ -<template> -<div class="ui-input" :class="[{ focused, filled, inline, disabled }, styl]"> - <div class="icon" ref="icon"><slot name="icon"></slot></div> - <div class="input"> - <div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> - <div class="value" ref="passwordMetar"></div> - </div> - <span class="label" ref="label"><slot></slot></span> - <span class="title" ref="title"> - <slot name="title"></slot> - <span class="warning" v-if="invalid"><fa :icon="['fa', 'exclamation-circle']"/>{{ $refs.input.validationMessage }}</span> - </span> - <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> - <template v-if="type != 'file'"> - <input v-if="debounce" ref="input" - v-debounce="500" - :type="type" - v-model.lazy="v" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - @focus="focused = true" - @blur="focused = false" - @keydown="$emit('keydown', $event)" - @change="$emit('change', $event)" - :list="id" - > - <input v-else ref="input" - :type="type" - v-model="v" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - @focus="focused = true" - @blur="focused = false" - @keydown="$emit('keydown', $event)" - @change="$emit('change', $event)" - :list="id" - > - <datalist :id="id" v-if="datalist"> - <option v-for="data in datalist" :value="data"/> - </datalist> - </template> - <template v-else> - <input ref="input" - type="text" - :value="filePlaceholder" - readonly - @click="chooseFile" - > - <input ref="file" - type="file" - :value="value" - @change="onChangeFile" - > - </template> - <div class="suffix" ref="suffix"><slot name="suffix"></slot></div> - </div> - <div class="toggle" v-if="withPasswordToggle"> - <a @click="togglePassword"> - <span v-if="type == 'password'"><fa :icon="['fa', 'eye']"/> {{ $t('@.show-password') }}</span> - <span v-if="type != 'password'"><fa :icon="['far', 'eye-slash']"/> {{ $t('@.hide-password') }}</span> - </a> - </div> - <div class="desc"><slot name="desc"></slot></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import debounce from 'v-debounce'; -const getPasswordStrength = require('syuilo-password-strength'); - -export default Vue.extend({ - directives: { - debounce - }, - inject: { - horizonGrouped: { - default: false - } - }, - props: { - value: { - required: false - }, - type: { - type: String, - required: false - }, - required: { - type: Boolean, - required: false - }, - readonly: { - type: Boolean, - required: false - }, - disabled: { - type: Boolean, - required: false - }, - pattern: { - type: String, - required: false - }, - placeholder: { - type: String, - required: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - autocomplete: { - required: false - }, - spellcheck: { - required: false - }, - debounce: { - required: false - }, - withPasswordMeter: { - type: Boolean, - required: false, - default: false - }, - withPasswordToggle: { - type: Boolean, - required: false, - default: false - }, - datalist: { - type: Array, - required: false, - }, - inline: { - type: Boolean, - required: false, - default(): boolean { - return this.horizonGrouped; - } - }, - styl: { - type: String, - required: false, - default: 'line' - } - }, - data() { - return { - v: this.value, - focused: false, - invalid: false, - passwordStrength: '', - id: Math.random().toString() - }; - }, - computed: { - filled(): boolean { - return this.v != '' && this.v != null; - }, - filePlaceholder(): string { - if (this.type != 'file') return null; - if (this.v == null) return null; - - if (typeof this.v == 'string') return this.v; - - if (Array.isArray(this.v)) { - return this.v.map(file => file.name).join(', '); - } else { - return this.v.name; - } - } - }, - watch: { - value(v) { - this.v = v; - }, - v(v) { - if (this.type === 'number') { - this.$emit('input', parseInt(v, 10)); - } else { - this.$emit('input', v); - } - - if (this.withPasswordMeter) { - if (v == '') { - this.passwordStrength = ''; - return; - } - - const strength = getPasswordStrength(v); - this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; - (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; - } - - this.invalid = this.$refs.input.validity.badInput; - } - }, - mounted() { - if (this.autofocus) { - this.$nextTick(() => { - this.$refs.input.focus(); - }); - } - - this.$nextTick(() => { - // ã“ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒä½œæˆã•ã‚ŒãŸæ™‚ã€éžè¡¨ç¤ºçŠ¶æ…‹ã§ã‚ã‚‹å ´åˆãŒã‚ã‚‹ - // éžè¡¨ç¤ºçŠ¶æ…‹ã ã¨è¦ç´ ã®å¹…ãªã©ã¯0ã«ãªã£ã¦ã—ã¾ã†ã®ã§ã€å®šæœŸçš„ã«è¨ˆç®—ã™ã‚‹ - const clock = setInterval(() => { - if (this.$refs.prefix) { - this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; - if (this.$refs.prefix.offsetWidth) { - this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px'; - } - } - if (this.$refs.suffix) { - if (this.$refs.suffix.offsetWidth) { - this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px'; - } - } - }, 100); - - this.$once('hook:beforeDestroy', () => { - clearInterval(clock); - }); - }); - - this.$on('keydown', (e: KeyboardEvent) => { - if (e.code == 'Enter') { - this.$emit('enter'); - } - }); - }, - methods: { - focus() { - this.$refs.input.focus(); - }, - togglePassword() { - if (this.type == 'password') { - this.type = 'text' - } else { - this.type = 'password' - } - }, - chooseFile() { - this.$refs.file.click(); - }, - onChangeFile() { - this.v = Array.from((this.$refs.file as any).files); - this.$emit('input', this.v); - this.$emit('change', this.v); - } - } -}); -</script> - -<style lang="stylus" scoped> -root(fill) - margin 32px 0 - - > .icon - position absolute - top 0 - left 0 - width 24px - text-align center - line-height 32px - color var(--inputLabel) - - &:not(:empty) + .input - margin-left 28px - - > .input - - if !fill - &:before - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 1px - background var(--inputBorder) - - &:after - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 2px - background var(--primary) - opacity 0 - transform scaleX(0.12) - transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) - will-change border opacity transform - - > .password-meter - position absolute - top 0 - left 0 - width 100% - height 100% - border-radius 6px - overflow hidden - opacity 0.3 - - &[data-strength=''] - display none - - &[data-strength='low'] - > .value - background #d73612 - - &[data-strength='medium'] - > .value - background #d7ca12 - - &[data-strength='high'] - > .value - background #61bb22 - - > .value - display block - width 0 - height 100% - background transparent - border-radius 6px - transition all 0.1s ease - - > .label - position absolute - z-index 1 - top fill ? 6px : 0 - left 0 - pointer-events none - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - transition-duration 0.3s - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(1) - - > .title - position absolute - z-index 1 - top fill ? -24px : -17px - left 0 !important - pointer-events none - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(.75) - white-space nowrap - width 133% - overflow hidden - text-overflow ellipsis - - > .warning - margin-left 0.5em - color var(--infoWarnFg) - - > svg - margin-right 0.1em - - > input - display block - width 100% - margin 0 - padding 0 - font inherit - font-weight fill ? bold : normal - font-size 16px - line-height 32px - color var(--inputText) - background transparent - border none - border-radius 0 - outline none - box-shadow none - - if fill - padding 6px 12px - background rgba(#000, 0.035) - border-radius 6px - - &[type='file'] - display none - - > .prefix - > .suffix - display block - position absolute - z-index 1 - top 0 - font-size 16px - line-height fill ? 44px : 32px - color var(--inputLabel) - pointer-events none - - &:empty - display none - - > * - display inline-block - min-width 16px - max-width 150px - overflow hidden - white-space nowrap - text-overflow ellipsis - - > .prefix - left 0 - padding-right 4px - - if fill - padding-left 12px - - > .suffix - right 0 - padding-left 4px - - if fill - padding-right 12px - - > .toggle - cursor pointer - padding-left 0.5em - font-size 0.7em - opacity 0.7 - text-align left - - > a - color var(--inputLabel) - text-decoration none - - > .desc - margin 6px 0 - font-size 13px - - &:empty - display none - - * - margin 0 - - &.focused - > .input - if fill - background rgba(#000, 0.05) - else - &:after - opacity 1 - transform scaleX(1) - - > .label - color var(--primary) - - &.focused - &.filled - > .input - > .label - top fill ? -24px : -17px - left 0 !important - transform scale(0.75) - -.ui-input - &.fill - root(true) - &:not(.fill) - root(false) - - &.inline - display inline-block - margin 0 - - &.disabled - opacity 0.7 - - &, * - cursor not-allowed !important - -</style> diff --git a/src/client/app/common/views/components/ui/margin.vue b/src/client/app/common/views/components/ui/margin.vue deleted file mode 100644 index 508116f07005125c692e29a3786cb1a25c449716..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/margin.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<div class="zdcrxcne"> - <slot></slot> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({}); -</script> - -<style lang="stylus" scoped> -.zdcrxcne - margin 16px - -</style> diff --git a/src/client/app/common/views/components/ui/modal.vue b/src/client/app/common/views/components/ui/modal.vue deleted file mode 100644 index 413dc39fa5de78a9582993970b3bb7e3741d1399..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/modal.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<div class="modal"> - <div class="bg" ref="bg" @click="onBgClick" /> - <slot class="main" /> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: { - closeOnBgClick: { - type: Boolean, - required: false, - default: true - }, - openAnimeDuration: { - type: Number, - required: false, - default: 100 - }, - closeAnimeDuration: { - type: Number, - required: false, - default: 100 - } - }, - mounted() { - anime({ - targets: this.$refs.bg, - opacity: 1, - duration: this.openAnimeDuration, - easing: 'linear' - }); - }, - methods: { - onBgClick() { - this.$emit('bg-click'); - if (this.closeOnBgClick) this.close(); - }, - close() { - this.$emit('before-close'); - - anime({ - targets: this.$refs.bg, - opacity: 0, - duration: this.closeAnimeDuration, - easing: 'linear', - complete: () => (this as any).destroyDom() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.modal - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - -.bg - display block - position fixed - z-index 1 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 0.7) - opacity 0 - -.main - z-index 1 -</style> diff --git a/src/client/app/common/views/components/ui/pagination.vue b/src/client/app/common/views/components/ui/pagination.vue deleted file mode 100644 index 67aa89d369027de2638a4d56c3fa577b4852fa3c..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/pagination.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<div class="mwermpua" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <slot :items="items"></slot> - </sequential-entrance> - <div class="more" v-if="more"> - <ui-button @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import paging from '../../../scripts/paging'; - -export default Vue.extend({ - mixins: [ - paging({ - captureWindowScroll: false, - }), - ], - - props: { - pagination: { - required: true - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.mwermpua - > .more - margin-top 16px - -</style> diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue deleted file mode 100644 index 468318b58ea08a18ab30d0811b7c30b90246f15b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/radio.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> -<div - class="ui-radio" - :class="{ disabled, checked }" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input type="radio" - :disabled="disabled" - > - <span class="button"> - <span></span> - </span> - <span class="label"><slot></slot></span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - model: { - prop: 'model', - event: 'change' - }, - props: { - model: { - required: false - }, - value: { - required: false - }, - disabled: { - type: Boolean, - default: false - } - }, - computed: { - checked(): boolean { - return this.model === this.value; - } - }, - methods: { - toggle() { - this.$emit('change', this.value); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ui-radio - display inline-block - margin 0 32px 0 0 - cursor pointer - transition all 0.3s - - > * - user-select none - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - border-color var(--radioActive) - - &:after - background-color var(--radioActive) - transform scale(1) - opacity 1 - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - > .button - position absolute - width 20px - height 20px - background none - border solid 2px var(--inputLabel) - border-radius 100% - transition inherit - - &:after - content '' - display block - position absolute - top 3px - right 3px - bottom 3px - left 3px - border-radius 100% - opacity 0 - transform scale(0) - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - - > .label - margin-left 28px - display block - font-size 16px - line-height 20px - cursor pointer - -</style> diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue deleted file mode 100644 index 1057d60d07ffdc977efaa059acacc6df51d8bfa2..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/select.vue +++ /dev/null @@ -1,238 +0,0 @@ -<template> -<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]"> - <div class="icon" ref="icon"><slot name="icon"></slot></div> - <div class="input" @click="focus"> - <span class="label" ref="label"><slot name="label"></slot></span> - <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> - <select ref="input" - v-model="v" - :required="required" - :disabled="disabled" - @focus="focused = true" - @blur="focused = false" - > - <slot></slot> - </select> - <div class="suffix"><slot name="suffix"></slot></div> - </div> - <div class="text"><slot name="text"></slot></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - inject: { - horizonGrouped: { - default: false - } - }, - props: { - value: { - required: false - }, - required: { - type: Boolean, - required: false - }, - disabled: { - type: Boolean, - required: false - }, - styl: { - type: String, - required: false, - default: 'line' - }, - inline: { - type: Boolean, - required: false, - default(): boolean { - return this.horizonGrouped; - } - }, - }, - data() { - return { - focused: false - }; - }, - computed: { - v: { - get() { - return this.value; - }, - set(v) { - this.$emit('input', v); - } - }, - filled(): boolean { - return this.v != '' && this.v != null; - } - }, - mounted() { - if (this.$refs.prefix) { - this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; - } - }, - methods: { - focus() { - this.$refs.input.focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -root(fill) - margin 32px 0 - - > .icon - position absolute - top 0 - left 0 - width 24px - text-align center - line-height 32px - color var(--inputLabel) - - &:not(:empty) + .input - margin-left 28px - - > .input - display flex - - if fill - padding 6px 12px - background rgba(#000, 0.035) - border-radius 6px - else - &:before - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 1px - background var(--inputBorder) - - &:after - content '' - display block - position absolute - bottom 0 - left 0 - right 0 - height 2px - background var(--primary) - opacity 0 - transform scaleX(0.12) - transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) - will-change border opacity transform - - > .label - position absolute - top fill ? 6px : 0 - left 0 - pointer-events none - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - transition-duration 0.3s - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(1) - - > select - display block - flex 1 - width 100% - padding 0 - font inherit - font-weight fill ? bold : normal - font-size 16px - height 32px - color var(--inputText) - background transparent - border none - border-radius 0 - outline none - box-shadow none - - * - color #000 - - > .prefix - > .suffix - display block - align-self center - justify-self center - font-size 16px - line-height 32px - color rgba(#000, 0.54) - pointer-events none - - &:empty - display none - - > * - display block - min-width 16px - - > .prefix - padding-right 4px - - > .suffix - padding-left 4px - - > .text - margin 6px 0 - font-size 13px - - &:empty - display none - - * - margin 0 - - &.focused - > .input - if fill - background rgba(#000, 0.05) - else - &:after - opacity 1 - transform scaleX(1) - - > .label - color var(--primary) - - &.focused - &.filled - > .input - > .label - top fill ? -24px : -17px - left 0 !important - transform scale(0.75) - -.ui-select - &.fill - root(true) - &:not(.fill) - root(false) - - &.inline - display inline-block - margin 0 - - &.disabled - opacity 0.7 - - &, * - cursor not-allowed !important - -</style> diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue deleted file mode 100644 index 8e3997ae78a5c20989388d1b6a64a2c43632f31a..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/switch.vue +++ /dev/null @@ -1,135 +0,0 @@ -<template> -<div - class="ui-switch" - :class="{ disabled, checked }" - role="switch" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input - type="checkbox" - ref="input" - :disabled="disabled" - @keydown.enter="toggle" - > - <span class="button"> - <span></span> - </span> - <span class="label"> - <span :aria-hidden="!checked"><slot></slot></span> - <p :aria-hidden="!checked"> - <slot name="desc"></slot> - </p> - </span> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - model: { - prop: 'value', - event: 'change' - }, - props: { - value: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - } - }, - computed: { - checked(): boolean { - return this.value; - } - }, - methods: { - toggle() { - if (this.disabled) return; - this.$emit('change', !this.checked); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ui-switch - display flex - margin 32px 0 - cursor pointer - transition all 0.3s - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > * - user-select none - - &.disabled - opacity 0.6 - cursor not-allowed - - &.checked - > .button - background-color var(--switchActiveTrack) - border-color var(--switchActiveTrack) - - > * - background-color var(--switchActive) - transform translateX(14px) - - > input - position absolute - width 0 - height 0 - opacity 0 - margin 0 - - > .button - display inline-block - flex-shrink 0 - margin 3px 0 0 0 - width 34px - height 14px - background var(--switchTrack) - outline none - border-radius 14px - transition inherit - - > * - position absolute - top -3px - left 0 - border-radius 100% - transition background-color 0.3s, transform 0.3s - width 20px - height 20px - background-color #fff - box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12) - - > .label - margin-left 8px - display block - font-size 16px - cursor pointer - transition inherit - color var(--text) - - > span - display block - line-height 20px - transition inherit - - > p - margin 0 - opacity 0.7 - font-size 90% - -</style> diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue deleted file mode 100644 index d265c7ac6d0893c9d6738321232cc02f230e240a..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/ui/textarea.vue +++ /dev/null @@ -1,194 +0,0 @@ -<template> -<div class="ui-textarea" :class="{ focused, filled, tall, pre }"> - <div class="input"> - <span class="label" ref="label"><slot></slot></span> - <textarea ref="input" - :value="value" - :required="required" - :readonly="readonly" - :pattern="pattern" - :autocomplete="autocomplete" - @input="$emit('input', $event.target.value)" - @focus="focused = true" - @blur="focused = false" - ></textarea> - </div> - <div class="desc"><slot name="desc"></slot></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - value: { - required: false - }, - required: { - type: Boolean, - required: false - }, - readonly: { - type: Boolean, - required: false - }, - pattern: { - type: String, - required: false - }, - autocomplete: { - type: String, - required: false - }, - tall: { - type: Boolean, - required: false, - default: false - }, - pre: { - type: Boolean, - required: false, - default: false - }, - }, - data() { - return { - focused: false, - passwordStrength: '' - } - }, - computed: { - filled(): boolean { - return this.value != '' && this.value != null; - } - }, - methods: { - focus() { - this.$refs.input.focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -root(fill) - margin 42px 0 32px 0 - - &:last-child - margin-bottom 0 - - > .input - padding 12px - - if fill - background rgba(#000, 0.035) - border-radius 6px - else - &:before - content '' - display block - position absolute - top 0 - bottom 0 - left 0 - right 0 - background none - border solid 1px var(--inputBorder) - border-radius 3px - pointer-events none - - &:after - content '' - display block - position absolute - top 0 - bottom 0 - left 0 - right 0 - background none - border solid 2px var(--primary) - border-radius 3px - opacity 0 - transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) - pointer-events none - - > .label - position absolute - top 6px - left 12px - pointer-events none - transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) - transition-duration 0.3s - font-size 16px - line-height 32px - color var(--inputLabel) - pointer-events none - //will-change transform - transform-origin top left - transform scale(1) - - > textarea - display block - width 100% - min-width 100% - max-width 100% - min-height 100px - padding 0 - font inherit - font-weight fill ? bold : normal - font-size 16px - color var(--inputText) - background transparent - border none - border-radius 0 - outline none - box-shadow none - - > .desc - margin 6px 0 - font-size 13px - opacity 0.7 - - &:empty - display none - - * - margin 0 - - &.focused - > .input - if fill - background rgba(#000, 0.05) - else - &:after - opacity 1 - - > .label - color var(--primary) - - &.focused - &.filled - > .input - > .label - top -24px - left 0 !important - transform scale(0.75) - - &.tall - > .input - > textarea - min-height 200px - - &.pre - > .input - > textarea - white-space pre - -.ui-textarea.fill - root(true) - -.ui-textarea:not(.fill) - root(false) - -</style> diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue deleted file mode 100644 index 9f02da6c1e5a48ca5df0dab5880eed4b66586094..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/uploader.vue +++ /dev/null @@ -1,231 +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> - <div class="top"> - <p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p> - <p class="status"> - <span class="initing" v-if="ctx.progress == undefined">{{ $t('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> - </div> - <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 i18n from '../../../i18n'; -import { apiUrl } from '../../../config'; -import getMD5 from '../../scripts/get-md5'; - -export default Vue.extend({ - i18n: i18n('common/views/components/uploader.vue'), - data() { - return { - uploads: [] - }; - }, - methods: { - checkExistence(fileData: ArrayBuffer): Promise<any> { - return new Promise((resolve, reject) => { - const data = new FormData(); - data.append('md5', getMD5(fileData)); - - this.$root.api('drive/files/find-by-hash', { - md5: getMD5(fileData) - }).then(resp => { - resolve(resp.length > 0 ? resp[0] : null); - }); - }); - }, - - upload(file: File, folder: any, name?: string) { - if (folder && typeof folder == 'object') folder = folder.id; - - const id = Math.random(); - - const reader = new FileReader(); - reader.onload = (e: any) => { - this.checkExistence(e.target.result).then(result => { - if (result !== null) { - this.$emit('uploaded', result); - return; - } - - const ctx = { - id: id, - name: name || file.name || 'untitled', - progress: undefined, - img: window.URL.createObjectURL(file) - }; - - this.uploads.push(ctx); - this.$emit('change', this.uploads); - - const data = new FormData(); - data.append('i', this.$store.state.i.token); - data.append('force', 'true'); - data.append('file', file); - - if (folder) data.append('folderId', folder); - if (name) data.append('name', name); - - 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); - }) - } - reader.readAsArrayBuffer(file); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-uploader - overflow auto - - &:empty - display none - - > ol - display block - margin 0 - padding 0 - list-style none - - > li - display grid - margin 8px 0 0 0 - padding 0 - height 36px - width: 100% - box-shadow 0 -1px 0 var(--primaryAlpha01) - border-top solid 8px transparent - grid-template-columns 36px calc(100% - 44px) - grid-template-rows 1fr 8px - column-gap 8px - box-sizing content-box - - &:first-child - margin 0 - box-shadow none - border-top none - - > .img - display block - background-size cover - background-position center center - grid-column 1 / 2 - grid-row 1 / 3 - - > .top - display flex - grid-column 2 / 3 - grid-row 1 / 2 - - > .name - display block - padding 0 8px 0 0 - margin 0 - font-size 0.8em - color var(--primaryAlpha07) - white-space nowrap - text-overflow ellipsis - overflow hidden - flex-shrink 1 - - > [data-icon] - margin-right 4px - - > .status - display block - margin 0 0 0 auto - padding 0 - font-size 0.8em - flex-shrink 0 - - > .initing - color var(--primaryAlpha05) - - > .kb - color var(--primaryAlpha05) - - > .percentage - display inline-block - width 48px - text-align right - - color var(--primaryAlpha07) - - &:after - content '%' - - > progress - display block - background transparent - border none - border-radius 4px - overflow hidden - grid-column 2 / 3 - grid-row 2 / 3 - z-index 2 - - &::-webkit-progress-value - background var(--primary) - - &::-webkit-progress-bar - background var(--primaryAlpha01) - - > .progress - display block - border none - border-radius 4px - background linear-gradient( - 45deg, - var(--primaryLighten30) 25%, - var(--primary) 25%, - var(--primary) 50%, - var(--primaryLighten30) 50%, - var(--primaryLighten30) 75%, - var(--primary) 75%, - var(--primary) - ) - background-size 32px 32px - animation bg 1.5s linear infinite - grid-column 2 / 3 - grid-row 2 / 3 - z-index 1 - - &.initing - opacity 0.3 - - @keyframes bg - from {background-position: 0 0;} - to {background-position: -64px 32px;} - -</style> diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue deleted file mode 100644 index 80aae5999d70cb4c51175241d7896ccf3229a819..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/url-preview.vue +++ /dev/null @@ -1,343 +0,0 @@ -<template> -<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> - <button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button> - <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen /> -</div> -<div v-else-if="tweetUrl && detail" class="twitter"> - <blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null"> - <a :href="url"></a> - </blockquote> -</div> -<div v-else class="mk-url-preview"> - <component :is="hasRoute ? 'router-link' : 'a'" :class="{ mini: narrow, compact }" :[attr]="hasRoute ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching"> - <div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`"> - <button v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="['far', 'play-circle']"/></button> - </div> - <article> - <header> - <h1 :title="title">{{ title }}</h1> - </header> - <p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p> - <footer> - <img class="icon" v-if="icon" :src="icon"/> - <p :title="sitename">{{ sitename }}</p> - </footer> - </article> - </component> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url as local, lang } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('common/views/components/url-preview.vue'), - - props: { - url: { - type: String, - require: true - }, - - detail: { - type: Boolean, - required: false, - default: false - }, - - compact: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - const isSelf = this.url.startsWith(local); - const hasRoute = - (this.url.substr(local.length) === '/') || - this.url.substr(local.length).startsWith('/@') || - this.url.substr(local.length).startsWith('/notes/') || - this.url.substr(local.length).startsWith('/tags/') || - this.url.substr(local.length).startsWith('/pages/'); - return { - local, - fetching: true, - title: null, - description: null, - thumbnail: null, - icon: null, - sitename: null, - player: { - url: null, - width: null, - height: null - }, - tweetUrl: null, - playerEnabled: false, - self: isSelf, - hasRoute: hasRoute, - attr: hasRoute ? 'to' : 'href', - target: hasRoute ? null : '_blank' - }; - }, - - created() { - const requestUrl = new URL(this.url); - - if (this.detail && requestUrl.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(requestUrl.pathname)) { - this.tweetUrl = requestUrl; - const twttr = (window as any).twttr || {}; - const loadTweet = () => twttr.widgets.load(this.$refs.tweet); - - if (twttr.widgets) { - Vue.nextTick(loadTweet); - } else { - const wjsId = 'twitter-wjs'; - if (!document.getElementById(wjsId)) { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('id', wjsId); - script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); - head.appendChild(script); - } - twttr.ready = loadTweet; - (window as any).twttr = twttr; - } - return; - } - - if (requestUrl.hostname === 'music.youtube.com') { - requestUrl.hostname = 'youtube.com'; - } - - const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP'); - - requestUrl.hash = ''; - - fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => { - res.json().then(info => { - if (info.url == null) return; - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - this.fetching = false; - this.player = info.player; - }) - }); - } -}); -</script> - -<style lang="stylus" scoped> -.player - position relative - width 100% - - > button - position absolute - top -1.5em - right 0 - font-size 1em - width 1.5em - height 1.5em - padding 0 - margin 0 - color var(--text) - background rgba(128, 128, 128, 0.2) - opacity 0.7 - - &:hover - opacity 0.9 - - > iframe - height 100% - left 0 - position absolute - top 0 - width 100% - -.mk-url-preview - > a - display block - font-size 14px - border solid var(--lineWidth) var(--urlPreviewBorder) - border-radius 4px - overflow hidden - - &:hover - text-decoration none - border-color var(--urlPreviewBorderHover) - - > article > header > h1 - text-decoration underline - - > .thumbnail - position absolute - width 100px - height 100% - background-position center - background-size cover - display flex - justify-content center - align-items center - - > button - font-size 3.5em - opacity: 0.7 - - &:hover - font-size 4em - opacity 0.9 - - & + article - left 100px - width calc(100% - 100px) - - > article - padding 16px - - > header - margin-bottom 8px - - > h1 - margin 0 - font-size 1em - color var(--urlPreviewTitle) - - > p - margin 0 - color var(--urlPreviewText) - 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 var(--urlPreviewInfo) - font-size 0.8em - line-height 16px - vertical-align top - - @media (max-width 700px) - > .thumbnail - position relative - width 100% - height 100px - - & + article - left 0 - width 100% - - @media (max-width 550px) - font-size 12px - - > .thumbnail - height 80px - - > article - padding 12px - - @media (max-width 500px) - font-size 10px - - > .thumbnail - height 70px - - > article - padding 8px - - > header - margin-bottom 4px - - > footer - margin-top 4px - - > img - width 12px - height 12px - - &.compact - > .thumbnail - position: absolute - width 56px - height 100% - - > article - left 56px - width calc(100% - 56px) - padding 4px - - > header - margin-bottom 2px - - > footer - margin-top 2px - - &.mini - font-size 10px - - > .thumbnail - position relative - width 100% - height 60px - - > article - left 0 - width 100% - padding 8px - - > header - margin-bottom 4px - - > footer - margin-top 4px - - > img - width 12px - height 12px - - &.compact - > .thumbnail - position absolute - width 56px - height 100% - - > article - left 56px - width calc(100% - 56px) - padding 4px - - > header - margin-bottom 2px - - > footer - margin-top 2px - - &.compact - > article - > header h1, p, footer - overflow hidden - white-space nowrap - text-overflow ellipsis -</style> diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue deleted file mode 100644 index 4ba4e67e549e03e89d50ee2a1b4f2de496b4e3d6..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/user-list.vue +++ /dev/null @@ -1,165 +0,0 @@ -<template> -<ui-container :body-togglable="true" :expanded="expanded"> - <template #header><slot></slot></template> - - <mk-error v-if="error" @retry="init()"/> - - <div class="efvhhmdq" :class="{ iconOnly }" v-size="[{ lt: 500, class: 'narrow' }]"> - <div class="no-users" v-if="empty"> - <p>{{ $t('no-users') }}</p> - </div> - <div class="user" v-for="user in users" :key="user.id"> - <mk-avatar class="avatar" :user="user"/> - <div class="body" v-if="!iconOnly"> - <div class="name"> - <router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link> - <p class="username">@{{ user | acct }}</p> - </div> - <div class="description" v-if="user.description" :title="user.description"> - <mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/> - </div> - <mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/> - </div> - </div> - <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore()" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - </div> -</ui-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-list.vue'), - - mixins: [ - paging({}), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - }, - iconOnly: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - - computed: { - users() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.efvhhmdq - &.narrow - > .user > .body > .name - width 100% - - > .user > .body > .description - display none - - &.iconOnly - padding 12px - - > .user - display inline-block - padding 0 - border-bottom none - - > .avatar - display inline-block - margin 4px - - > .no-users - text-align center - color var(--text) - - > .user - display flex - padding 16px - border-bottom solid 1px var(--faceDivider) - - &:last-child - border-bottom none - - > .avatar - display block - flex-shrink 0 - margin 0 12px 0 0 - width 42px - height 42px - border-radius 8px - - > .body - display flex - width calc(100% - 54px) - - > .name - width 45% - - > .name - margin 0 - font-size 16px - line-height 24px - color var(--text) - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color var(--text) - opacity 0.7 - - > .description - width 55% - color var(--text) - line-height 42px - white-space nowrap - overflow hidden - text-overflow ellipsis - opacity 0.7 - font-size 14px - padding-right 40px - - > .koudoku-button - position absolute - top 8px - right 0 - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - &:hover - background rgba(#000, 0.025) - - &:active - background rgba(#000, 0.05) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue deleted file mode 100644 index 532dcf35c2d122c07967a705843797e52db645de..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/user-menu.vue +++ /dev/null @@ -1,228 +0,0 @@ -<template> -<div style="position:initial"> - <mk-menu :source="source" :items="items" @closed="closed"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faExclamationCircle, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-menu.vue'), - - props: ['user', 'source'], - - data() { - let menu = [{ - icon: ['fas', 'at'], - text: this.$t('mention'), - action: () => { - this.$post({ mention: this.user }); - } - }, null, { - icon: ['fas', 'list'], - text: this.$t('push-to-list'), - action: this.pushList - }] as any; - - if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { - menu = menu.concat([null, { - icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], - text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), - action: this.toggleMute - }, { - icon: 'ban', - text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), - action: this.toggleBlock - }, null, { - icon: faExclamationCircle, - text: this.$t('report-abuse'), - action: this.reportAbuse - }]); - } - - if (this.$store.getters.isSignedIn && (this.$store.state.i.isAdmin || this.$store.state.i.isModerator)) { - menu = menu.concat([null, { - icon: faMicrophoneSlash, - text: this.user.isSilenced ? this.$t('unsilence') : this.$t('silence'), - action: this.toggleSilence - }, { - icon: faSnowflake, - text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'), - action: this.toggleSuspend - }]); - } - - return { - items: menu - }; - }, - - methods: { - closed() { - this.$nextTick(() => { - this.destroyDom(); - }); - }, - - async pushList() { - const t = this.$t('select-list'); // ãªãœã‹å¾Œã§å‚ç…§ã™ã‚‹ã¨ null ã«ãªã‚‹ã®ã§æœ€åˆã«ãƒ¡ãƒ¢ãƒªã«ç¢ºä¿ã—ã¦ãŠã - const lists = await this.$root.api('users/lists/list'); - const { canceled, result: listId } = await this.$root.dialog({ - type: null, - title: t, - select: { - items: lists.map(list => ({ - value: list.id, text: list.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - await this.$root.api('users/lists/push', { - listId: listId, - userId: this.user.id - }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }, - - async toggleMute() { - if (this.user.isMuted) { - if (!await this.getConfirmed(this.$t('unmute-confirm'))) return; - - this.$root.api('mute/delete', { - userId: this.user.id - }).then(() => { - this.user.isMuted = false; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } else { - if (!await this.getConfirmed(this.$t('mute-confirm'))) return; - - this.$root.api('mute/create', { - userId: this.user.id - }).then(() => { - this.user.isMuted = true; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - }, - - async toggleBlock() { - if (this.user.isBlocking) { - if (!await this.getConfirmed(this.$t('unblock-confirm'))) return; - - this.$root.api('blocking/delete', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = false; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } else { - if (!await this.getConfirmed(this.$t('block-confirm'))) return; - - this.$root.api('blocking/create', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = true; - }, () => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - } - }, - - async reportAbuse() { - const reported = this.$t('report-abuse-reported'); // ãªãœã‹å¾Œã§å‚ç…§ã™ã‚‹ã¨ null ã«ãªã‚‹ã®ã§æœ€åˆã«ãƒ¡ãƒ¢ãƒªã«ç¢ºä¿ã—ã¦ãŠã - const { canceled, result: comment } = await this.$root.dialog({ - title: this.$t('report-abuse-detail'), - input: true - }); - if (canceled) return; - this.$root.api('users/report-abuse', { - userId: this.user.id, - comment: comment - }).then(() => { - this.$root.dialog({ - type: 'success', - text: reported - }); - }, e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async toggleSilence() { - if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return; - - this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { - userId: this.user.id - }).then(() => { - this.user.isSilenced = !this.user.isSilenced; - this.$root.dialog({ - type: 'success', - splash: true - }); - }, e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async toggleSuspend() { - if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return; - - this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { - userId: this.user.id - }).then(() => { - this.user.isSuspended = !this.user.isSuspended; - this.$root.dialog({ - type: 'success', - splash: true - }); - }, e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async getConfirmed(text: string): Promise<Boolean> { - const confirm = await this.$root.dialog({ - type: 'warning', - showCancelButton: true, - title: 'confirm', - text, - }); - - return !confirm.canceled; - }, - } -}); -</script> diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue deleted file mode 100644 index 5aa481ed9a17c32d881b899c7a07b618f4cfc007..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/visibility-chooser.vue +++ /dev/null @@ -1,233 +0,0 @@ -<template> -<div class="gqyayizv"> - <div class="backdrop" ref="backdrop" @click="close"></div> - <div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover"> - <div @click="choose('public')" :class="{ active: v == 'public' }"> - <div><fa icon="globe"/></div> - <div> - <span>{{ $t('public') }}</span> - </div> - </div> - <div @click="choose('home')" :class="{ active: v == 'home' }"> - <div><fa icon="home"/></div> - <div> - <span>{{ $t('home') }}</span> - <span>{{ $t('home-desc') }}</span> - </div> - </div> - <div @click="choose('followers')" :class="{ active: v == 'followers' }"> - <div><fa icon="unlock"/></div> - <div> - <span>{{ $t('followers') }}</span> - <span>{{ $t('followers-desc') }}</span> - </div> - </div> - <div @click="choose('specified')" :class="{ active: v == 'specified' }"> - <div><fa icon="envelope"/></div> - <div> - <span>{{ $t('specified') }}</span> - <span>{{ $t('specified-desc') }}</span> - </div> - </div> - <div @click="choose('local-public')" :class="{ active: v == 'local-public' }"> - <div><fa icon="globe"/></div> - <div> - <span>{{ $t('local-public') }}</span> - <span>{{ $t('local-public-desc') }}</span> - </div> - </div> - <div @click="choose('local-home')" :class="{ active: v == 'local-home' }"> - <div><fa icon="home"/></div> - <div> - <span>{{ $t('local-home') }}</span> - </div> - </div> - <div @click="choose('local-followers')" :class="{ active: v == 'local-followers' }"> - <div><fa icon="unlock"/></div> - <div> - <span>{{ $t('local-followers') }}</span> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; - -export default Vue.extend({ - i18n: i18n('common/views/components/visibility-chooser.vue'), - props: { - source: { - required: true - }, - currentVisibility: { - type: String, - required: false - } - }, - data() { - return { - v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility) - } - }, - mounted() { - this.$nextTick(() => { - const popover = this.$refs.popover as any; - - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - let left; - let top; - - if (this.$root.isMobile) { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - const y = rect.top + window.pageYOffset + this.source.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (left + width > window.innerWidth) { - left = window.innerWidth - width; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - - anime({ - targets: this.$refs.backdrop, - opacity: 1, - duration: 100, - easing: 'linear' - }); - - anime({ - targets: this.$refs.popover, - opacity: 1, - scale: [0.5, 1], - duration: 500 - }); - }); - }, - methods: { - choose(visibility) { - if (this.$store.state.settings.rememberNoteVisibility) { - this.$store.commit('device/setVisibility', visibility); - } - this.$emit('chosen', visibility); - this.destroyDom(); - }, - 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.destroyDom() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gqyayizv - position initial - - > .backdrop - position fixed - top 0 - left 0 - z-index 10000 - width 100% - height 100% - background var(--modalBackdrop) - opacity 0 - - > .popover - $bgcolor = var(--popupBg) - position absolute - z-index 10001 - width 240px - padding 8px 0 - background $bgcolor - border-radius 4px - box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) - transform scale(0.5) - opacity 0 - - &:not(.isMobile) - $arrow-size = 10px - - margin-top $arrow-size - transform-origin center -($arrow-size) - - &:before - content "" - display block - position absolute - top -($arrow-size * 2) - left s('calc(50% - %s)', $arrow-size) - border-top solid $arrow-size transparent - border-left solid $arrow-size transparent - border-right solid $arrow-size transparent - border-bottom solid $arrow-size $bgcolor - - > div - display flex - padding 8px 14px - font-size 12px - color var(--popupFg) - cursor pointer - - &:hover - background var(--faceClearButtonHover) - - &:active - background var(--faceClearButtonActive) - - &.active - color var(--primaryForeground) - background var(--primary) - - > * - user-select none - pointer-events none - - > *:first-child - display flex - justify-content center - align-items center - margin-right 10px - width 16px - - > *:last-child - flex 1 1 auto - - > span:first-child - display block - font-weight bold - - > span:last-child:not(:first-child) - opacity 0.6 - -</style> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue deleted file mode 100644 index d812549b1e76fe085405608e8a8cc970a169ea8c..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div class="mk-welcome-timeline"> - <transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div"> - <div v-for="note in notes" :key="note.id"> - <mk-avatar class="avatar" :user="note.user" target="_blank"/> - <div class="body"> - <header> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"> - <mk-user-name :user="note.user"/> - </router-link> - <span class="username">@{{ note.user | acct }}</span> - <div class="info"> - <router-link class="created-at" :to="note | notePage"> - <mk-time :time="note.createdAt"/> - </router-link> - </div> - </header> - <div class="text"> - <mfm v-if="note.text" :text="note.cw != null ? note.cw : note.text" :author="note.user" :custom-emojis="note.emojis"/> - </div> - </div> - </div> - </transition-group> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - max: { - type: Number, - required: false, - default: undefined - } - }, - - data() { - return { - fetching: true, - notes: [], - connection: null - }; - }, - - mounted() { - this.fetch(); - - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - - this.connection.on('note', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - fetch(cb?) { - this.fetching = true; - this.$root.api('notes', { - limit: this.max, - local: true, - reply: false, - renote: false, - file: false, - poll: false - }).then(notes => { - this.notes = notes; - this.fetching = false; - }); - }, - - onNote(note) { - if (note.replyId != null) return; - if (note.renoteId != null) return; - if (note.poll != null) return; - if (note.localOnly) return; - - this.notes.unshift(note); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.ldzpakcixzickvggyixyrhqwjaefknon-enter -.ldzpakcixzickvggyixyrhqwjaefknon-leave-to - opacity 0 - transform translateY(-30px) - -.mk-welcome-timeline - background var(--face) - - > div - > * - transition transform .3s ease, opacity .3s ease - - > div - padding 16px - overflow-wrap break-word - font-size .9em - color var(--noteText) - border-bottom 1px solid var(--faceDivider) - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - position -webkit-sticky - position sticky - top 16px - 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 var(--noteHeaderName) - - > .username - margin 0 .5em 0 0 - color var(--noteHeaderAcct) - - > .info - margin-left auto - font-size 0.9em - - > .created-at - color var(--noteHeaderInfo) - - > .text - text-align left - -</style> diff --git a/src/client/app/common/views/deck/deck.column-core.vue b/src/client/app/common/views/deck/deck.column-core.vue deleted file mode 100644 index 974c58235df9e4957df8d6d751514f1466cdcf7b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.column-core.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XTlColumn from './deck.tl-column.vue'; -import XNotificationsColumn from './deck.notifications-column.vue'; -import XWidgetsColumn from './deck.widgets-column.vue'; -import XMentionsColumn from './deck.mentions-column.vue'; -import XDirectColumn from './deck.direct-column.vue'; - -export default Vue.extend({ - components: { - XTlColumn, - XNotificationsColumn, - XWidgetsColumn, - XMentionsColumn, - XDirectColumn - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: false, - default: false - } - }, - - methods: { - focus() { - this.$children[0].focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.column-template.vue b/src/client/app/common/views/deck/deck.column-template.vue deleted file mode 100644 index 59232851624720ce30966224de6a9f0e9cd9fa35..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.column-template.vue +++ /dev/null @@ -1,45 +0,0 @@ -<template> -<x-column> - <template #header> - <fa v-if="icon" :icon="icon"/>{{ title }} - </template> - - <div> - <component :is="component" @init="init" v-bind="$attrs"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XColumn from './deck.column.vue'; - -export default Vue.extend({ - components: { - XColumn, - }, - - props: { - component: { - required: true - } - }, - - data() { - return { - title: null, - icon: null, - }; - }, - - mounted() { - }, - - methods: { - init(v) { - this.title = v.title; - this.icon = v.icon; - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.column.vue b/src/client/app/common/views/deck/deck.column.vue deleted file mode 100644 index ac69a97df50d2c8ad7bce10e538c32c79950e618..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.column.vue +++ /dev/null @@ -1,444 +0,0 @@ -<template> -<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging, dropready, isMobile: $root.isMobile, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" - @dragover.prevent.stop="onDragover" - @dragleave="onDragleave" - @drop.prevent.stop="onDrop" - v-hotkey="keymap"> - <header :class="{ indicate: count > 0 }" - draggable="true" - @click="goTop" - @dragstart="onDragstart" - @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu"> - <button class="toggleActive" @click="toggleActive" v-if="isStacked"> - <template v-if="active"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - <span class="header"><slot name="header"></slot></span> - <span class="count" v-if="count > 0">({{ count }})</span> - <button v-if="!isTemporaryColumn" class="menu" ref="menu" @click.stop="showMenu"><fa icon="caret-down"/></button> - <button v-else class="close" @click.stop="close"><fa icon="times"/></button> - </header> - <div ref="body" v-show="active"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Menu from '../../../common/views/components/menu.vue'; -import { countIf } from '../../../../../prelude/array'; -import { faArrowUp, faArrowDown } from '@fortawesome/free-solid-svg-icons'; -import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('deck'), - props: { - column: { - type: Object, - required: false, - default: null - }, - isStacked: { - type: Boolean, - required: false, - default: false - }, - name: { - type: String, - required: false - }, - menu: { - type: Array, - required: false, - default: null - }, - naked: { - type: Boolean, - required: false, - default: false - }, - narrow: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - count: 0, - active: true, - dragging: false, - draghover: false, - dropready: false, - faArrowUp, faArrowDown - }; - }, - - computed: { - isTemporaryColumn(): boolean { - return this.column == null; - }, - - keymap(): any { - return { - 'shift+up': () => this.$parent.$emit('parentFocus', 'up'), - 'shift+down': () => this.$parent.$emit('parentFocus', 'down'), - 'shift+left': () => this.$parent.$emit('parentFocus', 'left'), - 'shift+right': () => this.$parent.$emit('parentFocus', 'right'), - }; - } - }, - - inject: { - getColumnVm: { from: 'getColumnVm' } - }, - - watch: { - active(v) { - if (v && this.isScrollTop()) { - this.$emit('top'); - } - }, - dragging(v) { - this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd'); - } - }, - - provide() { - return { - column: this, - isScrollTop: this.isScrollTop, - count: v => this.count = v, - inNakedDeckColumn: !this.naked - }; - }, - - mounted() { - this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true }); - - if (!this.isTemporaryColumn) { - this.$root.$on('deck.column.dragStart', this.onOtherDragStart); - this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); - } - }, - - beforeDestroy() { - this.$refs.body.removeEventListener('scroll', this.onScroll); - - if (!this.isTemporaryColumn) { - this.$root.$off('deck.column.dragStart', this.onOtherDragStart); - this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); - } - }, - - methods: { - onOtherDragStart() { - this.dropready = true; - }, - - onOtherDragEnd() { - this.dropready = false; - }, - - toggleActive() { - if (!this.isStacked) return; - const deck = this.$store.state.device.deckProfile ? this.$store.state.settings.deckProfiles[this.$store.state.device.deckProfile] : this.$store.state.device.deck; - const vms = deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id)); - if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return; - this.active = !this.active; - }, - - isScrollTop() { - return this.active && this.$refs.body.scrollTop == 0; - }, - - onScroll() { - if (this.isScrollTop()) { - this.$emit('top'); - } - - if (this.$store.state.settings.fetchOnScroll) { - const current = this.$refs.body.scrollTop + this.$refs.body.clientHeight; - if (current > this.$refs.body.scrollHeight - 1) this.$emit('bottom'); - } - }, - - getMenu() { - const items = [{ - icon: 'pencil-alt', - text: this.$t('rename'), - action: () => { - this.$root.dialog({ - title: this.$t('rename'), - input: { - default: this.name, - allowEmpty: false - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$store.commit('renameDeckColumn', { id: this.column.id, name }); - }); - } - }, null, { - icon: 'arrow-left', - text: this.$t('swap-left'), - action: () => { - this.$store.commit('swapLeftDeckColumn', this.column.id); - } - }, { - icon: 'arrow-right', - text: this.$t('swap-right'), - action: () => { - this.$store.commit('swapRightDeckColumn', this.column.id); - } - }, this.isStacked ? { - icon: faArrowUp, - text: this.$t('swap-up'), - action: () => { - this.$store.commit('swapUpDeckColumn', this.column.id); - } - } : undefined, this.isStacked ? { - icon: faArrowDown, - text: this.$t('swap-down'), - action: () => { - this.$store.commit('swapDownDeckColumn', this.column.id); - } - } : undefined, null, { - icon: ['far', 'window-restore'], - text: this.$t('stack-left'), - action: () => { - this.$store.commit('stackLeftDeckColumn', this.column.id); - } - }, this.isStacked ? { - icon: faWindowMaximize, - text: this.$t('pop-right'), - action: () => { - this.$store.commit('popRightDeckColumn', this.column.id); - } - } : undefined, null, { - icon: ['far', 'trash-alt'], - text: this.$t('remove'), - action: () => { - this.$store.commit('removeDeckColumn', this.column.id); - } - }]; - - if (this.menu) { - items.unshift(null); - for (const i of this.menu.reverse()) { - items.unshift(i); - } - } - - return items; - }, - - onContextmenu(e) { - if (this.isTemporaryColumn) return; - this.$contextmenu(e, this.getMenu()); - }, - - showMenu() { - this.$root.new(Menu, { - source: this.$refs.menu, - items: this.getMenu() - }); - }, - - close() { - this.$router.push('/'); - }, - - goTop() { - this.$refs.body.scrollTo({ - top: 0, - behavior: 'smooth' - }); - }, - - onDragstart(e) { - // テンãƒãƒ©ãƒªã‚«ãƒ©ãƒ ã¯ãƒ‰ãƒ©ãƒƒã‚°ã•ã›ãªã„ - if (this.isTemporaryColumn) { - e.preventDefault(); - return; - } - - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk-deck-column', this.column.id); - this.dragging = true; - }, - - onDragend(e) { - this.dragging = false; - }, - - onDragover(e) { - // テンãƒãƒ©ãƒªã‚«ãƒ©ãƒ ã«ã¯ãƒ‰ãƒãƒƒãƒ—ã•ã›ãªã„ - if (this.isTemporaryColumn) { - e.dataTransfer.dropEffect = 'none'; - return; - } - - // 自分自身ãŒãƒ‰ãƒ©ãƒƒã‚°ã•ã‚Œã¦ã„ã‚‹å ´åˆ - if (this.dragging) { - // 自分自身ã«ã¯ãƒ‰ãƒãƒƒãƒ—ã•ã›ãªã„ - e.dataTransfer.dropEffect = 'none'; - return; - } - - const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; - - e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; - - if (!this.dragging && isDeckColumn) this.draghover = true; - }, - - onDragleave() { - this.draghover = false; - }, - - onDrop(e) { - this.draghover = false; - this.$root.$emit('deck.column.dragEnd'); - - const id = e.dataTransfer.getData('mk-deck-column'); - if (id != null && id != '') { - this.$store.commit('swapDeckColumn', { - a: this.column.id, - b: id - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs - $header-height = 42px - - height 100% - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - &.draghover - box-shadow 0 0 0 2px var(--primaryAlpha08) - - &:after - content "" - display block - position absolute - z-index 1000 - top 0 - left 0 - width 100% - height 100% - background var(--primaryAlpha02) - - &.dragging - box-shadow 0 0 0 2px var(--primaryAlpha04) - - &.dropready - * - pointer-events none - - &:not(.active) - flex-basis $header-height - min-height $header-height - - &:not(.isStacked).narrow - width 285px - min-width 285px - flex-grow 0 !important - - &.naked - background var(--deckAcrylicColumnBg) - - > header - background transparent - box-shadow none - - > button - color var(--text) - - &.isMobile - > header - box-shadow none - - > header - display flex - z-index 2 - line-height $header-height - padding 0 16px - font-size 14px - color var(--faceHeaderText) - background var(--faceHeader) - box-shadow 0 var(--lineWidth) rgba(#000, 0.15) - cursor pointer - - &, * - user-select none - - *:not(button) - pointer-events none - - &.indicate - box-shadow 0 3px 0 0 var(--primary) - - > .header - display inline-block - align-items center - overflow hidden - text-overflow ellipsis - white-space nowrap - - [data-icon] - margin-right 8px - - > .count - margin-left 4px - opacity 0.5 - - > span:only-of-type - width 100% - - > .toggleActive - > .menu - > .close - padding 0 - width $header-height - line-height $header-height - font-size 16px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - > .toggleActive - margin-left -16px - - > .menu - > .close - margin-left auto - margin-right -16px - - > div - height "calc(100% - %s)" % $header-height - overflow auto - overflow-x hidden - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/common/views/deck/deck.direct-column.vue b/src/client/app/common/views/deck/deck.direct-column.vue deleted file mode 100644 index 66d34520afbe4a4c770c4af7d72aa1295a8a5aee..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.direct-column.vue +++ /dev/null @@ -1,79 +0,0 @@ -<template> -<x-column :name="name" :column="column" :is-stacked="isStacked"> - <template #header><fa :icon="['far', 'envelope']"/>{{ name }}</template> - - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XNotes - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - params: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - visibility: 'specified' - } - } - }; - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.direct'); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - // Prepend a note - if (note.visibility == 'specified') { - (this.$refs.timeline as any).prepend(note); - } - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.hashtag-column.vue b/src/client/app/common/views/deck/deck.hashtag-column.vue deleted file mode 100644 index 0d719c219987dee2cf7e2461305e18cc899de9df..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.hashtag-column.vue +++ /dev/null @@ -1,122 +0,0 @@ -<template> -<x-column> - <template #header> - <fa icon="hashtag"/><span>{{ tag }}</span> - </template> - - <div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb"> - <div ref="chart" class="chart"></div> - <x-hashtag-tl :tag-tl="tagTl" class="tl" :key="JSON.stringify(tagTl)"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XColumn from './deck.column.vue'; -import XHashtagTl from './deck.hashtag-tl.vue'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - components: { - XColumn, - XHashtagTl - }, - - computed: { - tag(): string { - return this.$route.params.tag; - }, - - tagTl(): any { - return { - query: [[this.tag]] - }; - } - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('charts/hashtag', { - tag: this.tag, - span: 'hour', - limit: 24 - }).then(stats => { - const local = []; - const remote = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - const h = now.getHours(); - - for (let i = 0; i < 24; i++) { - const x = new Date(y, m, d, h - i); - local.push([x, stats.local.count[i]]); - remote.push([x, stats.remote.count[i]]); - } - - const chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'area', - height: 70, - sparkline: { - enabled: true - }, - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - padding: { - top: 16, - right: 16, - bottom: 16, - left: 16 - } - }, - stroke: { - curve: 'straight', - width: 2 - }, - series: [{ - name: 'Local', - data: local - }, { - name: 'Remote', - data: remote - }], - xaxis: { - type: 'datetime', - } - }); - - chart.render(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.xroyrflcmhhtmlwmyiwpfqiirqokfueb - background var(--deckColumnBg) - - > .chart - margin-bottom 16px - background var(--face) - - > .tl - background var(--face) - -</style> diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue deleted file mode 100644 index 94d2efc430003c2020eb8a32e4946b75e77a8315..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.hashtag-tl.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - components: { - XNotes - }, - - props: { - tagTl: { - type: Object, - required: true - }, - mediaOnly: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/search-by-tag', - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl.query - }) - } - }; - }, - - watch: { - mediaOnly() { - this.$refs.timeline.reload(); - } - }, - - mounted() { - if (this.connection) this.connection.close(); - this.connection = this.$root.stream.connectToChannel('hashtag', { - q: this.tagTl.query - }); - this.connection.on('note', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - if (this.mediaOnly && note.files.length == 0) return; - (this.$refs.timeline as any).prepend(note); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.list-tl.vue b/src/client/app/common/views/deck/deck.list-tl.vue deleted file mode 100644 index 26d6ea9d5834b18c52de5d7d598bd8913910f321..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.list-tl.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - components: { - XNotes - }, - - props: { - list: { - type: Object, - required: true - }, - mediaOnly: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - watch: { - mediaOnly() { - this.$refs.timeline.reload(); - } - }, - - mounted() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - if (this.mediaOnly && note.files.length == 0) return; - (this.$refs.timeline as any).prepend(note); - }, - - onUserAdded() { - this.$refs.timeline.reload(); - }, - - onUserRemoved() { - this.$refs.timeline.reload(); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.mentions-column.vue b/src/client/app/common/views/deck/deck.mentions-column.vue deleted file mode 100644 index 12d7b2a16b18107607a71adcc4d57f2276e94a79..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.mentions-column.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<x-column :name="name" :column="column" :is-stacked="isStacked"> - <template #header><fa icon="at"/>{{ name }}</template> - - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XNotes - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.mentions'); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', this.onNote); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - (this.$refs.timeline as any).prepend(note); - }, - - focus() { - this.$refs.timeline.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.note-column.vue b/src/client/app/common/views/deck/deck.note-column.vue deleted file mode 100644 index bcc887e2fd93d3b1a3978e7c3b93dce95838b4e4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.note-column.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<x-column> - <template #header> - <fa :icon="['far', 'comment-alt']"/><mk-user-name :user="note.user" v-if="note"/> - </template> - - <div class="rvtscbadixhhbsczoorqoaygovdeecsx" v-if="note"> - <div class="is-remote" v-if="note.user.host != null"> - <details> - <summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-post') }}</summary> - <a :href="note.url || note.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a> - </details> - </div> - <mk-note :note="note" :detail="true" :key="note.id"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XColumn, - }, - - data() { - return { - note: null, - fetching: true - }; - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.rvtscbadixhhbsczoorqoaygovdeecsx - > .is-remote - padding 8px 16px - font-size 12px - - &.is-remote - color var(--remoteInfoFg) - background var(--remoteInfoBg) - - > a - font-weight bold - -</style> diff --git a/src/client/app/common/views/deck/deck.notes.vue b/src/client/app/common/views/deck/deck.notes.vue deleted file mode 100644 index 5081d1f99873da2fb94297817652455b6cd852b1..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.notes.vue +++ /dev/null @@ -1,168 +0,0 @@ -<template> -<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition notes" ref="notes" tag="div"> - <template v-for="(note, i) in _notes"> - <mk-note - :note="note" - :key="note.id" - :compact="true" - /> - <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - inject: ['column', 'isScrollTop', 'count'], - - mixins: [ - paging({ - limit: 20, - - onQueueChanged: (self, q) => { - self.count(q.length); - }, - - onPrepend: (self, note, silent) => { - // å¼¾ã - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブãŒéžè¡¨ç¤ºã¾ãŸã¯ã‚¹ã‚¯ãƒãƒ¼ãƒ«ä½ç½®ãŒæœ€ä¸Šéƒ¨ã§ã¯ãªã„ãªã‚‰ã‚¿ã‚¤ãƒˆãƒ«ã§é€šçŸ¥ - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - } - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - }, - - _notes(): any[] { - return (this.notes as any).map(note => { - const date = new Date(note.createdAt).getDate(); - const month = new Date(note.createdAt).getMonth() + 1; - note._date = date; - note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return note; - }); - } - }, - - created() { - this.column.$on('top', this.onTop); - this.column.$on('bottom', this.onBottom); - this.init(); - }, - - beforeDestroy() { - this.column.$off('top', this.onTop); - this.column.$off('bottom', this.onBottom); - }, - - methods: { - focus() { - (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.eamppglmnmimdhrlzhplwpvyeaqmmhxu - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .empty - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 16px - opacity 0.3 - - > .notes - > .date - display block - margin 0 - line-height 28px - font-size 12px - text-align center - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > footer - > button - display block - margin 0 - padding 16px - width 100% - text-align center - color #ccc - background var(--face) - border-top solid var(--lineWidth) var(--faceDivider) - border-bottom-left-radius 6px - border-bottom-right-radius 6px - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - -</style> diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue deleted file mode 100644 index da122ff4db39d4351eb071918fa0f7580c077e11..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.notification.vue +++ /dev/null @@ -1,186 +0,0 @@ -<template> -<div class="dsfykdcjpuwfvpefwufddclpjhzktmpw"> - <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <mk-reaction-icon :reaction="notification.reaction" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification renote" v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="retweet" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-plus" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-clock" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification pollVote" v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="chart-pie" class="icon"/> - <router-link :to="notification.user | userPage" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.dsfykdcjpuwfvpefwufddclpjhzktmpw - > .notification - padding 16px - font-size 12px - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > div - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - > .icon - margin-right 4px - - > .name - overflow hidden - text-overflow ellipsis - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - > .note-preview - color var(--noteText) - - > .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - > div > header - align-items normal - - &.renote - > div > header [data-icon] - color #77B255 - - &.follow - > div > header [data-icon] - color #53c7ce - - &.receiveFollowRequest - > div > header [data-icon] - color #888 - -</style> diff --git a/src/client/app/common/views/deck/deck.notifications-column.vue b/src/client/app/common/views/deck/deck.notifications-column.vue deleted file mode 100644 index b4361b054ae33ce7d4519dea46533fcbedd4fdcc..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.notifications-column.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> -<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> - <template #header><fa :icon="['far', 'bell']"/>{{ name }}</template> - - <x-notifications :type="column.notificationType === 'all' ? null : column.notificationType"/> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XNotifications from './deck.notifications.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XColumn, - XNotifications - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - menu: null, - } - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.notifications'); - } - }, - - created() { - if (this.column.notificationType == null) { - this.column.notificationType = 'all'; - this.$store.commit('updateDeckColumn', this.column); - } - - this.menu = [{ - icon: 'cog', - text: this.$t('@.notification-type'), - action: () => { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.column.notificationType, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.column.notificationType = type; - this.$store.commit('updateDeckColumn', this.column); - }); - } - }]; - }, -}); -</script> diff --git a/src/client/app/common/views/deck/deck.notifications.vue b/src/client/app/common/views/deck/deck.notifications.vue deleted file mode 100644 index aed2af64e987b6e19c8cdccd9c07c4314009dc08..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.notifications.vue +++ /dev/null @@ -1,177 +0,0 @@ -<template> -<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> - <template v-for="(notification, i) in _notifications"> - <x-notification class="notification" :notification="notification" :key="notification.id"/> - <p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? this.$t('@.loading') : this.$t('@.load-more') }} - </button> - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XNotification from './deck.notification.vue'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XNotification - }, - - inject: ['column', 'isScrollTop', 'count'], - - mixins: [ - paging({ - onQueueChanged: (self, q) => { - self.count(q.length); - }, - }), - ], - - props: { - type: { - type: String, - required: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'i/notifications', - limit: 20, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - - this.column.$on('top', this.onTop); - this.column.$on('bottom', this.onBottom); - }, - - beforeDestroy() { - this.connection.dispose(); - - this.column.$off('top', this.onTop); - this.column.$off('bottom', this.onBottom); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーãŒç”»é¢ã‚’見ã¦ãªã„ã¨æ€ã‚れるã¨ã(ブラウザやタブãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã˜ã‚ƒãªã„ãªã©)ã¯é€ä¿¡ã—ãªã„ - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.oxynyeqmfvracxnglgulyqfgqxnxmehl - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .placeholder - padding 16px - opacity 0.3 - - > .notifications - - > .notification:not(:last-child) - border-bottom solid var(--lineWidth) var(--faceDivider) - - > .date - display block - margin 0 - line-height 28px - text-align center - font-size 12px - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - &:hover - background rgba(#000, 0.025) - - &:active - background rgba(#000, 0.05) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - -</style> diff --git a/src/client/app/common/views/deck/deck.page-column.vue b/src/client/app/common/views/deck/deck.page-column.vue deleted file mode 100644 index 0ef391a51db9927f25756ef64d6eda38754825fb..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.page-column.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<x-column> - <template #header> - <fa :icon="faStickyNote"/>{{ page ? page.name : '' }} - </template> - - <div v-if="page"> - <x-page :page="page" :key="page.id"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XPage from '../../../common/views/components/page/page.vue'; - -export default Vue.extend({ - i18n: i18n(), - - components: { - XColumn, - XPage - }, - - props: { - pageName: { - type: String, - required: true - }, - username: { - type: String, - required: true - }, - }, - - data() { - return { - page: null, - faStickyNote - }; - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.$root.api('pages/show', { - name: this.pageName, - username: this.username, - }).then(page => { - this.page = page; - this.$emit('init', { - title: this.page.title, - icon: faStickyNote - }); - }); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.search-column.vue b/src/client/app/common/views/deck/deck.search-column.vue deleted file mode 100644 index a2d1142fbe1c9bc470246d4bc19204088f33e504..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.search-column.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<x-column> - <template #header> - <fa icon="search"/><span>{{ q }}</span> - </template> - - <div> - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XColumn from './deck.column.vue'; -import XNotes from './deck.notes.vue'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - components: { - XColumn, - XNotes - }, - - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - - computed: { - q(): string { - return this.$route.query.q; - } - }, - - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, -}); -</script> diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue deleted file mode 100644 index cad140ed5f5f90e8d561b392ca9c03f9419bd84b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.tl-column.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<x-column :menu="menu" :name="name" :column="column" :is-stacked="isStacked"> - <template #header> - <fa v-if="column.type == 'home'" icon="home"/> - <fa v-if="column.type == 'local'" :icon="['far', 'comments']"/> - <fa v-if="column.type == 'hybrid'" icon="share-alt"/> - <fa v-if="column.type == 'global'" icon="globe"/> - <fa v-if="column.type == 'list'" icon="list"/> - <fa v-if="column.type == 'hashtag'" icon="hashtag"/> - <span>{{ name }}</span> - </template> - - <div class="editor" style="padding:12px" v-if="edit"> - <ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">{{ $t('is-media-only') }}</ui-switch> - </div> - - <x-list-tl v-if="column.type == 'list'" - :list="column.list" - :media-only="column.isMediaOnly" - ref="tl" - /> - <x-hashtag-tl v-else-if="column.type == 'hashtag'" - :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" - :media-only="column.isMediaOnly" - ref="tl" - /> - <x-tl v-else - :src="column.type" - :media-only="column.isMediaOnly" - ref="tl" - /> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import XTl from './deck.tl.vue'; -import XListTl from './deck.list-tl.vue'; -import XHashtagTl from './deck.hashtag-tl.vue'; - -export default Vue.extend({ - i18n: i18n('deck/deck.tl-column.vue'), - components: { - XColumn, - XTl, - XListTl, - XHashtagTl - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - edit: false, - menu: [{ - icon: 'cog', - text: this.$t('edit'), - action: () => { - this.edit = !this.edit; - } - }] - } - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - - switch (this.column.type) { - case 'home': return this.$t('@deck.home'); - case 'local': return this.$t('@deck.local'); - case 'hybrid': return this.$t('@deck.hybrid'); - case 'global': return this.$t('@deck.global'); - case 'list': return this.column.list.name; - case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; - } - } - }, - - methods: { - onChangeSettings(v) { - this.$store.commit('updateDeckColumn', this.column); - }, - - focus() { - this.$refs.tl.focus(); - } - } -}); -</script> diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue deleted file mode 100644 index e6c716070ad144cda500ad01ed6cf6bf19d987eb..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.tl.vue +++ /dev/null @@ -1,135 +0,0 @@ -<template> -<div class="iwaalbte" v-if="disabled"> - <p> - <fa :icon="faMinusCircle"/> - {{ $t('disabled-timeline.title') }} - </p> - <p class="desc">{{ $t('disabled-timeline.description') }}</p> -</div> -<x-notes v-else ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XNotes from './deck.notes.vue'; -import { faMinusCircle } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('deck'), - - components: { - XNotes - }, - - props: { - src: { - type: String, - required: false, - default: 'home' - }, - mediaOnly: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - disabled: false, - faMinusCircle, - pagination: null - }; - }, - - computed: { - stream(): any { - switch (this.src) { - case 'home': return this.$root.stream.useSharedConnection('homeTimeline'); - case 'local': return this.$root.stream.useSharedConnection('localTimeline'); - case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline'); - case 'global': return this.$root.stream.useSharedConnection('globalTimeline'); - } - }, - - endpoint(): string { - switch (this.src) { - case 'home': return 'notes/timeline'; - case 'local': return 'notes/local-timeline'; - case 'hybrid': return 'notes/hybrid-timeline'; - case 'global': return 'notes/global-timeline'; - } - }, - }, - - watch: { - mediaOnly() { - (this.$refs.timeline as any).reload(); - } - }, - - created() { - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.mediaOnly, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - }; - }, - - mounted() { - this.connection = this.stream; - - this.connection.on('note', this.onNote); - if (this.src == 'home') { - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - } - - this.$root.getMeta().then(meta => { - this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( - meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) || - meta.disableGlobalTimeline && ['global'].includes(this.src)); - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNote(note) { - if (this.mediaOnly && note.files.length == 0) return; - (this.$refs.timeline as any).prepend(note); - }, - - onChangeFollowing() { - (this.$refs.timeline as any).reload(); - }, - - focus() { - (this.$refs.timeline as any).focus(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.iwaalbte - color var(--text) - text-align center - - > p - margin 16px - - &.desc - font-size 14px - -</style> diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue deleted file mode 100644 index 9fb50a6672ddd9c2c016f775fa35690740c06af5..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.user-column.home.vue +++ /dev/null @@ -1,229 +0,0 @@ -<template> -<div> - <ui-container v-if="user.pinnedPage" :body-togglable="true"> - <template #header><fa icon="thumbtack"/> {{ $t('pinned-page') }}</template> - <div> - <x-page :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - </div> - </ui-container> - <ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true"> - <template #header><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</template> - <div> - <mk-note v-for="n in user.pinnedNotes" :key="n.id" :note="n"/> - </div> - </ui-container> - <ui-container v-if="images.length > 0" :body-togglable="true" - :expanded="$store.state.device.expandUsersPhotos" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })"> - <template #header><fa :icon="['far', 'images']"/> {{ $t('images') }}</template> - <div class="sainvnaq"> - <router-link v-for="image in images" - :style="`background-image: url(${image.thumbnailUrl})`" - :key="`${image.id}:${image._note.id}`" - :to="image._note | notePage" - :title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`" - ></router-link> - </div> - </ui-container> - <ui-container :body-togglable="true" - :expanded="$store.state.device.expandUsersActivity" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })"> - <template #header><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</template> - <div> - <div ref="chart"></div> - </div> - </ui-container> - <ui-container> - <template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template> - <div> - <x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')" :key="user.id"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XNotes from './deck.notes.vue'; -import { concat } from '../../../../../prelude/array'; -import ApexCharts from 'apexcharts'; - -export default Vue.extend({ - i18n: i18n('deck/deck.user-column.vue'), - - components: { - XNotes, - XPage: () => import('../../../common/views/components/page/page.vue').then(m => m.default), - }, - - props: { - user: { - type: Object, - required: true - } - }, - - data() { - return { - withFiles: false, - images: [], - chart: null as ApexCharts - }; - }, - - computed: { - pagination() { - return { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - withFiles: this.withFiles, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - } - }, - - watch: { - user() { - this.fetch(); - } - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: !this.$store.state.device.alwaysShowNsfw, - limit: 9, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - file._note = note; - } - } - const files = concat(notes.map((n: any): any[] => n.files)); - this.images = files.filter(f => image.includes(f.type)).slice(0, 9); - }); - - this.$root.api('charts/user/notes', { - userId: this.user.id, - span: 'day', - limit: 21 - }).then(stats => { - const normal = []; - const reply = []; - const renote = []; - - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - - for (let i = 0; i < 21; i++) { - const x = new Date(y, m, d - i); - normal.push([ - x, - stats.diffs.normal[i] - ]); - reply.push([ - x, - stats.diffs.reply[i] - ]); - renote.push([ - x, - stats.diffs.renote[i] - ]); - } - - if (this.chart) this.chart.destroy(); - - this.chart = new ApexCharts(this.$refs.chart, { - chart: { - type: 'bar', - stacked: true, - height: 100, - sparkline: { - enabled: true - }, - }, - plotOptions: { - bar: { - columnWidth: '80%' - } - }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - padding: { - top: 16, - right: 16, - bottom: 16, - left: 16 - } - }, - tooltip: { - shared: true, - intersect: false - }, - series: [{ - name: 'Normal', - data: normal - }, { - name: 'Reply', - data: reply - }, { - name: 'Renote', - data: renote - }], - xaxis: { - type: 'datetime', - crosshairs: { - width: 1, - opacity: 1 - } - } - }); - - this.chart.render(); - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.sainvnaq - display grid - grid-template-columns 1fr 1fr 1fr - gap 8px - padding 16px - - > * - height 70px - background-position center center - background-size cover - background-clip content-box - border-radius 4px - -</style> diff --git a/src/client/app/common/views/deck/deck.user-column.vue b/src/client/app/common/views/deck/deck.user-column.vue deleted file mode 100644 index bc8cbc3154b4e4dc5e3e86e665ef143bb840e484..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.user-column.vue +++ /dev/null @@ -1,266 +0,0 @@ -<template> -<x-column> - <template #header> - <fa icon="user"/><mk-user-name :user="user" v-if="user" :key="user.id"/> - </template> - - <div class="zubukjlciycdsyynicqrnlsmdwmymzqu" v-if="user"> - <div class="is-remote" v-if="user.host != null"> - <details> - <summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary> - <a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a> - </details> - </div> - <header :style="bannerStyle"> - <div> - <button class="menu" @click="menu" ref="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow" mini/> - <mk-avatar class="avatar" :user="user" :disable-preview="true" :key="user.id"/> - <router-link class="name" :to="user | userPage()"> - <mk-user-name :user="user" :key="user.id" :nowrap="false"/> - </router-link> - <span class="acct">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span> - <span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> - </div> - </header> - <div class="info"> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="counts"> - <div> - <router-link :to="user | userPage()"> - <b>{{ user.notesCount | number }}</b> - <span>{{ $t('posts') }}</span> - </router-link> - </div> - <div> - <router-link :to="user | userPage('following')"> - <b>{{ user.followingCount | number }}</b> - <span>{{ $t('following') }}</span> - </router-link> - </div> - <div> - <router-link :to="user | userPage('followers')"> - <b>{{ user.followersCount | number }}</b> - <span>{{ $t('followers') }}</span> - </router-link> - </div> - </div> - </div> - <router-view :user="user"></router-view> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; -import XColumn from './deck.column.vue'; -import XUserMenu from '../../../common/views/components/user-menu.vue'; - -export default Vue.extend({ - i18n: i18n('deck/deck.user-column.vue'), - components: { - XColumn, - }, - - data() { - return { - user: null, - fetching: true, - }; - }, - - computed: { - bannerStyle(): any { - if (this.user == null) return {}; - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - }, - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.fetching = true; - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - }); - }, - - menu() { - const w = this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - this.$once('hook:beforeDestroy', () => { - w.destroyDom(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.zubukjlciycdsyynicqrnlsmdwmymzqu - background var(--deckColumnBg) - - > .is-remote - padding 8px 16px - font-size 12px - - &.is-remote - color var(--remoteInfoFg) - background var(--remoteInfoBg) - - > a - font-weight bold - - > header - overflow hidden - background-size cover - background-position center - - > div - padding 32px - background rgba(#000, 0.5) - color #fff - text-align center - - > .menu - position absolute - top 8px - left 8px - padding 8px - font-size 16px - text-shadow 0 0 8px #000 - - > .follow - position absolute - top 16px - right 16px - - > .avatar - display block - width 64px - height 64px - margin 0 auto - - > .name - display block - margin-top 8px - font-weight bold - text-shadow 0 0 8px #000 - color #fff - - > .acct - display block - font-size 14px - opacity 0.7 - text-shadow 0 0 8px #000 - - > .locked - opacity 0.8 - - > .followed - display inline-block - font-size 12px - background rgba(0, 0, 0, 0.5) - opacity 0.7 - margin-top: 2px - padding 4px - border-radius 4px - - > .info - padding 16px - font-size 12px - color var(--text) - text-align center - background var(--face) - - &:before - content "" - display blcok - position absolute - top -32px - left 0 - right 0 - width 0 - margin 0 auto - border-top solid 16px transparent - border-left solid 16px transparent - border-right solid 16px transparent - border-bottom solid 16px var(--face) - - > .fields - margin-top 8px - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - - > .counts - display grid - grid-template-columns 2fr 2fr 2fr - margin-top 8px - border-top solid var(--lineWidth) var(--faceDivider) - - > div - padding 8px 8px 0 8px - text-align center - - > a - color var(--text) - - > b - display block - font-size 110% - - > span - display block - font-size 80% - opacity 0.7 - -</style> diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue deleted file mode 100644 index a3a26302e9486b8975b23d1370959890136af3b8..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.vue +++ /dev/null @@ -1,394 +0,0 @@ -<template> -<mk-ui :class="$style.root"> - <div class="qlvquzbjribqcaozciifydkngcwtyzje" ref="body" :style="style" :class="`${$store.state.device.deckColumnAlign} ${$store.state.device.deckColumnWidth}`" v-hotkey.global="keymap"> - <template v-for="ids in layout"> - <div v-if="ids.length > 1" class="folder"> - <template v-for="id, i in ids"> - <x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true" @parentFocus="moveFocus(id, $event)"/> - </template> - </div> - <x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])" @parentFocus="moveFocus(ids[0], $event)"/> - </template> - <router-view></router-view> - <button ref="add" @click="add" :title="$t('@deck.add-column')"><fa icon="plus"/></button> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumnCore from './deck.column-core.vue'; -import Menu from '../../../common/views/components/menu.vue'; - -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('deck'), - - components: { - XColumnCore - }, - - computed: { - deck() { - return this.$store.getters.deck; - }, - - columns(): any[] { - if (this.deck == null) return []; - return this.deck.columns; - }, - - layout(): any[] { - if (this.deck == null) return []; - if (this.deck.layout == null) return this.deck.columns.map(c => [c.id]); - return this.deck.layout; - }, - - style(): any { - return { - height: `calc(100vh - ${this.$store.state.uiHeaderHeight}px)` - }; - }, - - keymap(): any { - return { - 't': this.focus - }; - } - }, - - watch: { - $route() { - if (this.$route.name == 'index') return; - this.$nextTick(() => { - this.$refs.body.scrollTo({ - left: this.$refs.body.scrollWidth - this.$refs.body.clientWidth, - behavior: 'smooth' - }); - }); - } - }, - - provide() { - return { - inDeck: true, - getColumnVm: this.getColumnVm, - narrow: true - }; - }, - - created() { - if (this.deck == null) { - const deck = { - columns: [/*{ - type: 'widgets', - widgets: [] - }, */{ - id: uuid(), - type: 'home', - name: null, - }, { - id: uuid(), - type: 'notifications', - name: null, - }, { - id: uuid(), - type: 'local', - name: null, - }, { - id: uuid(), - type: 'global', - name: null, - }] - }; - - deck.layout = deck.columns.map(c => [c.id]); - - this.$store.commit('setDeck', deck); - } - }, - - mounted() { - document.title = this.$root.instanceName; - document.documentElement.style.overflow = 'hidden'; - }, - - beforeDestroy() { - document.documentElement.style.overflow = 'auto'; - }, - - methods: { - getColumnVm(id) { - return this.$refs[id][0]; - }, - - add() { - this.$root.new(Menu, { - source: this.$refs.add, - items: [{ - icon: 'home', - text: this.$t('@deck.home'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'home' - }); - } - }, { - icon: ['far', 'comments'], - text: this.$t('@deck.local'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'local' - }); - } - }, { - icon: 'share-alt', - text: this.$t('@deck.hybrid'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'hybrid' - }); - } - }, { - icon: 'globe', - text: this.$t('@deck.global'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'global' - }); - } - }, { - icon: 'at', - text: this.$t('@deck.mentions'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'mentions' - }); - } - }, { - icon: ['far', 'envelope'], - text: this.$t('@deck.direct'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'direct' - }); - } - }, { - icon: 'list', - text: this.$t('@deck.list'), - action: async () => { - const lists = await this.$root.api('users/lists/list'); - const { canceled, result: listId } = await this.$root.dialog({ - type: null, - title: this.$t('@deck.select-list'), - select: { - items: lists.map(list => ({ - value: list.id, text: list.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'list', - list: lists.find(l => l.id === listId) - }); - } - }, { - icon: 'hashtag', - text: this.$t('@deck.hashtag'), - action: () => { - this.$root.dialog({ - title: this.$t('enter-hashtag-tl-title'), - input: true - }).then(({ canceled, result: title }) => { - if (canceled) return; - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'hashtag', - tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id - }); - }); - } - }, { - icon: ['far', 'bell'], - text: this.$t('@deck.notifications'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'notifications' - }); - } - }, { - icon: 'calculator', - text: this.$t('@deck.widgets'), - action: () => { - this.$store.commit('addDeckColumn', { - id: uuid(), - type: 'widgets', - widgets: [] - }); - } - }] - }); - }, - - focus() { - // Flatten array of arrays - const ids = [].concat.apply([], this.layout); - const firstTl = ids.find(id => this.isTlColumn(id)); - - if (firstTl) { - this.$refs[firstTl][0].focus(); - } - }, - - moveFocus(id, direction) { - let targetColumn; - - if (direction == 'right') { - const currentColumnIndex = this.layout.findIndex(ids => ids.includes(id)); - this.layout.some((ids, i) => { - if (i <= currentColumnIndex) return false; - const tl = ids.find(id => this.isTlColumn(id)); - if (tl) { - targetColumn = tl; - return true; - } - }); - } else if (direction == 'left') { - const currentColumnIndex = [...this.layout].reverse().findIndex(ids => ids.includes(id)); - [...this.layout].reverse().some((ids, i) => { - if (i <= currentColumnIndex) return false; - const tl = ids.find(id => this.isTlColumn(id)); - if (tl) { - targetColumn = tl; - return true; - } - }); - } else if (direction == 'down') { - const currentColumn = this.layout.find(ids => ids.includes(id)); - const currentIndex = currentColumn.indexOf(id); - currentColumn.some((_id, i) => { - if (i <= currentIndex) return false; - if (this.isTlColumn(_id)) { - targetColumn = _id; - return true; - } - }); - } else if (direction == 'up') { - const currentColumn = [...this.layout.find(ids => ids.includes(id))].reverse(); - const currentIndex = currentColumn.indexOf(id); - currentColumn.some((_id, i) => { - if (i <= currentIndex) return false; - if (this.isTlColumn(_id)) { - targetColumn = _id; - return true; - } - }); - } - - if (targetColumn) { - this.$refs[targetColumn][0].focus(); - } - }, - - isTlColumn(id) { - const column = this.columns.find(c => c.id === id); - return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type); - } - } -}); -</script> - -<style lang="stylus" module> -.root - height 100vh -</style> - -<style lang="stylus" scoped> -.qlvquzbjribqcaozciifydkngcwtyzje - display flex - flex 1 - padding 16px 0 16px 16px - overflow auto - overflow-y hidden - -webkit-overflow-scrolling touch - - @media (max-width 500px) - padding 8px 0 8px 8px - - > div - margin-right 8px - width 330px - min-width 330px - - &:last-of-type - margin-right 0 - - &.folder - display flex - flex-direction column - - > *:not(:last-child) - margin-bottom 8px - - &.narrow - > div - width 303px - min-width 303px - - &.narrower - > div - width 316.5px - min-width 316.5px - - &.wider - > div - width 343.5px - min-width 343.5px - - &.wide - > div - width 357px - min-width 357px - - &.center - > * - &:first-child - margin-left auto - - &:last-child - margin-right auto - - &.:not(.flexible) - > * - flex-grow 0 - flex-shrink 0 - - &.flexible - > * - flex-grow 1 - flex-shrink 0 - - > button - padding 0 16px - color var(--faceTextButton) - flex-grow 0 !important - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - -</style> diff --git a/src/client/app/common/views/deck/deck.widgets-column.vue b/src/client/app/common/views/deck/deck.widgets-column.vue deleted file mode 100644 index d9a77479096c6174fbbaf8c964e12ee91e000b71..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/deck/deck.widgets-column.vue +++ /dev/null @@ -1,173 +0,0 @@ -<template> -<x-column :menu="menu" :naked="true" :narrow="true" :name="name" :column="column" :is-stacked="isStacked" class="wtdtxvecapixsepjtcupubtsmometobz"> - <template #header><fa icon="calculator"/>{{ name }}</template> - - <div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq"> - <template v-if="edit"> - <header> - <select v-model="widgetAdderSelected" @change="addWidget"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="trends">{{ $t('@.widgets.trends') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> - <option value="notifications">{{ $t('@.widgets.notifications') }}</option> - <option value="users">{{ $t('@.widgets.users') }}</option> - <option value="polls">{{ $t('@.widgets.polls') }}</option> - <option value="post-form">{{ $t('@.widgets.post-form') }}</option> - <option value="messaging">{{ $t('@.messaging') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - </header> - <x-draggable - :list="column.widgets" - animation="150" - @sort="onWidgetSort" - > - <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)"> - <button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck" :column="column"/> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck" :column="column"/> - </template> - </div> -</x-column> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XColumn from './deck.column.vue'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XColumn, - XDraggable - }, - - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - edit: false, - menu: null, - widgetAdderSelected: null - } - }, - - computed: { - name(): string { - if (this.column.name) return this.column.name; - return this.$t('@deck.widgets'); - } - }, - - created() { - this.menu = [{ - icon: 'cog', - text: this.$t('edit'), - action: () => { - this.edit = !this.edit; - } - }]; - }, - - methods: { - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveWidgets(); - }, - - addWidget() { - this.$store.commit('addDeckWidget', { - id: this.column.id, - widget: { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - } - }); - - this.widgetAdderSelected = null; - }, - - removeWidget(widget) { - this.$store.commit('removeDeckWidget', { - id: this.column.id, - widget - }); - }, - - saveWidgets() { - this.$store.commit('updateDeckColumn', this.column); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wtdtxvecapixsepjtcupubtsmometobz - .gqpwvtwtprsbmnssnbicggtwqhmylhnq - > header - padding 16px - - > * - width 100% - padding 4px - - .widget, .customize-container - margin 8px - - &:first-of-type - margin-top 0 - - .customize-container - cursor move - - > *:not(.remove) - pointer-events none - - > .remove - position absolute - z-index 1 - top 8px - right 8px - width 32px - height 32px - color #fff - background rgba(#000, 0.7) - border-radius 4px - -</style> - diff --git a/src/client/app/common/views/directives/index.ts b/src/client/app/common/views/directives/index.ts deleted file mode 100644 index 1bb4fd6d4dfb2799ab11854322d1cd0c4f728385..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/directives/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Vue from 'vue'; - -import autocomplete from './autocomplete'; -import particle from './particle'; - -Vue.directive('autocomplete', autocomplete); -Vue.directive('particle', particle); diff --git a/src/client/app/common/views/directives/particle.ts b/src/client/app/common/views/directives/particle.ts deleted file mode 100644 index 5f8413117f4b94d2d4a7f3ffc10b0b5964697eeb..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/directives/particle.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Particle from '../components/particle.vue'; - -export default { - bind(el, binding, vn) { - if (vn.context.$store.state.device.reduceMotion) return; - - el.addEventListener('click', () => { - if (binding.value === false) return; - - const rect = el.getBoundingClientRect(); - - const x = rect.left + (el.clientWidth / 2); - const y = rect.top + (el.clientHeight / 2); - - const particle = new Particle({ - parent: vn.context, - propsData: { - x, - y - } - }).$mount(); - - document.body.appendChild(particle.$el); - }); - } -}; diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts deleted file mode 100644 index 3dccbfc923be321b9c72ecb5e501f1840e1866fd..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/filters/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue'; -import * as JSON5 from 'json5'; - -Vue.filter('json5', x => { - return JSON5.stringify(x, null, 2); -}); - -require('./bytes'); -require('./number'); -require('./user'); -require('./note'); diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue deleted file mode 100644 index b4a4e1d5026ea14ca295b453ae7171d255ba70ae..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/explore.vue +++ /dev/null @@ -1,198 +0,0 @@ -<template> -<div> - <div class="localfedi7" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> - <header>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</header> - <div>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</div> - </div> - - <template v-if="tag == null"> - <mk-user-list :pagination="pinnedUsers" :expanded="false"> - <fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }} - </mk-user-list> - <mk-user-list :pagination="popularUsers" :expanded="false"> - <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyUpdatedUsers" :expanded="false"> - <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyRegisteredUsers" :expanded="false"> - <fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }} - </mk-user-list> - </template> - - <div class="localfedi7" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)` }"> - <header>{{ $t('explore-fediverse') }}</header> - </div> - - <ui-container :body-togglable="true" :expanded="false" ref="tags"> - <template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template> - - <div class="vxjfqztj"> - <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link> - <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link> - </div> - </ui-container> - - <mk-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}`"> - <fa :icon="faHashtag" fixed-width/>{{ tag }} - </mk-user-list> - <template v-if="tag == null"> - <mk-user-list :pagination="popularUsersF" :expanded="false"> - <fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyUpdatedUsersF" :expanded="false"> - <fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }} - </mk-user-list> - <mk-user-list :pagination="recentlyRegisteredUsersF" :expanded="false"> - <fa :icon="faRocket" fixed-width/>{{ $t('recently-discovered-users') }} - </mk-user-list> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons'; -import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/explore.vue'), - - props: { - tag: { - type: String, - required: false - } - }, - - inject: { - inNakedDeckColumn: { - default: false - } - }, - - data() { - return { - pinnedUsers: { endpoint: 'pinned-users' }, - popularUsers: { endpoint: 'users', limit: 10, params: { - state: 'alive', - origin: 'local', - sort: '+follower', - } }, - recentlyUpdatedUsers: { endpoint: 'users', limit: 10, params: { - origin: 'local', - sort: '+updatedAt', - } }, - recentlyRegisteredUsers: { endpoint: 'users', limit: 10, params: { - origin: 'local', - state: 'alive', - sort: '+createdAt', - } }, - popularUsersF: { endpoint: 'users', limit: 10, params: { - state: 'alive', - origin: 'remote', - sort: '+follower', - } }, - recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, params: { - origin: 'combined', - sort: '+updatedAt', - } }, - recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, params: { - origin: 'combined', - sort: '+createdAt', - } }, - tagsLocal: [], - tagsRemote: [], - stats: null, - meta: null, - num: Vue.filter('number'), - faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket - }; - }, - - computed: { - tagUsers(): any { - return { - endpoint: 'hashtags/users', - limit: 30, - params: { - tag: this.tag, - origin: 'combined', - sort: '+follower', - } - }; - }, - }, - - watch: { - tag() { - if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); - } - }, - - created() { - this.$emit('init', { - title: this.$t('@.explore'), - icon: faHashtag - }); - this.$root.api('hashtags/list', { - sort: '+attachedLocalUsers', - attachedToLocalUserOnly: true, - limit: 30 - }).then(tags => { - this.tagsLocal = tags; - }); - this.$root.api('hashtags/list', { - sort: '+attachedRemoteUsers', - attachedToRemoteUserOnly: true, - limit: 30 - }).then(tags => { - this.tagsRemote = tags; - }); - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - this.$root.getMeta().then(meta => { - this.meta = meta; - }); - }, - - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> - -<style lang="stylus" scoped> -.localfedi7 - overflow hidden - background var(--face) - color #fff - text-shadow 0 0 8px #000 - border-radius 6px - padding 16px - margin-top 16px - margin-bottom 16px - height 80px - background-position 50% - background-size cover - > header - font-size 20px - font-weight bold - > div - font-size 14px - opacity 0.8 - -.localfedi7:first-child - margin-top 0 - -.vxjfqztj - padding 16px - - > * - margin-right 16px - - &.local - font-weight bold -</style> diff --git a/src/client/app/common/views/pages/favorites.vue b/src/client/app/common/views/pages/favorites.vue deleted file mode 100644 index e396615a93feee094927dac672f609a356228006..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/favorites.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div> - <component :is="notesComponent" :pagination="pagination" :extract="items => items.map(item => item.note)"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faStar } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -//import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n(), - - props: { - platform: { - type: String, - required: true - } - }, - - data() { - return { - pagination: { - endpoint: 'i/favorites', - limit: 10, - }, - - notesComponent: - this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null - }; - }, - - created() { - this.$emit('init', { - title: this.$t('@.favorites'), - icon: faStar - }); - }, - - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/featured.vue b/src/client/app/common/views/pages/featured.vue deleted file mode 100644 index c00361aa854159df7a0e8681e9ab2980022318a9..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/featured.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div> - <component :is="notesComponent" :pagination="pagination"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faNewspaper } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -//import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n(), - - props: { - platform: { - type: String, - required: true - } - }, - - data() { - return { - pagination: { - endpoint: 'notes/featured', - limit: 29, - }, - - notesComponent: - this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) : - this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null - }; - }, - - created() { - this.$emit('init', { - title: this.$t('@.featured-notes'), - icon: faNewspaper - }); - }, - - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/follow-requests.vue b/src/client/app/common/views/pages/follow-requests.vue deleted file mode 100644 index 07ff7b7d549c6c3d403a0b3c14575c12164c4fa2..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/follow-requests.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> -<div> - <ui-container :body-togglable="true"> - <template #header>{{ $t('received-follow-requests') }}</template> - <div v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25" tag="div"> - <div v-for="req in requests" class="mcbzkkaw"> - <router-link :key="req.id" :to="req.follower | userPage"> - <mk-user-name :user="req.follower"/> - </router-link> - <span> - <a @click="accept(req.follower)">{{ $t('accept') }}</a> | <a @click="reject(req.follower)">{{ $t('reject') }}</a> - </span> - </div> - </sequential-entrance> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../scripts/loading'; -import { faUserClock } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/follow-requests.vue'), - data() { - return { - fetching: true, - requests: [] - }; - }, - created() { - this.$emit('init', { - title: this.$t('received-follow-requests'), - icon: faUserClock - }); - }, - mounted() { - Progress.start(); - this.$root.api('following/requests/list').then(requests => { - this.fetching = false; - this.requests = requests; - Progress.done(); - }); - }, - methods: { - accept(user) { - this.$root.api('following/requests/accept', { userId: user.id }).then(() => { - this.requests = this.requests.filter(r => r.follower.id != user.id); - }); - }, - reject(user) { - this.$root.api('following/requests/reject', { userId: user.id }).then(() => { - this.requests = this.requests.filter(r => r.follower.id != user.id); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mcbzkkaw - display flex - padding 16px - border solid 1px var(--faceDivider) - border-radius 4px - - > span - margin 0 0 0 auto - color var(--text) - -</style> diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue deleted file mode 100644 index c7b07a5be2312b840aa8133c4cd850faa906ff87..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/follow.vue +++ /dev/null @@ -1,242 +0,0 @@ -<template> -<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching"> - <div class="signed-in-as"> - <mfm :text="$t('signed-in-as').replace('{}', myName)" :plain="true" :custom-emojis="$store.state.i.emojis"/> - </div> - <main> - <div class="banner" :style="bannerStyle"></div> - <mk-avatar class="avatar" :user="user" :disable-preview="true"/> - <div class="body"> - <router-link :to="user | userPage" class="name"> - <mk-user-name :user="user"/> - </router-link> - <span class="username">@{{ user | acct }}</span> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </div> - </div> - </main> - - <button - :class="{ wait: followWait, active: user.isFollowing || user.hasPendingFollowRequestFromYou }" - @click="onClick" - :disabled="followWait"> - <template v-if="!followWait"> - <template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template> - <template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="spinner"/> {{ $t('follow-processing') }}</template> - <template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template> - <template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template> - <template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template> - </template> - <template v-else><fa icon="spinner" pulse fixed-width/></template> - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/follow.vue'), - data() { - return { - fetching: true, - user: null, - followWait: false - }; - }, - - computed: { - myName(): string { - return Vue.filter('userName')(this.$store.state.i); - }, - - bannerStyle(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - } - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - const acct = new URL(location.href).searchParams.get('acct'); - this.fetching = true; - Progress.start(); - if (acct.match(/^https?:/)) { - this.$root.api('ap/show', { - uri: acct - }).then((res: { type: string, object: any }) => { - if (res.type === 'User') { - this.user = res.object; - } else if (res.type === 'Note') { - this.$router.replace(`/notes/${res.object.id}`); - } else { - this.$root.dialog({ - type: 'error', - text: 'Not supported' - }); - } - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }).finally(() => { - this.fetching = false; - Progress.done(); - }); - } else { - this.$root.api('users/show', parseAcct(acct)).then((user: any) => { - this.user = user; - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }).finally(() => { - this.fetching = false; - Progress.done(); - }); - } - }, - - async onClick() { - this.followWait = true; - - try { - if (this.user.isFollowing) { - this.user = await this.$root.api('following/delete', { - userId: this.user.id - }); - } else { - if (this.user.hasPendingFollowRequestFromYou) { - this.user = await this.$root.api('following/requests/cancel', { - userId: this.user.id - }); - } else if (this.user.isLocked) { - this.user = await this.$root.api('following/create', { - userId: this.user.id - }); - } else { - this.user = await this.$root.api('following/create', { - userId: this.user.id - }); - } - } - } catch (e) { - console.error(e); - } finally { - this.followWait = false; - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.syxhndwprovvuqhmyvveewmbqayniwkv - padding 32px - max-width 500px - margin 0 auto - text-align center - color var(--text) - - $bg = var(--face) - - @media (max-width 400px) - padding 16px - - > .signed-in-as - margin-bottom 16px - font-size 14px - font-weight bold - - > main - margin-bottom 16px - background $bg - border-radius 8px - box-shadow 0 4px 12px rgba(#000, 0.1) - overflow hidden - - > .banner - height 128px - background-position center - background-size cover - - > .avatar - display block - margin -50px auto 0 auto - width 100px - height 100px - border-radius 100% - border solid 4px $bg - - > .body - padding 4px 32px 32px 32px - - @media (max-width 400px) - padding 4px 16px 16px 16px - - > .name - font-size 20px - font-weight bold - - > .username - display block - opacity 0.7 - - > .description - margin-top 16px - - > button - display block - user-select none - cursor pointer - padding 10px 16px - margin 0 - width 100% - min-width 150px - font-size 14px - font-weight bold - color var(--primary) - background transparent - outline none - border solid 1px var(--primary) - border-radius 36px - - &:hover - background var(--primaryAlpha01) - - &:active - background var(--primaryAlpha02) - - &.active - color var(--primaryForeground) - background var(--primary) - - &:hover - background var(--primaryLighten10) - border-color var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - border-color var(--primaryDarken10) - - &.wait - cursor wait !important - opacity 0.7 - - * - pointer-events none - -</style> diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue deleted file mode 100644 index b546e69ae38be206dbfc64ef42fff175b3c10bc7..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/followers.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> -<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.follower)">{{ $t('@.followers') }}</mk-user-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import parseAcct from '../../../../../misc/acct/parse'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - - data() { - return { - pagination: { - endpoint: 'users/followers', - limit: 30, - params: { - ...parseAcct(this.$route.params.user), - } - }, - }; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue deleted file mode 100644 index 4e584c19d9a0899898f778003b5b15ce9be2e317..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/following.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> -<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.followee)">{{ $t('@.following') }}</mk-user-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import parseAcct from '../../../../../misc/acct/parse'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n(), - - data() { - return { - pagination: { - endpoint: 'users/following', - limit: 30, - params: { - ...parseAcct(this.$route.params.user), - } - }, - }; - }, -}); -</script> diff --git a/src/client/app/common/views/pages/not-found.vue b/src/client/app/common/views/pages/not-found.vue deleted file mode 100644 index cb1b19687c4048f70252205dff8b54490017a0df..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/not-found.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<figure class="megtcxgu"> - <img :src="src" alt=""> - <figcaption> - <h1><span>Not found</span></h1> - <p><span>{{ $t('page-not-found') }}</span></p> - </figcaption> -</figure> -</template> - -<script lang="ts"> -import Vue from 'vue' -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('common/views/pages/not-found.vue'), - data() { - return { - src: '' - } - }, - created() { - this.$root.getMeta().then(meta => { - if (meta.errorImageUrl) - this.src = meta.errorImageUrl; - }); - } -}) -</script> - -<style lang="stylus" scoped> -.megtcxgu - align-items center - bottom 0 - display flex - justify-content center - left 0 - margin auto - position fixed - right 0 - top 0 - - > img - width 500px - - > figcaption - margin 8px - - h1, - p - color var(--text) - display flex - flex-flow column - - * - position relative - width 100% - - @media (max-width: 767px) - flex-flow column - - > figcaption - text-align center - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue deleted file mode 100644 index 6a82b0eec9072d83a3840e74701dffb3015cc545..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template> - - <section class="xfhsjczc"> - <ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input> - <ui-switch v-model="value.primary"><span>{{ $t('blocks._button.colored') }}</span></ui-switch> - <ui-select v-model="value.action"> - <template #label>{{ $t('blocks._button.action') }}</template> - <option value="dialog">{{ $t('blocks._button._action.dialog') }}</option> - <option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option> - <option value="pushEvent">{{ $t('blocks._button._action.pushEvent') }}</option> - </ui-select> - <template v-if="value.action === 'dialog'"> - <ui-input v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input> - </template> - <template v-else-if="value.action === 'pushEvent'"> - <ui-input v-model="value.event"><span>{{ $t('blocks._button._action._pushEvent.event') }}</span></ui-input> - <ui-input v-model="value.message"><span>{{ $t('blocks._button._action._pushEvent.message') }}</span></ui-input> - <ui-select v-model="value.var"> - <template #label>{{ $t('blocks._button._action._pushEvent.variable') }}</template> - <option :value="null">{{ $t('blocks._button._action._pushEvent.no-variable') }}</option> - <option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> - <optgroup :label="$t('script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$t('script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option> - </optgroup> - </ui-select> - </template> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - aiScript: { - required: true, - }, - }, - - data() { - return { - faBolt - }; - }, - - created() { - if (this.value.text == null) Vue.set(this.value, 'text', ''); - if (this.value.action == null) Vue.set(this.value, 'action', 'dialog'); - if (this.value.content == null) Vue.set(this.value, 'content', null); - if (this.value.event == null) Vue.set(this.value, 'event', null); - if (this.value.message == null) Vue.set(this.value, 'message', null); - if (this.value.primary == null) Vue.set(this.value, 'primary', false); - if (this.value.var == null) Vue.set(this.value, 'var', null); - }, -}); -</script> - -<style lang="stylus" scoped> -.xfhsjczc - padding 0 16px 0 16px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue deleted file mode 100644 index 30c3938111dcfd31d90001186c2e7a4dbc747926..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.numberInput') }}</template> - - <section style="padding: 0 16px 0 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._numberInput.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._numberInput.text') }}</span></ui-input> - <ui-input v-model="value.default" type="number"><span>{{ $t('blocks._numberInput.default') }}</span></ui-input> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue deleted file mode 100644 index 174a344640c71aa05fd62923edab24ed0d8d8487..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template> - - <section class="kjuadyyj"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input> - <ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> - -<style lang="stylus" scoped> -.kjuadyyj - padding 0 16px 16px 16px - -</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue deleted file mode 100644 index 50f95fd205d1c07c06293568652e23d581a5dbe8..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.textInput') }}</template> - - <section style="padding: 0 16px 0 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textInput.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._textInput.text') }}</span></ui-input> - <ui-input v-model="value.default" type="text"><span>{{ $t('blocks._textInput.default') }}</span></ui-input> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue deleted file mode 100644 index da3eead0803540e925129e9ce05cd0d3c09f71fa..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.textareaInput') }}</template> - - <section style="padding: 0 16px 16px 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textareaInput.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._textareaInput.text') }}</span></ui-input> - <ui-textarea v-model="value.default"><span>{{ $t('blocks._textareaInput.default') }}</span></ui-textarea> - </section> -</x-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; -import XContainer from '../page-editor.container.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - faBolt, faMagic - }; - }, - - created() { - if (this.value.name == null) Vue.set(this.value, 'name', ''); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.container.vue b/src/client/app/common/views/pages/page-editor/page-editor.container.vue deleted file mode 100644 index a3a501afb4089e0592fe2b32d8630da99c324320..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/page-editor/page-editor.container.vue +++ /dev/null @@ -1,146 +0,0 @@ -<template> -<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }"> - <header> - <div class="title"><slot name="header"></slot></div> - <div class="buttons"> - <slot name="func"></slot> - <button v-if="removable" @click="remove()"> - <fa :icon="faTrashAlt"/> - </button> - <button v-if="draggable" class="drag-handle"> - <fa :icon="faBars"/> - </button> - <button @click="toggleContent(!showBody)"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </div> - </header> - <p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> - <p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBars } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('pages'), - - props: { - expanded: { - type: Boolean, - default: true - }, - removable: { - type: Boolean, - default: true - }, - draggable: { - type: Boolean, - default: false - }, - error: { - required: false, - default: null - }, - warn: { - required: false, - default: null - } - }, - data() { - return { - showBody: this.expanded, - faTrashAlt, faBars - }; - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - this.$emit('toggle', show); - }, - remove() { - this.$emit('remove'); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cpjygsrt - overflow hidden - background var(--face) - border solid 2px var(--pageBlockBorder) - border-radius 6px - - &:hover - border solid 2px var(--pageBlockBorderHover) - - &.warn - border solid 2px #dec44c - - &.error - border solid 2px #f00 - - & + .cpjygsrt - margin-top 16px - - > header - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - box-shadow 0 1px rgba(#000, 0.07) - - > [data-icon] - margin-right 6px - - &:empty - display none - - > .buttons - position absolute - z-index 2 - top 0 - right 0 - - > button - padding 0 - width 42px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - .drag-handle - cursor move - - > .warn - color #b19e49 - margin 0 - padding 16px 16px 0 16px - font-size 14px - - > .error - color #f00 - margin 0 - padding 16px 16px 0 16px - font-size 14px - -</style> diff --git a/src/client/app/common/views/pages/pages.vue b/src/client/app/common/views/pages/pages.vue deleted file mode 100644 index 236330db464f307302412620461ed7409228baee..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/pages.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<div> - <ui-container :body-togglable="true"> - <template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template> - <div class="rknalgpo my"> - <ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button> - <ui-pagination :pagination="myPagesPagination" #default="{items}"> - <x-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> - </ui-pagination> - </div> - </ui-container> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template> - <div class="rknalgpo"> - <ui-pagination :pagination="likedPagesPagination" #default="{items}"> - <x-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> - </ui-pagination> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons'; -import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import XPagePreview from '../../views/components/page-preview.vue'; - -export default Vue.extend({ - i18n: i18n('pages'), - components: { - XPagePreview - }, - data() { - return { - myPagesPagination: { - endpoint: 'i/pages', - limit: 5, - }, - likedPagesPagination: { - endpoint: 'i/page-likes', - limit: 5, - }, - faStickyNote, faPlus, faEdit, faHeart - }; - }, - created() { - this.$emit('init', { - title: this.$t('@.pages'), - icon: faStickyNote - }); - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - create() { - this.$router.push(`/i/pages/new`); - } - } -}); -</script> - -<style lang="stylus" scoped> -.rknalgpo - padding 16px - - &.my .ckltabjg:first-child - margin-top 16px - - .ckltabjg:not(:last-child) - margin-bottom 8px - - @media (min-width 500px) - .ckltabjg:not(:last-child) - margin-bottom 16px - -</style> diff --git a/src/client/app/common/views/pages/room/preview.vue b/src/client/app/common/views/pages/room/preview.vue deleted file mode 100644 index 94c13cee9f1c034c78ebe237c918530b6bdb28db..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/room/preview.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<canvas width=224 height=128></canvas> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as THREE from 'three'; - -export default Vue.extend({ - data() { - return { - selected: null, - objectHeight: 0, - orbitRadius: 5 - }; - }, - - mounted() { - const canvas = this.$el; - - const width = canvas.width; - const height = canvas.height; - - const scene = new THREE.Scene(); - - const renderer = new THREE.WebGLRenderer({ - canvas: canvas, - antialias: true, - alpha: false - }); - renderer.setPixelRatio(window.devicePixelRatio); - renderer.setSize(width, height); - renderer.setClearColor(0x000000); - renderer.autoClear = false; - renderer.shadowMap.enabled = true; - renderer.shadowMap.cullFace = THREE.CullFaceBack; - - const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100); - camera.zoom = 10; - camera.position.x = 0; - camera.position.y = 2; - camera.position.z = 0; - camera.updateProjectionMatrix(); - scene.add(camera); - - const ambientLight = new THREE.AmbientLight(0xffffff, 1); - ambientLight.castShadow = false; - scene.add(ambientLight); - - const light = new THREE.PointLight(0xffffff, 1, 100); - light.position.set(3, 3, 3); - scene.add(light); - - const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222); - scene.add(grid); - - const render = () => { - const timer = Date.now() * 0.0004; - requestAnimationFrame(render); - - camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius; // Math.PI / 6 => 30deg - camera.position.z = Math.cos(timer) * this.orbitRadius; - camera.position.x = Math.sin(timer) * this.orbitRadius; - camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0)); - renderer.render(scene, camera); - }; - - this.selected = selected => { - const obj = selected.clone(); - - // Remove current object - const current = scene.getObjectByName('obj'); - if (current != null) { - scene.remove(current); - } - - // Add new object - obj.name = 'obj'; - obj.position.x = 0; - obj.position.y = 0; - obj.position.z = 0; - obj.rotation.x = 0; - obj.rotation.y = 0; - obj.rotation.z = 0; - obj.traverse(child => { - if (child instanceof THREE.Mesh) { - child.material = child.material.clone(); - return child.material.emissive.setHex(0x000000); - } - }); - const objectBoundingBox = new THREE.Box3().setFromObject(obj); - this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y; - - const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x; - const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z; - - const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect; - this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180)); - - scene.add(obj); - }; - - render(); - }, -}); -</script> diff --git a/src/client/app/common/views/pages/room/room.vue b/src/client/app/common/views/pages/room/room.vue deleted file mode 100644 index 1e81920c22110e707f4ac000f6958c77dd6a0ccf..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/room/room.vue +++ /dev/null @@ -1,310 +0,0 @@ -<template> -<div class="hveuntkp"> - <div class="controller" v-if="objectSelected"> - <section> - <p class="name">{{ selectedFurnitureName }}</p> - <x-preview ref="preview"/> - <template v-if="selectedFurnitureInfo.props"> - <div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k"> - <p>{{ k }}</p> - <template v-if="selectedFurnitureInfo.props[k] === 'image'"> - <ui-button @click="chooseImage(k)">{{ $t('chooseImage') }}</ui-button> - </template> - <template v-else-if="selectedFurnitureInfo.props[k] === 'color'"> - <input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/> - </template> - </div> - </template> - </section> - <section> - <ui-button @click="translate()" :primary="isTranslateMode"><fa :icon="faArrowsAlt"/> {{ $t('translate') }}</ui-button> - <ui-button @click="rotate()" :primary="isRotateMode"><fa :icon="faUndo"/> {{ $t('rotate') }}</ui-button> - <ui-button v-if="isTranslateMode || isRotateMode" @click="exit()"><fa :icon="faBan"/> {{ $t('exit') }}</ui-button> - </section> - <section> - <ui-button @click="remove()"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</ui-button> - </section> - </div> - - <div class="menu" v-if="isMyRoom"> - <section> - <ui-button @click="add()"><fa :icon="faBoxOpen"/> {{ $t('add-furniture') }}</ui-button> - </section> - <section> - <ui-select :value="roomType" @input="updateRoomType($event)"> - <template #label>{{ $t('room-type') }}</template> - <option value="default">{{ $t('rooms.default') }}</option> - <option value="washitsu">{{ $t('rooms.washitsu') }}</option> - </ui-select> - <label v-if="roomType === 'default'"> - <span>{{ $t('carpet-color') }}</span> - <input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/> - </label> - </section> - <section> - <ui-button :primary="changed" @click="save()"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> - <ui-button @click="clear()"><fa :icon="faBroom"/> {{ $t('clear') }}</ui-button> - </section> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { Room } from '../../../scripts/room/room'; -import parseAcct from '../../../../../../misc/acct/parse'; -import XPreview from './preview.vue'; -const storeItems = require('../../../scripts/room/furnitures.json5'); -import { faBoxOpen, faUndo, faArrowsAlt, faBan, faBroom } from '@fortawesome/free-solid-svg-icons'; -import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import { query as urlQuery } from '../../../../../../prelude/url'; - -let room: Room; - -export default Vue.extend({ - i18n: i18n('room'), - - components: { - XPreview - }, - - props: { - acct: { - type: String, - required: true - }, - }, - - data() { - return { - objectSelected: false, - selectedFurnitureName: null, - selectedFurnitureInfo: null, - selectedFurnitureProps: null, - roomType: null, - carpetColor: null, - isTranslateMode: false, - isRotateMode: false, - isMyRoom: false, - changed: false, - faBoxOpen, faSave, faTrashAlt, faUndo, faArrowsAlt, faBan, faBroom, - }; - }, - - async mounted() { - window.addEventListener('beforeunload', this.beforeunload); - - const user = await this.$root.api('users/show', { - ...parseAcct(this.acct) - }); - - this.isMyRoom = this.$store.getters.isSignedIn && this.$store.state.i.id === user.id; - - const roomInfo = await this.$root.api('room/show', { - userId: user.id - }); - - this.roomType = roomInfo.roomType; - this.carpetColor = roomInfo.carpetColor; - - room = new Room(user, this.isMyRoom, roomInfo, this.$el, { - graphicsQuality: this.$store.state.device.roomGraphicsQuality, - onChangeSelect: obj => { - this.objectSelected = obj != null; - if (obj) { - const f = room.findFurnitureById(obj.name); - this.selectedFurnitureName = this.$t('furnitures.' + f.type); - this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type); - this.selectedFurnitureProps = f.props - ? JSON.parse(JSON.stringify(f.props)) // Disable reactivity - : null; - this.$nextTick(() => { - this.$refs.preview.selected(obj); - }); - } - }, - useOrthographicCamera: this.$store.state.device.roomUseOrthographicCamera - }); - }, - - beforeRouteLeave(to, from, next) { - if (this.changed) { - this.$root.dialog({ - type: 'warning', - text: this.$t('leave-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) { - next(false); - } else { - next(); - } - }); - } else { - next(); - } - }, - - beforeDestroy() { - room.destroy(); - window.removeEventListener('beforeunload', this.beforeunload); - }, - - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, - - async add() { - const { canceled, result: id } = await this.$root.dialog({ - type: null, - title: this.$t('add-furniture'), - select: { - items: storeItems.map(item => ({ - value: item.id, text: this.$t('furnitures.' + item.id) - })) - }, - showCancelButton: true - }); - if (canceled) return; - room.addFurniture(id); - this.changed = true; - }, - - remove() { - this.isTranslateMode = false; - this.isRotateMode = false; - room.removeFurniture(); - this.changed = true; - }, - - save() { - this.$root.api('room/update', { - room: room.getRoomInfo() - }).then(() => { - this.changed = false; - this.$root.dialog({ - type: 'success', - text: this.$t('saved') - }); - }).catch((e: any) => { - this.$root.dialog({ - type: 'error', - text: e.message - }); - }); - }, - - clear() { - this.$root.dialog({ - type: 'warning', - text: this.$t('clear-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - room.removeAllFurnitures(); - this.changed = true; - }); - }, - - chooseImage(key) { - this.$chooseDriveFile({ - multiple: false - }).then(file => { - room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`); - this.$refs.preview.selected(room.getSelectedObject()); - this.changed = true; - }); - }, - - updateColor(key, ev) { - room.updateProp(key, ev.target.value); - this.$refs.preview.selected(room.getSelectedObject()); - this.changed = true; - }, - - updateCarpetColor(ev) { - room.updateCarpetColor(ev.target.value); - this.carpetColor = ev.target.value; - this.changed = true; - }, - - updateRoomType(type) { - room.changeRoomType(type); - this.roomType = type; - this.changed = true; - }, - - translate() { - if (this.isTranslateMode) { - this.exit(); - } else { - this.isRotateMode = false; - this.isTranslateMode = true; - room.enterTransformMode('translate'); - } - this.changed = true; - }, - - rotate() { - if (this.isRotateMode) { - this.exit(); - } else { - this.isTranslateMode = false; - this.isRotateMode = true; - room.enterTransformMode('rotate'); - } - this.changed = true; - }, - - exit() { - this.isTranslateMode = false; - this.isRotateMode = false; - room.exitTransformMode(); - this.changed = true; - } - } -}); -</script> - -<style lang="stylus" scoped> -.hveuntkp - > .controller - > .menu - position fixed - z-index 1 - padding 16px - background var(--face) - color var(--text) - - > section - padding 16px 0 - - &:first-child - padding-top 0 - - &:last-child - padding-bottom 0 - - &:not(:last-child) - border-bottom solid 1px var(--faceDivider) - - > .controller - top 16px - left 16px - width 256px - - > section - > .name - margin 0 - - > .menu - top 16px - right 16px - width 256px - -</style> diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue deleted file mode 100644 index 293a9bcfb581bc51aec8125453fbba3699c1abec..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/share.vue +++ /dev/null @@ -1,79 +0,0 @@ -<template> -<div class="azibmfpleajagva420swmu4c3r7ni7iw"> - <h1>{{ $t('share-with', { name }) }}</h1> - <div> - <mk-signin v-if="!$store.getters.isSignedIn"/> - <x-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/> - <p v-if="posted" class="posted"><fa icon="check"/></p> - </div> - <ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/share.vue'), - components: { - XPostForm: () => import('../../../desktop/views/components/post-form.vue').then(m => m.default) - }, - data() { - return { - name: null, - posted: false, - text: new URLSearchParams(location.search).get('text'), - url: new URLSearchParams(location.search).get('url'), - title: new URLSearchParams(location.search).get('title'), - }; - }, - computed: { - template(): string { - let t = ''; - if (this.title && this.url) t += `ã€[${this.title}](${this.url})】\n`; - if (this.title && !this.url) t += `ã€${this.title}】\n`; - if (this.text) t += `${this.text}\n`; - if (!this.title && this.url) t += `${this.url}`; - return t.trim(); - } - }, - mounted() { - this.$root.getMeta().then(meta => { - this.name = meta.name || 'Misskey'; - }); - }, - methods: { - close() { - window.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.azibmfpleajagva420swmu4c3r7ni7iw - > h1 - margin 8px 0 - color #555 - font-size 20px - text-align center - - > div - max-width 500px - margin 0 auto - - > .posted - display block - margin 0 auto - padding 64px - text-align center - background #fff - border-radius 6px - width calc(100% - 32px) - - > .close - display block - margin 16px auto - width calc(100% - 32px) -</style> diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue deleted file mode 100644 index 9cc012af7ae2252c61a97d67ad6eed3a1f85cb41..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/user-group-editor.vue +++ /dev/null @@ -1,256 +0,0 @@ -<template> -<div class="ivrbakop"> - <ui-container v-if="group"> - <template #header><fa :icon="faUsers"/> {{ group.name }}</template> - - <section> - <ui-margin> - <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> - <ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - <ui-button @click="transfer"><fa :icon="faCrown"/> {{ $t('transfer') }}</ui-button> - </ui-margin> - </section> - </ui-container> - - <ui-container> - <template #header><fa :icon="faUsers"/> {{ $t('users') }}</template> - - <section> - <ui-margin> - <ui-button @click="invite()"><fa :icon="faPlus"/> {{ $t('invite') }}</ui-button> - </ui-margin> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="kjlrfbes" v-for="user in users"> - <div> - <a :href="user | userPage"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div> - <header> - <b><mk-user-name :user="user"/></b> - <span class="is-owner" v-if="group.ownerId === user.id">owner</span> - <span class="username">@{{ user | acct }}</span> - </header> - <div v-if="group.ownerId !== user.id"> - <a @click="remove(user)">{{ $t('remove-user') }}</a> - </div> - </div> - </div> - </sequential-entrance> - </section> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faCrown, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-group-editor.vue'), - - props: { - groupId: { - required: true - } - }, - - data() { - return { - group: null, - users: [], - faCrown, faICursor, faTrashAlt, faUsers, faPlus - }; - }, - - created() { - this.$root.api('users/groups/show', { - groupId: this.groupId - }).then(group => { - this.group = group; - this.fetchUsers(); - this.$emit('init', { - title: this.group.name, - icon: faUsers - }); - }); - }, - - methods: { - fetchGroup() { - this.$root.api('users/groups/show', { - groupId: this.group.id - }).then(group => { - this.group = group; - }) - }, - - fetchUsers() { - this.$root.api('users/show', { - userIds: this.group.userIds - }).then(users => { - this.users = users; - }); - }, - - rename() { - this.$root.dialog({ - title: this.$t('rename'), - input: { - default: this.group.name - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('users/groups/update', { - groupId: this.group.id, - name: name - }).then(() => { - this.fetchGroup(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }) - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-are-you-sure').replace('$1', this.group.name), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('users/groups/delete', { - groupId: this.group.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - }, - - remove(user: any) { - this.$root.api('users/groups/pull', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.fetchGroup(); - this.fetchUsers(); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async invite() { - const t = this.$t('invited'); - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - this.$root.api('users/groups/invite', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: t - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }, - - async transfer() { - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - - this.$root.dialog({ - type: 'warning', - text: this.$t('transfer-are-you-sure').replace('$1', this.group.name).replace('$2', user.username), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('users/groups/transfer', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('transferred') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ivrbakop - .kjlrfbes - display flex - padding 16px - border-top solid 1px var(--faceDivider) - - > div:first-child - > a - > .avatar - width 64px - height 64px - - > div:last-child - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - color var(--text) - - > .is-owner - flex-shrink 0 - align-self center - margin-left 8px - padding 1px 6px - font-size 80% - background var(--groupUserListOwnerBg) - color var(--groupUserListOwnerFg) - border-radius 3px - - > .username - margin-left 8px - opacity 0.7 - -</style> diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue deleted file mode 100644 index 6501a26061e1670bd910166d64c90f8604a2929b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/user-groups.vue +++ /dev/null @@ -1,132 +0,0 @@ -<template> -<div> - <ui-container :body-togglable="true"> - <template #header><fa :icon="faUsers"/> {{ $t('owned-groups') }}</template> - <ui-margin> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button> - </ui-margin> - <div class="hwgkdrbl" v-for="group in ownedGroups" :key="group.id"> - <ui-hr/> - <ui-margin> - <router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link> - <x-avatars :user-ids="group.userIds" style="margin-top:8px;"/> - </ui-margin> - </div> - </ui-container> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faUsers"/> {{ $t('joined-groups') }}</template> - <div class="hwgkdrbl" v-for="(group, i) in joinedGroups" :key="group.id"> - <ui-hr v-if="i != 0"/> - <ui-margin> - <div style="color:var(--text);">{{ group.name }}</div> - <x-avatars :user-ids="group.userIds" style="margin-top:8px;"/> - </ui-margin> - </div> - </ui-container> - - <ui-container :body-togglable="true"> - <template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template> - <div class="fvlojuur" v-for="(invite, i) in invites" :key="invite.id"> - <ui-hr v-if="i != 0"/> - <ui-margin> - <div class="name" style="color:var(--text);">{{ invite.group.name }}</div> - <x-avatars :user-ids="invite.group.userIds" style="margin-top:8px;"/> - <ui-horizon-group> - <ui-button @click="acceptInvite(invite)"><fa :icon="faCheck"/> {{ $t('accept-invite') }}</ui-button> - <ui-button @click="rejectInvite(invite)"><fa :icon="faBan"/> {{ $t('reject-invite') }}</ui-button> - </ui-horizon-group> - </ui-margin> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; -import XAvatars from '../../views/components/avatars.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-groups.vue'), - components: { - XAvatars - }, - data() { - return { - ownedGroups: [], - joinedGroups: [], - invites: [], - faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText - }; - }, - mounted() { - document.title = this.$root.instanceName; - - this.$root.api('users/groups/owned').then(groups => { - this.ownedGroups = groups; - }); - - this.$root.api('users/groups/joined').then(groups => { - this.joinedGroups = groups; - }); - - this.$root.api('i/user-group-invites').then(invites => { - this.invites = invites; - }); - - this.$emit('init', { - title: this.$t('user-groups'), - icon: faUsers - }); - }, - methods: { - add() { - this.$root.dialog({ - title: this.$t('group-name'), - input: true - }).then(async ({ canceled, result: name }) => { - if (canceled) return; - const group = await this.$root.api('users/groups/create', { - name - }); - - this.ownedGroups.push(group) - }); - }, - acceptInvite(invite) { - this.$root.api('users/groups/invitations/accept', { - inviteId: invite.id - }).then(() => { - this.$root.dialog({ - type: 'success', - splash: true - }); - this.$root.api('i/user-group-invites').then(invites => { - this.invites = invites; - }).then(() => { - this.$root.api('users/groups/joined').then(groups => { - this.joinedGroups = groups; - }); - }); - }); - }, - rejectInvite(invite) { - this.$root.api('users/groups/invitations/reject', { - inviteId: invite.id - }).then(() => { - this.$root.api('i/user-group-invites').then(invites => { - this.invites = invites; - }); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.hwgkdrbl - display block - -</style> diff --git a/src/client/app/common/views/pages/user-list-editor.vue b/src/client/app/common/views/pages/user-list-editor.vue deleted file mode 100644 index 3bc5cca7781a82124a9036cd4865778b0760d537..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/user-list-editor.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<div class="cudqjmnl"> - <ui-container v-if="list"> - <template #header><fa :icon="faListUl"/> {{ list.name }}</template> - - <section class="fwvevrks"> - <ui-margin> - <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> - <ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> - </ui-margin> - </section> - </ui-container> - - <ui-container> - <template #header><fa :icon="faUsers"/> {{ $t('users') }}</template> - - <section> - <ui-margin> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button> - </ui-margin> - <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="phcqulfl" v-for="user in users"> - <div> - <a :href="user | userPage"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div> - <header> - <b><mk-user-name :user="user"/></b> - <span class="username">@{{ user | acct }}</span> - </header> - <div> - <a @click="remove(user)">{{ $t('remove-user') }}</a> - </div> - </div> - </div> - </sequential-entrance> - </section> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faListUl, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-list-editor.vue'), - - props: { - listId: { - required: true - } - }, - - data() { - return { - list: null, - users: [], - faListUl, faICursor, faTrashAlt, faUsers, faPlus - }; - }, - - created() { - this.$root.api('users/lists/show', { - listId: this.listId - }).then(list => { - this.list = list; - this.fetchUsers(); - this.$emit('init', { - title: this.list.name, - icon: faListUl - }); - }); - }, - - methods: { - fetchUsers() { - this.$root.api('users/show', { - userIds: this.list.userIds - }).then(users => { - this.users = users; - }); - }, - - rename() { - this.$root.dialog({ - title: this.$t('rename'), - input: { - default: this.list.name - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('users/lists/update', { - listId: this.list.id, - name: name - }); - }); - }, - - del() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-are-you-sure').replace('$1', this.list.name), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('users/lists/delete', { - listId: this.list.id - }).then(() => { - this.$root.dialog({ - type: 'success', - text: this.$t('deleted') - }); - }).catch(e => { - this.$root.dialog({ - type: 'error', - text: e - }); - }); - }); - }, - - remove(user: any) { - this.$root.api('users/lists/pull', { - listId: this.list.id, - userId: user.id - }).then(() => { - this.fetchUsers(); - }); - }, - - async add() { - const { result: user } = await this.$root.dialog({ - user: { - local: true - } - }); - if (user == null) return; - this.$root.api('users/lists/push', { - listId: this.list.id, - userId: user.id - }).then(() => { - this.fetchUsers(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cudqjmnl - .phcqulfl - display flex - padding 16px - border-top solid 1px var(--faceDivider) - - > div:first-child - > a - > .avatar - width 64px - height 64px - - > div:last-child - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - color var(--text) - - > .username - margin-left 8px - opacity 0.7 - -</style> diff --git a/src/client/app/common/views/pages/user-lists.vue b/src/client/app/common/views/pages/user-lists.vue deleted file mode 100644 index 955eef993a3c263547909acc5bc5540c8304cf0a..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/pages/user-lists.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<ui-container> - <template #header><fa :icon="faListUl"/> {{ $t('user-lists') }}</template> - <ui-margin> - <ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-list') }}</ui-button> - </ui-margin> - <div class="cpqqyrst" v-for="list in lists" :key="list.id"> - <ui-hr/> - <ui-margin> - <router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link> - <x-avatars :user-ids="list.userIds" style="margin-top:8px;"/> - </ui-margin> - </div> -</ui-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons'; -import XAvatars from '../../views/components/avatars.vue'; - -export default Vue.extend({ - i18n: i18n('common/views/components/user-lists.vue'), - components: { - XAvatars - }, - data() { - return { - fetching: true, - lists: [], - faListUl, faPlus - }; - }, - mounted() { - document.title = this.$root.instanceName; - - this.$root.api('users/lists/list').then(lists => { - this.fetching = false; - this.lists = lists; - }); - - this.$emit('init', { - title: this.$t('user-lists'), - icon: faListUl - }); - }, - methods: { - add() { - this.$root.dialog({ - title: this.$t('list-name'), - input: true - }).then(async ({ canceled, result: name }) => { - if (canceled) return; - const list = await this.$root.api('users/lists/create', { - name - }); - - this.lists.push(list) - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.cpqqyrst - display block - -</style> diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue deleted file mode 100644 index bff01f89b54fedc6c152b0f5b97c762cb6e464e9..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/analog-clock.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> -<div class="mkw-analog-clock"> - <ui-container :naked="props.style % 2 === 0" :show-header="false"> - <div class="mkw-analog-clock--body"> - <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'analog-clock', - props: () => ({ - style: 0 - }) -}).extend({ - methods: { - func() { - this.props.style = (this.props.style + 1) % 4; - this.save(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-analog-clock - .mkw-analog-clock--body - padding 8px - -</style> diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue deleted file mode 100644 index 9423d25da89c54b458c312155f22103927cacb9b..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/broadcast.vue +++ /dev/null @@ -1,205 +0,0 @@ -<template> -<div class="anltbovirfeutcigvwgmgxipejaeozxi"> - <ui-container :show-header="false" :naked="props.design === 1"> - <div class="anltbovirfeutcigvwgmgxipejaeozxi-body" - :data-found="announcements && announcements.length !== 0" - :data-melt="props.design == 1" - :data-mobile="platform == 'mobile'" - > - <div class="broadcast-left" v-show="announcements && announcements.length !== 0"> - <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> - <div class="broadcast-nav" v-show="announcements && announcements.length > 1"> - <mk-frac class="broadcast-page" :value="i + 1" :total="announcements.length"/> - <ui-button class="broadcast-prev" @click="prev" :title="$t('next')"><fa :icon="faAngleLeft"/></ui-button> - <ui-button class="broadcast-next" @click="next" :title="$t('prev')"><fa :icon="faAngleRight"/></ui-button> - </div> - </div> - <div class="broadcast-right"> - <p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p> - <h1 v-if="!fetching">{{ announcements.length == 0 ? $t('no-broadcasts') : announcements[i].title }}</h1> - <p v-if="!fetching"> - <mfm v-if="announcements.length != 0" :text="announcements[i].text" :key="i"/> - <img v-if="announcements.length != 0 && announcements[i].image" :src="announcements[i].image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/> - <template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template> - </p> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../i18n'; - -export default define({ - name: 'broadcast', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/broadcast.vue'), - data() { - return { - i: 0, - fetching: true, - announcements: [], - faAngleLeft, faAngleRight - }; - }, - mounted() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - this.fetching = false; - }); - }, - methods: { - next() { - if (this.i === this.announcements.length - 1) { - this.i = 0; - } else { - this.i++; - } - }, - prev() { - if (this.i === 0) { - this.i = this.announcements.length - 1; - } else { - this.i--; - } - }, - func() { - if (this.props.design === 1) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.anltbovirfeutcigvwgmgxipejaeozxi-body - display flex - padding 10px - background var(--announcementsBg) - - &[data-melt] - background transparent - - > .broadcast-left - width 32px - margin-right 8px - - > .icon - > svg - fill currentColor - color var(--announcementsTitle) - - > .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 - - > .broadcast-nav - display flex - flex-wrap wrap - padding 1px 0 2px - - > .broadcast-page - width 100% - color var(--announcementsTitle) - text-align center - font-size .6rem - - > .broadcast-prev, - > .broadcast-next - flex 1 - width 50% - display block - margin 0 - padding 0 - font-size .9rem - line-height 1.3em - color var(--link) - background transparent - cursor pointer - - &:focus - &:after - top -1px - right -1px - bottom -1px - left -1px - - &.round:focus:after - border-radius 5px - - > .broadcast-prev - padding-right 3px - - > .broadcast-next - padding-left 3px - - > .broadcast-right - flex 1 - word-break break-word - - > h1 - margin 0 - font-size .975em - font-weight normal - line-height 1.3em - color var(--announcementsTitle) - padding-bottom 2px - - > p - display block - z-index 1 - margin 0 - font-size .8em - color var(--announcementsText) - width 100% - - &.fetching - text-align center - - &[data-mobile] - > p - color #fff - -</style> diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue deleted file mode 100644 index 32ce1efeb7ef500740b23be6f1bb49b6961f4355..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/calendar.vue +++ /dev/null @@ -1,202 +0,0 @@ -<template> -<div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'"> - <ui-container :naked="props.design == 1" :show-header="false"> - <div class="mkw-calendar--body"> - <div class="calendar" :data-is-holiday="isHoliday"> - <p class="month-and-year"> - <span class="year">{{ this.$t('year').split('{}')[0] }}{{ year }}{{ this.$t('year').split('{}')[1] }}</span> - <span class="month">{{ this.$t('month').split('{}')[0] }}{{ month }}{{ this.$t('month').split('{}')[1] }}</span> - </p> - <p class="day">{{ this.$t('day').split('{}')[0] }}{{ day }}{{ this.$t('day').split('{}')[1] }}</p> - <p class="week-day">{{ weekDay }}</p> - </div> - <div class="info"> - <div> - <p>{{ $t('today') }}<b>{{ dayP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${dayP}%` }"></div> - </div> - </div> - <div> - <p>{{ $t('this-month') }}<b>{{ monthP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${monthP}%` }"></div> - </div> - </div> - <div> - <p>{{ $t('this-year') }}<b>{{ yearP.toFixed(1) }}%</b></p> - <div class="meter"> - <div class="val" :style="{ width: `${yearP}%` }"></div> - </div> - </div> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'calendar', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/calendar.vue'), - 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.platform == 'mobile') return; - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - 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 = [ - this.$t('@.weekday.sunday'), - this.$t('@.weekday.monday'), - this.$t('@.weekday.tuesday'), - this.$t('@.weekday.wednesday'), - this.$t('@.weekday.thursday'), - this.$t('@.weekday.friday'), - this.$t('@.weekday.saturday') - ][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> -.mkw-calendar - &[data-special='on-new-years-day'] - border-color #ef95a0 - - .mkw-calendar--body - padding 16px 0 - color var(--calendarDay) - - &: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 var(--text) - opacity 0.8 - - > b - margin-left 2px - - > .meter - width 100% - overflow hidden - background var(--materBg) - border-radius 8px - - > .val - height 4px - background var(--primary) - transition width .3s cubic-bezier(0.23, 1, 0.32, 1) - - &: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/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue deleted file mode 100644 index b266d5f6e6a97dbaa517fcf3f4e7ed14fd17d7d7..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/hashtags.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<div class="mkw-hashtags"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="hashtag"/>{{ $t('title') }}</template> - - <div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> - <mk-trends/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'hashtags', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('common/views/widgets/hashtags.vue'), - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> diff --git a/src/client/app/common/views/widgets/index.ts b/src/client/app/common/views/widgets/index.ts deleted file mode 100644 index d923a01941bc6d16b0aa119c60d82098771ef9fd..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Vue from 'vue'; - -import wAnalogClock from './analog-clock.vue'; -import wVersion from './version.vue'; -import wRss from './rss.vue'; -import wServer from './server.vue'; -import wPostsMonitor from './posts-monitor.vue'; -import wMemo from './memo.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 wNav from './nav.vue'; -import wHashtags from './hashtags.vue'; -import wInstance from './instance.vue'; -import wPostForm from './post-form.vue'; - -Vue.component('mkw-analog-clock', wAnalogClock); -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-broadcast', wBroadcast); -Vue.component('mkw-server', wServer); -Vue.component('mkw-posts-monitor', wPostsMonitor); -Vue.component('mkw-memo', wMemo); -Vue.component('mkw-rss', wRss); -Vue.component('mkw-version', wVersion); -Vue.component('mkw-hashtags', wHashtags); -Vue.component('mkw-instance', wInstance); -Vue.component('mkw-post-form', wPostForm); -Vue.component('mkw-queue', () => import('./queue.vue').then(m => m.default)); diff --git a/src/client/app/common/views/widgets/instance.vue b/src/client/app/common/views/widgets/instance.vue deleted file mode 100644 index 96d6184d1e7796b7f34a52877f90ea83f1e84936..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/instance.vue +++ /dev/null @@ -1,14 +0,0 @@ -<template> -<div class="mkw-instance"> - <ui-container> - <mk-instance/> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'instance' -}); -</script> diff --git a/src/client/app/common/views/widgets/memo.vue b/src/client/app/common/views/widgets/memo.vue deleted file mode 100644 index b3b668a9ad9105b19197ba06bdbeea925a58c1ad..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/memo.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<div class="mkw-memo"> - <ui-container :show-header="!props.compact"> - <template #header><fa :icon="['far', 'sticky-note']"/>{{ $t('title') }}</template> - - <div class="mkw-memo--body"> - <textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> - <button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'memo', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('common/views/widgets/memo.vue'), - data() { - return { - text: null, - changed: false, - timeoutId: null - }; - }, - - created() { - this.text = this.$store.state.settings.memo; - - this.$watch('$store.state.settings.memo', text => { - this.text = text; - }); - }, - - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - - onChange() { - this.changed = true; - clearTimeout(this.timeoutId); - this.timeoutId = setTimeout(this.saveMemo, 1000); - }, - - saveMemo() { - this.$store.dispatch('settings/set', { - key: 'memo', - value: this.text - }); - this.changed = false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-memo - .mkw-memo--body - padding-bottom 28px + 16px - - > textarea - display block - width 100% - max-width 100% - min-width 100% - padding 16px - color var(--inputText) - background var(--face) - border none - border-bottom solid var(--lineWidth) var(--faceDivider) - border-radius 0 - - > button - display block - position absolute - bottom 8px - right 8px - margin 0 - padding 0 10px - height 28px - color var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - - &:disabled - opacity 0.7 - cursor default - -</style> diff --git a/src/client/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue deleted file mode 100644 index 2b8caa7be897968975a511e94f464e755cbc9f3d..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/nav.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<div class="mkw-nav"> - <ui-container> - <div class="mkw-nav--body"> - <mk-nav/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -export default define({ - name: 'nav' -}); -</script> - -<style lang="stylus" scoped> -.mkw-nav - .mkw-nav--body - padding 16px - font-size 12px - color var(--text) - background var(--face) - - a - color var(--text) - - i - color var(--text) - -</style> diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue deleted file mode 100644 index eae6d0a1904f57953c582479411a8333d65c6967..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/photo-stream.vue +++ /dev/null @@ -1,125 +0,0 @@ -<template> -<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2"> - <ui-container :show-header="props.design == 0" :naked="props.design == 2"> - <template #header><fa icon="camera"/>{{ $t('title') }}</template> - - <p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div :class="$style.stream" v-if="!fetching && images.length > 0"> - <div v-for="(image, i) in images" :key="i" - :class="$style.img" - :style="`background-image: url(${thumbnail(image)})`" - draggable="true" - @dragstart="onDragstart(image, $event)" - ></div> - </div> - <p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import { getStaticImageUrl } from '../../scripts/get-static-image-url'; - -export default define({ - name: 'photo-stream', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/photo-stream.vue'), - - data() { - return { - images: [], - fetching: true, - connection: null - }; - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('driveFileCreated', this.onDriveFileCreated); - - this.$root.api('drive/stream', { - type: 'image/*', - limit: 9 - }).then(images => { - this.images = images; - this.fetching = false; - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - 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++; - } - - this.save(); - }, - - onDragstart(file, e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('mk_drive_file', JSON.stringify(file)); - }, - - thumbnail(image: any): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - } -}); -</script> - -<style lang="stylus" module> -.root[data-melt] - .stream - padding 0 - - .img - border solid 4px transparent - border-radius 8px - -.stream - 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 var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue deleted file mode 100644 index 6680a114356598b38c7687c4fadce19203969393..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/post-form.vue +++ /dev/null @@ -1,294 +0,0 @@ -<template> -<div> - <ui-container :show-header="props.design == 0"> - <template #header><fa icon="pencil-alt"/>{{ $t('title') }}</template> - - <div class="lhcuptdmcdkfwmipgazeawoiuxpzaclc-body" - @dragover.stop="onDragover" - @drop.stop="onDrop" - > - <div class="textarea"> - <textarea - :disabled="posting" - v-model="text" - @keydown="onKeydown" - @paste="onPaste" - :placeholder="placeholder" - ref="text" - v-autocomplete="{ model: 'text' }" - ></textarea> - <button class="emoji" @click="emoji" ref="emoji" v-if="!$root.isMobile"> - <fa :icon="['far', 'laugh']"/> - </button> - </div> - <x-post-form-attaches class="files" :files="files" :detach-media-fn="detachMedia"/> - <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/> - <mk-uploader ref="uploader" @uploaded="attachMedia"/> - <footer> - <button @click="chooseFile"><fa icon="upload"/></button> - <button @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button @click="post" :disabled="posting" class="post">{{ $t('note') }}</button> - </footer> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import insertTextAtCursor from 'insert-text-at-cursor'; -import { formatTimeString } from '../../../../../misc/format-time-string'; - -export default define({ - name: 'post-form', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('desktop/views/widgets/post-form.vue'), - - components: { - XPostFormAttaches: () => import('../components/post-form-attaches.vue').then(m => m.default) - }, - - data() { - return { - posting: false, - text: '', - files: [], - }; - }, - - computed: { - placeholder(): string { - const xs = [ - this.$t('@.note-placeholders.a'), - this.$t('@.note-placeholders.b'), - this.$t('@.note-placeholders.c'), - this.$t('@.note-placeholders.d'), - this.$t('@.note-placeholders.e'), - this.$t('@.note-placeholders.f') - ]; - return xs[Math.floor(Math.random() * xs.length)]; - } - }, - - methods: { - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - - chooseFile() { - (this.$refs.file as any).click(); - }, - - chooseFileFromDrive() { - this.$chooseDriveFile({ - multiple: true - }).then(files => { - for (const x of files) this.attachMedia(x); - }); - }, - - attachMedia(driveFile) { - this.files.push(driveFile); - this.$emit('change-attached-files', this.files); - }, - - detachMedia(id) { - this.files = this.files.filter(x => x.id != id); - this.$emit('change-attached-files', this.files); - }, - - onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post(); - }, - - async onPaste(e: ClipboardEvent) { - for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { - if (item.kind == 'file') { - const file = item.getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - const name = this.$store.state.settings.pasteDialog - ? await this.$root.dialog({ - title: this.$t('@.post-form.enter-file-name'), - input: { - default: formatted - }, - allowEmpty: false - }).then(({ canceled, result }) => canceled ? false : result) - : formatted; - if (name) this.upload(file, name); - } - } - }, - - onChangeFile() { - for (const x of Array.from((this.$refs.file as any).files)) this.upload(x); - }, - - upload(file: File, name?: string) { - (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name); - }, - - 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 > 0) { - e.preventDefault(); - for (const x of Array.from(e.dataTransfer.files)) this.upload(x); - return; - } - - //#region ドライブã®ãƒ•ã‚¡ã‚¤ãƒ« - const driveFile = e.dataTransfer.getData('mk_drive_file'); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.files.push(file); - e.preventDefault(); - } - //#endregion - }, - - async emoji() { - const Picker = await import('../../../desktop/views/components/emoji-picker-dialog.vue').then(m => m.default); - const button = this.$refs.emoji; - const rect = button.getBoundingClientRect(); - const vm = this.$root.new(Picker, { - x: button.offsetWidth + rect.left + window.pageXOffset, - y: rect.top + window.pageYOffset - }); - vm.$once('chosen', emoji => { - insertTextAtCursor(this.$refs.text, emoji); - }); - }, - - post() { - this.posting = true; - - let visibility = 'public'; - let localOnly = false; - - const m = this.$store.state.settings.defaultNoteVisibility.match(/^local-(.+)/); - if (m) { - visibility = m[1]; - localOnly = true; - } else { - visibility = this.$store.state.settings.defaultNoteVisibility; - } - - this.$root.api('notes/create', { - text: this.text == '' ? undefined : this.text, - fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, - visibility, - localOnly, - }).then(data => { - this.clear(); - }).catch(err => { - this.$root.dialog({ - type: 'error', - text: this.$t('something-happened') - }); - }).then(() => { - this.posting = false; - this.$nextTick(() => { - this.$refs.text.focus(); - }); - }); - }, - - clear() { - this.text = ''; - this.files = []; - } - } -}); -</script> - -<style lang="stylus" scoped> -.lhcuptdmcdkfwmipgazeawoiuxpzaclc-body - > .textarea - > .emoji - position absolute - top 0 - right 0 - padding 10px - font-size 18px - color var(--text) - opacity 0.5 - - &:hover - color var(--textHighlighted) - opacity 1 - - &:active - color var(--primary) - opacity 1 - - > textarea - display block - width 100% - max-width 100% - min-width 100% - padding 16px - color var(--desktopPostFormTextareaFg) - outline none - background var(--desktopPostFormTextareaBg) - border none - border-bottom solid 1px var(--faceDivider) - padding-right 30px - - &:focus - & + .emoji - opacity 0.7 - - > input[type=file] - display none - - > footer - display flex - padding 8px - - > button:not(.post) - color var(--text) - - &:hover - color var(--textHighlighted) - - > .post - display block - margin 0 0 0 auto - padding 0 10px - height 28px - color var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - -</style> diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue deleted file mode 100644 index 64c3b51540c9b7805de8b33abaa918b6f9b3d763..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/posts-monitor.vue +++ /dev/null @@ -1,203 +0,0 @@ -<template> -<div class="mkw-posts-monitor"> - <ui-container :show-header="props.design == 0" :naked="props.design == 2"> - <template #header><fa icon="chart-line"/>{{ $t('title') }}</template> - <template #func><button @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button></template> - - <div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2"> - <defs> - <linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop> - </linearGradient> - <mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="localPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="localPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - <circle - :cx="localHeadX" - :cy="localHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/> - <text x="1" y="5">Local</text> - </svg> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1"> - <defs> - <linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop> - <stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop> - </linearGradient> - <mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> - <polygon - :points="fediPolygonPoints" - fill="#fff" - fill-opacity="0.5"/> - <polyline - :points="fediPolylinePoints" - fill="none" - stroke="#fff" - stroke-width="1"/> - <circle - :cx="fediHeadX" - :cy="fediHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/> - <text x="1" y="5">Fedi</text> - </svg> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import { v4 as uuid } from 'uuid'; - -export default define({ - name: 'posts-monitor', - props: () => ({ - design: 0, - view: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/posts-monitor.vue'), - - data() { - return { - connection: null, - viewBoxY: 30, - stats: [], - fediGradientId: uuid(), - fediMaskId: uuid(), - localGradientId: uuid(), - localMaskId: uuid(), - fediPolylinePoints: '', - localPolylinePoints: '', - fediPolygonPoints: '', - localPolygonPoints: '', - fediHeadX: null, - fediHeadY: null, - localHeadX: null, - localHeadY: null - }; - }, - computed: { - viewBoxX(): number { - return this.props.view == 0 ? 50 : 100; - } - }, - watch: { - viewBoxX() { - this.draw(); - } - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('notesStats'); - - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog',{ - id: Math.random().toString().substr(2, 8) - }); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - toggle() { - if (this.props.view == 2) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - }, - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - draw() { - const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats; - const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1; - const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1; - - const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]); - const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]); - this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0]; - this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1]; - this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0]; - this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1]; - }, - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 100) this.stats.shift(); - this.draw(); - }, - onStatsLog(statsLog) { - for (const stats of statsLog) this.onStats(stats); - } - } -}); -</script> - -<style lang="stylus" scoped> -.qpdmibaztplkylerhdbllwcokyrfxeyj - &.dual - > svg - width 50% - float left - - &:first-child - padding-right 5px - - &:last-child - padding-left 5px - - > svg - display block - padding 10px - width 100% - - > text - font-size 5px - fill var(--chartCaption) - - > tspan - opacity 0.5 - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/queue.vue b/src/client/app/common/views/widgets/queue.vue deleted file mode 100644 index 6e49f1efb0373395c645941a7f3f3333c659c0ee..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/queue.vue +++ /dev/null @@ -1,173 +0,0 @@ -<template> -<div> - <ui-container :show-header="!props.compact"> - <template #header><fa :icon="faTasks"/>Queue</template> - - <div class="mntrproz"> - <div> - <b>In</b> - <span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span> - <div ref="in"></div> - </div> - <div> - <b>Out</b> - <span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span> - <div ref="out"></div> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../define-widget'; -import { faTasks } from '@fortawesome/free-solid-svg-icons'; -import ApexCharts from 'apexcharts'; - -export default define({ - name: 'queue', - props: () => ({ - compact: false - }) -}).extend({ - data() { - return { - stats: [], - inChart: null, - outChart: null, - faTasks - }; - }, - - watch: { - stats(stats) { - this.inChart.updateSeries([{ - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) - }, { - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.inbox.active })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) - }]); - this.outChart.updateSeries([{ - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) - }, { - type: 'area', - data: stats.map((x, i) => ({ x: i, y: x.deliver.active })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting })) - }, { - type: 'line', - data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) - }]); - } - }, - - computed: { - latestStats(): any { - return this.stats[this.stats.length - 1]; - } - }, - - mounted() { - const chartOpts = { - chart: { - type: 'area', - height: 70, - animations: { - dynamicAnimation: { - enabled: false - } - }, - sparkline: { - enabled: true, - } - }, - dataLabels: { - enabled: false - }, - tooltip: { - enabled: false - }, - stroke: { - curve: 'straight', - width: 1 - }, - colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], - series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any, - yaxis: { - min: 0, - } - }; - - this.inChart = new ApexCharts(this.$refs.in, chartOpts); - this.outChart = new ApexCharts(this.$refs.out, chartOpts); - - this.inChart.render(); - this.outChart.render(); - - const connection = this.$root.stream.useSharedConnection('queueStats'); - connection.on('stats', this.onStats); - connection.on('statsLog', this.onStatsLog); - connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 50 - }); - - this.$once('hook:beforeDestroy', () => { - connection.dispose(); - this.inChart.destroy(); - this.outChart.destroy(); - }); - }, - - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - }, - - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { - this.onStats(stats); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mntrproz - display flex - padding 4px - - > div - width 50% - padding 4px - - > b - display block - font-size 12px - color var(--text) - - > span - position absolute - top 4px - right 4px - opacity 0.7 - font-size 12px - color var(--text) - -</style> diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue deleted file mode 100644 index c1a66bfebb4482ca7570224ecaeba739c9f81c32..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/rss.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> -<div class="mkw-rss"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="rss-square"/>RSS</template> - <template #func><button title="è¨å®š" @click="setting"><fa icon="cog"/></button></template> - - <div class="mkw-rss--body" :data-mobile="platform == 'mobile'"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div class="feed" v-else> - <a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> - </div> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'rss', - props: () => ({ - compact: false, - url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews' - }) -}).extend({ - i18n: i18n(), - data() { - return { - 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; - this.save(); - }, - fetch() { - fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { - }).then(res => { - res.json().then(feed => { - this.items = feed.items; - this.fetching = false; - }); - }); - }, - setting() { - this.$root.dialog({ - title: 'URL', - input: { - type: 'url', - default: this.props.url - } - }).then(({ canceled, result: url }) => { - if (canceled) return; - this.props.url = url; - this.save(); - this.fetch(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-rss - .mkw-rss--body - .feed - padding 12px 16px - font-size 0.9em - - > a - display block - padding 4px 0 - color var(--text) - border-bottom dashed var(--lineWidth) var(--faceDivider) - white-space nowrap - text-overflow ellipsis - overflow hidden - - &:last-child - border-bottom none - - .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - - &[data-mobile] - background var(--face) - - .feed - padding 0 - - > a - padding 8px 16px - border-bottom none - - &:nth-child(even) - background rgba(#000, 0.05) - -</style> diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue deleted file mode 100644 index 799773a70caf0cdef369ec2e21a68d9b3055b22e..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/server.cpu-memory.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div class="cpu-memory"> - <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <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"/> - <circle - :cx="cpuHeadX" - :cy="cpuHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :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 }`"> - <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"/> - <circle - :cx="memHeadX" - :cy="memHeadY" - r="1.5" - fill="#fff"/> - </mask> - </defs> - <rect - x="-2" y="-2" - :width="viewBoxX + 4" :height="viewBoxY + 4" - :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 { v4 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: '', - cpuHeadX: null, - cpuHeadY: null, - memHeadX: null, - memHeadY: null, - cpuP: '', - memP: '' - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8) - }); - }, - beforeDestroy() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - }, - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); - - const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]); - const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]); - this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - - this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - - this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0]; - this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1]; - this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0]; - this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1]; - - this.cpuP = (stats.cpu_usage * 100).toFixed(0); - this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); - }, - onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) this.onStats(stats); - } - } -}); -</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 var(--chartCaption) - - > tspan - opacity 0.5 - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue deleted file mode 100644 index c08971e11c216cf94def0f5bb7a2bef5560317f8..0000000000000000000000000000000000000000 --- a/src/client/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 icon="microchip"/>CPU</p> - <p>{{ meta.cpu.cores }} Logical 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 var(--chartCaption) - - &:first-child - font-weight bold - - > [data-icon] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue deleted file mode 100644 index 039c4f5c2935ec1c30fd3dee8f91f731a24e1c1b..0000000000000000000000000000000000000000 --- a/src/client/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 :icon="['far', 'hdd']"/>Storage</p> - <p>Total: {{ total | bytes(1) }}</p> - <p>Free: {{ 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 var(--chartCaption) - - &:first-child - font-weight bold - - > [data-icon] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue deleted file mode 100644 index c6e0d68b113312c92cd9c6c26525b589efc1b6a5..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/server.info.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<div class="info"> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - <p>Machine: {{ meta.machine }}</p> - <p>Node: {{ meta.node }}</p> - <p>PSQL: {{ meta.psql }}</p> - <p>Redis: {{ meta.redis }}</p> - <p>Version: {{ meta.version }} </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 var(--text) -</style> diff --git a/src/client/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue deleted file mode 100644 index c3b2f3a1011e67d3600c60095ee6d31a16f89a5d..0000000000000000000000000000000000000000 --- a/src/client/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 icon="memory"/>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.free = stats.mem.total - stats.mem.used; - 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 var(--chartCaption) - - &:first-child - font-weight bold - - > [data-icon] - margin-right 4px - - &:after - content "" - display block - clear both - -</style> diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue deleted file mode 100644 index ce342fd41ba993929b727d54f8f6955a9aa8781d..0000000000000000000000000000000000000000 --- a/src/client/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 var(--chartCaption) - -</style> diff --git a/src/client/app/common/views/widgets/server.uptimes.vue b/src/client/app/common/views/widgets/server.uptimes.vue deleted file mode 100644 index 0da5c4ec500487e08928384d1d25d3b3b6b8f8b4..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/server.uptimes.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="uptimes"> - <p>Uptimes</p> - <p>Process: {{ process }}</p> - <p>OS: {{ os }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import formatUptime from '../../scripts/format-uptime'; - -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 = formatUptime(stats.process_uptime); - this.os = formatUptime(stats.os_uptime); - } - } -}); -</script> - -<style lang="stylus" scoped> -.uptimes - padding 10px 14px - - > p - margin 0 - font-size 12px - color var(--text) - - &:first-child - font-weight bold -</style> diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue deleted file mode 100644 index 90a0f0171b42ce52beac7bba7257f113e2a9c533..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/server.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> -<div class="mkw-server"> - <ui-container :show-header="props.design == 0" :naked="props.design == 2"> - <template #header><fa icon="server"/>{{ $t('title') }}</template> - <template #func><button @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button></template> - - <p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.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> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -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({ - i18n: i18n('common/views/widgets/server.vue'), - - components: { - XCpuMemory, - XCpu, - XMemory, - XDisk, - XUptimes, - XInfo - }, - data() { - return { - fetching: true, - meta: null, - connection: null - }; - }, - mounted() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.fetching = false; - }); - - this.connection = this.$root.stream.useSharedConnection('serverStats'); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - toggle() { - if (this.props.view == 5) { - this.props.view = 0; - } else { - this.props.view++; - } - this.save(); - }, - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue deleted file mode 100644 index 23ccb9da6b221b7a58f45d0be477229287097a0f..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/slideshow.vue +++ /dev/null @@ -1,165 +0,0 @@ -<template> -<div class="mkw-slideshow" :data-mobile="platform == 'mobile'"> - <div @click="choose"> - <p v-if="props.folder === undefined"> - <template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template> - <template v-else>{{ $t('folder') }}</template> - </p> - <p v-if="props.folder !== undefined && images.length == 0 && !fetching">{{ $t('no-image') }}</p> - <div ref="slideA" class="slide a"></div> - <div ref="slideB" class="slide b"></div> - </div> -</div> -</template> - -<script lang="ts"> -import anime from 'animejs'; -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'slideshow', - props: () => ({ - folder: undefined, - size: 0 - }) -}).extend({ - i18n: i18n('common/views/widgets/slideshow.vue'), - - 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.save(); - - 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 })`; - - (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.$root.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.$chooseDriveFolder().then(folder => { - this.props.folder = folder ? folder.id : null; - this.save(); - this.fetch(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-slideshow - overflow hidden - background #fff - border solid 1px rgba(#000, 0.075) - border-radius 6px - - &[data-mobile] - border none - border-radius 8px - box-shadow 0 0 0 1px rgba(#000, 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/client/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue deleted file mode 100644 index 9e047ef47c4c87d29d5cb439301b7ff15668c692..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/tips.vue +++ /dev/null @@ -1,109 +0,0 @@ -<template> -<div class="mkw-tips"> - <p ref="tip"><fa :icon="['far', 'lightbulb']"/><span v-html="tip"></span></p> -</div> -</template> - -<script lang="ts"> -import anime from 'animejs'; -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'tips' -}).extend({ - i18n: i18n('common/views/widgets/tips.vue'), - - data() { - return { - tips: [], - tip: null, - clock: null - }; - }, - created() { - this.tips = [ - this.$t('tips-line1'), - this.$t('tips-line2'), - this.$t('tips-line3'), - this.$t('tips-line4'), - this.$t('tips-line5'), - this.$t('tips-line6'), - this.$t('tips-line7'), - this.$t('tips-line8'), - this.$t('tips-line9'), - this.$t('tips-line10'), - this.$t('tips-line11'), - this.$t('tips-line13'), - this.$t('tips-line14'), - this.$t('tips-line17'), - this.$t('tips-line19'), - this.$t('tips-line20'), - this.$t('tips-line21'), - this.$t('tips-line23'), - this.$t('tips-line24'), - this.$t('tips-line25') - ]; - }, - mounted() { - this.$nextTick(() => { - this.set(); - }); - - this.clock = setInterval(this.change, 20000); - }, - beforeDestroy() { - clearInterval(this.clock); - }, - methods: { - set() { - this.tip = this.tips[Math.floor(Math.random() * this.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 - opacity 0.8 - - > p - display block - margin 0 - padding 0 12px - text-align center - font-size 0.7em - color var(--text) - - > [data-icon] - margin-right 4px - - kbd - display inline - padding 0 6px - margin 0 2px - font-size 1em - font-family inherit - border solid 1px var(--text) - border-radius 2px - -</style> diff --git a/src/client/app/common/views/widgets/version.vue b/src/client/app/common/views/widgets/version.vue deleted file mode 100644 index e8f6c08f343ffca5cc1153c629bf84189c2f4e1e..0000000000000000000000000000000000000000 --- a/src/client/app/common/views/widgets/version.vue +++ /dev/null @@ -1,30 +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 var(--text) - opacity 0.8 - -</style> diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts deleted file mode 100644 index 6b88b51ef1a37630dc87db2d97a3b83d44cdd1f0..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/api/update-avatar.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { apiUrl, locale } from '../../config'; -import ProgressDialog from '../views/components/progress-dialog.vue'; - -export default ($root: any) => { - - const cropImage = file => new Promise(async (resolve, reject) => { - const CropWindow = await import('../views/components/crop-window.vue').then(x => x.default); - const w = $root.new(CropWindow, { - image: file, - title: locale['desktop']['avatar-crop-title'], - aspectRatio: 1 / 1 - }); - - w.$once('cropped', blob => { - const data = new FormData(); - data.append('i', $root.$store.state.i.token); - data.append('file', blob, file.name + '.cropped.png'); - - $root.api('drive/folders/find', { - name: locale['desktop']['avatar'] - }).then(avatarFolder => { - if (avatarFolder.length === 0) { - $root.api('drive/folders/create', { - name: locale['desktop']['avatar'] - }).then(iconFolder => { - resolve(upload(data, iconFolder)); - }); - } else { - resolve(upload(data, avatarFolder[0])); - } - }); - }); - - w.$once('skipped', () => { - resolve(file); - }); - - w.$once('cancelled', reject); - - document.body.appendChild(w.$el); - }); - - const upload = (data, folder) => new Promise((resolve, reject) => { - const dialog = $root.new(ProgressDialog, { - title: locale['desktop']['uploading-avatar'] - }); - 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(); - resolve(file); - }; - xhr.onerror = reject; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); - }; - - xhr.send(data); - }); - - const setAvatar = file => { - return $root.api('i/update', { - avatarId: file.id - }).then(i => { - $root.$store.commit('updateIKeyValue', { - key: 'avatarId', - value: i.avatarId - }); - $root.$store.commit('updateIKeyValue', { - key: 'avatarUrl', - value: i.avatarUrl - }); - - $root.dialog({ - title: locale['desktop']['avatar-updated'], - text: null - }); - - return i; - }).catch(err => { - switch (err.id) { - case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191': - $root.dialog({ - type: 'error', - title: locale['desktop']['unable-to-process'], - text: locale['desktop']['invalid-filetype'] - }); - break; - default: - $root.dialog({ - type: 'error', - text: locale['desktop']['unable-to-process'] - }); - } - }); - }; - - return (file = null) => { - const selectedFile = file - ? Promise.resolve(file) - : $root.$chooseDriveFile({ - multiple: false, - type: 'image/*', - title: locale['desktop']['choose-avatar'] - }); - - return selectedFile - .then(cropImage) - .then(setAvatar) - .catch(err => err && console.warn(err)); - }; -}; diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts deleted file mode 100644 index 09632b19418fcbaa3eafa04a612f053a3023de08..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/api/update-banner.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { apiUrl, locale } from '../../config'; -import ProgressDialog from '../views/components/progress-dialog.vue'; - -export default ($root: any) => { - - const cropImage = file => new Promise(async (resolve, reject) => { - const CropWindow = await import('../views/components/crop-window.vue').then(x => x.default); - const w = $root.new(CropWindow, { - image: file, - title: locale['desktop']['banner-crop-title'], - aspectRatio: 16 / 9 - }); - - w.$once('cropped', blob => { - const data = new FormData(); - data.append('i', $root.$store.state.i.token); - data.append('file', blob, file.name + '.cropped.png'); - - $root.api('drive/folders/find', { - name: locale['desktop']['banner'] - }).then(bannerFolder => { - if (bannerFolder.length === 0) { - $root.api('drive/folders/create', { - name: locale['desktop']['banner'] - }).then(iconFolder => { - resolve(upload(data, iconFolder)); - }); - } else { - resolve(upload(data, bannerFolder[0])); - } - }); - }); - - w.$once('skipped', () => { - resolve(file); - }); - - w.$once('cancelled', reject); - - document.body.appendChild(w.$el); - }); - - const upload = (data, folder) => new Promise((resolve, reject) => { - const dialog = $root.new(ProgressDialog, { - title: locale['desktop']['uploading-banner'] - }); - 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(); - resolve(file); - }; - xhr.onerror = reject; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); - }; - - xhr.send(data); - }); - - const setBanner = file => { - return $root.api('i/update', { - bannerId: file.id - }).then(i => { - $root.$store.commit('updateIKeyValue', { - key: 'bannerId', - value: i.bannerId - }); - $root.$store.commit('updateIKeyValue', { - key: 'bannerUrl', - value: i.bannerUrl - }); - - $root.dialog({ - title: locale['desktop']['banner-updated'], - text: null - }); - - return i; - }).catch(err => { - switch (err.id) { - case '75aedb19-2afd-4e6d-87fc-67941256fa60': - $root.dialog({ - type: 'error', - title: locale['desktop']['unable-to-process'], - text: locale['desktop']['invalid-filetype'] - }); - break; - default: - $root.dialog({ - type: 'error', - text: locale['desktop']['unable-to-process'] - }); - } - }); - }; - - return (file = null) => { - const selectedFile = file - ? Promise.resolve(file) - : $root.$chooseDriveFile({ - multiple: false, - type: 'image/*', - title: locale['desktop']['choose-banner'] - }); - - return selectedFile - .then(cropImage) - .then(setBanner) - .catch(err => err && console.warn(err)); - }; -}; diff --git a/src/client/app/desktop/assets/grid.svg b/src/client/app/desktop/assets/grid.svg deleted file mode 100644 index d1d72cd8cecde5c008a728c168ae67e3f73f4893..0000000000000000000000000000000000000000 Binary files a/src/client/app/desktop/assets/grid.svg and /dev/null differ diff --git a/src/client/app/desktop/assets/header-icon.svg b/src/client/app/desktop/assets/header-icon.svg deleted file mode 100644 index d677d2d16304709f93cc3d9be631cf686d724697..0000000000000000000000000000000000000000 Binary files a/src/client/app/desktop/assets/header-icon.svg and /dev/null differ diff --git a/src/client/app/desktop/assets/index.jpg b/src/client/app/desktop/assets/index.jpg deleted file mode 100644 index c054188159b19f4f96069d08af37576e97af19c6..0000000000000000000000000000000000000000 Binary files a/src/client/app/desktop/assets/index.jpg and /dev/null differ diff --git a/src/client/app/desktop/assets/remove.png b/src/client/app/desktop/assets/remove.png deleted file mode 100644 index c2e222a0fc7b983c77bc87fe36b1f90a848dfba8..0000000000000000000000000000000000000000 Binary files a/src/client/app/desktop/assets/remove.png and /dev/null differ diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts deleted file mode 100644 index 914e162c9a39888623dea43395c60b23dd3c6e1c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/script.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Desktop Client - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; -import composeNotification from '../common/scripts/compose-notification'; - -import MkHome from './views/home/home.vue'; -import MkSelectDrive from './views/pages/selectdrive.vue'; -import MkDrive from './views/pages/drive.vue'; -import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkReversi from './views/pages/games/reversi.vue'; -import MkShare from '../common/views/pages/share.vue'; -import MkFollow from '../common/views/pages/follow.vue'; -import MkNotFound from '../common/views/pages/not-found.vue'; -import MkSettings from './views/pages/settings.vue'; -import DeckColumn from '../common/views/deck/deck.column-template.vue'; - -import Ctx from './views/components/context-menu.vue'; -import RenoteFormWindow from './views/components/renote-form-window.vue'; -import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue'; -import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue'; -import MkHomeTimeline from './views/home/timeline.vue'; -import Notification from './views/components/ui-notification.vue'; - -import { url } from '../config'; -import MiOS from '../mios'; - -/** - * init - */ -init(async (launch, os) => { - Vue.mixin({ - methods: { - $contextmenu(e, menu, opts?) { - const o = opts || {}; - const vm = this.$root.new(Ctx, { - menu, - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset, - }); - vm.$once('closed', () => { - if (o.closed) o.closed(); - }); - }, - - $post(opts) { - const o = opts || {}; - if (o.renote) { - const vm = this.$root.new(RenoteFormWindow, { - note: o.renote, - animation: o.animation == null ? true : o.animation - }); - if (o.cb) vm.$once('closed', o.cb); - } else { - this.$root.newAsync(() => import('./views/components/post-form-window.vue').then(m => m.default), { - reply: o.reply, - mention: o.mention, - animation: o.animation == null ? true : o.animation, - initialText: o.initialText, - instant: o.instant, - initialNote: o.initialNote, - }).then(vm => { - if (o.cb) vm.$once('closed', o.cb); - }); - } - }, - - $chooseDriveFile(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - - if (document.body.clientWidth > 800) { - const w = this.$root.new(MkChooseFileFromDriveWindow, { - title: o.title, - type: o.type, - multiple: o.multiple, - initFolder: o.currentFolder - }); - w.$once('selected', file => { - res(file); - }); - } else { - window['cb'] = file => { - res(file); - }; - - window.open(url + `/selectdrive?multiple=${o.multiple}`, - 'choose_drive_window', - 'height=500, width=800'); - } - }); - }, - - $chooseDriveFolder(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const w = this.$root.new(MkChooseFolderFromDriveWindow, { - title: o.title, - initFolder: o.currentFolder - }); - w.$once('selected', folder => { - res(folder); - }); - }); - }, - - $notify(message) { - this.$root.new(Notification, { - message - }); - } - } - }); - - // Register directives - require('./views/directives'); - - // Register components - require('./views/components'); - require('./views/widgets'); - - // Init router - const router = new VueRouter({ - mode: 'history', - routes: [ - os.store.state.device.inDeckMode - ? { path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ - { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, - { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, - { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, - { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) }, - ]} - : { path: '/', component: MkHome, children: [ - { path: '', name: 'index', component: MkHomeTimeline }, - { path: '/@:user', component: () => import('./views/home/user/index.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('./views/home/user/user.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) }, - { path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: () => import('../common/views/pages/featured.vue').then(m => m.default), props: { platform: 'desktop' } }, - { path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, - { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, - { path: '/i/favorites', component: () => import('../common/views/pages/favorites.vue').then(m => m.default), props: { platform: 'desktop' } }, - { path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, - { path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }, - { path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) }, - { path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }, - { path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) }, - { path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }, - { path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }, - { path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/@:user/pages/:page', component: () => import('../common/views/pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, - ]}, - { path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }, - { path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/i/messaging/group/:group', component: MkMessagingRoom }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/i/settings', redirect: '/i/settings/profile' }, - { path: '/i/settings/:page', component: MkSettings }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) }, - { path: '/share', component: MkShare }, - { path: '/games/reversi/:game?', component: MkReversi }, - { path: '/authorize-follow', component: MkFollow }, - { path: '/deck', redirect: '/' }, - { path: '*', component: MkNotFound } - ], - scrollBehavior(to, from, savedPosition) { - return { x: 0, y: 0 }; - } - }); - - // Launch the app - const [app, _] = launch(router); - - /** - * Init Notification - */ - if ('Notification' in window && os.store.getters.isSignedIn) { - // 許å¯ã‚’å¾—ã¦ã„ãªã‹ã£ãŸã‚‰ãƒªã‚¯ã‚¨ã‚¹ãƒˆ - if ((Notification as any).permission == 'default') { - await Notification.requestPermission(); - } - - if ((Notification as any).permission == 'granted') { - registerNotifications(os); - } - } -}, true); - -function registerNotifications(os: MiOS) { - const stream = os.stream; - - if (stream == null) return; - - const connection = stream.useSharedConnection('main'); - - connection.on('notification', notification => { - const _n = composeNotification('notification', notification); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 6000); - }); - - connection.on('driveFileCreated', file => { - const _n = composeNotification('driveFileCreated', file); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - setTimeout(n.close.bind(n), 5000); - }); - - connection.on('unreadMessagingMessage', message => { - const _n = composeNotification('unreadMessagingMessage', 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('reversiInvited', matching => { - const _n = composeNotification('reversiInvited', matching); - const n = new Notification(_n.title, { - body: _n.body, - icon: _n.icon - }); - }); -} diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl deleted file mode 100644 index 249d3db2ed2551f2f40498ed8ab0b3bd7f11fbf2..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/style.styl +++ /dev/null @@ -1,40 +0,0 @@ -@import "../app" -@import "../reset" - -*::input-placeholder - color #D8CBC5 - -*:focus - outline none - -html - height 100% - background var(--bg) - - &, div, textarea - scrollbar-width thin - - &, * - scrollbar-color var(--scrollbarHandle) var(--scrollbarTrack) - - &:hover - scrollbar-color var(--scrollbarHandleHover) var(--scrollbarTrack) - - &:active - scrollbar-color var(--primary) var(--scrollbarTrack) - - &::-webkit-scrollbar - width 6px - height 6px - - &::-webkit-scrollbar-track - background var(--scrollbarTrack) - - &::-webkit-scrollbar-thumb - background var(--scrollbarHandle) - - &:hover - background var(--scrollbarHandleHover) - - &:active - background var(--primary) diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue deleted file mode 100644 index da74a97f68afbfbbad209c66e9855d3a915fdbc9..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/activity.calendar.vue +++ /dev/null @@ -1,80 +0,0 @@ -<template> -<svg viewBox="0 0 21 7"> - <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 + 1 }}/{{ 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[0].x" :y="data[0].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() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - - let x = 20; - this.data.slice().forEach((d, i) => { - d.x = x; - - const date = new Date(year, month, day - i); - d.date = { - year: date.getFullYear(), - month: date.getMonth(), - day: date.getDate(), - weekday: date.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 == 0) x--; - }); - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - - > rect - transform-origin center - - &.day - &:hover - fill rgba(#000, 0.05) - -</style> diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue deleted file mode 100644 index 648b64a3fe21f692bd2ab988df57876097b5cbd9..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/activity.chart.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown"> - <title>{{ $t('total') }}<br/>{{ $t('notes') }}<br/>{{ $t('replies') }}<br/>{{ $t('renotes') }}</title> - <polyline - :points="pointsNote" - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - :points="pointsReply" - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - :points="pointsRenote" - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - :points="pointsTotal" - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -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({ - i18n: i18n('desktop/views/components/activity.chart.vue'), - props: ['data'], - data() { - return { - viewBoxX: 147, - viewBoxY: 60, - zoom: 1, - pos: 0, - pointsNote: null, - pointsReply: null, - pointsRenote: null, - pointsTotal: null - }; - }, - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - - this.render(); - }, - methods: { - render() { - const peak = Math.max.apply(null, this.data.map(d => d.total)); - if (peak != 0) { - const data = this.data.slice().reverse(); - this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '); - this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = 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/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue deleted file mode 100644 index 2cac125041322a95dd3a705fc40b3142f890cb1c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/activity.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="mk-activity"> - <ui-container :show-header="design == 0" :naked="design == 2"> - <template #header><fa icon="chart-bar"/>{{ $t('title') }}</template> - <template #func><button :title="$t('toggle')" @click="toggle"><fa icon="sort"/></button></template> - - <p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.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> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XCalendar from './activity.calendar.vue'; -import XChart from './activity.chart.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/activity.vue'), - 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.$root.api('charts/user/notes', { - userId: this.user.id, - span: 'day', - limit: 7 * 21 - }).then(activity => { - this.activity = activity.diffs.normal.map((_, i) => ({ - total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i], - notes: activity.diffs.normal[i], - replies: activity.diffs.reply[i], - renotes: activity.diffs.renote[i] - })); - 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" module> -.fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue deleted file mode 100644 index cdeac51638d61acfdff925e308b32d58aa4a9378..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/calendar.vue +++ /dev/null @@ -1,252 +0,0 @@ -<template> -<div class="mk-calendar" :data-melt="design == 4 || design == 5" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <template v-if="design == 0 || design == 1"> - <button @click="prev" :title="$t('prev')"><fa icon="chevron-circle-left"/></button> - <p class="title">{{ $t('title', { year, month }) }}</p> - <button @click="next" :title="$t('next')"><fa icon="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-weekend="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-weekend="isWeekend(i + 1)" - @click="go(i + 1)" - :title="isOutOfRange(i + 1) ? null : $t('go')" - > - <div>{{ i + 1 }}</div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - -function isLeapYear(year) { - return !(year & (year % 25 ? 3 : 15)); -} - -export default Vue.extend({ - i18n: i18n('desktop/views/components/calendar.vue'), - 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: [ - this.$t('@.weekday-short.sunday'), - this.$t('@.weekday-short.monday'), - this.$t('@.weekday-short.tuesday'), - this.$t('@.weekday-short.wednesday'), - this.$t('@.weekday-short.thursday'), - this.$t('@.weekday-short.friday'), - this.$t('@.weekday-short.saturday') - ] - }; - }, - 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); - }, - - isWeekend(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> -.mk-calendar - color var(--calendarDay) - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - &[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 var(--faceHeaderText) - background var(--faceHeader) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - > [data-icon] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - &: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 var(--calendarWeek) - - &[data-is-weekend] - color var(--calendarSaturdayOrSunday) - - &[data-today] - box-shadow 0 0 0 var(--lineWidth) var(--calendarWeek) inset - border-radius 6px - - &[data-is-weekend] - box-shadow 0 0 0 var(--lineWidth) var(--calendarSaturdayOrSunday) inset - - &.day - cursor pointer - color var(--calendarDay) - - > div - border-radius 6px - - &:hover > div - background var(--faceClearButtonHover) - - &:active > div - background var(--faceClearButtonActive) - - &[data-is-weekend] - color var(--calendarSaturdayOrSunday) - - &[data-is-out-of-range] - cursor default - opacity 0.5 - - &[data-selected] - font-weight bold - - > div - background var(--faceClearButtonHover) - - &:active > div - background var(--faceClearButtonActive) - - &[data-today] - > div - color var(--primaryForeground) - background var(--primary) - - &:hover > div - background var(--primaryLighten10) - - &:active > div - background var(--primaryDarken10) - -</style> diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue deleted file mode 100644 index 71c430edeb87893deda02da0be903aa6d8b7e17e..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> - <template #header> - <span class="jqiaciqv"> - <span class="title">{{ $t('choose-prompt') }}</span> - <span class="count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span> - </span> - </template> - - <div class="rqsvbumu"> - <x-drive - ref="browser" - class="browser" - :type="type" - :multiple="multiple" - @selected="onSelected" - @change-selection="onChangeSelection" - /> - <div class="footer"> - <button class="upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button> - <ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button> - <ui-button inline primary :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</ui-button> - </div> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('desktop/views/components/choose-file-from-drive-window.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: { - type: { - type: String, - required: false, - default: undefined - }, - multiple: { - default: false - } - }, - 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" scoped> -.jqiaciqv - .title - > [data-icon] - margin-right 4px - - .count - margin-left 8px - opacity 0.7 - -.rqsvbumu - display flex - flex-direction column - height 100% - - .browser - flex 1 - overflow auto - - .footer - padding 16px - background var(--desktopPostFormBg) - text-align right - - .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 var(--primaryAlpha05) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color var(--primaryAlpha03) - - &:active - color var(--primaryAlpha06) - background transparent - border-color var(--primaryAlpha05) - //box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - -</style> diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue deleted file mode 100644 index fe764365446707026c3a5b96e1dd0f1e76cae550..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> - <template #header> - <span>{{ $t('choose-prompt') }}</span> - </template> - - <div class="hllkpxxu"> - <x-drive - ref="browser" - class="browser" - :multiple="false" - /> - <div class="footer"> - <ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button> - <ui-button inline @click="ok" primary>{{ $t('ok') }}</ui-button> - </div> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('desktop/views/components/choose-folder-from-drive-window.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - 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" scoped> -.hllkpxxu - display flex - flex-direction column - height 100% - - .browser - flex 1 - overflow auto - - .footer - padding 16px - background var(--desktopPostFormBg) - text-align right - -</style> diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue deleted file mode 100644 index f2bb3bec23d7134834ac0a1479540ac8b178cc56..0000000000000000000000000000000000000000 --- a/src/client/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 ? item.type : item === null ? 'divider' : null"> - <template v-if="item"> - <template v-if="item.type == null || item.type == 'item'"> - <p @click="click(item)"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</p> - </template> - <template v-else-if="item.type == 'link'"> - <a :href="item.href" :target="item.target" @click="click(item)" :download="item.download"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</a> - </template> - <template v-else-if="item.type == 'nest'"> - <p><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}...<span class="caret"><fa icon="caret-right"/></span></p> - <me-nu :menu="item.menu" @x="click"/> - </template> - </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> -.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 var(--lineWidth) var(--faceDivider) - - &.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 var(--primary) - - > p, a - display block - z-index 1 - margin 0 - padding 0 32px 0 38px - line-height $item-height - color var(--text) - text-decoration none - cursor pointer - - &:hover - text-decoration none - - * - pointer-events none - - &:hover - > p, a - text-decoration none - background var(--primary) - color var(--primaryForeground) - - &:active - > p, a - text-decoration none - background var(--primaryDarken10) - color var(--primaryForeground) - - li > ul - visibility hidden - position absolute - top 0 - left $width - margin-top -($padding) - width $width - background var(--popupBg) - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(#000, 0.2) - transition visibility 0s linear 0.2s - -</style> - -<style lang="stylus" module> -.icon - display inline-block - width 28px - margin-left -28px - text-align center -</style> - diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue deleted file mode 100644 index e79536fc0f6310d8db26362a0d22bb7d8829f541..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/context-menu.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<div class="context-menu" @contextmenu.prevent="() => {}"> - <x-menu :menu="menu" @x="click"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import 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(() => { - const width = this.$el.offsetWidth; - const height = this.$el.offsetHeight; - - let x = this.x; - let y = this.y; - - if (x + width - window.pageXOffset > window.innerWidth) { - x = window.innerWidth - width + window.pageXOffset; - } - - if (y + height - window.pageYOffset > window.innerHeight) { - y = window.innerHeight - height + window.pageYOffset; - } - - this.$el.style.left = x + 'px'; - this.$el.style.top = y + 'px'; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - 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.action) item.action(); - this.close(); - }, - close() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.context-menu - $width = 240px - $item-height = 38px - $padding = 10px - - position fixed - top 0 - left 0 - z-index 4096 - width $width - font-size 0.8em - background var(--popupBg) - border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(#000, 0.2) - opacity 0 - -</style> diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue deleted file mode 100644 index 856f889b02bb7d1131f23c8a32b6b57eddedf544..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/crop-window.vue +++ /dev/null @@ -1,189 +0,0 @@ -<template> - <mk-window ref="window" is-modal width="800px" :can-close="false"> - <template #header><fa icon="crop"/>{{ title }}</template> - <div class="body"> - <vue-cropper ref="cropper" - :src="imageUrl" - :view-mode="1" - :aspect-ratio="aspectRatio" - :container-style="{ width: '100%', 'max-height': '400px' }" - /> - </div> - <div :class="$style.actions"> - <button :class="$style.skip" @click="skip">{{ $t('skip') }}</button> - <button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button> - <button :class="$style.ok" @click="ok">{{ $t('ok') }}</button> - </div> - </mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import VueCropper from 'vue-cropperjs'; -import 'cropperjs/dist/cropper.css'; -import * as url from '../../../../../prelude/url'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/crop-window.vue'), - components: { - VueCropper - }, - props: { - image: { - type: Object, - required: true - }, - title: { - type: String, - required: true - }, - aspectRatio: { - type: Number, - required: true - } - }, - computed: { - imageUrl() { - return `/proxy/?${url.query({ - url: this.image.url - })}`; - }, - }, - 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> - - -.header - > [data-icon] - margin-right 4px - -.img - width 100% - max-height 400px - -.actions - height 72px - background var(--primaryLighten95) - -.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 var(--primaryAlpha03) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - -.ok -.cancel - width 120px - -.ok - right 16px - color var(--primaryForeground) - background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%) - border solid 1px var(--primaryLighten15) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%) - border-color var(--primary) - - &:active:not(:disabled) - background var(--primary) - border-color var(--primary) - -.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: var(--primary); -} - -.cropper-line, .cropper-point { - background-color: var(--primary); -} - -.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/client/app/desktop/views/components/detail-notes.vue b/src/client/app/desktop/views/components/detail-notes.vue deleted file mode 100644 index e50dda7c6ffbe628c99f9d19cb5211ad2c119b86..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/detail-notes.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div class="ecsvsegy" v-if="!fetching"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> - <div class="more" v-if="more"> - <ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.ecsvsegy - margin 0 auto - - > * > .post - margin-bottom 16px - - > .more - margin 32px 16px 16px 16px - text-align center - -</style> diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue deleted file mode 100644 index 5f8a9316f35e41fbf2e08de20968e588c2358bec..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/drive-window.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout"> - <template #header> - <p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> {{ $t('used') }}</p> - <span :class="$style.title"><fa icon="cloud"/>{{ $t('@.drive') }}</span> - </template> - <x-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/drive-window.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: ['folder'], - data() { - return { - usage: null - }; - }, - mounted() { - this.$root.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-icon] - margin-right 4px - -.info - position absolute - top 0 - left 16px - margin 0 - font-size 80% - -.browser - height 100% - -</style> - diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue deleted file mode 100644 index e34fdff4230524f14aab83d0d6b42541c95e6256..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/drive.file.vue +++ /dev/null @@ -1,339 +0,0 @@ -<template> -<div class="gvfdktuvdgwhmztnuekzkswkjygptfcv" - :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="$store.state.i.avatarId == file.id"> - <img src="/assets/label.svg"/> - <p>{{ $t('avatar') }}</p> - </div> - <div class="label" v-if="$store.state.i.bannerId == file.id"> - <img src="/assets/label.svg"/> - <p>{{ $t('banner') }}</p> - </div> - <div class="label red" v-if="file.isSensitive"> - <img src="/assets/label-red.svg"/> - <p>{{ $t('nsfw') }}</p> - </div> - - <x-file-thumbnail class="thumbnail" :file="file" fit="contain"/> - - <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 i18n from '../../../i18n'; -import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; -import updateAvatar from '../../api/update-avatar'; -import updateBanner from '../../api/update-banner'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/drive.file.vue'), - props: ['file'], - components: { - XFileThumbnail - }, - 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.size)}`; - } - }, - methods: { - onClick() { - this.browser.chooseFile(this.file); - }, - - onContextmenu(e) { - this.isContextmenuShowing = true; - this.$contextmenu(e, [{ - type: 'item', - text: this.$t('contextmenu.rename'), - icon: 'i-cursor', - action: this.rename - }, { - type: 'item', - text: this.file.isSensitive ? this.$t('contextmenu.unmark-as-sensitive') : this.$t('contextmenu.mark-as-sensitive'), - icon: this.file.isSensitive ? ['far', 'eye'] : ['far', 'eye-slash'], - action: this.toggleSensitive - }, null, { - type: 'item', - text: this.$t('contextmenu.copy-url'), - icon: 'link', - action: this.copyUrl - }, { - type: 'link', - href: this.file.url, - target: '_blank', - text: this.$t('contextmenu.download'), - icon: 'download', - download: this.file.name - }, null, { - type: 'item', - text: this.$t('@.delete'), - icon: ['far', 'trash-alt'], - action: this.deleteFile - }, null, { - type: 'nest', - text: this.$t('contextmenu.else-files'), - menu: [{ - type: 'item', - text: this.$t('contextmenu.set-as-avatar'), - action: this.setAsAvatar - }, { - type: 'item', - text: this.$t('contextmenu.set-as-banner'), - action: this.setAsBanner - }] - }, /*{ - type: 'nest', - text: this.$t('contextmenu.open-in-app'), - menu: [{ - type: 'item', - text: '%i18n:@contextmenu.add-app%...', - action: 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: 'transparent', // TODO fade - duration: 100, - easing: 'linear' - }); - } - }, - - rename() { - this.$root.dialog({ - title: this.$t('contextmenu.rename-file'), - input: { - placeholder: this.$t('contextmenu.input-new-file-name'), - default: this.file.name, - allowEmpty: false - } - }).then(({ canceled, result: name }) => { - if (canceled) return; - this.$root.api('drive/files/update', { - fileId: this.file.id, - name: name - }); - }); - }, - - toggleSensitive() { - this.$root.api('drive/files/update', { - fileId: this.file.id, - isSensitive: !this.file.isSensitive - }); - }, - - copyUrl() { - copyToClipboard(this.file.url); - this.$root.dialog({ - title: this.$t('contextmenu.copied'), - text: this.$t('contextmenu.copied-url-to-clipboard') - }); - }, - - setAsAvatar() { - updateAvatar(this.$root)(this.file); - }, - - setAsBanner() { - updateBanner(this.$root)(this.file); - }, - - addApp() { - alert('not implemented yet'); - }, - - deleteFile() { - this.$root.api('drive/files/delete', { - fileId: this.file.id - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gvfdktuvdgwhmztnuekzkswkjygptfcv - padding 8px 0 0 0 - min-height 180px - border-radius 4px - - &, * - cursor pointer - - &:hover - background rgba(#000, 0.05) - - > .label - &:before - &:after - background #0b65a5 - - &.red - &:before - &:after - background #c12113 - - &:active - background rgba(#000, 0.1) - - > .label - &:before - &:after - background #0b588c - - &.red - &:before - &:after - background #ce2212 - - &[data-is-selected] - background var(--primary) - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - - > .label - &:before - &:after - display none - - > .name - color var(--primaryForeground) - - > .thumbnail - color var(--primaryForeground) - - &[data-is-contextmenu-showing] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed var(--primaryAlpha03) - border-radius 4px - - > .label - position absolute - top 0 - left 0 - pointer-events none - - &:before - &:after - content "" - display block - position absolute - z-index 1 - background #0c7ac9 - - &:before - top 0 - left 57px - width 28px - height 8px - - &:after - top 57px - left 0 - width 8px - height 28px - - &.red - &:before - &:after - background #c12113 - - > 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 - color var(--driveFileIcon) - - > .name - display block - margin 4px 0 0 0 - font-size 0.8em - text-align center - word-break break-all - color var(--text) - overflow hidden - - > .ext - opacity 0.5 - -</style> diff --git a/src/client/app/desktop/views/components/emoji-picker-dialog.vue b/src/client/app/desktop/views/components/emoji-picker-dialog.vue deleted file mode 100644 index 4ea0f441a902c74d9cdec45d01873a867607518e..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/emoji-picker-dialog.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> -<div class="gcafiosrssbtbnbzqupfmglvzgiaipyv"> - <x-picker @chosen="chosen"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import contains from '../../../common/scripts/contains'; - -export default Vue.extend({ - components: { - XPicker: () => import('../../../common/views/components/emoji-picker.vue').then(m => m.default) - }, - - props: { - x: { - type: Number, - required: true - }, - y: { - type: Number, - required: true - } - }, - - mounted() { - this.$nextTick(() => { - const width = this.$el.offsetWidth; - const height = this.$el.offsetHeight; - - let x = this.x; - let y = this.y; - - if (x + width - window.pageXOffset > window.innerWidth) { - x = window.innerWidth - width + window.pageXOffset; - } - - if (y + height - window.pageYOffset > window.innerHeight) { - y = window.innerHeight - height + window.pageYOffset; - } - - this.$el.style.left = x + 'px'; - this.$el.style.top = y + 'px'; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }); - }, - - methods: { - onMousedown(e) { - e.preventDefault(); - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - return false; - }, - - chosen(emoji) { - this.$emit('chosen', emoji); - this.close(); - }, - - close() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.gcafiosrssbtbnbzqupfmglvzgiaipyv - position absolute - top 0 - left 0 - z-index 3000 - box-shadow 0 2px 12px 0 rgba(0, 0, 0, 0.3) - -</style> diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue deleted file mode 100644 index 3dba4c3af4f2e6b7d126e3d4b2afc39589e1b027..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/game-window.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> - <template #header><fa icon="gamepad"/> {{ $t('game') }}</template> - <x-reversi :class="$style.content" @gamed="g => game = g"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/game-window.vue'), - components: { - XReversi: () => import('../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - data() { - return { - game: null - }; - }, - computed: { - popout(): string { - return this.game - ? `${url}/games/reversi/${this.game.id}` - : `${url}/games/reversi`; - } - } -}); -</script> - -<style lang="stylus" module> -.content - height 100% - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts deleted file mode 100644 index 0cc44e1bbd4192a21f668c69521a1e33a175b530..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import uiNotification from './ui-notification.vue'; -import note from './note.vue'; -import notes from './notes.vue'; -import subNoteContent from './sub-note-content.vue'; -import window from './window.vue'; -import renoteFormWindow from './renote-form-window.vue'; -import mediaVideo from './media-video.vue'; -import notifications from './notifications.vue'; -import renoteForm from './renote-form.vue'; -import notePreview from './note-preview.vue'; -import noteDetail from './note-detail.vue'; -import calendar from './calendar.vue'; -import activity from './activity.vue'; -import userListTimeline from './user-list-timeline.vue'; -import uiContainer from './ui-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-ui-notification', uiNotification); -Vue.component('mk-note', note); -Vue.component('mk-notes', notes); -Vue.component('mk-sub-note-content', subNoteContent); -Vue.component('mk-window', window); -Vue.component('mk-renote-form-window', renoteFormWindow); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-notifications', notifications); -Vue.component('mk-renote-form', renoteForm); -Vue.component('mk-note-preview', notePreview); -Vue.component('mk-note-detail', noteDetail); -Vue.component('mk-calendar', calendar); -Vue.component('mk-activity', activity); -Vue.component('mk-user-list-timeline', userListTimeline); -Vue.component('ui-container', uiContainer); diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue deleted file mode 100644 index 9d2d0527efc66f27cecbf887bda58eb6f07ff011..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/media-video-dialog.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<ui-modal v-hotkey.global="keymap"> - <video :src="video.url" :title="video.name" controls autoplay ref="video" @volumechange="volumechange" /> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['video', 'start'], - mounted() { - const videoTag = this.$refs.video as HTMLVideoElement; - if (this.start) videoTag.currentTime = this.start - videoTag.volume = this.$store.state.device.mediaVolume; - }, - computed: { - keymap(): any { - return { - 'esc': this.close, - }; - } - }, - methods: { - close() { - }, - volumechange() { - const videoTag = this.$refs.video as HTMLVideoElement; - this.$store.commit('device/set', { key: 'mediaVolume', value: videoTag.volume }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -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/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue deleted file mode 100644 index c53da0f49edc7e4ea2c0cc1f8b25575b1217ffaf..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/media-video.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<div class="uofhebxjdgksfmltszlxurtjnjjsvioh" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> - <div> - <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> -</div> -<div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else> - <a class="thumbnail" - :href="video.url" - :style="imageStyle" - @click.prevent="onClick" - :title="video.name" - > - <fa :icon="['far', 'play-circle']"/> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkMediaVideoDialog from './media-video-dialog.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/media-video.vue'), - props: { - video: { - type: Object, - required: true - }, - inlinePlayable: { - default: false - } - }, - data() { - return { - hide: true - }; - }, - computed: { - imageStyle(): any { - return { - 'background-image': `url(${this.video.thumbnailUrl})` - }; - } - }, - methods: { - onClick() { - const videoTag = this.$refs.video as (HTMLVideoElement | null) - var start = 0 - if (videoTag) { - start = videoTag.currentTime - videoTag.pause() - } - const viewer = this.$root.new(MkMediaVideoDialog, { - video: this.video, - start, - }); - this.$once('hook:beforeDestroy', () => { - viewer.close(); - }); - } - } -}) -</script> - -<style lang="stylus" scoped> -.vwxdhznewyashiknzolsoihtlpicqepe - .video - display block - width 100% - height 100% - border-radius 4px - - .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% - -.uofhebxjdgksfmltszlxurtjnjjsvioh - display flex - justify-content center - align-items center - background #111 - color #fff - - > div - display table-cell - text-align center - font-size 12px - - > b - display block -</style> diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue deleted file mode 100644 index 6c1708b59f758187e47bc24c194f6cbdfdbfca90..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ /dev/null @@ -1,37 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> - <template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name v-if="user" :user="user"/><span v-else>{{ group.name }}</span></template> - <x-messaging-room :user="user" :group="group" :class="$style.content"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - props: ['user', 'group'], - computed: { - popout(): string { - if (this.user) { - return `${url}/i/messaging/${getAcct(this.user)}`; - } else if (this.group) { - return `${url}/i/messaging/group/${this.group.id}`; - } - } - } -}); -</script> - -<style lang="stylus" module> -.content - height 100% - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue deleted file mode 100644 index 7cec9484d6c0b5b9aa4cadd272ef28cd9fe6e685..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/messaging-window.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<mk-window ref="window" width="500px" height="560px" @closed="destroyDom"> - <template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template> - <x-messaging :class="$style.content" @navigate="navigate" @navigateGroup="navigateGroup"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkMessagingRoomWindow from './messaging-room-window.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - methods: { - navigate(user) { - this.$root.new(MkMessagingRoomWindow, { - user: user - }); - }, - navigateGroup(group) { - this.$root.new(MkMessagingRoomWindow, { - group: group - }); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-icon] - margin-right 4px - -.content - height 100% - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue deleted file mode 100644 index e0ce5ce1c63e0dd0cb2f6689edd278da45de329d..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/note-detail.vue +++ /dev/null @@ -1,356 +0,0 @@ -<template> -<div class="mk-note-detail" :title="title" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <button - class="read-more" - v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" - :title="$t('title')" - @click="fetchConversation" - :disabled="conversationFetching" - > - <template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> - <template v-if="conversationFetching"><fa icon="spinner" pulse/></template> - </button> - <div class="conversation"> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - </div> - <div class="reply-to" v-if="appearNote.reply"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article> - <mk-avatar class="avatar" :user="appearNote.user"/> - <header> - <router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.user.id"> - <mk-user-name :user="appearNote.user"/> - </router-link> - <span class="username"><mk-acct :user="appearNote.user"/></span> - <div class="info"> - <router-link class="time" :to="appearNote | notePage"> - <mk-time :time="appearNote.createdAt"/> - </router-link> - <div class="visibility-info"> - <span class="visibility" v-if="appearNote.visibility != 'public'"> - <fa v-if="appearNote.visibility == 'home'" icon="home"/> - <fa v-if="appearNote.visibility == 'followers'" icon="unlock"/> - <fa v-if="appearNote.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span> - </div> - </div> - </header> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span> - <span v-if="appearNote.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files" :raw="true"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="map" v-if="appearNote.geo" ref="map"></div> - <div class="renote" v-if="appearNote.renote"> - <mk-note-preview :note="appearNote.renote"/> - </div> - </div> - </div> - <footer> - <span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span> - <mk-reactions-viewer :note="appearNote"/> - <button class="replyButton" @click="reply()" :title="$t('reply')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton" @click="renote()" :title="$t('renote')"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="inhibitedButton"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton" :title="$t('add-reaction')"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> - <fa icon="minus"/> - </button> - <button @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSub from './note.sub.vue'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; -import noteMixin from '../../../common/scripts/note-mixin'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/note-detail.vue'), - - components: { - XSub - }, - - mixins: [noteMixin(), noteSubscriber('note')], - - props: { - note: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - - data() { - return { - conversation: [], - conversationFetching: false, - replies: [] - }; - }, - - mounted() { - // Get replies - if (!this.compact) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - } - }, - - methods: { - fetchConversation() { - this.conversationFetching = true; - - // Fetch conversation - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversationFetching = false; - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-detail - overflow hidden - text-align left - background var(--face) - - &.round - border-radius 6px - - > .read-more - border-radius 6px 6px 0 0 - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > .read-more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color #999 - cursor pointer - background var(--subNoteBg) - outline none - border none - border-bottom solid 1px var(--faceDivider) - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - &:disabled - cursor wait - - > .conversation - > * - border-bottom 1px solid var(--faceDivider) - - > .renote + article - padding-top 8px - - > .reply-to - border-bottom 1px solid var(--faceDivider) - - > article - padding 28px 32px 18px 32px - - &:after - content "" - display block - clear both - - &:hover - > footer > button - color var(--noteActionsHighlighted) - - > .avatar - width 60px - height 60px - border-radius 8px - - > header - position absolute - top 28px - left 108px - width calc(100% - 108px) - - > .name - display inline-block - margin 0 - line-height 24px - color var(--noteHeaderName) - 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 var(--noteHeaderAcct) - - > .info - position absolute - top 0 - right 32px - font-size 1em - - > .time - color var(--noteHeaderInfo) - - > .visibility-info - text-align: right - color var(--noteHeaderInfo) - - > .localOnly - margin-left 4px - - > .body - padding 8px 0 - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.5em - color var(--noteText) - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed 1px var(--quoteBorder) - 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 - - > footer - font-size 1.2em - - > .app - display block - font-size 0.8em - margin-left 0.5em - color var(--noteHeaderInfo) - - > button - margin 0 28px 0 0 - padding 8px - background transparent - border none - font-size 1em - color var(--noteActions) - cursor pointer - - &:hover - color var(--noteActionsHover) - - &.replyButton:hover - color var(--noteActionsReplyHover) - - &.renoteButton:hover - color var(--noteActionsRenoteHover) - - &.reactionButton:hover - color var(--noteActionsReactionHover) - - &.inhibitedButton - cursor not-allowed - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted, &.reacted:hover - color var(--noteActionsReactionHover) - - > .replies - > * - border-top 1px solid var(--faceDivider) - -</style> diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue deleted file mode 100644 index 3b1e71e16855f77654caddce4c022a07b85b22eb..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/note-preview.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title"> - <mk-avatar class="avatar" :user="note.user" v-if="!narrow"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - }, - - computed: { - title(): string { - return new Date(this.note.createdAt).toLocaleString(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.qiziqtywpuaucsgarwajitwaakggnisj - display flex - overflow hidden - font-size 0.9em - - > .avatar - flex-shrink 0 - display block - margin 0 12px 0 0 - width 48px - height 48px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - -</style> diff --git a/src/client/app/desktop/views/components/note.sub.vue b/src/client/app/desktop/views/components/note.sub.vue deleted file mode 100644 index bfecef3eb2a25b288f671b67ae3a8714b8c1b163..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/note.sub.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="tkfdzaxtkdeianobciwadajxzbddorql" :class="{ mini: narrow }" :title="title"> - <mk-avatar class="avatar" :user="note.user"/> - <div class="main"> - <mk-note-header class="header" :note="note"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - }, - - computed: { - title(): string { - return new Date(this.note.createdAt).toLocaleString(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.tkfdzaxtkdeianobciwadajxzbddorql - display flex - padding 16px 32px - font-size 0.9em - background var(--subNoteBg) - - &.mini - padding 16px - font-size 10px - - > .avatar - margin 0 8px 0 0 - width 38px - height 38px - - > .avatar - flex-shrink 0 - display block - margin 0 12px 0 0 - width 48px - height 48px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - font-size calc(1em + var(--fontSize)) - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue deleted file mode 100644 index 1c00faed39f106658c756c912f5dfa4a95420ae9..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/note.vue +++ /dev/null @@ -1,323 +0,0 @@ -<template> -<div - class="note" - :class="{ mini: narrow }" - v-show="(this.$store.state.settings.remainDeletedNote || appearNote.deletedAt == null) && !hideThisNote" - :tabindex="appearNote.deletedAt == null ? '-1' : null" - v-hotkey="keymap" - :title="title" -> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article class="article"> - <mk-avatar class="avatar" :user="appearNote.user"/> - <div class="main"> - <mk-note-header class="header" :note="appearNote" :mini="narrow"/> - <div class="body" v-if="appearNote.deletedAt == null"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span> - <a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - <a class="rp" v-if="appearNote.renote">RN:</a> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> ä½ç½®æƒ…å ±</a> - <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="compact"/> - </div> - </div> - <footer v-if="appearNote.deletedAt == null" class="footer"> - <span class="app" v-if="appearNote.app && narrow && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span> - <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/> - <button class="replyButton button" @click="reply()" :title="$t('reply')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton button" @click="renote()" :title="$t('renote')"> - <fa icon="retweet"/> - <p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="inhibitedButton button"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')"> - <fa icon="plus"/> - <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> - <fa icon="minus"/> - <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> - </button> - <button @click="menu()" ref="menuButton" class="button"> - <fa icon="ellipsis-h"/> - </button> - </footer> - <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> - </div> - </article> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -import XSub from './note.sub.vue'; -import noteMixin from '../../../common/scripts/note-mixin'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/note.vue'), - - components: { - XSub - }, - - mixins: [ - noteMixin(), - noteSubscriber('note') - ], - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - compact: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - conversation: [], - replies: [] - }; - }, - - created() { - if (this.detail) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - margin 0 - padding 0 - overflow hidden - background var(--face) - border-bottom solid var(--lineWidth) var(--faceDivider) - - &.mini - font-size 13px - - > .renote - padding 8px 16px 0 16px - - .avatar - width 20px - height 20px - - > .article - padding 16px 16px 4px - - > .avatar - margin 0 10px 8px 0 - width 42px - height 42px - - &: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 var(--primaryAlpha03) - border-radius 4px - - > .renote + article - padding-top 8px - - > .article - display flex - padding 28px 32px 18px 32px - - &:hover - > .main > footer > button - color var(--noteActionsHighlighted) - - > .avatar - flex-shrink 0 - display block - margin 0 16px 10px 0 - width 58px - height 58px - border-radius 8px - //position -webkit-sticky - //position sticky - //top 74px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 4px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - font-size calc(1em + var(--fontSize)) - - > .reply - margin-right 8px - color var(--text) - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 300px - - &:empty - display none - - .mk-url-preview - margin-top 8px - - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed var(--lineWidth) var(--quoteBorder) - border-radius 8px - - > .footer - > .app - display block - margin-top 0.5em - margin-left 0.5em - color var(--noteHeaderInfo) - font-size 0.8em - - > .button - margin 0 28px 0 0 - padding 0 8px - line-height 32px - font-size 1em - color var(--noteActions) - background transparent - border none - cursor pointer - - &:last-child - margin-right 0 - - &:hover - color var(--noteActionsHover) - - &.replyButton:hover - color var(--noteActionsReplyHover) - - &.renoteButton:hover - color var(--noteActionsRenoteHover) - - &.reactionButton:hover - color var(--noteActionsReactionHover) - - &.inhibitedButton - cursor not-allowed - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted, &.reacted:hover - color var(--noteActionsReactionHover) - - > .deleted - color var(--noteText) - opacity 0.7 - -</style> diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue deleted file mode 100644 index 0820d5d80c40e51096fafe78df31d9d5cdc7699a..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/notes.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<div class="mk-notes" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <slot name="header"></slot> - - <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> - - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes"> - <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id" :compact="true" ref="note"/> - <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as config from '../../../config'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - - onQueueChanged: (self, x) => { - if (x.length > 0) { - self.$store.commit('indicate', true); - } else { - self.$store.commit('indicate', false); - } - }, - - onPrepend: (self, note, silent) => { - // å¼¾ã - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブãŒéžè¡¨ç¤ºã¾ãŸã¯ã‚¹ã‚¯ãƒãƒ¼ãƒ«ä½ç½®ãŒæœ€ä¸Šéƒ¨ã§ã¯ãªã„ãªã‚‰ã‚¿ã‚¤ãƒˆãƒ«ã§é€šçŸ¥ - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - - if (self.isScrollTop()) { - // サウンドをå†ç”Ÿã™ã‚‹ - if (self.$store.state.device.enableSounds && !silent) { - const sound = new Audio(`${config.url}/assets/post.mp3`); - sound.volume = self.$store.state.device.soundVolume; - sound.play(); - } - } - }, - - onInited: (self) => { - self.$emit('loaded'); - } - }), - ], - - props: { - pagination: { - required: true - }, - }, - - computed: { - _notes(): any[] { - return (this.items as any).map(item => { - const date = new Date(item.createdAt).getDate(); - const month = new Date(item.createdAt).getMonth() + 1; - item._date = date; - item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return item; - }); - } - }, - - methods: { - focus() { - (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notes - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .empty - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 32px - opacity 0.3 - - > .notes - > .date - display block - margin 0 - line-height 32px - font-size 14px - text-align center - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .newer-indicator - position -webkit-sticky - position sticky - z-index 100 - height 3px - background var(--primary) - - > footer - > button - display block - margin 0 - padding 16px - width 100% - text-align center - color #ccc - background var(--face) - border-top solid var(--lineWidth) var(--faceDivider) - border-bottom-left-radius 6px - border-bottom-right-radius 6px - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - -</style> diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue deleted file mode 100644 index a2504abe66c28e709699389a774ca958deb60396..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/notifications.vue +++ /dev/null @@ -1,379 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <div class="notifications" v-if="!empty"> - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> - <template v-for="(notification, i) in _notifications"> - <div class="notification" :class="notification.type" :key="notification.id"> - <template v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <mk-reaction-icon :reaction="notification.reaction" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="retweet" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'quote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="quote-left" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <fa icon="user-plus" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </template> - - <template v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <fa icon="user-clock" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="reply" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <header> - <fa icon="at" class="icon"/> - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name"> - <mk-user-name :user="notification.note.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/> - </router-link> - </div> - </template> - - <template v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <header> - <fa icon="chart-pie" class="icon"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name"> - <mk-user-name :user="notification.user"/> - </router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </template> - </div> - - <p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - </div> - <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import getNoteSummary from '../../../../../misc/get-note-summary'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - isContainer: true - }), - ], - - props: { - type: { - type: String, - required: false - } - }, - - data() { - return { - connection: null, - getNoteSummary, - pagination: { - endpoint: 'i/notifications', - limit: 10, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーãŒç”»é¢ã‚’見ã¦ãªã„ã¨æ€ã‚れるã¨ã(ブラウザやタブãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã˜ã‚ƒãªã„ãªã©)ã¯é€ä¿¡ã—ãªã„ - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .placeholder - padding 16px - opacity 0.3 - - > .notifications - > div - > .notification - margin 0 - padding 16px - overflow-wrap break-word - font-size 12px - border-bottom solid var(--lineWidth) var(--faceDivider) - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - position -webkit-sticky - position sticky - top 16px - width 36px - height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - > .icon - margin-right 4px - - > .name - overflow hidden - text-overflow ellipsis - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - .note-preview - color var(--noteText) - display inline-block - word-break break-word - - .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - .text header - align-items normal - - &.renote, &.quote - .text header [data-icon] - color #77B255 - - &.follow - .text header [data-icon] - color #53c7ce - - &.receiveFollowRequest - .text header [data-icon] - color #888 - - &.reply, &.mention - .text header [data-icon] - color #555 - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - &:hover - background rgba(#000, 0.025) - - &:active - background rgba(#000, 0.05) - - &.fetching - cursor wait - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - -</style> diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue deleted file mode 100644 index ff6f24b6e1ecdf08f4359ae177b3866f07951854..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/post-form-window.vue +++ /dev/null @@ -1,140 +0,0 @@ -<template> -<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation"> - <template #header> - <span class="mk-post-form-window--header"> - <span class="icon" v-if="geo"><fa icon="map-marker-alt"/></span> - <span v-if="!reply">{{ $t('note') }}</span> - <span v-if="reply">{{ $t('reply') }}</span> - <span class="count" v-if="files.length != 0">{{ $t('attaches').replace('{}', files.length) }}</span> - <span class="count" v-if="uploadings.length != 0">{{ $t('uploading-media').replace('{}', uploadings.length) }}<mk-ellipsis/></span> - </span> - </template> - - <div class="mk-post-form-window--body" :style="{ maxHeight: `${maxHeight}px` }"> - <mk-note-preview v-if="reply" class="notePreview" :note="reply"/> - <x-post-form ref="form" - :reply="reply" - :mention="mention" - :initial-text="initialText" - :initial-note="initialNote" - :instant="instant" - - @posted="onPosted" - @change-uploadings="onChangeUploadings" - @change-attached-files="onChangeFiles" - @geo-attached="onGeoAttached" - @geo-dettached="onGeoDettached"/> - </div> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XPostForm from './post-form.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/post-form-window.vue'), - - components: { - XPostForm - }, - - props: { - reply: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - - animation: { - type: Boolean, - required: false, - default: true - }, - - initialText: { - type: String, - required: false - }, - - initialNote: { - type: Object, - required: false - }, - - instant: { - type: Boolean, - required: false, - default: false - }, - }, - - data() { - return { - uploadings: [], - files: [], - geo: null - }; - }, - - computed: { - maxHeight() { - return window.innerHeight - 50; - }, - }, - - mounted() { - this.$nextTick(() => { - (this.$refs.form as any).focus(); - }); - }, - - methods: { - onChangeUploadings(files) { - this.uploadings = files; - }, - onChangeFiles(files) { - this.files = files; - }, - onGeoAttached(geo) { - this.geo = geo; - }, - onGeoDettached() { - this.geo = null; - }, - onPosted() { - (this.$refs.window as any).close(); - }, - onWindowClosed() { - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-post-form-window - .mk-post-form-window--header - .icon - margin-right 8px - - .count - margin-left 8px - opacity 0.8 - - &:before - content '(' - - &:after - content ')' - - .mk-post-form-window--body - .notePreview - margin 16px 22px - -</style> diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue deleted file mode 100644 index b9c0624bd73f0e7d07b48bf996157e90de56fd1d..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/post-form.vue +++ /dev/null @@ -1,331 +0,0 @@ -<template> -<div class="gjisdzwh" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" -> - <div class="content"> - <div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags"> - <b>{{ $t('@.post-form.recent-tags') }}:</b> - <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('@.post-form.click-to-tagging')">#{{ tag }}</a> - </div> - <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> - <div v-if="visibility === 'specified'" class="to-specified"> - <fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }} - <div class="visibleUsers"> - <span v-for="u in visibleUsers"> - <mk-user-name :user="u"/> - <button @click="removeVisibleUser(u)"><fa icon="times"/></button> - </span> - <button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button> - </div> - </div> - <div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div> - <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }"> - <div class="textarea"> - <textarea :class="{ with: (files.length != 0 || poll) }" - ref="text" v-model="text" :disabled="posting" - @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" - v-autocomplete="{ model: 'text' }" - ></textarea> - <button class="emoji" @click="emoji" ref="emoji"> - <fa :icon="['far', 'laugh']"/> - </button> - <x-post-form-attaches class="files" :class="{ with: poll }" :files="files"/> - <x-poll-editor class="poll-editor" v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> - </div> - </div> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <button class="upload" :title="$t('@.post-form.attach-media-from-local')" @click="chooseFile"><fa icon="upload"/></button> - <button class="drive" :title="$t('@.post-form.attach-media-from-drive')" @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button class="kao" :title="$t('@.post-form.insert-a-kao')" @click="kao"><fa :icon="['far', 'smile']"/></button> - <button class="poll" :title="$t('@.post-form.create-poll')" @click="poll = !poll"><fa icon="chart-pie"/></button> - <button class="cw" :title="$t('@.post-form.hide-contents')" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> - <button class="geo" :title="$t('@.post-form.attach-location-information')" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> - <button class="visibility" :title="$t('@.post-form.visibility')" @click="setVisibility" ref="visibilityButton"> - <span v-if="visibility === 'public'"><fa icon="globe"/></span> - <span v-if="visibility === 'home'"><fa icon="home"/></span> - <span v-if="visibility === 'followers'"><fa icon="unlock"/></span> - <span v-if="visibility === 'specified'"><fa icon="envelope"/></span> - </button> - <p class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</p> - <ui-button primary :wait="posting" class="submit" :disabled="!canPost" @click="post"> - {{ posting ? $t('@.post-form.posting') : submitText }}<mk-ellipsis v-if="posting"/> - </ui-button> - <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/> - <div class="dropzone" v-if="draghover"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import form from '../../../common/scripts/post-form'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/post-form.vue'), - - mixins: [ - form({ - onSuccess: self => { - self.$notify(self.renote - ? self.$t('reposted') - : self.reply - ? self.$t('replied') - : self.$t('posted')); - }, - onFailure: self => { - self.$notify(self.renote - ? self.$t('renote-failed') - : self.reply - ? self.$t('reply-failed') - : self.$t('note-failed')); - } - }), - ], -}); -</script> - -<style lang="stylus" scoped> -.gjisdzwh - display block - padding 16px - background var(--desktopPostFormBg) - overflow hidden - - &:after - content "" - display block - clear both - - > .content - > input - > .textarea > textarea - display block - width 100% - padding 12px - font-size 16px - color var(--desktopPostFormTextareaFg) - background var(--desktopPostFormTextareaBg) - outline none - border solid 1px var(--primaryAlpha01) - border-radius 4px - transition border-color .2s ease - padding-right 30px - - &:hover - border-color var(--primaryAlpha02) - transition border-color .1s ease - - &:focus - border-color var(--primaryAlpha05) - transition border-color 0s ease - - &:disabled - opacity 0.5 - - &::-webkit-input-placeholder - color var(--primaryAlpha03) - - > input - margin-bottom 8px - - > .textarea - > .emoji - position absolute - top 0 - right 0 - padding 10px - font-size 18px - color var(--text) - opacity 0.5 - - &:hover - color var(--textHighlighted) - opacity 1 - - &:active - color var(--primary) - opacity 1 - - > textarea - margin 0 - max-width 100% - min-width 100% - min-height 84px - - &:hover - & + * + * - & + * + * + * - border-color var(--primaryAlpha02) - transition border-color .1s ease - - &:focus - & + * + * - & + * + * + * - border-color var(--primaryAlpha05) - transition border-color 0s ease - - & + .emoji - opacity 0.7 - - &.with - border-bottom solid 1px var(--primaryAlpha01) !important - border-radius 4px 4px 0 0 - - > .files - margin 0 - padding 0 - background var(--desktopPostFormTextareaBg) - border solid 1px var(--primaryAlpha01) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - &.with - border-bottom solid 1px var(--primaryAlpha01) !important - border-radius 0 - - > .poll-editor - background var(--desktopPostFormTextareaBg) - border solid 1px var(--primaryAlpha01) - border-top none - border-radius 0 0 4px 4px - transition border-color .3s ease - - > .hashtags - margin 0 0 8px 0 - overflow hidden - white-space nowrap - font-size 14px - - > b - color var(--primary) - - > * - margin-right 8px - white-space nowrap - - > .with-quote - margin 0 0 8px 0 - color var(--primary) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .to-specified - margin 0 0 8px 0 - color var(--primary) - - > .visibleUsers - display inline - top -1px - font-size 14px - - > span - margin-left 14px - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .local-only - margin 0 0 8px 0 - color var(--primary) - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - border solid 1px var(--primaryAlpha02) - border-radius 4px - - input[type='file'] - display none - - .submit - display block - position absolute - bottom 16px - right 16px - width 110px - height 40px - - > .text-count - pointer-events none - display block - position absolute - bottom 16px - right 138px - margin 0 - line-height 40px - color var(--primaryAlpha05) - - &.over - color #ec3828 - - > .upload - > .drive - > .kao - > .poll - > .cw - > .geo - > .visibility - display inline-block - cursor pointer - padding 0 - margin 8px 4px 0 0 - width 40px - height 40px - font-size 1em - color var(--desktopPostFormTransparentButtonFg) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color var(--primaryAlpha03) - - &:active - color var(--primaryAlpha06) - background linear-gradient(to bottom, var(--desktopPostFormTransparentButtonActiveGradientStart) 0%, var(--desktopPostFormTransparentButtonActiveGradientEnd) 100%) - border-color var(--primaryAlpha05) - box-shadow 0 2px 4px rgba(#000, 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - border-radius 8px - - > .dropzone - position absolute - left 0 - top 0 - width 100% - height 100% - border dashed 2px var(--primaryAlpha05) - pointer-events none - -</style> diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue deleted file mode 100644 index 28b35dbd97030f2a7b3dc5cafa4eb7aeb187f82c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/progress-dialog.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom"> - <template #header>{{ title }}<mk-ellipsis/></template> - <div :class="$style.body"> - <p :class="$style.init" v-if="isNaN(value)">{{ $t('waiting') }}<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'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/progress-dialog.vue'), - 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> - - -.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 var(--primaryAlpha07) - - &:after - content '%' - -.progress - display block - margin 0 - width 100% - height 10px - background transparent - border none - border-radius 4px - overflow hidden - - &::-webkit-progress-value - background var(--primary) - - &::-webkit-progress-bar - background var(--primaryAlpha01) - -.waiting - background linear-gradient( - 45deg, - var(--primaryLighten30) 25%, - var(--primary) 25%, - var(--primary) 50%, - var(--primaryLighten30) 50%, - var(--primaryLighten30) 75%, - var(--primary) 75%, - var(--primary) - ) - 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/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue deleted file mode 100644 index 0ca347b530fb0bc4d8e43938f849006422b5c51f..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/renote-form-window.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation"> - <template #header :class="$style.header"><fa icon="retweet"/>{{ $t('title') }}</template> - <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/renote-form-window.vue'), - props: { - note: { - type: Object, - required: true - }, - - animation: { - type: Boolean, - required: false, - default: true - } - }, - - computed: { - keymap(): any { - return { - 'esc': this.close, - 'enter': this.post, - 'q': this.quote, - }; - } - }, - - methods: { - post() { - (this.$refs.form as any).ok(); - }, - quote() { - (this.$refs.form as any).onQuote(); - }, - close() { - (this.$refs.window as any).close(); - }, - onPosted() { - (this.$refs.window as any).close(); - }, - onCanceled() { - (this.$refs.window as any).close(); - }, - onWindowClosed() { - this.$emit('closed'); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue deleted file mode 100644 index 53fbf0ff3027f173782762735de1f763ad8c9e74..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/renote-form.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<div class="mk-renote-form"> - <mk-note-preview class="preview" :note="note"/> - <template v-if="!quote"> - <footer> - <a class="quote" v-if="!quote" @click="onQuote">{{ $t('quote') }}</a> - <ui-button class="button cancel" inline @click="cancel">{{ $t('cancel') }}</ui-button> - <ui-button class="button home" inline :primary="visibility != 'public'" @click="ok('home')" :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote-home') }}</ui-button> - <ui-button class="button ok" inline :primary="visibility == 'public'" @click="ok('public')" :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote') }}</ui-button> - </footer> - </template> - <template v-if="quote"> - <x-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/renote-form.vue'), - - components: { - XPostForm: () => import('./post-form.vue').then(m => m.default) - }, - - props: { - note: { - type: Object, - required: true - } - }, - - data() { - return { - wait: false, - quote: false, - visibility: this.$store.state.settings.defaultNoteVisibility - }; - }, - - methods: { - ok(v: string) { - this.wait = true; - this.$root.api('notes/create', { - renoteId: this.note.id, - visibility: v || this.visibility - }).then(data => { - this.$emit('posted'); - this.$notify(this.$t('success')); - }).catch(err => { - this.$notify(this.$t('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> -.mk-renote-form - > .preview - margin 16px 22px - - > footer - height 72px - background var(--desktopRenoteFormFooter) - - > .quote - position absolute - bottom 16px - left 28px - line-height 40px - - > .button - display block - position absolute - bottom 16px - width 120px - height 40px - - &.cancel - right 280px - - &.home - right 148px - font-size 13px - - &.ok - right 16px - -</style> diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue deleted file mode 100644 index 9bfd5a14c79b82e996beef8cbb6a7a5df9d09a80..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/settings-window.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom"> - <template #header :class="$style.header"><fa icon="cog"/>{{ $t('@.settings') }}</template> - <x-settings :initial-page="initialPage" @done="close"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/settings-window.vue'), - - components: { - XSettings: () => import('./settings.vue').then(m => m.default) - }, - - props: { - initialPage: { - type: String, - required: false - } - }, - methods: { - close() { - (this as any).$refs.window.close(); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue deleted file mode 100644 index 65701cd5f388c54e353af452ec96281a93fab7ec..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/settings.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<div class="mk-settings"> - <div class="nav" :class="{ inWindow }"> - <router-link to="/i/settings/profile" active-class="active"><fa icon="user" fixed-width/>{{ $t('@._settings.profile') }}</router-link> - <router-link to="/i/settings/appearance" active-class="active"><fa icon="palette" fixed-width/>{{ $t('@._settings.appearance') }}</router-link> - <router-link to="/i/settings/behavior" active-class="active"><fa icon="desktop" fixed-width/>{{ $t('@._settings.behavior') }}</router-link> - <router-link to="/i/settings/notification" active-class="active"><fa :icon="['far', 'bell']" fixed-width/>{{ $t('@._settings.notification') }}</router-link> - <router-link to="/i/settings/drive" active-class="active"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</router-link> - <router-link to="/i/settings/hashtags" active-class="active"><fa icon="hashtag" fixed-width/>{{ $t('@._settings.tags') }}</router-link> - <router-link to="/i/settings/muteAndBlock" active-class="active"><fa icon="ban" fixed-width/>{{ $t('@._settings.mute-and-block') }}</router-link> - <router-link to="/i/settings/apps" active-class="active"><fa icon="puzzle-piece" fixed-width/>{{ $t('@._settings.apps') }}</router-link> - <router-link to="/i/settings/security" active-class="active"><fa icon="unlock-alt" fixed-width/>{{ $t('@._settings.security') }}</router-link> - <router-link to="/i/settings/api" active-class="active"><fa icon="key" fixed-width/>API</router-link> - <router-link to="/i/settings/other" active-class="active"><fa icon="cogs" fixed-width/>{{ $t('@._settings.other') }}</router-link> - </div> - <div class="pages"> - <x-settings :page="page"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSettings from '../../../common/views/components/settings/settings.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XSettings, - }, - props: { - page: { - type: String, - required: true, - }, - inWindow: { - type: Boolean, - required: false, - default: true - } - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-settings - display flex - width 100% - height 100% - - > .nav - flex 0 0 200px - width 100% - height 100% - padding 16px 0 0 0 - overflow auto - z-index 1 - font-size 15px - - > a - display block - padding 10px 16px - margin 0 - color var(--desktopSettingsNavItem) - cursor pointer - user-select none - transition margin-left 0.2s ease - - > [data-icon] - margin-right 4px - - &:hover - color var(--desktopSettingsNavItemHover) - - &.active - margin-left 8px - color var(--primary) !important - - > .pages - width 100% - height 100% - flex auto - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue deleted file mode 100644 index 78f9a6034be4c7d5341286aac4ae590d7f3450e5..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/sub-note-content.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div class="mk-sub-note-content"> - <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">{{ $t('private') }}</span> - <span v-if="note.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span> - <a class="reply" v-if="note.replyId"><fa icon="reply"/></a> - <mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/> - <a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a> - </div> - <details v-if="note.files.length > 0"> - <summary>({{ this.$t('media-count').replace('{}', note.files.length) }})</summary> - <mk-media-list :media-list="note.files"/> - </details> - <details v-if="note.poll"> - <summary>{{ $t('poll') }}</summary> - <mk-poll :note="note"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/sub-note-content.vue'), - props: ['note'] -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-note-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - mk-poll - font-size 80% - -</style> diff --git a/src/client/app/desktop/views/components/ui-container.vue b/src/client/app/desktop/views/components/ui-container.vue deleted file mode 100644 index 59954fee8eb7fff62152d9d8e006e91a4e5a72d8..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui-container.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<div class="kedshtep" :class="{ naked, inNakedDeckColumn, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header v-if="showHeader" :class="{ bodyTogglable }" @click="toggleContent(!showBody)"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - <button v-if="bodyTogglable"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </header> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - }, - bodyTogglable: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - inject: { - inNakedDeckColumn: { - default: false - } - }, - data() { - return { - showBody: this.expanded - }; - }, - methods: { - toggleContent(show: boolean) { - if (!this.bodyTogglable) return; - this.showBody = show; - this.$emit('toggle', show); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kedshtep - overflow hidden - - &:not(.inNakedDeckColumn) - background var(--face) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - & + .kedshtep - margin-top 16px - - &.naked - background transparent !important - box-shadow none !important - - > header - background var(--faceHeader) - - &.bodyTogglable - cursor pointer - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - > [data-icon] - margin-right 6px - - &: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 var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - &.inNakedDeckColumn - background var(--face) - - > header - margin 0 - padding 8px 16px - font-size 12px - color var(--text) - background var(--deckColumnBg) - - &.bodyTogglable - cursor pointer - - > button - position absolute - top 0 - right 8px - padding 8px 6px - font-size 14px - color var(--text) - -</style> diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue deleted file mode 100644 index 52e8e1d6cb0ca0b44c775e43548d6cd54c9edc64..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui-notification.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<div class="mk-ui-notification"> - <p>{{ message }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import 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.destroyDom() - }); - }, 5000); - }); - } -}); -</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 var(--desktopNotificationFg) - background var(--desktopNotificationBg) - border-radius 0 0 8px 8px - box-shadow 0 2px 4px var(--desktopNotificationShadow) - transform translateY(-64px) - opacity 0 - pointer-events none - - > p - margin 0 - line-height 64px - text-align center - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue deleted file mode 100644 index 690f3a5587df14f6497adef2da11ee3f3ee9fb5f..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ /dev/null @@ -1,343 +0,0 @@ -<template> -<div class="account" v-hotkey.global="keymap"> - <button class="header" :data-active="isOpen" @click="toggle"> - <span class="username">{{ $store.state.i.username }}<template v-if="!isOpen"><fa icon="angle-down"/></template><template v-if="isOpen"><fa icon="angle-up"/></template></span> - <mk-avatar class="avatar" :user="$store.state.i"/> - </button> - <transition name="zoom-in-top"> - <div class="menu" v-if="isOpen"> - <ul> - <li> - <router-link :to="`/@${ $store.state.i.username }`"> - <i><fa icon="user" fixed-width/></i> - <span>{{ $t('profile') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li @click="drive"> - <p> - <i><fa icon="cloud" fixed-width/></i> - <span>{{ $t('@.drive') }}</span> - <i><fa icon="angle-right"/></i> - </p> - </li> - <li> - <router-link to="/i/favorites"> - <i><fa icon="star" fixed-width/></i> - <span>{{ $t('@.favorites') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link to="/i/lists"> - <i><fa icon="list" fixed-width/></i> - <span>{{ $t('lists') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link to="/i/groups"> - <i><fa :icon="faUsers" fixed-width/></i> - <span>{{ $t('groups') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link to="/i/pages"> - <i><fa :icon="faStickyNote" fixed-width/></i> - <span>{{ $t('@.pages') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> - <router-link to="/i/follow-requests"> - <i><fa :icon="['far', 'envelope']" fixed-width/></i> - <span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li> - <router-link :to="`/@${ $store.state.i.username }/room`"> - <i><fa :icon="faDoorOpen" fixed-width/></i> - <span>{{ $t('room') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - </ul> - <ul> - <li> - <router-link to="/i/settings"> - <i><fa icon="cog" fixed-width/></i> - <span>{{ $t('@.settings') }}</span> - <i><fa icon="angle-right"/></i> - </router-link> - </li> - <li v-if="$store.state.i.isAdmin || $store.state.i.isModerator"> - <a href="/admin"> - <i><fa icon="terminal" fixed-width/></i> - <span>{{ $t('admin') }}</span> - <i><fa icon="angle-right"/></i> - </a> - </li> - </ul> - <ul> - <li @click="toggleDeckMode"> - <p> - <template v-if="$store.state.device.inDeckMode"><span>{{ $t('@.home') }}</span><i><fa :icon="faHome"/></i></template> - <template v-else><span>{{ $t('@.deck') }}</span><i><fa :icon="faColumns"/></i></template> - </p> - </li> - <li @click="dark"> - <p> - <span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span> - <template><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon"/></i></template> - </p> - </li> - </ul> - <ul> - <li @click="signout"> - <p class="signout"> - <i><fa icon="power-off" fixed-width/></i> - <span>{{ $t('@.signout') }}</span> - </p> - </li> - </ul> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -// import MkSettingsWindow from './settings-window.vue'; -import MkDriveWindow from './drive-window.vue'; -import contains from '../../../common/scripts/contains'; -import { faHome, faColumns, faUsers, faDoorOpen } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.account.vue'), - data() { - return { - isOpen: false, - faHome, faColumns, faMoon, faSun, faStickyNote, faUsers, faDoorOpen - }; - }, - computed: { - keymap(): any { - return { - 'a|m': this.toggle - }; - } - }, - beforeDestroy() { - this.close(); - }, - methods: { - toggle() { - this.isOpen ? this.close() : this.open(); - }, - open() { - this.isOpen = true; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - close() { - this.isOpen = false; - for (const el of Array.from(document.querySelectorAll('body *'))) { - 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.$root.new(MkDriveWindow); - }, - signout() { - this.$root.signout(); - }, - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - toggleDeckMode() { - this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode }); - location.replace('/'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.account - > .header - display block - margin 0 - padding 0 - color var(--desktopHeaderFg) - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color var(--desktopHeaderHoverFg) - - > .avatar - filter saturate(150%) - - > .username - display block - float left - margin 0 12px 0 16px - max-width 16em - line-height 48px - font-weight bold - text-decoration none - - @media (max-width 1100px) - display none - - [data-icon] - 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 - - @media (max-width 1100px) - margin-left 8px - - > .menu - $bgcolor = var(--face) - display block - position absolute - top 56px - right -2px - width 230px - font-size 0.8em - background $bgcolor - border-radius 4px - box-shadow 0 var(--lineWidth) 4px rgba(#000, 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(#000, 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 $bgcolor - border-left solid 14px transparent - - ul - display block - margin 10px 0 - padding 0 - list-style none - - & + ul - padding-top 10px - border-top solid var(--lineWidth) var(--faceDivider) - - > li - display block - margin 0 - padding 0 - - > a - > p - display block - z-index 1 - padding 0 28px - margin 0 - line-height 40px - color var(--text) - cursor pointer - - * - pointer-events none - - > span:first-child - padding-left 22px - - > span:nth-child(2) - > i - margin-left 4px - padding 2px 8px - font-size 90% - font-style normal - background var(--primary) - color var(--primaryForeground) - border-radius 8px - - > i:first-child - margin-right 6px - width 16px - - > i:last-child - 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 var(--primary) - color var(--primaryForeground) - - &:active - background var(--primaryDarken10) - - &.signout - $color = #e64137 - - &:hover, &:active - background $color - color #fff - - &:active - background darken($color, 10%) - -.zoom-in-top-enter-active, -.zoom-in-top-leave-active { - transform-origin: center -16px; -} - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.clock.vue b/src/client/app/desktop/views/components/ui.header.clock.vue deleted file mode 100644 index b8b638bc414fe74679613e403a2d4bae54b4534f..0000000000000000000000000000000000000000 --- a/src/client/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 :dark="true"/> - </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 var(--desktopHeaderFg) - - > .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/client/app/desktop/views/components/ui.header.messaging.vue b/src/client/app/desktop/views/components/ui.header.messaging.vue deleted file mode 100644 index c5d1da3a3d5fce4e763ac8c0e614793382c35c14..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.messaging.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<div class="toltmoik"> - <button @click="open()" :title="$t('@.messaging')"> - <i class="bell"><fa :icon="faComments"/></i> - <i class="circle" v-if="hasUnreadMessagingMessage"><fa icon="circle"/></i> - </button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkMessagingWindow from './messaging-window.vue'; -import { faComments } from '@fortawesome/free-regular-svg-icons'; - -export default Vue.extend({ - i18n: i18n(), - - data() { - return { - faComments - }; - }, - - computed: { - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - } - }, - - methods: { - open() { - this.$root.new(MkMessagingWindow); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.toltmoik - > button - display block - margin 0 - padding 0 - width 32px - color var(--desktopHeaderFg) - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color var(--desktopHeaderHoverFg) - - > i.bell - font-size 1.2em - line-height 48px - - > i.circle - margin-left -5px - vertical-align super - font-size 10px - color var(--notificationIndicator) - animation blink 1s infinite - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue deleted file mode 100644 index 2bd3cf87729ac47d972c634a58fd435273c8c033..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<div class="nav"> - <ul> - <li class="timeline" :class="{ active: $route.name == 'index' }" @click="goToTop"> - <router-link to="/"><fa icon="home"/><p>{{ $t('@.timeline') }}</p></router-link> - </li> - <li class="featured" :class="{ active: $route.name == 'featured' }"> - <router-link to="/featured"><fa :icon="faNewspaper"/><p>{{ $t('@.featured-notes') }}</p></router-link> - </li> - <li class="explore" :class="{ active: $route.name == 'explore' || $route.name == 'explore-tag' }"> - <router-link to="/explore"><fa :icon="faHashtag"/><p>{{ $t('@.explore') }}</p></router-link> - </li> - <li class="game"> - <a @click="game"> - <fa icon="gamepad"/> - <p>{{ $t('game') }}</p> - <template v-if="hasGameInvitations"><fa icon="circle"/></template> - </a> - </li> - </ul> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkGameWindow from './game-window.vue'; -import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.nav.vue'), - data() { - return { - hasGameInvitations: false, - connection: null, - faNewspaper, faHashtag - }; - }, - mounted() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - methods: { - onReversiInvited() { - this.hasGameInvitations = true; - }, - - onReversiNoInvites() { - this.hasGameInvitations = false; - }, - - game() { - this.$root.new(MkGameWindow); - }, - - goToTop() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.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 var(--primary) - - > a - display inline-block - z-index 1 - height 100% - padding 0 20px - font-size 13px - font-variant small-caps - color var(--desktopHeaderFg) - text-decoration none - transition none - cursor pointer - - * - pointer-events none - - &:hover - color var(--desktopHeaderHoverFg) - text-decoration none - - > [data-icon]:first-child - margin-right 8px - - > [data-icon]:last-child - margin-left 5px - font-size 10px - color var(--notificationIndicator) - - @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/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue deleted file mode 100644 index d3316d6a896312120712f3b4936a5a3752349717..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.notifications.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<div class="notifications" v-hotkey.global="keymap"> - <button :data-active="isOpen" @click="toggle" :title="$t('title')"> - <i class="bell"><fa :icon="['far', 'bell']"/></i> - <i class="circle" v-if="hasUnreadNotification"><fa icon="circle"/></i> - </button> - <div class="pop" v-if="isOpen"> - <mk-notifications/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import contains from '../../../common/scripts/contains'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.notifications.vue'), - data() { - return { - isOpen: false - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - keymap(): any { - return { - 'shift+n': this.toggle - }; - } - }, - - methods: { - toggle() { - this.isOpen ? this.close() : this.open(); - }, - - open() { - this.isOpen = true; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - - close() { - this.isOpen = false; - for (const el of Array.from(document.querySelectorAll('body *'))) { - 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> -.notifications - > button - display block - margin 0 - padding 0 - width 32px - color var(--desktopHeaderFg) - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color var(--desktopHeaderHoverFg) - - > i.bell - font-size 1.2em - line-height 48px - - > i.circle - margin-left -5px - vertical-align super - font-size 10px - color var(--notificationIndicator) - animation blink 1s infinite - - > .pop - $bgcolor = var(--face) - display block - position absolute - top 56px - right -72px - width 300px - background $bgcolor - border-radius 4px - box-shadow 0 1px 4px rgba(#000, 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(#000, 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 $bgcolor - border-left solid 14px transparent - - > .mk-notifications - max-height 350px - font-size 1rem - overflow auto - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue deleted file mode 100644 index b273ad8d4d88dd27ef8b1cfecc499d0f9d2fd287..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.post.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="note"> - <button @click="post" :title="$t('post')"><fa icon="pencil-alt"/></button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.post.vue'), - methods: { - post() { - this.$post(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - 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 var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue deleted file mode 100644 index 0cf5ca6f321880e50b82a3828033709b6f9f5a67..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.search.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> -<form class="wlvfdpkp" @submit.prevent="onSubmit"> - <i><fa icon="search"/></i> - <input v-model="q" type="search" :placeholder="$t('placeholder')" v-autocomplete="{ model: 'q' }"/> - <div class="result"></div> -</form> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { search } from '../../../common/scripts/search'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.header.search.vue'), - data() { - return { - q: '', - wait: false - }; - }, - methods: { - async onSubmit() { - if (this.wait) return; - - this.wait = true; - search(this, this.q).finally(() => { - this.wait = false; - this.q = ''; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wlvfdpkp - @media (max-width 800px) - display none !important - - > i - display block - position absolute - top 0 - left 0 - width 48px - text-align center - line-height 48px - color var(--desktopHeaderFg) - 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 var(--desktopHeaderSearchBg) - outline none - border none - border-radius 16px - transition color 0.5s ease, border 0.5s ease - color var(--desktopHeaderSearchFg) - - @media (max-width 1000px) - width 10em - - &::placeholder - color var(--desktopHeaderFg) - - &:hover - background var(--desktopHeaderSearchHoverBg) - - &:focus - box-shadow 0 0 0 2px var(--primaryAlpha05) !important - -</style> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue deleted file mode 100644 index 14a732155224bc0d5bff43a8a97923e94b5397fe..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.header.vue +++ /dev/null @@ -1,161 +0,0 @@ -<template> -<div class="header" :style="style"> - <p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }} <a href="/assets/flush.html?force">Flush</a></p> - <div class="main" ref="main"> - <div class="backdrop"></div> - <div class="main"> - <div class="container" ref="mainContainer"> - <div class="left"> - <x-nav/> - </div> - <div class="center"> - <div class="icon" @click="goToTop"> - <img svg-inline src="../../assets/header-icon.svg"/> - </div> - </div> - <div class="right"> - <x-search/> - <x-account v-if="$store.getters.isSignedIn"/> - <x-messaging v-if="$store.getters.isSignedIn"/> - <x-notifications v-if="$store.getters.isSignedIn"/> - <x-post v-if="$store.getters.isSignedIn"/> - <x-clock v-if="$store.state.settings.showClockOnHeader" class="clock"/> - </div> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { env } from '../../../config'; - -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'; -import XMessaging from './ui.header.messaging.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XNav, - XSearch, - XAccount, - XNotifications, - XMessaging, - XPost, - XClock - }, - - data() { - return { - env: env - }; - }, - - computed: { - style(): any { - return { - 'box-shadow': this.$store.state.device.useShadow ? '0 0px 8px rgba(0, 0, 0, 0.2)' : 'none' - }; - } - }, - - mounted() { - this.$store.commit('setUiHeaderHeight', this.$el.offsetHeight); - }, - - methods: { - goToTop() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - }, -}); -</script> - -<style lang="stylus" scoped> -.header - position fixed - top 0 - z-index 1000 - width 100% - - > .warn - display block - margin 0 - padding 4px - text-align center - font-size 12px - background #f00 - color #fff - - > .main - height 48px - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height 48px - background var(--desktopHeaderBg) - - > .main - z-index 1001 - margin 0 - padding 0 - background-clip content-box - font-size 0.9rem - user-select none - - > .container - display flex - width 100% - max-width 1208px - margin 0 auto - - > * - position absolute - height 48px - - > .center - right 0 - - > .icon - margin auto - display block - width 48px - text-align center - cursor pointer - opacity 0.5 - - > svg - width 24px - height 48px - vertical-align top - fill var(--desktopHeaderFg) - - > .left, - > .center - left 0 - - > .right - right 0 - - > * - display inline-block - vertical-align top - - @media (max-width 1100px) - > .clock - display none - -</style> diff --git a/src/client/app/desktop/views/components/ui.sidebar.vue b/src/client/app/desktop/views/components/ui.sidebar.vue deleted file mode 100644 index d1ceec51984f71ae4ebe8c293562851ed155b834..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.sidebar.vue +++ /dev/null @@ -1,363 +0,0 @@ -<template> -<div class="header" :class="navbar" :data-shadow="$store.state.device.useShadow"> - <div class="body"> - <div class="post"> - <button @click="post" :title="$t('title')"><fa icon="pencil-alt"/></button> - </div> - - <div class="nav" v-if="$store.getters.isSignedIn"> - <div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop"> - <router-link to="/"><fa icon="home"/></router-link> - </div> - <div class="featured" :class="{ active: $route.name == 'featured' }"> - <router-link to="/featured"><fa :icon="faNewspaper"/></router-link> - </div> - <div class="explore" :class="{ active: $route.name == 'explore' || $route.name == 'explore-tag' }"> - <router-link to="/explore"><fa :icon="faHashtag"/></router-link> - </div> - <div class="game"> - <a @click="game"><fa icon="gamepad"/><template v-if="hasGameInvitations"><fa icon="circle"/></template></a> - </div> - </div> - - <div class="nav bottom" v-if="$store.getters.isSignedIn"> - <div> - <a @click="drive"><fa icon="cloud"/></a> - </div> - <div ref="notificationsButton" :class="{ active: showNotifications }"> - <a @click="notifications"><fa :icon="['far', 'bell']"/></a> - </div> - <div class="messaging"> - <a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a> - </div> - <div> - <a @click="settings"><fa icon="cog"/></a> - </div> - <div class="signout"> - <a @click="signout"><fa icon="power-off"/></a> - </div> - <div> - <router-link to="/i/favorites"><fa icon="star"/></router-link> - </div> - <div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> - <a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a> - </div> - <div class="account"> - <router-link :to="`/@${ $store.state.i.username }`"> - <mk-avatar class="avatar" :user="$store.state.i"/> - </router-link> - </div> - <div> - <template v-if="$store.state.device.inDeckMode"> - <a @click="toggleDeckMode(false)"><fa icon="home"/></a> - </template> - <template v-else> - <a @click="toggleDeckMode(true)"><fa icon="columns"/></a> - </template> - </div> - <div> - <a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a> - </div> - </div> - </div> - - <transition :name="`slide-${navbar}`"> - <div class="notifications" v-if="showNotifications" ref="notifications" :class="navbar" :data-shadow="$store.state.device.useShadow"> - <mk-notifications/> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import MkSettingsWindow from './settings-window.vue'; -import MkDriveWindow from './drive-window.vue'; -import MkMessagingWindow from './messaging-window.vue'; -import MkGameWindow from './game-window.vue'; -import contains from '../../../common/scripts/contains'; -import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/ui.sidebar.vue'), - data() { - return { - hasGameInvitations: false, - connection: null, - showNotifications: false, - faNewspaper, faHashtag - }; - }, - - computed: { - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - }, - - navbar(): string { - return this.$store.state.device.navbar; - }, - }, - - mounted() { - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - toggleDeckMode(deck) { - this.$store.commit('device/set', { key: 'deckMode', value: deck }); - location.replace('/'); - }, - - onReversiInvited() { - this.hasGameInvitations = true; - }, - - onReversiNoInvites() { - this.hasGameInvitations = false; - }, - - messaging() { - this.$root.new(MkMessagingWindow); - }, - - game() { - this.$root.new(MkGameWindow); - }, - - post() { - this.$post(); - }, - - drive() { - this.$root.new(MkDriveWindow); - }, - - list() { - this.$root.new(MkUserListsWindow); - }, - - followRequests() { - this.$root.new(MkFollowRequestsWindow); - }, - - settings() { - this.$root.new(MkSettingsWindow); - }, - - signout() { - this.$root.signout(); - }, - - notifications() { - this.showNotifications ? this.closeNotifications() : this.openNotifications(); - }, - - openNotifications() { - this.showNotifications = true; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - - closeNotifications() { - this.showNotifications = false; - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - - onMousedown(e) { - e.preventDefault(); - if ( - !contains(this.$refs.notifications, e.target) && - this.$refs.notifications != e.target && - !contains(this.$refs.notificationsButton, e.target) && - this.$refs.notificationsButton != e.target - ) { - this.closeNotifications(); - } - return false; - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - - goToTop() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.header - $width = 68px - - position fixed - top 0 - z-index 1000 - width $width - height 100% - - &.left - left 0 - - &[data-shadow] - box-shadow 4px 0 4px rgba(0, 0, 0, 0.1) - - &.right - right 0 - - &[data-shadow] - box-shadow -4px 0 4px rgba(0, 0, 0, 0.1) - - > .body - position fixed - top 0 - z-index 1 - width $width - height 100% - background var(--desktopHeaderBg) - - > .post - width $width - height $width - padding 12px - - > button - display inline-block - margin 0 - padding 0 - height 100% - width 100% - font-size 1.2em - font-weight normal - text-decoration none - color var(--primaryForeground) - background var(--primary) !important - outline none - border none - border-radius 100% - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background var(--primaryLighten10) !important - - &:active - background var(--primaryDarken10) !important - transition background 0s ease - - > .nav.bottom - position absolute - bottom 0 - left 0 - - > .account - width $width - height $width - padding 14px - - > * - display block - width 100% - height 100% - - > .avatar - pointer-events none - width 100% - height 100% - - > .notifications - position fixed - top 0 - width 350px - height 100% - overflow auto - background var(--face) - - &.left - left $width - - &[data-shadow] - box-shadow 4px 0 4px rgba(0, 0, 0, 0.1) - - &.right - right $width - - &[data-shadow] - box-shadow -4px 0 4px rgba(0, 0, 0, 0.1) - - .nav - > * - > * - display block - width $width - line-height 52px - text-align center - font-size 18px - color var(--desktopHeaderFg) - - &:hover - background rgba(0, 0, 0, 0.05) - color var(--desktopHeaderHoverFg) - text-decoration none - - &:active - background rgba(0, 0, 0, 0.1) - - &.left - .nav - > * - &.active - box-shadow -4px 0 var(--primary) inset - - &.right - .nav - > * - &.active - box-shadow 4px 0 var(--primary) inset - -.slide-left-enter-active, -.slide-left-leave-active { - transition: all 0.2s ease; -} - -.slide-left-enter, .slide-left-leave-to { - transform: translateX(-16px); - opacity: 0; -} - -.slide-right-enter-active, -.slide-right-leave-active { - transition: all 0.2s ease; -} - -.slide-right-enter, .slide-right-leave-to { - transform: translateX(16px); - opacity: 0; -} -</style> diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue deleted file mode 100644 index f7961d508378b5d5c6ebdc178755008b4002bf61..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/ui.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<div class="mk-ui" v-hotkey.global="keymap"> - <div class="bg" v-if="$store.getters.isSignedIn && $store.state.settings.wallpaper" :style="style"></div> - <x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/> - <x-sidebar class="sidebar" v-if="navbar != 'top'" v-show="!zenMode" ref="sidebar"/> - <div class="content" :class="[{ sidebar: navbar != 'top', zen: zenMode }, navbar]"> - <slot></slot> - </div> - <mk-stream-indicator v-if="$store.getters.isSignedIn"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XHeader from './ui.header.vue'; -import XSidebar from './ui.sidebar.vue'; - -export default Vue.extend({ - components: { - XHeader, - XSidebar - }, - - data() { - return { - zenMode: false - }; - }, - - computed: { - navbar(): string { - return this.$store.state.device.navbar; - }, - - style(): any { - if (!this.$store.getters.isSignedIn || this.$store.state.settings.wallpaper == null) return {}; - return { - backgroundImage: `url(${ this.$store.state.settings.wallpaper })` - }; - }, - - keymap(): any { - return { - 'p': this.post, - 'n': this.post, - 'z': this.toggleZenMode - }; - } - }, - - watch: { - '$store.state.uiHeaderHeight'() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - }, - - navbar() { - if (this.navbar != 'top') { - this.$store.commit('setUiHeaderHeight', 0); - } - } - }, - - mounted() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - }, - - methods: { - post() { - this.$post(); - }, - - toggleZenMode() { - this.zenMode = !this.zenMode; - this.$nextTick(() => { - if (this.$refs.header) { - this.$store.commit('setUiHeaderHeight', this.$refs.header.$el.offsetHeight); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui - min-height 100vh - padding-top 48px - - > .bg - position fixed - top 0 - left 0 - width 100% - height 100vh - background-size cover - background-position center - background-attachment fixed - - > .content.sidebar.left - padding-left 68px - - > .content.sidebar.right - padding-right 68px - - > .content.zen - padding 0 !important - -</style> diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue deleted file mode 100644 index dae282ec5cc75d4506929c6aca47533a8ecb5405..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/user-list-timeline.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> - <template #header> - <slot></slot> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['list'], - data() { - return { - connection: null, - date: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - watch: { - $route: 'init' - }, - mounted() { - this.init(); - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - init() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - onNote(note) { - (this.$refs.timeline as any).prepend(note); - }, - onUserAdded() { - (this.$refs.timeline as any).reload(); - }, - onUserRemoved() { - (this.$refs.timeline as any).reload(); - }, - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue deleted file mode 100644 index 9328648ccb43bfc3d8d389a9634c7a7f749da50a..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/user-preview.vue +++ /dev/null @@ -1,164 +0,0 @@ -<template> -<div class="mk-user-preview"> - <template v-if="u != null"> - <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div> - <mk-avatar class="avatar" :user="u" :disable-preview="true"/> - <div class="title"> - <router-link class="name" :to="u | userPage"><mk-user-name :user="u" :nowrap="false"/></router-link> - <p class="username"><mk-acct :user="u"/></p> - </div> - <div class="description"> - <mfm v-if="u.description" :text="u.description" :author="u" :i="$store.state.i" :custom-emojis="u.emojis"/> - </div> - <div class="status"> - <div> - <p>{{ $t('notes') }}</p><span>{{ u.notesCount }}</span> - </div> - <div> - <p>{{ $t('following') }}</p><span>{{ u.followingCount }}</span> - </div> - <div> - <p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span> - </div> - </div> - <mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import anime from 'animejs'; -import parseAcct from '../../../../../misc/acct/parse'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/user-preview.vue'), - props: { - user: { - type: [Object, String], - required: true - } - }, - data() { - return { - u: null - }; - }, - mounted() { - if (typeof this.user == 'object') { - this.u = this.user; - this.$nextTick(() => { - this.open(); - }); - } else { - const query = this.user.startsWith('@') ? - parseAcct(this.user.substr(1)) : - { userId: this.user }; - - this.$root.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.destroyDom() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-user-preview - position absolute - z-index 2048 - margin-top -8px - width 250px - background var(--face) - background-clip content-box - border solid 1px rgba(#000, 0.1) - border-radius 4px - overflow hidden - opacity 0 - - > .banner - height 84px - background-color rgba(0, 0, 0, 0.1) - background-size cover - background-position center - - > .avatar - display block - position absolute - top 62px - left 13px - z-index 2 - width 58px - height 58px - border solid 3px var(--face) - border-radius 8px - - > .title - display block - padding 8px 0 8px 82px - - > .name - display inline-block - margin 0 - font-weight bold - line-height 16px - color var(--text) - - > .username - display block - margin 0 - line-height 16px - font-size 0.8em - color var(--text) - opacity 0.7 - - > .description - padding 0 16px - font-size 0.7em - color var(--text) - - > .status - padding 8px 16px - - > div - display inline-block - width 33% - - > p - margin 0 - font-size 0.7em - color var(--text) - - > span - font-size 1em - color var(--primary) - - > .koudoku-button - position absolute - top 8px - right 8px - -</style> diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue deleted file mode 100644 index 499f4e7c91584141de0ed00e0d75c9c29603fb94..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/components/window.vue +++ /dev/null @@ -1,620 +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" - @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown" - > - <h1><slot name="header"></slot></h1> - <div> - <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" :title="$t('popout')"> - <i><fa :icon="['far', 'window-restore']"/></i> - </button> - <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" :title="$t('close')"> - <i><fa icon="times"/></i> - </button> - </div> - </header> - <div class="content"> - <slot></slot> - </div> - </div> - <template v-if="canResize"> - <div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div> - <div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div> - <div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div> - <div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div> - <div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div> - <div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div> - <div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div> - <div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div> - </template> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import 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({ - i18n: i18n('desktop/views/components/window.vue'), - 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 - }, - animation: { - type: Boolean, - required: false, - default: true - } - }, - - computed: { - isFlexible(): boolean { - return this.height == 'auto'; - }, - canResize(): boolean { - return !this.isFlexible; - } - }, - - created() { - // ウィンドウをウィンドウシステムã«ç™»éŒ² - this.$root.os.windows.add(this); - }, - - mounted() { - 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.$root.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: this.animation ? 100 : 0, - easing: 'linear' - }); - } - - main.style.pointerEvents = 'auto'; - anime({ - targets: main, - opacity: 1, - scale: [1.1, 1], - duration: this.animation ? 200 : 0, - easing: 'easeOutQuad' - }); - - if (focus) main.focus(); - - setTimeout(() => { - this.$emit('opened'); - }, this.animation ? 300 : 0); - }, - - 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: this.animation ? 300 : 0, - easing: 'linear' - }); - } - - main.style.pointerEvents = 'none'; - - anime({ - targets: main, - opacity: 0, - scale: 0.8, - duration: this.animation ? 300 : 0, - easing: 'cubicBezier(0.5, -0.5, 1, 0.5)' - }); - - setTimeout(() => { - this.$emit('closed'); - this.destroyDom(); - }, this.animation ? 300 : 0); - }, - - 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; - - const ws = Array.from(this.$root.os.windows.getAll()).filter(w => w != this); - for (const w of ws) { - 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 + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; - - // å·¦ã¯ã¿å‡ºã— - if (moveLeft < 0) moveLeft = 0; - - // 上ã¯ã¿å‡ºã— - if (moveTop < 0) moveTop = 0; - - // å³ã¯ã¿å‡ºã— - 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 + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下ã¯ã¿å‡ºã— - if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // å³ã¯ã¿å‡ºã— - if (position.top < 0) main.style.top = 0; // 上ã¯ã¿å‡ºã— - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-window - display block - - > .bg - display block - position fixed - z-index 2000 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 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 0 1px var(--primaryAlpha05), 0 2px 12px 0 var(--desktopWindowShadow) - - > .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 var(--face) - border-radius 6px - box-shadow 0 2px 12px 0 rgba(#000, 0.5) - - > header - $header-height = 40px - - z-index 1001 - height $header-height - overflow hidden - white-space nowrap - cursor move - background var(--faceHeader) - border-radius 6px 6px 0 0 - box-shadow 0 1px 0 rgba(#000, 0.1) - - &, * - 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 var(--desktopWindowTitle) - - > 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 var(--faceTextButton) - border none - outline none - background transparent - - &:hover - color var(--faceTextButtonHover) - - &:active - color var(--faceTextButtonActive) - - > i - display inline-block - padding 0 - width $header-height - line-height $header-height - text-align center - - > .content - height 100% - overflow auto - - &:not([flexible]) - > .main > .body > .content - height calc(100% - 40px) - -</style> diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue deleted file mode 100644 index ec166d42c8aba844cb6f667b4ec7516c9299690c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/home.vue +++ /dev/null @@ -1,406 +0,0 @@ -<template> -<component :is="customize ? 'mk-dummy' : 'mk-ui'" v-hotkey.global="keymap" v-if="$store.getters.isSignedIn || $route.name != 'index'"> - <div class="wqsofvpm" :data-customize="customize"> - <div class="customize" v-if="customize"> - <a @click="done()"><fa icon="check"/>{{ $t('done') }}</a> - <div> - <div class="adder"> - <p>{{ $t('add-widget') }}</p> - <select v-model="widgetAdderSelected"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="trends">{{ $t('@.widgets.trends') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> - <option value="notifications">{{ $t('@.widgets.notifications') }}</option> - <option value="users">{{ $t('@.widgets.users') }}</option> - <option value="polls">{{ $t('@.widgets.polls') }}</option> - <option value="post-form">{{ $t('@.widgets.post-form') }}</option> - <option value="messaging">{{ $t('@.messaging') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - <button @click="addWidget">{{ $t('add') }}</button> - </div> - <div class="trash"> - <x-draggable v-model="trash" group="x" @add="onTrash"></x-draggable> - <p>{{ $t('@.trash') }}</p> - </div> - </div> - </div> - <div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }"> - <template v-if="customize"> - <x-draggable v-for="place in ['left', 'right']" - :list="widgets[place]" - :class="place" - :data-place="place" - 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" platform="desktop"/> - </div> - </x-draggable> - <div class="main"> - <a @click="hint">{{ $t('@.customization-tips.title') }}</a> - <div> - <x-timeline/> - </div> - </div> - </template> - <template v-else> - <div v-for="place in ['left', 'right']" :class="place" :key="place"> - <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/> - </div> - <div class="main"> - <router-view ref="content"></router-view> - </div> - </template> - </div> - </div> -</component> -<x-welcome v-else/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; -import XWelcome from '../pages/welcome.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/home.vue'), - - components: { - XDraggable, - XWelcome - }, - - data() { - return { - customize: window.location.search == '?customize', - connection: null, - widgetAdderSelected: null, - trash: [], - view: null - }; - }, - - computed: { - home(): any[] { - if (this.$store.getters.isSignedIn) { - return this.$store.getters.home || []; - } else { - return [{ - name: 'instance', - place: 'right' - }, { - name: 'broadcast', - place: 'right', - data: {} - }, { - name: 'hashtags', - place: 'right', - data: {} - }]; - } - }, - left(): any[] { - return this.home.filter(w => w.place == 'left'); - }, - right(): any[] { - return this.home.filter(w => w.place == 'right'); - }, - widgets(): any { - return { - left: this.left, - right: this.right - }; - }, - keymap(): any { - return { - 't': this.focus - }; - } - }, - - created() { - if (!this.$store.getters.isSignedIn) return; - - if (this.$store.getters.home == null) { - const defaultDesktopHomeWidgets = { - left: [ - 'profile', - 'calendar', - 'activity', - 'rss', - 'hashtags', - 'photo-stream', - 'version' - ], - right: [ - 'customize', - 'broadcast', - 'notifications', - 'users', - 'polls', - 'server', - 'nav', - 'tips' - ] - }; - - //#region Construct home data - const _defaultDesktopHomeWidgets = []; - - for (const widget of defaultDesktopHomeWidgets.left) { - _defaultDesktopHomeWidgets.push({ - name: widget, - id: uuid(), - place: 'left', - data: {} - }); - } - - for (const widget of defaultDesktopHomeWidgets.right) { - _defaultDesktopHomeWidgets.push({ - name: widget, - id: uuid(), - place: 'right', - data: {} - }); - } - //#endregion - - this.$store.commit('setHome', _defaultDesktopHomeWidgets); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - hint() { - this.$root.dialog({ - title: this.$t('@.customization-tips.title'), - text: this.$t('@.customization-tips.paragraph') - }); - }, - - onTlLoaded() { - this.$emit('loaded'); - }, - - onWidgetContextmenu(widgetId) { - const w = (this.$refs[widgetId] as any)[0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveHome(); - }, - - onTrash(evt) { - this.saveHome(); - }, - - addWidget() { - if(this.widgetAdderSelected == null) return; - - this.$store.commit('addHomeWidget', { - name: this.widgetAdderSelected, - id: uuid(), - place: 'left', - data: {} - }); - }, - - saveHome() { - const left = this.widgets.left; - const right = this.widgets.right; - this.$store.commit('setHome', left.concat(right)); - for (const w of left) w.place = 'left'; - for (const w of right) w.place = 'right'; - }, - - done() { - location.href = '/'; - }, - - focus() { - (this.$refs.content as any).focus(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wqsofvpm - 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 > *:not(.main):empty - display none - - > .customize - position fixed - z-index 1000 - top 0 - left 0 - width 100% - height 48px - color var(--text) - background var(--desktopHeaderBg) - box-shadow 0 1px 1px rgba(#000, 0.075) - - > a - display block - position absolute - z-index 1001 - top 0 - right 0 - padding 0 16px - line-height 48px - text-decoration none - color var(--primaryForeground) - background var(--primary) - transition background 0.1s ease - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - transition background 0s ease - - > [data-icon] - margin-right 8px - - > div - display flex - margin 0 auto - max-width 1220px - 32px - - > div - width 50% - - &.adder - > p - display inline - line-height 48px - - &.trash - border-left solid 1px var(--faceDivider) - - > 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 1240px - - > * - .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% - 280px * 2) - order 2 - - &.side - > .main - width calc(100% - 280px) - max-width 680px - - > *:not(.main) - width 280px - padding 16px 0 16px 0 - - > *:not(:last-child) - margin-bottom 16px - - > .left - padding-left 16px - order 1 - - > .right - padding-right 16px - order 3 - - &.side - @media (max-width 1000px) - > *:not(.main) - display none - - > .main - width 100% - max-width 700px - margin 0 auto - - &:not(.side) - @media (max-width 1100px) - > *:not(.main) - display none - - > .main - width 100% - max-width 700px - margin 0 auto - -</style> diff --git a/src/client/app/desktop/views/home/note.vue b/src/client/app/desktop/views/home/note.vue deleted file mode 100644 index c19f58cd2b0f8fe07353c337f71e16819b0ab450..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/note.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<div v-if="!fetching" class="kcthdwmv"> - <mk-note-detail :note="note" :key="note.id"/> - <footer> - <router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link> - <router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/note.vue'), - data() { - return { - fetching: true, - note: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kcthdwmv - text-align center - - > footer - margin-top 16px - - > a - display inline-block - margin 0 16px - -</style> diff --git a/src/client/app/desktop/views/home/search.vue b/src/client/app/desktop/views/home/search.vue deleted file mode 100644 index 06b354b133045d7e71c152dcc6385190efc84a35..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/search.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"> - <template #header> - <header class="oxgbmvii"> - <span><fa icon="search"/> {{ q }}</span> - </header> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/search.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - computed: { - q(): string { - return this.$route.query.q; - } - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll, { passive: true }); - Progress.start(); - }, - 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(); - } - } - }, - inited() { - Progress.done(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.oxgbmvii - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > span - padding 0 8px - font-size 0.9em - line-height 42px - color var(--text) -</style> diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue deleted file mode 100644 index 343b4ce9519b88d1e2d7fe26ba82e9b1e3fee81b..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/tag.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @loaded="inited"> - <template #header> - <header class="wqraeznr"> - <span><fa icon="hashtag"/> {{ $route.params.tag }}</span> - </header> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/tag.vue'), - computed: { - pagination() { - return { - endpoint: 'notes/search-by-tag', - limit: 20, - params: { - tag: this.$route.params.tag - } - }; - } - }, - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - Progress.start(); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); - }, - methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.timeline as any).focus(); - } - } - }, - inited() { - Progress.done(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wqraeznr - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > span - padding 0 8px - font-size 0.9em - line-height 42px - color var(--text) -</style> diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue deleted file mode 100644 index aae7dbc60e42185750001c4c3d20300a43b3adf7..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/timeline.core.vue +++ /dev/null @@ -1,144 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> - <template #header> - <slot></slot> - <div v-if="src == 'home' && alone" class="ibpylqas"> - <p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p> - <router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link> - </div> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/timeline.core.vue'), - - props: { - src: { - type: String, - required: true - }, - tagTl: { - required: false - } - }, - - data() { - return { - connection: null, - date: null, - baseQuery: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }, - query: {}, - endpoint: null, - pagination: null - }; - }, - - computed: { - alone(): boolean { - return this.$store.state.i.followingCount == 0; - } - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - this.connection.dispose(); - }); - - const prepend = note => { - (this.$refs.timeline as any).prepend(note); - }; - - if (this.src == 'tag') { - this.endpoint = 'notes/search-by-tag'; - this.query = { - query: this.tagTl.query - }; - this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query }); - this.connection.on('note', prepend); - } else if (this.src == 'home') { - this.endpoint = 'notes/timeline'; - const onChangeFollowing = () => { - this.fetch(); - }; - this.connection = this.$root.stream.useSharedConnection('homeTimeline'); - this.connection.on('note', prepend); - this.connection.on('follow', onChangeFollowing); - this.connection.on('unfollow', onChangeFollowing); - } else if (this.src == 'local') { - this.endpoint = 'notes/local-timeline'; - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'global') { - this.endpoint = 'notes/global-timeline'; - this.connection = this.$root.stream.useSharedConnection('globalTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'mentions') { - this.endpoint = 'notes/mentions'; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', prepend); - } else if (this.src == 'messages') { - this.endpoint = 'notes/mentions'; - this.query = { - visibility: 'specified' - }; - const onNote = note => { - if (note.visibility == 'specified') { - prepend(note); - } - }; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', onNote); - } - - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - ...this.baseQuery, ...this.query - }) - }; - }, - - methods: { - focus() { - (this.$refs.timeline as any).focus(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.ibpylqas - padding 16px - text-align center - color var(--text) - border-bottom solid var(--lineWidth) var(--faceDivider) - font-size 14px - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue deleted file mode 100644 index 224b937997bee9c09dc86c39fc7eba44a093d0dd..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/timeline.vue +++ /dev/null @@ -1,278 +0,0 @@ -<template> -<div class="pwbzawku"> - <x-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/> - <div class="main"> - <component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options"> - <header class="zahtxcqi"> - <div :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</div> - <div :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</div> - <div :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</div> - <div :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</div> - <div :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</div> - <div :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</div> - <div class="buttons"> - <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> - <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> - <button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> - <button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> - </div> - </header> - </component> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XCore from './timeline.core.vue'; -import Menu from '../../../common/views/components/menu.vue'; -import MkSettingsWindow from '../components/settings-window.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/components/timeline.vue'), - - components: { - XCore, - XPostForm: () => import('../components/post-form.vue').then(m => m.default) - }, - - data() { - return { - src: 'home', - list: null, - tagTl: null, - enableLocalTimeline: false, - enableGlobalTimeline: false, - }; - }, - - computed: { - options(): any { - return { - ...(this.src == 'list' ? { list: this.list } : { src: this.src }), - ...(this.src == 'tag' ? { tagTl: this.tagTl } : {}), - key: this.src == 'list' ? this.list.id : this.src - } - } - }, - - watch: { - src() { - this.saveSrc(); - }, - - list(x) { - this.saveSrc(); - if (x != null) this.tagTl = null; - }, - - tagTl(x) { - this.saveSrc(); - if (x != null) this.list = null; - } - }, - - created() { - this.$root.getMeta().then((meta: Record<string, any>) => { - if (!( - this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && this.src === 'global') this.src = 'local'; - if (!( - this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; - }); - - if (this.$store.state.device.tl) { - this.src = this.$store.state.device.tl.src; - if (this.src == 'list') { - this.list = this.$store.state.device.tl.arg; - } else if (this.src == 'tag') { - this.tagTl = this.$store.state.device.tl.arg; - } - } - }, - - mounted() { - document.title = this.$root.instanceName; - - (this.$refs.tl as any).$once('loaded', () => { - this.$emit('loaded'); - }); - }, - - methods: { - saveSrc() { - this.$store.commit('device/setTl', { - src: this.src, - arg: this.src == 'list' ? this.list : this.tagTl - }); - }, - - focus() { - (this.$refs.tl as any).focus(); - }, - - warp(date) { - (this.$refs.tl as any).warp(date); - }, - - async chooseList() { - const lists = await this.$root.api('users/lists/list'); - - let menu = [{ - icon: 'plus', - text: this.$t('add-list'), - action: () => { - this.$root.dialog({ - title: this.$t('list-name'), - input: true - }).then(async ({ canceled, result: name }) => { - if (canceled) return; - const list = await this.$root.api('users/lists/create', { - name - }); - - this.list = list; - this.src = 'list'; - }); - } - }]; - - if (lists.length > 0) { - menu.push(null); - } - - menu = menu.concat(lists.map(list => ({ - icon: 'list', - text: list.name, - action: () => { - this.list = list; - this.src = 'list'; - } - }))); - - this.$root.new(Menu, { - source: this.$refs.listButton, - items: menu - }); - }, - - chooseTag() { - let menu = [{ - icon: 'plus', - text: this.$t('add-tag-timeline'), - action: () => { - this.$root.new(MkSettingsWindow, { - initialPage: 'hashtags' - }); - } - }]; - - if (this.$store.state.settings.tagTimelines.length > 0) { - menu.push(null); - } - - menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ - icon: 'hashtag', - text: t.title, - action: () => { - this.tagTl = t; - this.src = 'tag'; - } - }))); - - this.$root.new(Menu, { - source: this.$refs.tagButton, - items: menu - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.pwbzawku - > .form - margin-bottom 16px - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - header.zahtxcqi - display flex - flex-wrap wrap - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) - - > * - flex-shrink 0 - - > .buttons - margin-left auto - - > button - padding 0 8px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - > .indicator - position absolute - top -4px - right 4px - font-size 10px - color var(--notificationIndicator) - animation blink 1s infinite - - &:hover - color var(--faceTextButtonHover) - - &[data-active] - color var(--primary) - cursor default - - &:before - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 2px - background var(--primary) - - > div:not(.buttons) - padding 0 10px - line-height 42px - font-size 12px - user-select none - - &[data-active] - color var(--primary) - cursor default - font-weight bold - - &:before - content "" - display block - position absolute - bottom 0 - left -8px - width calc(100% + 16px) - height 2px - background var(--primary) - - &:not([data-active]) - color var(--desktopTimelineSrc) - cursor pointer - - &:hover - color var(--desktopTimelineSrcHover) - -</style> diff --git a/src/client/app/desktop/views/home/user/index.vue b/src/client/app/desktop/views/home/user/index.vue deleted file mode 100644 index 98ad165d936092196b02f5934198daab0fe53623..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/user/index.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<div class="omechnps" v-if="!fetching"> - <div class="is-suspended" v-if="user.isSuspended" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }} - </div> - <div class="is-remote" v-if="user.host != null" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a> - </div> - <div class="main"> - <x-header class="header" :user="user"/> - <router-view :user="user"></router-view> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import parseAcct from '../../../../../../misc/acct/parse'; -import Progress from '../../../../common/scripts/loading'; -import XHeader from './user.header.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XHeader - }, - data() { - return { - fetching: true, - user: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - Progress.start(); - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - Progress.done(); - }); - }, - - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -.omechnps - width 100% - margin 0 auto - - > .is-suspended - > .is-remote - margin-bottom 16px - padding 14px 16px - font-size 14px - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - &.is-suspended - color var(--suspendedInfoFg) - background var(--suspendedInfoBg) - - &.is-remote - color var(--remoteInfoFg) - background var(--remoteInfoBg) - - > a - font-weight bold - - > .main - > .header - margin-bottom 16px - -</style> diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue deleted file mode 100644 index c8e777967845ee334941bc7c07ebcbb107786c96..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/user/user.header.vue +++ /dev/null @@ -1,292 +0,0 @@ -<template> -<div class="header" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <div class="banner-container" :style="style"> - <div class="banner" ref="banner" :style="style"></div> - <div class="fade"></div> - <div class="title"> - <p class="name"> - <mk-user-name :user="user" :nowrap="false"/> - </p> - <div> - <span class="username"><mk-acct :user="user" :detail="true" /></span> - <span v-if="user.isBot" :title="$t('is-bot')"><fa icon="robot"/></span> - </div> - </div> - <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('follows-you') }}</span> - <div class="actions" v-if="$store.getters.isSignedIn"> - <button @click="menu" class="menu" ref="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" class="follow"/> - </div> - </div> - <mk-avatar class="avatar" :user="user" :disable-preview="true"/> - <div class="body"> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - <p v-else class="empty">{{ $t('no-description') }}</p> - <x-integrations :user="user" style="margin-top:16px;"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="info"> - <span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span> - <span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> - </div> - <div class="status"> - <router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link> - <router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link> - <router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as age from 's-age'; -import XUserMenu from '../../../../common/views/components/user-menu.vue'; -import XIntegrations from '../../../../common/views/components/integrations.vue'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/user/user.header.vue'), - components: { - XIntegrations - }, - props: ['user'], - computed: { - style(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - }, - - age(): number { - return age(this.user.birthday); - } - }, - mounted() { - if (this.user.bannerUrl) { - //window.addEventListener('load', this.onScroll); - //window.addEventListener('scroll', this.onScroll, { passive: true }); - //window.addEventListener('resize', this.onScroll); - } - }, - beforeDestroy() { - if (this.user.bannerUrl) { - //window.removeEventListener('load', this.onScroll); - //window.removeEventListener('scroll', this.onScroll); - //window.removeEventListener('resize', this.onScroll); - } - }, - methods: { - mention() { - this.$post({ mention: this.user }); - }, - 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)`; - }, - - menu() { - const w = this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - this.$once('hook:beforeDestroy', () => { - w.destroyDom(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.header - background var(--face) - overflow hidden - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > .banner-container - height 250px - overflow hidden - background-size cover - background-position center - - > .banner - height 100% - background-color #383838 - background-size cover - background-position center - box-shadow 0 0 128px rgba(0, 0, 0, 0.5) inset - - > .fade - position absolute - bottom 0 - left 0 - width 100% - height 78px - background linear-gradient(transparent, rgba(#000, 0.7)) - - > .followed - position absolute - top 12px - left 12px - padding 4px 6px - color #fff - background rgba(0, 0, 0, 0.7) - font-size 12px - - > .actions - position absolute - top 12px - right 12px - - > .menu - height 100% - padding 0 14px - color #fff - text-shadow 0 0 8px #000 - font-size 16px - - > .title - position absolute - bottom 0 - left 0 - width 100% - padding 0 0 8px 154px - color #fff - - > .name - display block - margin 0 - line-height 32px - font-weight bold - font-size 1.8em - text-shadow 0 0 8px #000 - - > div - > * - display inline-block - margin-right 16px - line-height 20px - opacity 0.8 - - &.username - font-weight bold - - > .avatar - display block - position absolute - top 170px - left 16px - z-index 2 - width 120px - height 120px - box-shadow 1px 1px 3px rgba(#000, 0.2) - - > &.cat::before, - > &.cat::after - border-width 8px - - > .body - padding 16px 16px 16px 154px - color var(--text) - - > .description - font-size 15px - - > .empty - margin 0 - opacity 0.5 - - > .fields - margin-top 16px - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - border-right solid 1px var(--faceDivider) - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - text-align center - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - - > .info - margin-top 16px - padding-top 16px - border-top solid 1px var(--faceDivider) - font-size 15px - - &:empty - display none - - > * - margin-right 16px - - > .status - margin-top 16px - padding-top 16px - border-top solid 1px var(--faceDivider) - font-size 80% - - > * - display inline-block - padding-right 16px - margin-right 16px - color inherit - - &:not(:last-child) - border-right solid 1px var(--faceDivider) - - &.clickable - cursor pointer - - &:hover - color var(--faceTextButtonHover) - - > b - margin-right 4px - font-size 1rem - font-weight bold - color var(--primary) - -</style> diff --git a/src/client/app/desktop/views/home/user/user.home.vue b/src/client/app/desktop/views/home/user/user.home.vue deleted file mode 100644 index c47e0a0771d7e721ceccea1e246a2bb5e1511c70..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/user/user.home.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="lnctpgve"> - <x-page v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> - <!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>--> - <div class="activity"> - <ui-container :body-togglable="true" - :expanded="$store.state.device.expandUsersActivity" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <x-activity :user="user" :limit="35" style="padding: 16px;"/> - </ui-container> - </div> - <x-photos :user="user"/> - <x-timeline ref="tl" :user="user"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XTimeline from './user.timeline.vue'; -import XPhotos from './user.photos.vue'; -import XActivity from '../../../../common/views/components/activity.vue'; -import XPage from '../../../../common/views/components/page/page.vue'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XTimeline, - XPhotos, - XActivity, - XPage, - }, - props: { - user: { - type: Object, - required: true - } - }, - methods: { - warp(date) { - (this.$refs.tl as any).warp(date); - } - } -}); -</script> - -<style lang="stylus" scoped> -.lnctpgve - > * - margin-bottom 16px - -</style> diff --git a/src/client/app/desktop/views/home/user/user.photos.vue b/src/client/app/desktop/views/home/user/user.photos.vue deleted file mode 100644 index 03abcf865c0a5aedc64486906fcd186d0f275f24..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/user/user.photos.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<ui-container :body-togglable="true" - :expanded="$store.state.device.expandUsersPhotos" - @toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })"> - <template #header><fa icon="camera"/> {{ $t('title') }}</template> - - <div class="dzsuvbsrrrwobdxifudxuefculdfiaxd"> - <p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> - <div class="stream" v-if="!fetching && images.length > 0"> - <router-link v-for="image in images" class="img" - :style="`background-image: url(${image.thumbnailUrl})`" - :key="`${image.id}:${image._note.id}`" - :to="image._note | notePage" - :title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`" - ></router-link> - </div> - <p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> - </div> -</ui-container> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; -import { concat } from '../../../../../../prelude/array'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/user/user.photos.vue'), - props: ['user'], - data() { - return { - images: [], - fetching: true - }; - }, - mounted() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: !this.$store.state.device.alwaysShowNsfw, - limit: 9, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - file._note = note; - } - } - const files = concat(notes.map((n: any): any[] => n.files)); - this.images = files.filter(f => image.includes(f.type)).slice(0, 9); - this.fetching = false; - }); - }, - methods: { - thumbnail(image: any): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.dzsuvbsrrrwobdxifudxuefculdfiaxd - > .stream - display grid - grid-template-columns 1fr 1fr 1fr - gap 8px - padding 16px - background var(--face) - - > * - height 120px - background-position center center - background-size cover - background-clip content-box - border-radius 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > i - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/home/user/user.timeline.vue b/src/client/app/desktop/views/home/user/user.timeline.vue deleted file mode 100644 index 2a97f2c96e2b64c443de27cb11a689c2ce9a00af..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/home/user/user.timeline.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div> - <mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"> - <template #header> - <header class="kugajpep"> - <span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span> - <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span> - <span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span> - <span :data-active="mode == 'my-posts'" @click="mode = 'my-posts'"><fa icon="user"/> {{ $t('my-posts') }}</span> - </header> - </template> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/user/user.timeline.vue'), - - props: ['user'], - - data() { - return { - fetching: true, - mode: 'default', - unreadCount: 0, - date: null, - pagination: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeReplies: this.mode == 'with-replies', - includeMyRenotes: this.mode != 'my-posts', - withFiles: this.mode == 'with-media', - }) - } - }; - }, - - watch: { - mode() { - (this.$refs.timeline as any).reload(); - } - }, - - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - document.removeEventListener('keydown', this.onDocumentKeydown); - }); - }, - - methods: { - onDocumentKeydown(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.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kugajpep - padding 0 8px - z-index 10 - background var(--faceHeader) - box-shadow 0 1px var(--desktopTimelineHeaderShadow) - - > span - display inline-block - padding 0 10px - line-height 42px - font-size 12px - user-select none - - &[data-active] - color var(--primary) - cursor default - font-weight bold - - &:before - content "" - display block - position absolute - bottom 0 - left -8px - width calc(100% + 16px) - height 2px - background var(--primary) - - &:not([data-active]) - color var(--desktopTimelineSrc) - cursor pointer - - &:hover - color var(--desktopTimelineSrcHover) - -</style> diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue deleted file mode 100644 index b389392ec800208c119b9e03dd375d83308cd791..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/pages/drive.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="mk-drive-page"> - <x-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/drive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - folder: null - }; - }, - created() { - this.folder = this.$route.params.folder; - }, - mounted() { - document.title = this.$t('title'); - }, - methods: { - onMoveRoot() { - const title = this.$t('title'); - - // Rewrite URL - history.pushState(null, title, '/i/drive'); - - document.title = title; - }, - onOpenFolder(folder) { - const title = `${folder.name} | ${this.$t('title')}`; - - // 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/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue deleted file mode 100644 index b859b95d7fe217e75f3d4da42f369a5bea128364..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/pages/games/reversi.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<component :is="ui ? 'mk-ui' : 'div'"> - <x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> -</component> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - components: { - XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - props: { - ui: { - default: false - } - }, - methods: { - nav(game, actualNav) { - if (actualNav) { - this.$router.push(`/games/reversi/${game.id}`); - } else { - // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push(`/games/reversi/${game.id}`); - } - } - } -}); -</script> diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue deleted file mode 100644 index c725074b7d92dcde47a83c6b27a5baf769e075e5..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ /dev/null @@ -1,82 +0,0 @@ -<template> -<div class="mk-messaging-room-page"> - <x-messaging-room v-if="user || group" :user="user" :group="group" :is-naked="true"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../misc/acct/parse'; -import getUserName from '../../../../../misc/get-user-name'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - data() { - return { - fetching: true, - user: null, - group: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - const applyBg = v => - document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); - - applyBg(this.$store.state.device.darkmode); - - this.unwatchDarkmode = this.$store.watch(s => { - return s.device.darkmode; - }, applyBg); - - this.fetch(); - }, - beforeDestroy() { - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); // for safari's bug - this.unwatchDarkmode(); - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - if (this.$route.params.user) { - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + getUserName(this.user); - - Progress.done(); - }); - } else { - this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { - this.group = group; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + this.group.name; - - Progress.done(); - }); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-messaging-room-page - display flex - flex 1 - flex-direction column - min-height 100% - -</style> diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue deleted file mode 100644 index 7e6a8e1937729dc19296303682a165bddb7f283b..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/pages/selectdrive.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<div class="mkp-selectdrive"> - <x-drive ref="browser" - :multiple="multiple" - @selected="onSelected" - @change-selection="onChangeSelection" - /> - <footer> - <button class="upload" :title="$t('upload')" @click="upload"><fa icon="upload"/></button> - <button class="cancel" @click="close">{{ $t('cancel') }}</button> - <button class="ok" @click="ok">{{ $t('ok') }}</button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/selectdrive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = this.$t('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> - - -.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 var(--primaryLighten95) - - .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 var(--primaryAlpha05) - background transparent - outline none - border solid 1px transparent - border-radius 4px - - &:hover - background transparent - border-color var(--primaryAlpha03) - - &:active - color var(--primaryAlpha06) - background transparent - border-color var(--primaryAlpha05) - //box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid var(--primaryAlpha03) - 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 var(--primaryAlpha03) - border-radius 8px - - &:disabled - opacity 0.7 - cursor default - - .ok - right 16px - color var(--primaryForeground) - background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%) - border solid 1px var(--primaryLighten15) - - &:not(:disabled) - font-weight bold - - &:hover:not(:disabled) - background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%) - border-color var(--primary) - - &:active:not(:disabled) - background var(--primary) - border-color var(--primary) - - .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/client/app/desktop/views/pages/settings.vue b/src/client/app/desktop/views/pages/settings.vue deleted file mode 100644 index 826fae25290de5049b3892b37511a6eaf0ed551d..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/pages/settings.vue +++ /dev/null @@ -1,27 +0,0 @@ -<template> -<mk-ui> - <main> - <x-settings :in-window="false" :page="$route.params.page" /> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - components: { - XSettings: () => import('../components/settings.vue').then(m => m.default) - }, - mounted() { - document.title = this.$root.instanceName; - }, -}); -</script> - -<style lang="stylus" scoped> -main - margin 0 auto - max-width 900px - -</style> diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue deleted file mode 100644 index 511e1548e5d8704ff0fc59e991dd110847d5fac6..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/pages/welcome.vue +++ /dev/null @@ -1,509 +0,0 @@ -<template> -<div class="mk-welcome"> - <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> - - <button @click="dark"> - <template v-if="$store.state.device.darkmode"><fa icon="moon"/></template> - <template v-else><fa :icon="['far', 'moon']"/></template> - </button> - - <mk-forkit class="forkit"/> - - <main> - <div class="body"> - <div class="main block"> - <div> - <h1 v-if="name != null && name != ''">{{ name }}</h1> - <h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1> - - <div class="info"> - <span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span> - <span class="stats" v-if="stats"> - <span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span> - <span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span> - </span> - </div> - - <div class="desc"> - <span class="desc" v-html="description || $t('@.about')"></span> - <a class="about" @click="about">{{ $t('about') }}</a> - </div> - - <p class="sign"> - <span class="signup" @click="signup">{{ $t('@.signup') }}</span> - <span class="divider">|</span> - <span class="signin" @click="signin">{{ $t('@.signin') }}</span> - </p> - - <img v-if="meta" :src="meta.mascotImageUrl" alt="" title="è—" class="char"> - </div> - </div> - - <div class="announcements block"> - <header><fa icon="broadcast-tower"/> {{ $t('announcements') }}</header> - <div v-if="announcements && announcements.length > 0"> - <div v-for="announcement in announcements"> - <h1 v-html="announcement.title"></h1> - <mfm :text="announcement.text"/> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/> - </div> - </div> - </div> - - <div class="photos block"> - <header><fa :icon="['far', 'images']"/> {{ $t('photos') }}</header> - <div> - <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> - </div> - </div> - - <div class="tag-cloud block"> - <div> - <mk-tag-cloud/> - </div> - </div> - - <div class="nav block"> - <div> - <mk-nav class="nav"/> - </div> - </div> - - <div class="side"> - <div class="trends block"> - <div> - <mk-trends/> - </div> - </div> - - <div class="tl block"> - <header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</header> - <div> - <mk-welcome-timeline class="tl" :max="20"/> - </div> - </div> - - <div class="info block"> - <header><fa icon="info-circle"/> {{ $t('info') }}</header> - <div> - <div v-if="meta" class="body"> - <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - </div> - </div> - </div> - </div> - </div> - </main> - - <modal name="about" class="about modal" width="800px" height="auto" scrollable> - <article class="fpdezooorhntlzyeszemrsqdlgbysvxq"> - <h1>{{ $t('@.intro.title') }}</h1> - <p v-html="this.$t('@.intro.about')"></p> - <section> - <h2>{{ $t('@.intro.features') }}</h2> - <section> - <div class="body"> - <h3>{{ $t('@.intro.rich-contents') }}</h3> - <p v-html="this.$t('@.intro.rich-contents-desc')"></p> - </div> - <div class="image"><img src="/assets/about/post.png" alt=""></div> - </section> - <section> - <div class="body"> - <h3>{{ $t('@.intro.reaction') }}</h3> - <p v-html="this.$t('@.intro.reaction-desc')"></p> - </div> - <div class="image"><img src="/assets/about/reaction.png" alt=""></div> - </section> - <section> - <div class="body"> - <h3>{{ $t('@.intro.ui') }}</h3> - <p v-html="this.$t('@.intro.ui-desc')"></p> - </div> - <div class="image"><img src="/assets/about/ui.png" alt=""></div> - </section> - <section> - <div class="body"> - <h3>{{ $t('@.intro.drive') }}</h3> - <p v-html="this.$t('@.intro.drive-desc')"></p> - </div> - <div class="image"><img src="/assets/about/drive.png" alt=""></div> - </section> - </section> - <p v-html="this.$t('@.intro.outro')"></p> - </article> - </modal> - - <modal name="signup" class="modal" width="450px" height="auto" scrollable> - <header class="formHeader">{{ $t('@.signup') }}</header> - <mk-signup class="form"/> - </modal> - - <modal name="signin" class="modal" width="450px" height="auto" scrollable> - <header class="formHeader">{{ $t('@.signin') }}</header> - <mk-signin class="form"/> - </modal> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { host, copyright } from '../../../config'; -import { concat } from '../../../../../prelude/array'; -import { toUnicode } from 'punycode'; - -export default Vue.extend({ - i18n: i18n('desktop/views/pages/welcome.vue'), - data() { - return { - meta: null, - stats: null, - banner: null, - copyright, - host: toUnicode(host), - name: null, - description: '', - announcements: [], - photos: [] - }; - }, - - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.name = meta.name; - this.description = meta.description; - this.announcements = meta.announcements; - this.banner = meta.bannerUrl; - }); - - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('notes/local-timeline', { - fileType: image, - excludeNsfw: true, - limit: 6 - }).then((notes: any[]) => { - const files = concat(notes.map((n: any): any[] => n.files)); - this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); - }); - }, - - methods: { - about() { - this.$modal.show('about'); - }, - - signup() { - this.$modal.show('signup'); - }, - - signin() { - this.$modal.show('signin'); - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - } - } -}); -</script> - -<style lang="stylus"> -#wait - right auto - left 15px - -.v--modal-overlay - background rgba(0, 0, 0, 0.6) - -.modal - .form - padding 24px 48px 48px 48px - - .formHeader - text-align center - padding 48px 0 12px 0 - margin 0 48px - font-size 1.5em - - .v--modal-box - background var(--face) - color var(--text) - - .formHeader - border-bottom solid 1px rgba(#000, 0.2) - -.v--modal-overlay.about - .v--modal-box.v--modal - margin 32px 0 - -.fpdezooorhntlzyeszemrsqdlgbysvxq - padding 64px - - > p:last-child - margin-bottom 0 - - > h1 - margin-top 0 - - > section - > h2 - border-bottom 1px solid var(--faceDivider) - - > section - display grid - grid-template-rows 1fr - grid-template-columns 180px 1fr - gap 32px - margin-bottom 32px - padding-bottom 32px - border-bottom 1px solid var(--faceDivider) - - &:nth-child(odd) - grid-template-columns 1fr 180px - - > .body - grid-column 1 - - > .image - grid-column 2 - - > .body - grid-row 1 - grid-column 2 - - > .image - grid-row 1 - grid-column 1 - - > img - display block - width 100% - height 100% - object-fit cover -</style> - -<style lang="stylus" scoped> -.mk-welcome - display flex - min-height 100vh - - > .banner - position absolute - top 0 - left 0 - width 100% - height 400px - background-position center - background-size cover - opacity 0.7 - - &:after - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 100px - background linear-gradient(transparent, var(--bg)) - - > .forkit - position absolute - top 0 - right 0 - - > button - position fixed - z-index 1 - bottom 16px - left 16px - padding 16px - font-size 18px - color var(--text) - - > main - margin 0 auto - padding 64px - width 100% - max-width 1200px - - .block - color var(--text) - background var(--face) - overflow auto - - > header - z-index 1 - padding 0 16px - line-height 48px - background var(--faceHeader) - box-shadow 0 1px 0 rgba(0, 0, 0, 0.1) - - & + div - max-height calc(100% - 48px) - - > div - overflow auto - - > .body - display grid - grid-template-rows 390px 1fr 256px 64px - grid-template-columns 1fr 1fr 350px - gap 16px - height 1150px - - > .main - grid-row 1 - grid-column 1 / 3 - - > div - padding 32px - min-height 100% - - > h1 - margin 0 - - > svg - margin -8px 0 0 -16px - width 280px - height 100px - fill currentColor - - > .info - margin 0 auto 16px auto - width $width - font-size 14px - - > .stats - margin-left 16px - padding-left 16px - border-left solid 1px var(--faceDivider) - - > * - margin-right 16px - - > .desc - max-width calc(100% - 150px) - - > .sign - font-size 120% - margin-bottom 0 - - > .divider - margin 0 16px - - > .signin - > .signup - cursor pointer - - &:hover - color var(--primary) - - > .char - display block - position absolute - right 16px - bottom 0 - height 320px - opacity 0.7 - - > *:not(.char) - z-index 1 - - > .announcements - grid-row 2 - grid-column 1 - - > div - padding 32px - - > div - padding 0 0 16px 0 - margin 0 0 16px 0 - border-bottom 1px solid var(--faceDivider) - - > h1 - margin 0 - font-size 1.25em - - > .photos - grid-row 2 - grid-column 2 - - > div - display grid - grid-template-rows 1fr 1fr 1fr - grid-template-columns 1fr 1fr - gap 8px - height 100% - padding 16px - - > div - //border-radius 4px - background-position center center - background-size cover - - > .tag-cloud - grid-row 3 - grid-column 1 / 3 - - > div - height 256px - padding 32px - - > .nav - display flex - justify-content center - align-items center - grid-row 4 - grid-column 1 / 3 - font-size 14px - - > .side - display grid - grid-row 1 / 5 - grid-column 3 - grid-template-rows 1fr 350px - grid-template-columns 1fr - gap 16px - - > .tl - grid-row 1 - grid-column 1 - overflow auto - - > .trends - grid-row 2 - grid-column 1 - padding 8px - - > .info - grid-row 3 - grid-column 1 - - > div - padding 16px - - > .body - > p - display block - margin 0 - -</style> diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue deleted file mode 100644 index 73c6d0ef647be3ffb07d8be0ff83b5c4c50fe45c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/activity.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> -<mk-activity - :design="props.design" - :init-view="props.view" - :user="$store.state.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++; - } - this.save(); - }, - viewChanged(view) { - this.props.view = view; - this.save(); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/widgets/customize.vue b/src/client/app/desktop/views/widgets/customize.vue deleted file mode 100644 index eb719103820832e5815b4da1e46542f2db0697a2..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/customize.vue +++ /dev/null @@ -1,21 +0,0 @@ -<template> -<div class="mkw-customize"> - <ui-button @click="customize()">{{ $t('@.customize-home') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'customize', -}).extend({ - i18n: i18n(), - methods: { - customize(date) { - location.href = '/?customize'; - } - } -}); -</script> diff --git a/src/client/app/desktop/views/widgets/index.ts b/src/client/app/desktop/views/widgets/index.ts deleted file mode 100644 index c00cd2ff4dba8ad1ed59d2139f031db7bd83dd5c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/index.ts +++ /dev/null @@ -1,21 +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 wMessaging from './messaging.vue'; -import wProfile from './profile.vue'; -import wCustomize from './customize.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-messaging', wMessaging); -Vue.component('mkw-profile', wProfile); -Vue.component('mkw-customize', wCustomize); diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue deleted file mode 100644 index e94e745c197a82cb89d69f310a9b22b6bc70bc2c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/messaging.vue +++ /dev/null @@ -1,60 +0,0 @@ -<template> -<div class="mkw-messaging"> - <ui-container :show-header="props.design == 0"> - <template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template> - <template #func><button @click="add"><fa icon="plus"/></button></template> - - <x-messaging ref="index" compact @navigate="navigate" @navigateGroup="navigateGroup"/> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import MkMessagingRoomWindow from '../components/messaging-room-window.vue'; -import MkMessagingWindow from '../components/messaging-window.vue'; - -export default define({ - name: 'messaging', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n(''), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - methods: { - navigate(user) { - this.$root.new(MkMessagingRoomWindow, { - user: user - }); - }, - navigateGroup(group) { - this.$root.new(MkMessagingRoomWindow, { - group: group - }); - }, - add() { - this.$root.new(MkMessagingWindow); - }, - func() { - if (this.props.design == 1) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-messaging - .mk-messaging - max-height 250px - overflow auto - -</style> diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue deleted file mode 100644 index 5a84f7b371311c28314e9e00abf9f23430eafb5c..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/notifications.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> -<div class="mkw-notifications"> - <ui-container :show-header="!props.compact"> - <template #header><fa :icon="['far', 'bell']"/>{{ props.type === 'all' ? $t('title') : $t('@.notification-types.' + props.type) }}</template> - <template #func><button :title="$t('@.notification-type')" @click="settings"><fa icon="cog"/></button></template> - - <mk-notifications :class="$style.notifications" :type="props.type === 'all' ? null : props.type"/> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'notifications', - props: () => ({ - compact: false, - type: 'all' - }) -}).extend({ - i18n: i18n('desktop/views/widgets/notifications.vue'), - methods: { - settings() { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.props.type, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.props.type = type; - this.save(); - }); - }, - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.notifications - max-height 300px - overflow auto - -</style> diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue deleted file mode 100644 index c77762ecdfa519b1a4c2f3dcec75726009f06948..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/polls.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> -<div class="mkw-polls"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="chart-pie"/>{{ $t('title') }}</template> - <template #func> - <button :title="$t('title')" @click="fetch"> - <fa v-if="!fetching && more" icon="arrow-right"/> - <fa v-if="!fetching && !more" icon="sync"/> - </button> - </template> - - <div class="mkw-polls--body"> - <div class="poll" v-if="!fetching && poll != null"> - <p v-if="poll.text"><router-link :to="poll | notePage"> - <mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/> - </router-link></p> - <p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p> - <mk-poll :note="poll"/> - </div> - <p class="empty" v-if="!fetching && poll == null">{{ $t('nothing') }}</p> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'polls', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('desktop/views/widgets/polls.vue'), - data() { - return { - poll: null, - fetching: true, - more: true, - offset: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - fetch() { - this.fetching = true; - this.poll = null; - - this.$root.api('notes/polls/recommendation', { - limit: 1, - offset: this.offset - }).then(notes => { - const poll = notes ? notes[0] : null; - if (poll == null) { - this.more = false; - this.offset = 0; - } else { - this.more = true; - this.offset++; - } - this.poll = poll; - this.fetching = false; - }).catch(() => { - this.poll = null; - this.fetching = false; - this.more = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-polls--body - > .poll - padding 16px - font-size 12px - color var(--text) - - > p - margin 0 0 8px 0 - - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue deleted file mode 100644 index bad1925f69cad8e55128291fefa3c2e883e154a6..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/profile.vue +++ /dev/null @@ -1,132 +0,0 @@ -<template> -<div class="egwyvoaaryotefqhqtmiyawwefemjfsd"> - <ui-container :show-header="false" :naked="props.design == 2"> - <div class="egwyvoaaryotefqhqtmiyawwefemjfsd-body" - :data-compact="props.design == 1 || props.design == 2" - :data-melt="props.design == 2" - > - <div class="banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" - :title="$t('update-banner')" - @click="updateBanner()" - ></div> - <mk-avatar class="avatar" :user="$store.state.i" - :disable-link="true" - @click="updateAvatar()" - :title="$t('update-avatar')" - /> - <router-link class="name" :to="$store.state.i | userPage"><mk-user-name :user="$store.state.i"/></router-link> - <p class="username">@{{ $store.state.i | acct }}</p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; -import updateAvatar from '../../api/update-avatar'; -import updateBanner from '../../api/update-banner'; - -export default define({ - name: 'profile', - props: () => ({ - design: 0 - }) -}).extend({ - i18n: i18n('desktop/views/widgets/profile.vue'), - methods: { - func() { - if (this.props.design == 2) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - }, - updateAvatar() { - updateAvatar(this.$root)(); - }, - updateBanner() { - updateBanner(this.$root)(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.egwyvoaaryotefqhqtmiyawwefemjfsd-body - &[data-compact] - > .banner:before - content "" - display block - width 100% - height 100% - background rgba(#000, 0.5) - - > .avatar - top ((100px - 58px) / 2) - left ((100px - 58px) / 2) - border none - border-radius 100% - box-shadow 0 0 16px rgba(#000, 0.5) - - > .name - position absolute - top 0 - left 92px - margin 0 - line-height 100px - color #fff - text-shadow 0 0 8px rgba(#000, 0.5) - - > .username - display none - - &[data-melt] - > .banner - visibility hidden - - > .avatar - box-shadow none - - > .name - color #666 - text-shadow none - - > .banner - height 100px - background-color var(--primaryAlpha01) - background-size cover - background-position center - cursor pointer - - > .avatar - display block - position absolute - top 76px - left 16px - width 58px - height 58px - border solid 3px var(--face) - border-radius 8px - cursor pointer - - > .name - display block - margin 10px 0 0 84px - line-height 16px - font-weight bold - color var(--text) - overflow hidden - text-overflow ellipsis - - > .username - display block - margin 4px 0 8px 84px - line-height 16px - font-size 0.9em - color var(--text) - opacity 0.7 - -</style> diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue deleted file mode 100644 index 854b01c13e51935596334f08c46c7276dadfcc2d..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/timemachine.vue +++ /dev/null @@ -1,29 +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.$root.$emit('warp', date); - }, - func() { - if (this.props.design == 5) { - this.props.design = 0; - } else { - this.props.design++; - } - this.save(); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue deleted file mode 100644 index c512945895167f817ac945db35bf174654de3e75..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/trends.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template> -<div class="mkw-trends"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="fire"/>{{ $t('title') }}</template> - <template #func><button :title="$t('title')" @click="fetch"><fa icon="sync"/></button></template> - - <div class="mkw-trends--body"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div class="note" v-else-if="note != null"> - <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p> - <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p> - </div> - <p class="empty" v-else>{{ $t('nothing') }}</p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'trends', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('desktop/views/widgets/trends.vue'), - data() { - return { - note: null, - fetching: true, - offset: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - fetch() { - this.fetching = true; - this.note = null; - - this.$root.api('notes/trend', { - limit: 1, - offset: this.offset, - renote: false, - reply: false, - file: false, - poll: false - }).then(notes => { - const note = notes ? notes[0] : null; - if (note == null) { - this.offset = 0; - } else { - this.offset++; - } - this.note = note; - this.fetching = false; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-trends - .mkw-trends--body - > .note - 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 var(--text) - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue deleted file mode 100644 index 85902fc20c85734c1ab394dee3a63955eb16a3bb..0000000000000000000000000000000000000000 --- a/src/client/app/desktop/views/widgets/users.vue +++ /dev/null @@ -1,145 +0,0 @@ -<template> -<div class="mkw-users"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="users"/>{{ $t('title') }}</template> - <template #func> - <button :title="$t('title')" @click="refresh"> - <fa v-if="!fetching && more" icon="arrow-right"/> - <fa v-if="!fetching && !more" icon="sync"/> - </button> - </template> - - <div class="mkw-users--body"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <template v-else-if="users.length != 0"> - <div class="user" v-for="_user in users"> - <mk-avatar class="avatar" :user="_user"/> - <div class="body"> - <router-link class="name" :to="_user | userPage" v-user-preview="_user.id"><mk-user-name :user="_user"/></router-link> - <p class="username">@{{ _user | acct }}</p> - </div> - </div> - </template> - <p class="empty" v-else>{{ $t('no-one') }}</p> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -const limit = 3; - -export default define({ - name: 'users', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n('desktop/views/widgets/users.vue'), - data() { - return { - users: [], - fetching: true, - more: true, - page: 0 - }; - }, - mounted() { - this.fetch(); - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - }, - fetch() { - this.fetching = true; - this.users = []; - - this.$root.api('users/recommendation', { - limit: limit, - offset: limit * this.page - }).then(users => { - this.users = users; - this.fetching = false; - }).catch(() => { - this.users = []; - this.fetching = false; - this.more = false; - this.page = 0; - }); - }, - refresh() { - if (this.users.length < limit) { - this.more = false; - this.page = 0; - } else { - this.more = true; - this.page++; - } - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mkw-users - .mkw-users--body - > .user - padding 16px - border-bottom solid 1px var(--faceDivider) - - &:last-child - border-bottom none - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - margin 0 12px 0 0 - width 42px - height 42px - border-radius 8px - - > .body - float left - width calc(100% - 54px) - - > .name - margin 0 - font-size 16px - line-height 24px - color var(--text) - - > .username - display block - margin 0 - font-size 15px - line-height 16px - color var(--text) - opacity 0.7 - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .fetching - margin 0 - padding 16px - text-align center - color var(--text) - - > [data-icon] - margin-right 4px - -</style> diff --git a/src/client/app/dev/script.ts b/src/client/app/dev/script.ts deleted file mode 100644 index 9adcb84d7c01027cdd849be7e97a1822c2e2ee2f..0000000000000000000000000000000000000000 --- a/src/client/app/dev/script.ts +++ /dev/null @@ -1,46 +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'; -import NotFound from '../common/views/pages/not-found.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 }, - { path: '*', component: NotFound } - ] - }); - - // Launch the app - launch(router); -}); diff --git a/src/client/app/dev/style.styl b/src/client/app/dev/style.styl deleted file mode 100644 index e635897b170fee1f653983f1676d1d960350d35f..0000000000000000000000000000000000000000 --- a/src/client/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/client/app/dev/views/app.vue b/src/client/app/dev/views/app.vue deleted file mode 100644 index 2379d71aa588cec22f81336951297a9673b2a7b7..0000000000000000000000000000000000000000 --- a/src/client/app/dev/views/app.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<mk-ui> - <p v-if="fetching">{{ $t('@.loading') }}</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'; -import i18n from '../../i18n'; -export default Vue.extend({ - i18n: i18n(), - data() { - return { - fetching: true, - app: null - }; - }, - watch: { - $route: 'fetch' - }, - mounted() { - this.fetch(); - }, - methods: { - fetch() { - this.fetching = true; - this.$root.api('app/show', { - appId: this.$route.params.id - }).then(app => { - this.app = app; - this.fetching = false; - }); - } - } -}); -</script> diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue deleted file mode 100644 index b99ccdf5763708759f49b34d313f50c604b53690..0000000000000000000000000000000000000000 --- a/src/client/app/dev/views/apps.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<mk-ui> - <b-card :header="$t('manage-apps')"> - <b-button to="/app/new" variant="primary">{{ $t('create-app') }}</b-button> - <hr> - <div class="apps"> - <p v-if="fetching">{{ $t('@.loading') }}</p> - <template v-if="!fetching"> - <b-alert v-if="apps.length == 0">{{ $t('app-missing') }}</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'; -import i18n from '../../i18n'; -export default Vue.extend({ - i18n: i18n('dev/views/apps.vue'), - data() { - return { - fetching: true, - apps: [] - }; - }, - mounted() { - this.$root.api('my/apps').then(apps => { - this.apps = apps; - this.fetching = false; - }); - } -}); -</script> diff --git a/src/client/app/dev/views/index.vue b/src/client/app/dev/views/index.vue deleted file mode 100644 index db0e4d57c240f2dc15e6e29fccd98659c283ed99..0000000000000000000000000000000000000000 --- a/src/client/app/dev/views/index.vue +++ /dev/null @@ -1,13 +0,0 @@ -<template> -<mk-ui> - <b-button to="/apps" variant="primary">{{ $t('manage-apps') }}</b-button> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -export default Vue.extend({ - i18n: i18n('dev/views/index.vue') -}); -</script> diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue deleted file mode 100644 index dbb41211ccff6387bcef14eac14dbbeb6a8a5e82..0000000000000000000000000000000000000000 --- a/src/client/app/dev/views/new-app.vue +++ /dev/null @@ -1,62 +0,0 @@ -<template> -<mk-ui> - <b-card :header="$t('new-app')"> - <b-alert show variant="info"><fa icon="info-circle"/> {{ $t('new-app-info') }}</b-alert> - <b-form @submit.prevent="onSubmit" autocomplete="off"> - <b-form-group :label="$t('app-name')" :description="$t('app-name-desc')"> - <b-form-input v-model="name" type="text" :placeholder="$t('app-name-placeholder')" autocomplete="off" required/> - </b-form-group> - <b-form-group :label="$t('app-overview')" :description="$t('app-overview-desc')"> - <b-textarea v-model="description" :placeholder="$t('app-overview-placeholder')" autocomplete="off" required></b-textarea> - </b-form-group> - <b-form-group :label="$t('callback-url')" :description="$t('callback-url-desc')"> - <b-input v-model="cb" type="url" :placeholder="$t('callback-url-placeholder')" autocomplete="off"/> - </b-form-group> - <b-card :header="$t('authority')"> - <b-form-group :description="$t('authority-desc')"> - <b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert> - <b-form-checkbox-group v-model="permission" stacked> - <b-form-checkbox v-for="v in permissionsList" :value="v" :key="v">{{ $t(`@.permissions.${v}`) }} ({{ v }})</b-form-checkbox> - </b-form-checkbox-group> - </b-form-group> - </b-card> - <hr> - <b-button type="submit" variant="primary">{{ $t('create-app') }}</b-button> - </b-form> - </b-card> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; -import { kinds } from '../../../../server/api/kinds'; - -export default Vue.extend({ - i18n: i18n('dev/views/new-app.vue'), - data() { - return { - name: '', - description: '', - cb: '', - nidState: null, - permission: [], - permissionsList: kinds - }; - }, - methods: { - onSubmit() { - this.$root.api('app/create', { - name: this.name, - description: this.description, - callbackUrl: this.cb, - permission: this.permission - }).then(() => { - location.href = '/dev/apps'; - }).catch(() => { - alert(this.$t('@.dev.failed-to-create')); - }); - } - } -}); -</script> diff --git a/src/client/app/dev/views/ui.vue b/src/client/app/dev/views/ui.vue deleted file mode 100644 index f1e001909f422444965b25997c5a16a3264bf71a..0000000000000000000000000000000000000000 --- a/src/client/app/dev/views/ui.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> -<div> - <b-navbar toggleable="md" type="dark" variant="info"> - <b-navbar-brand>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/client/app/i18n.ts b/src/client/app/i18n.ts deleted file mode 100644 index 2d0d9ba5500aee9df82b340bc1ac59968b61e4a2..0000000000000000000000000000000000000000 --- a/src/client/app/i18n.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { lang, locale } from './config'; - -export default function(scope?: string) { - const texts = scope ? locale[scope] || {} : {}; - texts['@'] = locale['common']; - texts['@deck'] = locale['deck']; - return { - sync: false, - locale: lang, - messages: { - [lang]: texts - } - }; -} diff --git a/src/client/app/init.css b/src/client/app/init.css deleted file mode 100644 index db5e23c56d41e9c26c525639f373b4fdd0af29f9..0000000000000000000000000000000000000000 --- a/src/client/app/init.css +++ /dev/null @@ -1,57 +0,0 @@ -@charset "utf-8"; - -/** - * Boot screen style - */ - -html { - font-family: Roboto, HelveticaNeue, Arial, 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%; - background: var(--bg); - cursor: wait; -} - #ini > svg { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - width: 64px; - height: 64px; - animation: ini 0.6s infinite linear; - } - -@keyframes ini { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/client/app/init.ts b/src/client/app/init.ts deleted file mode 100644 index 6fd11b9b75ee98f6ca57f812918a43157f7b0b3b..0000000000000000000000000000000000000000 --- a/src/client/app/init.ts +++ /dev/null @@ -1,512 +0,0 @@ -/** - * App initializer - */ - -import Vue from 'vue'; -import Vuex from 'vuex'; -import VueRouter from 'vue-router'; -import VAnimateCss from 'v-animate-css'; -import VModal from 'vue-js-modal'; -import VueI18n from 'vue-i18n'; -import SequentialEntrance from 'vue-sequential-entrance'; - -import VueHotkey from './common/hotkey'; -import VueSize from './common/size'; -import App from './app.vue'; -import checkForUpdate from './common/scripts/check-for-update'; -import MiOS from './mios'; -import { version, codename, lang, locale } from './config'; -import { builtinThemes, applyTheme, futureTheme } from './theme'; -import Dialog from './common/views/components/dialog.vue'; - -if (localStorage.getItem('theme') == null) { - applyTheme(futureTheme); -} - -//#region FontAwesome -import { library } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; - -import { - faRetweet, - faPlus, - faUser, - faCog, - faCheck, - faStar, - faReply, - faEllipsisH, - faQuoteLeft, - faQuoteRight, - faAngleUp, - faAngleDown, - faAt, - faHashtag, - faHome, - faGlobe, - faCircle, - faList, - faHeart, - faUnlock, - faRssSquare, - faSort, - faChartPie, - faChartBar, - faPencilAlt, - faColumns, - faComments, - faGamepad, - faCloud, - faPowerOff, - faChevronCircleLeft, - faChevronCircleRight, - faShareAlt, - faTimes, - faThumbtack, - faSearch, - faAngleRight, - faWrench, - faTerminal, - faMoon, - faPalette, - faSlidersH, - faDesktop, - faVolumeUp, - faLanguage, - faInfoCircle, - faExclamationTriangle, - faKey, - faBan, - faCogs, - faUnlockAlt, - faPuzzlePiece, - faMobileAlt, - faSignInAlt, - faSyncAlt, - faPaperPlane, - faUpload, - faMapMarkerAlt, - faEnvelope, - faLock, - faFolderOpen, - faBirthdayCake, - faImage, - faEye, - faDownload, - faFileImport, - faLink, - faArrowRight, - faICursor, - faCaretRight, - faReplyAll, - faCamera, - faMinus, - faCaretDown, - faCalculator, - faUsers, - faBars, - faFileImage, - faPollH, - faFolder, - faMicrochip, - faMemory, - faServer, - faExclamationCircle, - faSpinner, - faBroadcastTower, - faChartLine, - faEllipsisV, - faStickyNote, - faUserClock, - faUserPlus, - faExternalLinkSquareAlt, - faSync, - faArrowLeft, - faMapMarker, - faRobot, - faHourglassHalf, - faGavel, - faUndoAlt, -} from '@fortawesome/free-solid-svg-icons'; - -import { - faBell as farBell, - faEnvelope as farEnvelope, - faComments as farComments, - faTrashAlt as farTrashAlt, - faWindowRestore as farWindowRestore, - faFolder as farFolder, - faLaugh as farLaugh, - faSmile as farSmile, - faEyeSlash as farEyeSlash, - faFolderOpen as farFolderOpen, - faSave as farSave, - faImages as farImages, - faChartBar as farChartBar, - faCommentAlt as farCommentAlt, - faClock as farClock, - faCalendarAlt as farCalendarAlt, - faHdd as farHdd, - faMoon as farMoon, - faPlayCircle as farPlayCircle, - faLightbulb as farLightbulb, - faStickyNote as farStickyNote, -} from '@fortawesome/free-regular-svg-icons'; - -import { - faTwitter as fabTwitter, - faGithub as fabGithub, - faDiscord as fabDiscord -} from '@fortawesome/free-brands-svg-icons'; -import i18n from './i18n'; - -library.add( - faRetweet, - faPlus, - faUser, - faCog, - faCheck, - faStar, - faReply, - faEllipsisH, - faQuoteLeft, - faQuoteRight, - faAngleUp, - faAngleDown, - faAt, - faHashtag, - faHome, - faGlobe, - faCircle, - faList, - faHeart, - faUnlock, - faRssSquare, - faSort, - faChartPie, - faChartBar, - faPencilAlt, - faColumns, - faComments, - faGamepad, - faCloud, - faPowerOff, - faChevronCircleLeft, - faChevronCircleRight, - faShareAlt, - faTimes, - faThumbtack, - faSearch, - faAngleRight, - faWrench, - faTerminal, - faMoon, - faPalette, - faSlidersH, - faDesktop, - faVolumeUp, - faLanguage, - faInfoCircle, - faExclamationTriangle, - faKey, - faBan, - faCogs, - faUnlockAlt, - faPuzzlePiece, - faMobileAlt, - faSignInAlt, - faSyncAlt, - faPaperPlane, - faUpload, - faMapMarkerAlt, - faEnvelope, - faLock, - faFolderOpen, - faBirthdayCake, - faImage, - faEye, - faDownload, - faFileImport, - faLink, - faArrowRight, - faICursor, - faCaretRight, - faReplyAll, - faCamera, - faMinus, - faCaretDown, - faCalculator, - faUsers, - faBars, - faFileImage, - faPollH, - faFolder, - faMicrochip, - faMemory, - faServer, - faExclamationCircle, - faSpinner, - faBroadcastTower, - faChartLine, - faEllipsisV, - faStickyNote, - faUserClock, - faUserPlus, - faExternalLinkSquareAlt, - faSync, - faArrowLeft, - faMapMarker, - faRobot, - faHourglassHalf, - faGavel, - faUndoAlt, - - farBell, - farEnvelope, - farComments, - farTrashAlt, - farWindowRestore, - farFolder, - farLaugh, - farSmile, - farEyeSlash, - farFolderOpen, - farSave, - farImages, - farChartBar, - farCommentAlt, - farClock, - farCalendarAlt, - farHdd, - farMoon, - farPlayCircle, - farLightbulb, - farStickyNote, - - fabTwitter, - fabGithub, - fabDiscord -); -//#endregion - -Vue.use(Vuex); -Vue.use(VueRouter); -Vue.use(VAnimateCss); -Vue.use(VModal); -Vue.use(VueHotkey); -Vue.use(VueSize); -Vue.use(VueI18n); -Vue.use(SequentialEntrance); - -Vue.component('fa', FontAwesomeIcon); - -// 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({ - methods: { - destroyDom() { - this.$destroy(); - - if (this.$el.parentNode) { - this.$el.parentNode.removeChild(this.$el); - } - } - } -}); - -/** - * APP ENTRY POINT! - */ - -console.info(`Misskey v${version} (${codename})`); -console.info( - `%c${locale['common']['do-not-copy-paste']}`, - 'color: red; background: yellow; font-size: 16px; font-weight: bold;'); - -// BootTimer解除 -window.clearTimeout((window as any).mkBootTimer); -delete (window as any).mkBootTimer; - -//#region Set lang attr -const html = document.documentElement; -html.setAttribute('lang', lang); -//#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) => [Vue, MiOS], os: MiOS) => void, sw = false) => { - const os = new MiOS(sw); - - os.init(() => { - // アプリ基底è¦ç´ マウント - document.body.innerHTML = '<div id="app"></div>'; - - const launch = (router: VueRouter) => { - //#region theme - os.store.watch(s => { - return s.device.darkmode; - }, v => { - const themes = os.store.state.device.themes.concat(builtinThemes); - const dark = themes.find(t => t.id == os.store.state.device.darkTheme); - const light = themes.find(t => t.id == os.store.state.device.lightTheme); - applyTheme(v ? dark : light); - }); - os.store.watch(s => { - return s.device.lightTheme; - }, v => { - const themes = os.store.state.device.themes.concat(builtinThemes); - const theme = themes.find(t => t.id == v); - if (!os.store.state.device.darkmode) { - applyTheme(theme); - } - }); - os.store.watch(s => { - return s.device.darkTheme; - }, v => { - const themes = os.store.state.device.themes.concat(builtinThemes); - const theme = themes.find(t => t.id == v); - if (os.store.state.device.darkmode) { - applyTheme(theme); - } - }); - //#endregion - - /*// Reapply current theme - try { - const themeName = os.store.state.device.darkmode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme; - const themes = os.store.state.device.themes.concat(builtinThemes); - const theme = themes.find(t => t.id == themeName); - if (theme) { - applyTheme(theme); - } - } catch (e) { - console.log(`Cannot reapply theme. ${e}`); - }*/ - - //#region line width - document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`); - os.store.watch(s => { - return s.device.lineWidth; - }, v => { - document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`); - }); - //#endregion - - //#region fontSize - document.documentElement.style.setProperty('--fontSize', `${os.store.state.device.fontSize}px`); - os.store.watch(s => { - return s.device.fontSize; - }, v => { - document.documentElement.style.setProperty('--fontSize', `${os.store.state.device.fontSize}px`); - }); - //#endregion - - document.addEventListener('visibilitychange', () => { - if (!document.hidden) { - os.store.commit('clearBehindNotes'); - } - }, false); - - window.addEventListener('scroll', () => { - if (window.scrollY <= 8) { - os.store.commit('clearBehindNotes'); - } - }, { passive: true }); - - const app = new Vue({ - i18n: i18n(), - store: os.store, - data() { - return { - os: { - windows: os.windows - }, - stream: os.stream, - instanceName: os.instanceName - }; - }, - methods: { - api: os.api, - getMeta: os.getMeta, - getMetaSync: os.getMetaSync, - signout: os.signout, - new(vm, props) { - const x = new vm({ - parent: this, - propsData: props - }).$mount(); - document.body.appendChild(x.$el); - return x; - }, - newAsync(vm, props) { - return new Promise((res) => { - vm().then(vm => { - const x = new vm({ - parent: this, - propsData: props - }).$mount(); - document.body.appendChild(x.$el); - res(x); - }); - }); - }, - dialog(opts) { - const vm = this.new(Dialog, opts); - const p: any = new Promise((res) => { - vm.$once('ok', result => res({ canceled: false, result })); - vm.$once('cancel', () => res({ canceled: true })); - }); - p.close = () => { - vm.close(); - }; - return p; - } - }, - router, - render: createEl => createEl(App) - }); - - os.app = app; - - // マウント - app.$mount('#app'); - - //#region æ›´æ–°ãƒã‚§ãƒƒã‚¯ - setTimeout(() => { - checkForUpdate(app); - }, 3000); - //#endregion - - return [app, os] as [Vue, MiOS]; - }; - - // Deck mode - os.store.commit('device/set', { - key: 'inDeckMode', - value: os.store.getters.isSignedIn && os.store.state.device.deckMode - && (document.location.pathname === '/' || window.performance.navigation.type === 1) - }); - - callback(launch, os); - }); -}; diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts deleted file mode 100644 index 26ef7408113455cf56d48fb8127648f9c3f5ed7d..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/script.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Mobile Client - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; - -import MkIndex from './views/pages/index.vue'; -import MkSignup from './views/pages/signup.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 MkNote from './views/pages/note.vue'; -import MkSearch from './views/pages/search.vue'; -import UI from './views/pages/ui.vue'; -import MkReversi from './views/pages/games/reversi.vue'; -import MkTag from './views/pages/tag.vue'; -import MkShare from '../common/views/pages/share.vue'; -import MkFollow from '../common/views/pages/follow.vue'; -import MkNotFound from '../common/views/pages/not-found.vue'; -import DeckColumn from '../common/views/deck/deck.column-template.vue'; -import PostFormDialog from './views/components/post-form-dialog.vue'; - -import FileChooser from './views/components/drive-file-chooser.vue'; -import FolderChooser from './views/components/drive-folder-chooser.vue'; - -/** - * init - */ -init((launch, os) => { - Vue.mixin({ - data() { - return { - isMobile: true - }; - }, - - methods: { - $post(opts) { - const o = opts || {}; - - document.documentElement.style.overflow = 'hidden'; - - function recover() { - document.documentElement.style.overflow = 'auto'; - } - - const vm = this.$root.new(PostFormDialog, { - reply: o.reply, - mention: o.mention, - renote: o.renote, - initialText: o.initialText, - instant: o.instant, - initialNote: o.initialNote, - }); - vm.$once('cancel', recover); - vm.$once('posted', recover); - if (o.cb) vm.$once('closed', o.cb); - (vm as any).focus(); - }, - - $chooseDriveFile(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const vm = this.$root.new(FileChooser, { - title: o.title, - multiple: o.multiple, - initFolder: o.currentFolder - }); - vm.$once('selected', file => { - res(file); - }); - }); - }, - - $chooseDriveFolder(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const vm = this.$root.new(FolderChooser, { - title: o.title, - initFolder: o.currentFolder - }); - vm.$once('selected', folder => { - res(folder); - }); - }); - }, - - $notify(message) { - alert(message); - } - } - }); - - // 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: [ - ...(os.store.state.device.inDeckMode - ? [{ path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ - { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, - { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, - { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, - { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) }, - ]}] - : [ - { path: '/', name: 'index', component: MkIndex }, - ]), - { path: '/signup', name: 'signup', component: MkSignup }, - { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, - { path: '/i/settings/:page', redirect: '/i/settings' }, - { path: '/i/favorites', name: 'favorites', component: UI, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'mobile' }) }, - { path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) }, - { path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) }, - { path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/i/widgets', name: 'widgets', component: () => import('./views/pages/widgets.vue').then(m => m.default) }, - { path: '/i/notifications', name: 'notifications', component: MkNotifications }, - { path: '/i/messaging', name: 'messaging', component: MkMessaging }, - { path: '/i/messaging/group/:group', component: MkMessagingRoom }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', name: 'drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/i/drive/file/:file', component: MkDrive }, - { path: '/i/pages/new', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }) }, - { path: '/i/pages/edit/:pageId', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initPageId: route.params.pageId }) }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/search', component: MkSearch }, - { path: '/tags/:tag', component: MkTag }, - { path: '/featured', name: 'featured', component: UI, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'mobile' }) }, - { path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/share', component: MkShare }, - { path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, - { path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [ - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) }, - { path: '/notes/:note', component: MkNote }, - { path: '/authorize-follow', component: MkFollow }, - { path: '*', component: MkNotFound } - ] - }); - - // Launch the app - launch(router); -}, true); diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl deleted file mode 100644 index 3a4fc9c0c6198cfea66611d38552ec16b00d117f..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/style.styl +++ /dev/null @@ -1,23 +0,0 @@ -@import "../app" -@import "../reset" - -#wait - top auto - bottom 15px - left 15px - -html - height 100% - background var(--bg) - -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px diff --git a/src/client/app/mobile/views/components/detail-notes.vue b/src/client/app/mobile/views/components/detail-notes.vue deleted file mode 100644 index bab79495342a2540fcc093751e96f30ce3d3826d..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/detail-notes.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<div class="fdcvngpy"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> - <ui-button v-if="more" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.fdcvngpy - > * > .post - margin-bottom 8px - - @media (min-width 500px) - > * > .post - margin-bottom 16px - -</style> diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue deleted file mode 100644 index 8795102f97bf4997714ea338057ed358f676a4c9..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/drive-file-chooser.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb"> - <div class="body"> - <header> - <h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="close" @click="cancel"><fa icon="times"/></button> - <button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive class="drive" ref="browser" - :select-file="true" - :type="type" - :multiple="multiple" - @change-selection="onChangeSelection" - @selected="onSelected" - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive-file-chooser.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: ['type', 'multiple'], - data() { - return { - files: [] - }; - }, - methods: { - onChangeSelection(files) { - this.files = files; - }, - onSelected(file) { - this.$emit('selected', file); - this.destroyDom(); - }, - cancel() { - this.$emit('canceled'); - this.destroyDom(); - }, - ok() { - this.$emit('selected', this.files); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cdxzvcfawjxdyxsekbxbfgtplebnoneb - position fixed - z-index 20000 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(#000, 0.2) - - > .body - width 100% - height 100% - background var(--faceHeader) - - > header - border-bottom solid 1px var(--faceDivider) - color var(--text) - - > 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 - - > .drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue deleted file mode 100644 index 250a7aca2c54213557a97c76f8454bebed40d971..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/drive-folder-chooser.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div class="mk-drive-folder-chooser"> - <div class="body"> - <header> - <h1>{{ $t('select-folder') }}</h1> - <button class="close" @click="cancel"><fa icon="times"/></button> - <button class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive ref="browser" - select-folder - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive-folder-chooser.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - methods: { - cancel() { - this.$emit('canceled'); - this.destroyDom(); - }, - ok() { - this.$emit('selected', (this.$refs.browser as any).folder); - this.destroyDom(); - } - } -}); -</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(#000, 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/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue deleted file mode 100644 index 328982a16bfb412d27ac544c8b7238b5b3702edc..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ /dev/null @@ -1,253 +0,0 @@ -<template> -<div class="pyvicwrksnfyhpfgkjwqknuururpaztw"> - <div class="preview"> - <x-file-thumbnail class="preview" :file="file" :detail="true"/> - <template v-if="kind != 'image'"><fa icon="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.size | bytes }}</span> - <span class="separator"></span> - <span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> - <template v-if="file.isSensitive"> - <span class="separator"></span> - <span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> - </template> - </div> - </div> - <div class="menu"> - <div> - <ui-input readonly :value="file.url">URL</ui-input> - <ui-button link :href="dlUrl" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button> - <ui-button @click="rename"><fa icon="pencil-alt"/> {{ $t('rename') }}</ui-button> - <ui-button @click="move"><fa :icon="['far', 'folder-open']"/> {{ $t('move') }}</ui-button> - <ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> {{ $t('unmark-as-sensitive') }}</ui-button> - <ui-button @click="toggleSensitive" v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="del"><fa :icon="['far', 'trash-alt']"/> {{ $t('delete') }}</ui-button> - </div> - </div> - <div class="hash"> - <div> - <p> - <fa icon="hashtag"/>{{ $t('hash') }} - </p> - <code>{{ file.md5 }}</code> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { gcd } from '../../../../../prelude/math'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.file-detail.vue'), - props: ['file'], - - components: { - XFileThumbnail - }, - - 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': this.file.properties.avgColor - } : {}; - }, - - dlUrl(): string { - return this.file.url; - } - }, - - methods: { - rename() { - const name = window.prompt(this.$t('rename'), this.file.name); - if (name == null || name == '' || name == this.file.name) return; - this.$root.api('drive/files/update', { - fileId: this.file.id, - name: name - }).then(() => { - this.browser.cf(this.file, true); - }); - }, - - move() { - this.$chooseDriveFolder().then(folder => { - this.$root.api('drive/files/update', { - fileId: this.file.id, - folderId: folder == null ? null : folder.id - }).then(() => { - this.browser.cf(this.file, true); - }); - }); - }, - - del() { - this.$root.api('drive/files/delete', { - fileId: this.file.id - }).then(() => { - this.browser.cd(this.file.folderId); - }); - }, - - toggleSensitive() { - this.$root.api('drive/files/update', { - fileId: this.file.id, - isSensitive: !this.file.isSensitive - }); - - this.file.isSensitive = !this.file.isSensitive; - }, - - showCreatedAt() { - this.$root.dialog({ - type: 'info', - text: (new Date(this.file.createdAt)).toLocaleString() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.pyvicwrksnfyhpfgkjwqknuururpaztw - > .preview - padding 8px - background var(--bg) - - > .preview - width fit-content - width -moz-fit-content - max-width 100% - margin 0 auto - box-shadow 1px 1px 4px rgba(#000, 0.2) - overflow hidden - color var(--driveFileIcon) - justify-content center - - > footer - padding 8px 8px 0 8px - text-align center - font-size 0.8em - color var(--text) - opacity 0.7 - - > .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 var(--faceDivider) - - > div - max-width 500px - margin 0 auto - color var(--text) - - > .separator - padding 0 4px - - > .created-at - - > [data-icon] - margin-right 2px - - > .nsfw - color #bf4633 - - > .menu - padding 0 14px 14px 14px - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - - > .hash - padding 14px - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - - > p - display block - margin 0 - padding 0 - color var(--text) - font-size 0.9em - - > [data-icon] - 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 - -</style> diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue deleted file mode 100644 index ed95537f9cbf8be00309b1302129347bbc0ec24f..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/drive.file.vue +++ /dev/null @@ -1,155 +0,0 @@ -<template> -<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> - <div class="container"> - <x-file-thumbnail class="thumbnail" :file="file" fit="cover"/> - <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> - <footer> - <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> - <span class="separator"></span> - <span class="data-size">{{ file.size | bytes }}</span> - <span class="separator"></span> - <span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> - <template v-if="file.isSensitive"> - <span class="separator"></span> - <span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> - </template> - </footer> - </div> - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.file.vue'), - props: ['file'], - components: { - XFileThumbnail - }, - data() { - return { - isSelected: false - }; - }, - computed: { - browser(): any { - return this.$parent; - } - }, - 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> -.vupkuhvjnjyqaqhsiogfbywvjxynrgsm - display block - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - display grid - max-width 500px - margin 0 auto - padding 16px - grid-template-columns 64px 1fr - grid-column-gap 10px - - &:after - content "" - display block - clear both - - > .thumbnail - width 64px - height 64px - color var(--driveFileIcon) - - > .body - display block - word-break break-all - - > .name - display block - margin 0 - padding 0 - font-size 0.9em - font-weight bold - color var(--text) - word-break 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 - color var(--text) - - > .separator - padding 0 4px - - > .type - opacity 0.7 - - > .mk-file-type-icon - margin-right 4px - - > .data-size - opacity 0.7 - - > .created-at - opacity 0.7 - - > [data-icon] - margin-right 2px - - > .nsfw - color #bf4633 - - &[data-is-selected] - background var(--primary) - - &, * - color var(--primaryForeground) !important - -</style> diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue deleted file mode 100644 index 0959c1e7d4ff5c2cfd2ab833093569d88bf3586b..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/drive.folder.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> - <div class="container"> - <p class="name"><fa icon="folder"/>{{ folder.name }}</p><fa icon="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> -.jvwxssxsytqlqvrpiymarjlzlsxskqsr - display block - color var(--text) - 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-icon] - margin-right 6px - - > [data-icon] - position absolute - top 0 - bottom 0 - right 20px - height 100% - -</style> diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue deleted file mode 100644 index fe193f311a06713f375138081042962b6176cdd2..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/drive.vue +++ /dev/null @@ -1,618 +0,0 @@ -<template> -<div class="kmmwchoexgckptowjmjgfsygeltxfeqs"> - <nav ref="nav"> - <a @click.prevent="goRoot()" href="/i/drive"><fa icon="cloud"/>{{ $t('@.drive') }}</a> - <template v-for="folder in hierarchyFolders"> - <span :key="folder.id + '>'"><fa icon="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 icon="angle-right"/></span> - <p>{{ folder.name }}</p> - </template> - <template v-if="file != null"> - <span><fa icon="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) }}% {{ $t('used') }}</p> - <p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)"> - <template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} {{ $t('folder-count') }}</template> - <template v-if="folder.foldersCount > 0 && folder.filesCount > 0">{{ $t('count-separator') }}</template> - <template v-if="folder.filesCount > 0">{{ folder.filesCount }} {{ $t('file-count') }}</template> - </p> - </div> - <div class="folders" v-if="folders.length > 0 || moreFolders"> - <x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/> - <p v-if="moreFolders">{{ $t('@.load-more') }}</p> - </div> - <div class="files" v-if="files.length > 0 || moreFiles"> - <x-file class="file" v-for="file in files" :key="file.id" :file="file"/> - <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> - {{ fetchingMoreFiles ? this.$t('@.loading') : this.$t('@.load-more') }} - </button> - </div> - <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching"> - <p v-if="folder == null">{{ $t('nothing-in-drive') }}</p> - <p v-if="folder != null">{{ $t('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 i18n from '../../../i18n'; -import XFolder from './drive.folder.vue'; -import XFile from './drive.file.vue'; -import XFileDetail from './drive.file-detail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.vue'), - 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, - - fetching: true, - fetchingMoreFiles: false, - fetchingMoreFolders: false - }; - }, - computed: { - isFileSelectMode(): boolean { - return this.selectFile; - } - }, - watch: { - top() { - if (this.isNaked) { - (this.$refs.nav as any).style.top = `${this.top}px`; - } - } - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('drive'); - - this.connection.on('fileCreated', this.onStreamDriveFileCreated); - this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); - this.connection.on('fileDeleted', this.onStreamDriveFileDeleted); - this.connection.on('folderCreated', this.onStreamDriveFolderCreated); - this.connection.on('folderUpdated', 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.dispose(); - }, - 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); - } - }, - - onStreamDriveFileDeleted(fileId) { - this.removeFile(fileId); - }, - - 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) { - if (target == null) { - this.goRoot(silent); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.file = null; - this.fetching = true; - - this.$root.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) { - // ã™ã§ã«rootã«ã„ã‚‹ãªã‚‰ä½•ã‚‚ã—ãªã„ - if (this.folder == null && this.file == null) return; - - 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.$root.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.$root.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) { - for (const x of fetchedFolders) this.appendFolder(x); - for (const x of fetchedFiles) this.appendFile(x); - this.fetching = false; - - // 一連ã®èªã¿è¾¼ã¿ãŒå®Œäº†ã—ãŸã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 - this.$emit('fetched'); - } else { - flag = true; - // 一連ã®èªã¿è¾¼ã¿ãŒåŠåˆ†å®Œäº†ã—ãŸã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 - this.$emit('fetch-mid'); - } - }; - - if (this.folder == null) { - // Fetch addtional drive info - this.$root.api('drive').then(info => { - this.info = info; - }); - } - }, - - fetchMoreFiles() { - this.fetching = true; - this.fetchingMoreFiles = true; - - const max = 30; - - // ファイル一覧å–å¾— - this.$root.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; - } - for (const x of files) this.appendFile(x); - 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.$root.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); - }); - }, - - selectLocalFile() { - (this.$refs.file as any).click(); - }, - - createFolder() { - this.$root.dialog({ - title: this.$t('folder-name'), - input: { - default: this.folder.name - } - }).then(({ result: name }) => { - if (!name) { - this.$root.dialog({ - type: 'error', - text: this.$t('folder-name-cannot-empty') - }); - return; - } - this.$root.api('drive/folders/create', { - name: name, - parentId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - }); - }); - }, - - renameFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$root.dialog({ - title: this.$t('folder-name'), - input: { - default: this.folder.name - } - }).then(({ result: name }) => { - if (!name) { - this.$root.dialog({ - type: 'error', - text: this.$t('cannot-empty') - }); - return; - } - this.$root.api('drive/folders/update', { - name: name, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - moveFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$chooseDriveFolder().then(folder => { - this.$root.api('drive/folders/update', { - parentId: folder ? folder.id : null, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - urlUpload() { - const url = window.prompt(this.$t('url-prompt')); - if (url == null || url == '') return; - this.$root.api('drive/files/upload_from_url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); - this.$root.dialog({ - type: 'info', - text: this.$t('uploading') - }); - }, - - onChangeLocalFile() { - for (const f of Array.from((this.$refs.file as any).files)) { - (this.$refs.uploader as any).upload(f, this.folder); - } - }, - - deleteFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$root.api('drive/folders/delete', { - folderId: this.folder.id - }).then(folder => { - this.cd(this.folder.parentId); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kmmwchoexgckptowjmjgfsygeltxfeqs - background var(--face) - - > 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 var(--text) - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color var(--mobileDriveNavBg) - border-bottom solid 1px rgba(#000, 0.13) - - > p - > a - display inline - margin 0 - padding 0 - text-decoration none !important - color inherit - - &:last-child - font-weight bold - - > [data-icon] - margin-right 4px - - > span - margin 0 8px - opacity 0.5 - - > .browser - &.fetching - opacity 0.5 - - > .info - border-bottom solid 1px var(--faceDivider) - - &:empty - display none - - > p - display block - max-width 500px - margin 0 auto - padding 4px 16px - font-size 10px - color var(--text) - - > .folders - > .folder - border-bottom solid 1px var(--faceDivider) - - > .files - > .file - border-bottom solid 1px var(--faceDivider) - - > .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(#000, 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/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts deleted file mode 100644 index 4e10d80f926c0844b6edf253018e29a5fc77020c..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import note from './note.vue'; -import notes from './notes.vue'; -import mediaVideo from './media-video.vue'; -import notePreview from './note-preview.vue'; -import subNoteContent from './sub-note-content.vue'; -import noteCard from './note-card.vue'; -import noteDetail from './note-detail.vue'; -import notification from './notification.vue'; -import notifications from './notifications.vue'; -import notificationPreview from './notification-preview.vue'; -import userTimeline from './user-timeline.vue'; -import userListTimeline from './user-list-timeline.vue'; -import uiContainer from './ui-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-note', note); -Vue.component('mk-notes', notes); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-note-preview', notePreview); -Vue.component('mk-sub-note-content', subNoteContent); -Vue.component('mk-note-card', noteCard); -Vue.component('mk-note-detail', noteDetail); -Vue.component('mk-notification', notification); -Vue.component('mk-notifications', notifications); -Vue.component('mk-notification-preview', notificationPreview); -Vue.component('mk-user-timeline', userTimeline); -Vue.component('mk-user-list-timeline', userListTimeline); -Vue.component('ui-container', uiContainer); diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue deleted file mode 100644 index 270482031873f32887fe50309b4d478b16692cb8..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/note-card.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<div class="mk-note-card"> - <a :href="note | notePage"> - <header> - <img :src="avator" alt="avatar"/> - <h3><mk-user-name :user="note.user"/></h3> - </header> - <div> - {{ text }} - </div> - <mk-time :time="note.createdAt"/> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import summary from '../../../../../misc/get-note-summary'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - props: ['note'], - computed: { - text(): string { - return summary(this.note); - }, - avator(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.note.user.avatarUrl) - : this.note.user.avatarUrl; - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-card - display inline-block - width 150px - //height 120px - font-size 12px - background var(--face) - border-radius 4px - box-shadow 0 2px 8px rgba(0, 0, 0, 0.2) - - > a - display block - color var(--noteText) - - &: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, transparent 0%, var(--face) 100%) - - > .mk-time - display inline-block - padding 8px - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue deleted file mode 100644 index 358b827a5c4878c3c6321748fd8c946007d65152..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/note-detail.vue +++ /dev/null @@ -1,351 +0,0 @@ -<template> -<div class="mk-note-detail" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <button - class="more" - v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" - @click="fetchConversation" - :disabled="conversationFetching" - > - <template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> - <template v-if="conversationFetching"><fa icon="spinner" pulse/></template> - </button> - <div class="conversation"> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - </div> - <div class="reply-to" v-if="appearNote.reply"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note" mini/> - <article> - <header> - <mk-avatar class="avatar" :user="appearNote.user"/> - <div> - <router-link class="name" :to="appearNote.user | userPage"><mk-user-name :user="appearNote.user"/></router-link> - <span class="username"><mk-acct :user="appearNote.user"/></span> - </div> - </header> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <span v-if="appearNote.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files" :raw="true"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="map" v-if="appearNote.geo" ref="map"></div> - <div class="renote" v-if="appearNote.renote"> - <mk-note-preview :note="appearNote.renote"/> - </div> - </div> - </div> - <router-link class="time" :to="appearNote | notePage"> - <mk-time :time="appearNote.createdAt" mode="detail"/> - </router-link> - <div class="visibility-info"> - <span class="visibility" v-if="appearNote.visibility != 'public'"> - <fa v-if="appearNote.visibility == 'home'" icon="home"/> - <fa v-if="appearNote.visibility == 'followers'" icon="unlock"/> - <fa v-if="appearNote.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span> - </div> - <footer> - <mk-reactions-viewer :note="appearNote"/> - <button @click="reply()" :title="$t('title')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton"> - <fa icon="minus"/> - </button> - <button @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSub from './note.sub.vue'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; -import noteMixin from '../../../common/scripts/note-mixin'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/note-detail.vue'), - - components: { - XSub - }, - - mixins: [noteMixin(), noteSubscriber('note')], - - props: { - note: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - - data() { - return { - conversation: [], - conversationFetching: false, - replies: [] - }; - }, - - watch: { - note() { - this.fetchReplies(); - } - }, - - mounted() { - this.fetchReplies(); - }, - - methods: { - fetchReplies() { - if (this.compact) return; - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - }, - - fetchConversation() { - this.conversationFetching = true; - - // Fetch conversation - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversationFetching = false; - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-detail - overflow hidden - width 100% - text-align left - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - - > .fetching - padding 64px 0 - - > .more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color var(--text) - cursor pointer - background var(--subNoteBg) - outline none - border none - border-bottom solid 1px var(--faceDivider) - border-radius 6px 6px 0 0 - box-shadow none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .conversation - > * - border-bottom 1px solid var(--faceDivider) - - > .renote + article - padding-top 8px - - > .reply-to - border-bottom 1px solid var(--faceDivider) - - > article - padding 14px 16px 9px 16px - - @media (min-width 500px) - padding 28px 32px 18px 32px - - > header - display flex - line-height 1.1em - - > .avatar - display block - margin 0 12px 0 0 - width 54px - height 54px - border-radius 8px - - @media (min-width 500px) - width 60px - height 60px - - > div - min-width 0 - - > .name - display inline-block - margin .4em 0 - color var(--noteHeaderName) - 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 var(--noteHeaderAcct) - - > .body - padding 8px 0 - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 16px - color var(--noteText) - - @media (min-width 500px) - font-size 24px - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed 1px var(--quoteBorder) - border-radius 8px - - > .location - margin 4px 0 - font-size 12px - color var(--text) - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .time - font-size 16px - color var(--noteHeaderInfo) - - > .visibility-info - color var(--noteHeaderInfo) - - > .localOnly - margin-left 4px - - > footer - font-size 1.2em - - > button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted - color var(--primary) - - > .replies - > * - border-top 1px solid var(--faceDivider) - -</style> diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue deleted file mode 100644 index 1dbbddaa6264566e1655178fc47bc3833706af15..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/note-preview.vue +++ /dev/null @@ -1,114 +0,0 @@ -<template> -<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart' && !narrow"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <span class="text" v-if="note.cw != ''">{{ note.cw }}</span> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="stylus" scoped> -.yohlumlkhizgfkvvscwfcrcggkotpvry - display flex - margin 0 - padding 0 - overflow hidden - font-size 10px - - &:not(.mini) - - @media (min-width 350px) - font-size 12px - - @media (min-width 500px) - font-size 14px - - > .avatar - - @media (min-width 350px) - margin 0 10px 0 0 - width 44px - height 44px - - @media (min-width 500px) - margin 0 12px 0 0 - width 48px - height 48px - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 10px 0 0 - width 40px - height 40px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - -</style> diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue deleted file mode 100644 index b951947f2af88916bb53522c93c4a376e537f0e6..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/note.sub.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - }, - // TODO - truncate: { - type: Boolean, - default: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="stylus" scoped> -.zlrxdaqttccpwhpaagdmkawtzklsccam - display flex - padding 16px - font-size 10px - background var(--subNoteBg) - - &:not(.mini) - - @media (min-width 350px) - font-size 12px - - @media (min-width 500px) - font-size 14px - - @media (min-width 600px) - padding 24px 32px - - > .avatar - - @media (min-width 350px) - margin-right 10px - width 42px - height 42px - - @media (min-width 500px) - margin-right 14px - width 50px - height 50px - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 8px 0 0 - width 38px - height 38px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - margin 0 - padding 0 - color var(--subNoteText) - font-size calc(1em + var(--fontSize)) - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue deleted file mode 100644 index 01514f05fc2e3097d72a7eee91a9c636d309a04a..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/note.vue +++ /dev/null @@ -1,302 +0,0 @@ -<template> -<div - class="note" - v-show="appearNote.deletedAt == null && !hideThisNote" - :tabindex="appearNote.deletedAt == null ? '-1' : null" - :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart', mini: narrow }" - v-hotkey="keymap" -> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article class="article"> - <mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/> - <div class="main"> - <mk-note-header class="header" :note="appearNote" :mini="true"/> - <div class="body" v-if="appearNote.deletedAt == null"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - <a class="rp" v-if="appearNote.renote != null">RN:</a> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> - </div> - <span class="app" v-if="appearNote.app && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span> - </div> - <footer v-if="appearNote.deletedAt == null" class="footer"> - <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/> - <button @click="reply()" class="button"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote" class="button"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="button"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="button" @click="react()" ref="reactButton"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="button reacted" @click="undoReact(appearNote)" ref="reactButton"> - <fa icon="minus"/> - </button> - <button class="button" @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> - </div> - </article> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -import XSub from './note.sub.vue'; -import noteMixin from '../../../common/scripts/note-mixin'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/note.vue'), - components: { - XSub - }, - - mixins: [ - noteMixin({ - mobile: true - }), - noteSubscriber('note') - ], - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - conversation: [], - replies: [] - }; - }, - - created() { - if (this.detail) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - overflow hidden - font-size 13px - border-bottom solid var(--lineWidth) var(--faceDivider) - - &:last-of-type - border-bottom none - - &:not(.mini) - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - > .article - @media (min-width 600px) - padding 32px 32px 22px - - > .avatar - @media (min-width 350px) - width 48px - height 48px - border-radius 6px - - @media (min-width 500px) - margin-right 16px - width 58px - height 58px - border-radius 8px - - > .main - > .header - @media (min-width 500px) - margin-bottom 2px - - > .body - @media (min-width 700px) - font-size 1.1em - - &.smart - > .article - > .main - > header - align-items center - margin-bottom 4px - - > .renote + .article - padding-top 8px - - > .article - display flex - padding 16px 16px 9px - - > .avatar - flex-shrink 0 - display block - margin 0 10px 8px 0 - width 42px - height 42px - border-radius 6px - //position -webkit-sticky - //position sticky - //top 62px - - > .main - flex 1 - min-width 0 - - > .body - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - font-size calc(1em + var(--fontSize)) - - > .reply - margin-right 8px - color var(--noteText) - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed var(--lineWidth) var(--quoteBorder) - border-radius 8px - - > .app - font-size 12px - color #ccc - - > .footer - > .button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted - color var(--primary) - - > .deleted - color var(--noteText) - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue deleted file mode 100644 index 1a0cd5cc240835d68240f1cfdfa32ef6a4816ccc..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/notes.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> - <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id"/> - <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - - onQueueChanged: (self, x) => { - if (x.length > 0) { - self.$store.commit('indicate', true); - } else { - self.$store.commit('indicate', false); - } - }, - - onPrepend: (self, note) => { - // å¼¾ã - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブãŒéžè¡¨ç¤ºã¾ãŸã¯ã‚¹ã‚¯ãƒãƒ¼ãƒ«ä½ç½®ãŒæœ€ä¸Šéƒ¨ã§ã¯ãªã„ãªã‚‰ã‚¿ã‚¤ãƒˆãƒ«ã§é€šçŸ¥ - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - }, - - onInited: (self) => { - self.$emit('loaded'); - } - }), - ], - - props: { - pagination: { - required: true - }, - }, - - computed: { - _notes(): any[] { - return (this.items as any).map(item => { - const date = new Date(item.createdAt).getDate(); - const month = new Date(item.createdAt).getMonth() + 1; - item._date = date; - item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return item; - }); - } - }, -}); -</script> - -<style lang="stylus" scoped> -.ivaojijs - overflow hidden - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - - > .empty - padding 16px - text-align center - color var(--text) - - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.9em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .placeholder - padding 16px - opacity 0.3 - - @media (min-width 500px) - padding 32px - - > .empty - margin 0 auto - padding 32px - max-width 400px - text-align center - color var(--text) - - > footer - text-align center - border-top solid var(--lineWidth) var(--faceDivider) - - &:empty - display none - - > button - margin 0 - padding 16px - width 100% - color var(--text) - - @media (min-width 500px) - padding 20px - - &:disabled - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue deleted file mode 100644 index 8422c7342064b87b1cd5729954becad705a82a2d..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ /dev/null @@ -1,137 +0,0 @@ -<template> -<div class="mk-notification-preview" :class="notification.type"> - <template v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><mk-reaction-icon :reaction="notification.reaction"/><mk-user-name :user="notification.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p> - </div> - </template> - - <template v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="retweet"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note.renote) }}<fa icon="quote-right"/></p> - </div> - </template> - - <template v-if="notification.type == 'quote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="quote-left"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="user-plus"/><mk-user-name :user="notification.user"/></p> - </div> - </template> - - <template v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="user-clock"/><mk-user-name :user="notification.user"/></p> - </div> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="reply"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="at"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p> - </div> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notification-preview - margin 0 - padding 8px - color #fff - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - [data-icon], mk-reaction-icon - margin-right 4px - - .note-ref - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.renote, &.quote - .text p [data-icon] - color #77B255 - - &.follow - .text p [data-icon] - color #53c7ce - - &.receiveFollowRequest - .text p [data-icon] - color #888 - - &.reply, &.mention - .text p [data-icon] - color #fff - -</style> - diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue deleted file mode 100644 index 2defef477755d7dc9fa10641e40efdad096cb509..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/notification.vue +++ /dev/null @@ -1,199 +0,0 @@ -<template> -<div class="mk-notification"> - <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <mk-reaction-icon :reaction="notification.reaction"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification renote" v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="retweet"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-plus"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-clock"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification pollVote" v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="chart-pie"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-notification - - &.wide - > .notification - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - @media (min-width 600px) - padding 24px 32px - - > .avatar - @media (min-width 500px) - width 42px - height 42px - - > div - @media (min-width 500px) - width calc(100% - 42px) - - > .notification - padding 16px - font-size 12px - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > div - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - [data-icon], .mk-reaction-icon - margin-right 4px - - > .name - text-overflow ellipsis - white-space nowrap - min-width 0 - overflow hidden - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - > .note-preview - color var(--noteText) - - > .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - > div > header - align-items normal - - &.renote - > div > header [data-icon] - color #77B255 - - &.follow - > div > header [data-icon] - color #53c7ce - - &.receiveFollowRequest - > div > header [data-icon] - color #888 - -</style> diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue deleted file mode 100644 index ca6a8beca328e4ee5dfcd7f51bd9e6ac7fa8ff11..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/notifications.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効ã«ã™ã‚‹ã¨ãªãœã‹ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã™ã‚‹ --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> - <template v-for="(notification, i) in _notifications"> - <mk-notification :notification="notification" :key="notification.id" :class="{ wide: wide }"/> - <p class="date" :key="notification.id + '_date'" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - {{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/notifications.vue'), - - mixins: [ - paging({ - beforeInit: (self) => { - self.$emit('beforeInit'); - }, - onInited: (self) => { - self.$emit('inited'); - } - }), - ], - - props: { - type: { - type: String, - required: false - }, - wide: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'i/notifications', - limit: 15, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーãŒç”»é¢ã‚’見ã¦ãªã„ã¨æ€ã‚れるã¨ã(ブラウザやタブãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã˜ã‚ƒãªã„ãªã©)ã¯é€ä¿¡ã—ãªã„ - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .notifications - - > .mk-notification:not(:last-child) - border-bottom solid var(--lineWidth) var(--faceDivider) - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 32px - opacity 0.3 - -</style> diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue deleted file mode 100644 index c6e1df0fde254f512126c27aeb837b16b89a29f3..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/notify.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<div class="mk-notify" :class="pos"> - <div> - <mk-notification-preview :notification="notification"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: ['notification'], - computed: { - pos() { - return this.$store.state.device.mobileNotificationPosition; - } - }, - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$el, - [this.pos]: '0px', - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.$el, - [this.pos]: `-${this.$el.offsetHeight}px`, - duration: 500, - easing: 'easeOutQuad', - complete: () => this.destroyDom() - }); - }, 6000); - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notify - $height = 78px - - position fixed - z-index 10000 - left 0 - right 0 - width 100% - max-width 500px - height $height - margin 0 auto - padding 8px - pointer-events none - font-size 80% - - &.bottom - bottom -($height) - - &.top - top -($height) - - > div - height 100% - -webkit-backdrop-filter blur(2px) - backdrop-filter blur(2px) - background-color rgba(#000, 0.5) - border-radius 7px - overflow hidden - -</style> diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue deleted file mode 100644 index 4ae79dbd7b52a377310dc7b8b0a8b338ab5e3c9f..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/post-form-dialog.vue +++ /dev/null @@ -1,120 +0,0 @@ -<template> -<ui-modal - ref="modal" - :close-on-bg-click="false" - :close-anime-duration="300" - @before-close="onBeforeClose"> - <div class="main" ref="main"> - <x-post-form ref="form" - :reply="reply" - :renote="renote" - :mention="mention" - :initial-text="initialText" - :initial-note="initialNote" - :instant="instant" - @posted="onPosted" - @cancel="onCanceled"/> - </div> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import XPostForm from './post-form.vue'; - -export default Vue.extend({ - components: { - XPostForm - }, - - props: { - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialNote: { - type: Object, - required: false - }, - instant: { - type: Boolean, - required: false, - default: false - } - }, - - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$refs.main, - opacity: 1, - translateY: [-16, 0], - duration: 300, - easing: 'easeOutQuad' - }); - }); - }, - - methods: { - focus() { - this.$refs.form.focus(); - }, - - onBeforeClose() { - (this.$refs.main as any).style.pointerEvents = 'none'; - - anime({ - targets: this.$refs.main, - opacity: 0, - translateY: 16, - duration: 300, - easing: 'easeOutQuad' - }); - }, - - close() { - (this.$refs.modal as any).close(); - }, - - onPosted() { - this.$emit('posted'); - this.close(); - }, - - onCanceled() { - this.$emit('cancel'); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> - -.main - display block - position fixed - z-index 10000 - top 0 - left 0 - right 0 - height 100% - overflow auto - margin 0 auto 0 auto - opacity 0 - transform translateY(-16px) - -</style> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue deleted file mode 100644 index 38c6a42dd569a33143e601d1c68ed22ad5209c94..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/post-form.vue +++ /dev/null @@ -1,244 +0,0 @@ -<template> -<div class="gafaadew"> - <div class="form" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" - > - <header> - <button class="cancel" @click="cancel"><fa icon="times"/></button> - <div> - <span class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</span> - <span class="geo" v-if="geo"><fa icon="map-marker-alt"/></span> - <button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> - </div> - </header> - <div class="form"> - <mk-note-preview class="preview" v-if="reply" :note="reply"/> - <mk-note-preview class="preview" v-if="renote" :note="renote"/> - <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> - <div v-if="visibility === 'specified'" class="to-specified"> - <fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }} - <div class="visibleUsers"> - <span v-for="u in visibleUsers"> - <mk-user-name :user="u"/> - <button @click="removeVisibleUser(u)"><fa icon="times"/></button> - </span> - <button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button> - </div> - </div> - <div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div> - <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }"> - <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @paste="onPaste"></textarea> - <x-post-form-attaches class="attaches" :files="files"/> - <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <footer> - <button class="upload" @click="chooseFile"><fa icon="upload"/></button> - <button class="drive" @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button class="kao" @click="kao"><fa :icon="['far', 'smile']"/></button> - <button class="poll" @click="poll = true"><fa icon="chart-pie"/></button> - <button class="poll" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> - <button class="geo" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> - <button class="visibility" @click="setVisibility" ref="visibilityButton"> - <span v-if="visibility === 'public'"><fa icon="globe"/></span> - <span v-if="visibility === 'home'"><fa icon="home"/></span> - <span v-if="visibility === 'followers'"><fa icon="unlock"/></span> - <span v-if="visibility === 'specified'"><fa icon="envelope"/></span> - </button> - </footer> - <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/> - </div> - </div> - <div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags"> - <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import form from '../../../common/scripts/post-form'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - form({ - mobile: true - }), - ], - - methods: { - cancel() { - this.$emit('cancel'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.gafaadew - max-width 500px - width calc(100% - 16px) - margin 8px auto - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > .form - box-shadow 0 8px 32px rgba(#000, 0.1) - - @media (min-width 600px) - margin 32px auto - - > .form - background var(--face) - border-radius 8px - box-shadow 0 0 2px rgba(#000, 0.1) - - > header - z-index 1000 - height 50px - box-shadow 0 1px 0 0 var(--mobilePostFormDivider) - - > .cancel - padding 0 - width 50px - line-height 50px - font-size 24px - color var(--text) - - > div - position absolute - top 0 - right 0 - color var(--text) - - > .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 var(--primaryForeground) - background var(--primary) - border-radius 4px - - &:disabled - opacity 0.7 - - > .form - max-width 500px - margin 0 auto - - > .preview - padding 16px - - > .with-quote - margin 0 0 8px 0 - color var(--primary) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .to-specified - margin 0 0 8px 0 - color var(--primary) - - > .visibleUsers - display inline - top -1px - font-size 14px - - > span - margin-left 14px - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .local-only - margin 0 0 8px 0 - color var(--primary) - - > input - z-index 1 - - > input - > textarea - display block - padding 12px - margin 0 - width 100% - font-size 16px - color var(--inputText) - background var(--mobilePostFormTextareaBg) - border none - border-radius 0 - box-shadow 0 1px 0 0 var(--mobilePostFormDivider) - - &:disabled - opacity 0.5 - - > textarea - max-width 100% - min-width 100% - min-height 80px - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - - > .file - display none - - > footer - white-space nowrap - overflow auto - -webkit-overflow-scrolling touch - overflow-scrolling touch - - > * - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color var(--mobilePostFormButton) - background transparent - outline none - border none - border-radius 0 - box-shadow none - - > .hashtags - margin 8px - - > * - margin-right 8px - -</style> diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue deleted file mode 100644 index 66dbb90ebbbe45d0e8736a8f09c51c014f7cc622..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/sub-note-content.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="mk-sub-note-content"> - <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> - <a class="reply" v-if="note.replyId"><fa icon="reply"/></a> - <mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/> - <a class="rp" v-if="note.renoteId">RN: ...</a> - </div> - <details v-if="note.files.length > 0"> - <summary>({{ $t('media-count').replace('{}', note.files.length) }})</summary> - <mk-media-list :media-list="note.files"/> - </details> - <details v-if="note.poll"> - <summary>{{ $t('poll') }}</summary> - <mk-poll :note="note"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/components/sub-note-content.vue'), - props: ['note'] -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-note-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - mk-poll - font-size 80% - -</style> diff --git a/src/client/app/mobile/views/components/ui-container.vue b/src/client/app/mobile/views/components/ui-container.vue deleted file mode 100644 index 08af7035f9e385f316ad887c99eb924cce369d1b..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/ui-container.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div class="ukygtjoj" :class="{ naked, inNakedDeckColumn, hideHeader: !showHeader, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header v-if="showHeader" @click="() => showBody = !showBody"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - <button v-if="bodyTogglable"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </header> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - }, - bodyTogglable: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - inject: { - inNakedDeckColumn: { - default: false - } - }, - data() { - return { - showBody: this.expanded - }; - }, - methods: { - toggleContent(show: boolean) { - if (!this.bodyTogglable) return; - this.showBody = show; - } - } -}); -</script> - -<style lang="stylus" scoped> -.ukygtjoj - overflow hidden - - &:not(.inNakedDeckColumn) - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - & + .ukygtjoj - margin-top 16px - - @media (max-width 500px) - margin-top 8px - - &.naked - background transparent !important - box-shadow none !important - - > header - > .title - margin 0 - padding 8px 10px - font-size 15px - font-weight normal - color var(--faceHeaderText) - background var(--faceHeader) - - > [data-icon] - 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 var(--faceTextButton) - - > div - color var(--text) - - &.inNakedDeckColumn - background var(--face) - - > header - margin 0 - padding 8px 16px - font-size 12px - color var(--text) - background var(--deckColumnBg) - - > button - position absolute - top 0 - right 8px - padding 8px 6px - font-size 14px - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue deleted file mode 100644 index f20f64e7ff102eaaf66808d7cb910ccfef9bec63..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/ui.header.vue +++ /dev/null @@ -1,142 +0,0 @@ -<template> -<div class="header" ref="root" :class="{ shadow: $store.state.device.useShadow }"> - <div class="main" ref="main"> - <div class="backdrop"></div> - <div class="content" ref="mainContainer"> - <button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button> - <i v-if="$parent.indicate" class="circle"><fa icon="circle"/></i> - <h1> - <slot>{{ $root.instanceName }}</slot> - </h1> - <slot name="func"></slot> - </div> - </div> - <div class="indicator" v-show="$store.state.indicate"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { env } from '../../../config'; - -export default Vue.extend({ - i18n: i18n(), - props: ['func'], - - data() { - return { - env: env - }; - }, - - mounted() { - this.$store.commit('setUiHeaderHeight', 48); - }, -}); -</script> - -<style lang="stylus" scoped> -.header - $height = 48px - - position fixed - top 0 - left -8px - z-index 1024 - width calc(100% + 16px) - padding 0 8px - - &.shadow - box-shadow 0 0 8px rgba(0, 0, 0, 0.25) - - &, * - user-select none - - > .indicator - height 3px - background var(--primary) - - > .warn - display block - margin 0 - padding 4px - text-align center - font-size 12px - background #f00 - color #fff - - > .main - color var(--mobileHeaderFg) - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height $height - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color var(--mobileHeaderBg) - - > .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 - - > 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-icon] - transition all 0.2s ease - - > i.circle - position absolute - top 8px - left 8px - pointer-events none - font-size 10px - color var(--notificationIndicator) - - > 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/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue deleted file mode 100644 index db250ec6f81eeb5037d4ef43b058b382716b236e..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ /dev/null @@ -1,346 +0,0 @@ -<template> -<div class="fquwcbxs"> - <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" :class="{ notifications: showNotifications }" v-if="isOpen"> - <div class="nav" v-show="!showNotifications"> - <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`"> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <p class="name"><mk-user-name :user="$store.state.i"/></p> - </router-link> - <div class="links"> - <ul> - <li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.state.device.enableMobileQuickNotificationView"><p @click="showNotifications = true"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li> - <li v-else><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/follow-requests" :data-active="$route.name == 'follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/groups" :data-active="$route.name == 'user-groups'"><i><fa :icon="faUsers" fixed-width/></i>{{ $t('user-groups') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> - <li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('@.settings') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> - </ul> - <ul> - <li @click="toggleDeckMode"><p><i><fa :icon="$store.state.device.inDeckMode ? faHome : faColumns" fixed-width/></i><span>{{ $store.state.device.inDeckMode ? $t('@.home') : $t('@.deck') }}</span></p></li> - <li @click="dark"><p><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li> - </ul> - </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span v-html="announcement.title" class="title"></span> - <div><mfm :text="announcement.text"/></div> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/> - </article> - </div> - <a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a> - </div> - <div class="notifications" v-if="showNotifications"> - <header> - <button @click="showNotifications = false"><fa icon="times"/></button> - <i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i> - </header> - <mk-notifications/> - </div> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { lang } from '../../../config'; -import { faNewspaper, faHashtag, faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun, faStickyNote, faBell } from '@fortawesome/free-regular-svg-icons'; -import { search } from '../../../common/scripts/search'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/ui.nav.vue'), - - props: ['isOpen'], - - provide: { - narrow: true - }, - - data() { - return { - hasGameInvitation: false, - connection: null, - aboutUrl: `/docs/${lang}/about`, - announcements: [], - searching: false, - showNotifications: false, - faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote, faUsers, faBell, - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - } - }, - - watch: { - isOpen() { - this.showNotifications = false; - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - }); - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - search() { - if (this.searching) return; - - this.$root.dialog({ - title: this.$t('search'), - input: true - }).then(async ({ canceled, result: query }) => { - if (canceled) return; - - this.searching = true; - search(this, query).finally(() => { - this.searching = false; - }); - }); - }, - - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - - toggleDeckMode() { - this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode }); - location.replace('/'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.fquwcbxs - $color = var(--text) - - .backdrop - position fixed - top 0 - left 0 - z-index 1025 - width 100% - height 100% - background var(--mobileNavBackdrop) - - .body - position fixed - top 0 - left 0 - z-index 1026 - width 240px - height 100% - overflow auto - -webkit-overflow-scrolling touch - background var(--secondary) - font-size 15px - - &.notifications - width 330px - - > .notifications - padding-top 42px - - > header - position fixed - top 0 - left 0 - z-index 1000 - width 330px - line-height 42px - background var(--secondary) - - > button - display block - padding 0 14px - font-size 20px - line-height 42px - color var(--text) - - > i - position absolute - top 0 - right 16px - font-size 12px - color var(--notificationIndicator) - - > .nav - - > .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 $color - 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 - - &:last-child - margin-bottom 0 - - > li - display block - font-size 1em - line-height 1em - - a, p - display block - margin 0 - padding 0 20px - line-height 3rem - line-height calc(1rem + 30px) - color $color - text-decoration none - - &[data-active] - color var(--primaryForeground) - background var(--primary) - - > i:last-child - color var(--primaryForeground) - - > i:first-child - margin-right 0.5em - width 20px - text-align center - - > i.circle - margin-left 6px - font-size 10px - color var(--notificationIndicator) - - > i:last-child - position absolute - top 0 - right 0 - padding 0 20px - font-size 1.2em - line-height calc(1rem + 30px) - color $color - opacity 0.5 - - .announcements - > article - background var(--mobileAnnouncement) - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px - - > .title - font-weight bold - - .about - margin 0 0 8px 0 - padding 1em 0 - text-align center - font-size 0.8em - color $color - opacity 0.5 - -.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/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue deleted file mode 100644 index 05c886a497dae4f51741ff573e27beeda9b76ba7..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/ui.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<div class="mk-ui" :class="{ deck: $store.state.device.inDeckMode }"> - <x-header v-if="!$store.state.device.inDeckMode"> - <template #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="$store.getters.isSignedIn"/> - <button class="nav button" v-if="$store.state.device.inDeckMode" @click="isDrawerOpening = !isDrawerOpening"><fa icon="bars"/><i v-if="indicate"><fa icon="circle"/></i></button> - <button class="post button" v-if="$store.state.device.inDeckMode" @click="$post()"><fa icon="pencil-alt"/></button> -</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 { - hasGameInvitation: false, - isDrawerOpening: false, - connection: null - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - }, - - indicate(): boolean { - return this.hasUnreadNotification || this.hasUnreadMessagingMessage || this.hasGameInvitation; - } - }, - - watch: { - '$store.state.uiHeaderHeight'() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - } - }, - - mounted() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('notification', this.onNotification); - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーãŒç”»é¢ã‚’見ã¦ãªã„ã¨æ€ã‚れるã¨ã(ブラウザやタブãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã˜ã‚ƒãªã„ãªã©)ã¯é€ä¿¡ã—ãªã„ - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.$root.new(MkNotify, { - notification - }); - }, - - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui - &:not(.deck) - padding-top 48px - - > .button - position fixed - z-index 1000 - bottom 28px - padding 0 - width 64px - height 64px - border-radius 100% - box-shadow 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12) - - > * - font-size 24px - - &.nav - left 28px - background var(--secondary) - color var(--text) - - > i - position absolute - top 0 - left 0 - color var(--notificationIndicator) - font-size 16px - animation blink 1s infinite - - &.post - right 28px - background var(--primary) - color var(--primaryForeground) - -</style> diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue deleted file mode 100644 index d9aa1dad8ac76eafc282a8558e7bcb6badf0a60d..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/user-list-timeline.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['list'], - - data() { - return { - connection: null, - date: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - watch: { - $route: 'init' - }, - - mounted() { - this.init(); - - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - init() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - - onNote(note) { - // Prepend a note - (this.$refs.timeline as any).prepend(note); - }, - - onUserAdded() { - (this.$refs.timeline as any).reload(); - }, - - onUserRemoved() { - (this.$refs.timeline as any).reload(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue deleted file mode 100644 index 3b6baa76befa6164bb263538cfcfc9350d7b1af5..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/user-timeline.vue'), - - props: ['user', 'withMedia'], - - data() { - return { - date: null, - pagination: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - withFiles: this.withMedia, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - }) - } - }; - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - - methods: { - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/directives/index.ts b/src/client/app/mobile/views/directives/index.ts deleted file mode 100644 index 324e07596d90b0d6f6263488fbb6c1c17f3987cd..0000000000000000000000000000000000000000 --- a/src/client/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/client/app/mobile/views/directives/user-preview.ts b/src/client/app/mobile/views/directives/user-preview.ts deleted file mode 100644 index 1a54abc20d7065beff4575a4255f6a396a3c02f5..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/directives/user-preview.ts +++ /dev/null @@ -1,2 +0,0 @@ -// nope -export default {}; diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue deleted file mode 100644 index 05163c6ed9319965f4015095d3c142d4987ad1e3..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/drive.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<mk-ui> - <template #header> - <template v-if="folder"><span style="margin-right:4px;"><fa :icon="['far', 'folder-open']"/></span>{{ folder.name }}</template> - <template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template> - <template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template> - </template> - <template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template> - <x-drive - ref="browser" - :init-folder="initFolder" - :init-file="initFile" - :is-naked="true" - :top="$store.state.uiHeaderHeight" - @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 i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import XMenu from '../../../common/views/components/menu.vue'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/drive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - 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 = `${this.$root.instanceName} Drive`; - }, - 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); - } - }, - onMoveRoot(silent) { - const title = `${this.$root.instanceName} 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} | ${this.$root.instanceName} 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} | ${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, `/i/drive/file/${file.id}`); - } - - document.title = title; - - this.file = file; - this.folder = null; - }, - openContextMenu() { - this.$root.new(XMenu, { - items: [{ - type: 'item', - text: this.$t('contextmenu.upload'), - icon: 'upload', - action: this.$refs.browser.selectLocalFile - }, { - type: 'item', - text: this.$t('contextmenu.url-upload'), - icon: faCloudUploadAlt, - action: this.$refs.browser.urlUpload - }, { - type: 'item', - text: this.$t('contextmenu.create-folder'), - icon: ['far', 'folder'], - action: this.$refs.browser.createFolder - }, ...(this.folder ? [{ - type: 'item', - text: this.$t('contextmenu.rename-folder'), - icon: 'i-cursor', - action: this.$refs.browser.renameFolder - }, { - type: 'item', - text: this.$t('contextmenu.move-folder'), - icon: ['far', 'folder-open'], - action: this.$refs.browser.moveFolder - }, { - type: 'item', - text: this.$t('contextmenu.delete-folder'), - icon: faTrashAlt, - action: this.$refs.browser.deleteFolder - }] : [])], - source: this.$refs.contextSource, - }); - } - } -}); -</script> - diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue deleted file mode 100644 index 69b7bdffb4386cf134ea27ae7d90f7249b444bda..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/games/reversi.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="gamepad"/></span>{{ $t('reversi') }}</template> - <x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/games/reversi.vue'), - components: { - XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - mounted() { - document.title = `${this.$root.instanceName} | ${this.$t('reversi')}`; - }, - methods: { - nav(game, actualNav) { - if (actualNav) { - this.$router.push(`/games/reversi/${game.id}`); - } else { - // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push(`/games/reversi/${game.id}`); - } - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue deleted file mode 100644 index f115458092ce9243ca765c74a382497c94b6716a..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ /dev/null @@ -1,143 +0,0 @@ -<template> -<div> - <ui-container v-if="src == 'home' && alone" :show-header="false" style="margin-bottom:8px;"> - <div class="zrzngnxs"> - <p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p> - <router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link> - </div> - </ui-container> - - <mk-notes ref="timeline" :pagination="pagination" @loaded="() => $emit('loaded')"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/home.timeline.vue'), - - props: { - src: { - type: String, - required: true - }, - tagTl: { - required: false - } - }, - - data() { - return { - streamManager: null, - connection: null, - unreadCount: 0, - date: null, - baseQuery: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }, - query: {}, - endpoint: null, - pagination: null - }; - }, - - computed: { - alone(): boolean { - return this.$store.state.i.followingCount == 0; - } - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - this.connection.dispose(); - }); - - const prepend = note => { - (this.$refs.timeline as any).prepend(note); - }; - - if (this.src == 'tag') { - this.endpoint = 'notes/search-by-tag'; - this.query = { - query: this.tagTl.query - }; - this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query }); - this.connection.on('note', prepend); - } else if (this.src == 'home') { - this.endpoint = 'notes/timeline'; - const onChangeFollowing = () => { - this.fetch(); - }; - this.connection = this.$root.stream.useSharedConnection('homeTimeline'); - this.connection.on('note', prepend); - this.connection.on('follow', onChangeFollowing); - this.connection.on('unfollow', onChangeFollowing); - } else if (this.src == 'local') { - this.endpoint = 'notes/local-timeline'; - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'global') { - this.endpoint = 'notes/global-timeline'; - this.connection = this.$root.stream.useSharedConnection('globalTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'mentions') { - this.endpoint = 'notes/mentions'; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', prepend); - } else if (this.src == 'messages') { - this.endpoint = 'notes/mentions'; - this.query = { - visibility: 'specified' - }; - const onNote = note => { - if (note.visibility == 'specified') { - prepend(note); - } - }; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', onNote); - } - - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - ...this.baseQuery, ...this.query - }) - }; - }, - - methods: { - focus() { - (this.$refs.timeline as any).focus(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.zrzngnxs - padding 16px - text-align center - font-size 14px - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue deleted file mode 100644 index 0d110bf2ee4586387cfa89da5c66aa42400024cd..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/home.vue +++ /dev/null @@ -1,249 +0,0 @@ -<template> -<mk-ui> - <template #header> - <span @click="showNav = true"> - <span :class="$style.title"> - <span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span> - <span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span> - <span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span> - <span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span> - <span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span> - <span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span> - <span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span> - <span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span> - </span> - <span style="margin-left:8px"> - <template v-if="!showNav"><fa icon="angle-down"/></template> - <template v-else><fa icon="angle-up"/></template> - </span> - <i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i> - </span> - </template> - - <template #func> - <button @click="fn"><fa icon="pencil-alt"/></button> - </template> - - <main> - <div class="nav" v-if="showNav"> - <div class="bg" @click="showNav = false"></div> - <div class="pointer"></div> - <div class="body"> - <div> - <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> - <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> - <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> - <div class="hr"></div> - <span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span> - <span :data-active="src == 'messages'" @click="src = 'messages'"><fa :icon="['far', 'envelope']"/> {{ $t('messages') }}<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></span> - <template v-if="lists"> - <div class="hr" v-if="lists.length > 0"></div> - <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id"><fa icon="list"/> {{ l.name }}</span> - </template> - <div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div> - <span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id"><fa icon="hashtag"/> {{ tl.title }}</span> - </div> - </div> - </div> - - <div class="tl"> - <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> - <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> - <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> - <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> - <x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> - <x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> - <x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> - <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> - </div> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import XTl from './home.timeline.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/home.vue'), - - components: { - XTl - }, - - data() { - return { - src: 'home', - list: null, - lists: null, - tagTl: null, - showNav: false, - enableLocalTimeline: false, - enableGlobalTimeline: false, - }; - }, - - watch: { - src() { - this.showNav = false; - this.saveSrc(); - }, - - list(x) { - this.showNav = false; - this.saveSrc(); - if (x != null) this.tagTl = null; - }, - - tagTl(x) { - this.showNav = false; - this.saveSrc(); - if (x != null) this.list = null; - }, - - showNav(v) { - if (v && this.lists === null) { - this.$root.api('users/lists/list').then(lists => { - this.lists = lists; - }); - } - } - }, - - created() { - this.$root.getMeta().then((meta: Record<string, any>) => { - if (!( - this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && this.src === 'global') this.src = 'local'; - if (!( - this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; - }); - - if (this.$store.state.device.tl) { - this.src = this.$store.state.device.tl.src; - if (this.src == 'list') { - this.list = this.$store.state.device.tl.arg; - } else if (this.src == 'tag') { - this.tagTl = this.$store.state.device.tl.arg; - } - } - }, - - mounted() { - document.title = this.$root.instanceName; - - Progress.start(); - - (this.$refs.tl as any).$once('loaded', () => { - Progress.done(); - }); - }, - - methods: { - fn() { - this.$post(); - }, - - saveSrc() { - this.$store.commit('device/setTl', { - src: this.src, - arg: this.src == 'list' ? this.list : this.tagTl - }); - }, - - warp() { - - } - } -}); -</script> - -<style lang="stylus" scoped> -main - > .nav - > .pointer - position fixed - z-index 10002 - top 56px - left 0 - right 0 - - $size = 16px - - &:after - content "" - display block - position absolute - top -($size * 2) - left s('calc(50% - %s)', $size) - border-top solid $size transparent - border-left solid $size transparent - border-right solid $size transparent - border-bottom solid $size var(--popupBg) - - > .bg - position fixed - z-index 10000 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 0.5) - - > .body - position fixed - z-index 10001 - top 56px - left 0 - right 0 - width 300px - max-height calc(100% - 70px) - margin 0 auto - overflow auto - -webkit-overflow-scrolling touch - background var(--popupBg) - border-radius 8px - box-shadow 0 0 16px rgba(#000, 0.1) - - > div - padding 8px 0 - - > .hr - margin 8px 0 - border-top solid 1px var(--faceDivider) - - > *:not(.hr) - display block - padding 8px 16px - color var(--text) - - &[data-active] - color var(--primaryForeground) - background var(--primary) - - &:not([data-active]):hover - background var(--mobileHomeTlItemHover) - - > .badge - margin-left 6px - font-size 10px - color var(--notificationIndicator) - -</style> - -<style lang="stylus" module> -.title - [data-icon] - margin-right 4px - -.badge - margin-left 6px - font-size 10px - color var(--notificationIndicator) - vertical-align middle - -</style> diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue deleted file mode 100644 index 7872847127db6f72ce948c647e9514d63ccb0352..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<mk-ui> - <template #header> - <template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template> - <template v-else-if="group"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ group.name }}</template> - <template v-else><mk-ellipsis/></template> - </template> - <x-messaging-room v-if="!fetching" :user="user" :group="group" :is-naked="true"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - data() { - return { - fetching: true, - user: null, - group: null, - unwatchDarkmode: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - const applyBg = v => - document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); - - applyBg(this.$store.state.device.darkmode); - - this.unwatchDarkmode = this.$store.watch(s => { - return s.device.darkmode; - }, applyBg); - - this.fetch(); - }, - beforeDestroy() { - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); // for safari's bug - this.unwatchDarkmode(); - }, - methods: { - fetch() { - this.fetching = true; - if (this.$route.params.user) { - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); - } else { - this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { - this.group = group; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + this.group.name; - }); - } - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue deleted file mode 100644 index ff66ae06e6489a51f453a50198f62a3fbb397a6d..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/messaging.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template> - <x-messaging @navigate="navigate" @navigateGroup="navigateGroup" :header-top="48"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - mounted() { - document.title = `${this.$root.instanceName} ${this.$t('@.messaging')}`; - }, - methods: { - navigate(user) { - (this as any).$router.push(`/i/messaging/${getAcct(user)}`); - }, - navigateGroup(group) { - (this as any).$router.push(`/i/messaging/group/${group.id}`); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue deleted file mode 100644 index 090851fc4e033646b36b83065fb118224c969681..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/note.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'sticky-note']"/></span>{{ $t('title') }}</template> - <main v-if="!fetching"> - <div> - <mk-note-detail :note="note" :key="note.id"/> - </div> - <footer> - <router-link v-if="note.prev" :to="note.prev"><fa icon="angle-left"/> {{ $t('prev') }}</router-link> - <router-link v-if="note.next" :to="note.next">{{ $t('next') }} <fa icon="angle-right"/></router-link> - </footer> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/note.vue'), - data() { - return { - fetching: true, - note: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - text-align center - - > footer - margin-top 16px - - > a - display inline-block - margin 0 16px - -</style> diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue deleted file mode 100644 index 24f8f79ccc9085cd5a090259e738e9de1bf3e529..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/notifications.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<mk-ui> - <template #header><fa :icon="faBell"/> {{ $t('notifications') }}</template> - <template #func> - <button @click="filter()"><fa icon="cog"/></button> - </template> - - <main> - <mk-notifications @before-init="beforeInit()" @inited="inited()" :type="type === 'all' ? null : type" :wide="true" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBell } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/notifications.vue'), - data() { - return { - type: 'all', - faBell, - }; - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - beforeInit() { - Progress.start(); - }, - inited() { - Progress.done(); - }, - filter() { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.type, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.type = type; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main > * - overflow hidden - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) -</style> diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue deleted file mode 100644 index dca1ffd40a9a1bd7d995aa63230dac8fc3b2d295..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/search.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<mk-ui> - <template #header><fa icon="search"/> {{ q }}</template> - - <main> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/search.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - computed: { - q(): string { - return this.$route.query.q; - } - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - mounted() { - document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`; - }, - methods: { - inited() { - Progress.done(); - }, - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue deleted file mode 100644 index 095c19cf2c4e98b20b048fd7ba362fbad694fd69..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/selectdrive.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<div class="mk-selectdrive"> - <header> - <h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="upload" @click="upload"><fa icon="upload"/></button> - <button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/selectdrive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = this.$t('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(#000, 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/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue deleted file mode 100644 index c24a56be7b5be16cbd900147c3ae779194b7716e..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/settings.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template> - <main> - <div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <mfm :text="$t('signed-in-as').replace('{}', name)" :plain="true" :custom-emojis="$store.state.i.emojis"/> - </div> - - <x-settings/> - - <div class="signout" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" @click="signout">{{ $t('@.signout') }}</div> - - <footer> - <small>ver {{ version }} ({{ codename }})</small> - </footer> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSettings from '../../../common/views/components/settings/settings.vue'; -import { version, codename } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/settings.vue'), - components: { - XSettings, - }, - data() { - return { - version, - codename, - }; - }, - computed: { - name(): string { - return Vue.filter('userName')(this.$store.state.i); - }, - }, - methods: { - signout() { - this.$root.signout(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -main - - > .signed-in-as - margin 16px - padding 16px - text-align center - color var(--mobileSignedInAsFg) - background var(--mobileSignedInAsBg) - font-weight bold - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > .signout - margin 16px - padding 16px - text-align center - color var(--mobileSignedInAsFg) - background var(--mobileSignedInAsBg) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > footer - margin 16px - text-align center - color var(--text) - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue deleted file mode 100644 index 81d2741ae565ef2fdc8f7039638641fccc033279..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/signup.vue +++ /dev/null @@ -1,29 +0,0 @@ -<template> -<div class="signup"> - <h1>{{ $t('lets-start') }}</h1> - <mk-signup/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/pages/signup.vue') -}); -</script> - -<style lang="stylus" scoped> -.signup - padding 32px - margin 0 auto - max-width 500px - - h1 - margin 0 - padding 8px 0 0 0 - font-size 1.5em - font-weight bold - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue deleted file mode 100644 index 19482ec382175f3a99049eab768738ab2d9a3a60..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/tag.vue +++ /dev/null @@ -1,40 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template> - - <main> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/tag.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search-by-tag', - limit: 20, - params: { - tag: this.$route.params.tag - } - } - }; - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - methods: { - inited() { - Progress.done(); - }, - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/ui.vue b/src/client/app/mobile/views/pages/ui.vue deleted file mode 100644 index 397ba5df07110ee3d6e19227802da2f23183ca05..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/ui.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;" v-if="icon"><fa :icon="icon"/></span>{{ title }}</template> - - <main> - <component :is="component" @init="init" v-bind="$attrs"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - component: { - required: true - } - }, - - data() { - return { - title: null, - icon: null, - }; - }, - - mounted() { - }, - - methods: { - init(v) { - this.title = v.title; - this.icon = v.icon; - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/user/home.notes.vue b/src/client/app/mobile/views/pages/user/home.notes.vue deleted file mode 100644 index 9abe5b893c5b1034f78f9246dd6b083d89983f7b..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/user/home.notes.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<div class="root notes"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div v-if="!fetching && notes.length > 0"> - <mk-note-card v-for="note in notes" :key="note.id" :note="note"/> - </div> - <p class="empty" v-if="!fetching && notes.length == 0">{{ $t('@.no-notes') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.notes.vue'), - props: ['user'], - data() { - return { - fetching: true, - notes: [] - }; - }, - mounted() { - this.$root.api('users/notes', { - userId: this.user.id, - }).then(notes => { - this.notes = notes; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.notes - - > 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 var(--text) - - > i - margin-right 4px - -</style> diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue deleted file mode 100644 index 316b2a12fe1afa854d5ff4ca55ac44030bd2e429..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/user/home.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<div class="wojmldye"> - <x-page class="page" v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - <mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> - <ui-container :body-togglable="true"> - <template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template> - <div> - <x-notes :user="user"/> - </div> - </ui-container> - <ui-container :body-togglable="true"> - <template #header><fa icon="image"/>{{ $t('images') }}</template> - <div> - <x-photos :user="user"/> - </div> - </ui-container> - <ui-container :body-togglable="true"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <div style="padding:8px;"> - <x-activity :user="user"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XNotes from './home.notes.vue'; -import XPhotos from './home.photos.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.vue'), - components: { - XNotes, - XPhotos, - XPage: () => import('../../../../common/views/components/page/page.vue').then(m => m.default), - XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default) - }, - props: ['user'], - data() { - return { - makeFrequentlyRepliedUsersPromise: () => this.$root.api('users/get_frequently_replied_users', { - userId: this.user.id - }).then(res => res.map(x => x.user)), - makeFollowersYouKnowPromise: () => this.$root.api('users/followers', { - userId: this.user.id, - iknow: true, - limit: 30 - }).then(res => res.users), - }; - } -}); -</script> - -<style lang="stylus" scoped> -.wojmldye - > .page - margin 0 0 8px 0 - - @media (min-width 500px) - margin 0 0 16px 0 - - > .note - margin 0 0 8px 0 - - @media (min-width 500px) - margin 0 0 16px 0 - -</style> diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue deleted file mode 100644 index b8a79a6b3488a727e83fe973d6bb6f80846d15fa..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/user/index.vue +++ /dev/null @@ -1,349 +0,0 @@ -<template> -<mk-ui> - <template #header v-if="!fetching"> - <img :src="avator" alt=""><mk-user-name :user="user" :key="user.id"/> - </template> - <div class="wwtwuxyh" v-if="!fetching"> - <div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div> - <div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div> - <header> - <div class="banner" :style="style"></div> - <div class="body"> - <div class="top"> - <a class="avatar"> - <img :src="avator" alt="avatar"/> - </a> - <button class="menu" ref="menu" @click="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> - </div> - <div class="title"> - <h1><mk-user-name :user="user" :key="user.id" :nowrap="false"/></h1> - <span class="username"><mk-acct :user="user" :detail="true" :key="user.id"/></span> - <span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> - </div> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/> - <x-integrations :user="user" style="margin:20px 0;"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="info"> - <p class="location" v-if="user.host === null && user.location"> - <fa icon="map-marker"/>{{ user.location }} - </p> - <p class="birthday" v-if="user.host === null && user.birthday"> - <fa icon="birthday-cake"/>{{ user.birthday.replace('-', 'å¹´').replace('-', '月') + 'æ—¥' }} ({{ $t('years-old', { age }) }}) - </p> - </div> - <div class="status"> - <router-link :to="user | userPage()"> - <b>{{ user.notesCount | number }}</b> - <i>{{ $t('notes') }}</i> - </router-link> - <router-link :to="user | userPage('following')"> - <b>{{ user.followingCount | number }}</b> - <i>{{ $t('following') }}</i> - </router-link> - <router-link :to="user | userPage('followers')"> - <b>{{ user.followersCount | number }}</b> - <i>{{ $t('followers') }}</i> - </router-link> - </div> - </div> - </header> - <nav v-if="$route.name == 'user'" :class="{ shadow: $store.state.device.useShadow }"> - <div class="nav-container"> - <a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> {{ $t('overview') }}</a> - <a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</a> - <a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> {{ $t('media') }}</a> - </div> - </nav> - <main> - <template v-if="$route.name == 'user'"> - <x-home v-if="page == 'home'" :user="user" :key="user.id"/> - <mk-user-timeline v-if="page == 'notes'" :user="user" :key="`tl:${user.id}`"/> - <mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" :key="`media:${user.id}`"/> - </template> - <router-view :user="user"></router-view> - </main> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as age from 's-age'; -import parseAcct from '../../../../../../misc/acct/parse'; -import Progress from '../../../../common/scripts/loading'; -import XUserMenu from '../../../../common/views/components/user-menu.vue'; -import XHome from './home.vue'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; -import XIntegrations from '../../../../common/views/components/integrations.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user.vue'), - components: { - XHome, - XIntegrations - }, - data() { - return { - fetching: true, - user: null, - page: this.$route.name == 'user' ? 'home' : null - }; - }, - computed: { - age(): number { - return age(this.user.birthday); - }, - avator(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.user.avatarUrl) - : this.user.avatarUrl; - }, - style(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - } - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - Progress.done(); - document.title = `${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); - }, - - menu() { - this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wwtwuxyh - $bg = var(--face) - - > .is-suspended - > .is-remote - &.is-suspended - color #570808 - background #ffdbdb - - &.is-remote - color #573c08 - background #fff0db - - > p - margin 0 auto - padding 14px - max-width 600px - font-size 14px - - > a - font-weight bold - - @media (max-width 500px) - padding 12px - font-size 12px - - > header - background $bg - - > .banner - padding-bottom 33.3% - background-color rgba(0, 0, 0, 0.1) - background-size cover - background-position center - - > .body - padding 12px - margin 0 auto - max-width 600px - - > .top - display flex - - > .avatar - display block - width 25% - height 40px - - > img - display block - position absolute - left -2px - bottom -2px - width 100% - background $bg - border 3px solid $bg - border-radius 6px - - @media (min-width 500px) - left -4px - bottom -4px - border 4px solid $bg - border-radius 12px - - > .menu - margin 0 0 0 auto - padding 8px - margin-right 8px - font-size 18px - color var(--text) - - > .title - margin 8px 0 - - > h1 - margin 0 - line-height 22px - font-size 20px - color var(--mobileUserPageName) - - > .username - display inline-block - line-height 20px - font-size 16px - font-weight bold - color var(--mobileUserPageAcct) - - > .followed - margin-left 8px - padding 2px 4px - font-size 12px - color var(--mobileUserPageFollowedFg) - background var(--mobileUserPageFollowedBg) - border-radius 4px - - > .description - margin 8px 0 - color var(--mobileUserPageDescription) - - @media (max-width 450px) - font-size 15px - - > .fields - margin 8px 0 - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - color var(--mobileUserPageStatusHighlight) - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - color var(--mobileUserPageStatusHighlight) - - > .info - margin 8px 0 - - @media (max-width 450px) - font-size 15px - - > p - display inline - margin 0 16px 0 0 - color var(--text) - - > i - margin-right 4px - - > .status - > a - color var(--text) - - &:not(:last-child) - margin-right 16px - - > b - margin-right 4px - font-size 16px - color var(--mobileUserPageStatusHighlight) - - > i - font-size 14px - - > button - color var(--text) - - > nav - position -webkit-sticky - position sticky - top 47px - background-color $bg - z-index 2 - - &.shadow - box-shadow 0 4px 4px var(--mobileUserPageHeaderShadow) - - > .nav-container - display flex - justify-content center - margin 0 auto - max-width 616px - - > a - display block - flex 1 1 - text-align center - line-height 48px - font-size 12px - text-decoration none - color var(--text) - border-bottom solid 2px transparent - - @media (min-width 400px) - line-height 52px - font-size 14px - - &[data-active] - font-weight bold - color var(--primary) - border-color var(--primary) - -</style> diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue deleted file mode 100644 index 6cf4a36f90eb8c73eb8a58ed4f4e6be5819fd96f..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/welcome.vue +++ /dev/null @@ -1,310 +0,0 @@ -<template> -<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes"> - <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> - - <div> - <img svg-inline src="../../../../assets/title.svg" alt="Misskey"> - <p class="host">{{ host }}</p> - <div class="about"> - <h2>{{ name || 'Misskey' }}</h2> - <p v-html="description || this.$t('@.about')"></p> - <router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link> - </div> - <div class="signin"> - <a href="/signin" @click.prevent="signin()">{{ $t('@.signin') }}</a> - </div> - <div class="tl"> - <mk-welcome-timeline/> - </div> - <div class="hashtags"> - <mk-tag-cloud/> - </div> - <div class="photos"> - <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> - </div> - <div class="stats" v-if="stats"> - <span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span> - <span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span> - </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span class="title" v-html="announcement.title"></span> - <mfm :text="announcement.text"/> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/> - </article> - </div> - <article class="about-misskey"> - <h1>{{ $t('@.intro.title') }}</h1> - <p v-html="this.$t('@.intro.about')"></p> - <section> - <h2>{{ $t('@.intro.features') }}</h2> - <section> - <h3>{{ $t('@.intro.rich-contents') }}</h3> - <div class="image"><img src="/assets/about/post.png" alt=""></div> - <p v-html="this.$t('@.intro.rich-contents-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.reaction') }}</h3> - <div class="image"><img src="/assets/about/reaction.png" alt=""></div> - <p v-html="this.$t('@.intro.reaction-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.ui') }}</h3> - <div class="image"><img src="/assets/about/ui.png" alt=""></div> - <p v-html="this.$t('@.intro.ui-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.drive') }}</h3> - <div class="image"><img src="/assets/about/drive.png" alt=""></div> - <p v-html="this.$t('@.intro.drive-desc')"></p> - </section> - </section> - <p v-html="this.$t('@.intro.outro')"></p> - </article> - <div class="info" v-if="meta"> - <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - </div> - <footer> - <small>{{ copyright }}</small> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { copyright, host } from '../../../config'; -import { concat } from '../../../../../prelude/array'; -import { toUnicode } from 'punycode'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/welcome.vue'), - data() { - return { - meta: null, - copyright, - stats: null, - banner: null, - host: toUnicode(host), - name: null, - description: '', - photos: [], - announcements: [] - }; - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.name = meta.name; - this.description = meta.description; - this.announcements = meta.announcements; - this.banner = meta.bannerUrl; - }); - - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('notes/local-timeline', { - fileType: image, - excludeNsfw: true, - limit: 6 - }).then((notes: any[]) => { - const files = concat(notes.map((n: any): any[] => n.files)); - this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); - }); - }, - methods: { - signin() { - this.$root.dialog({ - type: 'signin' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wgwfgvvimdjvhjfwxropcwksnzftjqes - text-align center - - > .banner - position absolute - top 0 - left 0 - width 100% - height 300px - background-position center - background-size cover - opacity 0.7 - - &:after - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 100px - background linear-gradient(transparent, var(--bg)) - - > div:not(.banner) - padding 32px - margin 0 auto - max-width 500px - - > svg - display block - width 200px - height 50px - margin 0 auto - - > .host - display block - text-align center - padding 6px 12px - line-height 32px - font-weight bold - color #333 - background rgba(#000, 0.035) - border-radius 6px - - > .about - margin-top 16px - padding 16px - color var(--text) - background var(--face) - border-radius 6px - - > h2 - margin 0 - - > p - margin 8px - - > .signup - font-weight bold - - > .signin - margin 16px 0 - - > .tl - margin 16px 0 - - > * - max-height 300px - border-radius 6px - overflow auto - -webkit-overflow-scrolling touch - - > .hashtags - padding 0 8px - height 200px - - > .photos - display grid - grid-template-rows 1fr 1fr 1fr - grid-template-columns 1fr 1fr - gap 8px - height 300px - margin-top 16px - - > div - border-radius 4px - background-position center center - background-size cover - - > .stats - margin 16px 0 - padding 8px - font-size 14px - color var(--text) - background rgba(#000, 0.1) - border-radius 6px - - > * - margin 0 8px - - > .announcements - margin 16px 0 - - > article - background var(--mobileAnnouncement) - border-radius 6px - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px - - > .title - font-weight bold - - > .about-misskey - margin 16px 0 - padding 32px - font-size 14px - background var(--face) - border-radius 6px - overflow hidden - color var(--text) - - > h1 - margin 0 - - & + p - margin-top 8px - - > p:last-child - margin-bottom 0 - - > section - > h2 - border-bottom 1px solid var(--faceDivider) - - > section - margin-bottom 16px - padding-bottom 16px - border-bottom 1px solid var(--faceDivider) - - > h3 - margin-bottom 8px - - > p - margin-bottom 0 - - > .image - > img - display block - width 100% - height 120px - object-fit cover - - > .info - padding 16px 0 - border solid 2px rgba(0, 0, 0, 0.1) - border-radius 8px - color var(--text) - - > * - margin 0 16px - - > footer - text-align center - color var(--text) - - > small - display block - margin 16px 0 0 0 - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue deleted file mode 100644 index 19df613b3aa44a62de1a6b806e3cf341cd813a3c..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/pages/widgets.vue +++ /dev/null @@ -1,192 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="home"/></span>{{ $t('dashboard') }}</template> - <template #func> - <button @click="customizing = !customizing"><fa icon="cog"/></button> - </template> - <main> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - <button @click="addWidget">{{ $t('add-widget') }}</button> - <p><a @click="hint">{{ $t('customization-tips') }}</a></p> - </header> - <x-draggable - :list="widgets" - handle=".handle" - animation="150" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle"><fa icon="bars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/> - </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" platform="mobile"/> - </template> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/widgets.vue'), - components: { - XDraggable - }, - - data() { - return { - showNav: false, - customizing: false, - widgetAdderSelected: null - }; - }, - - computed: { - widgets(): any[] { - return this.$store.getters.mobileHome || []; - } - }, - - created() { - if (this.widgets.length == 0) { - this.$store.commit('setMobileHome', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - } - }, - - mounted() { - document.title = this.$root.instanceName; - }, - - methods: { - hint() { - this.$root.dialog({ - type: 'info', - text: this.$t('widgets-hints') - }); - }, - - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveHome(); - }, - - addWidget() { - if(this.widgetAdderSelected == null) return; - - this.$store.commit('addMobileHomeWidget', { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - }); - }, - - removeWidget(widget) { - this.$store.commit('removeMobileHomeWidget', widget); - }, - - saveHome() { - this.$store.commit('setMobileHome', this.widgets); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - margin 0 auto - padding 8px - max-width 500px - width 100% - - @media (min-width 500px) - padding 16px 8px - - @media (min-width 600px) - padding 32px 8px - - > header - padding 8px - background #fff - - .widget - margin-bottom 8px - - @media (min-width 600px) - margin-bottom 16px - - .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/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue deleted file mode 100644 index 047784deacd9f33128e0fbc51d56faf0ab22c7b0..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/widgets/activity.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<div class="mkw-activity"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <div :class="$style.body"> - <x-activity :user="$store.state.i"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'activity', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n(), - components: { - XActivity: () => import('../../../common/views/components/activity.vue').then(m => m.default) - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.body - padding 8px -</style> diff --git a/src/client/app/mobile/views/widgets/index.ts b/src/client/app/mobile/views/widgets/index.ts deleted file mode 100644 index 4de912b64c6d736f5e86125ea75e4a86ce0b0687..0000000000000000000000000000000000000000 --- a/src/client/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/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue deleted file mode 100644 index d4ccc87e572d296e15c810b5a3ed850cb3bc360a..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/widgets/profile.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div class="mkw-profile"> - <ui-container> - <div :class="$style.banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" - ></div> - <img :class="$style.avatar" - :src="$store.state.i.avatarUrl" - alt="avatar" - /> - <router-link :class="$style.name" :to="$store.state.i | userPage"> - <mk-user-name :user="$store.state.i"/> - </router-link> - </ui-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(#000, 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(#000, 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(#000, 0.5) - -</style> diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl deleted file mode 100644 index 614f29a835c2e250c206fb9a13b2c310f6940d43..0000000000000000000000000000000000000000 --- a/src/client/app/reset.styl +++ /dev/null @@ -1,37 +0,0 @@ -input - min-width 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 Roboto, HelveticaNeue, Arial, sans-serif - -button - margin 0 - background transparent - border none - cursor pointer - color inherit - touch-action manipulation - - * - pointer-events none - user-select none - - &[disabled] - cursor default - -pre - overflow auto - white-space pre diff --git a/src/client/app/safe.js b/src/client/app/safe.js deleted file mode 100644 index 88c603f6b9086a5f144a6b805d03853dfeca11c0..0000000000000000000000000000000000000000 --- a/src/client/app/safe.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * ブラウザã®æ¤œè¨¼ - */ - -// Detect an old browser -if (!('fetch' in window)) { - alert( - 'ãŠä½¿ã„ã®ãƒ–ラウザ(ã¾ãŸã¯OS)ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒæ—§å¼ã®ãŸã‚ã€Misskeyを動作ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。' + - 'ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’最新ã®ã‚‚ã®ã«æ›´æ–°ã™ã‚‹ã‹ã€åˆ¥ã®ãƒ–ラウザをãŠè©¦ã—ãã ã•ã„。' + - '\n\n' + - 'Your browser (or your OS) seems outdated. ' + - 'To run Misskey, please update your browser to latest version or try other browsers.'); -} diff --git a/src/client/app/store.ts b/src/client/app/store.ts deleted file mode 100644 index fd3aceb72814ca9062602b7b8f8e2f94ba797b35..0000000000000000000000000000000000000000 --- a/src/client/app/store.ts +++ /dev/null @@ -1,463 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import createPersistedState from 'vuex-persistedstate'; -import * as nestedProperty from 'nested-property'; - -import MiOS from './mios'; -import { erase } from '../../prelude/array'; -import getNoteSummary from '../../misc/get-note-summary'; - -const defaultSettings = { - keepCw: false, - tagTimelines: [], - fetchOnScroll: true, - remainDeletedNote: false, - showPostFormOnTopOfTl: false, - suggestRecentHashtags: true, - showClockOnHeader: true, - circleIcons: true, - contrastedAcct: true, - showFullAcct: false, - showVia: true, - showReplyTarget: true, - showMyRenotes: true, - showRenotedMyNotes: true, - showLocalRenotes: true, - loadRemoteMedia: true, - disableViaMobile: false, - memo: null, - iLikeSushi: false, - rememberNoteVisibility: false, - defaultNoteVisibility: 'public', - wallpaper: null, - webSearchEngine: 'https://www.google.com/?#q={{query}}', - mutedWords: [], - gamesReversiShowBoardLabels: false, - gamesReversiUseAvatarStones: true, - disableAnimatedMfm: false, - homeProfiles: {}, - mobileHomeProfiles: {}, - deckProfiles: {}, - uploadFolder: null, - pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]', - pasteDialog: false, - reactions: ['like', 'love', 'laugh', 'hmm', 'surprise', 'congrats', 'angry', 'confused', 'rip', 'pudding'] -}; - -const defaultDeviceSettings = { - homeProfile: 'Default', - mobileHomeProfile: 'Default', - deckProfile: 'Default', - deckMode: false, - deckColumnAlign: 'center', - deckColumnWidth: 'normal', - useShadow: false, - roundedCorners: true, - reduceMotion: false, - darkmode: true, - darkTheme: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2', - lightTheme: 'light', - lineWidth: 1, - fontSize: 0, - themes: [], - enableSounds: true, - soundVolume: 0.5, - mediaVolume: 0.5, - lang: null, - appTypeForce: 'auto', - debug: false, - lightmode: false, - loadRawImages: false, - alwaysShowNsfw: false, - postStyle: 'standard', - navbar: 'top', - mobileNotificationPosition: 'bottom', - useOsDefaultEmojis: false, - disableShowingAnimatedImages: false, - expandUsersPhotos: true, - expandUsersActivity: true, - enableMobileQuickNotificationView: false, - roomGraphicsQuality: 'medium', - roomUseOrthographicCamera: true, - activeEmojiCategoryName: undefined, - recentEmojis: [], -}; - -export default (os: MiOS) => new Vuex.Store({ - plugins: [createPersistedState({ - paths: ['i', 'device', 'settings'] - })], - - state: { - i: null, - indicate: false, - uiHeaderHeight: 0, - behindNotes: [] - }, - - getters: { - isSignedIn: state => state.i != null, - - home: state => state.settings.homeProfiles[state.device.homeProfile], - - mobileHome: state => state.settings.mobileHomeProfiles[state.device.mobileHomeProfile], - - deck: state => state.settings.deckProfiles[state.device.deckProfile], - }, - - mutations: { - updateI(state, x) { - state.i = x; - }, - - updateIKeyValue(state, x) { - state.i[x.key] = x.value; - }, - - indicate(state, x) { - state.indicate = x; - }, - - setUiHeaderHeight(state, height) { - state.uiHeaderHeight = height; - }, - - pushBehindNote(state, note) { - if (note.userId === state.i.id) return; - if (state.behindNotes.some(n => n.id === note.id)) return; - state.behindNotes.push(note); - document.title = `(${state.behindNotes.length}) ${getNoteSummary(note)}`; - }, - - clearBehindNotes(state) { - state.behindNotes = []; - document.title = os.instanceName; - }, - - setHome(state, data) { - Vue.set(state.settings.homeProfiles, state.device.homeProfile, data); - os.store.dispatch('settings/updateHomeProfile'); - }, - - setDeck(state, data) { - Vue.set(state.settings.deckProfiles, state.device.deckProfile, data); - os.store.dispatch('settings/updateDeckProfile'); - }, - - addHomeWidget(state, widget) { - state.settings.homeProfiles[state.device.homeProfile].unshift(widget); - os.store.dispatch('settings/updateHomeProfile'); - }, - - setMobileHome(state, data) { - Vue.set(state.settings.mobileHomeProfiles, state.device.mobileHomeProfile, data); - os.store.dispatch('settings/updateMobileHomeProfile'); - }, - - updateWidget(state, x) { - let w; - - //#region Desktop home - const home = state.settings.homeProfiles[state.device.homeProfile]; - if (home) { - w = home.find(w => w.id == x.id); - if (w) { - w.data = x.data; - os.store.dispatch('settings/updateHomeProfile'); - } - } - //#endregion - - //#region Mobile home - const mobileHome = state.settings.mobileHomeProfiles[state.device.mobileHomeProfile]; - if (mobileHome) { - w = mobileHome.find(w => w.id == x.id); - if (w) { - w.data = x.data; - os.store.dispatch('settings/updateMobileHomeProfile'); - } - } - //#endregion - }, - - addMobileHomeWidget(state, widget) { - state.settings.mobileHomeProfiles[state.device.mobileHomeProfile].unshift(widget); - os.store.dispatch('settings/updateMobileHomeProfile'); - }, - - removeMobileHomeWidget(state, widget) { - Vue.set('state.settings.mobileHomeProfiles', state.device.mobileHomeProfile, state.settings.mobileHomeProfiles[state.device.mobileHomeProfile].filter(w => w.id != widget.id)); - os.store.dispatch('settings/updateMobileHomeProfile'); - }, - - addDeckColumn(state, column) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - if (column.name == undefined) column.name = null; - deck.columns.push(column); - deck.layout.push([column.id]); - os.store.dispatch('settings/updateDeckProfile'); - }, - - removeDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - deck.columns = deck.columns.filter(c => c.id != id); - deck.layout = deck.layout.map(ids => erase(id, ids)); - deck.layout = deck.layout.filter(ids => ids.length > 0); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapDeckColumn(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const a = x.a; - const b = x.b; - const aX = deck.layout.findIndex(ids => ids.indexOf(a) != -1); - const aY = deck.layout[aX].findIndex(id => id == a); - const bX = deck.layout.findIndex(ids => ids.indexOf(b) != -1); - const bY = deck.layout[bX].findIndex(id => id == b); - deck.layout[aX][aY] = b; - deck.layout[bX][bY] = a; - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapLeftDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - deck.layout.some((ids, i) => { - if (ids.indexOf(id) != -1) { - const left = deck.layout[i - 1]; - if (left) { - // https://vuejs.org/v2/guide/list.html#Caveats - //state.deck.layout[i - 1] = state.deck.layout[i]; - //state.deck.layout[i] = left; - deck.layout.splice(i - 1, 1, deck.layout[i]); - deck.layout.splice(i, 1, left); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapRightDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - deck.layout.some((ids, i) => { - if (ids.indexOf(id) != -1) { - const right = deck.layout[i + 1]; - if (right) { - // https://vuejs.org/v2/guide/list.html#Caveats - //state.deck.layout[i + 1] = state.deck.layout[i]; - //state.deck.layout[i] = right; - deck.layout.splice(i + 1, 1, deck.layout[i]); - deck.layout.splice(i, 1, right); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapUpDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const ids = deck.layout.find(ids => ids.indexOf(id) != -1); - ids.some((x, i) => { - if (x == id) { - const up = ids[i - 1]; - if (up) { - // https://vuejs.org/v2/guide/list.html#Caveats - //ids[i - 1] = id; - //ids[i] = up; - ids.splice(i - 1, 1, id); - ids.splice(i, 1, up); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - swapDownDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const ids = deck.layout.find(ids => ids.indexOf(id) != -1); - ids.some((x, i) => { - if (x == id) { - const down = ids[i + 1]; - if (down) { - // https://vuejs.org/v2/guide/list.html#Caveats - //ids[i + 1] = id; - //ids[i] = down; - ids.splice(i + 1, 1, id); - ids.splice(i, 1, down); - } - return true; - } - }); - os.store.dispatch('settings/updateDeckProfile'); - }, - - stackLeftDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const i = deck.layout.findIndex(ids => ids.indexOf(id) != -1); - deck.layout = deck.layout.map(ids => erase(id, ids)); - const left = deck.layout[i - 1]; - if (left) deck.layout[i - 1].push(id); - deck.layout = deck.layout.filter(ids => ids.length > 0); - os.store.dispatch('settings/updateDeckProfile'); - }, - - popRightDeckColumn(state, id) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const i = deck.layout.findIndex(ids => ids.indexOf(id) != -1); - deck.layout = deck.layout.map(ids => erase(id, ids)); - deck.layout.splice(i + 1, 0, [id]); - deck.layout = deck.layout.filter(ids => ids.length > 0); - os.store.dispatch('settings/updateDeckProfile'); - }, - - addDeckWidget(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column.widgets.unshift(x.widget); - os.store.dispatch('settings/updateDeckProfile'); - }, - - removeDeckWidget(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column.widgets = column.widgets.filter(w => w.id != x.widget.id); - os.store.dispatch('settings/updateDeckProfile'); - }, - - renameDeckColumn(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - const column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column.name = x.name; - os.store.dispatch('settings/updateDeckProfile'); - }, - - updateDeckColumn(state, x) { - const deck = state.settings.deckProfiles[state.device.deckProfile]; - let column = deck.columns.find(c => c.id == x.id); - if (column == null) return; - column = x; - os.store.dispatch('settings/updateDeckProfile'); - } - }, - - actions: { - login(ctx, i) { - ctx.commit('updateI', i); - ctx.dispatch('settings/merge', i.clientData); - }, - - logout(ctx) { - ctx.commit('updateI', null); - document.cookie = `i=; max-age=0; domain=${document.location.hostname}`; - localStorage.removeItem('i'); - }, - - mergeMe(ctx, me) { - for (const [key, value] of Object.entries(me)) { - ctx.commit('updateIKeyValue', { key, value }); - } - - if (me.clientData) { - ctx.dispatch('settings/merge', me.clientData); - } - }, - }, - - modules: { - device: { - namespaced: true, - - state: defaultDeviceSettings, - - mutations: { - set(state, x: { key: string; value: any }) { - state[x.key] = x.value; - }, - - setTl(state, x) { - state.tl = { - src: x.src, - arg: x.arg - }; - }, - - setVisibility(state, visibility) { - state.visibility = visibility; - }, - } - }, - - settings: { - namespaced: true, - - state: defaultSettings, - - mutations: { - set(state, x: { key: string; value: any }) { - nestedProperty.set(state, x.key, x.value); - }, - }, - - actions: { - merge(ctx, settings) { - if (settings == null) return; - for (const [key, value] of Object.entries(settings)) { - ctx.commit('set', { key, value }); - } - }, - - set(ctx, x) { - ctx.commit('set', x); - - if (ctx.rootGetters.isSignedIn) { - os.api('i/update-client-setting', { - name: x.key, - value: x.value - }); - } - }, - - updateHomeProfile(ctx) { - const profiles = ctx.state.homeProfiles; - ctx.commit('set', { - key: 'homeProfiles', - value: profiles - }); - os.api('i/update-client-setting', { - name: 'homeProfiles', - value: profiles - }); - }, - - updateMobileHomeProfile(ctx) { - const profiles = ctx.state.mobileHomeProfiles; - ctx.commit('set', { - key: 'mobileHomeProfiles', - value: profiles - }); - os.api('i/update-client-setting', { - name: 'mobileHomeProfiles', - value: profiles - }); - }, - - updateDeckProfile(ctx) { - const profiles = ctx.state.deckProfiles; - ctx.commit('set', { - key: 'deckProfiles', - value: profiles - }); - os.api('i/update-client-setting', { - name: 'deckProfiles', - value: profiles - }); - }, - } - } - } -}); diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts deleted file mode 100644 index b16fcdff4bfbedb5d65302fdc9664f6b328a8510..0000000000000000000000000000000000000000 --- a/src/client/app/theme.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as tinycolor from 'tinycolor2'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - vars: { [key: string]: string }; - props: { [key: string]: string }; -}; - -export const lightTheme: Theme = require('../themes/light.json5'); -export const darkTheme: Theme = require('../themes/dark.json5'); -export const lavenderTheme: Theme = require('../themes/lavender.json5'); -export const futureTheme: Theme = require('../themes/future.json5'); -export const halloweenTheme: Theme = require('../themes/halloween.json5'); -export const cafeTheme: Theme = require('../themes/cafe.json5'); -export const japaneseSushiSetTheme: Theme = require('../themes/japanese-sushi-set.json5'); -export const gruvboxDarkTheme: Theme = require('../themes/gruvbox-dark.json5'); -export const monokaiTheme: Theme = require('../themes/monokai.json5'); -export const vividTheme: Theme = require('../themes/vivid.json5'); -export const rainyTheme: Theme = require('../themes/rainy.json5'); -export const mauveTheme: Theme = require('../themes/mauve.json5'); -export const grayTheme: Theme = require('../themes/gray.json5'); -export const tweetDeckTheme: Theme = require('../themes/tweet-deck.json5'); - -export const builtinThemes = [ - lightTheme, - darkTheme, - lavenderTheme, - futureTheme, - halloweenTheme, - cafeTheme, - japaneseSushiSetTheme, - gruvboxDarkTheme, - monokaiTheme, - vividTheme, - rainyTheme, - mauveTheme, - grayTheme, - tweetDeckTheme, -]; - -export function applyTheme(theme: Theme, persisted = true) { - document.documentElement.classList.add('changing-theme'); - - setTimeout(() => { - document.documentElement.classList.remove('changing-theme'); - }, 1000); - - // Deep copy - const _theme = JSON.parse(JSON.stringify(theme)); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id == _theme.base); - _theme.vars = Object.assign({}, base.vars, _theme.vars); - _theme.props = Object.assign({}, base.props, _theme.props); - } - - const props = compile(_theme); - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - - if (persisted) { - localStorage.setItem('theme', JSON.stringify(props)); - } -} - -function compile(theme: Theme): { [key: string]: string } { - function getColor(code: string): tinycolor.Instance { - // ref - if (code[0] == '@') { - return getColor(theme.props[code.substr(1)]); - } - if (code[0] == '$') { - return getColor(theme.vars[code.substr(1)]); - } - - // func - if (code[0] == ':') { - const parts = code.split('<'); - const func = parts.shift().substr(1); - const arg = parseFloat(parts.shift()); - const color = getColor(parts.join('<')); - - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - } - } - - return tinycolor(code); - } - - const props = {}; - - for (const [k, v] of Object.entries(theme.props)) { - props[k] = genValue(getColor(v)); - } - - const primary = getColor(props['primary']); - - for (let i = 1; i < 10; i++) { - const color = primary.clone().setAlpha(i / 10); - props['primaryAlpha0' + i] = genValue(color); - } - - for (let i = 5; i < 100; i += 5) { - const color = primary.clone().lighten(i); - props['primaryLighten' + i] = genValue(color); - } - - for (let i = 5; i < 100; i += 5) { - const color = primary.clone().darken(i); - props['primaryDarken' + i] = genValue(color); - } - - return props; -} - -function genValue(c: tinycolor.Instance): string { - return c.toRgbString(); -} diff --git a/src/client/assets/error.jpg b/src/client/assets/error.jpg deleted file mode 100644 index 24d92f3803bad3c3e2518fc684e7104c2d0480dc..0000000000000000000000000000000000000000 Binary files a/src/client/assets/error.jpg and /dev/null differ diff --git a/src/client/assets/fedi.jpg b/src/client/assets/fedi.jpg deleted file mode 100644 index cbf3748eb84c67869a7d40ad3ceed79b32be2d41..0000000000000000000000000000000000000000 Binary files a/src/client/assets/fedi.jpg and /dev/null differ diff --git a/src/client/assets/flush.html b/src/client/assets/flush.html deleted file mode 100644 index 27725268f91861843e42902e2c270333eff39f70..0000000000000000000000000000000000000000 --- a/src/client/assets/flush.html +++ /dev/null @@ -1,16 +0,0 @@ -<!DOCTYPE html> - -<html> - <head> - <meta charset="utf-8"> - <title>Misskeyã®ãƒªã‚«ãƒãƒª</title> - <script> - const yn = location.search === '?force' || window.confirm('ã‚ャッシュをクリアã—ã¾ã™ã‹ï¼Ÿ\n\nDo you want to clear caches?'); - if (yn) { - localStorage.setItem('shouldFlush', 'true'); - } - - location.href = '/'; - </script> - </head> -</html> diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json index 895afbed3625999b688479a95e1483a5c4d7d3fb..f5a1d47a8aa85a810768adbc8279474df1393806 100644 --- a/src/client/assets/manifest.json +++ b/src/client/assets/manifest.json @@ -4,38 +4,13 @@ "start_url": "/", "display": "standalone", "background_color": "#313a42", - "theme_color": "#fb4e4e", + "theme_color": "#86b300", "icons": [ - { - "src": "/assets/icons/16.png", - "sizes": "16x16", - "type": "image/png" - }, - { - "src": "/assets/icons/32.png", - "sizes": "32x32", - "type": "image/png" - }, - { - "src": "/assets/icons/64.png", - "sizes": "64x64", - "type": "image/png" - }, - { - "src": "/assets/icons/128.png", - "sizes": "128x128", - "type": "image/png" - }, { "src": "/assets/icons/192.png", "sizes": "192x192", "type": "image/png" }, - { - "src": "/assets/icons/256.png", - "sizes": "256x256", - "type": "image/png" - }, { "src": "/assets/icons/512.png", "sizes": "512x512", diff --git a/src/client/assets/message.mp3 b/src/client/assets/message.mp3 deleted file mode 100644 index 64277444759d7d0be1c34574a2f13ca4a84a6e8b..0000000000000000000000000000000000000000 Binary files a/src/client/assets/message.mp3 and /dev/null differ diff --git a/src/client/assets/misskey-php-like-logo.png b/src/client/assets/misskey-php-like-logo.png deleted file mode 100644 index 882ef6708b0724d6190eeb967d666a75c0527489..0000000000000000000000000000000000000000 Binary files a/src/client/assets/misskey-php-like-logo.png and /dev/null differ diff --git a/src/client/assets/pointer.png b/src/client/assets/pointer.png deleted file mode 100644 index 255e0c8a4f12b4f30a3e735bc9a35f813008d31a..0000000000000000000000000000000000000000 Binary files a/src/client/assets/pointer.png and /dev/null differ diff --git a/src/client/assets/post.mp3 b/src/client/assets/post.mp3 deleted file mode 100644 index d3da88a933265ea66d24f31db01dbad15b92d932..0000000000000000000000000000000000000000 Binary files a/src/client/assets/post.mp3 and /dev/null differ diff --git a/src/client/assets/redoc.html b/src/client/assets/redoc.html deleted file mode 100644 index 9803464cb1d785b3152b85a7b44fa8361de99cee..0000000000000000000000000000000000000000 --- a/src/client/assets/redoc.html +++ /dev/null @@ -1,24 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <title>Misskey API</title> - <!-- needed for adaptive design --> - <meta charset="utf-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> - - <!-- - ReDoc doesn't change outer page styles - --> - <style> - body { - margin: 0; - padding: 0; - } - </style> - </head> - <body> - <redoc spec-url='/api.json'></redoc> - <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script> - </body> -</html> diff --git a/src/client/assets/reversi-put-me.mp3 b/src/client/assets/reversi-put-me.mp3 deleted file mode 100644 index 4e0e72091c6b5092c1ee9f3f352dcd276a343a47..0000000000000000000000000000000000000000 Binary files a/src/client/assets/reversi-put-me.mp3 and /dev/null differ diff --git a/src/client/assets/reversi-put-you.mp3 b/src/client/assets/reversi-put-you.mp3 deleted file mode 100644 index 9244189c2d5e5c0cf1064196e92b0843d5dacad1..0000000000000000000000000000000000000000 Binary files a/src/client/assets/reversi-put-you.mp3 and /dev/null differ diff --git a/src/client/assets/room/furnitures/bed/bed.blend b/src/client/assets/room/furnitures/bed/bed.blend deleted file mode 100644 index 731df76d0cd735d34f95bfaf05331ca2a6aa6909..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/bed/bed.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/bed/bed.glb b/src/client/assets/room/furnitures/bed/bed.glb deleted file mode 100644 index f35ecb9ef4125801c5f2ff036bc7463abfdabe70..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/bed/bed.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/bin/bin.blend b/src/client/assets/room/furnitures/bin/bin.blend deleted file mode 100644 index 8d459a0869ec309ac72a57a0f50f7e2f78b26965..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/bin/bin.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/bin/bin.glb b/src/client/assets/room/furnitures/bin/bin.glb deleted file mode 100644 index b45f203802511026d6c4760155b281d5307105c6..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/bin/bin.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/book/book.blend b/src/client/assets/room/furnitures/book/book.blend deleted file mode 100644 index 0d4899d4aec991e1f53e6f77d8c89d58159bf948..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book/book.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/book/book.glb b/src/client/assets/room/furnitures/book/book.glb deleted file mode 100644 index 546893da06b256e8a5e9710525961a01dc9439bb..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book/book.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/book2/barcode.png b/src/client/assets/room/furnitures/book2/barcode.png deleted file mode 100644 index 37cfe5add363f5324cf8ed15c4f06c35d2f030c4..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book2/barcode.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/book2/book2.blend b/src/client/assets/room/furnitures/book2/book2.blend deleted file mode 100644 index e0fdb4810197caad280ed2d1e715d9e254827a04..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book2/book2.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/book2/book2.glb b/src/client/assets/room/furnitures/book2/book2.glb deleted file mode 100644 index 2b26402f8ca80bf96517241bf1d0ba1c359605e2..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book2/book2.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/book2/texture.afdesign b/src/client/assets/room/furnitures/book2/texture.afdesign deleted file mode 100644 index b63771607a035da290e5e27f7d9b5824857440c2..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book2/texture.afdesign and /dev/null differ diff --git a/src/client/assets/room/furnitures/book2/texture.png b/src/client/assets/room/furnitures/book2/texture.png deleted file mode 100644 index 5aa84f0340b1e91f341761f605b50dda53dccaeb..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book2/texture.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/book2/uv.png b/src/client/assets/room/furnitures/book2/uv.png deleted file mode 100644 index 61c4fb040024b5647c98e667314af008fe127141..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/book2/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.blend b/src/client/assets/room/furnitures/cardboard-box/cardboard-box.blend deleted file mode 100644 index 3a528de32a376053a00fc5c9564ed9ebca1a011f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.glb b/src/client/assets/room/furnitures/cardboard-box/cardboard-box.glb deleted file mode 100644 index bed372e94f8b7bd18a272e8a38f5835cbc3a20ca..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend b/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend deleted file mode 100644 index 5f146267acf22370b4b7a6d60ace5217b3db91f7..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb b/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb deleted file mode 100644 index 85fcb5c0b60ebf15658dc49b687c7bda51843a5d..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box2/texture.png b/src/client/assets/room/furnitures/cardboard-box2/texture.png deleted file mode 100644 index e498d8f65b0d3816157eac2d91f22bace1cbf1e2..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box2/texture.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box2/uv.png b/src/client/assets/room/furnitures/cardboard-box2/uv.png deleted file mode 100644 index d547843ee015eff0edaf60ff86c5eebf2c7b5646..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box2/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend b/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend deleted file mode 100644 index 00681a3cfd5060e42378fd2e2fe6646074a6f862..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb b/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb deleted file mode 100644 index 1ef04276894f0c0f7b29533074d5716fb18d66a9..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box3/texture.png b/src/client/assets/room/furnitures/cardboard-box3/texture.png deleted file mode 100644 index 56c914cb9d8094a4ceacad5ff711f63f3df1b582..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box3/texture.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box3/texture.xcf b/src/client/assets/room/furnitures/cardboard-box3/texture.xcf deleted file mode 100644 index 7ffb3e3439d30c22841d77510c8d85815ced9475..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box3/texture.xcf and /dev/null differ diff --git a/src/client/assets/room/furnitures/cardboard-box3/uv.png b/src/client/assets/room/furnitures/cardboard-box3/uv.png deleted file mode 100644 index 797ac509dbacefd19ff3a9dbbcde418a9e42a2ca..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cardboard-box3/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend b/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend deleted file mode 100644 index 750343d4f00fc9af30c5158cfc0c517d73e2e02a..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb b/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb deleted file mode 100644 index 3066a69e353c4ab6668f6c010a804ac258e6f26a..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/chair/chair.blend b/src/client/assets/room/furnitures/chair/chair.blend deleted file mode 100644 index 79c29a840145a21373a2bb7a635b57745a57007f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/chair/chair.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/chair/chair.glb b/src/client/assets/room/furnitures/chair/chair.glb deleted file mode 100644 index 08ee1a0bb0fc569dad8b900fb8c0d5ab2cef4716..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/chair/chair.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/chair2/chair2.blend b/src/client/assets/room/furnitures/chair2/chair2.blend deleted file mode 100644 index c6a1acd96f5f5347536e31d99ee36d25e8e775d6..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/chair2/chair2.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/chair2/chair2.glb b/src/client/assets/room/furnitures/chair2/chair2.glb deleted file mode 100644 index 5ea2f3518b692efe6e5237aae464ba594c3f755c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/chair2/chair2.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/color-box/color-box.blend b/src/client/assets/room/furnitures/color-box/color-box.blend deleted file mode 100644 index f96a4ff76633b20fb292504702c0816e06859ce7..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/color-box/color-box.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/color-box/color-box.glb b/src/client/assets/room/furnitures/color-box/color-box.glb deleted file mode 100644 index 43f2abcae8ff2471236a5936844179edb4f154ec..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/color-box/color-box.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/corkboard/corkboard.blend b/src/client/assets/room/furnitures/corkboard/corkboard.blend deleted file mode 100644 index 9a7e1878cda339fc9f22151d9904bdddde7f892f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/corkboard/corkboard.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/corkboard/corkboard.glb b/src/client/assets/room/furnitures/corkboard/corkboard.glb deleted file mode 100644 index fee108fb91519ca8ec3f7dc9637db5dde02c6a6d..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/corkboard/corkboard.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/cube/cube.blend b/src/client/assets/room/furnitures/cube/cube.blend deleted file mode 100644 index 1af5bf40a9d3c63d4a8345bc34b36f4e36f8fdfe..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cube/cube.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/cube/cube.glb b/src/client/assets/room/furnitures/cube/cube.glb deleted file mode 100644 index 4ac8b6036d22ea22981582ee87e529277acab4c5..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cube/cube.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.blend b/src/client/assets/room/furnitures/cup-noodle/cup-noodle.blend deleted file mode 100644 index 37ca8868c7c7e9022d00ca7dbe5fb78f2b6bfd93..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.glb b/src/client/assets/room/furnitures/cup-noodle/cup-noodle.glb deleted file mode 100644 index 58efb1b3b42ee3188dc70e41307e2a88490a372c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/cup-noodle/noodle.png b/src/client/assets/room/furnitures/cup-noodle/noodle.png deleted file mode 100644 index 1d74e0bbe77d09732066802aff67ce4cf5d4963b..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/cup-noodle/noodle.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/desk/desk.blend b/src/client/assets/room/furnitures/desk/desk.blend deleted file mode 100644 index c88d01f0b233ec40c4475b2555d21386d9037691..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/desk/desk.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/desk/desk.glb b/src/client/assets/room/furnitures/desk/desk.glb deleted file mode 100644 index 4a58513095884e46661b178bc50b41140eeb3a47..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/desk/desk.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/energy-drink/energy-drink.blend b/src/client/assets/room/furnitures/energy-drink/energy-drink.blend deleted file mode 100644 index 65fc41273e60c3ce5ecb60838b596fb73c6f50fa..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/energy-drink/energy-drink.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/energy-drink/energy-drink.glb b/src/client/assets/room/furnitures/energy-drink/energy-drink.glb deleted file mode 100644 index 7fb1c278368ad14c7f18139524b172090e8aca07..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/energy-drink/energy-drink.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/energy-drink/texture.afdesign b/src/client/assets/room/furnitures/energy-drink/texture.afdesign deleted file mode 100644 index 8c117a49b18882c3ff5b87fd6a2299a097d3d441..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/energy-drink/texture.afdesign and /dev/null differ diff --git a/src/client/assets/room/furnitures/energy-drink/texture.png b/src/client/assets/room/furnitures/energy-drink/texture.png deleted file mode 100644 index 484ca0f96ffd29bbfda5a16039a924dfd0e22901..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/energy-drink/texture.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/energy-drink/uv.png b/src/client/assets/room/furnitures/energy-drink/uv.png deleted file mode 100644 index 2a3f20c999a041dd392611fe5fb921adac8be4c5..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/energy-drink/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/eraser/cover.png b/src/client/assets/room/furnitures/eraser/cover.png deleted file mode 100644 index 932a3fc62eb19dbafe95f91369249a7b232501da..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/eraser/cover.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/eraser/cover.psd b/src/client/assets/room/furnitures/eraser/cover.psd deleted file mode 100644 index c393337833d4d76f28a4f2de33ace2ce863f3b1c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/eraser/cover.psd and /dev/null differ diff --git a/src/client/assets/room/furnitures/eraser/eraser-uv.png b/src/client/assets/room/furnitures/eraser/eraser-uv.png deleted file mode 100644 index 89e4ea4c4508c20514f84977b3320f53c171e71f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/eraser/eraser-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/eraser/eraser.blend b/src/client/assets/room/furnitures/eraser/eraser.blend deleted file mode 100644 index 103c54fbae3f04db34ad3ad7cd36102890a393fb..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/eraser/eraser.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/eraser/eraser.glb b/src/client/assets/room/furnitures/eraser/eraser.glb deleted file mode 100644 index 016b60df202459182cb66c6f12b13222f8a88ae8..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/eraser/eraser.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png b/src/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png deleted file mode 100644 index e3865ad15e6798f43ec910b34aee19ccbe3f8d6e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.blend b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.blend deleted file mode 100644 index d59f87c1ee5482a6f375585fba575a60a4d454a8..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.glb b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.glb deleted file mode 100644 index 48b36ef34729727ff186714aaf6f08f473115eaa..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.png b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.png deleted file mode 100644 index 7cee4b1859e5ed39b7abb861a05ca05d0dc16c7e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.psd b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.psd deleted file mode 100644 index cd59fc007bb0d4c2472a99ec2326b8f8284cae87..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.psd and /dev/null differ diff --git a/src/client/assets/room/furnitures/fan/fan.blend b/src/client/assets/room/furnitures/fan/fan.blend deleted file mode 100644 index 8c8106e5fe61f92fce0e3e3f359bba90474a5576..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/fan/fan.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/fan/fan.glb b/src/client/assets/room/furnitures/fan/fan.glb deleted file mode 100644 index d9367f353407b2a9f66841ef2ac373e322ca64b5..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/fan/fan.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/holo-display/holo-display.blend b/src/client/assets/room/furnitures/holo-display/holo-display.blend deleted file mode 100644 index 56d2e1f8191d1970cb262127f917642ef3a3dc57..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/holo-display/holo-display.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/holo-display/holo-display.glb b/src/client/assets/room/furnitures/holo-display/holo-display.glb deleted file mode 100644 index 4d042a59b31f7fc04d6b6359af72f297903acfc0..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/holo-display/holo-display.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/holo-display/ray-uv.png b/src/client/assets/room/furnitures/holo-display/ray-uv.png deleted file mode 100644 index aa7e817e0f7840f2bbda96c031182ade732026f2..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/holo-display/ray-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/holo-display/ray.png b/src/client/assets/room/furnitures/holo-display/ray.png deleted file mode 100644 index 6a5d24e143bfc5ecde69ba0db283b20a5cdfe29c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/holo-display/ray.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/keyboard/keyboard.blend b/src/client/assets/room/furnitures/keyboard/keyboard.blend deleted file mode 100644 index ab33d134b3d99491c34b8557628c944948d01ce9..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/keyboard/keyboard.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/keyboard/keyboard.glb b/src/client/assets/room/furnitures/keyboard/keyboard.glb deleted file mode 100644 index 15dc69f47a4d5390dd2f5e83045cf1577ae98a75..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/keyboard/keyboard.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/low-table/low-table.blend b/src/client/assets/room/furnitures/low-table/low-table.blend deleted file mode 100644 index e1592174d9963b9996cdf1969ecc96885c31d2a5..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/low-table/low-table.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/low-table/low-table.glb b/src/client/assets/room/furnitures/low-table/low-table.glb deleted file mode 100644 index c69bf35d7b52eda2434c4a3db91a3c5f695e4adf..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/low-table/low-table.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/mat/mat.blend b/src/client/assets/room/furnitures/mat/mat.blend deleted file mode 100644 index a1e1a68c554e93b9b63f54801310d6f34a83d161..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/mat/mat.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/mat/mat.glb b/src/client/assets/room/furnitures/mat/mat.glb deleted file mode 100644 index 87ccd44e1a2fcb07c7ea55c7561bff356c5f6d70..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/mat/mat.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/milk/milk-uv.png b/src/client/assets/room/furnitures/milk/milk-uv.png deleted file mode 100644 index 258fd546380d4ba7f7f5d3611e792b0192101342..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/milk/milk-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/milk/milk.blend b/src/client/assets/room/furnitures/milk/milk.blend deleted file mode 100644 index 2df508d5b905dbc1ee0df0f5b755c5949dc66681..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/milk/milk.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/milk/milk.glb b/src/client/assets/room/furnitures/milk/milk.glb deleted file mode 100644 index b335fe3d021b227bcec7f82d13133442f1fe84cf..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/milk/milk.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/milk/milk.png b/src/client/assets/room/furnitures/milk/milk.png deleted file mode 100644 index 35181c8c8c2b3ccd614bdc4570475f3c32f2d28f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/milk/milk.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/milk/milk.psd b/src/client/assets/room/furnitures/milk/milk.psd deleted file mode 100644 index f31e439277821d8ad26e4a6f68c00d80cac2f8ba..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/milk/milk.psd and /dev/null differ diff --git a/src/client/assets/room/furnitures/monitor/monitor.blend b/src/client/assets/room/furnitures/monitor/monitor.blend deleted file mode 100644 index 6c042ccdd87628352089ef4a698546bc55b96b86..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/monitor/monitor.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/monitor/monitor.glb b/src/client/assets/room/furnitures/monitor/monitor.glb deleted file mode 100644 index fc33286a15226afe45cdd17bfcf90dcb668c7b65..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/monitor/monitor.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/monitor/monitor.psd b/src/client/assets/room/furnitures/monitor/monitor.psd deleted file mode 100644 index 57afff9cd9ef9717b57fee8df33bc5bd4814da1e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/monitor/monitor.psd and /dev/null differ diff --git a/src/client/assets/room/furnitures/monitor/screen-uv.png b/src/client/assets/room/furnitures/monitor/screen-uv.png deleted file mode 100644 index 35f74de8aa6a11d80b7710c20761ab0f81048510..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/monitor/screen-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/monitor/screen.jpg b/src/client/assets/room/furnitures/monitor/screen.jpg deleted file mode 100644 index 4004a1ede9979af67367bab49c48a5499c24bb77..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/monitor/screen.jpg and /dev/null differ diff --git a/src/client/assets/room/furnitures/moon/moon.blend b/src/client/assets/room/furnitures/moon/moon.blend deleted file mode 100644 index 4ff3deab8ec8c6cdc7ade407b72a8fe5ba28c782..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/moon/moon.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/moon/moon.glb b/src/client/assets/room/furnitures/moon/moon.glb deleted file mode 100644 index 07fa7e4c02a9b1902ca137b08e10266c040bfa48..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/moon/moon.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/moon/moon.jpg b/src/client/assets/room/furnitures/moon/moon.jpg deleted file mode 100644 index 8988ac64b9b5d1c993d6007864ce83c3d58a4d97..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/moon/moon.jpg and /dev/null differ diff --git a/src/client/assets/room/furnitures/mousepad/mousepad.blend b/src/client/assets/room/furnitures/mousepad/mousepad.blend deleted file mode 100644 index 14bd139c94a03e0b26685de5431e6fe9893d87ed..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/mousepad/mousepad.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/mousepad/mousepad.glb b/src/client/assets/room/furnitures/mousepad/mousepad.glb deleted file mode 100644 index 681ada49cd93e18d9b01f111c1de8a0fd6e7b460..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/mousepad/mousepad.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/pc/motherboard-uv.png b/src/client/assets/room/furnitures/pc/motherboard-uv.png deleted file mode 100644 index 355009fe7c75491c71de252a7cb649da77d75581..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pc/motherboard-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/pc/motherboard-uv.psd b/src/client/assets/room/furnitures/pc/motherboard-uv.psd deleted file mode 100644 index 971f33f79eadfdda3e206d09a23aa17d04ac16fa..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pc/motherboard-uv.psd and /dev/null differ diff --git a/src/client/assets/room/furnitures/pc/motherboard.jpg b/src/client/assets/room/furnitures/pc/motherboard.jpg deleted file mode 100644 index d894e4efcf13695b3d308c5481939a96ec023371..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pc/motherboard.jpg and /dev/null differ diff --git a/src/client/assets/room/furnitures/pc/pc.blend b/src/client/assets/room/furnitures/pc/pc.blend deleted file mode 100644 index 13dfec6ccc27bdd13bfdcb5d9eec42fcaf75b7b0..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pc/pc.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/pc/pc.glb b/src/client/assets/room/furnitures/pc/pc.glb deleted file mode 100644 index 44a48b18ae6031bf312552636e50c9014391e667..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pc/pc.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/pencil/pencil.blend b/src/client/assets/room/furnitures/pencil/pencil.blend deleted file mode 100644 index 0fc6bdd77699df7874b90b5694ff6e17c0d71a3c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pencil/pencil.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/pencil/pencil.glb b/src/client/assets/room/furnitures/pencil/pencil.glb deleted file mode 100644 index a938b5cdcc759f46990adfa9e2656587e7070400..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pencil/pencil.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/photoframe/photo-uv.png b/src/client/assets/room/furnitures/photoframe/photo-uv.png deleted file mode 100644 index 9b94906413c704cf62acd86eda0afef773e932e6..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/photoframe/photo-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/photoframe/photo.jpg b/src/client/assets/room/furnitures/photoframe/photo.jpg deleted file mode 100644 index af14f0f36a03e15c1e1f12cfb0d69e319378f625..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/photoframe/photo.jpg and /dev/null differ diff --git a/src/client/assets/room/furnitures/photoframe/photoframe.blend b/src/client/assets/room/furnitures/photoframe/photoframe.blend deleted file mode 100644 index 4224cde45b42b17acd08e3a98eb4d990d543e27f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/photoframe/photoframe.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/photoframe/photoframe.glb b/src/client/assets/room/furnitures/photoframe/photoframe.glb deleted file mode 100644 index 4255a77de6ccaacec292f79c0dec650bb22137c7..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/photoframe/photoframe.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/piano/piano.blend b/src/client/assets/room/furnitures/piano/piano.blend deleted file mode 100644 index 7653cdf672f6599d66e43aa71a1cf657c54e7f6f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/piano/piano.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/piano/piano.glb b/src/client/assets/room/furnitures/piano/piano.glb deleted file mode 100644 index 7242e78ceb4a382243d37cc5c326a26684033461..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/piano/piano.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/pinguin/pinguin.blend b/src/client/assets/room/furnitures/pinguin/pinguin.blend deleted file mode 100644 index 514c713e4cd6cdddcef2d2b775712565a950f931..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pinguin/pinguin.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/pinguin/pinguin.glb b/src/client/assets/room/furnitures/pinguin/pinguin.glb deleted file mode 100644 index 6df34c06e9bb2be58974941b90119e6c13ebb071..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pinguin/pinguin.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant/plant-soil-uv.png b/src/client/assets/room/furnitures/plant/plant-soil-uv.png deleted file mode 100644 index d4971a896cd210372a2532bf73d92a4c3dadf089..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant/plant-soil-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant/plant-soil.png b/src/client/assets/room/furnitures/plant/plant-soil.png deleted file mode 100644 index e79ccd240e5fb4710c29aaceafffb8d3bc150170..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant/plant-soil.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant/plant-soil.psd b/src/client/assets/room/furnitures/plant/plant-soil.psd deleted file mode 100644 index 1457b7ea5b3086877beb83ab3bb822648c8a8d4f..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant/plant-soil.psd and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant/plant.blend b/src/client/assets/room/furnitures/plant/plant.blend deleted file mode 100644 index aa38c7b54e4e6656a713b97964a1de0b8ea36b48..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant/plant.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant/plant.glb b/src/client/assets/room/furnitures/plant/plant.glb deleted file mode 100644 index 38422b4a9b2940de45816684ae45c77599382995..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant/plant.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant2/plant2.blend b/src/client/assets/room/furnitures/plant2/plant2.blend deleted file mode 100644 index 6592c5d98d91007550d41cdfd27e3da08f17bb3e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant2/plant2.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant2/plant2.glb b/src/client/assets/room/furnitures/plant2/plant2.glb deleted file mode 100644 index 223e6f58340f9a9ba76c8aac4777ca3ccfb51a0c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant2/plant2.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/plant2/soil.png b/src/client/assets/room/furnitures/plant2/soil.png deleted file mode 100644 index e79ccd240e5fb4710c29aaceafffb8d3bc150170..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/plant2/soil.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/poster-h/poster-h.blend b/src/client/assets/room/furnitures/poster-h/poster-h.blend deleted file mode 100644 index 40f944f3c115f9d3aaf966f6b7f06e436c85d9e4..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/poster-h/poster-h.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/poster-h/poster-h.glb b/src/client/assets/room/furnitures/poster-h/poster-h.glb deleted file mode 100644 index c6032c100979c713e6d6a4a15f30f7dcec160e6d..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/poster-h/poster-h.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/poster-h/uv.png b/src/client/assets/room/furnitures/poster-h/uv.png deleted file mode 100644 index f854231e0bcea8d17145035c1859ba980fe5e978..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/poster-h/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/poster-v/poster-v.blend b/src/client/assets/room/furnitures/poster-v/poster-v.blend deleted file mode 100644 index 07fe971634f2f9df6ab8802f670ebd09c41adf65..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/poster-v/poster-v.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/poster-v/poster-v.glb b/src/client/assets/room/furnitures/poster-v/poster-v.glb deleted file mode 100644 index 6e3782f19364233453bfee6d4af8451614af6de5..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/poster-v/poster-v.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/poster-v/uv.png b/src/client/assets/room/furnitures/poster-v/uv.png deleted file mode 100644 index 7bb2bf809e000338978284c7e25a56a6cbb4e15b..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/poster-v/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/pudding/pudding.blend b/src/client/assets/room/furnitures/pudding/pudding.blend deleted file mode 100644 index bba40ce161d37b08e974cbc875ee5193ffbfe862..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pudding/pudding.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/pudding/pudding.glb b/src/client/assets/room/furnitures/pudding/pudding.glb deleted file mode 100644 index 06c9ed80cc1507fc8562d12b7b3cf849897580fa..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/pudding/pudding.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.blend b/src/client/assets/room/furnitures/rubik-cube/rubik-cube.blend deleted file mode 100644 index 6c09067e78bfce812853e715ed6b467fd87ab2d4..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.glb b/src/client/assets/room/furnitures/rubik-cube/rubik-cube.glb deleted file mode 100644 index d640df9b06e50757d021866fb6eeda54ad272296..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/server/rack-uv.png b/src/client/assets/room/furnitures/server/rack-uv.png deleted file mode 100644 index 65bdb0ffd91b3b4dccbc0e83f1641c42c99afe59..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/server/rack-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/server/rack.png b/src/client/assets/room/furnitures/server/rack.png deleted file mode 100644 index b851295cfa1c5748c4b42b13ee9296c358be3544..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/server/rack.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/server/server.blend b/src/client/assets/room/furnitures/server/server.blend deleted file mode 100644 index 6675dfbdc2f020c98cb755bd1989de2a753b107a..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/server/server.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/server/server.glb b/src/client/assets/room/furnitures/server/server.glb deleted file mode 100644 index a8b530a2d22ee5cce3e76ac644c55f64e7e2f041..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/server/server.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/server/server.png b/src/client/assets/room/furnitures/server/server.png deleted file mode 100644 index 8e9a0d716c928c335b09511e92ab308e000ab7f6..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/server/server.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/server/uv.png b/src/client/assets/room/furnitures/server/uv.png deleted file mode 100644 index ca2e747d16227f000de4266c0e04c3888ba982ce..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/server/uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/sofa/sofa.blend b/src/client/assets/room/furnitures/sofa/sofa.blend deleted file mode 100644 index fb5aa51a2c05e9bed15a29e3e556775d75163d6e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/sofa/sofa.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/sofa/sofa.glb b/src/client/assets/room/furnitures/sofa/sofa.glb deleted file mode 100644 index 6ce77d94ac3aff8c008ac139367827cf2c93c362..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/sofa/sofa.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/spiral/spiral.blend b/src/client/assets/room/furnitures/spiral/spiral.blend deleted file mode 100644 index 9d3be77bce7e2f915a4840881ea0aaa302d3910c..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/spiral/spiral.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/spiral/spiral.glb b/src/client/assets/room/furnitures/spiral/spiral.glb deleted file mode 100644 index ee8e3c23b1558750f1fa56c8a9fdeb4cabf476d9..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/spiral/spiral.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/tv/screen-uv.png b/src/client/assets/room/furnitures/tv/screen-uv.png deleted file mode 100644 index 4bb74f031f857cc5a2f0fd0cbbf082c6f45453bb..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/tv/screen-uv.png and /dev/null differ diff --git a/src/client/assets/room/furnitures/tv/tv.blend b/src/client/assets/room/furnitures/tv/tv.blend deleted file mode 100644 index 490e298e7b24dd1928fd4d39d7f54b7fdea63250..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/tv/tv.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/tv/tv.glb b/src/client/assets/room/furnitures/tv/tv.glb deleted file mode 100644 index b9bd23896b394fd59991885296acea68b6b37dcd..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/tv/tv.glb and /dev/null differ diff --git a/src/client/assets/room/furnitures/wall-clock/wall-clock.blend b/src/client/assets/room/furnitures/wall-clock/wall-clock.blend deleted file mode 100644 index 0a61c8f01e3eab209e3a2ed73fbfc13feaaa84b7..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/wall-clock/wall-clock.blend and /dev/null differ diff --git a/src/client/assets/room/furnitures/wall-clock/wall-clock.glb b/src/client/assets/room/furnitures/wall-clock/wall-clock.glb deleted file mode 100644 index b9f0093a8dc470e2783a9a53b97a021e330ee037..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/furnitures/wall-clock/wall-clock.glb and /dev/null differ diff --git a/src/client/assets/room/rooms/default/default.blend b/src/client/assets/room/rooms/default/default.blend deleted file mode 100644 index 661154724aafc4b46db0d6100e0be6a9458335a7..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/default/default.blend and /dev/null differ diff --git a/src/client/assets/room/rooms/default/default.glb b/src/client/assets/room/rooms/default/default.glb deleted file mode 100644 index 3d378deee2a9c63add57a19753481a18e409c81a..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/default/default.glb and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/husuma-uv.png b/src/client/assets/room/rooms/washitsu/husuma-uv.png deleted file mode 100644 index ae2fca3911bdd50d5d80f21a8e69e269b4af92a9..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/husuma-uv.png and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/husuma.png b/src/client/assets/room/rooms/washitsu/husuma.png deleted file mode 100644 index 084cbed67c89845583ab8176a325aa01a86fa4e7..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/husuma.png and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/tatami-single1600.png b/src/client/assets/room/rooms/washitsu/tatami-single1600.png deleted file mode 100644 index c0e684d743d06352b35472a2bfce21e4a385947a..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/tatami-single1600.png and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/tatami-uv.png b/src/client/assets/room/rooms/washitsu/tatami-uv.png deleted file mode 100644 index 5b16c66091db13632800d0eea7798c93162ae48e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/tatami-uv.png and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/tatami.afdesign b/src/client/assets/room/rooms/washitsu/tatami.afdesign deleted file mode 100644 index 9300a26950a1ab857fe71d41893c8eb2b5429ba1..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/tatami.afdesign and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/tatami.png b/src/client/assets/room/rooms/washitsu/tatami.png deleted file mode 100644 index 8894d040ae35bb522c503ea771943e00d8194613..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/tatami.png and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/washitsu.blend b/src/client/assets/room/rooms/washitsu/washitsu.blend deleted file mode 100644 index 84dc11374d63314bc95b4556ad0c588dc420cb98..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/washitsu.blend and /dev/null differ diff --git a/src/client/assets/room/rooms/washitsu/washitsu.glb b/src/client/assets/room/rooms/washitsu/washitsu.glb deleted file mode 100644 index 5b4767bc73d5c27e677c6b3832e7900ecbeb7604..0000000000000000000000000000000000000000 Binary files a/src/client/assets/room/rooms/washitsu/washitsu.glb and /dev/null differ diff --git a/src/client/assets/thumbnail-not-available.png b/src/client/assets/thumbnail-not-available.png deleted file mode 100644 index 07cad9919c5a1c6a9398799fc75c989c602386ff..0000000000000000000000000000000000000000 Binary files a/src/client/assets/thumbnail-not-available.png and /dev/null differ diff --git a/src/client/assets/title.svg b/src/client/assets/title.svg deleted file mode 100644 index 0e4e0b8b3bff4a0574813e5b527f055280a84fa0..0000000000000000000000000000000000000000 Binary files a/src/client/assets/title.svg and /dev/null differ diff --git a/src/client/assets/unread.svg b/src/client/assets/unread.svg deleted file mode 100644 index 8c3cc9f4758c0be0f6f5a12c2929cfa2204acbf8..0000000000000000000000000000000000000000 Binary files a/src/client/assets/unread.svg and /dev/null differ diff --git a/src/client/assets/version.html b/src/client/assets/version.html deleted file mode 100644 index 177d37db8f18d1615a254cc6ac9b09328bc1e6db..0000000000000000000000000000000000000000 --- a/src/client/assets/version.html +++ /dev/null @@ -1,18 +0,0 @@ -<!DOCTYPE html> - -<html> - <head> - <meta charset="utf-8"> - <title>Misskeyã®ãƒªã‚«ãƒãƒª</title> - <script> - const v = window.prompt('Enter version:'); - if (v) { - localStorage.setItem('v', v); - } - - setTimeout(() => { - location.href = '/'; - }, 500); - </script> - </head> -</html> diff --git a/src/client/assets/welcome-bg.dark.svg b/src/client/assets/welcome-bg.dark.svg deleted file mode 100644 index 1866170327c30dbd279bec26779b2962d595c87e..0000000000000000000000000000000000000000 Binary files a/src/client/assets/welcome-bg.dark.svg and /dev/null differ diff --git a/src/client/assets/welcome-bg.light.svg b/src/client/assets/welcome-bg.light.svg deleted file mode 100644 index ebccb648ea53e2bf49199a19e7278f4782d6ccaa..0000000000000000000000000000000000000000 Binary files a/src/client/assets/welcome-bg.light.svg and /dev/null differ diff --git a/src/client/components/acct.vue b/src/client/components/acct.vue new file mode 100644 index 0000000000000000000000000000000000000000..250e8b2371e088ea3a19a8673bbf183c0160e980 --- /dev/null +++ b/src/client/components/acct.vue @@ -0,0 +1,29 @@ +<template> +<span class="mk-acct" v-once> + <span class="name">@{{ user.username }}</span> + <span class="host" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span> +</span> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { toUnicode } from 'punycode'; +import { host } from '../config'; + +export default Vue.extend({ + props: ['user', 'detail'], + data() { + return { + host: toUnicode(host), + }; + } +}); +</script> + +<style lang="scss" scoped> +.mk-acct { + > .host { + opacity: 0.5; + } +} +</style> diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/components/autocomplete.vue similarity index 78% rename from src/client/app/common/views/components/autocomplete.vue rename to src/client/components/autocomplete.vue index bbfb7896ae49701391d4859b1811a672bb6051c5..232b25dd61bcf1b543227316d4bcfad5b7d44de2 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/components/autocomplete.vue @@ -28,10 +28,10 @@ <script lang="ts"> import Vue from 'vue'; -import { emojilist } from '../../../../../misc/emojilist'; -import contains from '../../../common/scripts/contains'; -import { twemojiSvgBase } from '../../../../../misc/twemoji-base'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; +import { emojilist } from '../../misc/emojilist'; +import contains from '../scripts/contains'; +import { twemojiSvgBase } from '../../misc/twemoji-base'; +import { getStaticImageUrl } from '../scripts/get-static-image-url'; type EmojiDef = { emoji: string; @@ -73,42 +73,7 @@ for (const x of lib) { emjdb.sort((a, b) => a.name.length - b.name.length); export default Vue.extend({ - props: { - type: { - type: String, - required: true, - }, - - q: { - type: String, - required: true, - }, - - textarea: { - type: Object, - required: true, - }, - - complete: { - type: Function, - required: true, - }, - - close: { - type: Function, - required: true, - }, - - x: { - type: Number, - required: true, - }, - - y: { - type: Number, - required: true, - }, - }, + props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'], data() { return { @@ -184,7 +149,7 @@ export default Vue.extend({ this.textarea.addEventListener('keydown', this.onKeydown); - for (const el of Array.from(document.querySelectorAll('body *'))) { + for (const el of Array.from(document.querySelectorAll('*'))) { el.addEventListener('mousedown', this.onMousedown); } @@ -202,7 +167,7 @@ export default Vue.extend({ beforeDestroy() { this.textarea.removeEventListener('keydown', this.onKeydown); - for (const el of Array.from(document.querySelectorAll('body *'))) { + for (const el of Array.from(document.querySelectorAll('*'))) { el.removeEventListener('mousedown', this.onMousedown); } }, @@ -363,96 +328,116 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-autocomplete - position fixed - z-index 65535 - max-width 100% - margin-top calc(1em + 8px) - overflow hidden - background var(--faceHeader) - border solid 1px rgba(#000, 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 flex - align-items center - padding 4px 12px - white-space nowrap - overflow hidden - font-size 0.9em - color rgba(#000, 0.8) - cursor default - - &, * - user-select none - - * - overflow hidden - text-overflow ellipsis - - &:hover - background var(--autocompleteItemHoverBg) - - &[data-selected='true'] - background var(--primary) - - &, * - color #fff !important - - &:active - background var(--primaryDarken10) - - &, * - color #fff !important - - > .users > li - - .avatar - 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 var(--autocompleteItemText) - - .username - color var(--autocompleteItemTextSub) - - > .hashtags > li - - .name - color var(--autocompleteItemText) - - > .emojis > li - - .emoji - display inline-block - margin 0 4px 0 0 - width 24px - - > img - width 24px - vertical-align bottom - - .name - color var(--autocompleteItemText) - - .alias - margin 0 0 0 8px - color var(--autocompleteItemTextSub) +<style lang="scss" scoped> +.mk-autocomplete { + position: fixed; + z-index: 65535; + max-width: 100%; + margin-top: calc(1em + 8px); + overflow: hidden; + background: var(--panel); + border: solid 1px rgba(#000, 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: flex; + align-items: center; + padding: 4px 12px; + white-space: nowrap; + overflow: hidden; + font-size: 0.9em; + cursor: default; + + &, * { + user-select: none; + } + + * { + overflow: hidden; + text-overflow: ellipsis; + } + + &:hover { + background: var(--yrnqrguo); + } + + &[data-selected='true'] { + background: var(--accent); + + &, * { + color: #fff !important; + } + } + + &:active { + background: var(--accentDarken); + + &, * { + color: #fff !important; + } + } + } + } + + > .users > li { + + .avatar { + 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: var(--autocompleteItemText); + } + + .username { + color: var(--autocompleteItemTextSub); + } + } + + > .hashtags > li { + + .name { + color: var(--autocompleteItemText); + } + } + + > .emojis > li { + + .emoji { + display: inline-block; + margin: 0 4px 0 0; + width: 24px; + + > img { + width: 24px; + vertical-align: bottom; + } + } + + .name { + color: var(--autocompleteItemText); + } + + .alias { + margin: 0 0 0 8px; + color: var(--autocompleteItemTextSub); + } + } +} </style> diff --git a/src/client/components/avatar.vue b/src/client/components/avatar.vue new file mode 100644 index 0000000000000000000000000000000000000000..12cbb82478b3cfb0b5eb2ac7927d52d43486fdbc --- /dev/null +++ b/src/client/components/avatar.vue @@ -0,0 +1,116 @@ +<template> +<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> + <span class="inner" :style="icon"></span> +</span> +<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> + <span class="inner" :style="icon"></span> +</span> +<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"> + <span class="inner" :style="icon"></span> +</router-link> +<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview"> + <span class="inner" :style="icon"></span> +</router-link> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { getStaticImageUrl } from '../scripts/get-static-image-url'; + +export default Vue.extend({ + props: { + user: { + type: Object, + required: true + }, + target: { + required: false, + default: null + }, + disableLink: { + required: false, + default: false + }, + disablePreview: { + required: false, + default: false + } + }, + computed: { + cat(): boolean { + return this.user.isCat; + }, + url(): string { + return this.$store.state.device.disableShowingAnimatedImages + ? getStaticImageUrl(this.user.avatarUrl) + : this.user.avatarUrl; + }, + icon(): any { + return { + backgroundColor: this.user.avatarColor, + backgroundImage: `url(${this.url})`, + }; + } + }, + watch: { + 'user.avatarColor'() { + this.$el.style.color = this.user.avatarColor; + } + }, + mounted() { + if (this.user.avatarColor) { + this.$el.style.color = this.user.avatarColor; + } + }, + methods: { + onClick(e) { + this.$emit('click', e); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-avatar { + position: relative; + display: inline-block; + vertical-align: bottom; + flex-shrink: 0; + border-radius: 100%; + line-height: 16px; + + &.cat { + &:before, &:after { + background: #df548f; + border: solid 4px currentColor; + box-sizing: border-box; + content: ''; + display: inline-block; + height: 50%; + width: 50%; + } + + &:before { + border-radius: 0 75% 75%; + transform: rotate(37.5deg) skew(30deg); + } + + &:after { + border-radius: 75% 0 75% 75%; + transform: rotate(-37.5deg) skew(-30deg); + } + } + + .inner { + background-position: center center; + background-size: cover; + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + border-radius: 100%; + z-index: 1; + } +} +</style> diff --git a/src/client/app/common/views/components/avatars.vue b/src/client/components/avatars.vue similarity index 100% rename from src/client/app/common/views/components/avatars.vue rename to src/client/components/avatars.vue diff --git a/src/client/app/common/views/components/code-core.vue b/src/client/components/code-core.vue similarity index 99% rename from src/client/app/common/views/components/code-core.vue rename to src/client/components/code-core.vue index 219ed1d80a40cab95e45c4112cb64a6c2835fa95..a9253528d9613c34cfa6277a6a4114a0a79cca2b 100644 --- a/src/client/app/common/views/components/code-core.vue +++ b/src/client/components/code-core.vue @@ -7,7 +7,6 @@ import Vue from 'vue'; import 'prismjs'; import 'prismjs/themes/prism-okaidia.css'; import XPrism from 'vue-prism-component'; - export default Vue.extend({ components: { XPrism @@ -26,7 +25,6 @@ export default Vue.extend({ required: false } }, - computed: { prismLang() { return Prism.languages[this.lang] ? this.lang : 'js'; diff --git a/src/client/app/common/views/components/code.vue b/src/client/components/code.vue similarity index 99% rename from src/client/app/common/views/components/code.vue rename to src/client/components/code.vue index d52c9f7bc2da80e41eb7eae18cf771e410ffc690..94cad57be4d00fd3955e4b4924a9adeccbd42898 100644 --- a/src/client/app/common/views/components/code.vue +++ b/src/client/components/code.vue @@ -4,12 +4,10 @@ <script lang="ts"> import Vue from 'vue'; - export default Vue.extend({ components: { XCode: () => import('./code-core.vue').then(m => m.default) }, - props: { code: { type: String, diff --git a/src/client/components/cw-button.vue b/src/client/components/cw-button.vue new file mode 100644 index 0000000000000000000000000000000000000000..4516e5210c964c149fdd33af8709559beb9b0d0f --- /dev/null +++ b/src/client/components/cw-button.vue @@ -0,0 +1,73 @@ +<template> +<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh _button" @click="toggle"> + <b>{{ value ? this.$t('_cw.hide') : this.$t('_cw.show') }}</b> + <span v-if="!value">{{ this.label }}</span> +</button> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import { length } from 'stringz'; +import { concat } from '../../prelude/array'; + +export default Vue.extend({ + i18n, + + props: { + value: { + type: Boolean, + required: true + }, + note: { + type: Object, + required: true + } + }, + + computed: { + label(): string { + return concat([ + this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [], + this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [], + this.note.poll != null ? [this.$t('_cw.poll')] : [] + ] as string[][]).join(' / '); + } + }, + + methods: { + length, + + toggle() { + this.$emit('input', !this.value); + } + } +}); +</script> + +<style lang="scss" scoped> +.nrvgflfuaxwgkxoynpnumyookecqrrvh { + display: inline-block; + padding: 4px 8px; + font-size: 0.7em; + color: var(--cwFg); + background: var(--cwBg); + border-radius: 2px; + + &:hover { + background: var(--cwHoverBg); + } + + > span { + margin-left: 4px; + + &:before { + content: '('; + } + + &:after { + content: ')'; + } + } +} +</style> diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue new file mode 100644 index 0000000000000000000000000000000000000000..00c3cd6643cdaba3ebe15a7f3bcded6f077d62b4 --- /dev/null +++ b/src/client/components/date-separated-list.vue @@ -0,0 +1,94 @@ +<template> +<sequential-entrance class="sqadhkmv" ref="list" :direction="direction"> + <template v-for="(item, i) in items"> + <slot :item="item" :i="i"></slot> + <div class="separator" :key="item.id + '_date'" :data-index="i" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()"> + <p class="date"> + <span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span> + <span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span> + </p> + </div> + </template> +</sequential-entrance> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + + props: { + items: { + type: Array, + required: true, + }, + direction: { + type: String, + required: false + } + }, + + data() { + return { + faAngleUp, faAngleDown + }; + }, + + methods: { + getDateText(time: string) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return this.$t('monthAndDay', { + month: month.toString(), + day: date.toString() + }); + }, + + focus() { + this.$refs.list.focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.sqadhkmv { + > .separator { + text-align: center; + + > .date { + display: inline-block; + position: relative; + margin: 0; + padding: 0 16px; + line-height: 32px; + text-align: center; + font-size: 12px; + border-radius: 64px; + background: var(--dateLabelBg); + color: var(--dateLabelFg); + + > span { + &:first-child { + margin-right: 8px; + + > .icon { + margin-right: 8px; + } + } + + &:last-child { + margin-left: 8px; + + > .icon { + margin-left: 8px; + } + } + } + } + } +} +</style> diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..5311611575d57a6512daf754c86e86c1e67e76f2 --- /dev/null +++ b/src/client/components/dialog.vue @@ -0,0 +1,320 @@ +<template> +<div class="mk-dialog" :class="{ iconOnly }"> + <transition name="bg-fade" appear> + <div class="bg" ref="bg" @click="onBgClick" v-if="show"></div> + </transition> + <transition name="dialog" appear @after-leave="() => { destroyDom(); }"> + <div class="main" ref="main" v-if="show"> + <template v-if="type == 'signin'"> + <mk-signin/> + </template> + <template v-else> + <div class="icon" v-if="icon"> + <fa :icon="icon"/> + </div> + <div class="icon" v-else-if="!input && !select && !user" :class="type"> + <fa :icon="faCheck" v-if="type === 'success'"/> + <fa :icon="faTimesCircle" v-if="type === 'error'"/> + <fa :icon="faExclamationTriangle" v-if="type === 'warning'"/> + <fa :icon="faInfoCircle" v-if="type === 'info'"/> + <fa :icon="faQuestionCircle" v-if="type === 'question'"/> + <fa :icon="faSpinner" pulse v-if="type === 'waiting'"/> + </div> + <header v-if="title" v-html="title"></header> + <header v-if="title == null && user">{{ $t('enterUsername') }}</header> + <div class="body" v-if="text" v-html="text"></div> + <mk-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input> + <mk-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input> + <mk-select v-if="select" v-model="selectedValue" autofocus> + <template v-if="select.items"> + <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + </template> + <template v-else> + <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label"> + <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> + </optgroup> + </template> + </mk-select> + <div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions"> + <mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button> + <mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button> + </div> + <div class="buttons" v-if="actions"> + <mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button> + </div> + </template> + </div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSpinner, faInfoCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; +import MkButton from './ui/button.vue'; +import MkInput from './ui/input.vue'; +import MkSelect from './ui/select.vue'; +import parseAcct from '../../misc/acct/parse'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + MkInput, + MkSelect, + }, + + props: { + type: { + type: String, + required: false, + default: 'info' + }, + title: { + type: String, + required: false + }, + text: { + type: String, + required: false + }, + input: { + required: false + }, + select: { + required: false + }, + user: { + required: false + }, + icon: { + required: false + }, + actions: { + required: false + }, + showOkButton: { + type: Boolean, + default: true + }, + showCancelButton: { + type: Boolean, + default: false + }, + cancelableByBgClick: { + type: Boolean, + default: true + }, + iconOnly: { + type: Boolean, + default: false + }, + autoClose: { + type: Boolean, + default: false + } + }, + + data() { + return { + show: true, + inputValue: this.input && this.input.default ? this.input.default : null, + userInputValue: null, + selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null, + canOk: true, + faTimesCircle, faQuestionCircle, faSpinner, faInfoCircle, faExclamationTriangle, faCheck + }; + }, + + watch: { + userInputValue() { + if (this.user) { + this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => { + this.canOk = u != null; + }).catch(() => { + this.canOk = false; + }); + } + } + }, + + mounted() { + if (this.user) this.canOk = false; + + if (this.autoClose) { + setTimeout(() => { + this.close(); + }, 1000); + } + + document.addEventListener('keydown', this.onKeydown); + }, + + beforeDestroy() { + document.removeEventListener('keydown', this.onKeydown); + }, + + methods: { + async ok() { + if (!this.canOk) return; + if (!this.showOkButton) return; + + if (this.user) { + const user = await this.$root.api('users/show', parseAcct(this.userInputValue)); + if (user) { + this.$emit('ok', user); + this.close(); + } + } else { + const result = + this.input ? this.inputValue : + this.select ? this.selectedValue : + true; + this.$emit('ok', result); + this.close(); + } + }, + + cancel() { + this.$emit('cancel'); + this.close(); + }, + + close() { + if (!this.show) return; + this.show = false; + this.$el.style.pointerEvents = 'none'; + (this.$refs.bg as any).style.pointerEvents = 'none'; + (this.$refs.main as any).style.pointerEvents = 'none'; + }, + + onBgClick() { + if (this.cancelableByBgClick) { + this.cancel(); + } + }, + + onKeydown(e) { + if (e.which === 27) { // ESC + this.cancel(); + } + }, + + onInputKeydown(e) { + if (e.which === 13) { // Enter + e.preventDefault(); + e.stopPropagation(); + this.ok(); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.dialog-enter-active, .dialog-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.dialog-enter, .dialog-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.bg-fade-enter-active, .bg-fade-leave-active { + transition: opacity 0.3s !important; +} +.bg-fade-enter, .bg-fade-leave-to { + opacity: 0; +} + +.mk-dialog { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + z-index: 30000; + top: 0; + left: 0; + width: 100%; + height: 100%; + + &.iconOnly > .main { + min-width: 0; + width: initial; + } + + > .bg { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.7); + } + + > .main { + display: block; + position: fixed; + margin: auto; + padding: 32px; + min-width: 320px; + max-width: 480px; + box-sizing: border-box; + width: calc(100% - 32px); + text-align: center; + background: var(--panel); + border-radius: var(--radius); + + > .icon { + font-size: 32px; + + &.success { + color: var(--accent); + } + + &.error { + color: #ec4137; + } + + &.warning { + color: #ecb637; + } + + > * { + display: block; + margin: 0 auto; + } + + & + header { + margin-top: 16px; + } + } + + > header { + margin: 0 0 8px 0; + font-weight: bold; + font-size: 20px; + + & + .body { + margin-top: 8px; + } + } + + > .body { + margin: 16px 0 0 0; + } + + > .buttons { + margin-top: 16px; + + > * { + margin: 0 8px; + } + } + } +} +</style> diff --git a/src/client/app/common/views/components/drive-file-thumbnail.vue b/src/client/components/drive-file-thumbnail.vue similarity index 83% rename from src/client/app/common/views/components/drive-file-thumbnail.vue rename to src/client/components/drive-file-thumbnail.vue index f44223ad6f33924f8817ff1d2d890c11da96e44b..37a884dc3d9f915b0e63d9962d8fc6402194e73e 100644 --- a/src/client/app/common/views/components/drive-file-thumbnail.vue +++ b/src/client/components/drive-file-thumbnail.vue @@ -36,7 +36,6 @@ <script lang="ts"> import Vue from 'vue'; -import anime from 'animejs'; import { faFile, faFileAlt, @@ -120,12 +119,7 @@ export default Vue.extend({ methods: { onThumbnailLoaded() { if (this.file.properties.avgColor) { - anime({ - targets: this.$refs.thumbnail, - backgroundColor: 'transparent', // TODO fade - duration: 100, - easing: 'linear' - }); + this.$refs.thumbnail.style.backgroundColor = 'transparent'; } }, volumechange() { @@ -136,49 +130,59 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.zdjebgpv - display flex +<style lang="scss" scoped> +.zdjebgpv { + display: flex; > img, - > .icon - pointer-events none + > .icon { + pointer-events: none; + } - > .icon-sub - position absolute - width 30% - height auto - margin 0 - right 4% - bottom 4% + > .icon-sub { + position: absolute; + width: 30%; + height: auto; + margin: 0; + right: 4%; + bottom: 4%; + } - > * - margin auto + > * { + margin: auto; + } - &:not(.detail) - > img - height 100% - width 100% - object-fit cover + &:not(.detail) { + > img { + height: 100%; + width: 100%; + object-fit: cover; + } - > .icon - height 65% - width 65% + > .icon { + height: 65%; + width: 65%; + } > video, - > audio - width 100% - - &.detail - > .icon - height 100px - width 100px - margin 16px + > audio { + width: 100%; + } + } - > *:not(.icon) - max-height 300px - max-width 100% - height 100% - object-fit contain + &.detail { + > .icon { + height: 100px; + width: 100px; + margin: 16px; + } + > *:not(.icon) { + max-height: 300px; + max-width: 100%; + height: 100%; + object-fit: contain; + } + } +} </style> diff --git a/src/client/components/drive-window.vue b/src/client/components/drive-window.vue new file mode 100644 index 0000000000000000000000000000000000000000..64c4cee0c12cf0d7a6541bba58145db15139dba5 --- /dev/null +++ b/src/client/components/drive-window.vue @@ -0,0 +1,53 @@ +<template> +<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="selected.length === 0" @ok="ok()"> + <template #header>{{ multiple ? $t('selectFiles') : $t('selectFile') }}<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length | number }})</span></template> + <div> + <x-drive :multiple="multiple" @change-selection="onChangeSelection" :select-mode="true"/> + </div> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import XDrive from './drive.vue'; +import XWindow from './window.vue'; + +export default Vue.extend({ + i18n, + + components: { + XDrive, + XWindow, + }, + + props: { + type: { + type: String, + required: false, + default: undefined + }, + multiple: { + type: Boolean, + default: false + } + }, + + data() { + return { + selected: [] + }; + }, + + methods: { + ok() { + this.$emit('selected', this.selected); + this.$refs.window.close(); + }, + + onChangeSelection(files) { + this.selected = files; + } + } +}); +</script> diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue new file mode 100644 index 0000000000000000000000000000000000000000..22fc8c6fb713a6cf012e63f3c3e11feda10d1d54 --- /dev/null +++ b/src/client/components/drive.file.vue @@ -0,0 +1,368 @@ +<template> +<div class="ncvczrfv" + :data-is-selected="isSelected" + @click="onClick" + draggable="true" + @dragstart="onDragstart" + @dragend="onDragend" + :title="title" +> + <div class="label" v-if="$store.state.i.avatarId == file.id"> + <img src="/assets/label.svg"/> + <p>{{ $t('avatar') }}</p> + </div> + <div class="label" v-if="$store.state.i.bannerId == file.id"> + <img src="/assets/label.svg"/> + <p>{{ $t('banner') }}</p> + </div> + <div class="label red" v-if="file.isSensitive"> + <img src="/assets/label-red.svg"/> + <p>{{ $t('nsfw') }}</p> + </div> + + <x-file-thumbnail class="thumbnail" :file="file" fit="contain"/> + + <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 { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import copyToClipboard from '../scripts/copy-to-clipboard'; +//import updateAvatar from '../api/update-avatar'; +//import updateBanner from '../api/update-banner'; +import XFileThumbnail from './drive-file-thumbnail.vue'; +import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + i18n, + + props: { + file: { + type: Object, + required: true, + }, + selectMode: { + type: Boolean, + required: false, + default: false, + } + }, + + components: { + XFileThumbnail + }, + + data() { + return { + 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.size)}`; + } + }, + + methods: { + onClick(ev) { + if (this.selectMode) { + this.browser.chooseFile(this.file); + } else { + this.$root.menu({ + items: [{ + type: 'item', + text: this.$t('rename'), + icon: faICursor, + action: this.rename + }, { + type: 'item', + text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'), + icon: this.file.isSensitive ? faEye : faEyeSlash, + action: this.toggleSensitive + }, null, { + type: 'item', + text: this.$t('copyUrl'), + icon: faLink, + action: this.copyUrl + }, { + type: 'a', + href: this.file.url, + target: '_blank', + text: this.$t('download'), + icon: faDownload, + download: this.file.name + }, null, { + type: 'item', + text: this.$t('delete'), + icon: faTrashAlt, + action: this.deleteFile + }, null, { + type: 'nest', + text: this.$t('contextmenu.else-files'), + menu: [{ + type: 'item', + text: this.$t('contextmenu.set-as-avatar'), + action: this.setAsAvatar + }, { + type: 'item', + text: this.$t('contextmenu.set-as-banner'), + action: this.setAsBanner + }] + }], + source: ev.currentTarget || ev.target, + }); + } + }, + + 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: 'transparent', // TODO fade + duration: 100, + easing: 'linear' + }); + } + }, + + rename() { + this.$root.dialog({ + title: this.$t('contextmenu.rename-file'), + input: { + placeholder: this.$t('contextmenu.input-new-file-name'), + default: this.file.name, + allowEmpty: false + } + }).then(({ canceled, result: name }) => { + if (canceled) return; + this.$root.api('drive/files/update', { + fileId: this.file.id, + name: name + }); + }); + }, + + toggleSensitive() { + this.$root.api('drive/files/update', { + fileId: this.file.id, + isSensitive: !this.file.isSensitive + }); + }, + + copyUrl() { + copyToClipboard(this.file.url); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }, + + setAsAvatar() { + updateAvatar(this.$root)(this.file); + }, + + setAsBanner() { + updateBanner(this.$root)(this.file); + }, + + addApp() { + alert('not implemented yet'); + }, + + async deleteFile() { + const { canceled } = await this.$root.dialog({ + type: 'warning', + text: this.$t('driveFileDeleteConfirm', { name: this.file.name }), + showCancelButton: true + }); + if (canceled) return; + + this.$root.api('drive/files/delete', { + fileId: this.file.id + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.ncvczrfv { + position: relative; + padding: 8px 0 0 0; + min-height: 180px; + border-radius: 4px; + + &, * { + cursor: pointer; + } + + &:hover { + background: rgba(#000, 0.05); + + > .label { + &:before, + &:after { + background: #0b65a5; + } + + &.red { + &:before, + &:after { + background: #c12113; + } + } + } + } + + &:active { + background: rgba(#000, 0.1); + + > .label { + &:before, + &:after { + background: #0b588c; + } + + &.red { + &:before, + &:after { + background: #ce2212; + } + } + } + } + + &[data-is-selected] { + background: var(--accent); + + &:hover { + background: var(--accentLighten); + } + + &:active { + background: var(--accentDarken); + } + + > .label { + &:before, + &:after { + display: none; + } + } + + > .name { + color: #fff; + } + + > .thumbnail { + color: #fff; + } + } + + > .label { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + + &:before, + &:after { + content: ""; + display: block; + position: absolute; + z-index: 1; + background: #0c7ac9; + } + + &:before { + top: 0; + left: 57px; + width: 28px; + height: 8px; + } + + &:after { + top: 57px; + left: 0; + width: 8px; + height: 28px; + } + + &.red { + &:before, + &:after { + background: #c12113; + } + } + + > 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; + color: var(--driveFileIcon); + } + + > .name { + display: block; + margin: 4px 0 0 0; + font-size: 0.8em; + text-align: center; + word-break: break-all; + color: var(--fg); + overflow: hidden; + + > .ext { + opacity: 0.5; + } + } +} +</style> diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/components/drive.folder.vue similarity index 69% rename from src/client/app/desktop/views/components/drive.folder.vue rename to src/client/components/drive.folder.vue index cf59d51b01dcc909944d4cef7c8e8cf85440ed9d..39a9588772ca88947015be967e96951459b38b96 100644 --- a/src/client/app/desktop/views/components/drive.folder.vue +++ b/src/client/components/drive.folder.vue @@ -1,6 +1,5 @@ <template> -<div class="ynntpczxvnusfwdyxsfuhvcmuypqopdd" - :data-is-contextmenu-showing="isContextmenuShowing" +<div class="rghtznwe" :data-draghover="draghover" @click="onClick" @mouseover="onMouseover" @@ -12,12 +11,11 @@ draggable="true" @dragstart="onDragstart" @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu" :title="title" > <p class="name"> - <template v-if="hover"><fa :icon="['far', 'folder-open']" fixed-width/></template> - <template v-if="!hover"><fa :icon="['far', 'folder']" fixed-width/></template> + <template v-if="hover"><fa :icon="faFolderOpen" fixed-width/></template> + <template v-if="!hover"><fa :icon="faFolder" fixed-width/></template> {{ folder.name }} </p> <p class="upload" v-if="$store.state.settings.uploadFolder == folder.id"> @@ -28,19 +26,28 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; export default Vue.extend({ - i18n: i18n('desktop/views/components/drive.folder.vue'), - props: ['folder'], + i18n, + + props: { + folder: { + type: Object, + required: true, + } + }, + data() { return { hover: false, draghover: false, isDragging: false, - isContextmenuShowing: false + faFolder, faFolderOpen }; }, + computed: { browser(): any { return this.$parent; @@ -54,43 +61,6 @@ export default Vue.extend({ this.browser.move(this.folder); }, - onContextmenu(e) { - this.isContextmenuShowing = true; - this.$contextmenu(e, [{ - type: 'item', - text: this.$t('contextmenu.move-to-this-folder'), - icon: 'arrow-right', - action: this.go - }, { - type: 'item', - text: this.$t('contextmenu.show-in-new-window'), - icon: ['far', 'window-restore'], - action: this.newWindow - }, null, { - type: 'item', - text: this.$t('contextmenu.rename'), - icon: 'i-cursor', - action: this.rename - }, null, { - type: 'item', - text: this.$t('@.delete'), - icon: ['far', 'trash-alt'], - action: this.deleteFolder - }, null, { - type: 'nest', - text: this.$t('contextmenu.else-folders'), - menu: [{ - type: 'item', - text: this.$t('contextmenu.set-as-upload-folder'), - action: this.setAsUploadFolder - }] - }], { - closed: () => { - this.isContextmenuShowing = false; - } - }); - }, - onMouseover() { this.hover = true; }, @@ -259,55 +229,53 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.ynntpczxvnusfwdyxsfuhvcmuypqopdd - padding 8px - height 64px - background var(--desktopDriveFolderBg) - border-radius 4px - - &, * - cursor pointer - - * - pointer-events none - - &:hover - background var(--desktopDriveFolderHoverBg) - - &:active - background var(--desktopDriveFolderActiveBg) - - &[data-is-contextmenu-showing] - &[data-draghover] - &:after - content "" - pointer-events none - position absolute - top -4px - right -4px - bottom -4px - left -4px - border 2px dashed var(--primaryAlpha03) - border-radius 4px - - &[data-draghover] - background var(--desktopDriveFolderActiveBg) - - > .name - margin 0 - font-size 0.9em - color var(--desktopDriveFolderFg) - - > [data-icon] - margin-right 4px - margin-left 2px - text-align left - - > .upload - margin 4px 4px - font-size 0.8em - text-align right - color var(--desktopDriveFolderFg) +<style lang="scss" scoped> +.rghtznwe { + position: relative; + padding: 8px; + height: 64px; + background: var(--driveFolderBg); + border-radius: 4px; + + &, * { + cursor: pointer; + } + * { + pointer-events: none; + } + + &[data-draghover] { + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: -4px; + right: -4px; + bottom: -4px; + left: -4px; + border: 2px dashed var(--focus); + border-radius: 4px; + } + } + + > .name { + margin: 0; + font-size: 0.9em; + color: var(--desktopDriveFolderFg); + + > [data-icon] { + margin-right: 4px; + margin-left: 2px; + text-align: left; + } + } + + > .upload { + margin: 4px 4px; + font-size: 0.8em; + text-align: right; + color: var(--desktopDriveFolderFg); + } +} </style> diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/components/drive.nav-folder.vue similarity index 83% rename from src/client/app/desktop/views/components/drive.nav-folder.vue rename to src/client/components/drive.nav-folder.vue index 14ab46764218f0e855cd4df2431f4eeb16883213..0689faecd24897a6314daf4fc9cb2a860e2fee9c 100644 --- a/src/client/app/desktop/views/components/drive.nav-folder.vue +++ b/src/client/components/drive.nav-folder.vue @@ -1,5 +1,5 @@ <template> -<div class="root nav-folder" +<div class="drylbebk" :data-draghover="draghover" @click="onClick" @dragover.prevent.stop="onDragover" @@ -7,38 +7,53 @@ @dragleave="onDragleave" @drop.stop="onDrop" > - <i v-if="folder == null" class="cloud"><fa icon="cloud"/></i> - <span>{{ folder == null ? $t('@.drive') : folder.name }}</span> + <i v-if="folder == null"><fa :icon="faCloud"/></i> + <span>{{ folder == null ? $t('drive') : folder.name }}</span> </div> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faCloud } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; + export default Vue.extend({ - i18n: i18n(), - props: ['folder'], + i18n, + + props: { + folder: { + type: Object, + required: false, + } + }, + data() { return { hover: false, - draghover: false + draghover: false, + faCloud }; }, + 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) { @@ -57,12 +72,15 @@ export default Vue.extend({ 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; @@ -104,15 +122,18 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.root.nav-folder - > * - pointer-events none - - &[data-draghover] - background #eee +<style lang="scss" scoped> +.drylbebk { + > * { + pointer-events: none; + } - i.cloud - margin-right 4px + &[data-draghover] { + background: #eee; + } + > i { + margin-right: 4px; + } +} </style> diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/components/drive.vue similarity index 66% rename from src/client/app/desktop/views/components/drive.vue rename to src/client/components/drive.vue index ff4ff18e6ed54fcbb5757a74a0e0a16383814b10..2279e2eb6e548acf12bb85fe838517ba9745418f 100644 --- a/src/client/app/desktop/views/components/drive.vue +++ b/src/client/components/drive.vue @@ -1,76 +1,71 @@ <template> -<div class="mk-drive"> +<div class="yfudmmck"> <nav> <div class="path" @contextmenu.prevent.stop="() => {}"> <x-nav-folder :class="{ current: folder == null }"/> <template v-for="folder in hierarchyFolders"> - <span class="separator"><fa icon="angle-right"/></span> + <span class="separator"><fa :icon="faAngleRight"/></span> <x-nav-folder :folder="folder" :key="folder.id"/> </template> - <span class="separator" v-if="folder != null"><fa icon="angle-right"/></span> + <span class="separator" v-if="folder != null"><fa :icon="faAngleRight"/></span> <span class="folder current" v-if="folder != null">{{ folder.name }}</span> </div> </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 || moreFolders"> + <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> - <ui-button v-if="moreFolders">{{ $t('@.load-more') }}</ui-button> + <mk-button v-if="moreFolders">{{ $t('@.load-more') }}</mk-button> </div> - <div class="files" ref="filesContainer" v-if="files.length > 0 || moreFiles"> - <x-file v-for="file in files" :key="file.id" class="file" :file="file"/> + <div class="files" ref="filesContainer" v-if="files.length > 0"> + <x-file v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="selectMode"/> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <div class="padding" v-for="n in 16"></div> - <ui-button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</ui-button> + <mk-button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</mk-button> </div> - <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching"> + <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> <p v-if="draghover">{{ $t('empty-draghover') }}</p> - <p v-if="!draghover && folder == null"><strong>{{ $t('empty-drive') }}</strong><br/>{{ $t('empty-drive-description') }}</p> - <p v-if="!draghover && folder != null">{{ $t('empty-folder') }}</p> - </div> - </div> - <div class="fetching" v-if="fetching"> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> + <p v-if="!draghover && folder == null"><strong>{{ $t('emptyDrive') }}</strong><br/>{{ $t('empty-drive-description') }}</p> + <p v-if="!draghover && folder != null">{{ $t('emptyFolder') }}</p> </div> </div> + <mk-loading v-if="fetching"/> </div> <div class="dropzone" v-if="draghover"></div> - <mk-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/> + <x-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 i18n from '../../../i18n'; -import MkDriveWindow from './drive-window.vue'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; 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 { url } from '../../../config'; -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; +import XUploader from './uploader.vue'; +import MkButton from './ui/button.vue'; export default Vue.extend({ - i18n: i18n('desktop/views/components/drive.vue'), + i18n, + components: { XNavFolder, XFolder, - XFile + XFile, + XUploader, + MkButton, }, + props: { initFolder: { type: Object, @@ -79,13 +74,20 @@ export default Vue.extend({ type: { type: String, required: false, - default: undefined + default: undefined }, multiple: { type: Boolean, + required: false, + default: false + }, + selectMode: { + type: Boolean, + required: false, default: false } }, + data() { return { /** @@ -114,9 +116,18 @@ export default Vue.extend({ */ isDragSource: false, - fetching: true + fetching: true, + + faAngleRight }; }, + + watch: { + folder() { + this.$emit('cd', this.folder); + } + }, + mounted() { this.connection = this.$root.stream.useSharedConnection('drive'); @@ -133,29 +144,12 @@ export default Vue.extend({ this.fetch(); } }, + beforeDestroy() { this.connection.dispose(); }, - methods: { - onContextmenu(e) { - this.$contextmenu(e, [{ - type: 'item', - text: this.$t('contextmenu.create-folder'), - icon: ['far', 'folder'], - action: this.createFolder - }, { - type: 'item', - text: this.$t('contextmenu.upload'), - icon: 'upload', - action: this.selectLocalFile - }, { - type: 'item', - text: this.$t('contextmenu.url-upload'), - icon: faCloudUploadAlt, - action: this.urlUpload - }]); - }, + methods: { onStreamDriveFileCreated(file) { this.addFile(file, true); }, @@ -198,53 +192,6 @@ export default Vue.extend({ 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) { @@ -402,18 +349,6 @@ export default Vue.extend({ } }, - newWindow(folder) { - if (document.body.clientWidth > 800) { - this.$root.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(); @@ -590,171 +525,140 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-drive - > nav - display block - z-index 2 - width 100% - overflow auto - font-size 0.9em - color var(--text) - background var(--face) - box-shadow 0 1px 0 rgba(#000, 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 - - * - 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-icon] - margin 0 - - > .main - padding 8px - height calc(100% - 38px) - overflow auto - background var(--desktopDriveBg) - - &, * - 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 var(--primary) - background var(--primaryAlpha05) - 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(#000, 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); +<style lang="scss" scoped> +.yfudmmck { + > nav { + display: block; + z-index: 2; + width: 100%; + overflow: auto; + font-size: 0.9em; + box-shadow: 0 1px 0 var(--divider); + + &, * { + user-select: none; + } + + > .path { + display: inline-block; + vertical-align: bottom; + line-height: 38px; + white-space: nowrap; + + > * { + display: inline-block; + margin: 0; + padding: 0 8px; + line-height: 38px; + cursor: pointer; + + * { + 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-icon] { + margin: 0; + } } } + } + } + + > .main { + padding: 8px 0; + overflow: auto; + + &, * { + user-select: none; + } - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); + &.fetching { + cursor: wait !important; + + * { + pointer-events: none; + } + + > .contents { + opacity: 0.5; + } + } + + &.uploading { + height: calc(100% - 38px - 100px); + } + + > .contents { + + > .folders, + > .files { + display: flex; + flex-wrap: wrap; + + > .folder, + > .file { + flex-grow: 1; + width: 144px; + margin: 4px; + box-sizing: border-box; } - 50% { - transform: scale(1.0); + + > .padding { + flex-grow: 1; + pointer-events: none; + width: 144px + 8px; } } - > .dropzone - position absolute - left 0 - top 38px - width 100% - height calc(100% - 38px) - border dashed 2px var(--primaryAlpha05) - pointer-events none + > .empty { + padding: 16px; + text-align: center; + pointer-events: none; + opacity: 0.5; - > .mk-uploader - height 100px - padding 16px + > p { + margin: 0; + } + } + } + } - > input - display none + > .dropzone { + position: absolute; + left: 0; + top: 38px; + width: 100%; + height: calc(100% - 38px); + border: dashed 2px var(--focus); + pointer-events: none; + } + + > .mk-uploader { + height: 100px; + padding: 16px; + } + > input { + display: none; + } +} </style> diff --git a/src/client/components/ellipsis.vue b/src/client/components/ellipsis.vue new file mode 100644 index 0000000000000000000000000000000000000000..0a46f486d6875862a0821677898d290702102710 --- /dev/null +++ b/src/client/components/ellipsis.vue @@ -0,0 +1,34 @@ +<template> + <span class="mk-ellipsis"> + <span>.</span><span>.</span><span>.</span> + </span> +</template> + +<style lang="scss" 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/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue new file mode 100644 index 0000000000000000000000000000000000000000..61d641a023d6a53de79c2e722d565036282cd635 --- /dev/null +++ b/src/client/components/emoji-picker.vue @@ -0,0 +1,268 @@ +<template> +<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }"> + <div class="omfetrab"> + <header> + <button v-for="category in categories" + class="_button" + :title="category.text" + @click="go(category)" + :class="{ active: category.isActive }" + :key="category.text" + > + <fa :icon="category.icon" fixed-width/> + </button> + </header> + + <div class="emojis"> + <template v-if="categories[0].isActive"> + <header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsedEmojis') }}</header> + <div class="list"> + <button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])" + class="_button" + :title="emoji.name" + @click="chosen(emoji)" + :key="i" + > + <mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/> + <img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> + </button> + </div> + </template> + + <header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header> + <template v-if="categories.find(x => x.isActive).name"> + <div class="list"> + <button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)" + class="_button" + :title="emoji.name" + @click="chosen(emoji)" + :key="emoji.name" + > + <mk-emoji :emoji="emoji.char"/> + </button> + </div> + </template> + <template v-else> + <div v-for="(key, i) in Object.keys(customEmojis)" :key="i"> + <header class="sub" v-if="key">{{ key }}</header> + <div class="list"> + <button v-for="emoji in customEmojis[key]" + class="_button" + :title="emoji.name" + @click="chosen(emoji)" + :key="emoji.name" + > + <img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> + </button> + </div> + </div> + </template> + </div> + </div> +</x-popup> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import { emojilist } from '../../misc/emojilist'; +import { getStaticImageUrl } from '../scripts/get-static-image-url'; +import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory } from '@fortawesome/free-solid-svg-icons'; +import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons'; +import { groupByX } from '../../prelude/array'; +import XPopup from './popup.vue'; + +export default Vue.extend({ + i18n, + + components: { + XPopup, + }, + + props: { + source: { + required: true + }, + }, + + data() { + return { + emojilist, + getStaticImageUrl, + customEmojis: {}, + faGlobe, faHistory, + categories: [{ + text: this.$t('customEmoji'), + icon: faAsterisk, + isActive: true + }, { + name: 'people', + text: this.$t('people'), + icon: faLaugh, + isActive: false + }, { + name: 'animals_and_nature', + text: this.$t('animals-and-nature'), + icon: faLeaf, + isActive: false + }, { + name: 'food_and_drink', + text: this.$t('food-and-drink'), + icon: faUtensils, + isActive: false + }, { + name: 'activity', + text: this.$t('activity'), + icon: faFutbol, + isActive: false + }, { + name: 'travel_and_places', + text: this.$t('travel-and-places'), + icon: faCity, + isActive: false + }, { + name: 'objects', + text: this.$t('objects'), + icon: faDice, + isActive: false + }, { + name: 'symbols', + text: this.$t('symbols'), + icon: faHeart, + isActive: false + }, { + name: 'flags', + text: this.$t('flags'), + icon: faFlag, + isActive: false + }] + }; + }, + + created() { + let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || []; + local = groupByX(local, (x: any) => x.category || ''); + this.customEmojis = local; + }, + + methods: { + go(category: any) { + this.goCategory(category.name); + }, + + goCategory(name: string) { + let matched = false; + for (const c of this.categories) { + c.isActive = c.name === name; + if (c.isActive) { + matched = true; + } + } + if (!matched) { + this.categories[0].isActive = true; + } + }, + + chosen(emoji: any) { + const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`; + let recents = this.$store.state.device.recentEmojis || []; + recents = recents.filter((e: any) => getKey(e) !== getKey(emoji)); + recents.unshift(emoji) + this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) }); + this.$emit('chosen', getKey(emoji)); + }, + + close() { + this.$refs.popup.close(); + } + } +}); +</script> + +<style lang="scss" scoped> +.omfetrab { + width: 350px; + + > header { + display: flex; + + > button { + flex: 1; + padding: 10px 0; + font-size: 16px; + transition: color 0.2s ease; + + &:hover { + color: var(--textHighlighted); + transition: color 0s; + } + + &.active { + color: var(--accent); + transition: color 0s; + } + } + } + + > .emojis { + height: 300px; + overflow-y: auto; + overflow-x: hidden; + + > header.category { + position: sticky; + top: 0; + left: 0; + z-index: 1; + padding: 8px; + background: var(--panel); + font-size: 12px; + } + + header.sub { + padding: 4px 8px; + font-size: 12px; + } + + div.list { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + gap: 4px; + padding: 8px; + + > button { + position: relative; + padding: 0; + width: 100%; + + &:before { + content: ''; + display: block; + width: 1px; + height: 0; + padding-bottom: 100%; + } + + &:hover { + > * { + transform: scale(1.2); + transition: transform 0s; + } + } + + > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; + font-size: 28px; + transition: transform 0.2s ease; + pointer-events: none; + } + } + } + } +} +</style> diff --git a/src/client/app/common/views/components/emoji.vue b/src/client/components/emoji.vue similarity index 71% rename from src/client/app/common/views/components/emoji.vue rename to src/client/components/emoji.vue index 26992c5f7e792442c0729273faabfb4af4decff0..2e8bddb8034f8842b7e67d8410ae8e5ae0ed99d6 100644 --- a/src/client/app/common/views/components/emoji.vue +++ b/src/client/components/emoji.vue @@ -1,14 +1,14 @@ <template> -<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :class="{ normal: normal }" :src="url" :alt="alt" :title="alt"/> -<img v-else-if="char && !useOsDefaultEmojis" class="fvgwvorwhxigeolkkrcderjzcawqrscl" :src="url" :alt="alt" :title="alt"/> +<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt"/> +<img v-else-if="char && !useOsDefaultEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt"/> <span v-else-if="char && useOsDefaultEmojis">{{ char }}</span> <span v-else>:{{ name }}:</span> </template> <script lang="ts"> import Vue from 'vue'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; -import { twemojiSvgBase } from '../../../../../misc/twemoji-base'; +import { getStaticImageUrl } from '../scripts/get-static-image-url'; +import { twemojiSvgBase } from '../../misc/twemoji-base'; export default Vue.extend({ props: { @@ -25,6 +25,11 @@ export default Vue.extend({ required: false, default: false }, + noStyle: { + type: Boolean, + required: false, + default: false + }, customEmojis: { required: false, default: () => [] @@ -96,24 +101,32 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.fvgwvorwhxigeolkkrcderjzcawqrscl - height 1.25em - vertical-align -0.25em +<style lang="scss" scoped> +.mk-emoji { + height: 1.25em; + vertical-align: -0.25em; - &.custom - height 2.5em - vertical-align middle - transition transform 0.2s ease + &.custom { + height: 2.5em; + vertical-align: middle; + transition: transform 0.2s ease; - &:hover - transform scale(1.2) + &:hover { + transform: scale(1.2); + } - &.normal - height 1.25em - vertical-align -0.25em + &.normal { + height: 1.25em; + vertical-align: -0.25em; - &:hover - transform none + &:hover { + transform: none; + } + } + } + &.noStyle { + height: auto !important; + } +} </style> diff --git a/src/client/components/error.vue b/src/client/components/error.vue new file mode 100644 index 0000000000000000000000000000000000000000..1dc21dbb1934c85652cb509005f613b8fe5d5c9e --- /dev/null +++ b/src/client/components/error.vue @@ -0,0 +1,42 @@ +<template> +<div class="wjqjnyhzogztorhrdgcpqlkxhkmuetgj _panel"> + <p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p> + <mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import MkButton from './ui/button.vue'; + +export default Vue.extend({ + i18n, + components: { + MkButton, + }, + data() { + return { + faExclamationTriangle + }; + }, +}); +</script> + +<style lang="scss" scoped> +.wjqjnyhzogztorhrdgcpqlkxhkmuetgj { + max-width: 350px; + margin: 0 auto; + padding: 32px; + text-align: center; + + > p { + margin: 0 0 8px 0; + } + + > .button { + margin: 0 auto; + } +} +</style> diff --git a/src/client/components/file-type-icon.vue b/src/client/components/file-type-icon.vue new file mode 100644 index 0000000000000000000000000000000000000000..8492567ad71056693b784b4d6efc0d7c294bcc6c --- /dev/null +++ b/src/client/components/file-type-icon.vue @@ -0,0 +1,29 @@ +<template> +<span class="mk-file-type-icon"> + <template v-if="kind == 'image'"><fa :icon="faFileImage"/></template> +</span> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faFileImage } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + props: { + type: { + type: String, + required: true, + } + }, + data() { + return { + faFileImage + }; + }, + computed: { + kind(): string { + return this.type.split('/')[0]; + } + } +}); +</script> diff --git a/src/client/components/follow-button.vue b/src/client/components/follow-button.vue new file mode 100644 index 0000000000000000000000000000000000000000..4b57a2bd88d6b2978af4925118b491e6a20bf8b3 --- /dev/null +++ b/src/client/components/follow-button.vue @@ -0,0 +1,162 @@ +<template> +<button class="wfliddvnhxvyusikowhxozkyxyenqxqr _button" + :class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou }" + @click="onClick" + :disabled="wait" +> + <template v-if="!wait"> + <fa v-if="hasPendingFollowRequestFromYou && user.isLocked" :icon="faHourglassHalf"/> + <fa v-else-if="hasPendingFollowRequestFromYou && !user.isLocked" :icon="faSpinner" pulse/> + <fa v-else-if="isFollowing" :icon="faMinus"/> + <fa v-else-if="!isFollowing && user.isLocked" :icon="faPlus"/> + <fa v-else-if="!isFollowing && !user.isLocked" :icon="faPlus"/> + </template> + <template v-else><fa :icon="faSpinner" pulse fixed-width/></template> +</button> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + i18n, + + props: { + user: { + type: Object, + required: true + }, + }, + + data() { + return { + isFollowing: this.user.isFollowing, + hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou, + wait: false, + connection: null, + faSpinner, faPlus, faMinus, faHourglassHalf + }; + }, + + mounted() { + this.connection = this.$root.stream.useSharedConnection('main'); + + this.connection.on('follow', this.onFollowChange); + this.connection.on('unfollow', this.onFollowChange); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + + methods: { + onFollowChange(user) { + if (user.id == this.user.id) { + this.isFollowing = user.isFollowing; + this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; + } + }, + + async onClick() { + this.wait = true; + + try { + if (this.isFollowing) { + const { canceled } = await this.$root.dialog({ + type: 'warning', + text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }), + showCancelButton: true + }); + + if (canceled) return; + + await this.$root.api('following/delete', { + userId: this.user.id + }); + } else { + if (this.hasPendingFollowRequestFromYou) { + await this.$root.api('following/requests/cancel', { + userId: this.user.id + }); + } else if (this.user.isLocked) { + await this.$root.api('following/create', { + userId: this.user.id + }); + this.hasPendingFollowRequestFromYou = true; + } else { + await this.$root.api('following/create', { + userId: this.user.id + }); + this.hasPendingFollowRequestFromYou = true; + } + } + } catch (e) { + console.error(e); + } finally { + this.wait = false; + } + } + } +}); +</script> + +<style lang="scss" scoped> +.wfliddvnhxvyusikowhxozkyxyenqxqr { + position: relative; + display: inline-block; + font-weight: bold; + color: var(--accent); + background: transparent; + border: solid 1px var(--accent); + padding: 0; + width: 31px; + height: 31px; + font-size: 16px; + border-radius: 100%; + background: #fff; + + &:focus { + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: -5px; + right: -5px; + bottom: -5px; + left: -5px; + border: 2px solid var(--focus); + border-radius: 100%; + } + } + + &:hover { + //background: mix($primary, #fff, 20); + } + + &:active { + //background: mix($primary, #fff, 40); + } + + &.active { + color: #fff; + background: var(--accent); + + &:hover { + background: var(--accentLighten); + border-color: var(--accentLighten); + } + + &:active { + background: var(--accentDarken); + border-color: var(--accentDarken); + } + } + + &.wait { + cursor: wait !important; + opacity: 0.7; + } +} +</style> diff --git a/src/client/app/common/views/components/formula-core.vue b/src/client/components/formula-core.vue similarity index 88% rename from src/client/app/common/views/components/formula-core.vue rename to src/client/components/formula-core.vue index 69697d6df05118726652d6c3dc58006a657a35a8..45b27f902699dd8a9fa06c867e1ca3bf7c81e7a6 100644 --- a/src/client/app/common/views/components/formula-core.vue +++ b/src/client/components/formula-core.vue @@ -1,3 +1,4 @@ + <template> <div v-if="block" v-html="compiledFormula"></div> <span v-else v-html="compiledFormula"></span> @@ -6,7 +7,6 @@ <script lang="ts"> import Vue from 'vue'; import * as katex from 'katex'; - export default Vue.extend({ props: { formula: { @@ -29,5 +29,5 @@ export default Vue.extend({ </script> <style> -@import "../../../../../../node_modules/katex/dist/katex.min.css"; +@import "../../../node_modules/katex/dist/katex.min.css"; </style> diff --git a/src/client/app/common/views/components/formula.vue b/src/client/components/formula.vue similarity index 99% rename from src/client/app/common/views/components/formula.vue rename to src/client/components/formula.vue index 73572b72c6556d902af4b3643f3f51cfea124f40..4aaad1bf3e082412a37ab425d9c5b0811f8a71cf 100644 --- a/src/client/app/common/views/components/formula.vue +++ b/src/client/components/formula.vue @@ -4,12 +4,10 @@ <script lang="ts"> import Vue from 'vue'; - export default Vue.extend({ components: { XFormula: () => import('./formula-core.vue').then(m => m.default) }, - props: { formula: { type: String, diff --git a/src/client/components/google.vue b/src/client/components/google.vue new file mode 100644 index 0000000000000000000000000000000000000000..e6ef7f7d9067ab662f3cdd50a58427c60c752e0e --- /dev/null +++ b/src/client/components/google.vue @@ -0,0 +1,71 @@ +<template> +<div class="mk-google"> + <input type="search" v-model="query" :placeholder="q"> + <button @click="search"><fa icon="search"/> {{ $t('@.search') }}</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + props: ['q'], + data() { + return { + query: null + }; + }, + mounted() { + this.query = this.q; + }, + methods: { + search() { + const engine = this.$store.state.settings.webSearchEngine || + 'https://www.google.com/?#q={{query}}'; + const url = engine.replace('{{query}}', this.query) + window.open(url, '_blank'); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-google { + display: flex; + margin: 8px 0; + + > input { + flex-shrink: 1; + padding: 10px; + width: 100%; + height: 40px; + font-size: 16px; + color: var(--googleSearchFg); + background: var(--googleSearchBg); + border: solid 1px var(--googleSearchBorder); + border-radius: 4px 0 0 4px; + + &:hover { + border-color: var(--googleSearchHoverBorder); + } + } + + > button { + flex-shrink: 0; + padding: 0 16px; + border: solid 1px var(--googleSearchBorder); + border-left: none; + border-radius: 0 4px 4px 0; + + &:hover { + background-color: var(--googleSearchHoverButton); + } + + &:active { + box-shadow: 0 2px 4px rgba(#000, 0.15) inset; + } + } +} +</style> diff --git a/src/client/components/index.ts b/src/client/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9385c2af73424bda52231baa10d56581642af69f --- /dev/null +++ b/src/client/components/index.ts @@ -0,0 +1,25 @@ +import Vue from 'vue'; + +import mfm from './misskey-flavored-markdown.vue'; +import acct from './acct.vue'; +import avatar from './avatar.vue'; +import emoji from './emoji.vue'; +import userName from './user-name.vue'; +import ellipsis from './ellipsis.vue'; +import time from './time.vue'; +import url from './url.vue'; +import loading from './loading.vue'; +import SequentialEntrance from './sequential-entrance.vue'; +import error from './error.vue'; + +Vue.component('mfm', mfm); +Vue.component('mk-acct', acct); +Vue.component('mk-avatar', avatar); +Vue.component('mk-emoji', emoji); +Vue.component('mk-user-name', userName); +Vue.component('mk-ellipsis', ellipsis); +Vue.component('mk-time', time); +Vue.component('mk-url', url); +Vue.component('mk-loading', loading); +Vue.component('mk-error', error); +Vue.component('sequential-entrance', SequentialEntrance); diff --git a/src/client/components/loading.vue b/src/client/components/loading.vue new file mode 100644 index 0000000000000000000000000000000000000000..88d1ed77fac1f32be31b0ef3362495d07f476c33 --- /dev/null +++ b/src/client/components/loading.vue @@ -0,0 +1,30 @@ +<template> +<div class="yxspomdl"> + <fa :icon="faSpinner" pulse fixed-width class="icon"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + data() { + return { + faSpinner + }; + }, +}); +</script> + +<style lang="scss" scoped> +.yxspomdl { + padding: 32px; + text-align: center; + + > .icon { + font-size: 32px; + opacity: 0.5; + } +} +</style> diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/components/media-banner.vue similarity index 57% rename from src/client/app/common/views/components/media-banner.vue rename to src/client/components/media-banner.vue index 4e459ad666dd6a588d480c6f1acb02fcac2e3cfa..088c11fab7a9f113f278afb16df3dcb8d9da9845 100644 --- a/src/client/app/common/views/components/media-banner.vue +++ b/src/client/components/media-banner.vue @@ -1,9 +1,9 @@ <template> <div class="mk-media-banner"> <div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false"> - <span class="icon"><fa icon="exclamation-triangle"/></span> + <span class="icon"><fa :icon="faExclamationTriangle"/></span> <b>{{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> + <span>{{ $t('clickToShow') }}</span> </div> <div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'"> <audio class="audio" @@ -27,10 +27,11 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; export default Vue.extend({ - i18n: i18n('common/views/components/media-banner.vue'), + i18n, props: { media: { type: Object, @@ -39,7 +40,8 @@ export default Vue.extend({ }, data() { return { - hide: true + hide: true, + faExclamationTriangle }; }, mounted() { @@ -55,44 +57,53 @@ export default Vue.extend({ }) </script> -<style lang="stylus" scoped> -.mk-media-banner - width 100% - border-radius 4px - margin-top 4px - overflow hidden +<style lang="scss" scoped> +.mk-media-banner { + width: 100%; + border-radius: 4px; + margin-top: 4px; + overflow: hidden; > .download, - > .sensitive - display flex - align-items center - font-size 12px - padding 8px 12px - white-space nowrap + > .sensitive { + display: flex; + align-items: center; + font-size: 12px; + padding: 8px 12px; + white-space: nowrap; - > * - display block - - > b - overflow hidden - text-overflow ellipsis + > * { + display: block; + } - > *:not(:last-child) - margin-right .2em + > b { + overflow: hidden; + text-overflow: ellipsis; + } - > .icon - font-size 1.6em + > *:not(:last-child) { + margin-right: .2em; + } - > .download - background var(--noteAttachedFile) + > .icon { + font-size: 1.6em; + } + } - > .sensitive - background #111 - color #fff + > .download { + background: var(--noteAttachedFile); + } - > .audio - .audio - display block - width 100% + > .sensitive { + background: #111; + color: #fff; + } + > .audio { + .audio { + display: block; + width: 100%; + } + } +} </style> diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue new file mode 100644 index 0000000000000000000000000000000000000000..5ae167d490acc2eecb7ffadfc4daf64303469ea3 --- /dev/null +++ b/src/client/components/media-image.vue @@ -0,0 +1,113 @@ +<template> +<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> + <div> + <b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> + <span>{{ $t('clickToShow') }}</span> + </div> +</div> +<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else + :href="image.url" + :style="style" + :title="image.name" + @click.prevent="onClick" +> + <div v-if="image.type === 'image/gif'">GIF</div> +</a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import { getStaticImageUrl } from '../scripts/get-static-image-url'; + +export default Vue.extend({ + i18n, + props: { + image: { + type: Object, + required: true + }, + raw: { + default: false + } + }, + data() { + return { + hide: true, + faExclamationTriangle + }; + }, + computed: { + style(): any { + let url = `url(${ + this.$store.state.device.disableShowingAnimatedImages + ? getStaticImageUrl(this.image.thumbnailUrl) + : this.image.thumbnailUrl + })`; + + if (this.$store.state.device.loadRemoteMedia) { + url = null; + } else if (this.raw || this.$store.state.device.loadRawImages) { + url = `url(${this.image.url})`; + } + + return { + 'background-color': this.image.properties.avgColor || 'transparent', + 'background-image': url + }; + } + }, + methods: { + onClick() { + window.open(this.image.url, '_blank'); + } + } +}); +</script> + +<style lang="scss" scoped> +.gqnyydlzavusgskkfvwvjiattxdzsqlf { + display: block; + cursor: zoom-in; + overflow: hidden; + width: 100%; + height: 100%; + background-position: center; + background-size: contain; + background-repeat: no-repeat; + + > div { + background-color: var(--fg); + border-radius: 6px; + color: var(--secondary); + display: inline-block; + font-size: 14px; + font-weight: bold; + left: 12px; + opacity: .5; + padding: 0 6px; + text-align: center; + top: 12px; + pointer-events: none; + } +} + +.qjewsnkgzzxlxtzncydssfbgjibiehcy { + display: flex; + justify-content: center; + align-items: center; + background: #111; + color: #fff; + + > div { + display: table-cell; + text-align: center; + font-size: 12px; + + > * { + display: block; + } + } +} +</style> diff --git a/src/client/components/media-list.vue b/src/client/components/media-list.vue new file mode 100644 index 0000000000000000000000000000000000000000..08722ff91a0a4923ef5df1997e5334e425ec1c98 --- /dev/null +++ b/src/client/components/media-list.vue @@ -0,0 +1,130 @@ +<template> +<div class="mk-media-list"> + <template v-for="media in mediaList.filter(media => !previewable(media))"> + <x-banner :media="media" :key="media.id"/> + </template> + <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container"> + <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid"> + <template v-for="media in mediaList"> + <x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> + <x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> + </template> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XBanner from './media-banner.vue'; +import XImage from './media-image.vue'; +import XVideo from './media-video.vue'; + +export default Vue.extend({ + components: { + XBanner, + XImage, + XVideo, + }, + props: { + mediaList: { + required: true + }, + raw: { + default: false + } + }, + mounted() { + //#region for Safari bug + if (this.$refs.grid) { + this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` + : '287px'; + } + //#endregion + }, + methods: { + previewable(file) { + return file.type.startsWith('video') || file.type.startsWith('image'); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-media-list { + > .gird-container { + position: relative; + width: 100%; + margin-top: 4px; + + &:before { + content: ''; + display: block; + padding-top: 56.25% // 16:9; + } + + > div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: grid; + grid-gap: 4px; + + > * { + overflow: hidden; + border-radius: 4px; + } + + &[data-count="1"] { + grid-template-rows: 1fr; + } + + &[data-count="2"] { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + } + + &[data-count="3"] { + grid-template-columns: 1fr 0.5fr; + grid-template-rows: 1fr 1fr; + + > *:nth-child(1) { + grid-row: 1 / 3; + } + + > *:nth-child(3) { + grid-column: 2 / 3; + grid-row: 2 / 3; + } + } + + &[data-count="4"] { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + } + + > *: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/client/app/mobile/views/components/media-video.vue b/src/client/components/media-video.vue similarity index 51% rename from src/client/app/mobile/views/components/media-video.vue rename to src/client/components/media-video.vue index 044bb4c106b4c7c7eb81d61e528031041dc67ee5..f96e90297603705ff4f61a9771a5dfef8acf737d 100644 --- a/src/client/app/mobile/views/components/media-video.vue +++ b/src/client/components/media-video.vue @@ -2,7 +2,7 @@ <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> <div> <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> + <span>{{ $t('clickToShow') }}</span> </div> </div> <a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else @@ -12,16 +12,17 @@ :style="imageStyle" :title="video.name" > - <fa :icon="['far', 'play-circle']"/> + <fa :icon="faPlayCircle"/> </a> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faPlayCircle } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; export default Vue.extend({ - i18n: i18n('mobile/views/components/media-video.vue'), + i18n, props: { video: { type: Object, @@ -30,7 +31,8 @@ export default Vue.extend({ }, data() { return { - hide: true + hide: true, + faPlayCircle }; }, computed: { @@ -43,32 +45,35 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.kkjnbbplepmiyuadieoenjgutgcmtsvu - display flex - justify-content center - align-items center +<style lang="scss" scoped> +.kkjnbbplepmiyuadieoenjgutgcmtsvu { + display: flex; + justify-content: center; + align-items: center; - font-size 3.5em - overflow hidden - background-position center - background-size cover - width 100% - height 100% + font-size: 3.5em; + overflow: hidden; + background-position: center; + background-size: cover; + width: 100%; + height: 100%; +} -.icozogqfvdetwohsdglrbswgrejoxbdj - display flex - justify-content center - align-items center - background #111 - color #fff +.icozogqfvdetwohsdglrbswgrejoxbdj { + display: flex; + justify-content: center; + align-items: center; + background: #111; + color: #fff; - > div - display table-cell - text-align center - font-size 12px - - > b - display block + > div { + display: table-cell; + text-align: center; + font-size: 12px; + > b { + display: block; + } + } +} </style> diff --git a/src/client/app/common/views/components/mention.vue b/src/client/components/mention.vue similarity index 58% rename from src/client/app/common/views/components/mention.vue rename to src/client/components/mention.vue index 4e9f9e90d6d40159f370c2ba62fea099212677b1..06dcf1288787ee9a896ae888c049de457660cea9 100644 --- a/src/client/app/common/views/components/mention.vue +++ b/src/client/components/mention.vue @@ -1,27 +1,27 @@ <template> <router-link class="ldlomzub" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')"> - <span class="me" v-if="isMe">{{ $t('@.you') }}</span> + <span class="me" v-if="isMe">{{ $t('you') }}</span> <span class="main"> <span class="username">@{{ username }}</span> - <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span> + <span class="host" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span> </span> </router-link> <a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else> <span class="main"> <span class="username">@{{ username }}</span> - <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }">@{{ toUnicode(host) }}</span> + <span class="host">@{{ toUnicode(host) }}</span> </span> </a> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import i18n from '../i18n'; import { toUnicode } from 'punycode'; -import { host as localHost } from '../../../config'; +import { host as localHost } from '../config'; export default Vue.extend({ - i18n: i18n(), + i18n, props: { username: { type: String, @@ -62,26 +62,21 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.ldlomzub - color var(--mfmMention) - - > .me - pointer-events none - user-select none - padding 0 4px - background var(--mfmMention) - border solid var(--lineWidth) var(--mfmMention) - border-radius 4px 0 0 4px - color var(--mfmMentionForeground) - - & + .main - padding 0 4px - border solid var(--lineWidth) var(--mfmMention) - border-radius 0 4px 4px 0 - - > .main - > .host.fade - opacity 0.5 +<style lang="scss" scoped> +.ldlomzub { + color: var(--mention); + + > .me { + pointer-events: none; + user-select: none; + font-size: 70%; + vertical-align: top; + } + > .main { + > .host { + opacity: 0.5; + } + } +} </style> diff --git a/src/client/components/menu.vue b/src/client/components/menu.vue new file mode 100644 index 0000000000000000000000000000000000000000..c1c5ceaee721d82e30debbca0b5b2c251adea910 --- /dev/null +++ b/src/client/components/menu.vue @@ -0,0 +1,165 @@ +<template> +<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }"> + <sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction"> + <template v-for="(item, i) in items.filter(item => item !== undefined)"> + <div v-if="item === null" class="divider" :key="i" :data-index="i"></div> + <span v-else-if="item.type === 'label'" class="label item" :key="i" :data-index="i"> + <span>{{ item.text }}</span> + </span> + <router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i" :data-index="i"> + <fa v-if="item.icon" :icon="item.icon" fixed-width/> + <mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> + <span>{{ item.text }}</span> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </router-link> + <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i" :data-index="i"> + <fa v-if="item.icon" :icon="item.icon" fixed-width/> + <span>{{ item.text }}</span> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </a> + <button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i" :data-index="i"> + <mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </button> + <button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i" :data-index="i"> + <fa v-if="item.icon" :icon="item.icon" fixed-width/> + <mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> + <span>{{ item.text }}</span> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </button> + </template> + </sequential-entrance> +</x-popup> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCircle } from '@fortawesome/free-solid-svg-icons'; +import XPopup from './popup.vue'; + +export default Vue.extend({ + components: { + XPopup + }, + props: { + source: { + required: true + }, + items: { + type: Array, + required: true + }, + align: { + type: String, + required: false + }, + noCenter: { + type: Boolean, + required: false + }, + fixed: { + type: Boolean, + required: false + }, + width: { + type: Number, + required: false + }, + direction: { + type: String, + required: false + }, + }, + data() { + return { + faCircle + }; + }, + methods: { + clicked(fn) { + fn(); + this.close(); + }, + close() { + this.$refs.popup.close(); + } + } +}); +</script> + +<style lang="scss" scoped> +@keyframes blink { + 0% { opacity: 1; } + 30% { opacity: 1; } + 90% { opacity: 0; } +} + +.rrevdjwt { + padding: 8px 0; + + &.left { + > .item { + text-align: left; + } + } + + > .item { + display: block; + padding: 8px 16px; + width: 100%; + box-sizing: border-box; + white-space: nowrap; + font-size: 0.9em; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + color: #fff; + background: var(--accent); + text-decoration: none; + } + + &:active { + color: #fff; + background: var(--accentDarken); + } + + &.label { + pointer-events: none; + font-size: 0.7em; + padding-bottom: 4px; + + > span { + opacity: 0.7; + } + } + + > [data-icon] { + margin-right: 4px; + width: 20px; + } + + > .avatar { + margin-right: 4px; + width: 20px; + height: 20px; + } + + > i { + position: absolute; + top: 5px; + left: 13px; + color: var(--accent); + font-size: 12px; + animation: blink 1s infinite; + } + } + + > .divider { + margin: 8px 0; + height: 1px; + background: var(--divider); + } +} +</style> diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/components/mfm.ts similarity index 80% rename from src/client/app/common/views/components/mfm.ts rename to src/client/components/mfm.ts index 561c3d8e30e11408f5dd671e6a62b9e40ad66433..932beb907f5172ba2f756066bdaf16536f0bff3d 100644 --- a/src/client/app/common/views/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -1,20 +1,13 @@ import Vue, { VNode } from 'vue'; -import { length } from 'stringz'; -import { MfmForest } from '../../../../../mfm/types'; -import { parse, parsePlain } from '../../../../../mfm/parse'; +import { MfmForest } from '../../mfm/types'; +import { parse, parsePlain } from '../../mfm/parse'; import MkUrl from './url.vue'; import MkMention from './mention.vue'; -import { concat, sum } from '../../../../../prelude/array'; +import { concat } from '../../prelude/array'; import MkFormula from './formula.vue'; import MkCode from './code.vue'; import MkGoogle from './google.vue'; -import { host } from '../../../config'; -import { preorderF, countNodesF } from '../../../../../prelude/tree'; - -function sumTextsLength(ts: MfmForest): number { - const textNodes = preorderF(ts).filter(n => n.type === 'text'); - return sum(textNodes.map(x => length(x.props.text))); -} +import { host } from '../config'; export default Vue.component('misskey-flavored-markdown', { props: { @@ -52,9 +45,6 @@ export default Vue.component('misskey-flavored-markdown', { const ast = (this.plain ? parsePlain : parse)(this.text); - let bigCount = 0; - let motionCount = 0; - const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => { switch (token.node.type) { case 'text': { @@ -87,14 +77,11 @@ export default Vue.component('misskey-flavored-markdown', { } case 'big': { - bigCount++; - const isLong = sumTextsLength(token.children) > 15 || countNodesF(token.children) > 5; - const isMany = bigCount > 3; return (createElement as any)('strong', { attrs: { - style: `display: inline-block; font-size: ${ isMany ? '100%' : '150%' };` + style: `display: inline-block; font-size: 150% };` }, - directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : { + directives: [this.$store.state.settings.disableAnimatedMfm ? {} : { name: 'animate-css', value: { classes: 'tada', iteration: 'infinite' } }] @@ -118,14 +105,11 @@ export default Vue.component('misskey-flavored-markdown', { } case 'motion': { - motionCount++; - const isLong = sumTextsLength(token.children) > 15 || countNodesF(token.children) > 5; - const isMany = motionCount > 5; return (createElement as any)('span', { attrs: { style: 'display: inline-block;' }, - directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : { + directives: [this.$store.state.settings.disableAnimatedMfm ? {} : { name: 'animate-css', value: { classes: 'rubberBand', iteration: 'infinite' } }] @@ -133,14 +117,11 @@ export default Vue.component('misskey-flavored-markdown', { } case 'spin': { - motionCount++; - const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5; - const isMany = motionCount > 5; const direction = token.node.props.attr == 'left' ? 'reverse' : token.node.props.attr == 'alternate' ? 'alternate' : 'normal'; - const style = (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) + const style = (this.$store.state.settings.disableAnimatedMfm) ? '' : `animation: spin 1.5s linear infinite; animation-direction: ${direction};`; return (createElement as any)('span', { @@ -151,12 +132,9 @@ export default Vue.component('misskey-flavored-markdown', { } case 'jump': { - motionCount++; - const isLong = sumTextsLength(token.children) > 30 || countNodesF(token.children) > 5; - const isMany = motionCount > 5; return (createElement as any)('span', { attrs: { - style: (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;' + style: (this.$store.state.settings.disableAnimatedMfm) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;' }, }, genEl(token.children)); } @@ -177,7 +155,7 @@ export default Vue.component('misskey-flavored-markdown', { rel: 'nofollow noopener', }, attrs: { - style: 'color:var(--mfmUrl);' + style: 'color:var(--link);' } })]; } @@ -190,7 +168,7 @@ export default Vue.component('misskey-flavored-markdown', { rel: 'nofollow noopener', target: '_blank', title: token.node.props.url, - style: 'color:var(--mfmLink);' + style: 'color:var(--link);' } }, genEl(token.children))]; } @@ -210,7 +188,7 @@ export default Vue.component('misskey-flavored-markdown', { key: Math.random(), attrs: { to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, - style: 'color:var(--mfmHashtag);' + style: 'color:var(--hashtag);' } }, `#${token.node.props.hashtag}`)]; } diff --git a/src/client/components/misskey-flavored-markdown.vue b/src/client/components/misskey-flavored-markdown.vue new file mode 100644 index 0000000000000000000000000000000000000000..c8eee8c1269d8c8361e2fc57ce86f21ca1a4bddb --- /dev/null +++ b/src/client/components/misskey-flavored-markdown.vue @@ -0,0 +1,35 @@ +<template> +<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import MfmCore from './mfm'; + +export default Vue.extend({ + components: { + MfmCore + } +}); +</script> + +<style lang="scss" scoped> +.havbbuyv { + white-space: pre-wrap; + + &.nowrap { + white-space: pre; + word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html + overflow: hidden; + text-overflow: ellipsis; + } + + ::v-deep .quote { + display: block; + margin: 8px; + padding: 6px 0 6px 12px; + color: var(--mfmQuote); + border-left: solid 3px var(--mfmQuoteLine); + } +} +</style> diff --git a/src/client/components/modal.vue b/src/client/components/modal.vue new file mode 100644 index 0000000000000000000000000000000000000000..b7e6a336d751968918418f148cd5837cc5842904 --- /dev/null +++ b/src/client/components/modal.vue @@ -0,0 +1,84 @@ +<template> +<div class="mk-modal"> + <transition name="bg-fade" appear> + <div class="bg" ref="bg" v-if="show" @click="close()"></div> + </transition> + <transition name="modal" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> + <div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + }, + data() { + return { + show: true, + }; + }, + methods: { + close() { + this.show = false; + (this.$refs.bg as any).style.pointerEvents = 'none'; + (this.$refs.content as any).style.pointerEvents = 'none'; + } + } +}); +</script> + +<style lang="scss" scoped> +.modal-enter-active, .modal-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.modal-enter, .modal-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.bg-fade-enter-active, .bg-fade-leave-active { + transition: opacity 0.3s !important; +} +.bg-fade-enter, .bg-fade-leave-to { + opacity: 0; +} + +.mk-modal { + > .bg { + position: fixed; + top: 0; + left: 0; + z-index: 10000; + width: 100%; + height: 100%; + background: var(--modalBg) + } + + > .content { + position: fixed; + z-index: 10000; + top: 0; + bottom: 0; + left: 0; + right: 0; + max-width: calc(100% - 16px); + max-height: calc(100% - 16px); + overflow: auto; + margin: auto; + + ::v-deep > * { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + } + } +} +</style> diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue new file mode 100644 index 0000000000000000000000000000000000000000..30ecb80834c220fab9da9aafe4f5a74708f6cea8 --- /dev/null +++ b/src/client/components/note-header.vue @@ -0,0 +1,99 @@ +<template> +<header class="kkwtjztg"> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"> + <mk-user-name :user="note.user"/> + </router-link> + <span class="is-bot" v-if="note.user.isBot">bot</span> + <span class="username"><mk-acct :user="note.user"/></span> + <div class="info"> + <span class="mobile" v-if="note.viaMobile"><fa :icon="faMobileAlt"/></span> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + <span class="visibility" v-if="note.visibility != 'public'"> + <fa v-if="note.visibility == 'home'" :icon="faHome"/> + <fa v-if="note.visibility == 'followers'" :icon="faUnlock"/> + <fa v-if="note.visibility == 'specified'" :icon="faEnvelope"/> + </span> + </div> +</header> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faHome, faUnlock, faEnvelope, faMobileAlt } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + props: { + note: { + type: Object, + required: true + }, + }, + + data() { + return { + faHome, faUnlock, faEnvelope, faMobileAlt + }; + } +}); +</script> + +<style lang="scss" scoped> +.kkwtjztg { + display: flex; + align-items: baseline; + white-space: nowrap; + + > .name { + display: block; + margin: 0 .5em 0 0; + padding: 0; + overflow: hidden; + color: var(--noteHeaderName); + font-size: 1em; + font-weight: bold; + text-decoration: none; + text-overflow: ellipsis; + + &:hover { + text-decoration: underline; + } + } + + > .is-bot { + flex-shrink: 0; + align-self: center; + margin: 0 .5em 0 0; + padding: 1px 6px; + font-size: 80%; + color: var(--noteHeaderBadgeFg); + background: var(--noteHeaderBadgeBg); + border-radius: 3px; + } + + > .username { + margin: 0 .5em 0 0; + overflow: hidden; + text-overflow: ellipsis; + color: var(--noteHeaderAcct); + } + + > .info { + margin-left: auto; + font-size: 0.9em; + + > * { + color: var(--noteHeaderInfo); + } + + > .mobile { + margin-right: 8px; + } + + > .visibility { + margin-left: 8px; + } + } +} +</style> diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/components/note-menu.vue similarity index 61% rename from src/client/app/common/views/components/note-menu.vue rename to src/client/components/note-menu.vue index 1dcf58dd36b9097193e8dbcd8d3874420b07a12b..dd7b062f159cc1cccddd895be32a7284d75383ce 100644 --- a/src/client/app/common/views/components/note-menu.vue +++ b/src/client/components/note-menu.vue @@ -1,18 +1,21 @@ <template> -<div style="position:initial"> - <mk-menu :source="source" :items="items" @closed="closed"/> -</div> +<x-menu :source="source" :items="items" @closed="closed"/> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; -import { url } from '../../../config'; -import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; -import { faCopy, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import { faStar, faLink, faThumbtack, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import { url } from '../config'; +import copyToClipboard from '../scripts/copy-to-clipboard'; +import XMenu from './menu.vue'; export default Vue.extend({ - i18n: i18n('common/views/components/note-menu.vue'), + i18n, + components: { + XMenu + }, props: ['note', 'source'], data() { return { @@ -24,35 +27,27 @@ export default Vue.extend({ items(): any[] { if (this.$store.getters.isSignedIn) { return [{ - icon: 'at', - text: this.$t('mention'), - action: this.mention - }, null, { - icon: 'info-circle', - text: this.$t('detail'), - action: this.detail - }, { icon: faCopy, - text: this.$t('copy-content'), + text: this.$t('copyContent'), action: this.copyContent }, { - icon: 'link', - text: this.$t('copy-link'), + icon: faLink, + text: this.$t('copyLink'), action: this.copyLink }, this.note.uri ? { - icon: 'external-link-square-alt', - text: this.$t('remote'), + icon: faExternalLinkSquareAlt, + text: this.$t('showOnRemote'), action: () => { window.open(this.note.uri, '_blank'); } } : undefined, null, this.isFavorited ? { - icon: 'star', + icon: faStar, text: this.$t('unfavorite'), action: () => this.toggleFavorite(false) } : { - icon: 'star', + icon: faStar, text: this.$t('favorite'), action: () => this.toggleFavorite(true) }, @@ -66,23 +61,18 @@ export default Vue.extend({ action: () => this.toggleWatch(true) } : undefined, this.note.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.note.id) ? { - icon: 'thumbtack', + icon: faThumbtack, text: this.$t('unpin'), action: () => this.togglePin(false) } : { - icon: 'thumbtack', + icon: faThumbtack, text: this.$t('pin'), action: () => this.togglePin(true) } : undefined, - ...(this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? [ + ...(this.note.userId == this.$store.state.i.id ? [ null, - this.note.userId == this.$store.state.i.id ? { - icon: 'undo-alt', - text: this.$t('delete-and-edit'), - action: this.deleteAndEdit - } : undefined, { - icon: ['far', 'trash-alt'], + icon: faTrashAlt, text: this.$t('delete'), action: this.del }] @@ -91,20 +81,16 @@ export default Vue.extend({ .filter(x => x !== undefined); } else { return [{ - icon: 'info-circle', - text: this.$t('detail'), - action: this.detail - }, { icon: faCopy, - text: this.$t('copy-content'), + text: this.$t('copyContent'), action: this.copyContent }, { - icon: 'link', - text: this.$t('copy-link'), + icon: faLink, + text: this.$t('copyLink'), action: this.copyLink }, this.note.uri ? { - icon: 'external-link-square-alt', - text: this.$t('remote'), + icon: faExternalLinkSquareAlt, + text: this.$t('showOnRemote'), action: () => { window.open(this.note.uri, '_blank'); } @@ -124,19 +110,11 @@ export default Vue.extend({ }, methods: { - mention() { - this.$post({ mention: this.note.user }); - }, - - detail() { - this.$router.push(`/notes/${this.note.id}`); - }, - copyContent() { copyToClipboard(this.note.text); this.$root.dialog({ type: 'success', - splash: true + iconOnly: true, autoClose: true }); }, @@ -144,7 +122,7 @@ export default Vue.extend({ copyToClipboard(`${url}/notes/${this.note.id}`); this.$root.dialog({ type: 'success', - splash: true + iconOnly: true, autoClose: true }); }, @@ -154,14 +132,15 @@ export default Vue.extend({ }).then(() => { this.$root.dialog({ type: 'success', - splash: true + iconOnly: true, autoClose: true }); + this.$emit('closed'); this.destroyDom(); }).catch(e => { if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { this.$root.dialog({ type: 'error', - text: this.$t('pin-limit-exceeded') + text: this.$t('pinLimitExceeded') }); } }); @@ -170,7 +149,7 @@ export default Vue.extend({ del() { this.$root.dialog({ type: 'warning', - text: this.$t('delete-confirm'), + text: this.$t('noteDeleteConfirm'), showCancelButton: true }).then(({ canceled }) => { if (canceled) return; @@ -178,38 +157,21 @@ export default Vue.extend({ this.$root.api('notes/delete', { noteId: this.note.id }).then(() => { + this.$emit('closed'); this.destroyDom(); }); }); }, - deleteAndEdit() { - this.$root.dialog({ - type: 'warning', - text: this.$t('delete-and-edit-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.$root.api('notes/delete', { - noteId: this.note.id - }).then(() => { - this.destroyDom(); - }); - this.$post({ - initialNote: this.note, - reply: this.note.reply, - }); - }); - }, - toggleFavorite(favorite: boolean) { this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { noteId: this.note.id }).then(() => { this.$root.dialog({ type: 'success', - splash: true + iconOnly: true, autoClose: true }); + this.$emit('closed'); this.destroyDom(); }); }, @@ -220,7 +182,7 @@ export default Vue.extend({ }).then(() => { this.$root.dialog({ type: 'success', - splash: true + iconOnly: true, autoClose: true }); this.destroyDom(); }); diff --git a/src/client/components/note-preview.vue b/src/client/components/note-preview.vue new file mode 100644 index 0000000000000000000000000000000000000000..17ff5be8683421a393b863d830f306f4b3dbb304 --- /dev/null +++ b/src/client/components/note-preview.vue @@ -0,0 +1,121 @@ +<template> +<div class="yohlumlkhizgfkvvscwfcrcggkotpvry"> + <mk-avatar class="avatar" :user="note.user"/> + <div class="main"> + <x-note-header class="header" :note="note" :mini="true"/> + <div class="body"> + <p v-if="note.cw != null" class="cw"> + <span class="text" v-if="note.cw != ''">{{ note.cw }}</span> + <x-cw-button v-model="showContent" :note="note"/> + </p> + <div class="content" v-show="note.cw == null || showContent"> + <x-sub-note-content class="text" :note="note"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNoteHeader from './note-header.vue'; +import XSubNoteContent from './sub-note-content.vue'; +import XCwButton from './cw-button.vue'; + +export default Vue.extend({ + components: { + XNoteHeader, + XSubNoteContent, + XCwButton, + }, + + props: { + note: { + type: Object, + required: true + } + }, + + data() { + return { + showContent: false + }; + } +}); +</script> + +<style lang="scss" scoped> +.yohlumlkhizgfkvvscwfcrcggkotpvry { + display: flex; + margin: 0; + padding: 0; + overflow: hidden; + font-size: 10px; + + @media (min-width: 350px) { + font-size: 12px; + } + + @media (min-width: 500px) { + font-size: 14px; + } + + > .avatar { + + @media (min-width: 350px) { + margin: 0 10px 0 0; + width: 44px; + height: 44px; + } + + @media (min-width: 500px) { + margin: 0 12px 0 0; + width: 48px; + height: 48px; + } + } + + > .avatar { + flex-shrink: 0; + display: block; + margin: 0 10px 0 0; + width: 40px; + height: 40px; + border-radius: 8px; + } + + > .main { + flex: 1; + min-width: 0; + + > .header { + margin-bottom: 2px; + } + + > .body { + + > .cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; + color: var(--noteText); + + > .text { + margin-right: 8px; + } + } + + > .content { + > .text { + cursor: default; + margin: 0; + padding: 0; + color: var(--subNoteText); + } + } + } + } +} +</style> diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue new file mode 100644 index 0000000000000000000000000000000000000000..7f6f97289655d6e59c6093ef0b6827fb8081f2cb --- /dev/null +++ b/src/client/components/note.sub.vue @@ -0,0 +1,108 @@ +<template> +<div class="zlrxdaqttccpwhpaagdmkawtzklsccam"> + <mk-avatar class="avatar" :user="note.user"/> + <div class="main"> + <x-note-header class="header" :note="note" :mini="true"/> + <div class="body"> + <p v-if="note.cw != null" class="cw"> + <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> + <x-cw-button v-model="showContent" :note="note"/> + </p> + <div class="content" v-show="note.cw == null || showContent"> + <x-sub-note-content class="text" :note="note"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNoteHeader from './note-header.vue'; +import XSubNoteContent from './sub-note-content.vue'; +import XCwButton from './cw-button.vue'; + +export default Vue.extend({ + components: { + XNoteHeader, + XSubNoteContent, + XCwButton, + }, + + props: { + note: { + type: Object, + required: true + }, + // TODO + truncate: { + type: Boolean, + default: true + } + }, + + inject: { + narrow: { + default: false + } + }, + + data() { + return { + showContent: false + }; + } +}); +</script> + +<style lang="scss" scoped> +.zlrxdaqttccpwhpaagdmkawtzklsccam { + display: flex; + padding: 16px 32px; + font-size: 0.9em; + background: rgba(0, 0, 0, 0.03); + + @media (max-width: 450px) { + padding: 14px 16px; + } + + > .avatar { + flex-shrink: 0; + display: block; + margin: 0 8px 0 0; + width: 38px; + height: 38px; + border-radius: 8px; + } + + > .main { + flex: 1; + min-width: 0; + + > .header { + margin-bottom: 2px; + } + + > .body { + > .cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; + + > .text { + margin-right: 8px; + } + } + + > .content { + > .text { + margin: 0; + padding: 0; + } + } + } + } +} +</style> diff --git a/src/client/components/note.vue b/src/client/components/note.vue new file mode 100644 index 0000000000000000000000000000000000000000..8b3fa61a6533a446d600814e4f3f8467e80ca80f --- /dev/null +++ b/src/client/components/note.vue @@ -0,0 +1,729 @@ +<template> +<div + class="note _panel" + v-show="appearNote.deletedAt == null && !hideThisNote" + :tabindex="appearNote.deletedAt == null ? '-1' : null" + :class="{ renote: isRenote }" + v-hotkey="keymap" + v-size="[{ max: 500 }, { max: 450 }, { max: 350 }, { max: 300 }]" +> + <x-sub v-for="note in conversation" :key="note.id" :note="note"/> + <x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> + <div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div> + <div class="renote" v-if="isRenote"> + <mk-avatar class="avatar" :user="note.user"/> + <fa :icon="faRetweet"/> + <i18n path="renotedBy" tag="span"> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user"> + <mk-user-name :user="note.user"/> + </router-link> + </i18n> + <div class="info"> + <mk-time :time="note.createdAt"/> + <span class="visibility" v-if="note.visibility != 'public'"> + <fa v-if="note.visibility == 'home'" :icon="faHome"/> + <fa v-if="note.visibility == 'followers'" :icon="faUnlock"/> + <fa v-if="note.visibility == 'specified'" :icon="faEnvelope"/> + </span> + </div> + </div> + <article class="article"> + <mk-avatar class="avatar" :user="appearNote.user"/> + <div class="main"> + <x-note-header class="header" :note="appearNote" :mini="true"/> + <div class="body" v-if="appearNote.deletedAt == null"> + <p v-if="appearNote.cw != null" class="cw"> + <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> + <x-cw-button v-model="showContent" :note="appearNote"/> + </p> + <div class="content" v-show="appearNote.cw == null || showContent"> + <div class="text"> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> + <router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link> + <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> + <a class="rp" v-if="appearNote.renote != null">RN:</a> + </div> + <div class="files" v-if="appearNote.files.length > 0"> + <x-media-list :media-list="appearNote.files"/> + </div> + <x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> + <x-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" class="url-preview"/> + <div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div> + </div> + </div> + <footer v-if="appearNote.deletedAt == null" class="footer"> + <x-reactions-viewer :note="appearNote" ref="reactionsViewer"/> + <button @click="reply()" class="button _button"> + <template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template> + <template v-else><fa :icon="faReply"/></template> + <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> + </button> + <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" class="button _button" ref="renoteButton"> + <fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> + </button> + <button v-else class="button _button"> + <fa :icon="faBan"/> + </button> + <button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> + <fa :icon="faPlus"/> + </button> + <button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton"> + <fa :icon="faMinus"/> + </button> + <button class="button _button" @click="menu()" ref="menuButton"> + <fa :icon="faEllipsisH"/> + </button> + </footer> + <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> + </div> + </article> + <x-sub v-for="note in replies" :key="note.id" :note="note"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan } from '@fortawesome/free-solid-svg-icons'; +import { parse } from '../../mfm/parse'; +import { sum, unique } from '../../prelude/array'; +import i18n from '../i18n'; +import XSub from './note.sub.vue'; +import XNoteHeader from './note-header.vue'; +import XNotePreview from './note-preview.vue'; +import XReactionsViewer from './reactions-viewer.vue'; +import XMediaList from './media-list.vue'; +import XCwButton from './cw-button.vue'; +import XPoll from './poll.vue'; +import XUrlPreview from './url-preview.vue'; +import MkNoteMenu from './note-menu.vue'; +import MkReactionPicker from './reaction-picker.vue'; +import MkRenotePicker from './renote-picker.vue'; +import pleaseLogin from '../scripts/please-login'; + +function focus(el, fn) { + const target = fn(el); + if (target) { + if (target.hasAttribute('tabindex')) { + target.focus(); + } else { + focus(target, fn); + } + } +} + +export default Vue.extend({ + i18n, + + components: { + XSub, + XNoteHeader, + XNotePreview, + XReactionsViewer, + XMediaList, + XCwButton, + XPoll, + XUrlPreview, + }, + + props: { + note: { + type: Object, + required: true + }, + detail: { + type: Boolean, + required: false, + default: false + }, + pinned: { + type: Boolean, + required: false, + default: false + }, + }, + + data() { + return { + connection: null, + conversation: [], + replies: [], + showContent: false, + hideThisNote: false, + openingMenu: false, + faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan + }; + }, + + computed: { + keymap(): any { + return { + 'r': () => this.reply(true), + 'e|a|plus': () => this.react(true), + 'q': () => this.renote(true), + 'f|b': this.favorite, + 'delete|ctrl+d': this.del, + 'ctrl+q': this.renoteDirectly, + 'up|k|shift+tab': this.focusBefore, + 'down|j|tab': this.focusAfter, + 'esc': this.blur, + 'm|o': () => this.menu(true), + 's': this.toggleShowContent, + '1': () => this.reactDirectly(this.$store.state.settings.reactions[0]), + '2': () => this.reactDirectly(this.$store.state.settings.reactions[1]), + '3': () => this.reactDirectly(this.$store.state.settings.reactions[2]), + '4': () => this.reactDirectly(this.$store.state.settings.reactions[3]), + '5': () => this.reactDirectly(this.$store.state.settings.reactions[4]), + '6': () => this.reactDirectly(this.$store.state.settings.reactions[5]), + '7': () => this.reactDirectly(this.$store.state.settings.reactions[6]), + '8': () => this.reactDirectly(this.$store.state.settings.reactions[7]), + '9': () => this.reactDirectly(this.$store.state.settings.reactions[8]), + '0': () => this.reactDirectly(this.$store.state.settings.reactions[9]), + }; + }, + + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.fileIds.length == 0 && + this.note.poll == null); + }, + + appearNote(): any { + return this.isRenote ? this.note.renote : this.note; + }, + + isMyNote(): boolean { + return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId); + }, + + reactionsCount(): number { + return this.appearNote.reactions + ? sum(Object.values(this.appearNote.reactions)) + : 0; + }, + + title(): string { + return ''; + }, + + urls(): string[] { + if (this.appearNote.text) { + const ast = parse(this.appearNote.text); + // TODO: å†å¸°çš„ã«URLè¦ç´ ãŒãªã„ã‹èª¿ã¹ã‚‹ + const urls = unique(ast + .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) + .map(t => t.node.props.url)); + + // unique without hash + // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] + const removeHash = x => x.replace(/#[^#]*$/, ''); + + return urls.reduce((array, url) => { + const removed = removeHash(url); + if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); + return array; + }, []); + } else { + return null; + } + } + }, + + created() { + if (this.$store.getters.isSignedIn) { + this.connection = this.$root.stream; + } + + if (this.detail) { + this.$root.api('notes/children', { + noteId: this.appearNote.id, + limit: 30 + }).then(replies => { + this.replies = replies; + }); + + if (this.appearNote.replyId) { + this.$root.api('notes/conversation', { + noteId: this.appearNote.replyId + }).then(conversation => { + this.conversation = conversation.reverse(); + }); + } + } + }, + + mounted() { + this.capture(true); + + if (this.$store.getters.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + }, + + beforeDestroy() { + this.decapture(true); + + if (this.$store.getters.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + } + }, + + methods: { + capture(withHandler = false) { + if (this.$store.getters.isSignedIn) { + this.connection.send('sn', { id: this.appearNote.id }); + if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); + } + }, + + decapture(withHandler = false) { + if (this.$store.getters.isSignedIn) { + this.connection.send('un', { + id: this.appearNote.id + }); + if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); + } + }, + + onStreamConnected() { + this.capture(); + }, + + onStreamNoteUpdated(data) { + const { type, id, body } = data; + + if (id !== this.appearNote.id) return; + + switch (type) { + case 'reacted': { + const reaction = body.reaction; + + if (this.appearNote.reactions == null) { + Vue.set(this.appearNote, 'reactions', {}); + } + + if (this.appearNote.reactions[reaction] == null) { + Vue.set(this.appearNote.reactions, reaction, 0); + } + + // Increment the count + this.appearNote.reactions[reaction]++; + + if (body.userId == this.$store.state.i.id) { + Vue.set(this.appearNote, 'myReaction', reaction); + } + break; + } + + case 'unreacted': { + const reaction = body.reaction; + + if (this.appearNote.reactions == null) { + return; + } + + if (this.appearNote.reactions[reaction] == null) { + return; + } + + // Decrement the count + if (this.appearNote.reactions[reaction] > 0) this.appearNote.reactions[reaction]--; + + if (body.userId == this.$store.state.i.id) { + Vue.set(this.appearNote, 'myReaction', null); + } + break; + } + + case 'pollVoted': { + const choice = body.choice; + this.appearNote.poll.choices[choice].votes++; + if (body.userId == this.$store.state.i.id) { + Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true); + } + break; + } + + case 'deleted': { + Vue.set(this.appearNote, 'deletedAt', body.deletedAt); + Vue.set(this.appearNote, 'renote', null); + this.appearNote.text = null; + this.appearNote.fileIds = []; + this.appearNote.poll = null; + this.appearNote.cw = null; + break; + } + } + }, + + reply(viaKeyboard = false) { + pleaseLogin(this.$root); + this.$root.post({ + reply: this.appearNote, + animation: !viaKeyboard, + }, () => { + this.focus(); + }); + }, + + renote() { + pleaseLogin(this.$root); + this.blur(); + this.$root.new(MkRenotePicker, { + source: this.$refs.renoteButton, + note: this.appearNote, + }).$once('closed', this.focus); + }, + + renoteDirectly() { + (this as any).$root.api('notes/create', { + renoteId: this.appearNote.id + }); + }, + + react(viaKeyboard = false) { + pleaseLogin(this.$root); + this.blur(); + const picker = this.$root.new(MkReactionPicker, { + source: this.$refs.reactButton, + showFocus: viaKeyboard, + }); + picker.$once('chosen', reaction => { + this.$root.api('notes/reactions/create', { + noteId: this.appearNote.id, + reaction: reaction + }).then(() => { + picker.close(); + }); + }); + picker.$once('closed', this.focus); + }, + + reactDirectly(reaction) { + this.$root.api('notes/reactions/create', { + noteId: this.appearNote.id, + reaction: reaction + }); + }, + + undoReact(note) { + const oldReaction = note.myReaction; + if (!oldReaction) return; + this.$root.api('notes/reactions/delete', { + noteId: note.id + }); + }, + + favorite() { + pleaseLogin(this.$root); + this.$root.api('notes/favorites/create', { + noteId: this.appearNote.id + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + }, + + del() { + this.$root.dialog({ + type: 'warning', + text: this.$t('noteDeleteConfirm'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + + this.$root.api('notes/delete', { + noteId: this.appearNote.id + }); + }); + }, + + menu(viaKeyboard = false) { + if (this.openingMenu) return; + this.openingMenu = true; + const w = this.$root.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.appearNote, + animation: !viaKeyboard + }).$once('closed', () => { + this.openingMenu = false; + this.focus(); + }); + }, + + toggleShowContent() { + this.showContent = !this.showContent; + }, + + focus() { + this.$el.focus(); + }, + + blur() { + this.$el.blur(); + }, + + focusBefore() { + focus(this.$el, e => e.previousElementSibling); + }, + + focusAfter() { + focus(this.$el, e => e.nextElementSibling); + } + } +}); +</script> + +<style lang="scss" scoped> +.note { + position: relative; + transition: box-shadow 0.1s ease; + + &.max-width_500px { + font-size: 0.9em; + } + + &.max-width_450px { + > .renote { + padding: 8px 16px 0 16px; + } + + > .article { + padding: 14px 16px 9px; + + > .avatar { + margin: 0 10px 8px 0; + width: 50px; + height: 50px; + } + } + } + + &.max-width_350px { + > .article { + > .main { + > .footer { + > .button { + &:not(:last-child) { + margin-right: 18px; + } + } + } + } + } + } + + &.max-width_300px { + font-size: 0.825em; + + > .article { + > .avatar { + width: 44px; + height: 44px; + } + + > .main { + > .footer { + > .button { + &:not(:last-child) { + margin-right: 12px; + } + } + } + } + } + } + + &:focus { + outline: none; + box-shadow: 0 0 0 3px var(--focus); + } + + &:hover > .article > .main > .footer > .button { + opacity: 1; + } + + > *:first-child { + border-radius: var(--radius) var(--radius) 0 0; + } + + > *:last-child { + border-radius: 0 0 var(--radius) var(--radius); + } + + > .pinned { + padding: 16px 32px 8px 32px; + line-height: 24px; + font-size: 90%; + white-space: pre; + color: #d28a3f; + + @media (max-width: 450px) { + padding: 8px 16px 0 16px; + } + + > [data-icon] { + margin-right: 4px; + } + } + + > .pinned + .article { + padding-top: 8px; + } + + > .renote { + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 28px; + white-space: pre; + color: var(--renote); + + > .avatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; + border-radius: 6px; + } + + > [data-icon] { + margin-right: 4px; + } + + > span { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; + + > .name { + font-weight: bold; + } + } + + > .info { + margin-left: auto; + font-size: 0.9em; + + > .mk-time { + flex-shrink: 0; + } + + > .visibility { + margin-left: 8px; + + [data-icon] { + margin-right: 0; + } + } + } + } + + > .renote + .article { + padding-top: 8px; + } + + > .article { + display: flex; + padding: 28px 32px 18px; + + > .avatar { + flex-shrink: 0; + display: block; + //position: sticky; + //top: 72px; + margin: 0 14px 8px 0; + width: 58px; + height: 58px; + } + + > .main { + flex: 1; + min-width: 0; + + > .body { + > .cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; + + > .text { + margin-right: 8px; + } + } + + > .content { + > .text { + overflow-wrap: break-word; + + > .reply { + color: var(--accent); + margin-right: 0.5em; + } + + > .rp { + margin-left: 4px; + font-style: oblique; + color: var(--renote); + } + } + + > .url-preview { + margin-top: 8px; + } + + > .mk-poll { + font-size: 80%; + } + + > .renote { + padding: 8px 0; + + > * { + padding: 16px; + border: dashed 1px var(--renote); + border-radius: 8px; + } + } + } + } + + > .footer { + > .button { + margin: 0; + padding: 8px; + opacity: 0.7; + + &:not(:last-child) { + margin-right: 28px; + } + + &:hover { + color: var(--mkykhqkw); + } + + > .count { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; + } + + &.reacted { + color: var(--accent); + } + } + } + + > .deleted { + opacity: 0.7; + } + } + } +} +</style> diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue new file mode 100644 index 0000000000000000000000000000000000000000..7cf2aa2b02a8b08ed8284e4e3d13ff68706133b0 --- /dev/null +++ b/src/client/components/notes.vue @@ -0,0 +1,144 @@ +<template> +<div class="mk-notes" v-size="[{ max: 500 }]"> + <div class="empty" v-if="empty">{{ $t('noNotes') }}</div> + + <mk-error v-if="error" @retry="init()"/> + + <x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note, i }"> + <x-note :note="note" :detail="detail" :key="note.id" :data-index="i"/> + </x-list> + + <footer v-if="more"> + <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" class="_buttonPrimary"> + <template v-if="!moreFetching">{{ $t('loadMore') }}</template> + <template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template> + </button> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import paging from '../scripts/paging'; +import XNote from './note.vue'; +import XList from './date-separated-list.vue'; +import getUserName from '../../misc/get-user-name'; +import getNoteSummary from '../../misc/get-note-summary'; + +export default Vue.extend({ + i18n, + + components: { + XNote, XList + }, + + mixins: [ + paging({ + onPrepend: (self, note) => { + // タブãŒéžè¡¨ç¤ºãªã‚‰é€šçŸ¥ + if (document.hidden) { + if ('Notification' in window && Notification.permission === 'granted') { + new Notification(getUserName(note.user), { + body: getNoteSummary(note), + icon: note.user.avatarUrl, + tag: 'newNote' + }); + } + } + }, + + before: (self) => { + self.$emit('before'); + }, + + after: (self, e) => { + self.$emit('after', e); + } + }), + ], + + props: { + pagination: { + required: true + }, + + detail: { + type: Boolean, + required: false, + default: false + }, + + extract: { + required: false + } + }, + + data() { + return { + faSpinner + }; + }, + + computed: { + notes(): any[] { + return this.extract ? this.extract(this.items) : this.items; + }, + }, + + methods: { + focus() { + this.$refs.notes.focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-notes { + > .empty { + margin: 0 auto; + padding: 32px; + text-align: center; + background: rgba(0, 0, 0, 0.3); + color: #fff; + -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(16px); + border-radius: 6px; + } + + > .notes { + > ::v-deep * { + margin-bottom: var(--marginFull); + } + } + + &.max-width_500px { + > .notes { + > ::v-deep * { + margin-bottom: var(--marginHalf); + } + } + } + + > footer { + text-align: center; + + &:empty { + display: none; + } + + > button { + margin: 0; + padding: 16px; + width: 100%; + border-radius: var(--radius); + + &:disabled { + opacity: 0.7; + } + } + } +} +</style> diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue new file mode 100644 index 0000000000000000000000000000000000000000..e325f0adb69c45074a42cb62dd0cacc4ddb7aa5b --- /dev/null +++ b/src/client/components/notification.vue @@ -0,0 +1,219 @@ +<template> +<div class="mk-notification" :class="notification.type"> + <div class="head"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="icon" :class="notification.type"> + <fa :icon="faPlus" v-if="notification.type === 'follow'"/> + <fa :icon="faClock" v-if="notification.type === 'receiveFollowRequest'"/> + <fa :icon="faCheck" v-if="notification.type === 'followRequestAccepted'"/> + <fa :icon="faRetweet" v-if="notification.type === 'renote'"/> + <fa :icon="faReply" v-if="notification.type === 'reply'"/> + <fa :icon="faAt" v-if="notification.type === 'mention'"/> + <fa :icon="faQuoteLeft" v-if="notification.type === 'quote'"/> + <x-reaction-icon v-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> + </div> + </div> + <div class="tail"> + <header> + <router-link class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link> + <mk-time :time="notification.createdAt" v-if="withTime"/> + </header> + <router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> + <fa :icon="faQuoteLeft"/> + <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/> + <fa :icon="faQuoteRight"/> + </router-link> + <router-link v-if="notification.type === 'renote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> + <fa :icon="faQuoteLeft"/> + <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.renote.emojis"/> + <fa :icon="faQuoteRight"/> + </router-link> + <router-link v-if="notification.type === 'reply'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> + <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/> + </router-link> + <router-link v-if="notification.type === 'mention'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> + <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/> + </router-link> + <router-link v-if="notification.type === 'quote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> + <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/> + </router-link> + <span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}</span> + <span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span> + <span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="!nowrap && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { faClock } from '@fortawesome/free-regular-svg-icons'; +import getNoteSummary from '../../misc/get-note-summary'; +import XReactionIcon from './reaction-icon.vue'; + +export default Vue.extend({ + components: { + XReactionIcon + }, + props: { + notification: { + type: Object, + required: true, + }, + withTime: { + type: Boolean, + required: false, + default: false, + }, + nowrap: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + getNoteSummary, + followRequestDone: false, + faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faClock, faCheck + }; + }, + methods: { + acceptFollowRequest() { + this.followRequestDone = true; + this.$root.api('following/requests/accept', { userId: this.notification.user.id }); + }, + rejectFollowRequest() { + this.followRequestDone = true; + this.$root.api('following/requests/reject', { userId: this.notification.user.id }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-notification { + position: relative; + box-sizing: border-box; + padding: 16px; + font-size: 0.9em; + overflow-wrap: break-word; + display: flex; + + @media (max-width: 500px) { + padding: 12px; + font-size: 0.8em; + } + + &:after { + content: ""; + display: block; + clear: both; + } + + > .head { + position: sticky; + top: 0; + flex-shrink: 0; + width: 42px; + height: 42px; + margin-right: 8px; + + > .avatar { + display: block; + width: 100%; + height: 100%; + border-radius: 6px; + } + + > .icon { + position: absolute; + z-index: 1; + bottom: -2px; + right: -2px; + width: 20px; + height: 20px; + box-sizing: border-box; + border-radius: 100%; + background: var(--panel); + box-shadow: 0 0 0 3px var(--panel); + font-size: 12px; + pointer-events: none; + + > * { + color: #fff; + width: 100%; + height: 100%; + } + + &.follow, &.followRequestAccepted, &.receiveFollowRequest { + padding: 3px; + background: #36aed2; + } + + &.retweet { + padding: 3px; + background: #36d298; + } + + &.quote { + padding: 3px; + background: #36d298; + } + + &.reply { + padding: 3px; + background: #007aff; + } + + &.mention { + padding: 3px; + background: #88a6b7; + } + } + } + + > .tail { + flex: 1; + min-width: 0; + + > header { + display: flex; + align-items: baseline; + white-space: nowrap; + + > .name { + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + overflow: hidden; + } + + > .mk-time { + margin-left: auto; + font-size: 0.9em; + } + } + + > .text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > [data-icon] { + vertical-align: super; + font-size: 50%; + opacity: 0.5; + } + + > [data-icon]:first-child { + margin-right: 4px; + } + + > [data-icon]:last-child { + margin-left: 4px; + } + } + } +} +</style> diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue new file mode 100644 index 0000000000000000000000000000000000000000..ad82913380158e09ac5fae915fc949924c96e24a --- /dev/null +++ b/src/client/components/notifications.vue @@ -0,0 +1,136 @@ +<template> +<div class="mk-notifications"> + <div class="contents"> + <x-list class="notifications" :items="items" v-slot="{ item: notification, i }"> + <x-notification :notification="notification" :with-time="true" :nowrap="false" class="notification" :key="notification.id" :data-index="i"/> + </x-list> + + <button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching"> + <template v-if="!moreFetching">{{ $t('loadMore') }}</template> + <template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template> + </button> + + <p class="empty" v-if="empty">{{ $t('noNotifications') }}</p> + + <mk-error v-if="error" @retry="init()"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import paging from '../scripts/paging'; +import XNotification from './notification.vue'; +import XList from './date-separated-list.vue'; + +export default Vue.extend({ + i18n, + + components: { + XNotification, + XList, + }, + + mixins: [ + paging({}), + ], + + props: { + type: { + type: String, + required: false + }, + wide: { + type: Boolean, + required: false, + default: false + } + }, + + data() { + return { + connection: null, + pagination: { + endpoint: 'i/notifications', + limit: 10, + params: () => ({ + includeTypes: this.type ? [this.type] : undefined + }) + }, + faSpinner + }; + }, + + watch: { + type() { + this.reload(); + } + }, + + mounted() { + this.connection = this.$root.stream.useSharedConnection('main'); + this.connection.on('notification', this.onNotification); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + + methods: { + onNotification(notification) { + // TODO: ユーザーãŒç”»é¢ã‚’見ã¦ãªã„ã¨æ€ã‚れるã¨ã(ブラウザやタブãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã˜ã‚ƒãªã„ãªã©)ã¯é€ä¿¡ã—ãªã„ + this.$root.stream.send('readNotification', { + id: notification.id + }); + + this.prepend(notification); + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-notifications { + > .contents { + overflow: auto; + height: 100%; + padding: 8px 8px 0 8px; + + > .notifications { + > ::v-deep * { + margin-bottom: 8px; + } + + > .notification { + background: var(--panel); + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + } + + > .more { + display: block; + width: 100%; + padding: 16px; + + > [data-icon] { + margin-right: 4px; + } + } + + > .empty { + margin: 0; + padding: 16px; + text-align: center; + color: var(--fg); + } + + > .placeholder { + padding: 32px; + opacity: 0.3; + } + } +} +</style> diff --git a/src/client/components/page-preview.vue b/src/client/components/page-preview.vue new file mode 100644 index 0000000000000000000000000000000000000000..5ba226c481d62522f61cf5a173d2229e188162f3 --- /dev/null +++ b/src/client/components/page-preview.vue @@ -0,0 +1,163 @@ +<template> +<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1"> + <div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div> + <article> + <header> + <h1 :title="page.title">{{ page.title }}</h1> + </header> + <p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> + <footer> + <img class="icon" :src="page.user.avatarUrl"/> + <p>{{ page.user | userName }}</p> + </footer> + </article> +</router-link> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + page: { + type: Object, + required: true + }, + }, +}); +</script> + +<style lang="scss" scoped> +.vhpxefrj { + display: block; + overflow: hidden; + width: 100%; + border: solid var(--lineWidth) var(--urlPreviewBorder); + border-radius: 4px; + overflow: hidden; + + &:hover { + text-decoration: none; + border-color: var(--urlPreviewBorderHover); + } + + > .thumbnail { + position: absolute; + width: 100px; + height: 100%; + background-position: center; + background-size: cover; + display: flex; + justify-content: center; + align-items: center; + + > button { + font-size: 3.5em; + opacity: 0.7; + + &:hover { + font-size: 4em; + opacity: 0.9; + } + } + + & + article { + left: 100px; + width: calc(100% - 100px); + } + } + + > article { + padding: 16px; + + > header { + margin-bottom: 8px; + + > h1 { + margin: 0; + font-size: 1em; + color: var(--urlPreviewTitle); + } + } + + > p { + margin: 0; + color: var(--urlPreviewText); + 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: var(--urlPreviewInfo); + font-size: 0.8em; + line-height: 16px; + vertical-align: top; + } + } + } + + @media (max-width: 700px) { + > .thumbnail { + position: relative; + width: 100%; + height: 100px; + + & + article { + left: 0; + width: 100%; + } + } + } + + @media (max-width: 550px) { + font-size: 12px; + + > .thumbnail { + height: 80px; + } + + > article { + padding: 12px; + } + } + + @media (max-width: 500px) { + font-size: 10px; + + > .thumbnail { + height: 70px; + } + + > article { + padding: 8px; + + > header { + margin-bottom: 4px; + } + + > footer { + margin-top: 4px; + + > img { + width: 12px; + height: 12px; + } + } + } + } +} + +</style> diff --git a/src/client/app/common/views/components/page/page.block.vue b/src/client/components/page/page.block.vue similarity index 99% rename from src/client/app/common/views/components/page/page.block.vue rename to src/client/components/page/page.block.vue index 56d18220134bbffb55e105c15064e5a05b88889c..c1d046fa2ec4e156b0eb813373d45ed3125c2365 100644 --- a/src/client/app/common/views/components/page/page.block.vue +++ b/src/client/components/page/page.block.vue @@ -22,7 +22,6 @@ export default Vue.extend({ components: { XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton }, - props: { value: { required: true diff --git a/src/client/app/common/views/components/page/page.button.vue b/src/client/components/page/page.button.vue similarity index 74% rename from src/client/app/common/views/components/page/page.button.vue rename to src/client/components/page/page.button.vue index 87112aca0d33dadffa7156a6adbbfa02bde8d539..eeb56d5eca1dbd5f710317c534c941202bd697ca 100644 --- a/src/client/app/common/views/components/page/page.button.vue +++ b/src/client/components/page/page.button.vue @@ -1,13 +1,17 @@ <template> <div> - <ui-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</ui-button> + <mk-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</mk-button> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkButton from '../ui/button.vue'; export default Vue.extend({ + components: { + MkButton + }, props: { value: { required: true @@ -16,7 +20,6 @@ export default Vue.extend({ required: true } }, - methods: { click() { if (this.value.action === 'dialog') { @@ -46,10 +49,11 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.kudkigyw - display inline-block - min-width 200px - max-width 450px - margin 8px 0 +<style lang="scss" scoped> +.kudkigyw { + display: inline-block; + min-width: 200px; + max-width: 450px; + margin: 8px 0; +} </style> diff --git a/src/client/app/common/views/components/page/page.counter.vue b/src/client/components/page/page.counter.vue similarity index 60% rename from src/client/app/common/views/components/page/page.counter.vue rename to src/client/components/page/page.counter.vue index 8d55319fe97f707672083bdbfe3aff0471ea1df4..781a1bd549f61c306ae549b247f6ce50a97d960c 100644 --- a/src/client/app/common/views/components/page/page.counter.vue +++ b/src/client/components/page/page.counter.vue @@ -1,13 +1,17 @@ <template> <div> - <ui-button class="llumlmnx" @click="click()">{{ script.interpolate(value.text) }}</ui-button> + <mk-button class="llumlmnx" @click="click()">{{ script.interpolate(value.text) }}</mk-button> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkButton from '../ui/button.vue'; export default Vue.extend({ + components: { + MkButton + }, props: { value: { required: true @@ -16,20 +20,17 @@ export default Vue.extend({ required: true } }, - data() { return { v: 0, }; }, - watch: { v() { this.script.aiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } }, - methods: { click() { this.v = this.v + (this.value.inc || 1); @@ -38,10 +39,11 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.llumlmnx - display inline-block - min-width 300px - max-width 450px - margin 8px 0 +<style lang="scss" scoped> +.llumlmnx { + display: inline-block; + min-width: 300px; + max-width: 450px; + margin: 8px 0; +} </style> diff --git a/src/client/app/common/views/components/page/page.if.vue b/src/client/components/page/page.if.vue similarity index 98% rename from src/client/app/common/views/components/page/page.if.vue rename to src/client/components/page/page.if.vue index 417ef0c553c3185b268957c99b806e039a33e154..a714a522e895b53d75d5ae1155f8228589265447 100644 --- a/src/client/app/common/views/components/page/page.if.vue +++ b/src/client/components/page/page.if.vue @@ -22,9 +22,8 @@ export default Vue.extend({ required: true } }, - beforeCreate() { - this.$options.components.XBlock = require('./page.block.vue').default + this.$options.components.XBlock = require('./page.block.vue').default; }, }); </script> diff --git a/src/client/app/common/views/components/page/page.image.vue b/src/client/components/page/page.image.vue similarity index 75% rename from src/client/app/common/views/components/page/page.image.vue rename to src/client/components/page/page.image.vue index 1285445eb0ebf0bea45232092d1e10bc12c4f5b3..f0d7c7b30f4f33fb23d438e56ddee4200476d778 100644 --- a/src/client/app/common/views/components/page/page.image.vue +++ b/src/client/components/page/page.image.vue @@ -1,6 +1,6 @@ <template> <div class="lzyxtsnt"> - <img v-if="image" :src="image.url"/> + <img v-if="image" :src="image.url" alt=""/> </div> </template> @@ -16,21 +16,21 @@ export default Vue.extend({ required: true }, }, - data() { return { image: null, }; }, - created() { this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId); } }); </script> -<style lang="stylus" scoped> -.lzyxtsnt - > img - max-width 100% +<style lang="scss" scoped> +.lzyxtsnt { + > img { + max-width: 100%; + } +} </style> diff --git a/src/client/app/common/views/components/page/page.number-input.vue b/src/client/components/page/page.number-input.vue similarity index 56% rename from src/client/app/common/views/components/page/page.number-input.vue rename to src/client/components/page/page.number-input.vue index 31da37330a0c01f407ed69baab5c15d57ed33123..9ee2730fac9438ebb18f7a8e7d2cf94b11e5ec8d 100644 --- a/src/client/app/common/views/components/page/page.number-input.vue +++ b/src/client/components/page/page.number-input.vue @@ -1,13 +1,17 @@ <template> <div> - <ui-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</ui-input> + <mk-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</mk-input> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkInput from '../ui/input.vue'; export default Vue.extend({ + components: { + MkInput + }, props: { value: { required: true @@ -16,13 +20,11 @@ export default Vue.extend({ required: true } }, - data() { return { v: this.value.default, }; }, - watch: { v() { this.script.aiScript.updatePageVar(this.value.name, this.v); @@ -32,10 +34,11 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.kudkigyw - display inline-block - min-width 300px - max-width 450px - margin 8px 0 +<style lang="scss" scoped> +.kudkigyw { + display: inline-block; + min-width: 300px; + max-width: 450px; + margin: 8px 0; +} </style> diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue new file mode 100644 index 0000000000000000000000000000000000000000..010a96c855757c93fb130fd032225bfa30a4fa27 --- /dev/null +++ b/src/client/components/page/page.post.vue @@ -0,0 +1,75 @@ +<template> +<div class="ngbfujlo"> + <mk-textarea class="textarea" :value="text" readonly></mk-textarea> + <mk-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</mk-button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../i18n'; +import MkTextarea from '../ui/textarea.vue'; +import MkButton from '../ui/button.vue'; + +export default Vue.extend({ + i18n, + components: { + MkTextarea, + MkButton, + }, + props: { + value: { + required: true + }, + script: { + required: true + } + }, + data() { + return { + text: this.script.interpolate(this.value.text), + posted: false, + posting: false, + }; + }, + watch: { + 'script.vars': { + handler() { + this.text = this.script.interpolate(this.value.text); + }, + deep: true + } + }, + methods: { + post() { + this.posting = true; + this.$root.api('notes/create', { + text: this.text, + }).then(() => { + this.posted = true; + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.ngbfujlo { + padding: 0 32px 32px 32px; + border: solid 2px var(--divider); + border-radius: 6px; + + @media (max-width: 600px) { + padding: 0 16px 16px 16px; + + > .textarea { + margin-top: 16px; + margin-bottom: 16px; + } + } +} +</style> diff --git a/src/client/app/common/views/components/page/page.radio-button.vue b/src/client/components/page/page.radio-button.vue similarity index 73% rename from src/client/app/common/views/components/page/page.radio-button.vue rename to src/client/components/page/page.radio-button.vue index 27c11bebad936495011300ade2757f8cd6f6a66e..fda0a03927bf45a7b2f43a85f187c0a69450d2e7 100644 --- a/src/client/app/common/views/components/page/page.radio-button.vue +++ b/src/client/components/page/page.radio-button.vue @@ -1,14 +1,18 @@ <template> <div> <div>{{ script.interpolate(value.title) }}</div> - <ui-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</ui-radio> + <mk-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</mk-radio> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkRadio from '../ui/radio.vue'; export default Vue.extend({ + components: { + MkRadio + }, props: { value: { required: true @@ -17,13 +21,11 @@ export default Vue.extend({ required: true } }, - data() { return { v: this.value.default, }; }, - watch: { v() { this.script.aiScript.updatePageVar(this.value.name, this.v); @@ -32,6 +34,3 @@ export default Vue.extend({ } }); </script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/page/page.section.vue b/src/client/components/page/page.section.vue similarity index 71% rename from src/client/app/common/views/components/page/page.section.vue rename to src/client/components/page/page.section.vue index 03c009d9c3e83e42bc00cb56c917828ab48c65a7..b83c773f7109fc261355db93b3711280498656eb 100644 --- a/src/client/app/common/views/components/page/page.section.vue +++ b/src/client/components/page/page.section.vue @@ -26,30 +26,33 @@ export default Vue.extend({ required: true } }, - beforeCreate() { - this.$options.components.XBlock = require('./page.block.vue').default + this.$options.components.XBlock = require('./page.block.vue').default; }, }); </script> -<style lang="stylus" scoped> -.sdgxphyu - margin 1.5em 0 +<style lang="scss" scoped> +.sdgxphyu { + margin: 1.5em 0; - > h2 - font-size 1.35em - margin 0 0 0.5em 0 + > h2 { + font-size: 1.35em; + margin: 0 0 0.5em 0; + } - > h3 - font-size 1em - margin 0 0 0.5em 0 + > h3 { + font-size: 1em; + margin: 0 0 0.5em 0; + } - > h4 - font-size 1em - margin 0 0 0.5em 0 + > h4 { + font-size: 1em; + margin: 0 0 0.5em 0; + } - > .children + > .children { //padding 16px - + } +} </style> diff --git a/src/client/app/common/views/components/page/page.switch.vue b/src/client/components/page/page.switch.vue similarity index 61% rename from src/client/app/common/views/components/page/page.switch.vue rename to src/client/components/page/page.switch.vue index 53695f1b36760202fb5f4bec98bde070e9b94308..416c36e9ad00dbca5422dc6c30a48c2004d1a114 100644 --- a/src/client/app/common/views/components/page/page.switch.vue +++ b/src/client/components/page/page.switch.vue @@ -1,13 +1,17 @@ <template> <div class="hkcxmtwj"> - <ui-switch v-model="v">{{ script.interpolate(value.text) }}</ui-switch> + <mk-switch v-model="v">{{ script.interpolate(value.text) }}</mk-switch> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkSwitch from '../ui/switch.vue'; export default Vue.extend({ + components: { + MkSwitch + }, props: { value: { required: true @@ -16,13 +20,11 @@ export default Vue.extend({ required: true } }, - data() { return { v: this.value.default, }; }, - watch: { v() { this.script.aiScript.updatePageVar(this.value.name, this.v); @@ -32,12 +34,13 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.hkcxmtwj - display inline-block - margin 16px auto - - & + .hkcxmtwj - margin-left 16px +<style lang="scss" scoped> +.hkcxmtwj { + display: inline-block; + margin: 16px auto; + & + .hkcxmtwj { + margin-left: 16px; + } +} </style> diff --git a/src/client/app/common/views/components/page/page.text-input.vue b/src/client/components/page/page.text-input.vue similarity index 57% rename from src/client/app/common/views/components/page/page.text-input.vue rename to src/client/components/page/page.text-input.vue index cf917dd5a8ee3bbb70895322a276d55dc6ad65bc..fcc181d67371fb2a31705c9ffe00129fcae8734c 100644 --- a/src/client/app/common/views/components/page/page.text-input.vue +++ b/src/client/components/page/page.text-input.vue @@ -1,13 +1,17 @@ <template> <div> - <ui-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</ui-input> + <mk-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</mk-input> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkInput from '../ui/input.vue'; export default Vue.extend({ + components: { + MkInput + }, props: { value: { required: true @@ -16,13 +20,11 @@ export default Vue.extend({ required: true } }, - data() { return { v: this.value.default, }; }, - watch: { v() { this.script.aiScript.updatePageVar(this.value.name, this.v); @@ -32,10 +34,11 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.kudkigyw - display inline-block - min-width 300px - max-width 450px - margin 8px 0 +<style lang="scss" scoped> +.kudkigyw { + display: inline-block; + min-width: 300px; + max-width: 450px; + margin: 8px 0; +} </style> diff --git a/src/client/app/common/views/components/page/page.text.vue b/src/client/components/page/page.text.vue similarity index 67% rename from src/client/app/common/views/components/page/page.text.vue rename to src/client/components/page/page.text.vue index 326fd3905033457c9db99193d632ede82ce6fbc5..aeab31225ecaea62c92adb7caa43644f15eac0b1 100644 --- a/src/client/app/common/views/components/page/page.text.vue +++ b/src/client/components/page/page.text.vue @@ -1,15 +1,14 @@ <template> <div class="mrdgzndn"> <mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" class="url"/> </div> </template> <script lang="ts"> import Vue from 'vue'; -import { parse } from '../../../../../../mfm/parse'; -import { unique } from '../../../../../../prelude/array'; +import { parse } from '../../../mfm/parse'; +import { unique } from '../../../prelude/array'; export default Vue.extend({ props: { @@ -20,13 +19,11 @@ export default Vue.extend({ required: true } }, - data() { return { text: this.script.interpolate(this.value.text), }; }, - computed: { urls(): string[] { if (this.text) { @@ -40,23 +37,29 @@ export default Vue.extend({ } } }, - - created() { - this.$watch('script.vars', () => { - this.text = this.script.interpolate(this.value.text); - }, { deep: true }); - } + watch: { + 'script.vars': { + handler() { + this.text = this.script.interpolate(this.value.text); + }, + deep: true + } + }, }); </script> -<style lang="stylus" scoped> -.mrdgzndn - &:not(:first-child) - margin-top 0.5em +<style lang="scss" scoped> +.mrdgzndn { + &:not(:first-child) { + margin-top: 0.5em; + } - &:not(:last-child) - margin-bottom 0.5em + &:not(:last-child) { + margin-bottom: 0.5em; + } - > .url - margin 0.5em 0 + > .url { + margin: 0.5em 0; + } +} </style> diff --git a/src/client/app/common/views/components/page/page.textarea-input.vue b/src/client/components/page/page.textarea-input.vue similarity index 70% rename from src/client/app/common/views/components/page/page.textarea-input.vue rename to src/client/components/page/page.textarea-input.vue index eece59fefb6a2d90e9f451988312325a6648f156..d1cf9813c4088c6cccffd220d1b5a0433a3bc37b 100644 --- a/src/client/app/common/views/components/page/page.textarea-input.vue +++ b/src/client/components/page/page.textarea-input.vue @@ -1,13 +1,17 @@ <template> <div> - <ui-textarea class="" v-model="v">{{ script.interpolate(value.text) }}</ui-textarea> + <mk-textarea v-model="v">{{ script.interpolate(value.text) }}</mk-textarea> </div> </template> <script lang="ts"> import Vue from 'vue'; +import MkTextarea from '../ui/textarea.vue'; export default Vue.extend({ + components: { + MkTextarea + }, props: { value: { required: true @@ -16,13 +20,11 @@ export default Vue.extend({ required: true } }, - data() { return { v: this.value.default, }; }, - watch: { v() { this.script.aiScript.updatePageVar(this.value.name, this.v); @@ -31,6 +33,3 @@ export default Vue.extend({ } }); </script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/page/page.textarea.vue b/src/client/components/page/page.textarea.vue similarity index 51% rename from src/client/app/common/views/components/page/page.textarea.vue rename to src/client/components/page/page.textarea.vue index 03c8542cb06adef44edbfd22abf5a85856ca87b9..78b74dd64c74b73351b6207f502a85b162e30c4c 100644 --- a/src/client/app/common/views/components/page/page.textarea.vue +++ b/src/client/components/page/page.textarea.vue @@ -1,11 +1,15 @@ <template> -<ui-textarea class="" :value="text" readonly></ui-textarea> +<mk-textarea :value="text" readonly></mk-textarea> </template> <script lang="ts"> import Vue from 'vue'; +import MkTextarea from '../ui/textarea.vue'; export default Vue.extend({ + components: { + MkTextarea + }, props: { value: { required: true @@ -14,20 +18,18 @@ export default Vue.extend({ required: true } }, - data() { return { text: this.script.interpolate(this.value.text), }; }, - - created() { - this.$watch('script.vars', () => { - this.text = this.script.interpolate(this.value.text); - }, { deep: true }); + watch: { + 'script.vars': { + handler() { + this.text = this.script.interpolate(this.value.text); + }, + deep: true + } } }); </script> - -<style lang="stylus" scoped> -</style> diff --git a/src/client/app/common/views/components/page/page.vue b/src/client/components/page/page.vue similarity index 66% rename from src/client/app/common/views/components/page/page.vue rename to src/client/components/page/page.vue index 1bfb93780ff7f4005a702becdd6c3e394d0904ed..bd7831347558da3f82d9384aa9584d4500016ebb 100644 --- a/src/client/app/common/views/components/page/page.vue +++ b/src/client/components/page/page.vue @@ -1,5 +1,5 @@ <template> -<div class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter, serif: page.font === 'serif' }"> +<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }"> <header v-if="showTitle"> <div class="title">{{ page.title }}</div> </header> @@ -11,7 +11,7 @@ <footer v-if="showFooter"> <small>@{{ page.user.username }}</small> <template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId"> - <router-link :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link> + <router-link :to="`/my/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link> <a v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)">{{ $t('unpin-this-page') }}</a> <a v-else @click="pin(true)">{{ $t('pin-this-page') }}</a> </template> @@ -27,13 +27,13 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../../i18n'; +import i18n from '../../i18n'; import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; import { faHeart } from '@fortawesome/free-regular-svg-icons'; import XBlock from './page.block.vue'; -import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator'; -import { collectPageVars } from '../../../scripts/collect-page-vars'; -import { url } from '../../../../config'; +import { ASEvaluator } from '../../scripts/aiscript/evaluator'; +import { collectPageVars } from '../../scripts/collect-page-vars'; +import { url } from '../../config'; class Script { public aiScript: ASEvaluator; @@ -66,7 +66,7 @@ class Script { } export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { XBlock @@ -146,77 +146,85 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.iroscrza - overflow hidden - background var(--face) - - &.serif - > div - font-family serif - - &.center - text-align center - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > header - > .title - z-index 1 - margin 0 - padding 16px 32px - font-size 20px - font-weight bold - color var(--text) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - @media (max-width 600px) - padding 16px 32px - font-size 20px +<style lang="scss" scoped> +.iroscrza { + &.serif { + > div { + font-family: serif; + } + } - @media (max-width 400px) - padding 10px 20px - font-size 16px + &.center { + text-align: center; + } - > div - color var(--text) - padding 24px 32px - font-size 16px + > header { + > .title { + z-index: 1; + margin: 0; + padding: 16px 32px; + font-size: 20px; + font-weight: bold; + color: var(--text); + box-shadow: 0 var(--lineWidth) rgba(#000, 0.07); + + @media (max-width: 600px) { + padding: 16px 32px; + font-size: 20px; + } + + @media (max-width: 400px) { + padding: 10px 20px; + font-size: 16px; + } + } + } - @media (max-width 600px) - padding 24px 32px - font-size 16px + > div { + color: var(--text); + padding: 24px 32px; + font-size: 16px; - @media (max-width 400px) - padding 20px 20px - font-size 15px + @media (max-width: 600px) { + padding: 24px 32px; + font-size: 16px; + } - > footer - color var(--text) - padding 0 32px 28px 32px + @media (max-width: 400px) { + padding: 20px 20px; + font-size: 15px; + } + } - @media (max-width 600px) - padding 0 32px 28px 32px + > footer { + color: var(--text); + padding: 0 32px 28px 32px; - @media (max-width 400px) - padding 0 20px 20px 20px - font-size 14px + @media (max-width: 600px) { + padding: 0 32px 28px 32px; + } - > small - display block - opacity 0.5 + @media (max-width: 400px) { + padding: 0 20px 20px 20px; + font-size: 14px; + } - > a - font-size 90% + > small { + display: block; + opacity: 0.5; + } - > a + a - margin-left 8px + > a { + font-size: 90%; + } - > .like - margin-top 16px + > a + a { + margin-left: 8px; + } + > .like { + margin-top: 16px; + } + } +} </style> diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue new file mode 100644 index 0000000000000000000000000000000000000000..b5b8c2c02d7dd31bdb0cce3d3f6791ee0f95a507 --- /dev/null +++ b/src/client/components/poll-editor.vue @@ -0,0 +1,218 @@ +<template> +<div class="zmdxowus"> + <p class="caution" v-if="choices.length < 2"> + <fa :icon="faExclamationTriangle"/>{{ $t('_poll.noOnlyOneChoice') }} + </p> + <ul ref="choices"> + <li v-for="(choice, i) in choices" :key="i"> + <mk-input class="input" :value="choice" @input="onInput(i, $event)"> + <span>{{ $t('_poll.choiceN', { n: i + 1 }) }}</span> + </mk-input> + <button @click="remove(i)" class="_button"> + <fa :icon="faTimes"/> + </button> + </li> + </ul> + <mk-button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</mk-button> + <mk-button class="add" v-else disabled>{{ $t('_poll.noMore') }}</mk-button> + <section> + <mk-switch v-model="multiple">{{ $t('_poll.canMultipleVote') }}</mk-switch> + <div> + <mk-select v-model="expiration"> + <template #label>{{ $t('_poll.expiration') }}</template> + <option value="infinite">{{ $t('_poll.infinite') }}</option> + <option value="at">{{ $t('_poll.at') }}</option> + <option value="after">{{ $t('_poll.after') }}</option> + </mk-select> + <section v-if="expiration === 'at'"> + <mk-input v-model="atDate" type="date" class="input"> + <span>{{ $t('_poll.deadlineDate') }}</span> + </mk-input> + <mk-input v-model="atTime" type="time" class="input"> + <span>{{ $t('_poll.deadlineTime') }}</span> + </mk-input> + </section> + <section v-if="expiration === 'after'"> + <mk-input v-model="after" type="number" class="input"> + <span>{{ $t('_poll.duration') }}</span> + </mk-input> + <mk-select v-model="unit"> + <option value="second">{{ $t('_time.second') }}</option> + <option value="minute">{{ $t('_time.minute') }}</option> + <option value="hour">{{ $t('_time.hour') }}</option> + <option value="day">{{ $t('_time.day') }}</option> + </mk-select> + </section> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import { erase } from '../../prelude/array'; +import { addTimespan } from '../../prelude/time'; +import { formatDateTimeString } from '../../misc/format-time-string'; +import MkInput from './ui/input.vue'; +import MkSelect from './ui/select.vue'; +import MkSwitch from './ui/switch.vue'; +import MkButton from './ui/button.vue'; + +export default Vue.extend({ + i18n, + components: { + MkInput, + MkSelect, + MkSwitch, + MkButton, + }, + data() { + return { + choices: ['', ''], + multiple: false, + expiration: 'infinite', + atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'), + atTime: '00:00', + after: 0, + unit: 'second', + faExclamationTriangle, faTimes + }; + }, + watch: { + choices() { + this.$emit('updated'); + } + }, + methods: { + onInput(i, e) { + Vue.set(this.choices, i, e); + }, + + 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); + }, + + get() { + const at = () => { + return new Date(`${this.atDate} ${this.atTime}`).getTime(); + }; + + const after = () => { + let base = parseInt(this.after); + switch (this.unit) { + case 'day': base *= 24; + case 'hour': base *= 60; + case 'minute': base *= 60; + case 'second': return base *= 1000; + default: return null; + } + }; + + return { + choices: erase('', this.choices), + multiple: this.multiple, + ...( + this.expiration === 'at' ? { expiresAt: at() } : + this.expiration === 'after' ? { expiredAfter: after() } : {}) + }; + }, + + set(data) { + if (data.choices.length == 0) return; + this.choices = data.choices; + if (data.choices.length == 1) this.choices = this.choices.concat(''); + this.multiple = data.multiple; + if (data.expiresAt) { + this.expiration = 'at'; + this.atDate = this.atTime = data.expiresAt; + } else if (typeof data.expiredAfter === 'number') { + this.expiration = 'after'; + this.after = data.expiredAfter; + } else { + this.expiration = 'infinite'; + } + } + } +}); +</script> + +<style lang="scss" scoped> +.zmdxowus { + padding: 8px; + + > .caution { + margin: 0 0 8px 0; + font-size: 0.8em; + color: #f00; + + > [data-icon] { + margin-right: 4px; + } + } + + > ul { + display: block; + margin: 0; + padding: 0; + list-style: none; + + > li { + display: flex; + margin: 8px 0; + padding: 0; + width: 100%; + + > .input { + flex: 1; + margin-top: 16px; + margin-bottom: 0; + } + + > button { + width: 32px; + padding: 4px 0; + } + } + } + + > .add { + margin: 8px 0 0 0; + z-index: 1; + } + + > section { + margin: 16px 0 -16px 0; + + > div { + margin: 0 8px; + + &:last-child { + flex: 1 0 auto; + + > section { + align-items: center; + display: flex; + margin: -32px 0 0; + + > &:first-child { + margin-right: 16px; + } + + > .input { + flex: 1 0 auto; + } + } + } + } + } +} +</style> diff --git a/src/client/components/poll.vue b/src/client/components/poll.vue new file mode 100644 index 0000000000000000000000000000000000000000..15be1b282d3b66d8210effca8ba2d664748ac2b1 --- /dev/null +++ b/src/client/components/poll.vue @@ -0,0 +1,174 @@ +<template> +<div class="mk-poll" :data-done="closed || isVoted"> + <ul> + <li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }"> + <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> + <span> + <template v-if="choice.isVoted"><fa :icon="faCheck"/></template> + <mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> + <span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span> + </span> + </li> + </ul> + <p> + <span>{{ $t('_poll.totalVotes', { n: total }) }}</span> + <span> · </span> + <a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('_poll.vote') : $t('_poll.showResult') }}</a> + <span v-if="isVoted">{{ $t('_poll.voted') }}</span> + <span v-else-if="closed">{{ $t('_poll.closed') }}</span> + <span v-if="remaining > 0"> · {{ timer }}</span> + </p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import { sum } from '../../prelude/array'; + +export default Vue.extend({ + i18n, + props: { + note: { + type: Object, + required: true + } + }, + data() { + return { + remaining: -1, + showResult: false, + faCheck + }; + }, + computed: { + poll(): any { + return this.note.poll; + }, + total(): number { + return sum(this.poll.choices.map(x => x.votes)); + }, + closed(): boolean { + return !this.remaining; + }, + timer(): string { + return this.$t( + this.remaining > 86400 ? '_poll.remainingDays' : + this.remaining > 3600 ? '_poll.remainingHours' : + this.remaining > 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', { + s: Math.floor(this.remaining % 60), + m: Math.floor(this.remaining / 60) % 60, + h: Math.floor(this.remaining / 3600) % 24, + d: Math.floor(this.remaining / 86400) + }); + }, + isVoted(): boolean { + return !this.poll.multiple && this.poll.choices.some(c => c.isVoted); + } + }, + created() { + this.showResult = this.isVoted; + + if (this.note.poll.expiresAt) { + const update = () => { + if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000)) + requestAnimationFrame(update); + else + this.showResult = true; + }; + + update(); + } + }, + methods: { + toggleShowResult() { + this.showResult = !this.showResult; + }, + vote(id) { + if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return; + this.$root.api('notes/polls/vote', { + noteId: this.note.id, + choice: id + }).then(() => { + if (!this.showResult) this.showResult = !this.poll.multiple; + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-poll { + > ul { + display: block; + margin: 0; + padding: 0; + list-style: none; + + > li { + display: block; + position: relative; + margin: 4px 0; + padding: 4px 8px; + width: 100%; + color: var(--pollChoiceText); + border: solid 1px var(--pollChoiceBorder); + border-radius: 4px; + overflow: hidden; + cursor: pointer; + + &:hover { + background: rgba(#000, 0.05); + } + + &:active { + background: rgba(#000, 0.1); + } + + > .backdrop { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--accent); + transition: width 1s ease; + } + + > span { + position: relative; + + > [data-icon] { + margin-right: 4px; + } + + > .votes { + margin-left: 4px; + } + } + } + } + + > p { + color: var(--fg); + + a { + color: inherit; + } + } + + &[data-done] { + > ul > li { + cursor: default; + + &:hover { + background: transparent; + } + + &:active { + background: transparent; + } + } + } +} +</style> diff --git a/src/client/components/popup.vue b/src/client/components/popup.vue new file mode 100644 index 0000000000000000000000000000000000000000..d5b1f9423bcc0ffbe41baff7a62da2893e9c380f --- /dev/null +++ b/src/client/components/popup.vue @@ -0,0 +1,147 @@ +<template> +<div class="mk-popup"> + <transition name="bg-fade" appear> + <div class="bg" ref="bg" @click="close()" v-if="show"></div> + </transition> + <transition name="popup" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> + <div class="content" :class="{ fixed }" ref="content" v-if="show" :style="{ width: width ? width + 'px' : 'auto' }"><slot></slot></div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + source: { + required: true + }, + noCenter: { + type: Boolean, + required: false + }, + fixed: { + type: Boolean, + required: false + }, + width: { + type: Number, + required: false + } + }, + data() { + return { + show: true, + }; + }, + mounted() { + this.$nextTick(() => { + const popover = this.$refs.content as any; + + const rect = this.source.getBoundingClientRect(); + const width = popover.offsetWidth; + const height = popover.offsetHeight; + + let left; + let top; + + if (this.$root.isMobile && !this.noCenter) { + const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); + const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.source.offsetHeight / 2); + left = (x - (width / 2)); + top = (y - (height / 2)); + popover.style.transformOrigin = 'center'; + } else { + const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); + const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.source.offsetHeight; + left = (x - (width / 2)); + top = y; + } + + if (this.fixed) { + if (left + width > window.innerWidth) { + left = window.innerWidth - width; + popover.style.transformOrigin = 'center'; + } + + if (top + height > window.innerHeight) { + top = window.innerHeight - height; + popover.style.transformOrigin = 'center'; + } + } else { + if (left + width - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - width + window.pageXOffset; + popover.style.transformOrigin = 'center'; + } + + if (top + height - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - height + window.pageYOffset; + popover.style.transformOrigin = 'center'; + } + } + + if (top < 0) { + top = 0; + } + + if (left < 0) { + left = 0; + } + + popover.style.left = left + 'px'; + popover.style.top = top + 'px'; + }); + }, + methods: { + close() { + this.show = false; + (this.$refs.bg as any).style.pointerEvents = 'none'; + (this.$refs.content as any).style.pointerEvents = 'none'; + } + } +}); +</script> + +<style lang="scss" scoped> +.popup-enter-active, .popup-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.popup-enter, .popup-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.bg-fade-enter-active, .bg-fade-leave-active { + transition: opacity 0.3s !important; +} +.bg-fade-enter, .bg-fade-leave-to { + opacity: 0; +} + +.mk-popup { + > .bg { + position: fixed; + top: 0; + left: 0; + z-index: 10000; + width: 100%; + height: 100%; + background: var(--modalBg) + } + + > .content { + position: absolute; + z-index: 10001; + background: var(--panel); + border-radius: 4px; + box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); + overflow: hidden; + transform-origin: center top; + + &.fixed { + position: fixed; + } + } +} +</style> diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue new file mode 100644 index 0000000000000000000000000000000000000000..50ba9bfdcf4229c87c8d14c3c4e8e64f355b7afb --- /dev/null +++ b/src/client/components/post-form-attaches.vue @@ -0,0 +1,158 @@ +<template> +<div class="skeikyzd" v-show="files.length != 0"> + <x-draggable class="files" :list="files" animation="150"> + <div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)"> + <x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/> + <div class="sensitive" v-if="file.isSensitive"> + <fa class="icon" :icon="faExclamationTriangle"/> + </div> + </div> + </x-draggable> + <p class="remain">{{ 4 - files.length }}/4</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import * as XDraggable from 'vuedraggable'; +import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import { faExclamationTriangle, faICursor } from '@fortawesome/free-solid-svg-icons'; +import XFileThumbnail from './drive-file-thumbnail.vue' + +export default Vue.extend({ + i18n, + + components: { + XDraggable, + XFileThumbnail + }, + + props: { + files: { + type: Array, + required: true + }, + detachMediaFn: { + type: Function, + required: false + } + }, + + data() { + return { + faExclamationTriangle + }; + }, + + methods: { + detachMedia(id) { + if (this.detachMediaFn) { + this.detachMediaFn(id); + } else if (this.$parent.detachMedia) { + this.$parent.detachMedia(id); + } + }, + toggleSensitive(file) { + this.$root.api('drive/files/update', { + fileId: file.id, + isSensitive: !file.isSensitive + }).then(() => { + file.isSensitive = !file.isSensitive; + this.$parent.updateMedia(file); + }); + }, + async rename(file) { + const { canceled, result } = await this.$root.dialog({ + title: this.$t('enterFileName'), + input: { + default: file.name + }, + allowEmpty: false + }); + if (canceled) return; + this.$root.api('drive/files/update', { + fileId: file.id, + name: result + }).then(() => { + file.name = result; + this.$parent.updateMedia(file); + }); + }, + showFileMenu(file, ev: MouseEvent) { + this.$root.menu({ + items: [{ + text: this.$t('renameFile'), + icon: faICursor, + action: () => { this.rename(file) } + }, { + text: file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'), + icon: file.isSensitive ? faEyeSlash : faEye, + action: () => { this.toggleSensitive(file) } + }, { + text: this.$t('attachCancel'), + icon: faTimesCircle, + action: () => { this.detachMedia(file.id) } + }], + source: ev.currentTarget || ev.target + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.skeikyzd { + padding: 4px; + position: relative; + + > .files { + display: flex; + flex-wrap: wrap; + + > div { + position: relative; + width: 64px; + height: 64px; + margin: 4px; + cursor: move; + + &:hover > .remove { + display: block; + } + + > .thumbnail { + width: 100%; + height: 100%; + z-index: 1; + color: var(--fg); + } + + > .sensitive { + display: flex; + position: absolute; + width: 64px; + height: 64px; + top: 0; + left: 0; + z-index: 2; + background: rgba(17, 17, 17, .7); + color: #fff; + + > .icon { + margin: auto; + } + } + } + } + + > .remain { + display: block; + position: absolute; + top: 8px; + right: 8px; + margin: 0; + padding: 0; + } +} +</style> diff --git a/src/client/components/post-form-dialog.vue b/src/client/components/post-form-dialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..fe70b88218390f0a4b70fdb7a0e5487f7673b77d --- /dev/null +++ b/src/client/components/post-form-dialog.vue @@ -0,0 +1,157 @@ +<template> +<div class="ulveipglmagnxfgvitaxyszerjwiqmwl"> + <transition name="form-fade" appear> + <div class="bg" ref="bg" v-if="show" @click="close()"></div> + </transition> + <div class="main" ref="main" @click.self="close()" @keydown="onKeydown"> + <transition name="form" appear + @after-leave="destroyDom" + > + <x-post-form ref="form" + v-if="show" + :reply="reply" + :renote="renote" + :mention="mention" + :specified="specified" + :initial-text="initialText" + :initial-note="initialNote" + :instant="instant" + @posted="onPosted" + @cancel="onCanceled"/> + </transition> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XPostForm from './post-form.vue'; + +export default Vue.extend({ + components: { + XPostForm + }, + + props: { + reply: { + type: Object, + required: false + }, + renote: { + type: Object, + required: false + }, + mention: { + type: Object, + required: false + }, + specified: { + type: Object, + required: false + }, + initialText: { + type: String, + required: false + }, + initialNote: { + type: Object, + required: false + }, + instant: { + type: Boolean, + required: false, + default: false + } + }, + + data() { + return { + show: true + }; + }, + + methods: { + focus() { + this.$refs.form.focus(); + }, + + close() { + this.show = false; + (this.$refs.bg as any).style.pointerEvents = 'none'; + (this.$refs.main as any).style.pointerEvents = 'none'; + }, + + onPosted() { + this.$emit('posted'); + this.close(); + }, + + onCanceled() { + this.$emit('cancel'); + this.close(); + }, + + onKeydown(e) { + if (e.which === 27) { // Esc + e.preventDefault(); + e.stopPropagation(); + this.close(); + } + }, + } +}); +</script> + +<style lang="scss" scoped> +.form-enter-active, .form-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.form-enter, .form-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.form-fade-enter-active, .form-fade-leave-active { + transition: opacity 0.3s !important; +} +.form-fade-enter, .form-fade-leave-to { + opacity: 0; +} + +.ulveipglmagnxfgvitaxyszerjwiqmwl { + > .bg { + display: block; + position: fixed; + z-index: 10000; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(#000, 0.7); + } + + > .main { + display: block; + position: fixed; + z-index: 10000; + top: 32px; + left: 0; + right: 0; + height: calc(100% - 64px); + width: 500px; + max-width: calc(100% - 16px); + overflow: auto; + margin: 0 auto 0 auto; + + @media (max-width: 550px) { + top: 16px; + height: calc(100% - 32px); + } + + @media (max-width: 520px) { + top: 8px; + height: calc(100% - 16px); + } + } +} +</style> diff --git a/src/client/app/common/scripts/post-form.ts b/src/client/components/post-form.vue similarity index 54% rename from src/client/app/common/scripts/post-form.ts rename to src/client/components/post-form.vue index 496782fd309e9bcdeb7b958040ea210e8cdbe179..762b82036bff0d1ee4d312669a7361d958cc48ab 100644 --- a/src/client/app/common/scripts/post-form.ts +++ b/src/client/components/post-form.vue @@ -1,21 +1,82 @@ +<template> +<div class="gafaadew" + @dragover.stop="onDragover" + @dragenter="onDragenter" + @dragleave="onDragleave" + @drop.stop="onDrop" +> + <header> + <button class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button> + <div> + <span class="text-count" :class="{ over: trimmedLength(text) > 500 }">{{ 500 - trimmedLength(text) }}</span> + <button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}</button> + </div> + </header> + <div class="form"> + <x-note-preview class="preview" v-if="reply" :note="reply"/> + <x-note-preview class="preview" v-if="renote" :note="renote"/> + <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> + <div v-if="visibility === 'specified'" class="to-specified"> + <span style="margin-right: 8px;">{{ $t('recipient') }}</span> + <div class="visibleUsers"> + <span v-for="u in visibleUsers"> + <mk-acct :user="u"/> + <button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button> + </span> + <button @click="addVisibleUser" class="_buttonPrimary"><fa :icon="faPlus" fixed-width/></button> + </div> + </div> + <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotation')" v-autocomplete="{ model: 'cw' }"> + <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @keydown="onKeydown" @paste="onPaste"></textarea> + <x-post-form-attaches class="attaches" :files="files"/> + <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> + <x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> + <footer> + <button class="_button" @click="chooseFileFrom"><fa :icon="faPhotoVideo"/></button> + <button class="_button" @click="poll = !poll"><fa :icon="faChartPie"/></button> + <button class="_button" @click="useCw = !useCw"><fa :icon="faEyeSlash"/></button> + <button class="_button" @click="insertMention"><fa :icon="faAt"/></button> + <button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button> + <button class="_button" @click="setVisibility" ref="visibilityButton"> + <span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span> + <span v-if="visibility === 'home'"><fa :icon="faHome"/></span> + <span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span> + <span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span> + </button> + </footer> + <input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt } from '@fortawesome/free-solid-svg-icons'; +import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons'; import insertTextAtCursor from 'insert-text-at-cursor'; import { length } from 'stringz'; import { toASCII } from 'punycode'; -import MkVisibilityChooser from '../views/components/visibility-chooser.vue'; -import getFace from './get-face'; -import { parse } from '../../../../mfm/parse'; -import { host, url } from '../../config'; -import i18n from '../../i18n'; -import { erase, unique } from '../../../../prelude/array'; -import extractMentions from '../../../../misc/extract-mentions'; -import { formatTimeString } from '../../../../misc/format-time-string'; - -export default (opts) => ({ - i18n: i18n(), +import i18n from '../i18n'; +import MkVisibilityChooser from './visibility-chooser.vue'; +import MkUserSelect from './user-select.vue'; +import XNotePreview from './note-preview.vue'; +import XEmojiPicker from './emoji-picker.vue'; +import { parse } from '../../mfm/parse'; +import { host, url } from '../config'; +import { erase, unique } from '../../prelude/array'; +import extractMentions from '../../misc/extract-mentions'; +import getAcct from '../../misc/acct/render'; +import { formatTimeString } from '../../misc/format-time-string'; +import { selectDriveFile } from '../scripts/select-drive-file'; + +export default Vue.extend({ + i18n, components: { - XPostFormAttaches: () => import('../views/components/post-form-attaches.vue').then(m => m.default), - XPollEditor: () => import('../views/components/poll-editor.vue').then(m => m.default) + XNotePreview, + XUploader: () => import('./uploader.vue').then(m => m.default), + XPostFormAttaches: () => import('./post-form-attaches.vue').then(m => m.default), + XPollEditor: () => import('./poll-editor.vue').then(m => m.default) }, props: { @@ -31,6 +92,10 @@ export default (opts) => ({ type: Object, required: false }, + specified: { + type: Object, + required: false + }, initialText: { type: String, required: false @@ -58,15 +123,13 @@ export default (opts) => ({ pollExpiration: [], useCw: false, cw: null, - geo: null, visibility: 'public', visibleUsers: [], - localOnly: false, autocomplete: null, draghover: false, quoteId: null, recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), - maxNoteTextLength: 1000 + faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt }; }, @@ -81,44 +144,38 @@ export default (opts) => ({ placeholder(): string { const xs = [ - this.$t('@.note-placeholders.a'), - this.$t('@.note-placeholders.b'), - this.$t('@.note-placeholders.c'), - this.$t('@.note-placeholders.d'), - this.$t('@.note-placeholders.e'), - this.$t('@.note-placeholders.f') + this.$t('_postForm._placeholders.a'), + this.$t('_postForm._placeholders.b'), + this.$t('_postForm._placeholders.c'), + this.$t('_postForm._placeholders.d'), + this.$t('_postForm._placeholders.e'), + this.$t('_postForm._placeholders.f') ]; const x = xs[Math.floor(Math.random() * xs.length)]; - + return this.renote - ? opts.mobile ? this.$t('@.post-form.option-quote-placeholder') : this.$t('@.post-form.quote-placeholder') + ? this.$t('_postForm.quotePlaceholder') : this.reply - ? this.$t('@.post-form.reply-placeholder') + ? this.$t('_postForm.replyPlaceholder') : x; }, submitText(): string { return this.renote - ? this.$t('@.post-form.renote') + ? this.$t('renote') : this.reply - ? this.$t('@.post-form.reply') - : this.$t('@.post-form.submit'); + ? this.$t('reply') + : this.$t('_postForm.post'); }, canPost(): boolean { return !this.posting && (1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && - (length(this.text.trim()) <= this.maxNoteTextLength) && + (length(this.text.trim()) <= 500) && (!this.poll || this.pollChoices.length >= 2); } }, - created() { - this.$root.getMeta().then(meta => { - this.maxNoteTextLength = meta.maxNoteTextLength; - }); - }, - mounted() { if (this.initialText) { this.text = this.initialText; @@ -153,10 +210,6 @@ export default (opts) => ({ // デフォルト公開範囲 this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility); - if (this.reply && this.reply.localOnly) { - this.localOnly = true; - } - // 公開以外ã¸ã®ãƒªãƒ—ライ時ã¯å…ƒã®å…¬é–‹ç¯„囲を引ã継ã if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { this.visibility = this.reply.visibility; @@ -175,6 +228,11 @@ export default (opts) => ({ } } + if (this.specified) { + this.visibility = 'specified'; + this.visibleUsers.push(this.specified); + } + // keep cw when reply if (this.$store.state.settings.keepCw && this.reply && this.reply.cw) { this.useCw = true; @@ -220,10 +278,7 @@ export default (opts) => ({ }); }); } - // hack ä½ç½®æƒ…å ±æŠ•ç¨¿ãŒå‹•ãよã†ã«ãªã£ãŸã‚‰é©ç”¨ã™ã‚‹ - this.geo = null; this.visibility = init.visibility; - this.localOnly = init.localOnly; this.quoteId = init.renote ? init.renote.id : null; } @@ -250,26 +305,50 @@ export default (opts) => ({ (this.$refs.text as any).focus(); }, - chooseFile() { + chooseFileFrom(ev) { + this.$root.menu({ + items: [{ + type: 'label', + text: this.$t('attachFile'), + }, { + text: this.$t('upload'), + icon: faUpload, + action: () => { this.chooseFileFromPc() } + }, { + text: this.$t('fromDrive'), + icon: faCloud, + action: () => { this.chooseFileFromDrive() } + }, { + text: this.$t('fromUrl'), + icon: faLink, + action: () => { this.chooseFileFromUrl() } + }], + source: ev.currentTarget || ev.target + }); + }, + + chooseFileFromPc() { (this.$refs.file as any).click(); }, chooseFileFromDrive() { - this.$chooseDriveFile({ - multiple: true - }).then(files => { - for (const x of files) this.attachMedia(x); + selectDriveFile(this.$root, true).then(files => { + for (const file of files) { + this.attachMedia(file); + } }); }, attachMedia(driveFile) { this.files.push(driveFile); - this.$emit('change-attached-files', this.files); }, detachMedia(id) { this.files = this.files.filter(x => x.id != id); - this.$emit('change-attached-files', this.files); + }, + + updateMedia(file) { + Vue.set(this.files, this.files.findIndex(x => x.id === file.id), file); }, onChangeFile() { @@ -292,64 +371,23 @@ export default (opts) => ({ this.saveDraft(); }, - setGeo() { - if (navigator.geolocation == null) { - this.$root.dialog({ - type: 'warning', - text: this.$t('@.post-form.geolocation-alert') - }); - return; - } - - navigator.geolocation.getCurrentPosition(pos => { - this.geo = pos.coords; - this.$emit('geo-attached', this.geo); - }, err => { - this.$root.dialog({ - type: 'error', - title: this.$t('@.post-form.error'), - text: err.message - }); - }, { - enableHighAccuracy: true - }); - }, - - removeGeo() { - this.geo = null; - this.$emit('geo-dettached'); - }, - setVisibility() { const w = this.$root.new(MkVisibilityChooser, { source: this.$refs.visibilityButton, - currentVisibility: this.localOnly ? `local-${this.visibility}` : this.visibility + currentVisibility: this.visibility }); w.$once('chosen', v => { this.applyVisibility(v); }); - this.$once('hook:beforeDestroy', () => { - w.close(); - }); }, applyVisibility(v: string) { - const m = v.match(/^local-(.+)/); - if (m) { - this.localOnly = true; - this.visibility = m[1]; - } else { - this.localOnly = false; - this.visibility = v; - } + this.visibility = v; }, addVisibleUser() { - this.$root.dialog({ - title: this.$t('@.post-form.enter-username'), - user: true - }).then(({ canceled, result: user }) => { - if (canceled) return; + const vm = this.$root.new(MkUserSelect, {}); + vm.$once('selected', user => { this.visibleUsers.push(user); }); }, @@ -377,16 +415,7 @@ export default (opts) => ({ const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - const name = this.$store.state.settings.pasteDialog - ? await this.$root.dialog({ - title: this.$t('@.post-form.enter-file-name'), - input: { - default: formatted - }, - allowEmpty: false - }).then(({ canceled, result }) => canceled ? false : result) - : formatted; - if (name) this.upload(file, name); + this.upload(file, formatted); } } @@ -411,6 +440,7 @@ export default (opts) => ({ }, onDragover(e) { + if (!e.dataTransfer.items[0]) return; const isFile = e.dataTransfer.items[0].kind == 'file'; const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; if (isFile || isDriveFile) { @@ -449,22 +479,6 @@ export default (opts) => ({ //#endregion }, - async emoji() { - const Picker = await import('../../desktop/views/components/emoji-picker-dialog.vue').then(m => m.default); - const button = this.$refs.emoji; - const rect = button.getBoundingClientRect(); - const vm = this.$root.new(Picker, { - x: button.offsetWidth + rect.left + window.pageXOffset, - y: rect.top + window.pageYOffset - }); - vm.$once('chosen', emoji => { - insertTextAtCursor(this.$refs.text, emoji); - }); - this.$once('hook:beforeDestroy', () => { - vm.close(); - }); - }, - saveDraft() { if (this.instant) return; @@ -490,13 +504,8 @@ export default (opts) => ({ localStorage.setItem('drafts', JSON.stringify(data)); }, - kao() { - this.text += getFace(); - }, - post() { this.posting = true; - const viaMobile = opts.mobile && !this.$store.state.settings.disableViaMobile; this.$root.api('notes/create', { text: this.text == '' ? undefined : this.text, fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, @@ -506,23 +515,12 @@ export default (opts) => ({ cw: this.useCw ? this.cw || '' : undefined, visibility: this.visibility, visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, - localOnly: this.localOnly, - 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 + viaMobile: this.$root.isMobile }).then(data => { this.clear(); this.deleteDraft(); this.$emit('posted'); - if (opts.onSuccess) opts.onSuccess(this); }).catch(err => { - if (opts.onSuccess) opts.onFailure(this); }).then(() => { this.posting = false; }); @@ -533,5 +531,217 @@ export default (opts) => ({ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); } }, + + cancel() { + this.$emit('cancel'); + }, + + insertMention() { + const vm = this.$root.new(MkUserSelect, {}); + vm.$once('selected', user => { + insertTextAtCursor(this.$refs.text, getAcct(user) + ' '); + }); + }, + + insertEmoji(ev) { + const vm = this.$root.new(XEmojiPicker, { + source: ev.currentTarget || ev.target + }).$once('chosen', emoji => { + insertTextAtCursor(this.$refs.text, emoji); + vm.close(); + }); + } } }); +</script> + +<style lang="scss" scoped> +.gafaadew { + background: var(--panel); + border-radius: var(--radius); + box-shadow: 0 0 2px rgba(#000, 0.1); + + > header { + z-index: 1000; + height: 66px; + + @media (max-width: 500px) { + height: 50px; + } + + > .cancel { + padding: 0; + font-size: 20px; + width: 64px; + line-height: 66px; + + @media (max-width: 500px) { + width: 50px; + line-height: 50px; + } + } + + > div { + position: absolute; + top: 0; + right: 0; + + > .text-count { + line-height: 66px; + + @media (max-width: 500px) { + line-height: 50px; + } + } + + > .submit { + margin: 16px; + padding: 0 16px; + line-height: 34px; + vertical-align: bottom; + border-radius: 4px; + + @media (max-width: 500px) { + margin: 8px; + } + + &:disabled { + opacity: 0.7; + } + } + } + } + + > .form { + max-width: 500px; + margin: 0 auto; + + > .preview { + padding: 16px; + } + + > .with-quote { + margin: 0 0 8px 0; + color: var(--accent); + + > button { + padding: 4px 8px; + color: var(--accentAlpha04); + + &:hover { + color: var(--accentAlpha06); + } + + &:active { + color: var(--accentDarken30); + } + } + } + + > .to-specified { + padding: 6px 24px; + margin-bottom: 8px; + overflow: auto; + white-space: nowrap; + + @media (max-width: 500px) { + padding: 6px 16px; + } + + > .visibleUsers { + display: inline; + top: -1px; + font-size: 14px; + + > button { + padding: 4px; + border-radius: 8px; + } + + > span { + margin-right: 14px; + padding: 8px 0 8px 8px; + border-radius: 8px; + background: var(--nwjktjjq); + + > button { + padding: 4px 8px; + } + } + } + } + + > input { + z-index: 1; + } + + > input, + > textarea { + display: block; + box-sizing: border-box; + padding: 0 24px; + margin: 0; + width: 100%; + font-size: 16px; + border: none; + border-radius: 0; + background: transparent; + color: var(--fg); + font-family: initial; + + @media (max-width: 500px) { + padding: 0 16px; + } + + &:focus { + outline: none; + } + + &:disabled { + opacity: 0.5; + } + } + + > textarea { + max-width: 100%; + min-width: 100%; + min-height: 90px; + + @media (max-width: 500px) { + min-height: 80px; + } + } + + > .mk-uploader { + margin: 8px 0 0 0; + padding: 8px; + } + + > .file { + display: none; + } + + > footer { + padding: 0 16px 16px 16px; + + @media (max-width: 500px) { + padding: 0 8px 8px 8px; + } + + > * { + display: inline-block; + padding: 0; + margin: 0; + font-size: 16px; + width: 48px; + height: 48px; + border-radius: 6px; + + &:hover { + background: var(--geavgsxy); + } + } + } + } +} +</style> diff --git a/src/client/components/reaction-icon.vue b/src/client/components/reaction-icon.vue new file mode 100644 index 0000000000000000000000000000000000000000..368ddc0efcb4c75d5ab52f65963266a715a1c313 --- /dev/null +++ b/src/client/components/reaction-icon.vue @@ -0,0 +1,32 @@ +<template> +<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true" :no-style="noStyle"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +export default Vue.extend({ + i18n, + props: { + reaction: { + type: String, + required: true + }, + noStyle: { + type: Boolean, + required: false, + default: false + }, + }, + data() { + return { + customEmojis: [] + }; + }, + created() { + this.$root.getMeta().then(meta => { + if (meta && meta.emojis) this.customEmojis = meta.emojis; + }); + }, +}); +</script> diff --git a/src/client/components/reaction-picker.vue b/src/client/components/reaction-picker.vue new file mode 100644 index 0000000000000000000000000000000000000000..00b964f07ca98e3725e5c0600345c8fbbc583ca5 --- /dev/null +++ b/src/client/components/reaction-picker.vue @@ -0,0 +1,229 @@ +<template> +<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap"> + <div class="rdfaahpb"> + <transition-group + name="reaction-fade" + tag="div" + class="buttons" + ref="buttons" + :class="{ showFocus }" + :css="false" + @before-enter="beforeEnter" + @enter="enter" + mode="out-in" + appear + > + <button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :data-index="i" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction"><x-reaction-icon :reaction="reaction"/></button> + </transition-group> + <input class="text" v-model="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> + </div> +</x-popup> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import { emojiRegex } from '../../misc/emoji-regex'; +import XReactionIcon from './reaction-icon.vue'; +import XPopup from './popup.vue'; + +export default Vue.extend({ + i18n, + + components: { + XPopup, + XReactionIcon, + }, + + props: { + source: { + required: true + }, + + reactions: { + required: false + }, + + showFocus: { + type: Boolean, + required: false, + default: false + }, + }, + + data() { + return { + rs: this.reactions || this.$store.state.settings.reactions, + text: null, + focus: null + }; + }, + + computed: { + keymap(): any { + return { + 'esc': this.close, + 'enter|space|plus': this.choose, + 'up|k': this.focusUp, + 'left|h|shift+tab': this.focusLeft, + 'right|l|tab': this.focusRight, + 'down|j': this.focusDown, + '1': () => this.react(this.rs[0]), + '2': () => this.react(this.rs[1]), + '3': () => this.react(this.rs[2]), + '4': () => this.react(this.rs[3]), + '5': () => this.react(this.rs[4]), + '6': () => this.react(this.rs[5]), + '7': () => this.react(this.rs[6]), + '8': () => this.react(this.rs[7]), + '9': () => this.react(this.rs[8]), + '0': () => this.react(this.rs[9]), + }; + }, + }, + + watch: { + focus(i) { + this.$refs.buttons.children[i].elm.focus(); + } + }, + + mounted() { + this.focus = 0; + }, + + methods: { + close() { + this.$refs.popup.close(); + }, + + react(reaction) { + this.$emit('chosen', reaction); + }, + + reactText() { + if (!this.text) return; + this.react(this.text); + }, + + tryReactText() { + if (!this.text) return; + if (!this.text.match(emojiRegex)) return; + this.reactText(); + }, + + focusUp() { + this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5); + }, + + focusDown() { + this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5); + }, + + focusRight() { + this.focus = this.focus == 9 ? 0 : (this.focus + 1); + }, + + focusLeft() { + this.focus = this.focus == 0 ? 9 : (this.focus - 1); + }, + + choose() { + this.$refs.buttons.children[this.focus].elm.click(); + }, + + beforeEnter(el) { + el.style.opacity = 0; + el.style.transform = 'scale(0.7)'; + }, + + enter(el, done) { + el.style.transition = [getComputedStyle(el).transition, 'transform 1s cubic-bezier(0.23, 1, 0.32, 1)', 'opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1)'].filter(x => x != '').join(','); + setTimeout(() => { + el.style.opacity = 1; + el.style.transform = 'scale(1)'; + setTimeout(done, 1000); + }, 0 * el.dataset.index) + }, + } +}); +</script> + +<style lang="scss" scoped> +.rdfaahpb { + > .buttons { + padding: 6px 6px 0 6px; + width: 212px; + box-sizing: border-box; + text-align: center; + + @media (max-width: 1025px) { + padding: 8px 8px 0 8px; + width: 256px; + } + + &.showFocus { + > button:focus { + z-index: 1; + + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border: 2px solid var(--focus); + border-radius: 4px; + } + } + } + + > button { + padding: 0; + width: 40px; + height: 40px; + font-size: 24px; + border-radius: 2px; + + @media (max-width: 1025px) { + width: 48px; + height: 48px; + font-size: 26px; + } + + > * { + height: 1em; + } + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &:active { + background: var(--accent); + box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); + } + } + } + + > .text { + width: 208px; + padding: 8px; + margin: 0 0 6px 0; + box-sizing: border-box; + text-align: center; + font-size: 16px; + outline: none; + border: none; + background: transparent; + color: var(--fg); + + @media (max-width: 1025px) { + width: 256px; + margin: 4px 0 8px 0; + } + } +} +</style> diff --git a/src/client/components/reactions-viewer.details.vue b/src/client/components/reactions-viewer.details.vue new file mode 100644 index 0000000000000000000000000000000000000000..ea2523a11f495b4a7970915ddc5127a51db37a03 --- /dev/null +++ b/src/client/components/reactions-viewer.details.vue @@ -0,0 +1,117 @@ +<template> +<transition name="zoom-in-top"> + <div class="buebdbiu" ref="popover" v-if="show"> + <template v-if="users.length <= 10"> + <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> + <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> + <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> + </b> + </template> + <template v-if="10 < users.length"> + <b v-for="u in users" :key="u.id" style="margin-right: 12px;"> + <mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> + <mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> + </b> + <span slot="omitted">+{{ count - 10 }}</span> + </template> + </div> +</transition> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + props: { + reaction: { + type: String, + required: true, + }, + users: { + type: Array, + required: true, + }, + count: { + type: Number, + required: true, + }, + source: { + required: true, + } + }, + data() { + return { + show: false + }; + }, + mounted() { + this.show = true; + + this.$nextTick(() => { + const popover = this.$refs.popover as any; + + if (this.source == null) { + this.destroyDom(); + return; + } + const rect = this.source.getBoundingClientRect(); + + const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); + const y = rect.top + window.pageYOffset + this.source.offsetHeight; + popover.style.left = (x - 28) + 'px'; + popover.style.top = (y + 16) + 'px'; + }); + } + methods: { + close() { + this.show = false; + setTimeout(this.destroyDom, 300); + } + } +}) +</script> + +<style lang="scss" scoped> +.buebdbiu { + z-index: 10000; + display: block; + position: absolute; + max-width: 240px; + font-size: 0.8em; + padding: 6px 8px; + background: var(--panel); + text-align: center; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + pointer-events: none; + transform-origin: center -16px; + + &:before { + content: ""; + pointer-events: none; + display: block; + position: absolute; + top: -28px; + left: 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; + left: 12px; + border-top: solid 14px transparent; + border-right: solid 14px transparent; + border-bottom: solid 14px var(--panel); + border-left: solid 14px transparent; + } +} +</style> diff --git a/src/client/app/common/views/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue similarity index 61% rename from src/client/app/common/views/components/reactions-viewer.reaction.vue rename to src/client/components/reactions-viewer.reaction.vue index dade012c298969758b75ed8887edb92858b09a87..a878a283ff1aed4e739d0a9eb8d948aeb86c9e87 100644 --- a/src/client/app/common/views/components/reactions-viewer.reaction.vue +++ b/src/client/components/reactions-viewer.reaction.vue @@ -1,26 +1,27 @@ <template> <span - class="reaction" + class="reaction _button" :class="{ reacted: note.myReaction == reaction }" @click="toggleReaction(reaction)" v-if="count > 0" - v-particle="!isMe" @mouseover="onMouseover" @mouseleave="onMouseleave" ref="reaction" > - <mk-reaction-icon :reaction="reaction" ref="icon"/> + <x-reaction-icon :reaction="reaction" ref="icon"/> <span>{{ count }}</span> </span> </template> <script lang="ts"> import Vue from 'vue'; -import Icon from './reaction-icon.vue'; -import anime from 'animejs'; import XDetails from './reactions-viewer.details.vue'; +import XReactionIcon from './reaction-icon.vue'; export default Vue.extend({ + components: { + XReactionIcon + }, props: { reaction: { type: String, @@ -126,83 +127,41 @@ export default Vue.extend({ } }, anime() { - if (this.$store.state.device.reduceMotion) return; if (document.hidden) return; - this.$nextTick(() => { - if (this.$refs.icon == null) return; - - const rect = this.$refs.icon.$el.getBoundingClientRect(); - - const x = rect.left; - const y = rect.top; - - const icon = new Icon({ - parent: this, - propsData: { - reaction: this.reaction - } - }).$mount(); - - icon.$el.style.position = 'absolute'; - icon.$el.style.zIndex = 100; - icon.$el.style.top = (y + window.scrollY) + 'px'; - icon.$el.style.left = (x + window.scrollX) + 'px'; - icon.$el.style.fontSize = window.getComputedStyle(this.$refs.icon.$el).fontSize; - - document.body.appendChild(icon.$el); - - anime({ - targets: icon.$el, - opacity: [1, 0], - translateY: [0, -64], - duration: 1000, - easing: 'linear', - complete: () => { - icon.destroyDom(); - } - }); - }); + // TODO }, } }); </script> -<style lang="stylus" scoped> -.reaction - display inline-block - height 32px - margin 2px - padding 0 6px - border-radius 4px - cursor pointer - - &, * - -webkit-touch-callout none - -webkit-user-select none - -khtml-user-select none - -moz-user-select none - -ms-user-select none - user-select none +<style lang="scss" scoped> +.reaction { + display: inline-block; + height: 32px; + margin: 2px; + padding: 0 6px; + border-radius: 4px; - * - user-select none - pointer-events none + &.reacted { + background: var(--accent); - &.reacted - background var(--primary) - - > span - color var(--primaryForeground) + > span { + color: #fff; + } + } - &:not(.reacted) - background var(--reactionViewerButtonBg) + &:not(.reacted) { + background: rgba(0, 0, 0, 0.05); - &:hover - background var(--reactionViewerButtonHoverBg) + &:hover { + background: rgba(0, 0, 0, 0.1); + } + } - > span - font-size 1.1em - line-height 32px - color var(--text) + > span { + font-size: 0.9em; + line-height: 32px; + } +} </style> diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue similarity index 77% rename from src/client/app/common/views/components/reactions-viewer.vue rename to src/client/components/reactions-viewer.vue index 9701d2481a92b4a276bfc70424fc457a52b0075f..d089cf682c15d44cead582426c7bca60885d0ebf 100644 --- a/src/client/app/common/views/components/reactions-viewer.vue +++ b/src/client/components/reactions-viewer.vue @@ -31,17 +31,18 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-reactions-viewer - margin: 4px -2px +<style lang="scss" scoped> +.mk-reactions-viewer { + margin: 4px -2px 0 -2px; - &:empty - display none + &:empty { + display: none; + } - &.isMe - > span - cursor default !important - - &:hover - background var(--reactionViewerButtonBg) !important + &.isMe { + > span { + cursor: default !important; + } + } +} </style> diff --git a/src/client/components/renote-picker.vue b/src/client/components/renote-picker.vue new file mode 100644 index 0000000000000000000000000000000000000000..d8258d5f5df9210a3c454f1ab9f2598ea1c8bcda --- /dev/null +++ b/src/client/components/renote-picker.vue @@ -0,0 +1,94 @@ +<template> +<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap"> + <div class="rdfaahpc"> + <button class="_button" @click="renote()"><fa :icon="faRetweet"/></button> + <button class="_button" @click="quote()"><fa :icon="faQuoteRight"/></button> + </div> +</x-popup> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faQuoteRight, faRetweet } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import XPopup from './popup.vue'; + +export default Vue.extend({ + i18n, + + components: { + XPopup, + }, + + props: { + note: { + type: Object, + required: true + }, + + source: { + required: true + }, + }, + + data() { + return { + faQuoteRight, faRetweet + }; + }, + + computed: { + keymap(): any { + return { + 'esc': this.close, + }; + } + }, + + methods: { + renote() { + (this as any).$root.api('notes/create', { + renoteId: this.note.id + }).then(() => { + this.$emit('closed'); + this.destroyDom(); + }); + }, + + quote() { + this.$emit('closed'); + this.destroyDom(); + this.$root.post({ + renote: this.note, + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.rdfaahpc { + padding: 4px; + + > button { + padding: 0; + width: 40px; + height: 40px; + font-size: 16px; + border-radius: 2px; + + > * { + height: 1em; + } + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &:active { + background: var(--accent); + box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); + } + } +} +</style> diff --git a/src/client/components/sequential-entrance.vue b/src/client/components/sequential-entrance.vue new file mode 100644 index 0000000000000000000000000000000000000000..70e486719ed24586147d5b9e718b6a5d2fa74241 --- /dev/null +++ b/src/client/components/sequential-entrance.vue @@ -0,0 +1,63 @@ +<template> +<transition-group + name="staggered-fade" + tag="div" + :css="false" + @before-enter="beforeEnter" + @enter="enter" + @leave="leave" + mode="out-in" + appear +> + <slot></slot> +</transition-group> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + delay: { + type: Number, + required: false, + default: 40 + }, + direction: { + type: String, + required: false, + default: 'down' + } + }, + methods: { + beforeEnter(el) { + el.style.opacity = 0; + el.style.transform = this.direction === 'down' ? 'translateY(-64px)' : 'translateY(64px)'; + }, + enter(el, done) { + el.style.transition = [getComputedStyle(el).transition, 'transform 0.7s cubic-bezier(0.23, 1, 0.32, 1)', 'opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1)'].filter(x => x != '').join(','); + setTimeout(() => { + el.style.opacity = 1; + el.style.transform = 'translateY(0px)'; + setTimeout(done, 700); + }, this.delay * el.dataset.index) + }, + leave(el, done) { + setTimeout(() => { + el.style.opacity = 0; + el.style.transform = this.direction === 'down' ? 'translateY(64px)' : 'translateY(-64px)'; + setTimeout(done, 700); + }, this.delay * el.dataset.index) + }, + focus() { + this.$slots.default[0].elm.focus(); + } + } +}); +</script> + +<style lang="scss"> +.staggered-fade-move { + transition: transform 0.7s !important; +} +</style> diff --git a/src/client/components/signin-dialog.vue b/src/client/components/signin-dialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..dbc63c93bf52c4e805e8c7ffbb315fd2f70da88e --- /dev/null +++ b/src/client/components/signin-dialog.vue @@ -0,0 +1,37 @@ +<template> +<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }"> + <template #header>{{ $t('login') }}</template> + <x-signin :auto-set="autoSet" @login="onLogin"/> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import XWindow from './window.vue'; +import XSignin from './signin.vue'; + +export default Vue.extend({ + i18n, + + components: { + XSignin, + XWindow, + }, + + props: { + autoSet: { + type: Boolean, + required: false, + default: false, + } + }, + + methods: { + onLogin(res) { + this.$emit('login', res); + this.$refs.window.close(); + } + } +}); +</script> diff --git a/src/client/app/common/views/components/signin.vue b/src/client/components/signin.vue similarity index 69% rename from src/client/app/common/views/components/signin.vue rename to src/client/components/signin.vue index bb4a6605bd31a9c23e2202fb51b9db70717c211a..dc6fad1c5d6c23e645843874ab872aa0c9776706 100644 --- a/src/client/app/common/views/components/signin.vue +++ b/src/client/components/signin.vue @@ -1,17 +1,17 @@ <template> -<form class="mk-signin" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> +<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> <div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div> <div class="normal-signin" v-if="!totpLogin"> - <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange"> + <mk-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange"> <span>{{ $t('username') }}</span> <template #prefix>@</template> <template #suffix>@{{ host }}</template> - </ui-input> - <ui-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required> + </mk-input> + <mk-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required> <span>{{ $t('password') }}</span> - <template #prefix><fa icon="lock"/></template> - </ui-input> - <ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button> + <template #prefix><fa :icon="faLock"/></template> + </mk-input> + <mk-button type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $t('loggingIn') : $t('login') }}</mk-button> <p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p> <p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p> <p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p> @@ -19,24 +19,24 @@ <div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="user && user.securityKeys" class="twofa-group tap-group"> <p>{{ $t('tap-key') }}</p> - <ui-button @click="queryKey" v-if="!queryingKey"> + <mk-button @click="queryKey" v-if="!queryingKey"> {{ $t('@.error.retry') }} - </ui-button> + </mk-button> </div> <div class="or-hr" v-if="user && user.securityKeys"> <p class="or-msg">{{ $t('or') }}</p> </div> <div class="twofa-group totp-group"> - <p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p> - <ui-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required> + <p style="margin-bottom:0;">{{ $t('twoStepAuthentication') }}</p> + <mk-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required> <span>{{ $t('password') }}</span> - <template #prefix><fa icon="lock"/></template> - </ui-input> - <ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required> - <span>{{ $t('@.2fa') }}</span> - <template #prefix><fa icon="gavel"/></template> - </ui-input> - <ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button> + <template #prefix><fa :icon="faLock"/></template> + </mk-input> + <mk-input v-model="token" type="number" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required> + <span>{{ $t('token') }}</span> + <template #prefix><fa :icon="faGavel"/></template> + </mk-input> + <mk-button type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $t('loggingIn') : $t('login') }}</mk-button> </div> </div> </form> @@ -44,19 +44,32 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; -import { apiUrl, host } from '../../../config'; import { toUnicode } from 'punycode'; -import { hexifyAB } from '../../scripts/2fa'; +import { faLock, faGavel } from '@fortawesome/free-solid-svg-icons'; +import MkButton from './ui/button.vue'; +import MkInput from './ui/input.vue'; +import i18n from '../i18n'; +import { apiUrl, host } from '../config'; +import { hexifyAB } from '../scripts/2fa'; export default Vue.extend({ - i18n: i18n('common/views/components/signin.vue'), + i18n, + + components: { + MkButton, + MkInput, + }, props: { withAvatar: { type: Boolean, required: false, default: true + }, + autoSet: { + type: Boolean, + required: false, + default: false, } }, @@ -74,6 +87,7 @@ export default Vue.extend({ credential: null, challengeData: null, queryingKey: false, + faLock, faGavel }; }, @@ -81,6 +95,13 @@ export default Vue.extend({ this.$root.getMeta().then(meta => { this.meta = meta; }); + + if (this.autoSet) { + this.$once('login', res => { + localStorage.setItem('i', res.i); + location.reload(); + }); + } }, methods: { @@ -127,8 +148,7 @@ export default Vue.extend({ challengeId: this.challengeData.challengeId }); }).then(res => { - localStorage.setItem('i', res.i); - location.reload(); + this.$emit('login', res); }).catch(err => { if (err === null) return; this.$root.dialog({ @@ -141,7 +161,6 @@ export default Vue.extend({ onSubmit() { this.signing = true; - if (!this.totpLogin && this.user && this.user.twoFactorEnabled) { if (window.PublicKeyCredential && this.user.securityKeys) { this.$root.api('signin', { @@ -171,12 +190,11 @@ export default Vue.extend({ password: this.password, token: this.user && this.user.twoFactorEnabled ? this.token : undefined }).then(res => { - localStorage.setItem('i', res.i); - location.reload(); + this.$emit('login', res); }).catch(() => { this.$root.dialog({ type: 'error', - text: this.$t('login-failed') + text: this.$t('loginFailed') }); this.signing = false; }); @@ -186,63 +204,16 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-signin - color #555 - - .or-hr, - .or-hr .or-msg, - .twofa-group, - .twofa-group p - color var(--text) - - .tap-group > button - margin-bottom 1em - - .securityKeys .or-hr - & - position relative - - .or-msg - &:before - right 100% - margin-right 0.125em - - &:after - left 100% - margin-left 0.125em - - &:before, &:after - content "" - position absolute - top 50% - width 100% - height 2px - background #555 - - & - position relative - margin auto - left 0 - right 0 - top 0 - bottom 0 - font-size 1.5em - height 1.5em - width 3em - text-align center - - &.signing - &, * - cursor wait !important - - > .avatar - margin 0 auto 0 auto - width 64px - height 64px - background #ddd - background-position center - background-size cover - border-radius 100% - +<style lang="scss" scoped> +.eppvobhk { + > .avatar { + margin: 0 auto 0 auto; + width: 64px; + height: 64px; + background: #ddd; + background-position: center; + background-size: cover; + border-radius: 100%; + } +} </style> diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..76421d44ece5b30441ae062f6452a57d4434eed0 --- /dev/null +++ b/src/client/components/signup-dialog.vue @@ -0,0 +1,22 @@ +<template> +<x-window @closed="() => { $emit('closed'); destroyDom(); }"> + <template #header>{{ $t('signup') }}</template> + <x-signup/> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import XWindow from './window.vue'; +import XSignup from './signup.vue'; + +export default Vue.extend({ + i18n, + + components: { + XSignup, + XWindow, + }, +}); +</script> diff --git a/src/client/app/common/views/components/signup.vue b/src/client/components/signup.vue similarity index 67% rename from src/client/app/common/views/components/signup.vue rename to src/client/components/signup.vue index 893f6575fb179fce12caf5cd779bff3b61410081..c03a99def6191ea9745f51400a1326e0d6c894d7 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/components/signup.vue @@ -1,62 +1,72 @@ <template> <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> <template v-if="meta"> - <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> + <mk-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required> <span>{{ $t('invitation-code') }}</span> <template #prefix><fa icon="id-card-alt"/></template> <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template> - </ui-input> - <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> + </mk-input> + <mk-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername"> <span>{{ $t('username') }}</span> <template #prefix>@</template> <template #suffix>@{{ host }}</template> <template #desc> - <span v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner" pulse fixed-width/> {{ $t('checking') }}</span> - <span v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</span> - <span v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</span> - <span v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</span> - <span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('invalid-format') }}</span> - <span v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-short') }}</span> - <span v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-long') }}</span> + <span v-if="usernameState == 'wait'" style="color:#999"><fa :icon="faSpinner" pulse fixed-width/> {{ $t('checking') }}</span> + <span v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('available') }}</span> + <span v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('unavailable') }}</span> + <span v-if="usernameState == 'error'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('error') }}</span> + <span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('invalid-format') }}</span> + <span v-if="usernameState == 'min-range'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('too-short') }}</span> + <span v-if="usernameState == 'max-range'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('too-long') }}</span> </template> - </ui-input> - <ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill"> + </mk-input> + <mk-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword"> <span>{{ $t('password') }}</span> - <template #prefix><fa icon="lock"/></template> + <template #prefix><fa :icon="faLock"/></template> <template #desc> - <p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('weak-password') }}</p> - <p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('normal-password') }}</p> - <p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('strong-password') }}</p> + <p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('weak-password') }}</p> + <p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('normal-password') }}</p> + <p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('strong-password') }}</p> </template> - </ui-input> - <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill"> + </mk-input> + <mk-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype"> <span>{{ $t('password') }} ({{ $t('retype') }})</span> - <template #prefix><fa icon="lock"/></template> + <template #prefix><fa :icon="faLock"/></template> <template #desc> - <p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('password-matched') }}</p> - <p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('password-not-matched') }}</p> + <p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('password-matched') }}</p> + <p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('password-not-matched') }}</p> </template> - </ui-input> - <ui-switch v-model="ToSAgreement" v-if="meta.ToSUrl"> - <i18n path="agree-to"> - <a :href="meta.ToSUrl" target="_blank">{{ $t('tos') }}</a> + </mk-input> + <mk-switch v-model="ToSAgreement" v-if="meta.tosUrl"> + <i18n path="agreeTo"> + <a :href="meta.tosUrl" target="_blank">{{ $t('tos') }}</a> </i18n> - </ui-switch> + </mk-switch> <div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div> - <ui-button type="submit" :disabled=" submitting || !(meta.ToSUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'">{{ $t('create') }}</ui-button> + <mk-button type="submit" :disabled=" submitting || !(meta.tosUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'" primary>{{ $t('start') }}</mk-button> </template> </form> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faLock, faExclamationTriangle, faSpinner, faCheck } from '@fortawesome/free-solid-svg-icons'; const getPasswordStrength = require('syuilo-password-strength'); -import { host, url } from '../../../config'; import { toUnicode } from 'punycode'; +import i18n from '../i18n'; +import { host, url } from '../config'; +import MkButton from './ui/button.vue'; +import MkInput from './ui/input.vue'; +import MkSwitch from './ui/switch.vue'; export default Vue.extend({ - i18n: i18n('common/views/components/signup.vue'), + i18n, + + components: { + MkButton, + MkInput, + MkSwitch, + }, data() { return { @@ -71,7 +81,8 @@ export default Vue.extend({ passwordRetypeState: null, meta: {}, submitting: false, - ToSAgreement: false + ToSAgreement: false, + faLock, faExclamationTriangle, faSpinner, faCheck } }, @@ -178,8 +189,3 @@ export default Vue.extend({ } }); </script> - -<style lang="stylus" scoped> -.mk-signup - min-width 302px -</style> diff --git a/src/client/components/sub-note-content.vue b/src/client/components/sub-note-content.vue new file mode 100644 index 0000000000000000000000000000000000000000..e60c19744231a071328a9babc5da59ae3517743e --- /dev/null +++ b/src/client/components/sub-note-content.vue @@ -0,0 +1,65 @@ +<template> +<div class="wrmlmaau"> + <div class="body"> + <span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> + <span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> + <router-link class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><fa :icon="faReply"/></router-link> + <mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/> + <router-link class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</router-link> + </div> + <details v-if="note.files.length > 0"> + <summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary> + <x-media-list :media-list="note.files"/> + </details> + <details v-if="note.poll"> + <summary>{{ $t('poll') }}</summary> + <x-poll :note="note"/> + </details> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faReply } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import XPoll from './poll.vue'; +import XMediaList from './media-list.vue'; + +export default Vue.extend({ + i18n, + components: { + XPoll, + XMediaList, + }, + props: { + note: { + type: Object, + required: true + } + }, + data() { + return { + faReply + }; + } +}); +</script> + +<style lang="scss" scoped> +.wrmlmaau { + overflow-wrap: break-word; + + > .body { + > .reply { + margin-right: 6px; + color: var(--accent); + } + + > .rp { + margin-left: 4px; + font-style: oblique; + color: var(--renote); + } + } +} +</style> diff --git a/src/client/app/common/views/components/time.vue b/src/client/components/time.vue similarity index 52% rename from src/client/app/common/views/components/time.vue rename to src/client/components/time.vue index 8cfcc4cb4f33032b552cd855ed337544ffb1ab53..922067b4d5efd055e7cc037b8267429c19379d16 100644 --- a/src/client/app/common/views/components/time.vue +++ b/src/client/components/time.vue @@ -1,17 +1,17 @@ <template> <time class="mk-time" :title="absolute"> - <span v-if=" mode == 'relative' ">{{ relative }}</span> - <span v-if=" mode == 'absolute' ">{{ absolute }}</span> - <span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span> + <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'; -import i18n from '../../../i18n'; +import i18n from '../i18n'; export default Vue.extend({ - i18n: i18n(), + i18n, props: { time: { type: [Date, String], @@ -39,15 +39,15 @@ export default Vue.extend({ const time = this._time; const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/; return ( - ago >= 31536000 ? this.$t('@.time.years_ago') .replace('{}', (~~(ago / 31536000)).toString()) : - ago >= 2592000 ? this.$t('@.time.months_ago') .replace('{}', (~~(ago / 2592000)).toString()) : - ago >= 604800 ? this.$t('@.time.weeks_ago') .replace('{}', (~~(ago / 604800)).toString()) : - ago >= 86400 ? this.$t('@.time.days_ago') .replace('{}', (~~(ago / 86400)).toString()) : - ago >= 3600 ? this.$t('@.time.hours_ago') .replace('{}', (~~(ago / 3600)).toString()) : - ago >= 60 ? this.$t('@.time.minutes_ago').replace('{}', (~~(ago / 60)).toString()) : - ago >= 10 ? this.$t('@.time.seconds_ago').replace('{}', (~~(ago % 60)).toString()) : - ago >= -1 ? this.$t('@.time.just_now') : - ago < -1 ? this.$t('@.time.future') : + ago >= 31536000 ? this.$t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) : + ago >= 2592000 ? this.$t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) : + ago >= 604800 ? this.$t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) : + ago >= 86400 ? this.$t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) : + ago >= 3600 ? this.$t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) : + ago >= 60 ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : + ago >= 10 ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : + ago >= -1 ? this.$t('_ago.justNow') : + ago < -1 ? this.$t('_ago.future') : this.$t('@.time.unknown')); } }, diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..f5edb185501fa3ce65d7ef0c4b0665612ffaca7b --- /dev/null +++ b/src/client/components/timeline.vue @@ -0,0 +1,118 @@ +<template> +<x-notes ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNotes from './notes.vue'; + +export default Vue.extend({ + components: { + XNotes + }, + + props: { + src: { + type: String, + required: true + }, + list: { + required: false + }, + antenna: { + required: false + } + }, + + data() { + return { + connection: null, + pagination: null, + baseQuery: { + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }, + query: {}, + }; + }, + + created() { + this.$once('hook:beforeDestroy', () => { + this.connection.dispose(); + }); + + const prepend = note => { + (this.$refs.tl as any).prepend(note); + }; + + const onUserAdded = () => { + (this.$refs.tl as any).reload(); + }; + + const onUserRemoved = () => { + (this.$refs.tl as any).reload(); + }; + + let endpoint; + + if (this.src == 'antenna') { + endpoint = 'antennas/notes'; + this.query = { + antennaId: this.antenna.id + }; + this.connection = this.$root.stream.connectToChannel('antenna', { + antennaId: this.antenna.id + }); + this.connection.on('note', prepend); + } else if (this.src == 'home') { + endpoint = 'notes/timeline'; + const onChangeFollowing = () => { + this.fetch(); + }; + this.connection = this.$root.stream.useSharedConnection('homeTimeline'); + this.connection.on('note', prepend); + this.connection.on('follow', onChangeFollowing); + this.connection.on('unfollow', onChangeFollowing); + } else if (this.src == 'local') { + endpoint = 'notes/local-timeline'; + this.connection = this.$root.stream.useSharedConnection('localTimeline'); + this.connection.on('note', prepend); + } else if (this.src == 'social') { + endpoint = 'notes/hybrid-timeline'; + this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); + this.connection.on('note', prepend); + } else if (this.src == 'global') { + endpoint = 'notes/global-timeline'; + this.connection = this.$root.stream.useSharedConnection('globalTimeline'); + this.connection.on('note', prepend); + } else if (this.src == 'list') { + endpoint = 'notes/user-list-timeline'; + this.query = { + listId: this.list.id + }; + this.connection = this.$root.stream.connectToChannel('userList', { + listId: this.list.id + }); + this.connection.on('note', prepend); + this.connection.on('userAdded', onUserAdded); + this.connection.on('userRemoved', onUserRemoved); + } + + this.pagination = { + endpoint: endpoint, + limit: 10, + params: init => ({ + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + ...this.baseQuery, ...this.query + }) + }; + }, + + methods: { + focus() { + this.$refs.tl.focus(); + } + } +}); +</script> diff --git a/src/client/components/toast.vue b/src/client/components/toast.vue new file mode 100644 index 0000000000000000000000000000000000000000..fefe91e3bd6e137050f9b7e147371864138198cd --- /dev/null +++ b/src/client/components/toast.vue @@ -0,0 +1,76 @@ +<template> +<div class="mk-toast"> + <transition name="notification-slide" appear @after-leave="() => { destroyDom(); }"> + <x-notification :notification="notification" class="notification" v-if="show"/> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNotification from './notification.vue'; + +export default Vue.extend({ + components: { + XNotification + }, + props: { + notification: { + type: Object, + required: true + } + }, + data() { + return { + show: true + }; + }, + mounted() { + setTimeout(() => { + this.show = false; + }, 6000); + } +}); +</script> + +<style lang="scss" scoped> +.notification-slide-enter-active, .notification-slide-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.notification-slide-enter, .notification-slide-leave-to { + opacity: 0; + transform: translateX(-250px); +} + +.mk-toast { + position: fixed; + z-index: 10000; + left: 0; + width: 250px; + top: 32px; + padding: 0 32px; + pointer-events: none; + + @media (max-width: 700px) { + top: initial; + bottom: 112px; + padding: 0 16px; + } + + @media (max-width: 500px) { + bottom: 92px; + padding: 0 8px; + } + + > .notification { + height: 100%; + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + background-color: var(--toastBg); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + border-radius: 8px; + color: var(--toastFg); + overflow: hidden; + } +} +</style> diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue new file mode 100644 index 0000000000000000000000000000000000000000..4071faa1dde0e03372173ec335d2ad41a33e386d --- /dev/null +++ b/src/client/components/ui/button.vue @@ -0,0 +1,204 @@ +<template> +<component class="bghgjjyj _button" + :is="link ? 'a' : 'button'" + :class="{ inline, primary }" + :type="type" + @click="$emit('click', $event)" + @mousedown="onMousedown" +> + <div ref="ripples" class="ripples"></div> + <div class="content"> + <slot></slot> + </div> +</component> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + type: { + type: String, + required: false + }, + primary: { + type: Boolean, + required: false, + default: false + }, + inline: { + type: Boolean, + required: false, + default: false + }, + link: { + type: Boolean, + required: false, + default: false + }, + autofocus: { + type: Boolean, + required: false, + default: false + }, + wait: { + type: Boolean, + required: false, + default: false + }, + }, + mounted() { + if (this.autofocus) { + this.$nextTick(() => { + this.$el.focus(); + }); + } + }, + methods: { + onMousedown(e: MouseEvent) { + function distance(p, q) { + return Math.hypot(p.x - q.x, p.y - q.y); + } + + function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) { + const origin = {x: circleCenterX, y: circleCenterY}; + const dist1 = distance({x: 0, y: 0}, origin); + const dist2 = distance({x: boxW, y: 0}, origin); + const dist3 = distance({x: 0, y: boxH}, origin); + const dist4 = distance({x: boxW, y: boxH }, origin); + return Math.max(dist1, dist2, dist3, dist4) * 2; + } + + const rect = e.target.getBoundingClientRect(); + + const ripple = document.createElement('div'); + ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px'; + ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px'; + + this.$refs.ripples.appendChild(ripple); + + const circleCenterX = e.clientX - rect.left; + const circleCenterY = e.clientY - rect.top; + + const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY); + + setTimeout(() => { + ripple.style.transform = 'scale(' + (scale / 2) + ')'; + }, 1); + setTimeout(() => { + ripple.style.transition = 'all 1s ease'; + ripple.style.opacity = '0'; + }, 1000); + setTimeout(() => { + if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple); + }, 2000); + } + } +}); +</script> + +<style lang="scss" scoped> +.bghgjjyj { + position: relative; + display: block; + min-width: 100px; + padding: 8px 14px; + text-align: center; + font-weight: normal; + font-size: 14px; + line-height: 24px; + box-shadow: none; + text-decoration: none; + background: var(--buttonBg); + border-radius: 6px; + overflow: hidden; + + &:not(:disabled):hover { + background: var(--buttonHoverBg); + } + + &:not(:disabled):active { + background: var(--buttonHoverBg); + } + + &.primary { + color: #fff; + background: var(--accent); + + &:not(:disabled):hover { + background: var(--jkhztclx); + } + + &:not(:disabled):active { + background: var(--jkhztclx); + } + } + + &:disabled { + opacity: 0.7; + } + + &:focus { + &:after { + content: ""; + pointer-events: none; + position: absolute; + top: -5px; + right: -5px; + bottom: -5px; + left: -5px; + border: 2px solid var(--accentAlpha03); + border-radius: 10px; + } + } + + &.inline + .bghgjjyj { + margin-left: 12px; + } + + &:not(.inline) + .bghgjjyj { + margin-top: 16px; + } + + &.inline { + display: inline-block; + width: auto; + min-width: 100px; + } + + &.primary { + font-weight: bold; + } + + > .ripples { + position: absolute; + z-index: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 6px; + overflow: hidden; + + ::v-deep div { + position: absolute; + width: 2px; + height: 2px; + border-radius: 100%; + background: rgba(0, 0, 0, 0.1); + opacity: 1; + transform: scale(1); + transition: all 0.5s cubic-bezier(0,.5,0,1); + } + } + + &.primary > .ripples ::v-deep div { + background: rgba(0, 0, 0, 0.15); + } + + > .content { + position: relative; + z-index: 1; + } +} +</style> diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue new file mode 100644 index 0000000000000000000000000000000000000000..19820a307d47c581bf6872783b136f4fec697a27 --- /dev/null +++ b/src/client/components/ui/container.vue @@ -0,0 +1,104 @@ +<template> +<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }"> + <header v-if="showHeader"> + <div class="title"><slot name="header"></slot></div> + <slot name="func"></slot> + <button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody"> + <template v-if="showBody"><fa :icon="faAngleUp"/></template> + <template v-else><fa :icon="faAngleDown"/></template> + </button> + </header> + <div v-show="showBody"> + <slot></slot> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + props: { + showHeader: { + type: Boolean, + required: false, + default: true + }, + naked: { + type: Boolean, + required: false, + default: false + }, + bodyTogglable: { + type: Boolean, + required: false, + default: false + }, + expanded: { + type: Boolean, + required: false, + default: true + }, + }, + data() { + return { + showBody: this.expanded, + faAngleUp, faAngleDown + }; + }, + methods: { + toggleContent(show: boolean) { + if (!this.bodyTogglable) return; + this.showBody = show; + } + } +}); +</script> + +<style lang="scss" scoped> +.ukygtjoj { + position: relative; + overflow: hidden; + + & + .ukygtjoj { + margin-top: var(--margin); + } + + &.naked { + background: transparent !important; + box-shadow: none !important; + } + + > header { + position: relative; + + > .title { + margin: 0; + padding: 12px 16px; + + @media (max-width: 500px) { + padding: 8px 10px; + } + + > [data-icon] { + margin-right: 6px; + } + + &:empty { + display: none; + } + } + + > button { + position: absolute; + z-index: 2; + top: 0; + right: 0; + padding: 0; + width: 42px; + height: 100%; + } + } +} +</style> diff --git a/src/client/app/common/views/components/ui/hr.vue b/src/client/components/ui/hr.vue similarity index 88% rename from src/client/app/common/views/components/ui/hr.vue rename to src/client/components/ui/hr.vue index 38572cfcc329b1351770ee4d7e51589595dba3df..ae7f7dbf8e17fa1c8809f9bde796f6e107cfbbce 100644 --- a/src/client/app/common/views/components/ui/hr.vue +++ b/src/client/components/ui/hr.vue @@ -7,7 +7,7 @@ import Vue from 'vue'; export default Vue.extend({}); </script> -<style lang="stylus" scoped> +<style lang="scss" scoped> .evrzpitu margin 16px 0 border-bottom solid var(--lineWidth) var(--faceDivider) diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue new file mode 100644 index 0000000000000000000000000000000000000000..3e87fe261d8c3db3066006613b90d0abc6a46e66 --- /dev/null +++ b/src/client/components/ui/info.vue @@ -0,0 +1,55 @@ +<template> +<div class="fpezltsf" :class="{ warn }"> + <i v-if="warn"><fa :icon="faExclamationTriangle"/></i> + <i v-else><fa :icon="faInfoCircle"/></i> + <slot></slot> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faInfoCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + props: { + warn: { + type: Boolean, + required: false, + default: false + }, + }, + data() { + return { + faInfoCircle, faExclamationTriangle + }; + } +}); +</script> + +<style lang="scss" scoped> +.fpezltsf { + margin: 16px 0; + padding: 16px; + font-size: 90%; + background: var(--infoBg); + color: var(--infoFg); + border-radius: 5px; + + &.warn { + background: var(--infoWarnBg); + color: var(--infoWarnFg); + } + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + > i { + margin-right: 4px; + } +} +</style> diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue new file mode 100644 index 0000000000000000000000000000000000000000..69d842ef0f338149d53d24c3f5fe62ff4bd9cdc7 --- /dev/null +++ b/src/client/components/ui/input.vue @@ -0,0 +1,443 @@ +<template> +<div class="juejbjww" :class="{ focused, filled, inline, disabled }"> + <div class="icon" ref="icon"><slot name="icon"></slot></div> + <div class="input"> + <span class="label" ref="label"><slot></slot></span> + <span class="title" ref="title"> + <slot name="title"></slot> + <span class="warning" v-if="invalid"><fa :icon="faExclamationCircle"/>{{ $refs.input.validationMessage }}</span> + </span> + <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> + <template v-if="type != 'file'"> + <input v-if="debounce" ref="input" + v-debounce="500" + :type="type" + v-model.lazy="v" + :disabled="disabled" + :required="required" + :readonly="readonly" + :placeholder="placeholder" + :pattern="pattern" + :autocomplete="autocomplete" + :spellcheck="spellcheck" + @focus="focused = true" + @blur="focused = false" + @keydown="$emit('keydown', $event)" + @input="onInput" + :list="id" + > + <input v-else ref="input" + :type="type" + v-model="v" + :disabled="disabled" + :required="required" + :readonly="readonly" + :placeholder="placeholder" + :pattern="pattern" + :autocomplete="autocomplete" + :spellcheck="spellcheck" + @focus="focused = true" + @blur="focused = false" + @keydown="$emit('keydown', $event)" + @input="onInput" + :list="id" + > + <datalist :id="id" v-if="datalist"> + <option v-for="data in datalist" :value="data"/> + </datalist> + </template> + <template v-else> + <input ref="input" + type="text" + :value="filePlaceholder" + readonly + @click="chooseFile" + > + <input ref="file" + type="file" + :value="value" + @change="onChangeFile" + > + </template> + <div class="suffix" ref="suffix"><slot name="suffix"></slot></div> + </div> + <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button> + <div class="desc"><slot name="desc"></slot></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import debounce from 'v-debounce'; +import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + directives: { + debounce + }, + props: { + value: { + required: false + }, + type: { + type: String, + required: false + }, + required: { + type: Boolean, + required: false + }, + readonly: { + type: Boolean, + required: false + }, + disabled: { + type: Boolean, + required: false + }, + pattern: { + type: String, + required: false + }, + placeholder: { + type: String, + required: false + }, + autofocus: { + type: Boolean, + required: false, + default: false + }, + autocomplete: { + required: false + }, + spellcheck: { + required: false + }, + debounce: { + required: false + }, + datalist: { + type: Array, + required: false, + }, + inline: { + type: Boolean, + required: false, + default: false + }, + save: { + type: Function, + required: false, + }, + }, + data() { + return { + v: this.value, + focused: false, + invalid: false, + changed: false, + id: Math.random().toString(), + faExclamationCircle + }; + }, + computed: { + filled(): boolean { + return this.v !== '' && this.v != null; + }, + filePlaceholder(): string | null { + if (this.type != 'file') return null; + if (this.v == null) return null; + + if (typeof this.v == 'string') return this.v; + + if (Array.isArray(this.v)) { + return this.v.map(file => file.name).join(', '); + } else { + return this.v.name; + } + } + }, + watch: { + value(v) { + this.v = v; + }, + v(v) { + if (this.type === 'number') { + this.$emit('input', parseInt(v, 10)); + } else { + this.$emit('input', v); + } + + this.invalid = this.$refs.input.validity.badInput; + } + }, + mounted() { + if (this.autofocus) { + this.$nextTick(() => { + this.$refs.input.focus(); + }); + } + + this.$nextTick(() => { + // ã“ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒä½œæˆã•ã‚ŒãŸæ™‚ã€éžè¡¨ç¤ºçŠ¶æ…‹ã§ã‚ã‚‹å ´åˆãŒã‚ã‚‹ + // éžè¡¨ç¤ºçŠ¶æ…‹ã ã¨è¦ç´ ã®å¹…ãªã©ã¯0ã«ãªã£ã¦ã—ã¾ã†ã®ã§ã€å®šæœŸçš„ã«è¨ˆç®—ã™ã‚‹ + const clock = setInterval(() => { + if (this.$refs.prefix) { + this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; + if (this.$refs.prefix.offsetWidth) { + this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px'; + } + } + if (this.$refs.suffix) { + if (this.$refs.suffix.offsetWidth) { + this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px'; + } + } + }, 100); + + this.$once('hook:beforeDestroy', () => { + clearInterval(clock); + }); + }); + + this.$on('keydown', (e: KeyboardEvent) => { + if (e.code == 'Enter') { + this.$emit('enter'); + } + }); + }, + methods: { + focus() { + this.$refs.input.focus(); + }, + togglePassword() { + if (this.type == 'password') { + this.type = 'text' + } else { + this.type = 'password' + } + }, + chooseFile() { + this.$refs.file.click(); + }, + onChangeFile() { + this.v = Array.from((this.$refs.file as any).files); + this.$emit('input', this.v); + this.$emit('change', this.v); + }, + onInput(ev) { + this.changed = true; + this.$emit('change', ev); + } + } +}); +</script> + +<style lang="scss" scoped> +.juejbjww { + position: relative; + margin: 32px 0; + + > .icon { + position: absolute; + top: 0; + left: 0; + width: 24px; + text-align: center; + line-height: 32px; + + &:not(:empty) + .input { + margin-left: 28px; + } + } + + > .input { + position: relative; + + &:before { + content: ''; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: var(--inputBorder); + } + + &:after { + content: ''; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: var(--accent); + opacity: 0; + transform: scaleX(0.12); + transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + will-change: border opacity transform; + } + + > .label { + position: absolute; + z-index: 1; + top: 0; + left: 0; + pointer-events: none; + transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transition-duration: 0.3s; + font-size: 16px; + line-height: 32px; + color: var(--inputLabel); + pointer-events: none; + //will-change transform + transform-origin: top left; + transform: scale(1); + } + + > .title { + position: absolute; + z-index: 1; + top: -17px; + left: 0 !important; + pointer-events: none; + font-size: 16px; + line-height: 32px; + color: var(--inputLabel); + pointer-events: none; + //will-change transform + transform-origin: top left; + transform: scale(.75); + white-space: nowrap; + width: 133%; + overflow: hidden; + text-overflow: ellipsis; + + > .warning { + margin-left: 0.5em; + color: var(--infoWarnFg); + + > svg { + margin-right: 0.1em; + } + } + } + + > input { + display: block; + width: 100%; + margin: 0; + padding: 0; + font: inherit; + font-weight: normal; + font-size: 16px; + line-height: 32px; + color: var(--inputText); + background: transparent; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + box-sizing: border-box; + + &[type='file'] { + display: none; + } + } + + > .prefix, + > .suffix { + display: block; + position: absolute; + z-index: 1; + top: 0; + font-size: 16px; + line-height: 32px; + color: var(--inputLabel); + pointer-events: none; + + &:empty { + display: none; + } + + > * { + display: inline-block; + min-width: 16px; + max-width: 150px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + > .prefix { + left: 0; + padding-right: 4px; + } + + > .suffix { + right: 0; + padding-left: 4px; + } + } + + > .save { + margin: 6px 0 0 0; + font-size: 13px; + } + + > .desc { + margin: 6px 0 0 0; + font-size: 13px; + opacity: 0.7; + + &:empty { + display: none; + } + + * { + margin: 0; + } + } + + &.focused { + > .input { + &:after { + opacity: 1; + transform: scaleX(1); + } + + > .label { + color: var(--accent); + } + } + } + + &.focused, + &.filled { + > .input { + > .label { + top: -17px; + left: 0 !important; + transform: scale(0.75); + } + } + } + + &.inline { + display: inline-block; + margin: 0; + } + + &.disabled { + opacity: 0.7; + + &, * { + cursor: not-allowed !important; + } + } +} +</style> diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue new file mode 100644 index 0000000000000000000000000000000000000000..d953824e00af2f23defbd097346a6da255aff2da --- /dev/null +++ b/src/client/components/ui/pagination.vue @@ -0,0 +1,59 @@ +<template> +<sequential-entrance class="cxiknjgy" :class="{ autoMargin }"> + <slot :items="items"></slot> + <div class="empty" v-if="empty" key="_empty_"> + <slot name="empty"></slot> + </div> + <div class="more" v-if="more" key="_more_"> + <mk-button :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()"> + <template v-if="!moreFetching">{{ $t('loadMore') }}</template> + <template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template> + </mk-button> + </div> +</sequential-entrance> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import MkButton from './button.vue'; +import paging from '../../scripts/paging'; + +export default Vue.extend({ + mixins: [ + paging({}), + ], + + components: { + MkButton + }, + + props: { + pagination: { + required: true + }, + autoMargin: { + required: false, + default: true + } + }, + + data() { + return { + faSpinner + }; + }, +}); +</script> + +<style lang="scss" scoped> +.cxiknjgy { + &.autoMargin > *:not(:last-child) { + margin-bottom: 16px; + + @media (max-width: 500px) { + margin-bottom: 8px; + } + } +} +</style> diff --git a/src/client/components/ui/radio.vue b/src/client/components/ui/radio.vue new file mode 100644 index 0000000000000000000000000000000000000000..7659d147e60a428302c72ec9312baa96986d1ca0 --- /dev/null +++ b/src/client/components/ui/radio.vue @@ -0,0 +1,119 @@ +<template> +<div + class="novjtctn" + :class="{ disabled, checked }" + :aria-checked="checked" + :aria-disabled="disabled" + @click="toggle" +> + <input type="radio" + :disabled="disabled" + > + <span class="button"> + <span></span> + </span> + <span class="label"><slot></slot></span> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + model: { + prop: 'model', + event: 'change' + }, + props: { + model: { + required: false + }, + value: { + required: false + }, + disabled: { + type: Boolean, + default: false + } + }, + computed: { + checked(): boolean { + return this.model === this.value; + } + }, + methods: { + toggle() { + this.$emit('change', this.value); + } + } +}); +</script> + +<style lang="scss" scoped> +.novjtctn { + display: inline-block; + margin: 0 32px 0 0; + cursor: pointer; + transition: all 0.3s; + + > * { + user-select: none; + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.checked { + > .button { + border-color: var(--radioActive); + + &:after { + background-color: var(--radioActive); + transform: scale(1); + opacity: 1; + } + } + } + + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; + } + + > .button { + position: absolute; + width: 20px; + height: 20px; + background: none; + border: solid 2px var(--inputLabel); + border-radius: 100%; + transition: inherit; + + &:after { + content: ''; + display: block; + position: absolute; + top: 3px; + right: 3px; + bottom: 3px; + left: 3px; + border-radius: 100%; + opacity: 0; + transform: scale(0); + transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + } + } + + > .label { + margin-left: 28px; + display: block; + font-size: 16px; + line-height: 20px; + cursor: pointer; + } +} +</style> diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue new file mode 100644 index 0000000000000000000000000000000000000000..8bad7c5d6552462a6e599884f8695fb825b669f1 --- /dev/null +++ b/src/client/components/ui/select.vue @@ -0,0 +1,220 @@ +<template> +<div class="eiipwacr" :class="{ focused, disabled, filled, inline }"> + <div class="icon" ref="icon"><slot name="icon"></slot></div> + <div class="input" @click="focus"> + <span class="label" ref="label"><slot name="label"></slot></span> + <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> + <select ref="input" + v-model="v" + :required="required" + :disabled="disabled" + @focus="focused = true" + @blur="focused = false" + > + <slot></slot> + </select> + <div class="suffix"><slot name="suffix"></slot></div> + </div> + <div class="text"><slot name="text"></slot></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + value: { + required: false + }, + required: { + type: Boolean, + required: false + }, + disabled: { + type: Boolean, + required: false + }, + inline: { + type: Boolean, + required: false, + default: false + }, + }, + data() { + return { + focused: false + }; + }, + computed: { + v: { + get() { + return this.value; + }, + set(v) { + this.$emit('input', v); + } + }, + filled(): boolean { + return this.v != '' && this.v != null; + } + }, + mounted() { + if (this.$refs.prefix) { + this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; + } + }, + methods: { + focus() { + this.$refs.input.focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.eiipwacr { + position: relative; + margin: 32px 0; + + > .icon { + position: absolute; + top: 0; + left: 0; + width: 24px; + text-align: center; + line-height: 32px; + + &:not(:empty) + .input { + margin-left: 28px; + } + } + + > .input { + display: flex; + + &:before { + content: ''; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: var(--inputBorder); + } + + &:after { + content: ''; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: var(--accent); + opacity: 0; + transform: scaleX(0.12); + transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + will-change: border opacity transform; + } + + > .label { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transition-duration: 0.3s; + font-size: 16px; + line-height: 32px; + pointer-events: none; + //will-change transform + transform-origin: top left; + transform: scale(1); + } + + > select { + display: block; + flex: 1; + width: 100%; + padding: 0; + font: inherit; + font-weight: normal; + font-size: 16px; + height: 32px; + background: var(--panel); + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + color: var(--fg); + } + + > .prefix, + > .suffix { + display: block; + align-self: center; + justify-self: center; + font-size: 16px; + line-height: 32px; + color: rgba(#000, 0.54); + pointer-events: none; + + &:empty { + display: none; + } + + > * { + display: block; + min-width: 16px; + } + } + + > .prefix { + padding-right: 4px; + } + + > .suffix { + padding-left: 4px; + } + } + + > .text { + margin: 6px 0; + font-size: 13px; + + &:empty { + display: none; + } + + * { + margin: 0; + } + } + + &.focused { + > .input { + &:after { + opacity: 1; + transform: scaleX(1); + } + + > .label { + color: var(--accent); + } + } + } + + &.focused, + &.filled { + > .input { + > .label { + top: -17px; + left: 0 !important; + transform: scale(0.75); + } + } + } +} +</style> diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue new file mode 100644 index 0000000000000000000000000000000000000000..d4680ca2efadd85c02f7aa7af1c0ef3cfaaf0b43 --- /dev/null +++ b/src/client/components/ui/switch.vue @@ -0,0 +1,150 @@ +<template> +<div + class="ziffeoms" + :class="{ disabled, checked }" + role="switch" + :aria-checked="checked" + :aria-disabled="disabled" + @click="toggle" +> + <input + type="checkbox" + ref="input" + :disabled="disabled" + @keydown.enter="toggle" + > + <span class="button"> + <span></span> + </span> + <span class="label"> + <span :aria-hidden="!checked"><slot></slot></span> + <p :aria-hidden="!checked"> + <slot name="desc"></slot> + </p> + </span> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + model: { + prop: 'value', + event: 'change' + }, + props: { + value: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + } + }, + computed: { + checked(): boolean { + return this.value; + } + }, + methods: { + toggle() { + if (this.disabled) return; + this.$emit('change', !this.checked); + } + } +}); +</script> + +<style lang="scss" scoped> +.ziffeoms { + position: relative; + display: flex; + margin: 32px 0; + cursor: pointer; + transition: all 0.3s; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + > * { + user-select: none; + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.checked { + > .button { + background-color: var(--xxubwiul); + border-color: var(--xxubwiul); + + > * { + background-color: var(--accent); + transform: translateX(14px); + } + } + } + + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; + } + + > .button { + position: relative; + display: inline-block; + flex-shrink: 0; + margin: 3px 0 0 0; + width: 34px; + height: 14px; + background: var(--nhzhphzx); + outline: none; + border-radius: 14px; + transition: inherit; + + > * { + position: absolute; + top: -3px; + left: 0; + border-radius: 100%; + transition: background-color 0.3s, transform 0.3s; + width: 20px; + height: 20px; + background-color: #fff; + box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12); + } + } + + > .label { + margin-left: 8px; + display: block; + font-size: 16px; + cursor: pointer; + transition: inherit; + color: var(--fg); + + > span { + display: block; + line-height: 20px; + transition: inherit; + } + + > p { + margin: 0; + opacity: 0.7; + font-size: 90%; + } + } +} +</style> diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue new file mode 100644 index 0000000000000000000000000000000000000000..7b42b78a7396608befc926844b62b1bf1241484e --- /dev/null +++ b/src/client/components/ui/textarea.vue @@ -0,0 +1,218 @@ +<template> +<div class="adhpbeos" :class="{ focused, filled, tall, pre }"> + <div class="input"> + <span class="label" ref="label"><slot></slot></span> + <textarea ref="input" + :value="value" + :required="required" + :readonly="readonly" + :pattern="pattern" + :autocomplete="autocomplete" + @input="onInput" + @focus="focused = true" + @blur="focused = false" + ></textarea> + </div> + <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button> + <div class="desc"><slot name="desc"></slot></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + value: { + required: false + }, + required: { + type: Boolean, + required: false + }, + readonly: { + type: Boolean, + required: false + }, + pattern: { + type: String, + required: false + }, + autocomplete: { + type: String, + required: false + }, + tall: { + type: Boolean, + required: false, + default: false + }, + pre: { + type: Boolean, + required: false, + default: false + }, + save: { + type: Function, + required: false, + }, + }, + data() { + return { + focused: false, + changed: false, + } + }, + computed: { + filled(): boolean { + return this.value != '' && this.value != null; + } + }, + methods: { + focus() { + this.$refs.input.focus(); + }, + onInput(ev) { + this.changed = true; + this.$emit('input', ev.target.value); + } + } +}); +</script> + +<style lang="scss" scoped> +.adhpbeos { + margin: 42px 0 32px 0; + position: relative; + + &:last-child { + margin-bottom: 0; + } + + > .input { + position: relative; + + &:before { + content: ''; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: none; + border: solid 1px var(--inputBorder); + border-radius: 3px; + pointer-events: none; + } + + &:after { + content: ''; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: none; + border: solid 2px var(--accent); + border-radius: 3px; + opacity: 0; + transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; + } + + > .label { + position: absolute; + top: 6px; + left: 12px; + pointer-events: none; + transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transition-duration: 0.3s; + font-size: 16px; + line-height: 32px; + pointer-events: none; + //will-change transform + transform-origin: top left; + transform: scale(1); + } + + > textarea { + display: block; + width: 100%; + min-width: 100%; + max-width: 100%; + min-height: 130px; + padding: 12px; + box-sizing: border-box; + font: inherit; + font-weight: normal; + font-size: 16px; + background: transparent; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + color: var(--fg); + } + } + + > .save { + margin: 6px 0 0 0; + font-size: 13px; + } + + > .desc { + margin: 6px 0 0 0; + font-size: 13px; + opacity: 0.7; + + &:empty { + display: none; + } + + * { + margin: 0; + } + } + + &.focused { + > .input { + &:after { + opacity: 1; + } + + > .label { + color: var(--accent); + } + } + } + + &.focused, + &.filled { + > .input { + > .label { + top: -24px; + left: 0 !important; + transform: scale(0.75); + } + } + } + + &.tall { + > .input { + > textarea { + min-height: 200px; + } + } + } + + &.pre { + > .input { + > textarea { + white-space: pre; + } + } + } +} +</style> diff --git a/src/client/components/uploader.vue b/src/client/components/uploader.vue new file mode 100644 index 0000000000000000000000000000000000000000..14a4f845c18acae42bf76e2067b455384250f6f2 --- /dev/null +++ b/src/client/components/uploader.vue @@ -0,0 +1,242 @@ +<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> + <div class="top"> + <p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p> + <p class="status"> + <span class="initing" v-if="ctx.progress == undefined">{{ $t('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> + </div> + <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 i18n from '../i18n'; +import { apiUrl } from '../config'; +//import getMD5 from '../../scripts/get-md5'; + +export default Vue.extend({ + i18n, + data() { + return { + uploads: [] + }; + }, + methods: { + checkExistence(fileData: ArrayBuffer): Promise<any> { + return new Promise((resolve, reject) => { + const data = new FormData(); + data.append('md5', getMD5(fileData)); + + this.$root.api('drive/files/find-by-hash', { + md5: getMD5(fileData) + }).then(resp => { + resolve(resp.length > 0 ? resp[0] : null); + }); + }); + }, + + upload(file: File, folder: any, name?: string) { + if (folder && typeof folder == 'object') folder = folder.id; + + const id = Math.random(); + + const reader = new FileReader(); + reader.onload = (e: any) => { + const ctx = { + id: id, + name: name || file.name || 'untitled', + progress: undefined, + img: window.URL.createObjectURL(file) + }; + + this.uploads.push(ctx); + this.$emit('change', this.uploads); + + const data = new FormData(); + data.append('i', this.$store.state.i.token); + data.append('force', 'true'); + data.append('file', file); + + if (folder) data.append('folderId', folder); + if (name) data.append('name', name); + + 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); + } + reader.readAsArrayBuffer(file); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-uploader { + overflow: auto; +} +.mk-uploader:empty { + display: none; +} +.mk-uploader > ol { + display: block; + margin: 0; + padding: 0; + list-style: none; +} +.mk-uploader > ol > li { + display: grid; + margin: 8px 0 0 0; + padding: 0; + height: 36px; + width: 100%; + box-shadow: 0 -1px 0 var(--accentAlpha01); + border-top: solid 8px transparent; + grid-template-columns: 36px calc(100% - 44px); + grid-template-rows: 1fr 8px; + column-gap: 8px; + box-sizing: content-box; +} +.mk-uploader > ol > li:first-child { + margin: 0; + box-shadow: none; + border-top: none; +} +.mk-uploader > ol > li > .img { + display: block; + background-size: cover; + background-position: center center; + grid-column: 1/2; + grid-row: 1/3; +} +.mk-uploader > ol > li > .top { + display: flex; + grid-column: 2/3; + grid-row: 1/2; +} +.mk-uploader > ol > li > .top > .name { + display: block; + padding: 0 8px 0 0; + margin: 0; + font-size: 0.8em; + color: var(--accentAlpha07); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex-shrink: 1; +} +.mk-uploader > ol > li > .top > .name > [data-icon] { + margin-right: 4px; +} +.mk-uploader > ol > li > .top > .status { + display: block; + margin: 0 0 0 auto; + padding: 0; + font-size: 0.8em; + flex-shrink: 0; +} +.mk-uploader > ol > li > .top > .status > .initing { + color: var(--accentAlpha05); +} +.mk-uploader > ol > li > .top > .status > .kb { + color: var(--accentAlpha05); +} +.mk-uploader > ol > li > .top > .status > .percentage { + display: inline-block; + width: 48px; + text-align: right; + color: var(--accentAlpha07); +} +.mk-uploader > ol > li > .top > .status > .percentage:after { + content: '%'; +} +.mk-uploader > ol > li > progress { + display: block; + background: transparent; + border: none; + border-radius: 4px; + overflow: hidden; + grid-column: 2/3; + grid-row: 2/3; + z-index: 2; +} +.mk-uploader > ol > li > progress::-webkit-progress-value { + background: var(--accent); +} +.mk-uploader > ol > li > progress::-webkit-progress-bar { + background: var(--accentAlpha01); +} +.mk-uploader > ol > li > .progress { + display: block; + border: none; + border-radius: 4px; + background: linear-gradient(45deg, var(--accentLighten30) 25%, var(--accent) 25%, var(--accent) 50%, var(--accentLighten30) 50%, var(--accentLighten30) 75%, var(--accent) 75%, var(--accent)); + background-size: 32px 32px; + animation: bg 1.5s linear infinite; + grid-column: 2/3; + grid-row: 2/3; + z-index: 1; +} +.mk-uploader > ol > li > .progress.initing { + opacity: 0.3; +} +@-moz-keyframes bg { + from { + background-position: 0 0; + } + to { + background-position: -64px 32px; + } +} +@-webkit-keyframes bg { + from { + background-position: 0 0; + } + to { + background-position: -64px 32px; + } +} +@-o-keyframes bg { + from { + background-position: 0 0; + } + to { + background-position: -64px 32px; + } +} +@keyframes bg { + from { + background-position: 0 0; + } + to { + background-position: -64px 32px; + } +} +</style> diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue new file mode 100644 index 0000000000000000000000000000000000000000..f2ef1f1ba398f3b77c76464a9af9e577a471642b --- /dev/null +++ b/src/client/components/url-preview.vue @@ -0,0 +1,331 @@ +<template> +<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> + <button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button> + <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen /> +</div> +<div v-else-if="tweetUrl && detail" class="twitter"> + <blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null"> + <a :href="url"></a> + </blockquote> +</div> +<div v-else class="mk-url-preview" v-size="[{ max: 400 }, { max: 350 }]"> + <transition name="zoom" mode="out-in"> + <component :is="hasRoute ? 'router-link' : 'a'" :class="{ compact }" :[attr]="hasRoute ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching"> + <div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`"> + <button class="_button" v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="faPlayCircle"/></button> + </div> + <article> + <header> + <h1 :title="title">{{ title }}</h1> + </header> + <p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p> + <footer> + <img class="icon" v-if="icon" :src="icon"/> + <p :title="sitename">{{ sitename }}</p> + </footer> + </article> + </component> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPlayCircle } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import { url as local, lang } from '../config'; + +export default Vue.extend({ + i18n, + + props: { + url: { + type: String, + require: true + }, + + detail: { + type: Boolean, + required: false, + default: false + }, + + compact: { + type: Boolean, + required: false, + default: false + }, + }, + + data() { + const isSelf = this.url.startsWith(local); + const hasRoute = + (this.url.substr(local.length) === '/') || + this.url.substr(local.length).startsWith('/@') || + this.url.substr(local.length).startsWith('/notes/') || + this.url.substr(local.length).startsWith('/tags/'); + return { + local, + fetching: true, + title: null, + description: null, + thumbnail: null, + icon: null, + sitename: null, + player: { + url: null, + width: null, + height: null + }, + tweetUrl: null, + playerEnabled: false, + self: isSelf, + hasRoute: hasRoute, + attr: hasRoute ? 'to' : 'href', + target: hasRoute ? null : '_blank', + faPlayCircle + }; + }, + + created() { + const requestUrl = new URL(this.url); + + if (this.detail && requestUrl.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(requestUrl.pathname)) { + this.tweetUrl = requestUrl; + const twttr = (window as any).twttr || {}; + const loadTweet = () => twttr.widgets.load(this.$refs.tweet); + + if (twttr.widgets) { + Vue.nextTick(loadTweet); + } else { + const wjsId = 'twitter-wjs'; + if (!document.getElementById(wjsId)) { + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('id', wjsId); + script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); + head.appendChild(script); + } + twttr.ready = loadTweet; + (window as any).twttr = twttr; + } + return; + } + + if (requestUrl.hostname === 'music.youtube.com') { + requestUrl.hostname = 'youtube.com'; + } + + const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP'); + + requestUrl.hash = ''; + + fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => { + res.json().then(info => { + if (info.url == null) return; + this.title = info.title; + this.description = info.description; + this.thumbnail = info.thumbnail; + this.icon = info.icon; + this.sitename = info.sitename; + this.fetching = false; + this.player = info.player; + }) + }); + } +}); +</script> + +<style lang="scss" scoped> +.player { + position: relative; + width: 100%; + + > button { + position: absolute; + top: -1.5em; + right: 0; + font-size: 1em; + width: 1.5em; + height: 1.5em; + padding: 0; + margin: 0; + color: var(--fg); + background: rgba(128, 128, 128, 0.2); + opacity: 0.7; + + &:hover { + opacity: 0.9; + } + } + + > iframe { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } +} + +.mk-url-preview { + &.max-width_400px { + > a { + font-size: 12px; + + > .thumbnail { + height: 80px; + } + + > article { + padding: 12px; + } + } + } + + &.max-width_350px { + > a { + font-size: 10px; + + > .thumbnail { + height: 70px; + } + + > article { + padding: 8px; + + > header { + margin-bottom: 4px; + } + + > footer { + margin-top: 4px; + + > img { + width: 12px; + height: 12px; + } + } + } + + &.compact { + > .thumbnail { + position: absolute; + width: 56px; + height: 100%; + } + + > article { + left: 56px; + width: calc(100% - 56px); + padding: 4px; + + > header { + margin-bottom: 2px; + } + + > footer { + margin-top: 2px; + } + } + } + } + } + + > a { + position: relative; + display: block; + font-size: 14px; + box-shadow: 0 1px 4px var(--tyvedwbe); + border-radius: 4px; + overflow: hidden; + + &:hover { + text-decoration: none; + border-color: rgba(0, 0, 0, 0.2); + + > article > header > h1 { + text-decoration: underline; + } + } + + > .thumbnail { + position: absolute; + width: 100px; + height: 100%; + background-position: center; + background-size: cover; + display: flex; + justify-content: center; + align-items: center; + + > button { + font-size: 3.5em; + opacity: 0.7; + + &:hover { + font-size: 4em; + opacity: 0.9; + } + } + + & + article { + left: 100px; + width: calc(100% - 100px); + } + } + + > article { + position: relative; + box-sizing: border-box; + padding: 16px; + + > header { + margin-bottom: 8px; + + > h1 { + margin: 0; + font-size: 1em; + } + } + + > p { + margin: 0; + 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: var(--urlPreviewInfo); + font-size: 0.8em; + line-height: 16px; + vertical-align: top; + } + } + } + + &.compact { + > article { + > header h1, p, footer { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + } +} +</style> diff --git a/src/client/app/common/views/components/url.vue b/src/client/components/url.vue similarity index 69% rename from src/client/app/common/views/components/url.vue rename to src/client/components/url.vue index 3a304ad6e7658ab20f5be6147876243bed3b1edc..082e74400161b1bfa02a985527857260b5335514 100644 --- a/src/client/app/common/views/components/url.vue +++ b/src/client/components/url.vue @@ -11,14 +11,15 @@ <span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span> <span class="query">{{ query }}</span> <span class="hash">{{ hash }}</span> - <fa icon="external-link-square-alt" v-if="target === '_blank'"/> + <fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/> </component> </template> <script lang="ts"> import Vue from 'vue'; +import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; import { toUnicode as decodePunycode } from 'punycode'; -import { url as local } from '../../../config'; +import { url as local } from '../config'; export default Vue.extend({ props: ['url', 'rel'], @@ -28,8 +29,7 @@ export default Vue.extend({ (this.url.substr(local.length) === '/') || this.url.substr(local.length).startsWith('/@') || this.url.substr(local.length).startsWith('/notes/') || - this.url.substr(local.length).startsWith('/tags/') || - this.url.substr(local.length).startsWith('/pages/')); + this.url.substr(local.length).startsWith('/tags/')); return { local, schema: null, @@ -41,7 +41,8 @@ export default Vue.extend({ self: isSelf, hasRoute: hasRoute, attr: hasRoute ? 'to' : 'href', - target: hasRoute ? null : '_blank' + target: hasRoute ? null : '_blank', + faExternalLinkSquareAlt }; }, created() { @@ -56,31 +57,39 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-url - word-break break-all +<style lang="scss" scoped> +.mk-url { + word-break: break-all; - > [data-icon] - padding-left 2px - font-size .9em - font-weight 400 - font-style normal + > .icon { + padding-left: 2px; + font-size: .9em; + font-weight: 400; + font-style: normal; + } - > .self - font-weight bold + > .self { + font-weight: bold; + } - > .schema - opacity 0.5 + > .schema { + opacity: 0.5; + } - > .hostname - font-weight bold + > .hostname { + font-weight: bold; + } - > .pathname - opacity 0.8 + > .pathname { + opacity: 0.8; + } - > .query - opacity 0.5 + > .query { + opacity: 0.5; + } - > .hash - font-style italic + > .hash { + font-style: italic; + } +} </style> diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue new file mode 100644 index 0000000000000000000000000000000000000000..14a96f3c6f6f7f2f490d72b67e7fc9c92d86764f --- /dev/null +++ b/src/client/components/user-list.vue @@ -0,0 +1,148 @@ +<template> +<mk-container :body-togglable="true" :expanded="expanded"> + <template #header><slot></slot></template> + + <mk-error v-if="error" @retry="init()"/> + + <div class="efvhhmdq"> + <div class="no-users" v-if="empty"> + <p>{{ $t('no-users') }}</p> + </div> + <div class="user" v-for="user in users" :key="user.id"> + <mk-avatar class="avatar" :user="user"/> + <div class="body"> + <div class="name"> + <router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link> + <span class="username"><mk-acct :user="user"/></span> + </div> + <div class="description"> + <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> + <span v-else class="empty">{{ $t('noAccountDescription') }}</span> + </div> + </div> + <x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/> + </div> + <button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore()" :disabled="moreFetching"> + <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }} + </button> + </div> +</mk-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import paging from '../scripts/paging'; +import MkContainer from './ui/container.vue'; +import XFollowButton from './follow-button.vue'; + +export default Vue.extend({ + i18n, + + components: { + MkContainer, + XFollowButton, + }, + + mixins: [ + paging({}), + ], + + props: { + pagination: { + required: true + }, + extract: { + required: false + }, + expanded: { + type: Boolean, + default: true + }, + }, + + computed: { + users() { + return this.extract ? this.extract(this.items) : this.items; + } + } +}); +</script> + +<style lang="scss" scoped> +.efvhhmdq { + > .no-users { + text-align: center; + } + + > .user { + position: relative; + display: flex; + padding: 16px; + border-bottom: solid 1px var(--divider); + + &:last-child { + border-bottom: none; + } + + > .avatar { + display: block; + flex-shrink: 0; + margin: 0 12px 0 0; + width: 42px; + height: 42px; + border-radius: 8px; + } + + > .body { + flex: 1; + + > .name { + font-weight: bold; + + > .name { + margin-right: 8px; + } + + > .username { + opacity: 0.7; + } + } + + > .description { + font-size: 90%; + + > .empty { + opacity: 0.7; + } + } + } + + > .koudoku-button { + flex-shrink: 0; + } + } + + > .more { + display: block; + width: 100%; + padding: 16px; + + &:hover { + background: rgba(#000, 0.025); + } + + &:active { + background: rgba(#000, 0.05); + } + + &.fetching { + cursor: wait; + } + + > [data-icon] { + margin-right: 4px; + } + } +} +</style> diff --git a/src/client/components/user-menu.vue b/src/client/components/user-menu.vue new file mode 100644 index 0000000000000000000000000000000000000000..6e3280031c1d8e6f62f7608b06e89a19a72cf77b --- /dev/null +++ b/src/client/components/user-menu.vue @@ -0,0 +1,188 @@ +<template> +<x-menu :source="source" :items="items" @closed="$emit('closed')"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments } from '@fortawesome/free-solid-svg-icons'; +import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import XMenu from './menu.vue'; +import copyToClipboard from '../scripts/copy-to-clipboard'; +import { host } from '../config'; +import getAcct from '../../misc/acct/render'; + +export default Vue.extend({ + i18n, + + components: { + XMenu + }, + + props: ['user', 'source'], + + data() { + let menu = [{ + icon: faAt, + text: this.$t('copyUsername'), + action: () => { + copyToClipboard(`@${this.user.username}@${this.user.host || host}`); + } + }, { + icon: faEnvelope, + text: this.$t('sendMessage'), + action: () => { + this.$root.post({ specified: this.user }); + } + }, this.$store.state.i.id != this.user.id ? { + type: 'link', + to: `/my/messaging/${getAcct(this.user)}`, + icon: faComments, + text: this.$t('startMessaging'), + } : undefined, null, { + icon: faListUl, + text: this.$t('addToList'), + action: this.pushList + }] as any; + + if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { + menu = menu.concat([null, { + icon: this.user.isMuted ? faEye : faEyeSlash, + text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), + action: this.toggleMute + }, { + icon: faBan, + text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), + action: this.toggleBlock + }]); + + if (this.$store.state.i.isAdmin) { + menu = menu.concat([null, { + icon: faSnowflake, + text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'), + action: this.toggleSuspend + }]); + } + } + + if (this.$store.getters.isSignedIn && this.$store.state.i.id === this.user.id) { + menu = menu.concat([null, { + icon: faPencilAlt, + text: this.$t('editProfile'), + action: () => { + this.$router.push('/my/settings'); + } + }]); + } + + return { + items: menu + }; + }, + + methods: { + async pushList() { + const t = this.$t('selectList'); // ãªãœã‹å¾Œã§å‚ç…§ã™ã‚‹ã¨ null ã«ãªã‚‹ã®ã§æœ€åˆã«ãƒ¡ãƒ¢ãƒªã«ç¢ºä¿ã—ã¦ãŠã + const lists = await this.$root.api('users/lists/list'); + if (lists.length === 0) { + this.$root.dialog({ + type: 'error', + text: this.$t('youHaveNoLists') + }); + return; + } + const { canceled, result: listId } = await this.$root.dialog({ + type: null, + title: t, + select: { + items: lists.map(list => ({ + value: list.id, text: list.name + })) + }, + showCancelButton: true + }); + if (canceled) return; + this.$root.api('users/lists/push', { + listId: listId, + userId: this.user.id + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + async toggleMute() { + this.$root.api(this.user.isMuted ? 'mute/delete' : 'mute/create', { + userId: this.user.id + }).then(() => { + this.user.isMuted = !this.user.isMuted; + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }, e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + async toggleBlock() { + if (!await this.getConfirmed(this.user.isBlocking ? this.$t('unblockConfirm') : this.$t('blockConfirm'))) return; + + this.$root.api(this.user.isBlocking ? 'blocking/delete' : 'blocking/create', { + userId: this.user.id + }).then(() => { + this.user.isBlocking = !this.user.isBlocking; + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }, e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + async toggleSuspend() { + if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; + + this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { + userId: this.user.id + }).then(() => { + this.user.isSuspended = !this.user.isSuspended; + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }, e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + async getConfirmed(text: string): Promise<Boolean> { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + title: 'confirm', + text, + }); + + return !confirm.canceled; + }, + } +}); +</script> diff --git a/src/client/components/user-moderate-dialog.vue b/src/client/components/user-moderate-dialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..894db5384efdce18ae6e763ab2c84f09ade0e3d5 --- /dev/null +++ b/src/client/components/user-moderate-dialog.vue @@ -0,0 +1,108 @@ +<template> +<x-window @closed="() => { $emit('closed'); destroyDom(); }" :avatar="user"> + <template #header><mk-user-name :user="user"/></template> + <div class="vrcsvlkm"> + <mk-button @click="changePassword()">{{ $t('changePassword') }}</mk-button> + <mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch> + <mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch> + </div> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import MkButton from './ui/button.vue'; +import MkSwitch from './ui/switch.vue'; +import XWindow from './window.vue'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + MkSwitch, + XWindow, + }, + + props: { + user: { + type: Object, + required: true + } + }, + + data() { + return { + silenced: this.user.isSilenced, + suspended: this.user.isSuspended, + }; + }, + + methods: { + async changePassword() { + const { canceled: canceled, result: newPassword } = await this.$root.dialog({ + title: this.$t('newPassword'), + input: { + type: 'password' + } + }); + if (canceled) return; + + const dialog = this.$root.dialog({ + type: 'waiting', + iconOnly: true + }); + + this.$root.api('admin/change-password', { + userId: this.user.id, + newPassword + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }).finally(() => { + dialog.close(); + }); + }, + + async toggleSilence() { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + text: this.silenced ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'), + }); + if (confirm.canceled) { + this.silenced = !this.silenced; + } else { + this.$root.api(this.silenced ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id }); + } + }, + + async toggleSuspend() { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + text: this.suspended ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'), + }); + if (confirm.canceled) { + this.suspended = !this.suspended; + } else { + this.$root.api(this.silenced ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id }); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.vrcsvlkm { + +} +</style> diff --git a/src/client/app/common/views/components/user-name.vue b/src/client/components/user-name.vue similarity index 100% rename from src/client/app/common/views/components/user-name.vue rename to src/client/components/user-name.vue diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue new file mode 100644 index 0000000000000000000000000000000000000000..f20335d02bd36c7ca8a58084a889cae6a7da9013 --- /dev/null +++ b/src/client/components/user-preview.vue @@ -0,0 +1,181 @@ +<template> +<transition name="popup" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> + <div v-if="show" class="fxxzrfni _panel" ref="content" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }"> + <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div> + <mk-avatar class="avatar" :user="u" :disable-preview="true"/> + <div class="title"> + <router-link class="name" :to="u | userPage"><mk-user-name :user="u" :nowrap="false"/></router-link> + <p class="username"><mk-acct :user="u"/></p> + </div> + <div class="description"> + <mfm v-if="u.description" :text="u.description" :author="u" :i="$store.state.i" :custom-emojis="u.emojis"/> + </div> + <div class="status"> + <div> + <p>{{ $t('notes') }}</p><span>{{ u.notesCount }}</span> + </div> + <div> + <p>{{ $t('following') }}</p><span>{{ u.followingCount }}</span> + </div> + <div> + <p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span> + </div> + </div> + <x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/> + </div> +</transition> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import parseAcct from '../../misc/acct/parse'; +import XFollowButton from './follow-button.vue'; + +export default Vue.extend({ + i18n, + + components: { + XFollowButton + }, + + props: { + user: { + type: [Object, String], + required: true + }, + source: { + required: true + } + }, + + data() { + return { + u: null, + show: false, + top: 0, + left: 0, + }; + }, + + mounted() { + if (typeof this.user == 'object') { + this.u = this.user; + this.show = true; + } else { + const query = this.user.startsWith('@') ? + parseAcct(this.user.substr(1)) : + { userId: this.user }; + + this.$root.api('users/show', query).then(user => { + this.u = user; + this.show = true; + }); + } + + const rect = this.source.getBoundingClientRect(); + const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset; + const y = rect.top + this.source.offsetHeight + window.pageYOffset; + + this.top = y; + this.left = x; + }, + + methods: { + close() { + this.show = false; + (this.$refs.content as any).style.pointerEvents = 'none'; + } + } +}); +</script> + +<style lang="scss" scoped> +.popup-enter-active, .popup-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.popup-enter, .popup-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.fxxzrfni { + position: absolute; + z-index: 11000; + width: 300px; + overflow: hidden; + + > .banner { + height: 84px; + background-color: rgba(0, 0, 0, 0.1); + background-size: cover; + background-position: center; + } + + > .avatar { + display: block; + position: absolute; + top: 62px; + left: 13px; + z-index: 2; + width: 58px; + height: 58px; + border: solid 3px var(--face); + border-radius: 8px; + } + + > .title { + display: block; + padding: 8px 0 8px 82px; + + > .name { + display: inline-block; + margin: 0; + font-weight: bold; + line-height: 16px; + word-break: break-all; + } + + > .username { + display: block; + margin: 0; + line-height: 16px; + font-size: 0.8em; + color: var(--text); + opacity: 0.7; + } + } + + > .description { + padding: 0 16px; + font-size: 0.8em; + color: var(--text); + } + + > .status { + padding: 8px 16px; + + > div { + display: inline-block; + width: 33%; + + > p { + margin: 0; + font-size: 0.7em; + color: var(--text); + } + + > span { + font-size: 1em; + color: var(--accent); + } + } + } + + > .koudoku-button { + position: absolute; + top: 8px; + right: 8px; + } +} +</style> diff --git a/src/client/components/user-select.vue b/src/client/components/user-select.vue new file mode 100644 index 0000000000000000000000000000000000000000..a82626652d6207ba100782a27bbc4d1f9230eb28 --- /dev/null +++ b/src/client/components/user-select.vue @@ -0,0 +1,152 @@ +<template> +<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="selected == null" @ok="ok()"> + <template #header>{{ $t('selectUser') }}</template> + <div class="tbhwbxda"> + <div class="inputs"> + <mk-input v-model="username" class="input" @input="search" ref="username"><span>{{ $t('username') }}</span><template #prefix>@</template></mk-input> + <mk-input v-model="host" class="input" @input="search"><span>{{ $t('host') }}</span><template #prefix>@</template></mk-input> + </div> + <div class="users"> + <div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> + <mk-avatar :user="user" class="avatar" :disable-link="true"/> + <div class="body"> + <mk-user-name :user="user" class="name"/> + <mk-acct :user="user" class="acct"/> + </div> + </div> + </div> + </div> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons'; +import MkInput from './ui/input.vue'; +import XWindow from './window.vue'; + +export default Vue.extend({ + i18n, + + components: { + MkInput, + XWindow, + }, + + props: { + }, + + data() { + return { + username: '', + host: '', + users: [], + selected: null, + faTimes, faCheck + }; + }, + + mounted() { + this.focus(); + + this.$nextTick(() => { + this.focus(); + }); + }, + + methods: { + search() { + if (this.username == '' && this.host == '') { + this.users = []; + return; + } + this.$root.api('users/search-by-username-and-host', { + username: this.username, + host: this.host, + limit: 10, + detail: false + }).then(users => { + this.users = users; + }); + }, + + focus() { + this.$refs.username.focus(); + }, + + close() { + this.$refs.window.close(); + }, + + ok() { + this.$emit('selected', this.selected); + this.close(); + }, + } +}); +</script> + +<style lang="scss" scoped> +.tbhwbxda { + display: flex; + flex-direction: column; + overflow: auto; + height: 100%; + + > .inputs { + margin-top: 16px; + + > .input { + display: inline-block; + width: 50%; + margin: 0; + } + } + + > .users { + flex: 1; + overflow: auto; + + > .user { + display: flex; + align-items: center; + padding: 8px 16px; + font-size: 14px; + + &:hover { + background: var(--bwqtlupy); + } + + &.selected { + background: var(--accent); + color: #fff; + } + + > * { + pointer-events: none; + user-select: none; + } + + > .avatar { + width: 45px; + height: 45px; + } + + > .body { + padding: 0 8px; + min-width: 0; + + > .name { + display: block; + font-weight: bold; + } + + > .acct { + opacity: 0.5; + } + } + } + } +} +</style> diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..19310bc4e14f4e84bb41c459399b947725fb5b4a --- /dev/null +++ b/src/client/components/users-dialog.vue @@ -0,0 +1,161 @@ +<template> +<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }"> + <div class="mk-users-dialog"> + <div class="header"> + <span>{{ title }}</span> + <button class="_button" @click="close()"><fa :icon="faTimes"/></button> + </div> + + <sequential-entrance class="users"> + <router-link v-for="(item, i) in items" class="user" :key="item.id" :data-index="i" :to="extract ? extract(item) : item | userPage"> + <mk-avatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/> + <div class="body"> + <mk-user-name :user="extract ? extract(item) : item" class="name"/> + <mk-acct :user="extract ? extract(item) : item" class="acct"/> + </div> + </router-link> + </sequential-entrance> + + <button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching"> + <template v-if="!moreFetching">{{ $t('loadMore') }}</template> + <template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template> + </button> + + <p class="empty" v-if="empty">{{ $t('noUsers') }}</p> + + <mk-error v-if="error" @retry="init()"/> + </div> +</x-modal> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import paging from '../scripts/paging'; +import XModal from './modal.vue'; + +export default Vue.extend({ + i18n, + + components: { + XModal, + }, + + mixins: [ + paging({}), + ], + + props: { + title: { + required: true + }, + pagination: { + required: true + }, + extract: { + required: false + } + }, + + data() { + return { + faTimes + }; + }, + + methods: { + close() { + this.$refs.modal.close(); + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-users-dialog { + width: 350px; + height: 350px; + background: var(--panel); + border-radius: var(--radius); + overflow: hidden; + display: flex; + flex-direction: column; + + > .header { + display: flex; + flex-shrink: 0; + + > button { + height: 58px; + width: 58px; + + @media (max-width: 500px) { + height: 42px; + width: 42px; + } + } + + > span { + flex: 1; + line-height: 58px; + padding-left: 32px; + font-weight: bold; + + @media (max-width: 500px) { + line-height: 42px; + padding-left: 16px; + } + } + } + + > .users { + flex: 1; + overflow: auto; + + &:empty { + display: none; + } + + > .user { + display: flex; + align-items: center; + font-size: 14px; + padding: 8px 32px; + + @media (max-width: 500px) { + padding: 8px 16px; + } + + > * { + pointer-events: none; + user-select: none; + } + + > .avatar { + width: 45px; + height: 45px; + } + + > .body { + padding: 0 8px; + overflow: hidden; + + > .name { + display: block; + font-weight: bold; + } + + > .acct { + opacity: 0.5; + } + } + } + } + + > .empty { + text-align: center; + opacity: 0.5; + } +} +</style> diff --git a/src/client/components/visibility-chooser.vue b/src/client/components/visibility-chooser.vue new file mode 100644 index 0000000000000000000000000000000000000000..aa422b27dcac0fe2792ef59810515f9b72195baa --- /dev/null +++ b/src/client/components/visibility-chooser.vue @@ -0,0 +1,127 @@ +<template> +<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }"> + <sequential-entrance class="gqyayizv" :delay="30"> + <button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="0" key="0"> + <div><fa :icon="faGlobe"/></div> + <div> + <span>{{ $t('_visibility.public') }}</span> + <span>{{ $t('_visibility.publicDescription') }}</span> + </div> + </button> + <button class="_button" @click="choose('home')" :class="{ active: v == 'home' }" data-index="1" key="1"> + <div><fa :icon="faHome"/></div> + <div> + <span>{{ $t('_visibility.home') }}</span> + <span>{{ $t('_visibility.homeDescription') }}</span> + </div> + </button> + <button class="_button" @click="choose('followers')" :class="{ active: v == 'followers' }" data-index="2" key="2"> + <div><fa :icon="faUnlock"/></div> + <div> + <span>{{ $t('_visibility.followers') }}</span> + <span>{{ $t('_visibility.followersDescription') }}</span> + </div> + </button> + <button class="_button" @click="choose('specified')" :class="{ active: v == 'specified' }" data-index="3" key="3"> + <div><fa :icon="faEnvelope"/></div> + <div> + <span>{{ $t('_visibility.specified') }}</span> + <span>{{ $t('_visibility.specifiedDescription') }}</span> + </div> + </button> + </sequential-entrance> +</x-popup> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faGlobe, faUnlock, faHome } from '@fortawesome/free-solid-svg-icons'; +import { faEnvelope } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import XPopup from './popup.vue'; + +export default Vue.extend({ + i18n, + components: { + XPopup + }, + props: { + source: { + required: true + }, + currentVisibility: { + type: String, + required: false + } + }, + data() { + return { + v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility), + faGlobe, faUnlock, faEnvelope, faHome + } + }, + methods: { + choose(visibility) { + if (this.$store.state.settings.rememberNoteVisibility) { + this.$store.commit('device/setVisibility', visibility); + } + this.$emit('chosen', visibility); + this.destroyDom(); + }, + } +}); +</script> + +<style lang="scss" scoped> +.gqyayizv { + width: 240px; + padding: 8px 0; + + > button { + display: flex; + padding: 8px 14px; + font-size: 12px; + text-align: left; + width: 100%; + box-sizing: border-box; + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &:active { + background: rgba(0, 0, 0, 0.1); + } + + &.active { + color: #fff; + background: var(--accent); + } + + > *:first-child { + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + width: 16px; + top: 0; + bottom: 0; + margin-top: auto; + margin-bottom: auto; + } + + > *:last-child { + flex: 1 1 auto; + + > span:first-child { + display: block; + font-weight: bold; + } + + > span:last-child:not(:first-child) { + opacity: 0.6; + } + } + } +} +</style> diff --git a/src/client/components/window.vue b/src/client/components/window.vue new file mode 100644 index 0000000000000000000000000000000000000000..bfdabee0595d5e55628d0af2bd1a82567ffe0522 --- /dev/null +++ b/src/client/components/window.vue @@ -0,0 +1,155 @@ +<template> +<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }"> + <div class="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown"> + <div class="header"> + <button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button> + <span class="title"> + <mk-avatar :user="avatar" v-if="avatar" class="avatar"/> + <slot name="header"></slot> + </span> + <button class="_button" v-if="!withOkButton" @click="close()"><fa :icon="faTimes"/></button> + <button class="_button" v-if="withOkButton" @click="() => { $emit('ok'); close(); }" :disabled="okButtonDisabled"><fa :icon="faCheck"/></button> + </div> + <div class="body"> + <slot></slot> + </div> + </div> +</x-modal> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import XModal from './modal.vue'; + +export default Vue.extend({ + i18n, + + components: { + XModal, + }, + + props: { + avatar: { + type: Object, + required: false + }, + withOkButton: { + type: Boolean, + required: false, + default: false + }, + okButtonDisabled: { + type: Boolean, + required: false, + default: false + }, + noPadding: { + type: Boolean, + required: false, + default: false + } + }, + + data() { + return { + faTimes, faCheck + }; + }, + + methods: { + close() { + this.$refs.modal.close(); + }, + + onKeydown(e) { + if (e.which === 27) { // Esc + e.preventDefault(); + e.stopPropagation(); + this.close(); + } + }, + } +}); +</script> + +<style lang="scss" scoped> +.ebkgoccj { + width: 400px; + height: 400px; + background: var(--panel); + border-radius: var(--radius); + overflow: hidden; + display: flex; + flex-direction: column; + + @media (max-width: 500px) { + width: 350px; + height: 350px; + } + + > .header { + $height: 58px; + $height-narrow: 42px; + display: flex; + flex-shrink: 0; + + > button { + height: $height; + width: $height; + + @media (max-width: 500px) { + height: $height-narrow; + width: $height-narrow; + } + } + + > .title { + flex: 1; + line-height: $height; + padding-left: 32px; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; + + @media (max-width: 500px) { + line-height: $height-narrow; + padding-left: 16px; + } + + > .avatar { + $size: 32px; + height: $size; + width: $size; + margin: (($height - $size) / 2) 8px (($height - $size) / 2) 0; + + @media (max-width: 500px) { + $size: 24px; + height: $size; + width: $size; + margin: (($height-narrow - $size) / 2) 8px (($height-narrow - $size) / 2) 0; + } + } + } + + > button + .title { + padding-left: 0; + } + } + + > .body { + overflow: auto; + } + + &:not(.noPadding) > .body { + padding: 0 32px 32px 32px; + + @media (max-width: 500px) { + padding: 0 16px 16px 16px; + } + } +} +</style> diff --git a/src/client/app/config.ts b/src/client/config.ts similarity index 68% rename from src/client/app/config.ts rename to src/client/config.ts index 55c0c6b3a53c0040c835896241249e20574f32b5..175a3f0b29202403ee5a55c0e593c82625ee631b 100644 --- a/src/client/app/config.ts +++ b/src/client/config.ts @@ -1,20 +1,18 @@ declare const _LANGS_: string[]; -declare const _COPYRIGHT_: string; declare const _VERSION_: string; -declare const _CODENAME_: string; declare const _ENV_: string; const address = new URL(location.href); +const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement; export const host = address.host; export const hostname = address.hostname; export const url = address.origin; export const apiUrl = url + '/api'; export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; -export const lang = localStorage.getItem('lang') || window.lang; // windowã¯å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚ +export const lang = localStorage.getItem('lang'); export const langs = _LANGS_; export const locale = JSON.parse(localStorage.getItem('locale')); -export const copyright = _COPYRIGHT_; export const version = _VERSION_; -export const codename = _CODENAME_; export const env = _ENV_; +export const instanceName = siteName && siteName.content ? siteName.content : 'Misskey'; diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/directives/autocomplete.ts similarity index 100% rename from src/client/app/common/views/directives/autocomplete.ts rename to src/client/directives/autocomplete.ts diff --git a/src/client/app/desktop/views/directives/index.ts b/src/client/directives/index.ts similarity index 51% rename from src/client/app/desktop/views/directives/index.ts rename to src/client/directives/index.ts index 324e07596d90b0d6f6263488fbb6c1c17f3987cd..2e05b5202313f514d72f704aeced55af8b76fd6b 100644 --- a/src/client/app/desktop/views/directives/index.ts +++ b/src/client/directives/index.ts @@ -1,6 +1,10 @@ import Vue from 'vue'; import userPreview from './user-preview'; +import autocomplete from './autocomplete'; +import size from './size'; +Vue.directive('autocomplete', autocomplete); Vue.directive('userPreview', userPreview); Vue.directive('user-preview', userPreview); +Vue.directive('size', size); diff --git a/src/client/directives/size.ts b/src/client/directives/size.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7d797e5aee1e7ea6d2ce3ae7d3a81b3d1bb2127 --- /dev/null +++ b/src/client/directives/size.ts @@ -0,0 +1,63 @@ +export default { + inserted(el, binding) { + const query = binding.value; + + /* + const addClassRecursive = (el: Element, cls: string) => { + el.classList.add(cls); + if (el.children) { + for (const child of el.children) { + addClassRecursive(child, cls); + } + } + }; + + const removeClassRecursive = (el: Element, cls: string) => { + el.classList.remove(cls); + if (el.children) { + for (const child of el.children) { + removeClassRecursive(child, cls); + } + } + };*/ + + const addClass = (el: Element, cls: string) => { + el.classList.add(cls); + }; + + const removeClass = (el: Element, cls: string) => { + el.classList.remove(cls); + }; + + const calc = () => { + const width = el.clientWidth; + + for (const q of query) { + if (q.max) { + if (width <= q.max) { + addClass(el, 'max-width_' + q.max + 'px'); + } else { + removeClass(el, 'max-width_' + q.max + 'px'); + } + } + if (q.min) { + if (width >= q.min) { + addClass(el, 'min-width_' + q.min + 'px'); + } else { + removeClass(el, 'min-width_' + q.min + 'px'); + } + } + } + }; + + calc(); + + el._sizeResizeCb_ = calc; + + window.addEventListener('resize', calc); + }, + + unbind(el, binding, vn) { + window.removeEventListener('resize', el._sizeResizeCb_); + } +}; diff --git a/src/client/app/desktop/views/directives/user-preview.ts b/src/client/directives/user-preview.ts similarity index 63% rename from src/client/app/desktop/views/directives/user-preview.ts rename to src/client/directives/user-preview.ts index 8a4035881ad536ccaa408632a6c5507741d5e430..c3b4e7fce6b9d1d33bf2520da681bb43b31ea366 100644 --- a/src/client/app/desktop/views/directives/user-preview.ts +++ b/src/client/directives/user-preview.ts @@ -1,12 +1,8 @@ -/** - * マウスオーãƒãƒ¼ã™ã‚‹ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒ—レビューã•ã‚Œã‚‹è¦ç´ ã‚’è¨å®šã—ã¾ã™ - */ - import MkUserPreview from '../components/user-preview.vue'; export default { - bind(el, binding, vn) { - const self = el._userPreviewDirective_ = {} as any; + bind(el: HTMLElement, binding, vn) { + const self = (el as any)._userPreviewDirective_ = {} as any; self.user = binding.value; self.tag = null; @@ -26,28 +22,21 @@ export default { self.tag = new MkUserPreview({ parent: vn.context, propsData: { - user: self.user + user: self.user, + source: el } }).$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', () => { + self.tag.$on('mouseover', () => { clearTimeout(self.hideTimer); }); - preview.addEventListener('mouseleave', () => { + self.tag.$on('mouseleave', () => { clearTimeout(self.showTimer); self.hideTimer = setTimeout(self.close, 500); }); - document.body.appendChild(preview); + document.body.appendChild(self.tag.$el); }; el.addEventListener('mouseover', () => { diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/filters/bytes.ts similarity index 86% rename from src/client/app/common/views/filters/bytes.ts rename to src/client/filters/bytes.ts index 227ccae3a496140108a062589173963e4486a302..5b5d966cfd32257e530016f913b4c7bb75106db1 100644 --- a/src/client/app/common/views/filters/bytes.ts +++ b/src/client/filters/bytes.ts @@ -2,7 +2,7 @@ import Vue from 'vue'; Vue.filter('bytes', (v, digits = 0) => { if (v == null) return '?'; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; if (v == 0) return '0'; const isMinus = v < 0; if (isMinus) v = -v; diff --git a/src/client/filters/index.ts b/src/client/filters/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1759c19c2cfd0f476144da2130c743c31032fad8 --- /dev/null +++ b/src/client/filters/index.ts @@ -0,0 +1,4 @@ +require('./bytes'); +require('./number'); +require('./user'); +require('./note'); diff --git a/src/client/app/common/views/filters/note.ts b/src/client/filters/note.ts similarity index 100% rename from src/client/app/common/views/filters/note.ts rename to src/client/filters/note.ts diff --git a/src/client/app/common/views/filters/number.ts b/src/client/filters/number.ts similarity index 100% rename from src/client/app/common/views/filters/number.ts rename to src/client/filters/number.ts diff --git a/src/client/app/common/views/filters/user.ts b/src/client/filters/user.ts similarity index 65% rename from src/client/app/common/views/filters/user.ts rename to src/client/filters/user.ts index 9d4ae5c58b2eac0672094a048f563a83816a238b..e8f10c3db6e5a10bd2e526f2005425d5e5b567cf 100644 --- a/src/client/app/common/views/filters/user.ts +++ b/src/client/filters/user.ts @@ -1,7 +1,7 @@ import Vue from 'vue'; -import getAcct from '../../../../../misc/acct/render'; -import getUserName from '../../../../../misc/get-user-name'; -import { url } from '../../../config'; +import getAcct from '../../misc/acct/render'; +import getUserName from '../../misc/get-user-name'; +import { url } from '../config'; Vue.filter('acct', user => { return getAcct(user); diff --git a/src/client/i18n.ts b/src/client/i18n.ts new file mode 100644 index 0000000000000000000000000000000000000000..05d319fbafd8a6045504080bf76a555ed3f74b6e --- /dev/null +++ b/src/client/i18n.ts @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import VueI18n from 'vue-i18n'; +import { lang, locale } from './config'; + +Vue.use(VueI18n); + +export default new VueI18n({ + locale: lang, + messages: { + [lang]: locale + } +}); diff --git a/src/client/init.ts b/src/client/init.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ea95aa96b6c27d093ea46444f6adead196405bd --- /dev/null +++ b/src/client/init.ts @@ -0,0 +1,199 @@ +/** + * App entry point + */ + +import Vue from 'vue'; +import Vuex from 'vuex'; +import VueMeta from 'vue-meta'; +import PortalVue from 'portal-vue'; +import VAnimateCss from 'v-animate-css'; +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; + +import i18n from './i18n'; +import VueHotkey from './scripts/hotkey'; +import App from './app.vue'; +import MiOS from './mios'; +import { version, langs, instanceName } from './config'; +import PostFormDialog from './components/post-form-dialog.vue'; +import Dialog from './components/dialog.vue'; +import Menu from './components/menu.vue'; +import { router } from './router'; +import { applyTheme, lightTheme } from './theme'; + +Vue.use(Vuex); +Vue.use(VueHotkey); +Vue.use(VueMeta); +Vue.use(PortalVue); +Vue.use(VAnimateCss); +Vue.component('fa', FontAwesomeIcon); + +require('./directives'); +require('./components'); +require('./widgets'); +require('./filters'); + +Vue.mixin({ + methods: { + destroyDom() { + this.$destroy(); + + if (this.$el.parentNode) { + this.$el.parentNode.removeChild(this.$el); + } + } + } +}); + +console.info(`Misskey v${version}`); + +// v11互æ›æ€§ã®ãŸã‚ +if (localStorage.getItem('kyoppie') === 'yuppie') { + localStorage.clear(); + location.reload(true); +} + +if (localStorage.getItem('theme') == null) { + applyTheme(lightTheme); +} + +//#region Detect the user language +let lang = null; + +if (langs.map(x => x[0]).includes(navigator.language)) { + lang = navigator.language; +} else { + lang = langs.map(x => x[0]).find(x => x.split('-')[0] == navigator.language); + + if (lang == null) { + // Fallback + lang = 'en-US'; + } +} + +localStorage.setItem('lang', lang); +//#endregion + +// Detect the user agent +const ua = navigator.userAgent.toLowerCase(); +let 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 || window.innerWidth <= 1024) { + const viewport = document.getElementsByName("viewport").item(0); + viewport.setAttribute('content', + `${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`); + head.appendChild(viewport); +} + +//#region Fetch locale data +const cachedLocale = localStorage.getItem('locale'); + +if (cachedLocale == null) { + fetch(`/assets/locales/${lang}.${version}.json`) + .then(response => response.json()).then(locale => { + localStorage.setItem('locale', JSON.stringify(locale)); + i18n.locale = lang; + i18n.setLocaleMessage(lang, locale); + }); +} else { + // TODO: å¤ã„時ã ã‘æ›´æ–° + setTimeout(() => { + fetch(`/assets/locales/${lang}.${version}.json`) + .then(response => response.json()).then(locale => { + localStorage.setItem('locale', JSON.stringify(locale)); + }); + }, 1000 * 5); +} +//#endregion + +//#region Set lang attr +const html = document.documentElement; +html.setAttribute('lang', lang); +//#endregion + +// iOSã§ãƒ—ライベートモードã ã¨localStorageãŒä½¿ãˆãªã„ã®ã§æ—¢å˜ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’上書ãã™ã‚‹ +try { + localStorage.setItem('foo', 'bar'); +} catch (e) { + Storage.prototype.setItem = () => { }; // noop +} + +// http://qiita.com/junya/items/3ff380878f26ca447f85 +document.body.setAttribute('ontouchstart', ''); + +// アプリ基底è¦ç´ マウント +document.body.innerHTML = '<div id="app"></div>'; + +const os = new MiOS(); + +os.init(async () => { + if (os.store.state.settings.wallpaper) document.documentElement.style.backgroundImage = `url(${os.store.state.settings.wallpaper})`; + + if ('Notification' in window && os.store.getters.isSignedIn) { + // 許å¯ã‚’å¾—ã¦ã„ãªã‹ã£ãŸã‚‰ãƒªã‚¯ã‚¨ã‚¹ãƒˆ + if (Notification.permission === 'default') { + Notification.requestPermission(); + } + } + + const app = new Vue({ + store: os.store, + metaInfo: { + title: null, + titleTemplate: title => title ? `${title} | ${instanceName}` : instanceName + }, + data() { + return { + stream: os.stream, + isMobile: isMobile + }; + }, + methods: { + api: os.api, + getMeta: os.getMeta, + getMetaSync: os.getMetaSync, + signout: os.signout, + new(vm, props) { + const x = new vm({ + parent: this, + propsData: props + }).$mount(); + document.body.appendChild(x.$el); + return x; + }, + dialog(opts) { + const vm = this.new(Dialog, opts); + const p: any = new Promise((res) => { + vm.$once('ok', result => res({ canceled: false, result })); + vm.$once('cancel', () => res({ canceled: true })); + }); + p.close = () => { + vm.close(); + }; + return p; + }, + menu(opts) { + const vm = this.new(Menu, opts); + const p: any = new Promise((res) => { + vm.$once('closed', () => res()); + }); + return p; + }, + post(opts, cb) { + const vm = this.new(PostFormDialog, opts); + if (cb) vm.$once('closed', cb); + (vm as any).focus(); + }, + }, + router: router, + render: createEl => createEl(App) + }); + + os.app = app; + + // マウント + app.$mount('#app'); +}); diff --git a/src/client/app/mios.ts b/src/client/mios.ts similarity index 72% rename from src/client/app/mios.ts rename to src/client/mios.ts index 2c62f120ead81aedb5eca65f7dcb82541820ea45..282c51185f198a65ab4a4c85b30e69489ae530c1 100644 --- a/src/client/app/mios.ts +++ b/src/client/mios.ts @@ -1,14 +1,12 @@ import autobind from 'autobind-decorator'; import Vue from 'vue'; import { EventEmitter } from 'eventemitter3'; -import { v4 as uuid } from 'uuid'; import initStore from './store'; import { apiUrl, version, locale } from './config'; -import Progress from './common/scripts/loading'; +import Progress from './scripts/loading'; -import Err from './common/views/components/connect-failed.vue'; -import Stream from './common/scripts/stream'; +import Stream from './scripts/stream'; //#region api requests let spinner = null; @@ -27,26 +25,10 @@ export default class MiOS extends EventEmitter { chachedAt: Date; }; - public get instanceName() { - const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement; - if (siteName && siteName.content) { - return siteName.content; - } - - return 'Misskey'; - } - private isMetaFetching = false; public app: Vue; - /** - * Whether is debug mode - */ - public get debug() { - return this.store ? this.store.state.device.debug : false; - } - public store: ReturnType<typeof initStore>; /** @@ -59,54 +41,6 @@ export default class MiOS extends EventEmitter { */ 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; - - if (this.debug) { - (window as any).os = this; - } - } - - @autobind - public log(...args) { - if (!this.debug) return; - console.log.apply(null, args); - } - - @autobind - public logInfo(...args) { - if (!this.debug) return; - console.info.apply(null, args); - } - - @autobind - public logWarn(...args) { - if (!this.debug) return; - console.warn.apply(null, args); - } - - @autobind - public logError(...args) { - if (!this.debug) return; - console.error.apply(null, args); - } - @autobind public signout() { this.store.dispatch('logout'); @@ -154,10 +88,7 @@ export default class MiOS extends EventEmitter { // When failure .catch(() => { // Render the error screen - document.body.innerHTML = '<div id="err"></div>'; - new Vue({ - render: createEl => createEl(Err) - }).$mount('#err'); + document.body.innerHTML = '<div id="err">Error</div>'; Progress.done(); }); @@ -177,11 +108,9 @@ export default class MiOS extends EventEmitter { callback(); // Init service worker - if (this.shouldRegisterSw) { - this.getMeta().then(data => { - if (data.swPublickey) this.registerSw(data.swPublickey); - }); - } + this.getMeta().then(data => { + if (data.swPublickey) this.registerSw(data.swPublickey); + }); }; // ã‚ャッシュãŒã‚ã£ãŸã¨ã @@ -199,9 +128,9 @@ export default class MiOS extends EventEmitter { this.store.dispatch('mergeMe', freshData); }); } else { - // Get token from cookie or localStorage - const i = (document.cookie.match(/i=(\w+)/) || [null, null])[1] || localStorage.getItem('i'); - + // Get token from localStorage + const i = localStorage.getItem('i'); + fetchme(i, me => { if (me) { this.store.dispatch('login', me); @@ -240,18 +169,6 @@ export default class MiOS extends EventEmitter { }); }); - main.on('readAllMessagingMessages', () => { - this.store.dispatch('mergeMe', { - hasUnreadMessagingMessage: false - }); - }); - - main.on('unreadMessagingMessage', () => { - this.store.dispatch('mergeMe', { - hasUnreadMessagingMessage: true - }); - }); - main.on('unreadMention', () => { this.store.dispatch('mergeMe', { hasUnreadMentions: true @@ -276,6 +193,36 @@ export default class MiOS extends EventEmitter { }); }); + main.on('readAllMessagingMessages', () => { + this.store.dispatch('mergeMe', { + hasUnreadMessagingMessage: false + }); + }); + + main.on('unreadMessagingMessage', () => { + this.store.dispatch('mergeMe', { + hasUnreadMessagingMessage: true + }); + }); + + main.on('readAllAntennas', () => { + this.store.dispatch('mergeMe', { + hasUnreadAntenna: false + }); + }); + + main.on('unreadAntenna', () => { + this.store.dispatch('mergeMe', { + hasUnreadAntenna: true + }); + }); + + main.on('readAllAnnouncements', () => { + this.store.dispatch('mergeMe', { + hasUnreadAnnouncement: false + }); + }); + main.on('clientSettingUpdated', x => { this.store.commit('settings/set', { key: x.key, @@ -309,8 +256,6 @@ export default class MiOS extends EventEmitter { // When service worker activated navigator.serviceWorker.ready.then(registration => { - this.log('[sw] ready: ', registration); - this.swRegistration = registration; // Options of pushManager.subscribe @@ -327,8 +272,6 @@ export default class MiOS extends EventEmitter { // 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))); } @@ -342,11 +285,8 @@ export default class MiOS extends EventEmitter { }) // 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; } @@ -362,69 +302,40 @@ export default class MiOS extends EventEmitter { const sw = `/sw.${version}.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); - }); + navigator.serviceWorker.register(sw); } - public requests = []; - /** * Misskey APIã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã—ã¾ã™ * @param endpoint エンドãƒã‚¤ãƒ³ãƒˆå * @param data パラメータ */ @autobind - public api(endpoint: string, data: { [x: string]: any } = {}, silent = false): Promise<{ [x: string]: any }> { - if (!silent) { - if (++pending === 1) { - spinner = document.createElement('div'); - spinner.setAttribute('id', 'wait'); - document.body.appendChild(spinner); - } + public api(endpoint: string, data: { [x: string]: any } = {}, token?): Promise<{ [x: string]: any }> { + if (++pending === 1) { + spinner = document.createElement('div'); + spinner.setAttribute('id', 'wait'); + document.body.appendChild(spinner); } const onFinally = () => { - if (!silent) { - if (--pending === 0) spinner.parentNode.removeChild(spinner); - } + if (--pending === 0) spinner.parentNode.removeChild(spinner); }; const promise = new Promise((resolve, reject) => { // Append a credential if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token; - - const req = { - id: uuid(), - date: new Date(), - name: endpoint, - data, - res: null, - status: null - }; - - if (this.debug) { - this.requests.push(req); - } + if (token) (data as any).i = token; // Send request fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { method: 'POST', body: JSON.stringify(data), - credentials: endpoint === 'signin' ? 'include' : 'omit', + credentials: 'omit', cache: 'no-cache' }).then(async (res) => { 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) { @@ -484,24 +395,6 @@ export default class MiOS extends EventEmitter { } } -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 diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue new file mode 100644 index 0000000000000000000000000000000000000000..e47856bb94ffdcccf93d029950c7dbae0fd28cbd --- /dev/null +++ b/src/client/pages/about.vue @@ -0,0 +1,106 @@ +<template> +<div class="mmnnbwxb"> + <portal to="icon"><fa :icon="faInfoCircle"/></portal> + <portal to="title">{{ $t('about') }}</portal> + + <section class="_section info" v-if="meta"> + <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div> + <div class="_content" v-if="meta.description"> + <div>{{ meta.description }}</div> + </div> + <div class="_content table"> + <div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div> + <div><b></b><span>{{ meta.maintainerEmail }}</span></div> + </div> + <div class="_content table" v-if="stats"> + <div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div> + <div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div> + </div> + <div class="_content table"> + <div><b>Misskey</b><span>v{{ version }}</span></div> + </div> + </section> + + <section class="_section aboutMisskey"> + <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('aboutMisskey') }}</div> + <div class="_content"> + <div style="margin-bottom: 1em;">{{ $t('aboutMisskeyText') }}</div> + <div>{{ $t('misskeyMembers') }}</div> + <span class="members"> + <a href="https://github.com/syuilo" target="_blank">@syuilo</a> + <a href="https://github.com/AyaMorisawa" target="_blank">@AyaMorisawa</a> + <a href="https://github.com/mei23" target="_blank">@mei23</a> + <a href="https://github.com/acid-chicken" target="_blank">@acid-chicken</a> + <a href="https://github.com/tamaina" target="_blank">@tamaina</a> + <a href="https://github.com/rinsuki" target="_blank">@rinsuki</a> + </span> + <div style="margin-top: 1em;">{{ $t('misskeySource') }}</div> + <a href="https://github.com/syuilo/misskey" target="_blank" style="color: var(--link);">https://github.com/syuilo/misskey</a> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import { version } from '../config'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('instance') as string + }; + }, + + data() { + return { + version, + meta: null, + stats: null, + serverInfo: null, + faInfoCircle + } + }, + + created() { + this.$root.getMeta().then(meta => { + this.meta = meta; + }); + + this.$root.api('stats').then(res => { + this.stats = res; + }); + }, +}); +</script> + +<style lang="scss" scoped> +.mmnnbwxb { + > .info { + > .table { + > div { + display: flex; + + > * { + flex: 1; + } + } + } + } + + > .aboutMisskey { + > ._content { + > .members { + > a { + color: var(--link); + margin-right: 0.5em; + } + } + } + } +} +</style> diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue new file mode 100644 index 0000000000000000000000000000000000000000..586bc0c03c8cf9a24b081dd7585c54f04b286348 --- /dev/null +++ b/src/client/pages/announcements.vue @@ -0,0 +1,73 @@ +<template> +<div> + <portal to="icon"><fa :icon="faBroadcastTower"/></portal> + <portal to="title">{{ $t('announcements') }}</portal> + + <mk-pagination :pagination="pagination" #default="{items}" class="ruryvtyk" ref="list"> + <section class="_section announcement" v-for="(announcement, i) in items" :key="announcement.id" :data-index="i"> + <div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> + <div class="_content"> + <mfm :text="announcement.text"/> + <img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt=""/> + </div> + <div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead"> + <mk-button @click="read(announcement)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button> + </div> + </section> + </mk-pagination> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCheck, faBroadcastTower } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import MkPagination from '../components/ui/pagination.vue'; +import MkButton from '../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('announcements') as string + }; + }, + + components: { + MkPagination, + MkButton + }, + + data() { + return { + pagination: { + endpoint: 'announcements', + limit: 10, + }, + faCheck, faBroadcastTower + }; + }, + + methods: { + read(announcement) { + announcement.isRead = true; + this.$root.api('i/read-announcement', { announcementId: announcement.id }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.ruryvtyk { + > .announcement { + > ._content { + > img { + display: block; + max-height: 300px; + max-width: 100%; + } + } + } +} +</style> diff --git a/src/client/pages/auth.form.vue b/src/client/pages/auth.form.vue new file mode 100644 index 0000000000000000000000000000000000000000..80a792eb3601db728cb44a25c9669e57ed90f570 --- /dev/null +++ b/src/client/pages/auth.form.vue @@ -0,0 +1,63 @@ +<template> +<section class="_section"> + <div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div> + <div class="_content"> + <h2>{{ app.name }}</h2> + <p class="id">{{ app.id }}</p> + <p class="description">{{ app.description }}</p> + </div> + <div class="_content"> + <h2>{{ $t('_auth.permissionAsk') }}</h2> + <ul> + <template v-for="p in app.permission"> + <li :key="p">{{ $t(`_permissions.${p}`) }}</li> + </template> + </ul> + </div> + <div class="_footer"> + <mk-button @click="cancel" inline>{{ $t('cancel') }}</mk-button> + <mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import MkButton from '../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + components: { + MkButton + }, + props: ['session'], + computed: { + name(): string { + const el = document.createElement('div'); + el.textContent = this.app.name + return el.innerHTML; + }, + app(): any { + return this.session.app; + } + }, + methods: { + cancel() { + this.$root.api('auth/deny', { + token: this.session.token + }).then(() => { + this.$emit('denied'); + }); + }, + + accept() { + this.$root.api('auth/accept', { + token: this.session.token + }).then(() => { + this.$emit('accepted'); + }); + } + } +}); +</script> diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue new file mode 100644 index 0000000000000000000000000000000000000000..15ec81e01985fa8a60ffbbebb0414e71e1560008 --- /dev/null +++ b/src/client/pages/auth.vue @@ -0,0 +1,93 @@ +<template> +<div class="_panel" v-if="$store.getters.isSignedIn && fetching"> + <mk-loading/> +</div> +<div v-else-if="$store.getters.isSignedIn"> + <x-form + class="form" + ref="form" + v-if="state == 'waiting'" + :session="session" + @denied="state = 'denied'" + @accepted="accepted" + /> + <div class="denied _panel" v-if="state == 'denied'"> + <h1>{{ $t('denied') }}</h1> + <p>{{ $t('denied-paragraph') }}</p> + </div> + <div class="accepted _panel" v-if="state == 'accepted'"> + <h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1> + <p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p> + <p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p> + </div> + <div class="error _panel" v-if="state == 'fetch-session-error'"> + <p>{{ $t('error') }}</p> + </div> +</div> +<div class="signin" v-else> + <h1>{{ $t('sign-in') }}</h1> + <mk-signin/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import XForm from './auth.form.vue'; + +export default Vue.extend({ + i18n, + components: { + XForm + }, + data() { + return { + state: null, + session: null, + fetching: true + }; + }, + computed: { + token(): string { + return this.$route.params.token; + } + }, + mounted() { + if (!this.$store.getters.isSignedIn) return; + + // Fetch session + this.$root.api('auth/session/show', { + token: this.token + }).then(session => { + this.session = session; + this.fetching = false; + + // æ—¢ã«é€£æºã—ã¦ã„ãŸå ´åˆ + if (this.session.app.isAuthorized) { + this.$root.api('auth/accept', { + token: this.session.token + }).then(() => { + this.accepted(); + }); + } else { + this.state = 'waiting'; + } + }).catch(error => { + this.state = 'fetch-session-error'; + this.fetching = false; + }); + }, + methods: { + accepted() { + this.state = 'accepted'; + if (this.session.app.callbackUrl) { + location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; + } + } + } +}); +</script> + +<style lang="scss" scoped> + +</style> diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue new file mode 100644 index 0000000000000000000000000000000000000000..24a0d91ff6bb2c480022056f66e473dff091a6c8 --- /dev/null +++ b/src/client/pages/drive.vue @@ -0,0 +1,87 @@ +<template> +<div> + <portal to="header"> + <button @click="menu" class="_button _jmoebdiw_"> + <fa :icon="faCloud" style="margin-right: 8px;"/> + <span v-if="folder">{{ $t('drive') }} ({{ folder.name }})</span> + <span v-else>{{ $t('drive') }}</span> + <fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> + </button> + </portal> + <x-drive ref="drive" @cd="x => folder = x"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCloud, faAngleDown, faAngleUp, faFolderPlus, faUpload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import XDrive from '../components/drive.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('drive') as string + }; + }, + + components: { + XDrive + }, + + data() { + return { + menuOpened: false, + folder: null, + faCloud, faAngleDown, faAngleUp + }; + }, + + methods: { + menu(ev) { + this.menuOpened = true; + this.$root.menu({ + items: [{ + text: this.$t('addFile'), + type: 'label' + }, { + text: this.$t('upload'), + icon: faUpload, + action: () => { this.$refs.drive.selectLocalFile(); } + }, { + text: this.$t('fromUrl'), + icon: faLink, + action: () => { this.$refs.drive.urlUpload(); } + }, null, { + text: this.folder ? this.folder.name : this.$t('drive'), + type: 'label' + }, this.folder ? { + text: this.$t('renameFolder'), + icon: faICursor, + action: () => { this.$refs.drive.renameFolder(); } + } : undefined, this.folder ? { + text: this.$t('deleteFolder'), + icon: faTrashAlt, + action: () => { this.$refs.drive.deleteFolder(); } + } : undefined, { + text: this.$t('createFolder'), + icon: faFolderPlus, + action: () => { this.$refs.drive.createFolder(); } + }], + fixed: true, + noCenter: true, + source: ev.currentTarget || ev.target + }).then(() => { + this.menuOpened = false; + }); + } + } +}); +</script> + +<style lang="scss"> +._jmoebdiw_ { + height: 100%; + padding: 0 16px; + font-weight: bold; +} +</style> diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue new file mode 100644 index 0000000000000000000000000000000000000000..ba2c3faa6cde543c8693ce402e3f5679582fa59b --- /dev/null +++ b/src/client/pages/explore.vue @@ -0,0 +1,212 @@ +<template> +<div> + <portal to="icon"><fa :icon="faHashtag"/></portal> + <portal to="title">{{ $t('explore') }}</portal> + + <div class="localfedi7 _panel" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> + <header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header> + <div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div> + </div> + + <template v-if="tag == null"> + <x-user-list :pagination="pinnedUsers" :expanded="false"> + <fa :icon="faBookmark" fixed-width/>{{ $t('pinnedUsers') }} + </x-user-list> + <x-user-list :pagination="popularUsers" :expanded="false"> + <fa :icon="faChartLine" fixed-width/>{{ $t('popularUsers') }} + </x-user-list> + <x-user-list :pagination="recentlyUpdatedUsers" :expanded="false"> + <fa :icon="faCommentAlt" fixed-width/>{{ $t('recentlyUpdatedUsers') }} + </x-user-list> + <x-user-list :pagination="recentlyRegisteredUsers" :expanded="false"> + <fa :icon="faPlus" fixed-width/>{{ $t('recentlyRegisteredUsers') }} + </x-user-list> + </template> + + <div class="localfedi7 _panel" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)`, marginTop: 'var(--margin)' }"> + <header><span>{{ $t('exploreFediverse') }}</span></header> + </div> + + <mk-container :body-togglable="true" :expanded="false" ref="tags"> + <template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popularTags') }}</template> + + <div class="vxjfqztj"> + <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link> + <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link> + </div> + </mk-container> + + <x-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}`"> + <fa :icon="faHashtag" fixed-width/>{{ tag }} + </x-user-list> + <template v-if="tag == null"> + <x-user-list :pagination="popularUsersF" :expanded="false"> + <fa :icon="faChartLine" fixed-width/>{{ $t('popularUsers') }} + </x-user-list> + <x-user-list :pagination="recentlyUpdatedUsersF" :expanded="false"> + <fa :icon="faCommentAlt" fixed-width/>{{ $t('recentlyUpdatedUsers') }} + </x-user-list> + <x-user-list :pagination="recentlyRegisteredUsersF" :expanded="false"> + <fa :icon="faRocket" fixed-width/>{{ $t('recentlyDiscoveredUsers') }} + </x-user-list> + </template> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons'; +import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import XUserList from '../components/user-list.vue'; +import MkContainer from '../components/ui/container.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('explore') as string + }; + }, + + components: { + XUserList, + MkContainer, + }, + + props: { + tag: { + type: String, + required: false + } + }, + + data() { + return { + pinnedUsers: { endpoint: 'pinned-users' }, + popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { + state: 'alive', + origin: 'local', + sort: '+follower', + } }, + recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { + origin: 'local', + sort: '+updatedAt', + } }, + recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { + origin: 'local', + state: 'alive', + sort: '+createdAt', + } }, + popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { + state: 'alive', + origin: 'remote', + sort: '+follower', + } }, + recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { + origin: 'combined', + sort: '+updatedAt', + } }, + recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { + origin: 'combined', + sort: '+createdAt', + } }, + tagsLocal: [], + tagsRemote: [], + stats: null, + meta: null, + num: Vue.filter('number'), + faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket + }; + }, + + computed: { + tagUsers(): any { + return { + endpoint: 'hashtags/users', + limit: 30, + params: { + tag: this.tag, + origin: 'combined', + sort: '+follower', + } + }; + }, + }, + + watch: { + tag() { + if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); + } + }, + + created() { + this.$root.api('hashtags/list', { + sort: '+attachedLocalUsers', + attachedToLocalUserOnly: true, + limit: 30 + }).then(tags => { + this.tagsLocal = tags; + }); + this.$root.api('hashtags/list', { + sort: '+attachedRemoteUsers', + attachedToRemoteUserOnly: true, + limit: 30 + }).then(tags => { + this.tagsRemote = tags; + }); + this.$root.api('stats').then(stats => { + this.stats = stats; + }); + this.$root.getMeta().then(meta => { + this.meta = meta; + }); + }, +}); +</script> + +<style lang="scss" scoped> +.localfedi7 { + color: #fff; + padding: 16px; + height: 80px; + background-position: 50%; + background-size: cover; + margin-bottom: var(--margin); + + > * { + &:not(:last-child) { + margin-bottom: 8px; + } + + > span { + display: inline-block; + padding: 6px 8px; + background: rgba(0, 0, 0, 0.7); + } + } + + > header { + font-size: 20px; + font-weight: bold; + } + + > div { + font-size: 14px; + opacity: 0.8; + } +} + +.vxjfqztj { + padding: 16px; + + > * { + margin-right: 16px; + + &.local { + font-weight: bold; + } + } +} +</style> diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue new file mode 100644 index 0000000000000000000000000000000000000000..59bef2ca91d392a00f343d53af84fc1bed83e8a4 --- /dev/null +++ b/src/client/pages/favorites.vue @@ -0,0 +1,48 @@ +<template> +<div> + <portal to="icon"><fa :icon="faStar"/></portal> + <portal to="title">{{ $t('favorites') }}</portal> + <x-notes :pagination="pagination" :detail="true" :extract="items => items.map(item => item.note)" @before="before()" @after="after()"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faStar } from '@fortawesome/free-solid-svg-icons'; +import Progress from '../scripts/loading'; +import XNotes from '../components/notes.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('favorites') as string + }; + }, + + components: { + XNotes + }, + + data() { + return { + pagination: { + endpoint: 'i/favorites', + limit: 10, + params: () => ({ + }) + }, + faStar + }; + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue new file mode 100644 index 0000000000000000000000000000000000000000..e6293e9e836967b6be022436527e1c9a4931dbf6 --- /dev/null +++ b/src/client/pages/featured.vue @@ -0,0 +1,47 @@ +<template> +<div> + <portal to="icon"><fa :icon="faFireAlt"/></portal> + <portal to="title">{{ $t('featured') }}</portal> + <x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faFireAlt } from '@fortawesome/free-solid-svg-icons'; +import Progress from '../scripts/loading'; +import XNotes from '../components/notes.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('featured') as string + }; + }, + + components: { + XNotes + }, + + data() { + return { + pagination: { + endpoint: 'notes/featured', + limit: 10, + offsetMode: true + }, + faFireAlt + }; + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue new file mode 100644 index 0000000000000000000000000000000000000000..c302088b971c217ed1b807f3e99873a32d5d0ec4 --- /dev/null +++ b/src/client/pages/follow-requests.vue @@ -0,0 +1,142 @@ +<template> +<mk-pagination :pagination="pagination" #default="{items}" class="mk-follow-requests" ref="list"> + <div class="user _panel" v-for="(req, i) in items" :key="req.id" :data-index="i"> + <mk-avatar class="avatar" :user="req.follower"/> + <div class="body"> + <div class="name"> + <router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link> + <p class="acct">@{{ req.follower | acct }}</p> + </div> + <div class="description" v-if="req.follower.description" :title="req.follower.description"> + <mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/> + </div> + <div class="actions"> + <button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button> + <button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button> + </div> + </div> + </div> +</mk-pagination> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '../components/ui/pagination.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('followRequests') as string + }; + }, + + components: { + MkPagination + }, + + data() { + return { + pagination: { + endpoint: 'following/requests/list', + limit: 10, + }, + faCheck, faTimes + }; + }, + + methods: { + accept(user) { + this.$root.api('following/requests/accept', { userId: user.id }).then(() => { + this.$refs.list.reload(); + }); + }, + reject(user) { + this.$root.api('following/requests/reject', { userId: user.id }).then(() => { + this.$refs.list.reload(); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-follow-requests { + > .user { + display: flex; + padding: 16px; + + > .avatar { + display: block; + flex-shrink: 0; + margin: 0 12px 0 0; + width: 42px; + height: 42px; + border-radius: 8px; + } + + > .body { + display: flex; + width: calc(100% - 54px); + position: relative; + + > .name { + width: 45%; + + @media (max-width: 500px) { + width: 100%; + } + + > .name, + > .acct { + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0; + } + + > .name { + font-size: 16px; + line-height: 24px; + } + + > .acct { + font-size: 15px; + line-height: 16px; + opacity: 0.7; + } + } + + > .description { + width: 55%; + line-height: 42px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.7; + font-size: 14px; + padding-right: 40px; + padding-left: 8px; + box-sizing: border-box; + + @media (max-width: 500px) { + display: none; + } + } + + > .actions { + position: absolute; + top: 0; + bottom: 0; + right: 0; + margin: auto 0; + + > button { + padding: 12px; + } + } + } + } +} +</style> diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue new file mode 100644 index 0000000000000000000000000000000000000000..d765259737bb1b11868ef079e0fd0d3733af1751 --- /dev/null +++ b/src/client/pages/follow.vue @@ -0,0 +1,98 @@ +<template> +<div class="mk-follow-page"> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + + created() { + const acct = new URL(location.href).searchParams.get('acct'); + if (acct == null) return; + + const dialog = this.$root.dialog({ + type: 'waiting', + text: this.$t('fetchingAsApObject') + '...', + showOkButton: false, + showCancelButton: false, + cancelableByBgClick: false + }); + + if (acct.startsWith('https://')) { + this.$root.api('ap/show', { + uri: acct + }).then(res => { + if (res.type == 'User') { + this.follow(res.object); + } else { + this.$root.dialog({ + type: 'error', + text: 'Not a user' + }).then(() => { + window.close(); + }); + } + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }).then(() => { + window.close(); + }); + }).finally(() => { + dialog.close(); + }); + } else { + this.$root.api('users/show', parseAcct(acct)).then(user => { + this.follow(user); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }).then(() => { + window.close(); + }); + }).finally(() => { + dialog.close(); + }); + } + }, + + methods: { + async follow(user) { + const { canceled } = await this.$root.dialog({ + type: 'question', + text: this.$t('followConfirm', { name: user.name || user.username }), + showCancelButton: true + }); + + if (canceled) { + window.close(); + return; + } + + this.$root.api('following/create', { + userId: user.id + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }).then(() => { + window.close(); + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }).then(() => { + window.close(); + }); + }); + } + } +}); +</script> diff --git a/src/client/pages/index.home.vue b/src/client/pages/index.home.vue new file mode 100644 index 0000000000000000000000000000000000000000..0c3d2e7f8646c166562021eb1138fd45267d5af6 --- /dev/null +++ b/src/client/pages/index.home.vue @@ -0,0 +1,190 @@ +<template> +<div class="mk-home" v-hotkey.global="keymap"> + <portal to="header"> + <button @click="choose" class="_button _kjvfvyph_"> + <i><fa v-if="$store.state.i.hasUnreadAntenna" :icon="faCircle"/></i> + <fa v-if="src === 'home'" :icon="faHome"/> + <fa v-if="src === 'local'" :icon="faComments"/> + <fa v-if="src === 'social'" :icon="faShareAlt"/> + <fa v-if="src === 'global'" :icon="faGlobe"/> + <fa v-if="src === 'list'" :icon="faListUl"/> + <fa v-if="src === 'antenna'" :icon="faSatellite"/> + <span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : $t('_timelines.' + src) }}</span> + <fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> + </button> + </portal> + <x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" @before="before()" @after="after()"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faCircle } from '@fortawesome/free-solid-svg-icons'; +import { faComments } from '@fortawesome/free-regular-svg-icons'; +import Progress from '../scripts/loading'; +import XTimeline from '../components/timeline.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('timeline') as string + }; + }, + + components: { + XTimeline + }, + + data() { + return { + src: 'home', + list: null, + antenna: null, + menuOpened: false, + faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faCircle + }; + }, + + computed: { + keymap(): any { + return { + 't': this.focus + }; + } + }, + + watch: { + src() { + this.showNav = false; + this.saveSrc(); + }, + list(x) { + this.showNav = false; + this.saveSrc(); + if (x != null) this.antenna = null; + }, + antenna(x) { + this.showNav = false; + this.saveSrc(); + if (x != null) this.list = null; + }, + }, + + created() { + this.$root.getMeta().then((meta: Record<string, any>) => { + if (!( + this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin + ) && this.src === 'global') this.src = 'local'; + if (!( + this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin + ) && ['local', 'social'].includes(this.src)) this.src = 'home'; + }); + if (this.$store.state.device.tl) { + this.src = this.$store.state.device.tl.src; + if (this.src === 'list') { + this.list = this.$store.state.device.tl.arg; + } else if (this.src === 'antenna') { + this.antenna = this.$store.state.device.tl.arg; + } + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + }, + + async choose(ev) { + this.menuOpened = true; + const [antennas, lists] = await Promise.all([ + this.$root.api('antennas/list'), + this.$root.api('users/lists/list') + ]); + const antennaItems = antennas.map(antenna => ({ + text: antenna.name, + icon: faSatellite, + indicate: antenna.hasUnreadNote, + action: () => { + this.antenna = antenna; + this.setSrc('antenna'); + } + })); + const listItems = lists.map(list => ({ + text: list.name, + icon: faListUl, + action: () => { + this.list = list; + this.setSrc('list'); + } + })); + this.$root.menu({ + items: [{ + text: this.$t('_timelines.home'), + icon: faHome, + action: () => { this.setSrc('home') } + }, { + text: this.$t('_timelines.local'), + icon: faComments, + action: () => { this.setSrc('local') } + }, { + text: this.$t('_timelines.social'), + icon: faShareAlt, + action: () => { this.setSrc('social') } + }, { + text: this.$t('_timelines.global'), + icon: faGlobe, + action: () => { this.setSrc('global') } + }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], + fixed: true, + noCenter: true, + source: ev.currentTarget || ev.target + }).then(() => { + this.menuOpened = false; + }); + }, + + setSrc(src) { + this.src = src; + }, + + saveSrc() { + this.$store.commit('device/setTl', { + src: this.src, + arg: this.src == 'list' ? this.list : this.antenna + }); + }, + + focus() { + (this.$refs.tl as any).focus(); + } + } +}); +</script> + +<style lang="scss"> +@keyframes blink { + 0% { opacity: 1; } + 30% { opacity: 1; } + 90% { opacity: 0; } +} + +._kjvfvyph_ { + position: relative; + height: 100%; + padding: 0 16px; + font-weight: bold; + + > i { + position: absolute; + top: 16px; + right: 8px; + color: var(--accent); + font-size: 12px; + animation: blink 1s infinite; + } +} +</style> diff --git a/src/client/app/mobile/views/pages/index.vue b/src/client/pages/index.vue similarity index 66% rename from src/client/app/mobile/views/pages/index.vue rename to src/client/pages/index.vue index 5d11fc54231f1f1421b71deb37ab41e68db988a1..732d9b71cc0d568ad6157cfbf0baf89be91bfc53 100644 --- a/src/client/app/mobile/views/pages/index.vue +++ b/src/client/pages/index.vue @@ -4,13 +4,12 @@ <script lang="ts"> import Vue from 'vue'; -import Home from './home.vue'; -import Welcome from './welcome.vue'; +import Home from './index.home.vue'; export default Vue.extend({ components: { Home, - Welcome + Welcome: () => import('./index.welcome.vue').then(m => m.default), } }); </script> diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue new file mode 100644 index 0000000000000000000000000000000000000000..1b0cc7d034aeba0c84b46c1639da8db05b78f2b7 --- /dev/null +++ b/src/client/pages/index.welcome.entrance.vue @@ -0,0 +1,103 @@ +<template> +<div class="rsqzvsbo"> + <div class="_panel about"> + <div class="banner" :style="{ backgroundImage: `url(${ banner })` }"></div> + <div class="body"> + <h1 class="name" v-html="name || host"></h1> + <div class="desc" v-html="description || $t('introMisskey')"></div> + <mk-button @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</mk-button> + <mk-button @click="signin()" style="display: inline-block;">{{ $t('login') }}</mk-button> + </div> + </div> + <x-notes :pagination="featuredPagination"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { toUnicode } from 'punycode'; +import XSigninDialog from '../components/signin-dialog.vue'; +import XSignupDialog from '../components/signup-dialog.vue'; +import MkButton from '../components/ui/button.vue'; +import XNotes from '../components/notes.vue'; +import i18n from '../i18n'; +import { host } from '../config'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + XNotes, + }, + + data() { + return { + featuredPagination: { + endpoint: 'notes/featured', + limit: 10, + noPaging: true, + }, + host: toUnicode(host), + meta: null, + name: null, + description: null, + banner: null, + announcements: [], + }; + }, + + created() { + this.$root.getMeta().then(meta => { + this.meta = meta; + this.name = meta.name; + this.description = meta.description; + this.announcements = meta.announcements; + this.banner = meta.bannerUrl; + }); + + this.$root.api('stats').then(stats => { + this.stats = stats; + }); + }, + + methods: { + signin() { + this.$root.new(XSigninDialog, { + autoSet: true + }); + }, + + signup() { + this.$root.new(XSignupDialog); + } + } +}); +</script> + +<style lang="scss" scoped> +.rsqzvsbo { + > .about { + overflow: hidden; + margin-bottom: var(--margin); + + > .banner { + height: 170px; + background-size: cover; + background-position: center center; + } + + > .body { + padding: 32px; + + @media (max-width: 500px) { + padding: 16px; + } + + > .name { + margin: 0 0 0.5em 0; + } + } + } +} +</style> diff --git a/src/client/pages/index.welcome.setup.vue b/src/client/pages/index.welcome.setup.vue new file mode 100644 index 0000000000000000000000000000000000000000..a339ac0a28e8e4624ff763bc694a47eadfc1ba41 --- /dev/null +++ b/src/client/pages/index.welcome.setup.vue @@ -0,0 +1,102 @@ +<template> +<form class="mk-setup" @submit.prevent="submit()"> + <h1>Welcome to Misskey!</h1> + <div> + <p>{{ $t('intro') }}</p> + <mk-input v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required> + <span>{{ $t('username') }}</span> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + </mk-input> + <mk-input v-model="password" type="password"> + <span>{{ $t('password') }}</span> + <template #prefix><fa :icon="faLock"/></template> + </mk-input> + <footer> + <mk-button primary type="submit" :disabled="submitting">{{ submitting ? $t('processing') : $t('done') }}<mk-ellipsis v-if="submitting"/></mk-button> + </footer> + </div> +</form> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faLock } from '@fortawesome/free-solid-svg-icons'; +import MkButton from '../components/ui/button.vue'; +import MkInput from '../components/ui/input.vue'; +import { host } from '../config'; +import i18n from '../i18n'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + MkInput, + }, + + data() { + return { + username: '', + password: '', + submitting: false, + host, + faLock + } + }, + + methods: { + submit() { + if (this.submitting) return; + this.submitting = true; + + this.$root.api('admin/accounts/create', { + username: this.username, + password: this.password, + }).then(res => { + localStorage.setItem('i', res.token); + location.href = '/'; + }).catch(() => { + this.submitting = false; + + this.$root.dialog({ + type: 'error', + text: this.$t('some-error') + }); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-setup { + border-radius: var(--radius); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + overflow: hidden; + + > h1 { + margin: 0; + font-size: 1.5em; + text-align: center; + padding: 32px; + background: var(--accent); + color: #fff; + } + + > div { + padding: 32px; + background: var(--panel); + + > p { + margin-top: 0; + } + + > footer { + > * { + margin: 0 auto; + } + } + } +} +</style> diff --git a/src/client/pages/index.welcome.vue b/src/client/pages/index.welcome.vue new file mode 100644 index 0000000000000000000000000000000000000000..213c3db22c238e592253a9ac87e40c276a88f698 --- /dev/null +++ b/src/client/pages/index.welcome.vue @@ -0,0 +1,34 @@ +<template> +<div v-if="meta" class="mk-welcome"> + <portal to="title">{{ instanceName }}</portal> + <x-setup v-if="meta.requireSetup"/> + <x-entrance v-else/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XSetup from './index.welcome.setup.vue'; +import XEntrance from './index.welcome.entrance.vue'; +import { getInstanceName } from '../scripts/get-instance-name'; + +export default Vue.extend({ + components: { + XSetup, + XEntrance, + }, + + data() { + return { + meta: null, + instanceName: getInstanceName(), + } + }, + + created() { + this.$root.getMeta().then(meta => { + this.meta = meta; + }); + } +}); +</script> diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue new file mode 100644 index 0000000000000000000000000000000000000000..71cec64c7b13334e0d0c8fc1dbc46bc445a3bd15 --- /dev/null +++ b/src/client/pages/instance/announcements.vue @@ -0,0 +1,129 @@ +<template> +<div class="ztgjmzrw"> + <portal to="icon"><fa :icon="faBroadcastTower"/></portal> + <portal to="title">{{ $t('announcements') }}</portal> + <mk-button @click="add()" primary style="margin: 0 auto 16px auto;"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button> + <section class="_section announcements"> + <div class="_content announcement" v-for="announcement in announcements"> + <mk-input v-model="announcement.title" style="margin-top: 8px;"> + <span>{{ $t('title') }}</span> + </mk-input> + <mk-textarea v-model="announcement.text"> + <span>{{ $t('text') }}</span> + </mk-textarea> + <mk-input v-model="announcement.imageUrl"> + <span>{{ $t('imageUrl') }}</span> + </mk-input> + <p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> + <div class="buttons"> + <mk-button class="button" inline @click="save(announcement)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + <mk-button class="button" inline @click="remove(announcement)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button> + </div> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../../i18n'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkTextarea from '../../components/ui/textarea.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('announcements') as string + }; + }, + + components: { + MkButton, + MkInput, + MkTextarea, + }, + + data() { + return { + announcements: [], + faBroadcastTower, faSave, faTrashAlt, faPlus + } + }, + + created() { + this.$root.api('admin/announcements/list').then(announcements => { + this.announcements = announcements; + }); + }, + + methods: { + add() { + this.announcements.unshift({ + id: null, + title: '', + text: '', + imageUrl: null + }); + }, + + remove(announcement) { + this.$root.dialog({ + type: 'warning', + text: this.$t('removeAreYouSure', { x: announcement.title }), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + this.announcements = this.announcements.filter(x => x != announcement); + this.$root.api('admin/announcements/delete', announcement); + }); + }, + + save(announcement) { + if (announcement.id == null) { + this.$root.api('admin/announcements/create', announcement).then(() => { + this.$root.dialog({ + type: 'success', + text: this.$t('saved') + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } else { + this.$root.api('admin/announcements/update', announcement).then(() => { + this.$root.dialog({ + type: 'success', + text: this.$t('saved') + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.ztgjmzrw { + > .announcements { + > .announcement { + > .buttons { + > .button:first-child { + margin-right: 8px; + } + } + } + } +} +</style> diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue new file mode 100644 index 0000000000000000000000000000000000000000..7a69a7efe6f0f7db70d926de45e32aef53fc9683 --- /dev/null +++ b/src/client/pages/instance/emojis.vue @@ -0,0 +1,253 @@ +<template> +<div class="mk-instance-emojis"> + <portal to="icon"><fa :icon="faLaugh"/></portal> + <portal to="title">{{ $t('customEmojis') }}</portal> + <section class="_section local"> + <div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div> + <div class="_content"> + <input ref="file" type="file" style="display: none;" @change="onChangeFile"/> + <mk-pagination :pagination="pagination" class="emojis" ref="emojis"> + <template #empty><span>{{ $t('noCustomEmojis') }}</span></template> + <template #default="{items}"> + <div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" :data-index="i" @click="selected = emoji" :class="{ selected: selected && (selected.id === emoji.id) }"> + <img :src="emoji.url" class="img" :alt="emoji.name"/> + <div class="body"> + <span class="name">{{ emoji.name }}</span> + </div> + </div> + </template> + </mk-pagination> + </div> + <div class="_footer"> + <mk-button inline primary @click="add()"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button> + <mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button> + </div> + </section> + <section class="_section remote"> + <div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div> + <div class="_content"> + <mk-input v-model="host" :debounce="true" style="margin-top: 0;"><span>{{ $t('host') }}</span></mk-input> + <mk-pagination :pagination="remotePagination" class="emojis" ref="remoteEmojis"> + <template #empty><span>{{ $t('noCustomEmojis') }}</span></template> + <template #default="{items}"> + <div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" :data-index="i" @click="selectedRemote = emoji" :class="{ selected: selectedRemote && (selectedRemote.id === emoji.id) }"> + <img :src="emoji.url" class="img" :alt="emoji.name"/> + <div class="body"> + <span class="name">{{ emoji.name }}</span> + <span class="host">{{ emoji.host }}</span> + </div> + </div> + </template> + </mk-pagination> + </div> + <div class="_footer"> + <mk-button inline primary :disabled="selectedRemote == null" @click="im()"><fa :icon="faPlus"/> {{ $t('import') }}</mk-button> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt, faLaugh } from '@fortawesome/free-regular-svg-icons'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkPagination from '../../components/ui/pagination.vue'; +import { apiUrl } from '../../config'; + +export default Vue.extend({ + metaInfo() { + return { + title: `${this.$t('customEmojis')} | ${this.$t('instance')}` + }; + }, + + components: { + MkButton, + MkInput, + MkPagination, + }, + + data() { + return { + name: null, + selected: null, + selectedRemote: null, + host: '', + pagination: { + endpoint: 'admin/emoji/list', + limit: 10, + }, + remotePagination: { + endpoint: 'admin/emoji/list-remote', + limit: 10, + params: () => ({ + host: this.host ? this.host : null + }) + }, + faTrashAlt, faPlus, faLaugh + } + }, + + watch: { + host() { + this.$refs.remoteEmojis.reload(); + } + }, + + methods: { + async add() { + const { canceled: canceled, result: name } = await this.$root.dialog({ + title: this.$t('emojiName'), + input: true + }); + if (canceled) return; + + this.name = name; + + (this.$refs.file as any).click(); + }, + + onChangeFile() { + const [file] = Array.from((this.$refs.file as any).files); + if (file == null) return; + + const data = new FormData(); + data.append('file', file); + data.append('name', this.name); + data.append('i', this.$store.state.i.token); + + const dialog = this.$root.dialog({ + type: 'waiting', + text: this.$t('uploading') + '...', + showOkButton: false, + showCancelButton: false, + cancelableByBgClick: false + }); + + fetch(apiUrl + '/admin/emoji/add', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.$refs.emojis.reload(); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }) + .finally(() => { + dialog.close(); + }); + }, + + async del() { + const { canceled } = await this.$root.dialog({ + type: 'warning', + text: this.$t('removeAreYouSure', { x: this.selected.name }), + showCancelButton: true + }); + if (canceled) return; + + this.$root.api('admin/emoji/remove', { + id: this.selected.id + }).then(() => { + this.$refs.emojis.reload(); + }); + }, + + im() { + this.$root.api('admin/emoji/copy', { + emojiId: this.selectedRemote.id, + }).then(() => { + this.$refs.emojis.reload(); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-instance-emojis { + > .local { + > ._content { + max-height: 300px; + overflow: auto; + + > .emojis { + > .emoji { + display: flex; + align-items: center; + + &.selected { + background: var(--accent); + box-shadow: 0 0 0 8px var(--accent); + color: #fff; + } + + > .img { + width: 50px; + height: 50px; + } + + > .body { + padding: 8px; + + > .name { + display: block; + } + } + } + } + } + } + + > .remote { + > ._content { + max-height: 300px; + overflow: auto; + + > .emojis { + > .emoji { + display: flex; + align-items: center; + + &.selected { + background: var(--accent); + box-shadow: 0 0 0 8px var(--accent); + color: #fff; + } + + > .img { + width: 32px; + height: 32px; + } + + > .body { + padding: 0 8px; + + > .name { + display: block; + } + + > .host { + opacity: 0.5; + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/instance/federation.instance.vue b/src/client/pages/instance/federation.instance.vue new file mode 100644 index 0000000000000000000000000000000000000000..a27556064a84dd6f6d36866043af078d2b0cd202 --- /dev/null +++ b/src/client/pages/instance/federation.instance.vue @@ -0,0 +1,576 @@ +<template> +<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true"> + <template #header>{{ instance.host }}</template> + <div class="mk-instance-info"> + <div class="table info"> + <div class="row"> + <div class="cell"> + <div class="label">{{ $t('software') }}</div> + <div class="data">{{ instance.softwareName || '?' }}</div> + </div> + <div class="cell"> + <div class="label">{{ $t('version') }}</div> + <div class="data">{{ instance.softwareVersion || '?' }}</div> + </div> + </div> + </div> + <div class="table data"> + <div class="row"> + <div class="cell"> + <div class="label"><fa :icon="faCrosshairs" fixed-width class="icon"/>{{ $t('registeredAt') }}</div> + <div class="data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div> + </div> + </div> + <div class="row"> + <div class="cell"> + <div class="label"><fa :icon="faCloudDownloadAlt" fixed-width class="icon"/>{{ $t('following') }}</div> + <div class="data clickable" @click="showFollowing()">{{ instance.followingCount | number }}</div> + </div> + <div class="cell"> + <div class="label"><fa :icon="faCloudUploadAlt" fixed-width class="icon"/>{{ $t('followers') }}</div> + <div class="data clickable" @click="showFollowers()">{{ instance.followersCount | number }}</div> + </div> + </div> + <div class="row"> + <div class="cell"> + <div class="label"><fa :icon="faUsers" fixed-width class="icon"/>{{ $t('users') }}</div> + <div class="data clickable" @click="showUsers()">{{ instance.usersCount | number }}</div> + </div> + <div class="cell"> + <div class="label"><fa :icon="faPencilAlt" fixed-width class="icon"/>{{ $t('notes') }}</div> + <div class="data">{{ instance.notesCount | number }}</div> + </div> + </div> + <div class="row"> + <div class="cell"> + <div class="label"><fa :icon="faFileImage" fixed-width class="icon"/>{{ $t('files') }}</div> + <div class="data">{{ instance.driveFiles | number }}</div> + </div> + <div class="cell"> + <div class="label"><fa :icon="faDatabase" fixed-width class="icon"/>{{ $t('storageUsage') }}</div> + <div class="data">{{ instance.driveUsage | bytes }}</div> + </div> + </div> + <div class="row"> + <div class="cell"> + <div class="label"><fa :icon="faLongArrowAltUp" fixed-width class="icon"/>{{ $t('latestRequestSentAt') }}</div> + <div class="data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> + </div> + <div class="cell"> + <div class="label"><fa :icon="faTrafficLight" fixed-width class="icon"/>{{ $t('latestStatus') }}</div> + <div class="data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div> + </div> + </div> + <div class="row"> + <div class="cell"> + <div class="label"><fa :icon="faLongArrowAltDown" fixed-width class="icon"/>{{ $t('latestRequestReceivedAt') }}</div> + <div class="data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> + </div> + </div> + </div> + <div class="chart"> + <div class="header"> + <span class="label">{{ $t('charts') }}</span> + <div class="selects"> + <mk-select v-model="chartSrc" style="margin: 0; flex: 1;"> + <option value="requests">{{ $t('_instanceCharts.requests') }}</option> + <option value="users">{{ $t('_instanceCharts.users') }}</option> + <option value="users-total">{{ $t('_instanceCharts.usersTotal') }}</option> + <option value="notes">{{ $t('_instanceCharts.notes') }}</option> + <option value="notes-total">{{ $t('_instanceCharts.notesTotal') }}</option> + <option value="ff">{{ $t('_instanceCharts.ff') }}</option> + <option value="ff-total">{{ $t('_instanceCharts.ffTotal') }}</option> + <option value="drive-usage">{{ $t('_instanceCharts.cacheSize') }}</option> + <option value="drive-usage-total">{{ $t('_instanceCharts.cacheSizeTotal') }}</option> + <option value="drive-files">{{ $t('_instanceCharts.files') }}</option> + <option value="drive-files-total">{{ $t('_instanceCharts.filesTotal') }}</option> + </mk-select> + <mk-select v-model="chartSpan" style="margin: 0;"> + <option value="hour">{{ $t('perHour') }}</option> + <option value="day">{{ $t('perDay') }}</option> + </mk-select> + </div> + </div> + <div class="chart"> + <canvas ref="chart"></canvas> + </div> + </div> + <div class="operations"> + <span class="label">{{ $t('operations') }}</span> + <mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch> + <mk-switch v-model="isBlocked" class="switch">{{ $t('blockThisInstance') }}</mk-switch> + </div> + <details class="metadata"> + <summary class="label">{{ $t('metadata') }}</summary> + <pre><code>{{ JSON.stringify(instance.metadata, null, 2) }}</code></pre> + </details> + </div> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Chart from 'chart.js'; +import i18n from '../../i18n'; +import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown } from '@fortawesome/free-solid-svg-icons'; +import XWindow from '../../components/window.vue'; +import MkUsersDialog from '../../components/users-dialog.vue'; +import MkSelect from '../../components/ui/select.vue'; +import MkSwitch from '../../components/ui/switch.vue'; + +const chartLimit = 90; +const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); +const negate = arr => arr.map(x => -x); +const alpha = hex => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, 0.1)`; +}; + +export default Vue.extend({ + i18n, + + components: { + XWindow, + MkSelect, + MkSwitch, + }, + + props: { + instance: { + type: Object, + required: true + } + }, + + data() { + return { + meta: null, + isSuspended: false, + isBlocked: false, + now: null, + chart: null, + chartInstance: null, + chartSrc: 'requests', + chartSpan: 'hour', + faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown + }; + }, + + computed: { + data(): any { + if (this.chart == null) return null; + switch (this.chartSrc) { + case 'requests': return this.requestsChart(); + case 'users': return this.usersChart(false); + case 'users-total': return this.usersChart(true); + case 'notes': return this.notesChart(false); + case 'notes-total': return this.notesChart(true); + case 'ff': return this.ffChart(false); + case 'ff-total': return this.ffChart(true); + case 'drive-usage': return this.driveUsageChart(false); + case 'drive-usage-total': return this.driveUsageChart(true); + case 'drive-files': return this.driveFilesChart(false); + case 'drive-files-total': return this.driveFilesChart(true); + } + }, + + stats(): any[] { + const stats = + this.chartSpan == 'day' ? this.chart.perDay : + this.chartSpan == 'hour' ? this.chart.perHour : + null; + + return stats; + } + }, + + watch: { + isSuspended() { + this.$root.api('admin/federation/update-instance', { + host: this.instance.host, + isSuspended: this.isSuspended + }); + }, + + isBlocked() { + this.$root.api('admin/update-meta', { + blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host) + }); + }, + + chartSrc() { + this.renderChart(); + }, + + chartSpan() { + this.renderChart(); + } + }, + + async created() { + this.$root.getMeta().then(meta => { + this.meta = meta; + this.isSuspended = this.instance.isSuspended; + this.isBlocked = this.meta.blockedHosts.includes(this.instance.host); + }); + + this.now = new Date(); + + const [perHour, perDay] = await Promise.all([ + this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), + this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), + ]); + + const chart = { + perHour: perHour, + perDay: perDay + }; + + this.chart = chart; + + this.renderChart(); + }, + + methods: { + setSrc(src) { + this.chartSrc = src; + }, + + renderChart() { + if (this.chartInstance) { + this.chartInstance.destroy(); + } + + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + this.chartInstance = new Chart(this.$refs.chart, { + type: 'line', + data: { + labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), + datasets: this.data.series.map(x => ({ + label: x.name, + data: x.data.slice().reverse(), + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: x.color, + backgroundColor: alpha(x.color), + })) + }, + options: { + aspectRatio: 2.5, + layout: { + padding: { + left: 16, + right: 16, + top: 16, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + }, + + getDate(ago: number) { + const y = this.now.getFullYear(); + const m = this.now.getMonth(); + const d = this.now.getDate(); + const h = this.now.getHours(); + + return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); + }, + + format(arr) { + return arr; + }, + + requestsChart(): any { + return { + series: [{ + name: 'In', + color: '#008FFB', + data: this.format(this.stats.requests.received) + }, { + name: 'Out (succ)', + color: '#00E396', + data: this.format(this.stats.requests.succeeded) + }, { + name: 'Out (fail)', + color: '#FEB019', + data: this.format(this.stats.requests.failed) + }] + }; + }, + + usersChart(total: boolean): any { + return { + series: [{ + name: 'Users', + color: '#008FFB', + data: this.format(total + ? this.stats.users.total + : sum(this.stats.users.inc, negate(this.stats.users.dec)) + ) + }] + }; + }, + + notesChart(total: boolean): any { + return { + series: [{ + name: 'Notes', + color: '#008FFB', + data: this.format(total + ? this.stats.notes.total + : sum(this.stats.notes.inc, negate(this.stats.notes.dec)) + ) + }] + }; + }, + + ffChart(total: boolean): any { + return { + series: [{ + name: 'Following', + color: '#008FFB', + data: this.format(total + ? this.stats.following.total + : sum(this.stats.following.inc, negate(this.stats.following.dec)) + ) + }, { + name: 'Followers', + color: '#00E396', + data: this.format(total + ? this.stats.followers.total + : sum(this.stats.followers.inc, negate(this.stats.followers.dec)) + ) + }] + }; + }, + + driveUsageChart(total: boolean): any { + return { + bytes: true, + series: [{ + name: 'Drive usage', + color: '#008FFB', + data: this.format(total + ? this.stats.drive.totalUsage + : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) + ) + }] + }; + }, + + driveFilesChart(total: boolean): any { + return { + series: [{ + name: 'Drive files', + color: '#008FFB', + data: this.format(total + ? this.stats.drive.totalFiles + : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) + ) + }] + }; + }, + + showFollowing() { + this.$root.new(MkUsersDialog, { + title: this.$t('instanceFollowing'), + pagination: { + endpoint: 'federation/following', + limit: 10, + params: { + host: this.instance.host + } + }, + extract: item => item.follower + }); + }, + + showFollowers() { + this.$root.new(MkUsersDialog, { + title: this.$t('instanceFollowers'), + pagination: { + endpoint: 'federation/followers', + limit: 10, + params: { + host: this.instance.host + } + }, + extract: item => item.followee + }); + }, + + showUsers() { + this.$root.new(MkUsersDialog, { + title: this.$t('instanceUsers'), + pagination: { + endpoint: 'federation/users', + limit: 10, + params: { + host: this.instance.host + } + } + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-instance-info { + overflow: auto; + + > .table { + padding: 0 32px; + + @media (max-width: 500px) { + padding: 0 16px; + } + + > .row { + display: flex; + + &:not(:last-child) { + margin-bottom: 8px; + } + + > .cell { + flex: 1; + + > .label { + font-size: 80%; + opacity: 0.7; + + > .icon { + margin-right: 4px; + display: none; + } + } + + > .data.clickable { + color: var(--accent); + cursor: pointer; + } + } + } + } + + > .data { + margin-top: 16px; + padding-top: 16px; + border-top: solid 1px var(--divider); + + @media (max-width: 500px) { + margin-top: 8px; + padding-top: 8px; + } + } + + > .chart { + margin-top: 16px; + padding-top: 16px; + border-top: solid 1px var(--divider); + + @media (max-width: 500px) { + margin-top: 8px; + padding-top: 8px; + } + + > .header { + padding: 0 32px; + + @media (max-width: 500px) { + padding: 0 16px; + } + + > .label { + font-size: 80%; + opacity: 0.7; + } + + > .selects { + display: flex; + } + } + + > .chart { + padding: 0 16px; + + @media (max-width: 500px) { + padding: 0; + } + } + } + + > .operations { + padding: 16px 32px 16px 32px; + margin-top: 8px; + border-top: solid 1px var(--divider); + + @media (max-width: 500px) { + padding: 8px 16px 8px 16px; + margin-top: 0; + } + + > .label { + font-size: 80%; + opacity: 0.7; + } + + > .switch { + margin: 16px 0; + } + } + + > .metadata { + padding: 16px 32px 16px 32px; + border-top: solid 1px var(--divider); + + @media (max-width: 500px) { + padding: 8px 16px 8px 16px; + } + + > .label { + font-size: 80%; + opacity: 0.7; + } + + > pre > code { + display: block; + max-height: 200px; + overflow: auto; + } + } +} +</style> diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue new file mode 100644 index 0000000000000000000000000000000000000000..224ff72a9f7cecdef027da39fc556e1463e534e3 --- /dev/null +++ b/src/client/pages/instance/federation.vue @@ -0,0 +1,165 @@ +<template> +<div class="mk-federation"> + <section class="_section instances"> + <div class="_title"><fa :icon="faGlobe"/> {{ $t('instances') }}</div> + <div class="_content"> + <div class="inputs" style="display: flex;"> + <mk-input v-model="host" :debounce="true" style="margin: 0; flex: 1;"><span>{{ $t('host') }}</span></mk-input> + <mk-select v-model="state" style="margin: 0;"> + <option value="all">{{ $t('all') }}</option> + <option value="federating">{{ $t('federating') }}</option> + <option value="subscribing">{{ $t('subscribing') }}</option> + <option value="publishing">{{ $t('publishing') }}</option> + <option value="suspended">{{ $t('suspended') }}</option> + <option value="blocked">{{ $t('blocked') }}</option> + <option value="notResponding">{{ $t('notResponding') }}</option> + </mk-select> + </div> + </div> + <div class="_content"> + <mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state"> + <div class="instance" v-for="(instance, i) in items" :key="instance.id" :data-index="i" @click="info(instance)"> + <div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div> + <div class="status"> + <span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span> + <span class="sub" v-else><fa :icon="faCaretDown" class="icon"/>-</span> + <span class="pub" v-if="instance.followingCount > 0"><fa :icon="faCaretUp" class="icon"/>Pub</span> + <span class="pub" v-else><fa :icon="faCaretUp" class="icon"/>-</span> + <span class="lastCommunicatedAt"><fa :icon="faExchangeAlt" class="icon"/><mk-time :time="instance.lastCommunicatedAt"/></span> + <span class="latestStatus"><fa :icon="faTrafficLight" class="icon"/>{{ instance.latestStatus || '-' }}</span> + </div> + </div> + </mk-pagination> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../i18n'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkSelect from '../../components/ui/select.vue'; +import MkPagination from '../../components/ui/pagination.vue'; +import MkInstanceInfo from './federation.instance.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('federation') as string + }; + }, + + components: { + MkButton, + MkInput, + MkSelect, + MkPagination, + }, + + data() { + return { + host: '', + state: 'federating', + sort: '+pubSub', + pagination: { + endpoint: 'federation/instances', + limit: 10, + offsetMode: true, + params: () => ({ + sort: this.sort, + host: this.host != '' ? this.host : null, + ...( + this.state === 'federating' ? { federating: true } : + this.state === 'subscribing' ? { subscribing: true } : + this.state === 'publishing' ? { publishing: true } : + this.state === 'suspended' ? { suspended: true } : + this.state === 'blocked' ? { blocked: true } : + this.state === 'notResponding' ? { notResponding: true } : + {}) + }) + }, + faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight + } + }, + + watch: { + host() { + this.$refs.instances.reload(); + }, + state() { + this.$refs.instances.reload(); + } + }, + + methods: { + getStatus(instance) { + if (instance.isSuspended) return 'off'; + if (instance.isNotResponding) return 'red'; + return 'green'; + }, + + info(instance) { + this.$root.new(MkInstanceInfo, { + instance: instance + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-federation { + > .instances { + > ._content { + > .instances { + > .instance { + cursor: pointer; + + > .host { + > .indicator { + font-size: 70%; + vertical-align: baseline; + margin-right: 4px; + + &.green { + color: #49c5ba; + } + + &.yellow { + color: #c5a549; + } + + &.red { + color: #c54949; + } + + &.off { + color: rgba(0, 0, 0, 0.5); + } + } + } + + > .status { + display: flex; + align-items: center; + font-size: 90%; + + > span { + flex: 1; + + > .icon { + margin-right: 6px; + } + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue new file mode 100644 index 0000000000000000000000000000000000000000..e7475e94c18207e1c4496e3a20f44637d8d0497f --- /dev/null +++ b/src/client/pages/instance/files.vue @@ -0,0 +1,54 @@ +<template> +<section class="_section"> + <div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div> + <div class="_content"> + <mk-button primary @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearCachedFiles') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCloud } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import MkButton from '../../components/ui/button.vue'; +import MkPagination from '../../components/ui/pagination.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: `${this.$t('files')} | ${this.$t('instance')}` + }; + }, + + components: { + MkButton, + MkPagination, + }, + + data() { + return { + faTrashAlt, faCloud + } + }, + + methods: { + clear() { + this.$root.dialog({ + type: 'warning', + text: this.$t('clearCachedFilesConfirm'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + + this.$root.api('admin/drive/clean-remote-files', {}).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + }); + } + } +}); +</script> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..5301fc7e01f6762b8242cecefff89e22cc72f911 --- /dev/null +++ b/src/client/pages/instance/index.vue @@ -0,0 +1,393 @@ +<template> +<div v-if="meta" class="mk-instance-page"> + <portal to="icon"><fa :icon="faServer"/></portal> + <portal to="title">{{ $t('instance') }}</portal> + + <section class="_section info"> + <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div> + <div class="_content"> + <mk-input v-model="name" style="margin-top: 8px;">{{ $t('instanceName') }}</mk-input> + <mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea> + <mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input> + <mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input> + <mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input> + <mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input> + <mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section info"> + <div class="_content"> + <mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch> + <mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch> + <mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info> + </div> + </section> + + <section class="_section info"> + <div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div> + <div class="_content"> + <mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch> + <mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div> + <div class="_content"> + <mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch> + <template v-if="enableRecaptcha"> + <mk-info>{{ $t('recaptcha-info') }}</mk-info> + <mk-info warn>{{ $t('recaptcha-info2') }}</mk-info> + <mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input> + <mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input> + </template> + </div> + <div class="_content" v-if="enableRecaptcha && recaptchaSiteKey"> + <header>{{ $t('preview') }}</header> + <div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div> + <div class="_content"> + <mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></mk-switch> + <template v-if="enableServiceWorker"> + <mk-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></mk-info> + <mk-horizon-group inputs class="fit-bottom"> + <mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>{{ $t('vapid-publickey') }}</mk-input> + <mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>{{ $t('vapid-privatekey') }}</mk-input> + </mk-horizon-group> + </template> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div> + <div class="_content"> + <mk-textarea v-model="pinnedUsers" style="margin-top: 0;"> + <template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template> + </mk-textarea> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div> + <div class="_content"> + <mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch> + <mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch> + <mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input> + <mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div> + <div class="_content"> + <mk-input v-model="proxyAccount" style="margin: 0;"><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div> + <div class="_content"> + <mk-textarea v-model="blockedHosts" style="margin-top: 0;"> + <template #desc>{{ $t('blockedInstancesDescription') }}</template> + </mk-textarea> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section"> + <div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div> + <div class="_content"> + <header><fa :icon="faTwitter"/> {{ $t('twitter-integration-config') }}</header> + <mk-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</mk-switch> + <template v-if="enableTwitterIntegration"> + <mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-key') }}</mk-input> + <mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-secret') }}</mk-input> + <mk-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</mk-info> + </template> + </div> + <div class="_content"> + <header><fa :icon="faGithub"/> {{ $t('github-integration-config') }}</header> + <mk-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</mk-switch> + <template v-if="enableGithubIntegration"> + <mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-id') }}</mk-input> + <mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-secret') }}</mk-input> + <mk-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</mk-info> + </template> + </div> + <div class="_content"> + <header><fa :icon="faDiscord"/> {{ $t('discord-integration-config') }}</header> + <mk-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</mk-switch> + <template v-if="enableDiscordIntegration"> + <mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-id') }}</mk-input> + <mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-secret') }}</mk-input> + <mk-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</mk-info> + </template> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + + <section class="_section info"> + <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div> + <div class="_content table" v-if="stats"> + <div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div> + <div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div> + </div> + <div class="_content table"> + <div><b>Misskey</b><span>v{{ version }}</span></div> + </div> + <div class="_content table" v-if="serverInfo"> + <div><b>Node.js</b><span>{{ serverInfo.node }}</span></div> + <div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div> + <div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons'; +import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkTextarea from '../../components/ui/textarea.vue'; +import MkSwitch from '../../components/ui/switch.vue'; +import MkInfo from '../../components/ui/info.vue'; +import MkUserSelect from '../../components/user-select.vue'; +import { version } from '../../config'; +import i18n from '../../i18n'; +import getAcct from '../../../misc/acct/render'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('instance') as string + }; + }, + + components: { + MkButton, + MkInput, + MkTextarea, + MkSwitch, + MkInfo, + }, + + data() { + return { + version, + meta: null, + stats: null, + serverInfo: null, + proxyAccount: null, + cacheRemoteFiles: false, + proxyRemoteFiles: false, + localDriveCapacityMb: 0, + remoteDriveCapacityMb: 0, + blockedHosts: '', + pinnedUsers: '', + maintainerName: null, + maintainerEmail: null, + name: null, + description: null, + tosUrl: null, + bannerUrl: null, + iconUrl: null, + enableRegistration: false, + enableLocalTimeline: false, + enableGlobalTimeline: false, + enableRecaptcha: false, + recaptchaSiteKey: null, + recaptchaSecretKey: null, + enableServiceWorker: false, + swPublicKey: null, + swPrivateKey: null, + enableTwitterIntegration: false, + twitterConsumerKey: null, + twitterConsumerSecret: null, + enableGithubIntegration: false, + githubClientId: null, + githubClientSecret: null, + enableDiscordIntegration: false, + discordClientId: null, + discordClientSecret: null, + faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt + } + }, + + created() { + this.$root.getMeta().then(meta => { + this.meta = meta; + this.name = this.meta.name; + this.description = this.meta.description; + this.tosUrl = this.meta.tosUrl; + this.bannerUrl = this.meta.bannerUrl; + this.iconUrl = this.meta.iconUrl; + this.maintainerName = this.meta.maintainerName; + this.maintainerEmail = this.meta.maintainerEmail; + this.enableRegistration = !this.meta.disableRegistration; + this.enableLocalTimeline = !this.meta.disableLocalTimeline; + this.enableGlobalTimeline = !this.meta.disableGlobalTimeline; + this.enableRecaptcha = this.meta.enableRecaptcha; + this.recaptchaSiteKey = this.meta.recaptchaSiteKey; + this.recaptchaSecretKey = this.meta.recaptchaSecretKey; + this.proxyAccount = this.meta.proxyAccount; + this.cacheRemoteFiles = this.meta.cacheRemoteFiles; + this.proxyRemoteFiles = this.meta.proxyRemoteFiles; + this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb; + this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb; + this.blockedHosts = this.meta.blockedHosts.join('\n'); + this.pinnedUsers = this.meta.pinnedUsers.join('\n'); + this.enableServiceWorker = this.meta.enableServiceWorker; + this.swPublicKey = this.meta.swPublickey; + this.swPrivateKey = this.meta.swPrivateKey; + this.enableTwitterIntegration = this.meta.enableTwitterIntegration; + this.twitterConsumerKey = this.meta.twitterConsumerKey; + this.twitterConsumerSecret = this.meta.twitterConsumerSecret; + this.enableGithubIntegration = this.meta.enableGithubIntegration; + this.githubClientId = this.meta.githubClientId; + this.githubClientSecret = this.meta.githubClientSecret; + this.enableDiscordIntegration = this.meta.enableDiscordIntegration; + this.discordClientId = this.meta.discordClientId; + this.discordClientSecret = this.meta.discordClientSecret; + }); + + this.$root.api('admin/server-info').then(res => { + this.serverInfo = res; + }); + + this.$root.api('stats').then(res => { + this.stats = res; + }); + }, + + mounted() { + const renderRecaptchaPreview = () => { + if (!(window as any).grecaptcha) return; + if (!this.$refs.recaptcha) return; + if (!this.recaptchaSiteKey) return; + (window as any).grecaptcha.render(this.$refs.recaptcha, { + sitekey: this.recaptchaSiteKey + }); + }; + window.onRecaotchaLoad = () => { + renderRecaptchaPreview(); + }; + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad'); + head.appendChild(script); + this.$watch('enableRecaptcha', () => { + renderRecaptchaPreview(); + }); + this.$watch('recaptchaSiteKey', () => { + renderRecaptchaPreview(); + }); + }, + + methods: { + addPinUser() { + this.$root.new(MkUserSelect, {}).$once('selected', user => { + this.pinnedUsers = this.pinnedUsers.trim(); + this.pinnedUsers += '\n@' + getAcct(user); + this.pinnedUsers = this.pinnedUsers.trim(); + }); + }, + + save(withDialog = false) { + this.$root.api('admin/update-meta', { + name: this.name, + description: this.description, + tosUrl: this.tosUrl, + bannerUrl: this.bannerUrl, + iconUrl: this.iconUrl, + maintainerName: this.maintainerName, + maintainerEmail: this.maintainerEmail, + disableRegistration: !this.enableRegistration, + disableLocalTimeline: !this.enableLocalTimeline, + disableGlobalTimeline: !this.enableGlobalTimeline, + enableRecaptcha: this.enableRecaptcha, + recaptchaSiteKey: this.recaptchaSiteKey, + recaptchaSecretKey: this.recaptchaSecretKey, + proxyAccount: this.proxyAccount, + cacheRemoteFiles: this.cacheRemoteFiles, + proxyRemoteFiles: this.proxyRemoteFiles, + localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), + remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), + blockedHosts: this.blockedHosts.split('\n') || [], + pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [], + enableServiceWorker: this.enableServiceWorker, + swPublicKey: this.swPublicKey, + swPrivateKey: this.swPrivateKey, + enableTwitterIntegration: this.enableTwitterIntegration, + twitterConsumerKey: this.twitterConsumerKey, + twitterConsumerSecret: this.twitterConsumerSecret, + enableGithubIntegration: this.enableGithubIntegration, + githubClientId: this.githubClientId, + githubClientSecret: this.githubClientSecret, + enableDiscordIntegration: this.enableDiscordIntegration, + discordClientId: this.discordClientId, + discordClientSecret: this.discordClientSecret, + }).then(() => { + if (withDialog) { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + } + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-instance-page { + > .info { + > .table { + > div { + display: flex; + + > * { + flex: 1; + } + } + } + } +} +</style> diff --git a/src/client/pages/instance/monitor.vue b/src/client/pages/instance/monitor.vue new file mode 100644 index 0000000000000000000000000000000000000000..3f3ce6d73a29ef40bab66c8f6b87f5b558478560 --- /dev/null +++ b/src/client/pages/instance/monitor.vue @@ -0,0 +1,381 @@ +<template> +<div class="mk-instance-monitor"> + <section class="_section"> + <div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div> + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <canvas ref="cpumem"></canvas> + </div> + <div class="_content" v-if="serverInfo"> + <div class="table"> + <div class="row"> + <div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div> + </div> + <div class="row"> + <div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div> + <div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div> + <div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div> + </div> + </div> + </div> + </section> + <section class="_section"> + <div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div> + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <canvas ref="disk"></canvas> + </div> + <div class="_content" v-if="serverInfo"> + <div class="table"> + <div class="row"> + <div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div> + <div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div> + <div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div> + </div> + </div> + </div> + </section> + <section class="_section"> + <div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div> + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <canvas ref="net"></canvas> + </div> + <div class="_content" v-if="serverInfo"> + <div class="table"> + <div class="row"> + <div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div> + </div> + </div> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTachometerAlt, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons'; +import Chart from 'chart.js'; +import i18n from '../../i18n'; + +const alpha = (hex, a) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: `${this.$t('monitor')} | ${this.$t('instance')}` + }; + }, + + components: { + }, + + data() { + return { + connection: null, + serverInfo: null, + memUsage: 0, + chartCpuMem: null, + chartNet: null, + faTachometerAlt, faExchangeAlt, faMicrochip, faHdd + } + }, + + mounted() { + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + + this.chartCpuMem = new Chart(this.$refs.cpumem, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'CPU', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#86b300', + backgroundColor: alpha('#86b300', 0.1), + data: [] + }, { + label: 'MEM (active)', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#935dbf', + backgroundColor: alpha('#935dbf', 0.02), + data: [] + }, { + label: 'MEM (used)', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#935dbf', + borderDash: [5, 5], + fill: false, + data: [] + }] + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 0, + top: 8, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false, + max: 100 + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + + this.chartNet = new Chart(this.$refs.net, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'In', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#94a029', + backgroundColor: alpha('#94a029', 0.1), + data: [] + }, { + label: 'Out', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#ff9156', + backgroundColor: alpha('#ff9156', 0.1), + data: [] + }] + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 0, + top: 8, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false, + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + + this.chartDisk = new Chart(this.$refs.disk, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Read', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#94a029', + backgroundColor: alpha('#94a029', 0.1), + data: [] + }, { + label: 'Write', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#ff9156', + backgroundColor: alpha('#ff9156', 0.1), + data: [] + }] + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 0, + top: 8, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false, + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + + this.$root.api('admin/server-info', {}).then(res => { + this.serverInfo = res; + + this.connection = this.$root.stream.useSharedConnection('serverStats'); + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + this.connection.send('requestLog', { + id: Math.random().toString().substr(2, 8), + length: 150 + }); + }); + }, + + beforeDestroy() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + this.connection.dispose(); + }, + + methods: { + onStats(stats) { + const cpu = (stats.cpu * 100).toFixed(0); + const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0); + const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0); + this.memUsage = stats.mem.active; + + this.chartCpuMem.data.labels.push(''); + this.chartCpuMem.data.datasets[0].data.push(cpu); + this.chartCpuMem.data.datasets[1].data.push(memActive); + this.chartCpuMem.data.datasets[2].data.push(memUsed); + this.chartNet.data.labels.push(''); + this.chartNet.data.datasets[0].data.push(stats.net.rx); + this.chartNet.data.datasets[1].data.push(stats.net.tx); + this.chartDisk.data.labels.push(''); + this.chartDisk.data.datasets[0].data.push(stats.fs.r); + this.chartDisk.data.datasets[1].data.push(stats.fs.w); + if (this.chartCpuMem.data.datasets[0].data.length > 150) { + this.chartCpuMem.data.labels.shift(); + this.chartCpuMem.data.datasets[0].data.shift(); + this.chartCpuMem.data.datasets[1].data.shift(); + this.chartCpuMem.data.datasets[2].data.shift(); + this.chartNet.data.labels.shift(); + this.chartNet.data.datasets[0].data.shift(); + this.chartNet.data.datasets[1].data.shift(); + this.chartDisk.data.labels.shift(); + this.chartDisk.data.datasets[0].data.shift(); + this.chartDisk.data.datasets[1].data.shift(); + } + this.chartCpuMem.update(); + this.chartNet.update(); + this.chartDisk.update(); + }, + + onStatsLog(statsLog) { + for (const stats of statsLog.reverse()) { + this.onStats(stats); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-instance-monitor { + > section { + > ._content { + > .table { + > .row { + display: flex; + + &:not(:last-child) { + margin-bottom: 16px; + + @media (max-width: 500px) { + margin-bottom: 8px; + } + } + + > .cell { + flex: 1; + + > .label { + font-size: 80%; + opacity: 0.7; + + > .icon { + margin-right: 4px; + display: none; + } + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.queue.vue new file mode 100644 index 0000000000000000000000000000000000000000..cc542b176f06e9a9cdc1dcc4933334b3b31b4193 --- /dev/null +++ b/src/client/pages/instance/queue.queue.vue @@ -0,0 +1,204 @@ +<template> +<section class="_section mk-queue-queue"> + <div class="_title"><slot name="title"></slot></div> + <div class="_content status"> + <div class="cell"><div class="label">Process</div>{{ activeSincePrevTick | number }}</div> + <div class="cell"><div class="label">Active</div>{{ active | number }}</div> + <div class="cell"><div class="label">Waiting</div>{{ waiting | number }}</div> + <div class="cell"><div class="label">Delayed</div>{{ delayed | number }}</div> + </div> + <div class="_content" style="margin-bottom: -8px;"> + <canvas ref="chart"></canvas> + </div> + <div class="_content" style="max-height: 180px; overflow: auto;"> + <sequential-entrance :delay="15" v-if="jobs.length > 0"> + <div v-for="(job, i) in jobs" :key="job[0]" :data-index="i"> + <span>{{ job[0] }}</span> + <span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span> + </div> + </sequential-entrance> + <span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Chart from 'chart.js'; +import i18n from '../../i18n'; + +const alpha = (hex, a) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +export default Vue.extend({ + i18n, + + props: { + domain: { + required: true + }, + connection: { + required: true + }, + }, + + data() { + return { + chart: null, + jobs: [], + activeSincePrevTick: 0, + active: 0, + waiting: 0, + delayed: 0, + } + }, + + mounted() { + this.fetchJobs(); + + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + + this.chart = new Chart(this.$refs.chart, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Process', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#00E396', + backgroundColor: alpha('#00E396', 0.1), + data: [] + }, { + label: 'Active', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#00BCD4', + backgroundColor: alpha('#00BCD4', 0.1), + data: [] + }, { + label: 'Waiting', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#FFB300', + backgroundColor: alpha('#FFB300', 0.1), + data: [] + }, { + label: 'Delayed', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#E53935', + borderDash: [5, 5], + fill: false, + data: [] + }] + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 0, + top: 8, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false, + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + }, + + beforeDestroy() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + }, + + methods: { + onStats(stats) { + this.activeSincePrevTick = stats[this.domain].activeSincePrevTick; + this.active = stats[this.domain].active; + this.waiting = stats[this.domain].waiting; + this.delayed = stats[this.domain].delayed; + this.chart.data.labels.push(''); + this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick); + this.chart.data.datasets[1].data.push(stats[this.domain].active); + this.chart.data.datasets[2].data.push(stats[this.domain].waiting); + this.chart.data.datasets[3].data.push(stats[this.domain].delayed); + if (this.chart.data.datasets[0].data.length > 200) { + this.chart.data.labels.shift(); + this.chart.data.datasets[0].data.shift(); + this.chart.data.datasets[1].data.shift(); + this.chart.data.datasets[2].data.shift(); + this.chart.data.datasets[3].data.shift(); + } + this.chart.update(); + }, + + onStatsLog(statsLog) { + for (const stats of statsLog.reverse()) { + this.onStats(stats); + } + }, + + fetchJobs() { + this.$root.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => { + this.jobs = jobs; + }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-queue-queue { + > .status { + display: flex; + + > .cell { + flex: 1; + + > .label { + font-size: 80%; + opacity: 0.7; + } + } + } +} +</style> diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue new file mode 100644 index 0000000000000000000000000000000000000000..b7e633081f17cad97f2c499c4206a0c19c08c867 --- /dev/null +++ b/src/client/pages/instance/queue.vue @@ -0,0 +1,79 @@ +<template> +<div> + <x-queue :connection="connection" domain="inbox"> + <template #title><fa :icon="faExchangeAlt"/> In</template> + </x-queue> + <x-queue :connection="connection" domain="deliver"> + <template #title><fa :icon="faExchangeAlt"/> Out</template> + </x-queue> + <section class="_section"> + <div class="_content"> + <mk-button @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearQueue') }}</mk-button> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../../i18n'; +import MkButton from '../../components/ui/button.vue'; +import XQueue from './queue.queue.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: `${this.$t('jobQueue')} | ${this.$t('instance')}` + }; + }, + + components: { + MkButton, + XQueue, + }, + + data() { + return { + connection: this.$root.stream.useSharedConnection('queueStats'), + faExchangeAlt, faTrashAlt + } + }, + + mounted() { + this.$nextTick(() => { + this.connection.send('requestLog', { + id: Math.random().toString().substr(2, 8), + length: 200 + }); + }); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + + methods: { + clear() { + this.$root.dialog({ + type: 'warning', + title: this.$t('clearQueueConfirmTitle'), + text: this.$t('clearQueueConfirmText'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + + this.$root.api('admin/queue/clear', {}).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + }); + } + } +}); +</script> diff --git a/src/client/app/admin/views/dashboard.charts.vue b/src/client/pages/instance/stats.vue similarity index 54% rename from src/client/app/admin/views/dashboard.charts.vue rename to src/client/pages/instance/stats.vue index b2ac19efff17dc2081be19230d7c3634641849cc..595ad2cc3cbf65355597206f23e9a95bd9b1cbef 100644 --- a/src/client/app/admin/views/dashboard.charts.vue +++ b/src/client/pages/instance/stats.vue @@ -1,69 +1,89 @@ <template> -<div class="qvgidhudpqhjttdhxubzuyrhyzgslujw"> - <header> - <b><fa :icon="['far', 'chart-bar']"/> {{ $t('title') }}:</b> - <select v-model="src"> - <optgroup :label="$t('federation')"> - <option value="federation-instances">{{ $t('charts.federation-instances') }}</option> - <option value="federation-instances-total">{{ $t('charts.federation-instances-total') }}</option> - </optgroup> - <optgroup :label="$t('users')"> - <option value="users">{{ $t('charts.users') }}</option> - <option value="users-total">{{ $t('charts.users-total') }}</option> - <option value="active-users">{{ $t('charts.active-users') }}</option> - </optgroup> - <optgroup :label="$t('notes')"> - <option value="notes">{{ $t('charts.notes') }}</option> - <option value="local-notes">{{ $t('charts.local-notes') }}</option> - <option value="remote-notes">{{ $t('charts.remote-notes') }}</option> - <option value="notes-total">{{ $t('charts.notes-total') }}</option> - </optgroup> - <optgroup :label="$t('drive')"> - <option value="drive-files">{{ $t('charts.drive-files') }}</option> - <option value="drive-files-total">{{ $t('charts.drive-files-total') }}</option> - <option value="drive">{{ $t('charts.drive') }}</option> - <option value="drive-total">{{ $t('charts.drive-total') }}</option> - </optgroup> - <optgroup :label="$t('network')"> - <option value="network-requests">{{ $t('charts.network-requests') }}</option> - <option value="network-time">{{ $t('charts.network-time') }}</option> - <option value="network-usage">{{ $t('charts.network-usage') }}</option> - </optgroup> - </select> - <div> - <span @click="span = 'day'" :class="{ active: span == 'day' }">{{ $t('per-day') }}</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">{{ $t('per-hour') }}</span> +<div class="mk-instance-stats"> + <section class="_section"> + <div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div> + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <div class="selects" style="display: flex;"> + <mk-select v-model="chartSrc" style="margin: 0; flex: 1;"> + <optgroup :label="$t('federation')"> + <option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option> + <option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option> + </optgroup> + <optgroup :label="$t('users')"> + <option value="users">{{ $t('_charts.usersIncDec') }}</option> + <option value="users-total">{{ $t('_charts.usersTotal') }}</option> + <option value="active-users">{{ $t('_charts.activeUsers') }}</option> + </optgroup> + <optgroup :label="$t('notes')"> + <option value="notes">{{ $t('_charts.notesIncDec') }}</option> + <option value="local-notes">{{ $t('_charts.localNotesIncDec') }}</option> + <option value="remote-notes">{{ $t('_charts.remoteNotesIncDec') }}</option> + <option value="notes-total">{{ $t('_charts.notesTotal') }}</option> + </optgroup> + <optgroup :label="$t('drive')"> + <option value="drive-files">{{ $t('_charts.filesIncDec') }}</option> + <option value="drive-files-total">{{ $t('_charts.filesTotal') }}</option> + <option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option> + <option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option> + </optgroup> + </mk-select> + <mk-select v-model="chartSpan" style="margin: 0;"> + <option value="hour">{{ $t('perHour') }}</option> + <option value="day">{{ $t('perDay') }}</option> + </mk-select> + </div> + <canvas ref="chart"></canvas> </div> - </header> - <div ref="chart"></div> + </section> </div> </template> <script lang="ts"> import Vue from 'vue'; +import { faChartBar } from '@fortawesome/free-solid-svg-icons'; +import Chart from 'chart.js'; import i18n from '../../i18n'; -import * as tinycolor from 'tinycolor2'; -import ApexCharts from 'apexcharts'; - -const limit = 90; +import MkSelect from '../../components/ui/select.vue'; +const chartLimit = 90; const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const negate = arr => arr.map(x => -x); +const alpha = (hex, a) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; export default Vue.extend({ - i18n: i18n('admin/views/charts.vue'), + i18n, + + metaInfo() { + return { + title: `${this.$t('statistics')} | ${this.$t('instance')}` + }; + }, + + components: { + MkSelect + }, + data() { return { + now: null, chart: null, - src: 'notes', - span: 'hour', - chartInstance: null - }; + chartInstance: null, + chartSrc: 'notes', + chartSpan: 'hour', + faChartBar + } }, computed: { data(): any { if (this.chart == null) return null; - switch (this.src) { + switch (this.chartSrc) { case 'federation-instances': return this.federationInstancesChart(false); case 'federation-instances-total': return this.federationInstancesChart(true); case 'users': return this.usersChart(false); @@ -77,16 +97,13 @@ export default Vue.extend({ case 'drive-total': return this.driveTotalChart(); case 'drive-files': return this.driveFilesChart(); case 'drive-files-total': return this.driveFilesTotalChart(); - case 'network-requests': return this.networkRequestsChart(); - case 'network-time': return this.networkTimeChart(); - case 'network-usage': return this.networkUsageChart(); } }, stats(): any[] { const stats = - this.span == 'day' ? this.chart.perDay : - this.span == 'hour' ? this.chart.perHour : + this.chartSpan == 'day' ? this.chart.perDay : + this.chartSpan == 'hour' ? this.chart.perHour : null; return stats; @@ -94,32 +111,30 @@ export default Vue.extend({ }, watch: { - src() { - this.render(); + chartSrc() { + this.renderChart(); }, - span() { - this.render(); + chartSpan() { + this.renderChart(); } }, - async mounted() { + async created() { this.now = new Date(); const [perHour, perDay] = await Promise.all([Promise.all([ - this.$root.api('charts/federation', { limit: limit, span: 'hour' }), - this.$root.api('charts/users', { limit: limit, span: 'hour' }), - this.$root.api('charts/active-users', { limit: limit, span: 'hour' }), - this.$root.api('charts/notes', { limit: limit, span: 'hour' }), - this.$root.api('charts/drive', { limit: limit, span: 'hour' }), - this.$root.api('charts/network', { limit: limit, span: 'hour' }) + this.$root.api('charts/federation', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/users', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/active-users', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/notes', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/drive', { limit: chartLimit, span: 'hour' }), ]), Promise.all([ - this.$root.api('charts/federation', { limit: limit, span: 'day' }), - this.$root.api('charts/users', { limit: limit, span: 'day' }), - this.$root.api('charts/active-users', { limit: limit, span: 'day' }), - this.$root.api('charts/notes', { limit: limit, span: 'day' }), - this.$root.api('charts/drive', { limit: limit, span: 'day' }), - this.$root.api('charts/network', { limit: limit, span: 'day' }) + this.$root.api('charts/federation', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/users', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/active-users', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/notes', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/drive', { limit: chartLimit, span: 'day' }), ])]); const chart = { @@ -129,7 +144,6 @@ export default Vue.extend({ activeUsers: perHour[2], notes: perHour[3], drive: perHour[4], - network: perHour[5] }, perDay: { federation: perDay[0], @@ -137,115 +151,94 @@ export default Vue.extend({ activeUsers: perDay[2], notes: perDay[3], drive: perDay[4], - network: perDay[5] } }; this.chart = chart; - this.render(); - }, - - beforeDestroy() { - this.chartInstance.destroy(); + this.renderChart(); }, methods: { - setSrc(src) { - this.src = src; - }, - - render() { + renderChart() { if (this.chartInstance) { this.chartInstance.destroy(); } - this.chartInstance = new ApexCharts(this.$refs.chart, { - chart: { - type: 'area', - height: 300, - animations: { - dynamicAnimation: { - enabled: false - } - }, - toolbar: { - show: false - }, - zoom: { - enabled: false - } + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + this.chartInstance = new Chart(this.$refs.chart, { + type: 'line', + data: { + labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), + datasets: this.data.series.map(x => ({ + label: x.name, + data: x.data.slice().reverse(), + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: x.color, + backgroundColor: alpha(x.color, 0.1), + hidden: !!x.hidden + })) }, - dataLabels: { - enabled: false - }, - grid: { - clipMarkers: false, - borderColor: 'rgba(0, 0, 0, 0.1)', - xaxis: { - lines: { - show: true, + options: { + aspectRatio: 2.5, + layout: { + padding: { + left: 0, + right: 0, + top: 16, + bottom: 0 } }, - }, - stroke: { - curve: 'straight', - width: 2 - }, - legend: { - labels: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - }, - }, - xaxis: { - type: 'datetime', - labels: { - style: { - colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() + legend: { + position: 'bottom', + labels: { + boxWidth: 16, } }, - axisBorder: { - color: 'rgba(0, 0, 0, 0.1)' + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false + } + }] }, - axisTicks: { - color: 'rgba(0, 0, 0, 0.1)' - }, - }, - yaxis: { - labels: { - formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v), - style: { - color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() - } + tooltips: { + intersect: false, + mode: 'index', } - }, - series: this.data.series + } }); - - this.chartInstance.render(); }, - getDate(i: number) { + getDate(ago: number) { const y = this.now.getFullYear(); const m = this.now.getMonth(); const d = this.now.getDate(); const h = this.now.getHours(); - return ( - this.span == 'day' ? new Date(y, m, d - i) : - this.span == 'hour' ? new Date(y, m, d, h - i) : - null - ); + return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); }, format(arr) { - return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v })); + return arr; }, federationInstancesChart(total: boolean): any { return { series: [{ name: 'Instances', + color: '#008FFB', data: this.format(total ? this.stats.federation.instance.total : sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)) @@ -259,6 +252,7 @@ export default Vue.extend({ series: [{ name: 'All', type: 'line', + color: '#008FFB', data: this.format(type == 'combined' ? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec)) : sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec)) @@ -266,6 +260,7 @@ export default Vue.extend({ }, { name: 'Renotes', type: 'area', + color: '#00E396', data: this.format(type == 'combined' ? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote) : this.stats.notes[type].diffs.renote @@ -273,6 +268,7 @@ export default Vue.extend({ }, { name: 'Replies', type: 'area', + color: '#FEB019', data: this.format(type == 'combined' ? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply) : this.stats.notes[type].diffs.reply @@ -280,6 +276,7 @@ export default Vue.extend({ }, { name: 'Normal', type: 'area', + color: '#FF4560', data: this.format(type == 'combined' ? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal) : this.stats.notes[type].diffs.normal @@ -293,14 +290,19 @@ export default Vue.extend({ series: [{ name: 'Combined', type: 'line', + color: '#008FFB', data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total)) }, { name: 'Local', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.notes.local.total) }, { name: 'Remote', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.notes.remote.total) }] }; @@ -311,6 +313,7 @@ export default Vue.extend({ series: [{ name: 'Combined', type: 'line', + color: '#008FFB', data: this.format(total ? sum(this.stats.users.local.total, this.stats.users.remote.total) : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) @@ -318,6 +321,8 @@ export default Vue.extend({ }, { name: 'Local', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(total ? this.stats.users.local.total : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec)) @@ -325,6 +330,8 @@ export default Vue.extend({ }, { name: 'Remote', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(total ? this.stats.users.remote.total : sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) @@ -338,14 +345,19 @@ export default Vue.extend({ series: [{ name: 'Combined', type: 'line', + color: '#008FFB', data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count)) }, { name: 'Local', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.activeUsers.local.count) }, { name: 'Remote', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.activeUsers.remote.count) }] }; @@ -357,6 +369,7 @@ export default Vue.extend({ series: [{ name: 'All', type: 'line', + color: '#008FFB', data: this.format( sum( this.stats.drive.local.incSize, @@ -368,18 +381,22 @@ export default Vue.extend({ }, { name: 'Local +', type: 'area', + color: '#008FFB', data: this.format(this.stats.drive.local.incSize) }, { name: 'Local -', type: 'area', + color: '#008FFB', data: this.format(negate(this.stats.drive.local.decSize)) }, { name: 'Remote +', type: 'area', + color: '#008FFB', data: this.format(this.stats.drive.remote.incSize) }, { name: 'Remote -', type: 'area', + color: '#008FFB', data: this.format(negate(this.stats.drive.remote.decSize)) }] }; @@ -391,14 +408,19 @@ export default Vue.extend({ series: [{ name: 'Combined', type: 'line', + color: '#008FFB', data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize)) }, { name: 'Local', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.drive.local.totalSize) }, { name: 'Remote', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.drive.remote.totalSize) }] }; @@ -409,6 +431,7 @@ export default Vue.extend({ series: [{ name: 'All', type: 'line', + color: '#008FFB', data: this.format( sum( this.stats.drive.local.incCount, @@ -420,18 +443,22 @@ export default Vue.extend({ }, { name: 'Local +', type: 'area', + color: '#008FFB', data: this.format(this.stats.drive.local.incCount) }, { name: 'Local -', type: 'area', + color: '#008FFB', data: this.format(negate(this.stats.drive.local.decCount)) }, { name: 'Remote +', type: 'area', + color: '#008FFB', data: this.format(this.stats.drive.remote.incCount) }, { name: 'Remote -', type: 'area', + color: '#008FFB', data: this.format(negate(this.stats.drive.remote.decCount)) }] }; @@ -442,86 +469,23 @@ export default Vue.extend({ series: [{ name: 'Combined', type: 'line', + color: '#008FFB', data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount)) }, { name: 'Local', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.drive.local.totalCount) }, { name: 'Remote', type: 'area', + color: '#008FFB', + hidden: true, data: this.format(this.stats.drive.remote.totalCount) }] }; }, - - networkRequestsChart(): any { - return { - series: [{ - name: 'Incoming', - data: this.format(this.stats.network.incomingRequests) - }] - }; - }, - - networkTimeChart(): any { - const data = []; - - for (let i = 0; i < limit; i++) { - data.push(this.stats.network.incomingRequests[i] != 0 ? (this.stats.network.totalTime[i] / this.stats.network.incomingRequests[i]) : 0); - } - - return { - series: [{ - name: 'Avg time', - data: this.format(data) - }] - }; - }, - - networkUsageChart(): any { - return { - bytes: true, - series: [{ - name: 'Incoming', - data: this.format(this.stats.network.incomingBytes) - }, { - name: 'Outgoing', - data: this.format(this.stats.network.outgoingBytes) - }] - }; - }, } }); </script> - -<style lang="stylus" scoped> -.qvgidhudpqhjttdhxubzuyrhyzgslujw - display block - flex 1 - padding 32px 24px - padding-bottom 0 - box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - background var(--face) - border-radius 8px - - > header - display flex - margin 0 8px - padding 0 0 8px 0 - font-size 1em - color var(--adminDashboardCardFg) - border-bottom solid 1px var(--adminDashboardCardDivider) - - > b - margin-right 8px - - > *:last-child - margin-left auto - - * - &:not(.active) - color var(--primary) - cursor pointer - -</style> diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue new file mode 100644 index 0000000000000000000000000000000000000000..da59d8ce241ade3c7815a992cac134e265de38c4 --- /dev/null +++ b/src/client/pages/instance/users.vue @@ -0,0 +1,203 @@ +<template> +<div class="mk-instance-users"> + <portal to="icon"><fa :icon="faUsers"/></portal> + <portal to="title">{{ $t('users') }}</portal> + + <section class="_section lookup"> + <div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div> + <div class="_content"> + <mk-input class="target" v-model="target" type="text" @enter="showUser()" style="margin-top: 0;"> + <span>{{ $t('usernameOrUserId') }}</span> + </mk-input> + <mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button> + </div> + <div class="_footer"> + <mk-button inline primary @click="search()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button> + </div> + </section> + + <section class="_section users"> + <div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div> + <div class="_content _list"> + <mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false"> + <button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" :data-index="i" @click="show(user)"> + <mk-avatar :user="user" class="avatar"/> + <div class="body"> + <mk-user-name :user="user" class="name"/> + <mk-acct :user="user" class="acct"/> + </div> + </button> + </mk-pagination> + </div> + <div class="_footer"> + <mk-button inline primary @click="addUser()"><fa :icon="faPlus"/> {{ $t('addUser') }}</mk-button> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPlus, faUsers, faSearch } from '@fortawesome/free-solid-svg-icons'; +import parseAcct from '../../../misc/acct/parse'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkPagination from '../../components/ui/pagination.vue'; +import MkUserModerateDialog from '../../components/user-moderate-dialog.vue'; +import MkUserSelect from '../../components/user-select.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: `${this.$t('users')} | ${this.$t('instance')}` + }; + }, + + components: { + MkButton, + MkInput, + MkPagination, + }, + + data() { + return { + pagination: { + endpoint: 'admin/show-users', + limit: 10, + offsetMode: true + }, + target: '', + faPlus, faUsers, faSearch + } + }, + + methods: { + /** テã‚ストエリアã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’解決ã™ã‚‹ */ + fetchUser() { + return new Promise((res) => { + const usernamePromise = this.$root.api('users/show', parseAcct(this.target)); + const idPromise = this.$root.api('users/show', { userId: this.target }); + let _notFound = false; + const notFound = () => { + if (_notFound) { + this.$root.dialog({ + type: 'error', + text: this.$t('noSuchUser') + }); + } else { + _notFound = true; + } + }; + usernamePromise.then(res).catch(e => { + if (e.code === 'NO_SUCH_USER') { + notFound(); + } + }); + idPromise.then(res).catch(e => { + notFound(); + }); + }); + }, + + /** テã‚ストエリアã‹ã‚‰å‡¦ç†å¯¾è±¡ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¨å®šã™ã‚‹ */ + async showUser() { + const user = await this.fetchUser(); + this.$root.api('admin/show-user', { userId: user.id }).then(info => { + this.show(user, info); + }); + this.target = ''; + }, + + async addUser() { + const { canceled: canceled1, result: username } = await this.$root.dialog({ + title: this.$t('username'), + input: true + }); + if (canceled1) return; + + const { canceled: canceled2, result: password } = await this.$root.dialog({ + title: this.$t('password'), + input: { type: 'password' } + }); + if (canceled2) return; + + const dialog = this.$root.dialog({ + type: 'waiting', + iconOnly: true + }); + + this.$root.api('admin/accounts/create', { + username: username, + password: password, + }).then(res => { + this.$refs.users.reload(); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e.id + }); + }).finally(() => { + dialog.close(); + }); + }, + + async show(user, info) { + if (info == null) info = await this.$root.api('admin/show-user', { userId: user.id }); + this.$root.new(MkUserModerateDialog, { + user: { ...user, ...info } + }); + }, + + search() { + this.$root.new(MkUserSelect, {}).$once('selected', user => { + this.$root.api('admin/show-user', { userId: user.id }).then(info => { + this.show(user, info); + }); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-instance-users { + > .users { + > ._content { + max-height: 300px; + overflow: auto; + + > .users { + > .user { + display: flex; + width: 100%; + box-sizing: border-box; + text-align: left; + align-items: center; + + > .avatar { + width: 50px; + height: 50px; + } + + > .body { + padding: 8px; + + > .name { + display: block; + font-weight: bold; + } + + > .acct { + opacity: 0.5; + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue new file mode 100644 index 0000000000000000000000000000000000000000..333af917349a4d02069ee7ceda0d711cb81b6c67 --- /dev/null +++ b/src/client/pages/mentions.vue @@ -0,0 +1,46 @@ +<template> +<div> + <portal to="icon"><fa :icon="faAt"/></portal> + <portal to="title">{{ $t('mentions') }}</portal> + <x-notes :pagination="pagination" :detail="true" @before="before()" @after="after()"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAt } from '@fortawesome/free-solid-svg-icons'; +import Progress from '../scripts/loading'; +import XNotes from '../components/notes.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('mentions') as string + }; + }, + + components: { + XNotes + }, + + data() { + return { + pagination: { + endpoint: 'notes/mentions', + limit: 10, + }, + faAt + }; + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue new file mode 100644 index 0000000000000000000000000000000000000000..1165004e97b920a34b4127417d28bb7b39aa096b --- /dev/null +++ b/src/client/pages/messages.vue @@ -0,0 +1,49 @@ +<template> +<div> + <portal to="icon"><fa :icon="faEnvelope"/></portal> + <portal to="title">{{ $t('directNotes') }}</portal> + <x-notes :pagination="pagination" :detail="true" @before="before()" @after="after()"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; +import Progress from '../scripts/loading'; +import XNotes from '../components/notes.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('directNotes') as string + }; + }, + + components: { + XNotes + }, + + data() { + return { + pagination: { + endpoint: 'notes/mentions', + limit: 10, + params: () => ({ + visibility: 'specified' + }) + }, + faEnvelope + }; + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/pages/messaging-room.form.vue similarity index 63% rename from src/client/app/common/views/components/messaging-room.form.vue rename to src/client/pages/messaging-room.form.vue index bd63bab2c1c8074ca51556f559f0b3291d1b573d..4cdd2b1f325ccc2b598652318ed1802c4174f1ca 100644 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ b/src/client/pages/messaging-room.form.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-messaging-form" +<div class="mk-messaging-form _panel" @dragover.stop="onDragover" @drop.stop="onDrop" > @@ -12,15 +12,15 @@ v-autocomplete="{ model: '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="$t('send')"> - <template v-if="!sending"><fa icon="paper-plane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template> + <x-uploader ref="uploader" @uploaded="onUploaded"/> + <button class="send _button" @click="send" :disabled="!canSend || sending" :title="$t('send')"> + <template v-if="!sending"><fa :icon="faPaperPlane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template> </button> - <button class="attach-from-local" @click="chooseFile" :title="$t('attach-from-local')"> - <fa icon="upload"/> + <button class="attach-from-local _button" @click="chooseFile" :title="$t('attach-from-local')"> + <fa :icon="faUpload"/> </button> - <button class="attach-from-drive" @click="chooseFileFromDrive" :title="$t('attach-from-drive')"> - <fa :icon="['far', 'folder-open']"/> + <button class="attach-from-drive _button" @click="chooseFileFromDrive" :title="$t('attach-from-drive')"> + <fa :icon="faCloud"/> </button> <input ref="file" type="file" @change="onChangeFile"/> </div> @@ -28,12 +28,16 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faPaperPlane, faUpload, faCloud } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; import * as autosize from 'autosize'; -import { formatTimeString } from '../../../../../misc/format-time-string'; +import { formatTimeString } from '../../misc/format-time-string'; export default Vue.extend({ - i18n: i18n('common/views/components/messaging-room.form.vue'), + i18n, + components: { + XUploader: () => import('../components/uploader.vue').then(m => m.default), + }, props: { user: { type: Object, @@ -48,7 +52,8 @@ export default Vue.extend({ return { text: null, file: null, - sending: false + sending: false, + faPaperPlane, faUpload, faCloud }; }, computed: { @@ -226,110 +231,127 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.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 var(--inputText) - outline none - border none - border-top solid 1px var(--faceDivider) - 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 var(--primary) - - &:active - color var(--primaryDarken10) - 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 var(--primary) - - &:active - color var(--primaryDarken10) - transition color 0s ease - - input[type=file] - display none +<style lang="scss" scoped> +.mk-messaging-form { + position: relative; + + > textarea { + cursor: auto; + display: block; + width: 100%; + min-width: 100%; + max-width: 100%; + height: 80px; + margin: 0; + padding: 16px 16px 0 16px; + resize: none; + font-size: 1em; + outline: none; + border: none; + border-radius: 0; + box-shadow: none; + background: transparent; + box-sizing: border-box; + color: var(--fg); + } + + > .file { + padding: 8px; + color: #444; + background: #eee; + cursor: pointer; + } + > .send { + position: absolute; + bottom: 0; + right: 0; + margin: 0; + padding: 16px; + font-size: 1em; + color: #aaa; + transition: color 0.1s ease; + + &:hover { + color: var(--accent); + } + + &:active { + color: var(--accentDarken); + 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: 16px; + font-size: 1em; + font-weight: normal; + text-decoration: none; + color: #aaa; + transition: color 0.1s ease; + + &:hover { + color: var(--accent); + } + + &:active { + color: var(--accentDarken); + transition: color 0s ease; + } + } + + input[type=file] { + display: none; + } +} </style> diff --git a/src/client/pages/messaging-room.message.vue b/src/client/pages/messaging-room.message.vue new file mode 100644 index 0000000000000000000000000000000000000000..392eb6acb08a6d41671d5a4f62a5830b45fb012e --- /dev/null +++ b/src/client/pages/messaging-room.message.vue @@ -0,0 +1,336 @@ +<template> +<div class="thvuemwp" :data-is-me="isMe"> + <mk-avatar class="avatar" :user="message.user"/> + <div class="content"> + <div class="balloon _panel" :data-no-text="message.text == null"> + <button class="delete-button" v-if="isMe" :title="$t('@.delete')" @click="del"> + <img src="/assets/desktop/remove.png" alt="Delete"/> + </button> + <div class="content" v-if="!message.isDeleted"> + <mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> + <div class="file" v-if="message.file"> + <a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name"> + <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name" + :style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/> + <p v-else>{{ message.file.name }}</p> + </a> + </div> + </div> + <div class="content" v-else> + <p class="is-deleted">{{ $t('deleted') }}</p> + </div> + </div> + <div></div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <footer> + <template v-if="isGroup"> + <span class="read" v-if="message.reads.length > 0">{{ $t('messageRead') }} {{ message.reads.length }}</span> + </template> + <template v-else> + <span class="read" v-if="isMe && message.isRead">{{ $t('messageRead') }}</span> + </template> + <mk-time :time="message.createdAt"/> + <template v-if="message.is_edited"><fa icon="pencil-alt"/></template> + </footer> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import { parse } from '../../mfm/parse'; +import { unique } from '../../prelude/array'; + +export default Vue.extend({ + i18n, + props: { + message: { + required: true + }, + isGroup: { + required: false + } + }, + computed: { + isMe(): boolean { + return this.message.userId == this.$store.state.i.id; + }, + urls(): string[] { + if (this.message.text) { + const ast = parse(this.message.text); + return unique(ast + .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) + .map(t => t.node.props.url)); + } else { + return null; + } + } + }, + methods: { + del() { + this.$root.api('messaging/messages/delete', { + messageId: this.message.id + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.thvuemwp { + $me-balloon-color: var(--accent); + + position: relative; + background-color: transparent; + display: flex; + + > .avatar { + display: block; + width: 54px; + height: 54px; + transition: all 0.1s ease; + + @media (max-width: 400px) { + width: 48px; + height: 48px; + } + } + + > .content { + min-width: 0; + + > .balloon { + position: relative; + display: inline-flex; + align-items: center; + padding: 0; + min-height: 38px; + border-radius: 16px; + max-width: 100%; + + &: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; + } + } + + > .content { + max-width: 100%; + + > .is-deleted { + display: block; + margin: 0; + padding: 0; + overflow: hidden; + overflow-wrap: break-word; + font-size: 1em; + color: rgba(#000, 0.5); + } + + > .text { + display: block; + margin: 0; + padding: 12px 18px; + overflow: hidden; + overflow-wrap: break-word; + word-break: break-word; + font-size: 1em; + color: rgba(#000, 0.8); + + @media (max-width: 500px) { + padding: 8px 16px; + } + + @media (max-width: 400px) { + font-size: 0.9em; + } + + & + .file { + > a { + border-radius: 0 0 16px 16px; + } + } + } + + > .file { + > a { + display: block; + max-width: 100%; + border-radius: 16px; + overflow: hidden; + text-decoration: none; + + &:hover { + text-decoration: none; + + > p { + background: #ccc; + } + } + + > * { + display: block; + margin: 0; + width: 100%; + max-height: 512px; + object-fit: contain; + } + + > 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: var(--messagingRoomMessageInfo); + + > .read { + margin: 0 8px; + } + + > [data-icon] { + margin-left: 4px; + } + } + } + + &:not([data-is-me]) { + + > .content { + padding-left: 16px; + padding-right: 32px; + + > .balloon { + $color: var(--panel); + background: $color; + + &[data-no-text] { + background: transparent; + } + + &:not([data-no-text]):before { + left: -14px; + border-top: solid 8px transparent; + border-right: solid 8px $color; + border-bottom: solid 8px transparent; + border-left: solid 8px transparent; + } + + > .content { + > .text { + color: var(--fg); + } + } + } + + > footer { + text-align: left; + } + } + } + + &[data-is-me] { + flex-direction: row-reverse; + + > .content { + padding-right: 16px; + padding-left: 32px; + text-align: right; + + > .balloon { + background: $me-balloon-color; + text-align: left; + + &[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(#fff, 0.5); + } + + > .text { + &, * { + color: #fff !important; + } + } + } + } + + > footer { + text-align: right; + + > .read { + user-select: none; + } + } + } + } + + &[data-is-deleted] { + > .balloon { + opacity: 0.5; + } + } +} +</style> diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/pages/messaging-room.vue similarity index 53% rename from src/client/app/common/views/components/messaging-room.vue rename to src/client/pages/messaging-room.vue index d5fa4143a093f50758a37ab699f1be16ad3a92f8..cba84b6de7d2b957788f40e4649f7e4cec46614c 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/pages/messaging-room.vue @@ -3,65 +3,60 @@ @dragover.prevent.stop="onDragover" @drop.prevent.stop="onDrop" > + <template v-if="!fetching && user"> + <portal to="title"><mk-user-name :user="user" :nowrap="false" class="name"/></portal> + <portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> + </template> + <template v-if="!fetching && group"> + <portal to="title">{{ group.name }}</portal> + </template> + <div class="body"> - <p class="init" v-if="init"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}</p> - <p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p> - <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p> - <button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> + <mk-loading v-if="fetching"/> + <p class="empty" v-if="!fetching && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p> + <p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('noMoreHistory') }}</p> + <button class="more _button" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> <template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }} </button> - <template v-for="(message, i) in _messages"> - <x-message :message="message" :key="message.id" :is-group="group != null"/> - <p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"> - <span>{{ _messages[i + 1]._datetext }}</span> - </p> - </template> + <x-list class="messages" :items="messages" v-slot="{ item: message, i }" direction="up"> + <x-message :message="message" :is-group="group != null" :key="message.id" :data-index="messages.length - i"/> + </x-list> </div> <footer> <transition name="fade"> <div class="new-message" v-show="showIndicator"> - <button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button> + <button class="_buttonPrimary" @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button> </div> </transition> - <x-form :user="user" :group="group" ref="form"/> + <x-form v-if="!fetching" :user="user" :group="group" ref="form"/> </footer> </div> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../i18n'; +import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import XList from '../components/date-separated-list.vue'; import XMessage from './messaging-room.message.vue'; import XForm from './messaging-room.form.vue'; -import { url } from '../../../config'; -import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons'; +import { url } from '../config'; +import parseAcct from '../../misc/acct/parse'; export default Vue.extend({ - i18n: i18n('common/views/components/messaging-room.vue'), + i18n, components: { XMessage, - XForm - }, - - props: { - user: { - type: Object, - requird: false, - }, - group: { - type: Object, - requird: false, - }, - isNaked: { - type: Boolean, - requird: false, - }, + XForm, + XList, }, data() { return { - init: true, + fetching: true, + user: null, + group: null, fetchingMoreMessages: false, messages: [], existMoreMessages: false, @@ -73,58 +68,57 @@ export default Vue.extend({ }, 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 = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return message; - }); - }, - form(): any { return this.$refs.form; } }, - mounted() { - this.connection = this.$root.stream.connectToChannel('messaging', { - otherparty: this.user ? this.user.id : undefined, - group: this.group ? this.group.id : undefined, - }); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - this.connection.on('deleted', this.onDeleted); - - if (this.isNaked) { - window.addEventListener('scroll', this.onScroll, { passive: true }); - } else { - this.$el.addEventListener('scroll', this.onScroll, { passive: true }); - } - - document.addEventListener('visibilitychange', this.onVisibilitychange); + watch: { + $route: 'fetch' + }, - this.fetchMessages().then(() => { - this.init = false; - this.scrollToBottom(); - }); + mounted() { + this.fetch(); }, beforeDestroy() { this.connection.dispose(); - if (this.isNaked) { - window.removeEventListener('scroll', this.onScroll); - } else { - this.$el.removeEventListener('scroll', this.onScroll); - } + window.removeEventListener('scroll', this.onScroll); document.removeEventListener('visibilitychange', this.onVisibilitychange); }, methods: { + async fetch() { + this.fetching = true; + if (this.$route.params.user) { + const user = await this.$root.api('users/show', parseAcct(this.$route.params.user)); + this.user = user; + } else { + const group = await this.$root.api('users/groups/show', { groupId: this.$route.params.group }); + this.group = group; + } + + this.connection = this.$root.stream.connectToChannel('messaging', { + otherparty: this.user ? this.user.id : undefined, + group: this.group ? this.group.id : undefined, + }); + + this.connection.on('message', this.onMessage); + this.connection.on('read', this.onRead); + this.connection.on('deleted', this.onDeleted); + + window.addEventListener('scroll', this.onScroll, { passive: true }); + + document.addEventListener('visibilitychange', this.onVisibilitychange); + + this.fetchMessages().then(() => { + this.fetching = false; + this.scrollToBottom(); + }); + }, + onDragover(e) { const isFile = e.dataTransfer.items[0].kind == 'file'; const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; @@ -254,11 +248,7 @@ export default Vue.extend({ }, scrollToBottom() { - if (this.isNaked) { - window.scroll(0, document.body.offsetHeight); - } else { - this.$el.scrollTop = this.$el.scrollHeight; - } + window.scroll(0, document.body.offsetHeight); }, onIndicatorClick() { @@ -298,139 +288,108 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-messaging-room - background var(--messagingRoomBg) - - > .body - width 100% - max-width 600px - margin 0 auto - min-height calc(100% - 103px) - - > .init, - > .empty - width 100% - margin 0 - padding 16px 8px 8px 8px - text-align center - font-size 0.8em - color var(--messagingRoomInfo) - opacity 0.5 - - [data-icon] - margin-right 4px - - > .no-history - display block - margin 0 - padding 16px - text-align center - font-size 0.8em - color var(--messagingRoomInfo) - opacity 0.5 - - [data-icon] - margin-right 4px - - > .more - display block - margin 16px auto - padding 0 12px - line-height 24px - color #fff - background rgba(#000, 0.3) - border-radius 12px - - &:hover - background rgba(#000, 0.4) - - &:active - background rgba(#000, 0.5) - - &.fetching - cursor wait - - > [data-icon] - 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 var(--messagingRoomDateDividerLine) - - > span - display inline-block - margin 0 - padding 0 16px - //font-weight bold - line-height 32px - color var(--messagingRoomDateDividerText) - background var(--messagingRoomBg) - - > footer - position -webkit-sticky - position sticky - z-index 2 - bottom 0 - width 100% - max-width 600px - margin 0 auto - padding 0 - background var(--messagingRoomBg) - background-clip content-box - - > .new-message - position absolute - top -48px - width 100% - padding 8px 0 - text-align center - - > button - display inline-block - margin 0 - padding 0 12px 0 30px - cursor pointer - line-height 32px - font-size 12px - color var(--primaryForeground) - background var(--primary) - border-radius 16px - - &:hover - background var(--primaryLighten10) - - &:active - background var(--primaryDarken10) - - > i - position absolute - top 0 - left 10px - line-height 32px - font-size 16px - -.fade-enter-active, .fade-leave-active - transition opacity 0.1s - -.fade-enter, .fade-leave-to - transition opacity 0.5s - opacity 0 +<style lang="scss" scoped> +.mk-messaging-room { + + > .body { + width: 100%; + + > .empty { + width: 100%; + margin: 0; + padding: 16px 8px 8px 8px; + text-align: center; + font-size: 0.8em; + opacity: 0.5; + + [data-icon] { + margin-right: 4px; + } + } + + > .no-history { + display: block; + margin: 0; + padding: 16px; + text-align: center; + font-size: 0.8em; + color: var(--messagingRoomInfo); + opacity: 0.5; + + [data-icon] { + margin-right: 4px; + } + } + + > .more { + display: block; + margin: 16px auto; + padding: 0 12px; + line-height: 24px; + color: #fff; + background: rgba(#000, 0.3); + border-radius: 12px; + + &:hover { + background: rgba(#000, 0.4); + } + + &:active { + background: rgba(#000, 0.5); + } + + &.fetching { + cursor: wait; + } + + > [data-icon] { + margin-right: 4px; + } + } + + > .messages { + > ::v-deep * { + margin-bottom: 16px; + } + } + } + + > footer { + width: 100%; + + > .new-message { + position: absolute; + top: -48px; + width: 100%; + padding: 8px 0; + text-align: center; + + > button { + display: inline-block; + margin: 0; + padding: 0 12px 0 30px; + line-height: 32px; + font-size: 12px; + border-radius: 16px; + + > i { + position: absolute; + top: 0; + left: 10px; + line-height: 32px; + font-size: 16px; + } + } + } + } +} + +.fade-enter-active, .fade-leave-active { + transition: opacity 0.1s; +} +.fade-enter, .fade-leave-to { + transition: opacity 0.5s; + opacity: 0; +} </style> diff --git a/src/client/pages/messaging.vue b/src/client/pages/messaging.vue new file mode 100644 index 0000000000000000000000000000000000000000..b94e01cad99e7e7d439bb0a054c645f5356f11b6 --- /dev/null +++ b/src/client/pages/messaging.vue @@ -0,0 +1,328 @@ +<template> +<div class="mk-messaging"> + <portal to="icon"><fa :icon="faComments"/></portal> + <portal to="title">{{ $t('messaging') }}</portal> + + <mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button> + + <sequential-entrance class="history" v-if="messages.length > 0" :delay="30"> + <router-link v-for="(message, i) in messages" + class="message _panel" + :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" + :data-is-me="isMe(message)" + :data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead" + :data-index="i" + :key="message.id" + > + <div> + <mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> + <header v-if="message.groupId"> + <span class="name">{{ message.group.name }}</span> + <mk-time :time="message.createdAt"/> + </header> + <header v-else> + <span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> + <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> + <mk-time :time="message.createdAt"/> + </header> + <div class="body"> + <p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> + </div> + </div> + </router-link> + </sequential-entrance> + <p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p> + <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faUser, faUsers, faComments, faPlus } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../i18n'; +import getAcct from '../../misc/acct/render'; +import MkButton from '../components/ui/button.vue'; +import MkUserSelect from '../components/user-select.vue'; + +export default Vue.extend({ + i18n, + + components: { + MkButton + }, + + data() { + return { + fetching: true, + moreFetching: false, + messages: [], + connection: null, + faUser, faUsers, faComments, faPlus + }; + }, + + mounted() { + this.connection = this.$root.stream.useSharedConnection('messagingIndex'); + + this.connection.on('message', this.onMessage); + this.connection.on('read', this.onRead); + + this.$root.api('messaging/history', { group: false }).then(userMessages => { + this.$root.api('messaging/history', { group: true }).then(groupMessages => { + const messages = userMessages.concat(groupMessages); + messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + this.messages = messages; + this.fetching = false; + }); + }); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + + methods: { + getAcct, + + isMe(message) { + return message.userId == this.$store.state.i.id; + }, + + onMessage(message) { + if (message.recipientId) { + 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); + } else if (message.groupId) { + this.messages = this.messages.filter(m => m.groupId !== message.groupId); + this.messages.unshift(message); + } + }, + + onRead(ids) { + for (const id of ids) { + const found = this.messages.find(m => m.id == id); + if (found) { + if (found.recipientId) { + found.isRead = true; + } else if (found.groupId) { + found.reads.push(this.$store.state.i.id); + } + } + } + }, + + start(ev) { + this.$root.menu({ + items: [{ + text: this.$t('withUser'), + action: () => { this.startUser() } + }, { + text: this.$t('withGroup'), + action: () => { this.startGroup() } + }], + noCenter: true, + source: ev.currentTarget || ev.target, + }); + }, + + async startUser() { + this.$root.new(MkUserSelect, {}).$once('selected', user => { + this.$router.push(`/my/messaging/${getAcct(user)}`); + }); + }, + + async startGroup() { + const groups1 = await this.$root.api('users/groups/owned'); + const groups2 = await this.$root.api('users/groups/joined'); + const { canceled, result: group } = await this.$root.dialog({ + type: null, + title: this.$t('select-group'), + select: { + items: groups1.concat(groups2).map(group => ({ + value: group, text: group.name + })) + }, + showCancelButton: true + }); + if (canceled) return; + this.navigateGroup(group); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-messaging { + + > .start { + margin: 0 auto 16px auto; + } + + > .history { + > .message { + display: block; + text-decoration: none; + margin-bottom: 16px; + + @media (max-width: 500px) { + margin-bottom: 8px; + } + + * { + pointer-events: none; + user-select: none; + } + + &:hover { + .avatar { + filter: saturate(200%); + } + } + + &:active { + } + + &[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 { + 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; + font-weight: bold; + transition: all 0.1s ease; + } + + > .username { + margin: 0 8px; + } + + > .mk-time { + margin: 0 0 0 auto; + } + } + + > .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: var(--faceText); + + .me { + opacity: 0.7; + } + } + + > .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: var(--text); + + > [data-icon] { + margin-right: 4px; + } + } + + @media (max-width: 400px) { + > .search { + > .result { + > .users { + > li { + padding: 8px 16px; + } + } + } + } + + > .history { + > .message { + &: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/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue new file mode 100644 index 0000000000000000000000000000000000000000..a4b140db1e26154aa7f1b3f3c7bae1a36ed3f253 --- /dev/null +++ b/src/client/pages/my-antennas/index.antenna.vue @@ -0,0 +1,174 @@ +<template> +<div class="shaynizk _section"> + <div class="_title" v-if="antenna.name">{{ antenna.name }}</div> + <div class="_content body"> + <mk-input v-model="name" style="margin-top: 8px;"> + <span>{{ $t('name') }}</span> + </mk-input> + <mk-select v-model="src"> + <template #label>{{ $t('antennaSource') }}</template> + <option value="all">{{ $t('_antennaSources.all') }}</option> + <option value="home">{{ $t('_antennaSources.homeTimeline') }}</option> + <option value="users">{{ $t('_antennaSources.users') }}</option> + <option value="list">{{ $t('_antennaSources.userList') }}</option> + </mk-select> + <mk-select v-model="userListId" v-if="src === 'list'"> + <template #label>{{ $t('userList') }}</template> + <option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option> + </mk-select> + <mk-textarea v-model="users" v-if="src === 'users'"> + <span>{{ $t('users') }}</span> + <template #desc>{{ $t('antennaUsersDescription') }} <button class="_textButton" @click="addUser">{{ $t('addUser') }}</button></template> + </mk-textarea> + <mk-switch v-model="withReplies">{{ $t('withReplies') }}</mk-switch> + <mk-textarea v-model="keywords"> + <span>{{ $t('antennaKeywords') }}</span> + <template #desc>{{ $t('antennaKeywordsDescription') }}</template> + </mk-textarea> + <mk-switch v-model="caseSensitive">{{ $t('caseSensitive') }}</mk-switch> + <mk-switch v-model="withFile">{{ $t('withFileAntenna') }}</mk-switch> + <mk-switch v-model="notify">{{ $t('notifyAntenna') }}</mk-switch> + </div> + <div class="_footer"> + <mk-button inline @click="saveAntenna()" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + <mk-button inline @click="deleteAntenna()" v-if="antenna.id != null"><fa :icon="faTrash"/> {{ $t('delete') }}</mk-button> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../i18n'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkTextarea from '../../components/ui/textarea.vue'; +import MkSelect from '../../components/ui/select.vue'; +import MkSwitch from '../../components/ui/switch.vue'; +import MkUserSelect from '../../components/user-select.vue'; +import getAcct from '../../../misc/acct/render'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, MkInput, MkTextarea, MkSelect, MkSwitch + }, + + props: { + antenna: { + type: Object, + required: true + } + }, + + data() { + return { + name: '', + src: '', + userListId: null, + users: '', + keywords: '', + caseSensitive: false, + withReplies: false, + withFile: false, + notify: false, + userLists: null, + faSave, faTrash + }; + }, + + watch: { + async src() { + if (this.src === 'list' && this.userLists === null) { + this.userLists = await this.$root.api('users/lists/list'); + } + } + }, + + created() { + this.name = this.antenna.name; + this.src = this.antenna.src; + this.userListId = this.antenna.userListId; + this.users = this.antenna.users.join('\n'); + this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n'); + this.caseSensitive = this.antenna.caseSensitive; + this.withReplies = this.antenna.withReplies; + this.withFile = this.antenna.withFile; + this.notify = this.antenna.notify; + }, + + methods: { + async saveAntenna() { + if (this.antenna.id == null) { + await this.$root.api('antennas/create', { + name: this.name, + src: this.src, + userListId: this.userListId, + withReplies: this.withReplies, + withFile: this.withFile, + notify: this.notify, + caseSensitive: this.caseSensitive, + users: this.users.trim().split('\n').map(x => x.trim()), + keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')) + }); + this.$emit('created'); + } else { + await this.$root.api('antennas/update', { + antennaId: this.antenna.id, + name: this.name, + src: this.src, + userListId: this.userListId, + withReplies: this.withReplies, + withFile: this.withFile, + notify: this.notify, + caseSensitive: this.caseSensitive, + users: this.users.trim().split('\n').map(x => x.trim()), + keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')) + }); + } + + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }, + + async deleteAntenna() { + const { canceled } = await this.$root.dialog({ + type: 'warning', + text: this.$t('removeAreYouSure', { x: this.antenna.name }), + showCancelButton: true + }); + if (canceled) return; + + await this.$root.api('antennas/delete', { + antennaId: this.antenna.id, + }); + + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + this.$emit('deleted'); + }, + + addUser() { + this.$root.new(MkUserSelect, {}).$once('selected', user => { + this.users = this.users.trim(); + this.users += '\n@' + getAcct(user); + this.users = this.users.trim(); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.shaynizk { + > .body { + max-height: 250px; + overflow: auto; + } +} +</style> diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..3a9a11b5418f5568abaacfbcd782af3045ff6588 --- /dev/null +++ b/src/client/pages/my-antennas/index.vue @@ -0,0 +1,80 @@ +<template> +<div class="ieepwinx"> + <portal to="icon"><fa :icon="faSatellite"/></portal> + <portal to="title">{{ $t('manageAntennas') }}</portal> + + <mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createAntenna') }}</mk-button> + + <x-antenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/> + + <mk-pagination :pagination="pagination" #default="{items}" class="antennas" ref="list"> + <x-antenna v-for="(antenna, i) in items" :key="antenna.id" :data-index="i" :antenna="antenna" @created="onAntennaDeleted"/> + </mk-pagination> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSatellite, faPlus } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '../../components/ui/pagination.vue'; +import MkButton from '../../components/ui/button.vue'; +import XAntenna from './index.antenna.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('manageAntennas') as string, + }; + }, + + components: { + MkPagination, + MkButton, + XAntenna, + }, + + data() { + return { + pagination: { + endpoint: 'antennas/list', + limit: 10, + }, + draft: null, + faSatellite, faPlus + }; + }, + + methods: { + create() { + this.draft = { + name: '', + src: 'all', + userListId: null, + users: [], + keywords: [], + withReplies: false, + caseSensitive: false, + withFile: false, + notify: false + }; + }, + + onAntennaCreated() { + this.$refs.list.reload(); + this.draft = null; + }, + + onAntennaDeleted() { + this.$refs.list.reload(); + }, + } +}); +</script> + +<style lang="scss" scoped> +.ieepwinx { + > .add { + margin: 0 auto 16px auto; + } +} +</style> diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..6c4b46e85cd220f6c66e0571e70eb8051fb60554 --- /dev/null +++ b/src/client/pages/my-lists/index.vue @@ -0,0 +1,75 @@ +<template> +<div class="qkcjvfiv"> + <portal to="icon"><fa :icon="faListUl"/></portal> + <portal to="title">{{ $t('manageLists') }}</portal> + + <mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createList') }}</mk-button> + + <mk-pagination :pagination="pagination" #default="{items}" class="lists" ref="list"> + <div class="list _panel" v-for="(list, i) in items" :key="list.id" :data-index="i"> + <router-link :to="`/lists/${ list.id }`">{{ list.name }}</router-link> + </div> + </mk-pagination> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '../../components/ui/pagination.vue'; +import MkButton from '../../components/ui/button.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('manageLists') as string, + }; + }, + + components: { + MkPagination, + MkButton, + }, + + data() { + return { + pagination: { + endpoint: 'users/lists/list', + limit: 10, + }, + faListUl, faPlus + }; + }, + + methods: { + async create() { + const { canceled, result: name } = await this.$root.dialog({ + title: this.$t('enterListName'), + input: true + }); + if (canceled) return; + await this.$root.api('users/lists/create', { name: name }); + this.$refs.list.reload(); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.qkcjvfiv { + > .add { + margin: 0 auto 16px auto; + } + + > .lists { + > .list { + display: flex; + padding: 16px; + } + } +} +</style> diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue new file mode 100644 index 0000000000000000000000000000000000000000..8899b4c44d3b771b4e8c2fc19aa99f811ea46116 --- /dev/null +++ b/src/client/pages/my-lists/list.vue @@ -0,0 +1,163 @@ +<template> +<div class="mk-list-page"> + <transition name="zoom" mode="out-in"> + <div v-if="list" :key="list.id" class="_section list"> + <div class="_title">{{ list.name }}</div> + <div class="_content"> + <div class="users"> + <div class="user" v-for="(user, i) in users" :key="user.id" :data-index="i"> + <mk-avatar :user="user" class="avatar"/> + <div class="body"> + <mk-user-name :user="user" class="name"/> + <mk-acct :user="user" class="acct"/> + </div> + <div class="action"> + <button class="_button" @click="removeUser(user)"><fa :icon="faTimes"/></button> + </div> + </div> + </div> + </div> + <div class="_footer"> + <mk-button inline @click="renameList()">{{ $t('renameList') }}</mk-button> + <mk-button inline @click="deleteList()">{{ $t('deleteList') }}</mk-button> + </div> + </div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../i18n'; +import Progress from '../../scripts/loading'; +import MkButton from '../../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.list ? `${this.list.name} | ${this.$t('manageLists')}` : this.$t('manageLists') + }; + }, + + components: { + MkButton + }, + + data() { + return { + list: null, + users: [], + faTimes + }; + }, + + watch: { + $route: 'fetch' + }, + + created() { + this.fetch(); + }, + + methods: { + fetch() { + Progress.start(); + this.$root.api('users/lists/show', { + listId: this.$route.params.list + }).then(list => { + this.list = list; + this.$root.api('users/show', { + userIds: this.list.userIds + }).then(users => { + this.users = users; + Progress.done(); + }); + }); + }, + + removeUser(user) { + this.$root.api('users/lists/pull', { + listId: this.list.id, + userId: user.id + }).then(() => { + this.users = this.users.filter(x => x.id !== user.id); + }); + }, + + async renameList() { + const { canceled, result: name } = await this.$root.dialog({ + title: this.$t('enterListName'), + input: { + default: this.list.name + } + }); + if (canceled) return; + + await this.$root.api('users/lists/update', { + listId: this.list.id, + name: name + }); + + this.list.name = name; + }, + + async deleteList() { + const { canceled } = await this.$root.dialog({ + type: 'warning', + text: this.$t('deleteListConfirm', { list: this.list.name }), + showCancelButton: true + }); + if (canceled) return; + + await this.$root.api('users/lists/delete', { + listId: this.list.id + }); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + this.$router.push('/my/lists'); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-list-page { + > .list { + > ._content { + max-height: 400px; + overflow: auto; + + > .users { + > .user { + display: flex; + align-items: center; + + > .avatar { + width: 50px; + height: 50px; + } + + > .body { + flex: 1; + padding: 8px; + + > .name { + display: block; + font-weight: bold; + } + + > .acct { + opacity: 0.5; + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue new file mode 100644 index 0000000000000000000000000000000000000000..e7cdf19f81fa96918cb4b7046a47b18bf1f92487 --- /dev/null +++ b/src/client/pages/note.vue @@ -0,0 +1,55 @@ +<template> +<div class="mk-note-page"> + <transition name="zoom" mode="out-in"> + <x-note v-if="note" :note="note" :key="note.id" :detail="true"/> + <div v-else-if="error"> + <mk-error @retry="fetch()"/> + </div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import Progress from '../scripts/loading'; +import XNote from '../components/note.vue'; + +export default Vue.extend({ + i18n, + metaInfo() { + return { + title: this.$t('note') as string + }; + }, + components: { + XNote + }, + data() { + return { + note: null, + error: null, + }; + }, + watch: { + $route: 'fetch' + }, + created() { + this.fetch(); + }, + methods: { + fetch() { + Progress.start(); + this.$root.api('notes/show', { + noteId: this.$route.params.note + }).then(note => { + this.note = note; + }).catch(e => { + this.error = e; + }).finally(() => { + Progress.done(); + }); + } + } +}); +</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue new file mode 100644 index 0000000000000000000000000000000000000000..8e74124b7956b694e643f5a1ad400deb23feb728 --- /dev/null +++ b/src/client/pages/page-editor/els/page-editor.el.button.vue @@ -0,0 +1,83 @@ +<template> +<x-container @remove="() => $emit('remove')" :draggable="true"> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.button') }}</template> + + <section class="xfhsjczc"> + <mk-input v-model="value.text"><span>{{ $t('_pages.blocks._button.text') }}</span></mk-input> + <mk-switch v-model="value.primary"><span>{{ $t('_pages.blocks._button.colored') }}</span></mk-switch> + <mk-select v-model="value.action"> + <template #label>{{ $t('_pages.blocks._button.action') }}</template> + <option value="dialog">{{ $t('_pages.blocks._button._action.dialog') }}</option> + <option value="resetRandom">{{ $t('_pages.blocks._button._action.resetRandom') }}</option> + <option value="pushEvent">{{ $t('_pages.blocks._button._action.pushEvent') }}</option> + </mk-select> + <template v-if="value.action === 'dialog'"> + <mk-input v-model="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></mk-input> + </template> + <template v-else-if="value.action === 'pushEvent'"> + <mk-input v-model="value.event"><span>{{ $t('_pages.blocks._button._action._pushEvent.event') }}</span></mk-input> + <mk-input v-model="value.message"><span>{{ $t('_pages.blocks._button._action._pushEvent.message') }}</span></mk-input> + <mk-select v-model="value.var"> + <template #label>{{ $t('_pages.blocks._button._action._pushEvent.variable') }}</template> + <option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option> + <option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> + <optgroup :label="$t('_pages.script.pageVariables')"> + <option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option> + </optgroup> + <optgroup :label="$t('_pages.script.enviromentVariables')"> + <option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option> + </optgroup> + </mk-select> + </template> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBolt } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +import XContainer from '../page-editor.container.vue'; +import MkSelect from '../../../components/ui/select.vue'; +import MkInput from '../../../components/ui/input.vue'; +import MkSwitch from '../../../components/ui/switch.vue'; + +export default Vue.extend({ + i18n, + + components: { + XContainer, MkSelect, MkInput, MkSwitch + }, + + props: { + value: { + required: true + }, + aiScript: { + required: true, + }, + }, + + data() { + return { + faBolt + }; + }, + + created() { + if (this.value.text == null) Vue.set(this.value, 'text', ''); + if (this.value.action == null) Vue.set(this.value, 'action', 'dialog'); + if (this.value.content == null) Vue.set(this.value, 'content', null); + if (this.value.event == null) Vue.set(this.value, 'event', null); + if (this.value.message == null) Vue.set(this.value, 'message', null); + if (this.value.primary == null) Vue.set(this.value, 'primary', false); + if (this.value.var == null) Vue.set(this.value, 'var', null); + }, +}); +</script> + +<style lang="scss" scoped> +.xfhsjczc { + padding: 0 16px 0 16px; +} +</style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue similarity index 50% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue rename to src/client/pages/page-editor/els/page-editor.el.counter.vue index 4fc2aac8fc2d2f7774077e647bd60d0eb18d7b1e..d9a4ddddee3e18d8beac15ffb84168d11c616071 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue +++ b/src/client/pages/page-editor/els/page-editor.el.counter.vue @@ -1,11 +1,11 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.counter') }}</template> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.counter') }}</template> <section style="padding: 0 16px 0 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._counter.name') }}</span></ui-input> - <ui-input v-model="value.text"><span>{{ $t('blocks._counter.text') }}</span></ui-input> - <ui-input v-model="value.inc" type="number"><span>{{ $t('blocks._counter.inc') }}</span></ui-input> + <mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._counter.name') }}</span></mk-input> + <mk-input v-model="value.text"><span>{{ $t('_pages.blocks._counter.text') }}</span></mk-input> + <mk-input v-model="value.inc" type="number"><span>{{ $t('_pages.blocks._counter.inc') }}</span></mk-input> </section> </x-container> </template> @@ -13,14 +13,15 @@ <script lang="ts"> import Vue from 'vue'; import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; +import MkInput from '../../../components/ui/input.vue'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { - XContainer + XContainer, MkInput }, props: { diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue similarity index 76% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue rename to src/client/pages/page-editor/els/page-editor.el.if.vue index a3743d89d691b110cb17a7ebdb4da9eb9a3668bc..3c545a7ddc36ade88dfc556c0c90a314ed7b7684 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue +++ b/src/client/pages/page-editor/els/page-editor.el.if.vue @@ -1,6 +1,6 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faQuestion"/> {{ $t('blocks.if') }}</template> + <template #header><fa :icon="faQuestion"/> {{ $t('_pages.blocks.if') }}</template> <template #func> <button @click="add()"> <fa :icon="faPlus"/> @@ -8,16 +8,16 @@ </template> <section class="romcojzs"> - <ui-select v-model="value.var"> - <template #label>{{ $t('blocks._if.variable') }}</template> + <mk-select v-model="value.var"> + <template #label>{{ $t('_pages.blocks._if.variable') }}</template> <option v-for="v in aiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> - <optgroup :label="$t('script.pageVariables')"> + <optgroup :label="$t('_pages.script.pageVariables')"> <option v-for="v in aiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option> </optgroup> - <optgroup :label="$t('script.enviromentVariables')"> + <optgroup :label="$t('_pages.script.enviromentVariables')"> <option v-for="v in aiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> </optgroup> - </ui-select> + </mk-select> <x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> </section> @@ -28,14 +28,15 @@ import Vue from 'vue'; import { v4 as uuid } from 'uuid'; import { faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; +import MkSelect from '../../../components/ui/select.vue'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { - XContainer + XContainer, MkSelect }, inject: ['getPageBlockList'], @@ -83,8 +84,8 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.romcojzs - padding 0 16px 16px 16px - +<style lang="scss" scoped> +.romcojzs { + padding: 0 16px 16px 16px; +} </style> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue b/src/client/pages/page-editor/els/page-editor.el.image.vue similarity index 66% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue rename to src/client/pages/page-editor/els/page-editor.el.image.vue index e2e72b04c24ef6b3c915d37a4bdb8364dd91c5f3..e22701e5c0a1485c97295b2e7efc876ecbda1b39 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue +++ b/src/client/pages/page-editor/els/page-editor.el.image.vue @@ -1,6 +1,6 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template> + <template #header><fa :icon="faImage"/> {{ $t('_pages.blocks.image') }}</template> <template #func> <button @click="choose()"> <fa :icon="faFolderOpen"/> @@ -8,7 +8,7 @@ </template> <section class="oyyftmcf"> - <x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/> + <mk-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/> </section> </x-container> </template> @@ -17,15 +17,16 @@ import Vue from 'vue'; import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; -import XFileThumbnail from '../../../components/drive-file-thumbnail.vue'; +import MkFileThumbnail from '../../../components/drive-file-thumbnail.vue'; +import { selectDriveFile } from '../../../scripts/select-drive-file'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { - XContainer, XFileThumbnail + XContainer, MkFileThumbnail }, props: { @@ -59,9 +60,7 @@ export default Vue.extend({ methods: { async choose() { - this.$chooseDriveFile({ - multiple: false - }).then(file => { + selectDriveFile(this.$root, false).then(file => { this.file = file; this.value.fileId = file.id; }); @@ -70,9 +69,10 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.oyyftmcf - > .preview - height 150px - +<style lang="scss" scoped> +.oyyftmcf { + > .preview { + height: 150px; + } +} </style> diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue new file mode 100644 index 0000000000000000000000000000000000000000..76dd254464aaf553dd11595c5194151ee533deed --- /dev/null +++ b/src/client/pages/page-editor/els/page-editor.el.number-input.vue @@ -0,0 +1,43 @@ +<template> +<x-container @remove="() => $emit('remove')" :draggable="true"> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.numberInput') }}</template> + + <section style="padding: 0 16px 0 16px;"> + <mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._numberInput.name') }}</span></mk-input> + <mk-input v-model="value.text"><span>{{ $t('_pages.blocks._numberInput.text') }}</span></mk-input> + <mk-input v-model="value.default" type="number"><span>{{ $t('_pages.blocks._numberInput.default') }}</span></mk-input> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +import XContainer from '../page-editor.container.vue'; +import MkInput from '../../../components/ui/input.vue'; + +export default Vue.extend({ + i18n, + + components: { + XContainer, MkInput + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt, faMagic + }; + }, + + created() { + if (this.value.name == null) Vue.set(this.value, 'name', ''); + }, +}); +</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue similarity index 65% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue rename to src/client/pages/page-editor/els/page-editor.el.post.vue index fc2f5f90320e1abd17b71040c2fa0f1f501c415a..10ec885d0f0c4b3bbc4a6f54065b02753c523eb5 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue +++ b/src/client/pages/page-editor/els/page-editor.el.post.vue @@ -1,9 +1,9 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faPaperPlane"/> {{ $t('blocks.post') }}</template> + <template #header><fa :icon="faPaperPlane"/> {{ $t('_pages.blocks.post') }}</template> <section style="padding: 0 16px 16px 16px;"> - <ui-textarea v-model="value.text">{{ $t('blocks._post.text') }}</ui-textarea> + <mk-textarea v-model="value.text">{{ $t('_pages.blocks._post.text') }}</mk-textarea> </section> </x-container> </template> @@ -11,14 +11,15 @@ <script lang="ts"> import Vue from 'vue'; import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; +import MkTextarea from '../../../components/ui/textarea.vue'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { - XContainer + XContainer, MkTextarea }, props: { diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue similarity index 53% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue rename to src/client/pages/page-editor/els/page-editor.el.radio-button.vue index 3401c46f47e43725369d7a844d0728576f9fc61c..8d404ec0df79c3a9e66f74220259eab225617ad6 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue +++ b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue @@ -1,12 +1,12 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faBolt"/> {{ $t('blocks.radioButton') }}</template> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.radioButton') }}</template> <section style="padding: 0 16px 16px 16px;"> - <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._radioButton.name') }}</span></ui-input> - <ui-input v-model="value.title"><span>{{ $t('blocks._radioButton.title') }}</span></ui-input> - <ui-textarea v-model="values"><span>{{ $t('blocks._radioButton.values') }}</span></ui-textarea> - <ui-input v-model="value.default"><span>{{ $t('blocks._radioButton.default') }}</span></ui-input> + <mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._radioButton.name') }}</span></mk-input> + <mk-input v-model="value.title"><span>{{ $t('_pages.blocks._radioButton.title') }}</span></mk-input> + <mk-textarea v-model="values"><span>{{ $t('_pages.blocks._radioButton.values') }}</span></mk-textarea> + <mk-input v-model="value.default"><span>{{ $t('_pages.blocks._radioButton.default') }}</span></mk-input> </section> </x-container> </template> @@ -14,35 +14,32 @@ <script lang="ts"> import Vue from 'vue'; import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; +import MkTextarea from '../../../components/ui/textarea.vue'; +import MkInput from '../../../components/ui/input.vue'; export default Vue.extend({ - i18n: i18n('pages'), - + i18n, components: { - XContainer + XContainer, MkTextarea, MkInput }, - props: { value: { required: true }, }, - data() { return { values: '', faBolt, faMagic }; }, - watch: { values() { Vue.set(this.value, 'values', this.values.split('\n')); } }, - created() { if (this.value.name == null) Vue.set(this.value, 'name', ''); if (this.value.title == null) Vue.set(this.value, 'title', ''); diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue similarity index 93% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue rename to src/client/pages/page-editor/els/page-editor.el.section.vue index 0f8f8509479e48d7fa97089a198bc92296532b1c..d405ee196566a5fbe857a33488f10b5207b66c18 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue +++ b/src/client/pages/page-editor/els/page-editor.el.section.vue @@ -21,11 +21,11 @@ import Vue from 'vue'; import { v4 as uuid } from 'uuid'; import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { XContainer @@ -95,9 +95,10 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.ilrvjyvi - > .children - padding 16px - +<style lang="scss" scoped> +.ilrvjyvi { + > .children { + padding: 16px; + } +} </style> diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue new file mode 100644 index 0000000000000000000000000000000000000000..8f169c3d23cee06e59dd5a7d9322a7cf1261f665 --- /dev/null +++ b/src/client/pages/page-editor/els/page-editor.el.switch.vue @@ -0,0 +1,50 @@ +<template> +<x-container @remove="() => $emit('remove')" :draggable="true"> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.switch') }}</template> + + <section class="kjuadyyj"> + <mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._switch.name') }}</span></mk-input> + <mk-input v-model="value.text"><span>{{ $t('_pages.blocks._switch.text') }}</span></mk-input> + <mk-switch v-model="value.default"><span>{{ $t('_pages.blocks._switch.default') }}</span></mk-switch> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +import XContainer from '../page-editor.container.vue'; +import MkSwitch from '../../../components/ui/switch.vue'; +import MkInput from '../../../components/ui/input.vue'; + +export default Vue.extend({ + i18n, + + components: { + XContainer, MkSwitch, MkInput + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt, faMagic + }; + }, + + created() { + if (this.value.name == null) Vue.set(this.value, 'name', ''); + }, +}); +</script> + +<style lang="scss" scoped> +.kjuadyyj { + padding: 0 16px 16px 16px; +} +</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue new file mode 100644 index 0000000000000000000000000000000000000000..7c9e3d6a0e34ad5fa9c30644a9ca826686f57c70 --- /dev/null +++ b/src/client/pages/page-editor/els/page-editor.el.text-input.vue @@ -0,0 +1,43 @@ +<template> +<x-container @remove="() => $emit('remove')" :draggable="true"> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.textInput') }}</template> + + <section style="padding: 0 16px 0 16px;"> + <mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textInput.name') }}</span></mk-input> + <mk-input v-model="value.text"><span>{{ $t('_pages.blocks._textInput.text') }}</span></mk-input> + <mk-input v-model="value.default" type="text"><span>{{ $t('_pages.blocks._textInput.default') }}</span></mk-input> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +import XContainer from '../page-editor.container.vue'; +import MkInput from '../../../components/ui/input.vue'; + +export default Vue.extend({ + i18n, + + components: { + XContainer, MkInput + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt, faMagic + }; + }, + + created() { + if (this.value.name == null) Vue.set(this.value, 'name', ''); + }, +}); +</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue similarity index 57% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue rename to src/client/pages/page-editor/els/page-editor.el.text.vue index c09f9cc1cfba4295287b7403612fdbd8f65834f6..00b6cd8a361fd29c48017a505e49c19b36beae83 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue +++ b/src/client/pages/page-editor/els/page-editor.el.text.vue @@ -1,6 +1,6 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template> + <template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template> <section class="ihymsbbe"> <textarea v-model="value.text"></textarea> @@ -11,11 +11,11 @@ <script lang="ts"> import Vue from 'vue'; import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { XContainer @@ -39,20 +39,22 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.ihymsbbe - > textarea - display block - -webkit-appearance none - -moz-appearance none - appearance none - width 100% - min-width 100% - min-height 150px - border none - box-shadow none - padding 16px - background transparent - color var(--text) - font-size 14px +<style lang="scss" scoped> +.ihymsbbe { + > textarea { + display: block; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + min-width: 100%; + min-height: 150px; + border: none; + box-shadow: none; + padding: 16px; + background: transparent; + color: var(--fg); + font-size: 14px; + } +} </style> diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue new file mode 100644 index 0000000000000000000000000000000000000000..8081e706bcccbc375892a683b0b2367d0de2c74d --- /dev/null +++ b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue @@ -0,0 +1,44 @@ +<template> +<x-container @remove="() => $emit('remove')" :draggable="true"> + <template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.textareaInput') }}</template> + + <section style="padding: 0 16px 16px 16px;"> + <mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textareaInput.name') }}</span></mk-input> + <mk-input v-model="value.text"><span>{{ $t('_pages.blocks._textareaInput.text') }}</span></mk-input> + <mk-textarea v-model="value.default"><span>{{ $t('_pages.blocks._textareaInput.default') }}</span></mk-textarea> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../../i18n'; +import XContainer from '../page-editor.container.vue'; +import MkTextarea from '../../../components/ui/textarea.vue'; +import MkInput from '../../../components/ui/input.vue'; + +export default Vue.extend({ + i18n, + + components: { + XContainer, MkTextarea, MkInput + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt, faMagic + }; + }, + + created() { + if (this.value.name == null) Vue.set(this.value, 'name', ''); + }, +}); +</script> diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue similarity index 57% rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue rename to src/client/pages/page-editor/els/page-editor.el.textarea.vue index a0cc1966e814a5b880c8058a0b8e15d66018d34c..fd75849684c3cf56242b5073b76bd923311a0be2 100644 --- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue +++ b/src/client/pages/page-editor/els/page-editor.el.textarea.vue @@ -1,6 +1,6 @@ <template> <x-container @remove="() => $emit('remove')" :draggable="true"> - <template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.textarea') }}</template> + <template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.textarea') }}</template> <section class="ihymsbbe"> <textarea v-model="value.text"></textarea> @@ -11,11 +11,11 @@ <script lang="ts"> import Vue from 'vue'; import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'; -import i18n from '../../../../../i18n'; +import i18n from '../../../i18n'; import XContainer from '../page-editor.container.vue'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { XContainer @@ -39,20 +39,22 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.ihymsbbe - > textarea - display block - -webkit-appearance none - -moz-appearance none - appearance none - width 100% - min-width 100% - min-height 150px - border none - box-shadow none - padding 16px - background transparent - color var(--text) - font-size 14px +<style lang="scss" scoped> +.ihymsbbe { + > textarea { + display: block; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + min-width: 100%; + min-height: 150px; + border: none; + box-shadow: none; + padding: 16px; + background: transparent; + color: var(--fg); + font-size: 14px; + } +} </style> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue similarity index 100% rename from src/client/app/common/views/pages/page-editor/page-editor.blocks.vue rename to src/client/pages/page-editor/page-editor.blocks.vue diff --git a/src/client/pages/page-editor/page-editor.container.vue b/src/client/pages/page-editor/page-editor.container.vue new file mode 100644 index 0000000000000000000000000000000000000000..5a4f096c7f57ccd0ed569ec0092fa72dc1efd5ba --- /dev/null +++ b/src/client/pages/page-editor/page-editor.container.vue @@ -0,0 +1,152 @@ +<template> +<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }"> + <header> + <div class="title"><slot name="header"></slot></div> + <div class="buttons"> + <slot name="func"></slot> + <button v-if="removable" @click="remove()" class="_button"> + <fa :icon="faTrashAlt"/> + </button> + <button v-if="draggable" class="drag-handle _button"> + <fa :icon="faBars"/> + </button> + <button @click="toggleContent(!showBody)" class="_button"> + <template v-if="showBody"><fa :icon="faAngleUp"/></template> + <template v-else><fa :icon="faAngleDown"/></template> + </button> + </div> + </header> + <p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> + <p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> + <div v-show="showBody"> + <slot></slot> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBars, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../../i18n'; + +export default Vue.extend({ + i18n, + + props: { + expanded: { + type: Boolean, + default: true + }, + removable: { + type: Boolean, + default: true + }, + draggable: { + type: Boolean, + default: false + }, + error: { + required: false, + default: null + }, + warn: { + required: false, + default: null + } + }, + data() { + return { + showBody: this.expanded, + faTrashAlt, faBars, faAngleUp, faAngleDown + }; + }, + methods: { + toggleContent(show: boolean) { + this.showBody = show; + this.$emit('toggle', show); + }, + remove() { + this.$emit('remove'); + } + } +}); +</script> + +<style lang="scss" scoped> +.cpjygsrt { + position: relative; + overflow: hidden; + background: var(--panel); + border: solid 2px var(--jvhmlskx); + border-radius: 6px; + + &:hover { + border: solid 2px var(--yakfpmhl); + } + + &.warn { + border: solid 2px #dec44c; + } + + &.error { + border: solid 2px #f00; + } + + & + .cpjygsrt { + margin-top: 16px; + } + + > header { + > .title { + z-index: 1; + margin: 0; + padding: 0 16px; + line-height: 42px; + font-size: 0.9em; + font-weight: bold; + box-shadow: 0 1px rgba(#000, 0.07); + + > [data-icon] { + margin-right: 6px; + } + + &:empty { + display: none; + } + } + + > .buttons { + position: absolute; + z-index: 2; + top: 0; + right: 0; + + > button { + padding: 0; + width: 42px; + font-size: 0.9em; + line-height: 42px; + } + + .drag-handle { + cursor: move; + } + } + } + + > .warn { + color: #b19e49; + margin: 0; + padding: 16px 16px 0 16px; + font-size: 14px; + } + + > .error { + color: #f00; + margin: 0; + padding: 16px 16px 0 16px; + font-size: 14px; + } +} +</style> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue similarity index 77% rename from src/client/app/common/views/pages/page-editor/page-editor.script-block.vue rename to src/client/pages/page-editor/page-editor.script-block.vue index cf76cc003ee83dca44d7dd14357f8e1f9c8949c3..ae56803a39648c1b3f20fd7618a591a6110bffd1 100644 --- a/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue +++ b/src/client/pages/page-editor/page-editor.script-block.vue @@ -8,7 +8,7 @@ </template> <section v-if="value.type === null" class="pbglfege" @click="changeType()"> - {{ $t('script.emptySlot') }} + {{ $t('_pages.script.emptySlot') }} </section> <section v-else-if="value.type === 'text'" class="tbwccoaw"> <input v-model="value.value"/> @@ -17,7 +17,7 @@ <textarea v-model="value.value"></textarea> </section> <section v-else-if="value.type === 'textList'" class="tbwccoaw"> - <textarea v-model="value.value" :placeholder="$t('script.blocks._textList.info')"></textarea> + <textarea v-model="value.value" :placeholder="$t('_pages.script.blocks._textList.info')"></textarea> </section> <section v-else-if="value.type === 'number'" class="tbwccoaw"> <input v-model="value.value" type="number"/> @@ -25,46 +25,47 @@ <section v-else-if="value.type === 'ref'" class="hpdwcrvs"> <select v-model="value.value"> <option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> - <optgroup :label="$t('script.argVariables')"> + <optgroup :label="$t('_pages.script.argVariables')"> <option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option> </optgroup> - <optgroup :label="$t('script.pageVariables')"> + <optgroup :label="$t('_pages.script.pageVariables')"> <option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> </optgroup> - <optgroup :label="$t('script.enviromentVariables')"> + <optgroup :label="$t('_pages.script.enviromentVariables')"> <option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> </optgroup> </select> </section> <section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> - <ui-textarea v-model="slots"> - <span>{{ $t('script.blocks._fn.slots') }}</span> - <template #desc>{{ $t('script.blocks._fn.slots-info') }}</template> - </ui-textarea> - <x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/> + <mk-textarea v-model="slots"> + <span>{{ $t('_pages.script.blocks._fn.slots') }}</span> + <template #desc>{{ $t('_pages.script.blocks._fn.slots-info') }}</template> + </mk-textarea> + <x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/> </section> <section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;"> <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/> </section> <section v-else class="" style="padding:16px;"> - <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/> + <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/> </section> </x-container> </template> <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XContainer from './page-editor.container.vue'; import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons'; -import { isLiteralBlock, funcDefs, blockDefs } from '../../../../../../misc/aiscript/index'; import { v4 as uuid } from 'uuid'; +import i18n from '../../i18n'; +import XContainer from './page-editor.container.vue'; +import MkTextarea from '../../components/ui/textarea.vue'; +import { isLiteralBlock, funcDefs, blockDefs } from '../../scripts/aiscript/index'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { - XContainer + XContainer, MkTextarea }, inject: ['getScriptBlockList'], @@ -117,7 +118,7 @@ export default Vue.extend({ typeText(): any { if (this.value.type === null) return null; if (this.value.type.startsWith('fn:')) return this.value.type.split(':')[1]; - return this.$t(`script.blocks.${this.value.type}`); + return this.$t(`_pages.script.blocks.${this.value.type}`); }, }, @@ -228,44 +229,50 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.turmquns - opacity 0.7 - -.pbglfege - opacity 0.5 - padding 16px - text-align center - cursor pointer - color var(--text) +<style lang="scss" scoped> +.turmquns { + opacity: 0.7; +} -.tbwccoaw - > input - > textarea - display block - -webkit-appearance none - -moz-appearance none - appearance none - width 100% - max-width 100% - min-width 100% - border none - box-shadow none - padding 16px - font-size 16px - background transparent - color var(--text) +.pbglfege { + opacity: 0.5; + padding: 16px; + text-align: center; + cursor: pointer; + color: var(--fg); +} - > textarea - min-height 100px +.tbwccoaw { + > input, + > textarea { + display: block; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + max-width: 100%; + min-width: 100%; + border: none; + box-shadow: none; + padding: 16px; + font-size: 16px; + background: transparent; + color: var(--fg); + } -.hpdwcrvs - padding 16px + > textarea { + min-height: 100px; + } +} - > select - display block - padding 4px - font-size 16px - width 100% +.hpdwcrvs { + padding: 16px; + > select { + display: block; + padding: 4px; + font-size: 16px; + width: 100%; + } +} </style> diff --git a/src/client/app/common/views/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue similarity index 66% rename from src/client/app/common/views/pages/page-editor/page-editor.vue rename to src/client/pages/page-editor/page-editor.vue index cbe65ad6f03b5fdc2a359f73882d61d4da834dcf..a5a4588f132b6e05873432da3cb448e255d6a828 100644 --- a/src/client/app/common/views/pages/page-editor/page-editor.vue +++ b/src/client/pages/page-editor/page-editor.vue @@ -1,58 +1,58 @@ <template> <div> - <div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> + <div class="gwbmwxkm _panel"> <header> <div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('read-page') : pageId ? $t('edit-page') : $t('new-page') }}</div> <div class="buttons"> - <button @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button> - <button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button> - <button @click="save()" v-if="!readonly"><fa :icon="faSave"/></button> + <button class="_button" @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button> + <button class="_button" @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button> + <button class="_button" @click="save()" v-if="!readonly"><fa :icon="faSave"/></button> </div> </header> <section> <router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</router-link> - <ui-input v-model="title"> + <mk-input v-model="title"> <span>{{ $t('title') }}</span> - </ui-input> + </mk-input> <template v-if="showOptions"> - <ui-input v-model="summary"> + <mk-input v-model="summary"> <span>{{ $t('summary') }}</span> - </ui-input> + </mk-input> - <ui-input v-model="name"> + <mk-input v-model="name"> <template #prefix>{{ url }}/@{{ author.username }}/pages/</template> <span>{{ $t('url') }}</span> - </ui-input> + </mk-input> - <ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch> + <mk-switch v-model="alignCenter">{{ $t('align-center') }}</mk-switch> - <ui-select v-model="font"> + <mk-select v-model="font"> <template #label>{{ $t('font') }}</template> <option value="serif">{{ $t('fontSerif') }}</option> <option value="sans-serif">{{ $t('fontSansSerif') }}</option> - </ui-select> + </mk-select> - <ui-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</ui-switch> + <mk-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</mk-switch> <div class="eyeCatch"> - <ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</ui-button> + <mk-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</mk-button> <div v-else-if="eyeCatchingImage"> <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/> - <ui-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catching-image') }}</ui-button> + <mk-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catching-image') }}</mk-button> </div> </div> </template> <x-blocks class="content" v-model="content" :ai-script="aiScript"/> - <ui-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></ui-button> + <mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button> </section> </div> - <ui-container :body-togglable="true"> + <mk-container :body-togglable="true"> <template #header><fa :icon="faMagic"/> {{ $t('variables') }}</template> <div class="qmuvgica"> <x-draggable tag="div" class="variables" v-show="variables.length > 0" :list="variables" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> @@ -69,25 +69,25 @@ /> </x-draggable> - <ui-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></ui-button> + <mk-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></mk-button> - <ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info> + <x-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></x-info> <template v-if="moreDetails"> - <ui-info><span v-html="$t('variables-info2')"></span></ui-info> - <ui-info><span v-html="$t('variables-info3')"></span></ui-info> - <ui-info><span v-html="$t('variables-info4')"></span></ui-info> + <x-info><span v-html="$t('variables-info2')"></span></x-info> + <x-info><span v-html="$t('variables-info3')"></span></x-info> + <x-info><span v-html="$t('variables-info4')"></span></x-info> </template> </div> - </ui-container> + </mk-container> - <ui-container :body-togglable="true" :expanded="false"> + <mk-container :body-togglable="true" :expanded="false"> <template #header><fa :icon="faCode"/> {{ $t('inspector') }}</template> <div style="padding:0 32px 32px 32px;"> - <ui-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</ui-textarea> - <ui-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</ui-textarea> + <mk-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</mk-textarea> + <mk-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</mk-textarea> </div> - </ui-container> + </mk-container> </div> </template> @@ -96,20 +96,26 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../../i18n'; +import { v4 as uuid } from 'uuid'; +import i18n from '../../i18n'; import XVariable from './page-editor.script-block.vue'; import XBlocks from './page-editor.blocks.vue'; -import { v4 as uuid } from 'uuid'; -import { blockDefs } from '../../../../../../misc/aiscript/index'; -import { ASTypeChecker } from '../../../../../../misc/aiscript/type-checker'; -import { url } from '../../../../config'; -import { collectPageVars } from '../../../scripts/collect-page-vars'; +import MkTextarea from '../../components/ui/textarea.vue'; +import MkContainer from '../../components/ui/container.vue'; +import MkButton from '../../components/ui/button.vue'; +import MkSelect from '../../components/ui/select.vue'; +import MkSwitch from '../../components/ui/switch.vue'; +import MkInput from '../../components/ui/input.vue'; +import { blockDefs } from '../../scripts/aiscript/index'; +import { ASTypeChecker } from '../../scripts/aiscript/type-checker'; +import { url } from '../../config'; +import { collectPageVars } from '../../scripts/collect-page-vars'; export default Vue.extend({ - i18n: i18n('pages'), + i18n, components: { - XDraggable, XVariable, XBlocks + XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput }, props: { @@ -268,7 +274,7 @@ export default Vue.extend({ type: 'success', text: this.$t('page-created') }); - this.$router.push(`/i/pages/edit/${this.pageId}`); + this.$router.push(`/my/pages/edit/${this.pageId}`); }).catch(onError); } }, @@ -287,7 +293,7 @@ export default Vue.extend({ type: 'success', text: this.$t('page-deleted') }); - this.$router.push(`/i/pages`); + this.$router.push(`/my/pages`); }); }); }, @@ -344,27 +350,27 @@ export default Vue.extend({ return [{ label: this.$t('content-blocks'), items: [ - { value: 'section', text: this.$t('blocks.section') }, - { value: 'text', text: this.$t('blocks.text') }, - { value: 'image', text: this.$t('blocks.image') }, - { value: 'textarea', text: this.$t('blocks.textarea') }, + { value: 'section', text: this.$t('_pages.blocks.section') }, + { value: 'text', text: this.$t('_pages.blocks.text') }, + { value: 'image', text: this.$t('_pages.blocks.image') }, + { value: 'textarea', text: this.$t('_pages.blocks.textarea') }, ] }, { label: this.$t('input-blocks'), items: [ - { value: 'button', text: this.$t('blocks.button') }, - { value: 'radioButton', text: this.$t('blocks.radioButton') }, - { value: 'textInput', text: this.$t('blocks.textInput') }, - { value: 'textareaInput', text: this.$t('blocks.textareaInput') }, - { value: 'numberInput', text: this.$t('blocks.numberInput') }, - { value: 'switch', text: this.$t('blocks.switch') }, - { value: 'counter', text: this.$t('blocks.counter') } + { value: 'button', text: this.$t('_pages.blocks.button') }, + { value: 'radioButton', text: this.$t('_pages.blocks.radioButton') }, + { value: 'textInput', text: this.$t('_pages.blocks.textInput') }, + { value: 'textareaInput', text: this.$t('_pages.blocks.textareaInput') }, + { value: 'numberInput', text: this.$t('_pages.blocks.numberInput') }, + { value: 'switch', text: this.$t('_pages.blocks.switch') }, + { value: 'counter', text: this.$t('_pages.blocks.counter') } ] }, { label: this.$t('special-blocks'), items: [ - { value: 'if', text: this.$t('blocks.if') }, - { value: 'post', text: this.$t('blocks.post') } + { value: 'if', text: this.$t('_pages.blocks.if') }, + { value: 'post', text: this.$t('_pages.blocks.post') } ] }]; }, @@ -379,7 +385,7 @@ export default Vue.extend({ if (category) { category.items.push({ value: block.type, - text: this.$t(`script.blocks.${block.type}`) + text: this.$t(`_pages.script.blocks.${block.type}`) }); } else { list.push({ @@ -387,7 +393,7 @@ export default Vue.extend({ label: this.$t(`script.categories.${block.category}`), items: [{ value: block.type, - text: this.$t(`script.blocks.${block.type}`) + text: this.$t(`_pages.script.blocks.${block.type}`) }] }); } @@ -422,87 +428,89 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.gwbmwxkm - overflow hidden - background var(--face) - margin-bottom 16px - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) - - > header - background var(--faceHeader) - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color var(--faceHeaderText) - box-shadow 0 var(--lineWidth) rgba(#000, 0.07) - - > [data-icon] - margin-right 6px - - &:empty - display none - - > .buttons - position absolute - z-index 2 - top 0 - right 0 - - > button - padding 0 - width 42px - font-size 0.9em - line-height 42px - color var(--faceTextButton) - - &:hover - color var(--faceTextButtonHover) +<style lang="scss" scoped> +.gwbmwxkm { + margin-bottom: var(--margin); + + > header { + background: var(--faceHeader); + + > .title { + z-index: 1; + margin: 0; + padding: 0 16px; + line-height: 42px; + font-size: 0.9em; + font-weight: bold; + color: var(--faceHeaderText); + box-shadow: 0 var(--lineWidth) rgba(#000, 0.07); + + > [data-icon] { + margin-right: 6px; + } - &:active - color var(--faceTextButtonActive) + &:empty { + display: none; + } + } - > section - padding 0 32px 32px 32px + > .buttons { + position: absolute; + z-index: 2; + top: 0; + right: 0; + + > button { + padding: 0; + width: 42px; + font-size: 0.9em; + line-height: 42px; + } + } + } - @media (max-width 500px) - padding 0 16px 16px 16px + > section { + padding: 0 32px 32px 32px; - > .view - display inline-block - margin 16px 0 0 0 - font-size 14px + @media (max-width: 500px) { + padding: 0 16px 16px 16px; + } - > .content - margin-bottom 16px + > .view { + display: inline-block; + margin: 16px 0 0 0; + font-size: 14px; + } - > .eyeCatch - margin-bottom 16px + > .content { + margin-bottom: 16px; + } - > div - > img - max-width 100% + > .eyeCatch { + margin-bottom: 16px; -.qmuvgica - padding 32px + > div { + > img { + max-width: 100%; + } + } + } + } +} - @media (max-width 500px) - padding 16px +.qmuvgica { + padding: 32px; - > .variables - margin-bottom 16px + @media (max-width: 500px) { + padding: 16px; + } - > .add - margin-bottom 16px + > .variables { + margin-bottom: 16px; + } + > .add { + margin-bottom: 16px; + } +} </style> diff --git a/src/client/app/common/views/pages/page.vue b/src/client/pages/page.vue similarity index 66% rename from src/client/app/common/views/pages/page.vue rename to src/client/pages/page.vue index d1c4c2be43a0dfb34b56c370ab1430b4750a6005..72c51017315e054d11fe376364a7e870afdeec19 100644 --- a/src/client/app/common/views/pages/page.vue +++ b/src/client/pages/page.vue @@ -1,10 +1,14 @@ <template> -<x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/> +<div class="xcukqgmh _panel"> + <portal to="avatar" v-if="page"><mk-avatar class="avatar" :user="page.user" :disable-preview="true"/></portal> + <portal to="title" v-if="page">{{ page.title || page.name }}</portal> + + <x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/> +</div> </template> <script lang="ts"> import Vue from 'vue'; -import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; import XPage from '../components/page/page.vue'; export default Vue.extend({ @@ -52,12 +56,14 @@ export default Vue.extend({ username: this.username, }).then(page => { this.page = page; - this.$emit('init', { - title: this.page.title, - icon: faStickyNote - }); }); }, } }); </script> + +<style lang="scss" scoped> +.xcukqgmh { + +} +</style> diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue new file mode 100644 index 0000000000000000000000000000000000000000..bee7d30a6178922a44c3aed04f44ef107c3c0f0d --- /dev/null +++ b/src/client/pages/pages.vue @@ -0,0 +1,78 @@ +<template> +<div> + <mk-container :body-togglable="true"> + <template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template> + <div class="rknalgpo my"> + <mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button> + <mk-pagination :pagination="myPagesPagination" #default="{items}"> + <mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> + </mk-pagination> + </div> + </mk-container> + + <mk-container :body-togglable="true"> + <template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template> + <div class="rknalgpo"> + <mk-pagination :pagination="likedPagesPagination" #default="{items}"> + <mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> + </mk-pagination> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons'; +import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../i18n'; +import MkPagePreview from '../components/page-preview.vue'; +import MkPagination from '../components/ui/pagination.vue'; +import MkButton from '../components/ui/button.vue'; +import MkContainer from '../components/ui/container.vue'; + +export default Vue.extend({ + i18n, + components: { + MkPagePreview, MkPagination, MkButton, MkContainer + }, + data() { + return { + myPagesPagination: { + endpoint: 'i/pages', + limit: 5, + }, + likedPagesPagination: { + endpoint: 'i/page-likes', + limit: 5, + }, + faStickyNote, faPlus, faEdit, faHeart + }; + }, + methods: { + create() { + this.$router.push(`/my/pages/new`); + } + } +}); +</script> + +<style lang="scss" scoped> +.rknalgpo { + padding: 16px; + + &.my .ckltabjg:first-child { + margin-top: 16px; + } + + .ckltabjg:not(:last-child) { + margin-bottom: 8px; + } + + @media (min-width: 500px) { + .ckltabjg:not(:last-child) { + margin-bottom: 16px; + } + } +} +</style> diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue new file mode 100644 index 0000000000000000000000000000000000000000..c3e87c0d0c4322b3094d55e96d3bd73b35c0c9bb --- /dev/null +++ b/src/client/pages/search.vue @@ -0,0 +1,55 @@ +<template> +<div> + <portal to="icon"><fa :icon="faSearch"/></portal> + <portal to="title">{{ $t('searchWith', { q: $route.query.q }) }}</portal> + <x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import Progress from '../scripts/loading'; +import XNotes from '../components/notes.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('searchWith', { q: this.$route.query.q }) as string + }; + }, + + components: { + XNotes + }, + + data() { + return { + pagination: { + endpoint: 'notes/search', + limit: 10, + params: () => ({ + query: this.$route.query.q, + }) + }, + faSearch + }; + }, + + watch: { + $route() { + (this.$refs.notes as any).reload(); + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue new file mode 100644 index 0000000000000000000000000000000000000000..7163f2ece47c951aa600a0ac5cbe4f171b5bd7f2 --- /dev/null +++ b/src/client/pages/settings/2fa.vue @@ -0,0 +1,264 @@ +<template> +<section class="_section"> + <div class="_title"><fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div> + <div class="_content"> + <p v-if="!data && !$store.state.i.twoFactorEnabled"><mk-button @click="register">{{ $t('_2fa.registerDevice') }}</mk-button></p> + <template v-if="$store.state.i.twoFactorEnabled"> + <h2 class="heading">{{ $t('totp-header') }}</h2> + <p>{{ $t('already-registered') }}</p> + <mk-button @click="unregister">{{ $t('unregister') }}</mk-button> + + <template v-if="supportsCredentials"> + <hr class="totp-method-sep"> + + <h2 class="heading">{{ $t('security-key-header') }}</h2> + <p>{{ $t('security-key') }}</p> + <div class="key-list"> + <div class="key" v-for="key in $store.state.i.securityKeysList"> + <h3> + {{ key.name }} + </h3> + <div class="last-used"> + {{ $t('last-used') }} + <mk-time :time="key.lastUsed"/> + </div> + <mk-button @click="unregisterKey(key)"> + {{ $t('unregister') }} + </mk-button> + </div> + </div> + + <mk-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0"> + {{ $t('use-password-less-login') }} + </mk-switch> + + <mk-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</mk-info> + <mk-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</mk-button> + + <ol v-if="registration && !registration.error"> + <li v-if="registration.stage >= 0"> + {{ $t('activate-key') }} + <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" /> + </li> + <li v-if="registration.stage >= 1"> + <mk-form :disabled="registration.stage != 1 || registration.saving"> + <mk-input v-model="keyName" :max="30"> + <span>{{ $t('security-key-name') }}</span> + </mk-input> + <mk-button @click="registerKey" :disabled="this.keyName.length == 0"> + {{ $t('register-security-key') }} + </mk-button> + <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" /> + </mk-form> + </li> + </ol> + </template> + </template> + <div v-if="data && !$store.state.i.twoFactorEnabled"> + <ol style="margin: 0; padding: 0 0 0 1em;"> + <li> + <i18n path="_2fa.step1" tag="span"> + <a href="https://authy.com/" rel="noopener" target="_blank" place="a" style="color: var(--link);">Authy</a> + <a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" place="b" style="color: var(--link);">Google Authenticator</a> + </i18n> + </li> + <li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li> + <li>{{ $t('_2fa.step3') }}<br> + <mk-input v-model="token" type="number" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</mk-input> + <mk-button primary @click="submit">{{ $t('done') }}</mk-button> + </li> + </ol> + <mk-info>{{ $t('_2fa.step4') }}</mk-info> + </div> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faLock } from '@fortawesome/free-solid-svg-icons'; +import i18n from '../../i18n'; +import { hostname } from '../../config'; +import { hexifyAB } from '../../scripts/2fa'; +import MkButton from '../../components/ui/button.vue'; +import MkInfo from '../../components/ui/info.vue'; +import MkInput from '../../components/ui/input.vue'; + +function stringifyAB(buffer) { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +} + +export default Vue.extend({ + i18n, + components: { + MkButton, MkInfo, MkInput + }, + data() { + return { + data: null, + supportsCredentials: !!navigator.credentials, + usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin, + registration: null, + keyName: '', + token: null, + faLock + }; + }, + methods: { + register() { + this.$root.dialog({ + title: this.$t('password'), + input: { + type: 'password' + } + }).then(({ canceled, result: password }) => { + if (canceled) return; + this.$root.api('i/2fa/register', { + password: password + }).then(data => { + this.data = data; + }); + }); + }, + + unregister() { + this.$root.dialog({ + title: this.$t('password'), + input: { + type: 'password' + } + }).then(({ canceled, result: password }) => { + if (canceled) return; + this.$root.api('i/2fa/unregister', { + password: password + }).then(() => { + this.usePasswordLessLogin = false; + this.updatePasswordLessLogin(); + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + this.$store.state.i.twoFactorEnabled = false; + }); + }); + }, + + submit() { + this.$root.api('i/2fa/done', { + token: this.token + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + this.$store.state.i.twoFactorEnabled = true; + }).catch(e => { + this.$root.dialog({ + type: 'error', + iconOnly: true, autoClose: true + }); + }); + }, + + registerKey() { + this.registration.saving = true; + this.$root.api('i/2fa/key-done', { + password: this.registration.password, + name: this.keyName, + challengeId: this.registration.challengeId, + // we convert each 16 bits to a string to serialise + clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON), + attestationObject: hexifyAB(this.registration.credential.response.attestationObject) + }).then(key => { + this.registration = null; + key.lastUsed = new Date(); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }) + }, + + unregisterKey(key) { + this.$root.dialog({ + title: this.$t('password'), + input: { + type: 'password' + } + }).then(({ canceled, result: password }) => { + if (canceled) return; + return this.$root.api('i/2fa/remove-key', { + password, + credentialId: key.id + }).then(() => { + this.usePasswordLessLogin = false; + this.updatePasswordLessLogin(); + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + }); + }, + + addSecurityKey() { + this.$root.dialog({ + title: this.$t('password'), + input: { + type: 'password' + } + }).then(({ canceled, result: password }) => { + if (canceled) return; + this.$root.api('i/2fa/register-key', { + password + }).then(registration => { + this.registration = { + password, + challengeId: registration.challengeId, + stage: 0, + publicKeyOptions: { + challenge: Buffer.from( + registration.challenge + .replace(/\-/g, "+") + .replace(/_/g, "/"), + 'base64' + ), + rp: { + id: hostname, + name: 'Misskey' + }, + user: { + id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)), + name: this.$store.state.i.username, + displayName: this.$store.state.i.name, + }, + pubKeyCredParams: [{alg: -7, type: 'public-key'}], + timeout: 60000, + attestation: 'direct' + }, + saving: true + }; + return navigator.credentials.create({ + publicKey: this.registration.publicKeyOptions + }); + }).then(credential => { + this.registration.credential = credential; + this.registration.saving = false; + this.registration.stage = 1; + }).catch(err => { + console.warn('Error while registering?', err); + this.registration.error = err.message; + this.registration.stage = -1; + }); + }); + }, + updatePasswordLessLogin() { + this.$root.api('i/2fa/password-less', { + value: !!this.usePasswordLessLogin + }); + } + } +}); +</script> diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue new file mode 100644 index 0000000000000000000000000000000000000000..d0c18a07e5ff51d8be513b21f03aba1ad6181aa6 --- /dev/null +++ b/src/client/pages/settings/drive.vue @@ -0,0 +1,212 @@ +<template> +<section class="mk-settings-page-drive _section"> + <div class="_title"><fa :icon="faCloud"/> {{ $t('drive') }}</div> + <div class="_content"> + <mk-pagination :pagination="drivePagination" #default="{items}" class="drive" ref="drive"> + <div class="file" v-for="(file, i) in items" :key="file.id" :data-index="i" @click="selected = file" :class="{ selected: selected && (selected.id === file.id) }"> + <x-file-thumbnail class="thumbnail" :file="file" fit="cover"/> + <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> + <footer> + <span class="type"><x-file-type-icon :type="file.type" class="icon"/>{{ file.type }}</span> + <span class="separator"></span> + <span class="data-size">{{ file.size | bytes }}</span> + <span class="separator"></span> + <span class="created-at"><fa :icon="faClock"/><mk-time :time="file.createdAt"/></span> + <template v-if="file.isSensitive"> + <span class="separator"></span> + <span class="nsfw"><fa :icon="faEyeSlash"/> {{ $t('nsfw') }}</span> + </template> + </footer> + </div> + </div> + </mk-pagination> + </div> + <div class="_footer"> + <mk-button primary inline :disabled="selected == null" @click="download()"><fa :icon="faDownload"/> {{ $t('download') }}</mk-button> + <mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCloud, faDownload } from '@fortawesome/free-solid-svg-icons'; +import { faClock, faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import XFileTypeIcon from '../../components/file-type-icon.vue'; +import XFileThumbnail from '../../components/drive-file-thumbnail.vue'; +import MkButton from '../../components/ui/button.vue'; +import MkPagination from '../../components/ui/pagination.vue'; +import i18n from '../../i18n'; + +export default Vue.extend({ + i18n, + + components: { + XFileTypeIcon, + XFileThumbnail, + MkPagination, + MkButton, + }, + + data() { + return { + selected: null, + connection: null, + drivePagination: { + endpoint: 'drive/files', + limit: 10, + }, + faCloud, faClock, faEyeSlash, faDownload, faTrashAlt + } + }, + + created() { + this.connection = this.$root.stream.useSharedConnection('drive'); + + this.connection.on('fileCreated', this.onStreamDriveFileCreated); + this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); + this.connection.on('fileDeleted', this.onStreamDriveFileDeleted); + }, + + beforeDestroy() { + this.connection.dispose(); + }, + + methods: { + onStreamDriveFileCreated(file) { + this.$refs.drive.prepend(file); + }, + + onStreamDriveFileUpdated(file) { + // TODO + }, + + onStreamDriveFileDeleted(fileId) { + this.$refs.drive.remove(x => x.id === fileId); + }, + + download() { + window.open(this.selected.url, '_blank'); + }, + + async del() { + const { canceled } = await this.$root.dialog({ + type: 'warning', + text: this.$t('driveFileDeleteConfirm', { name: this.selected.name }), + showCancelButton: true + }); + if (canceled) return; + + this.$root.api('drive/files/delete', { + fileId: this.selected.id + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-settings-page-drive { + > ._content { + max-height: 350px; + overflow: auto; + + > .drive { + > .file { + display: grid; + margin: 0 auto; + grid-template-columns: 64px 1fr; + grid-column-gap: 10px; + cursor: pointer; + + &.selected { + background: var(--accent); + box-shadow: 0 0 0 8px var(--accent); + color: #fff; + } + + &:not(:last-child) { + margin-bottom: 16px; + } + + > .thumbnail { + width: 64px; + height: 64px; + } + + > .body { + display: block; + word-break: break-all; + padding-top: 4px; + + > .name { + display: block; + margin: 0; + padding: 0; + font-size: 0.9em; + font-weight: bold; + word-break: 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 { + padding: 0 4px; + } + + > .type { + opacity: 0.7; + + > .icon { + margin-right: 4px; + } + } + + > .data-size { + opacity: 0.7; + } + + > .created-at { + opacity: 0.7; + + > [data-icon] { + margin-right: 2px; + } + } + + > .nsfw { + color: #bf4633; + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue new file mode 100644 index 0000000000000000000000000000000000000000..6b63da742c02783cbb6452835cafd6b80f650c6c --- /dev/null +++ b/src/client/pages/settings/general.vue @@ -0,0 +1,108 @@ +<template> +<section class="mk-settings-page-general _section"> + <div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div> + <div class="_content"> + <mk-input type="file" @change="onWallpaperChange" style="margin-top: 0;"> + <span>{{ $t('wallpaper') }}</span> + <template #icon><fa :icon="faImage"/></template> + <template #desc v-if="wallpaperUploading">{{ $t('uploading') }}<mk-ellipsis/></template> + </mk-input> + <mk-button primary :disabled="$store.state.settings.wallpaper == null" @click="delWallpaper()">{{ $t('removeWallpaper') }}</mk-button> + </div> + <div class="_content"> + <mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch"> + {{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template> + </mk-switch> + </div> + <div class="_content"> + <mk-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</mk-button> + <mk-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</mk-button> + <mk-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faImage, faCog } from '@fortawesome/free-solid-svg-icons'; +import MkInput from '../../components/ui/input.vue'; +import MkButton from '../../components/ui/button.vue'; +import MkSwitch from '../../components/ui/switch.vue'; +import i18n from '../../i18n'; +import { apiUrl } from '../../config'; + +export default Vue.extend({ + i18n, + + components: { + MkInput, + MkButton, + MkSwitch, + }, + + data() { + return { + wallpaperUploading: false, + faImage, faCog + } + }, + + computed: { + wallpaper: { + get() { return this.$store.state.settings.wallpaper; }, + set(value) { this.$store.dispatch('settings/set', { key: 'wallpaper', value }); } + }, + }, + + methods: { + onWallpaperChange([file]) { + this.wallpaperUploading = true; + + const data = new FormData(); + data.append('file', file); + data.append('i', this.$store.state.i.token); + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.wallpaper = f.url; + this.wallpaperUploading = false; + document.documentElement.style.backgroundImage = `url(${this.$store.state.settings.wallpaper})`; + }) + .catch(e => { + this.wallpaperUploading = false; + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + delWallpaper() { + this.wallpaper = null; + document.documentElement.style.backgroundImage = 'none'; + }, + + onChangeAutoWatch(v) { + this.$root.api('i/update', { + autoWatch: v + }); + }, + + readAllUnreadNotes() { + this.$root.api('i/read_all_unread_notes'); + }, + + readAllMessagingMessages() { + this.$root.api('i/read_all_messaging_messages'); + }, + + readAllNotifications() { + this.$root.api('notifications/mark_all_as_read'); + } + } +}); +</script> diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue new file mode 100644 index 0000000000000000000000000000000000000000..5714aabbf641e02deba414b6cd8439e7d50fef84 --- /dev/null +++ b/src/client/pages/settings/import-export.vue @@ -0,0 +1,121 @@ +<template> +<section class="mk-settings-page-import-export _section"> + <div class="_title"><fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div> + <div class="_content"> + <input ref="file" type="file" style="display: none;" @change="onChangeFile"/> + <mk-select v-model="exportTarget" style="margin-top: 0;"> + <option value="notes">{{ $t('_exportOrImport.allNotes') }}</option> + <option value="following">{{ $t('_exportOrImport.followingList') }}</option> + <option value="user-lists">{{ $t('_exportOrImport.userLists') }}</option> + <option value="mute">{{ $t('_exportOrImport.muteList') }}</option> + <option value="blocking">{{ $t('_exportOrImport.blockingList') }}</option> + </mk-select> + <mk-button inline @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</mk-button> + <mk-button inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons'; +import MkButton from '../../components/ui/button.vue'; +import MkSelect from '../../components/ui/select.vue'; +import i18n from '../../i18n'; +import { apiUrl } from '../../config'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + MkSelect, + }, + + data() { + return { + exportTarget: 'notes', + faDownload, faUpload, faBoxes + } + }, + + methods: { + doExport() { + this.$root.api( + this.exportTarget == 'notes' ? 'i/export-notes' : + this.exportTarget == 'following' ? 'i/export-following' : + this.exportTarget == 'blocking' ? 'i/export-blocking' : + this.exportTarget == 'user-lists' ? 'i/export-user-lists' : + null, {}) + .then(() => { + this.$root.dialog({ + type: 'info', + text: this.$t('exportRequested') + }); + }).catch((e: any) => { + this.$root.dialog({ + type: 'error', + text: e.message + }); + }); + }, + + doImport() { + (this.$refs.file as any).click(); + }, + + onChangeFile() { + const [file] = Array.from((this.$refs.file as any).files); + + const data = new FormData(); + data.append('file', file); + data.append('i', this.$store.state.i.token); + + const dialog = this.$root.dialog({ + type: 'waiting', + text: this.$t('uploading') + '...', + showOkButton: false, + showCancelButton: false, + cancelableByBgClick: false + }); + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.reqImport(f); + }) + .catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }) + .finally(() => { + dialog.close(); + }); + }, + + reqImport(file) { + this.$root.api( + this.exportTarget == 'following' ? 'i/import-following' : + this.exportTarget == 'user-lists' ? 'i/import-user-lists' : + null, { + fileId: file.id + }).then(() => { + this.$root.dialog({ + type: 'info', + text: this.$t('importRequested') + }); + }).catch((e: any) => { + this.$root.dialog({ + type: 'error', + text: e.message + }); + }); + } + } +}); +</script> diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..1a00c65760c94b7540c59983797544f90785b700 --- /dev/null +++ b/src/client/pages/settings/index.vue @@ -0,0 +1,94 @@ +<template> +<div class="mk-settings-page"> + <portal to="icon"><fa :icon="faCog"/></portal> + <portal to="title">{{ $t('settings') }}</portal> + + <x-profile-setting/> + <x-privacy-setting/> + <x-reaction-setting/> + <x-theme/> + <x-import-export/> + <x-drive/> + <x-general/> + <x-mute-block/> + <x-security/> + <x-2fa/> + <x-integration/> + + <mk-button @click="cacheClear()" primary class="cacheClear">{{ $t('cacheClear') }}</mk-button> + <mk-button @click="$root.signout()" primary class="logout">{{ $t('logout') }}</mk-button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCog } from '@fortawesome/free-solid-svg-icons'; +import XProfileSetting from './profile.vue'; +import XPrivacySetting from './privacy.vue'; +import XImportExport from './import-export.vue'; +import XDrive from './drive.vue'; +import XGeneral from './general.vue'; +import XReactionSetting from './reaction.vue'; +import XMuteBlock from './mute-block.vue'; +import XSecurity from './security.vue'; +import XTheme from './theme.vue'; +import X2fa from './2fa.vue'; +import XIntegration from './integration.vue'; +import MkButton from '../../components/ui/button.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('settings') as string + }; + }, + + components: { + XProfileSetting, + XPrivacySetting, + XImportExport, + XDrive, + XGeneral, + XReactionSetting, + XMuteBlock, + XSecurity, + XTheme, + X2fa, + XIntegration, + MkButton, + }, + + data() { + return { + faCog + } + }, + + methods: { + cacheClear() { + // Clear cache (service worker) + try { + navigator.serviceWorker.controller.postMessage('clear'); + + navigator.serviceWorker.getRegistrations().then(registrations => { + for (const registration of registrations) registration.unregister(); + }); + } catch (e) { + console.error(e); + } + + // Force reload + location.reload(true); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-settings-page { + > .logout, + > .cacheClear { + margin: 8px auto; + } +} +</style> diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue new file mode 100644 index 0000000000000000000000000000000000000000..b156e130274a357c531ae0d989cb9744e0d34c5a --- /dev/null +++ b/src/client/pages/settings/integration.vue @@ -0,0 +1,122 @@ +<template> +<section class="_section" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration"> + <div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div> + <div class="_content" v-if="enableTwitterIntegration"> + <header><fa :icon="faTwitter"/> Twitter</header> + <p v-if="$store.state.i.twitter">{{ $t('connectedTo') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> + <mk-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnectSerice') }}</mk-button> + <mk-button v-else @click="connectTwitter">{{ $t('connectSerice') }}</mk-button> + </div> + + <div class="_content" v-if="enableDiscordIntegration"> + <header><fa :icon="faDiscord"/> Discord</header> + <p v-if="$store.state.i.discord">{{ $t('connectedTo') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p> + <mk-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnectSerice') }}</mk-button> + <mk-button v-else @click="connectDiscord">{{ $t('connectSerice') }}</mk-button> + </div> + + <div class="_content" v-if="enableGithubIntegration"> + <header><fa :icon="faGithub"/> GitHub</header> + <p v-if="$store.state.i.github">{{ $t('connectedTo') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.github.login }}</a></p> + <mk-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnectSerice') }}</mk-button> + <mk-button v-else @click="connectGithub">{{ $t('connectSerice') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faShareAlt } from '@fortawesome/free-solid-svg-icons'; +import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'; +import i18n from '../../i18n'; +import { apiUrl } from '../../config'; +import MkButton from '../../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + + components: { + MkButton + }, + + data() { + return { + apiUrl, + twitterForm: null, + discordForm: null, + githubForm: null, + enableTwitterIntegration: false, + enableDiscordIntegration: false, + enableGithubIntegration: false, + faShareAlt, faTwitter, faDiscord, faGithub + }; + }, + + created() { + this.$root.getMeta().then(meta => { + this.enableTwitterIntegration = meta.enableTwitterIntegration; + this.enableDiscordIntegration = meta.enableDiscordIntegration; + this.enableGithubIntegration = meta.enableGithubIntegration; + }); + }, + + mounted() { + if (!document.cookie.match(/i=(\w+)/)) { + document.cookie = `i=${this.$store.state.i.token}; path=/;` + + ` domain=${document.location.hostname}; max-age=31536000;` + + (document.location.protocol.startsWith('https') ? ' secure' : ''); + } + this.$watch('$store.state.i', () => { + if (this.$store.state.i.twitter) { + if (this.twitterForm) this.twitterForm.close(); + } + if (this.$store.state.i.discord) { + if (this.discordForm) this.discordForm.close(); + } + if (this.$store.state.i.github) { + if (this.githubForm) this.githubForm.close(); + } + }, { + deep: true + }); + }, + + methods: { + connectTwitter() { + this.twitterForm = window.open(apiUrl + '/connect/twitter', + 'twitter_connect_window', + 'height=570, width=520'); + }, + + disconnectTwitter() { + window.open(apiUrl + '/disconnect/twitter', + 'twitter_disconnect_window', + 'height=570, width=520'); + }, + + connectDiscord() { + this.discordForm = window.open(apiUrl + '/connect/discord', + 'discord_connect_window', + 'height=570, width=520'); + }, + + disconnectDiscord() { + window.open(apiUrl + '/disconnect/discord', + 'discord_disconnect_window', + 'height=570, width=520'); + }, + + connectGithub() { + this.githubForm = window.open(apiUrl + '/connect/github', + 'github_connect_window', + 'height=570, width=520'); + }, + + disconnectGithub() { + window.open(apiUrl + '/disconnect/github', + 'github_disconnect_window', + 'height=570, width=520'); + }, + } +}); +</script> diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue new file mode 100644 index 0000000000000000000000000000000000000000..109b33d4f55b38bc630c6a5c815265ec7876499f --- /dev/null +++ b/src/client/pages/settings/mute-block.vue @@ -0,0 +1,76 @@ +<template> +<section class="mk-settings-page-mute-block _section"> + <div class="_title"><fa :icon="faBan"/> {{ $t('muteAndBlock') }}</div> + <div class="_content"> + <span>{{ $t('mutedUsers') }}</span> + <mk-pagination :pagination="mutingPagination" class="muting"> + <template #empty><span>{{ $t('noUsers') }}</span></template> + <template #default="{items}"> + <div class="user" v-for="(mute, i) in items" :key="mute.id" :data-index="i"> + <router-link class="name" :to="mute.mutee | userPage"> + <mk-acct :user="mute.mutee"/> + </router-link> + </div> + </template> + </mk-pagination> + </div> + <div class="_content"> + <span>{{ $t('blockedUsers') }}</span> + <mk-pagination :pagination="blockingPagination" class="blocking"> + <template #empty><span>{{ $t('noUsers') }}</span></template> + <template #default="{items}"> + <div class="user" v-for="(block, i) in items" :key="block.id" :data-index="i"> + <router-link class="name" :to="block.blockee | userPage"> + <mk-acct :user="block.blockee"/> + </router-link> + </div> + </template> + </mk-pagination> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faBan } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '../../components/ui/pagination.vue'; +import i18n from '../../i18n'; + +export default Vue.extend({ + i18n, + + components: { + MkPagination, + }, + + data() { + return { + mutingPagination: { + endpoint: 'mute/list', + limit: 10, + }, + blockingPagination: { + endpoint: 'blocking/list', + limit: 10, + }, + faBan + } + }, +}); +</script> + +<style lang="scss" scoped> +.mk-settings-page-mute-block { + > ._content { + max-height: 350px; + overflow: auto; + + > .muting, + > .blocking { + > .empty { + opacity: 0.5 !important; + } + } + } +} +</style> diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue new file mode 100644 index 0000000000000000000000000000000000000000..0fc67d5b7db5b4f72ac45b8884c4bc3a61065c5a --- /dev/null +++ b/src/client/pages/settings/privacy.vue @@ -0,0 +1,69 @@ +<template> +<section class="mk-settings-page-privacy _section"> + <div class="_title"><fa :icon="faLock"/> {{ $t('privacy') }}</div> + <div class="_content"> + <mk-switch v-model="isLocked" @change="save()">{{ $t('makeFollowManuallyApprove') }}</mk-switch> + <mk-switch v-model="autoAcceptFollowed" :disabled="!isLocked" @change="save()">{{ $t('autoAcceptFollowed') }}</mk-switch> + </div> + <div class="_content"> + <mk-select v-model="defaultNoteVisibility" style="margin-top: 8px;"> + <template #label>{{ $t('defaultNoteVisibility') }}</template> + <option value="public">{{ $t('_visibility.public') }}</option> + <option value="followers">{{ $t('_visibility.followers') }}</option> + <option value="specified">{{ $t('_visibility.specified') }}</option> + </mk-select> + <mk-switch v-model="rememberNoteVisibility" @change="save()">{{ $t('rememberNoteVisibility') }}</mk-switch> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faLock } from '@fortawesome/free-solid-svg-icons'; +import MkSelect from '../../components/ui/select.vue'; +import MkSwitch from '../../components/ui/switch.vue'; +import i18n from '../../i18n'; + +export default Vue.extend({ + i18n, + + components: { + MkSelect, + MkSwitch, + }, + + data() { + return { + isLocked: false, + autoAcceptFollowed: false, + faLock + } + }, + + computed: { + defaultNoteVisibility: { + get() { return this.$store.state.settings.defaultNoteVisibility; }, + set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); } + }, + + rememberNoteVisibility: { + get() { return this.$store.state.settings.rememberNoteVisibility; }, + set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); } + }, + }, + + created() { + this.isLocked = this.$store.state.i.isLocked; + this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; + }, + + methods: { + save() { + this.$root.api('i/update', { + isLocked: !!this.isLocked, + autoAcceptFollowed: !!this.autoAcceptFollowed, + }); + } + } +}); +</script> diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue new file mode 100644 index 0000000000000000000000000000000000000000..e6219c2d56f4e25c8c0eaeeb3fd17f55a650637e --- /dev/null +++ b/src/client/pages/settings/profile.vue @@ -0,0 +1,246 @@ +<template> +<section class="mk-settings-page-profile _section"> + <div class="_title"><fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div> + <div class="_content"> + <mk-input v-model="name" :max="30"> + <span>{{ $t('_profile.name') }}</span> + </mk-input> + + <mk-textarea v-model="description" :max="500"> + <span>{{ $t('_profile.description') }}</span> + <template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template> + </mk-textarea> + + <mk-input v-model="location"> + <span>{{ $t('location') }}</span> + <template #prefix><fa :icon="faMapMarkerAlt"/></template> + </mk-input> + + <mk-input v-model="birthday" type="date"> + <template #title>{{ $t('birthday') }}</template> + <template #prefix><fa :icon="faBirthdayCake"/></template> + </mk-input> + + <mk-input type="file" @change="onAvatarChange"> + <span>{{ $t('avatar') }}</span> + <template #icon><fa :icon="faImage"/></template> + <template #desc v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></template> + </mk-input> + + <mk-input type="file" @change="onBannerChange"> + <span>{{ $t('banner') }}</span> + <template #icon><fa :icon="faImage"/></template> + <template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template> + </mk-input> + + <details class="fields"> + <summary>{{ $t('_profile.metadata') }}</summary> + <div class="row"> + <mk-input v-model="fieldName0">{{ $t('_profile.metadataLabel') }}</mk-input> + <mk-input v-model="fieldValue0">{{ $t('_profile.metadataContent') }}</mk-input> + </div> + <div class="row"> + <mk-input v-model="fieldName1">{{ $t('_profile.metadataLabel') }}</mk-input> + <mk-input v-model="fieldValue1">{{ $t('_profile.metadataContent') }}</mk-input> + </div> + <div class="row"> + <mk-input v-model="fieldName2">{{ $t('_profile.metadataLabel') }}</mk-input> + <mk-input v-model="fieldValue2">{{ $t('_profile.metadataContent') }}</mk-input> + </div> + <div class="row"> + <mk-input v-model="fieldName3">{{ $t('_profile.metadataLabel') }}</mk-input> + <mk-input v-model="fieldValue3">{{ $t('_profile.metadataContent') }}</mk-input> + </div> + </details> + + <mk-switch v-model="isBot">{{ $t('flagAsBot') }}</mk-switch> + <mk-switch v-model="isCat">{{ $t('flagAsCat') }}</mk-switch> + </div> + <div class="_footer"> + <mk-button @click="save(true)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faUnlockAlt, faCogs, faImage, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons'; +import { faSave } from '@fortawesome/free-regular-svg-icons'; +import MkButton from '../../components/ui/button.vue'; +import MkInput from '../../components/ui/input.vue'; +import MkTextarea from '../../components/ui/textarea.vue'; +import MkSwitch from '../../components/ui/switch.vue'; +import i18n from '../../i18n'; +import { apiUrl, host } from '../../config'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + MkInput, + MkTextarea, + MkSwitch, + }, + + data() { + return { + host, + name: null, + description: null, + birthday: null, + location: null, + fieldName0: null, + fieldValue0: null, + fieldName1: null, + fieldValue1: null, + fieldName2: null, + fieldValue2: null, + fieldName3: null, + fieldValue3: null, + avatarId: null, + bannerId: null, + isBot: false, + isCat: false, + saving: false, + avatarUploading: false, + bannerUploading: false, + faSave, faUnlockAlt, faCogs, faImage, faUser, faMapMarkerAlt, faBirthdayCake + } + }, + + created() { + this.name = this.$store.state.i.name; + this.description = this.$store.state.i.description; + this.location = this.$store.state.i.location; + this.birthday = this.$store.state.i.birthday; + this.avatarId = this.$store.state.i.avatarId; + this.bannerId = this.$store.state.i.bannerId; + this.isBot = this.$store.state.i.isBot; + this.isCat = this.$store.state.i.isCat; + + this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null; + this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null; + this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null; + this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null; + this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null; + this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null; + this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null; + this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null; + }, + + methods: { + onAvatarChange([file]) { + this.avatarUploading = true; + + const data = new FormData(); + data.append('file', file); + data.append('i', this.$store.state.i.token); + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.avatarId = f.id; + this.avatarUploading = false; + }) + .catch(e => { + this.avatarUploading = false; + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + onBannerChange([file]) { + this.bannerUploading = true; + + const data = new FormData(); + data.append('file', file); + data.append('i', this.$store.state.i.token); + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: data + }) + .then(response => response.json()) + .then(f => { + this.bannerId = f.id; + this.bannerUploading = false; + }) + .catch(e => { + this.bannerUploading = false; + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + save(notify) { + const fields = [ + { name: this.fieldName0, value: this.fieldValue0 }, + { name: this.fieldName1, value: this.fieldValue1 }, + { name: this.fieldName2, value: this.fieldValue2 }, + { name: this.fieldName3, value: this.fieldValue3 }, + ]; + + this.saving = true; + + this.$root.api('i/update', { + name: this.name || null, + description: this.description || null, + location: this.location || null, + birthday: this.birthday || null, + avatarId: this.avatarId || undefined, + bannerId: this.bannerId || undefined, + fields, + isBot: !!this.isBot, + isCat: !!this.isCat, + }).then(i => { + this.saving = false; + this.$store.state.i.avatarId = i.avatarId; + this.$store.state.i.avatarUrl = i.avatarUrl; + this.$store.state.i.bannerId = i.bannerId; + this.$store.state.i.bannerUrl = i.bannerUrl; + + if (notify) { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + } + }).catch(err => { + this.saving = false; + this.$root.dialog({ + type: 'error', + text: err.id + }); + }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-settings-page-profile { + > ._content { + > *:first-child { + margin-top: 0; + } + + > .fields { + > .row { + > * { + display: inline-block; + width: 50%; + margin-bottom: 0; + } + } + } + } +} +</style> diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue new file mode 100644 index 0000000000000000000000000000000000000000..310237b5fdd26e3cb010ed8f3fd164421bf2faec --- /dev/null +++ b/src/client/pages/settings/reaction.vue @@ -0,0 +1,62 @@ +<template> +<section class="mk-settings-page-reaction _section"> + <div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div> + <div class="_content"> + <mk-textarea v-model="reactions" style="margin-top: 16px;">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea> + </div> + <div class="_footer"> + <mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + <mk-button inline @click="preview"><fa :icon="faEye"/> {{ $t('preview') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons'; +import MkTextarea from '../../components/ui/textarea.vue'; +import MkButton from '../../components/ui/button.vue'; +import MkReactionPicker from '../../components/reaction-picker.vue'; +import i18n from '../../i18n'; + +export default Vue.extend({ + i18n, + + components: { + MkTextarea, + MkButton, + }, + + data() { + return { + reactions: this.$store.state.settings.reactions.join('\n'), + changed: false, + faLaugh, faSave, faEye + } + }, + + watch: { + reactions() { + this.changed = true; + } + }, + + methods: { + save() { + this.$store.dispatch('settings/set', { key: 'reactions', value: this.reactions.trim().split('\n') }); + this.changed = false; + }, + + preview(ev) { + const picker = this.$root.new(MkReactionPicker, { + source: ev.currentTarget || ev.target, + reactions: this.reactions.trim().split('\n'), + showFocus: false, + }); + picker.$once('chosen', reaction => { + picker.close(); + }); + } + } +}); +</script> diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue new file mode 100644 index 0000000000000000000000000000000000000000..ecf9c01dd5b70d34548f0aad56dfaa29a6c1e81e --- /dev/null +++ b/src/client/pages/settings/security.vue @@ -0,0 +1,87 @@ +<template> +<section class="_section"> + <div class="_title"><fa :icon="faLock"/> {{ $t('password') }}</div> + <div class="_content"> + <mk-button primary @click="change()">{{ $t('changePassword') }}</mk-button> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faLock } from '@fortawesome/free-solid-svg-icons'; +import MkButton from '../../components/ui/button.vue'; +import i18n from '../../i18n'; + +export default Vue.extend({ + i18n, + + components: { + MkButton, + }, + + data() { + return { + faLock + } + }, + + methods: { + async change() { + const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({ + title: this.$t('currentPassword'), + input: { + type: 'password' + } + }); + if (canceled1) return; + + const { canceled: canceled2, result: newPassword } = await this.$root.dialog({ + title: this.$t('newPassword'), + input: { + type: 'password' + } + }); + if (canceled2) return; + + const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({ + title: this.$t('newPasswordRetype'), + input: { + type: 'password' + } + }); + if (canceled3) return; + + if (newPassword !== newPassword2) { + this.$root.dialog({ + type: 'error', + text: this.$t('retypedNotMatch') + }); + return; + } + + const dialog = this.$root.dialog({ + type: 'waiting', + iconOnly: true + }); + + this.$root.api('i/change-password', { + currentPassword, + newPassword + }).then(() => { + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }).finally(() => { + dialog.close(); + }); + } + } +}); +</script> diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue new file mode 100644 index 0000000000000000000000000000000000000000..71628ab2b9c88adc4137ac19a51b919a4c305f98 --- /dev/null +++ b/src/client/pages/settings/theme.vue @@ -0,0 +1,76 @@ +<template> +<section class="mk-settings-page-theme _section"> + <div class="_title"><fa :icon="faPalette"/> {{ $t('theme') }}</div> + <div class="_content"> + <mk-select v-model="theme" :placeholder="$t('theme')"> + <template #label>{{ $t('theme') }}</template> + <optgroup :label="$t('lightThemes')"> + <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="$t('darkThemes')"> + <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + </mk-select> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faPalette } from '@fortawesome/free-solid-svg-icons'; +import MkInput from '../../components/ui/input.vue'; +import MkButton from '../../components/ui/button.vue'; +import MkSelect from '../../components/ui/select.vue'; +import i18n from '../../i18n'; +import { Theme, builtinThemes, applyTheme } from '../../theme'; + +export default Vue.extend({ + i18n, + + components: { + MkInput, + MkButton, + MkSelect, + }, + + data() { + return { + wallpaperUploading: false, + faPalette + } + }, + + computed: { + themes(): Theme[] { + return builtinThemes.concat(this.$store.state.device.themes); + }, + + installedThemes(): Theme[] { + return this.$store.state.device.themes; + }, + + darkThemes(): Theme[] { + return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark'); + }, + + lightThemes(): Theme[] { + return this.themes.filter(t => t.base == 'light' || t.kind == 'light'); + }, + + theme: { + get() { return this.$store.state.device.theme; }, + set(value) { this.$store.commit('device/set', { key: 'theme', value }); } + }, + }, + + watch: { + theme() { + applyTheme(this.themes.find(x => x.id === this.theme)); + } + }, + + methods: { + + } +}); +</script> diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue new file mode 100644 index 0000000000000000000000000000000000000000..f53f3c5ca174c1da93139015cbc4f36618be5ec6 --- /dev/null +++ b/src/client/pages/tag.vue @@ -0,0 +1,49 @@ +<template> +<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../scripts/loading'; +import XNotes from '../components/notes.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: '#' + this.$route.params.tag + }; + }, + + components: { + XNotes + }, + + data() { + return { + pagination: { + endpoint: 'notes/search-by-tag', + limit: 10, + params: () => ({ + tag: this.$route.params.tag, + }) + } + }; + }, + + watch: { + $route() { + (this.$refs.notes as any).reload(); + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + } + } +}); +</script> diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue new file mode 100644 index 0000000000000000000000000000000000000000..faaee3b107391c23a15fc5b4e5881f29db1c5486 --- /dev/null +++ b/src/client/pages/user/follow-list.vue @@ -0,0 +1,140 @@ +<template> +<mk-pagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list"> + <div class="user _panel" v-for="(user, i) in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :data-index="i"> + <mk-avatar class="avatar" :user="user"/> + <div class="body"> + <div class="name"> + <router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link> + <p class="acct">@{{ user | acct }}</p> + </div> + <div class="description" v-if="user.description" :title="user.description"> + <mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/> + </div> + <x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/> + </div> + </div> +</mk-pagination> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parseAcct from '../../../misc/acct/parse'; +import i18n from '../../i18n'; +import XFollowButton from '../../components/follow-button.vue'; +import MkPagination from '../../components/ui/pagination.vue'; + +export default Vue.extend({ + i18n, + + components: { + MkPagination, + XFollowButton, + }, + + props: { + type: { + type: String, + required: true + } + }, + + data() { + return { + pagination: { + endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers', + limit: 20, + params: { + ...parseAcct(this.$route.params.user), + } + }, + }; + }, + + watch: { + type() { + this.$refs.list.reload(); + }, + + '$route'() { + this.$refs.list.reload(); + } + } +}); +</script> + +<style lang="scss" scoped> +.mk-following-or-followers { + > .user { + display: flex; + padding: 16px; + + > .avatar { + display: block; + flex-shrink: 0; + margin: 0 12px 0 0; + width: 42px; + height: 42px; + border-radius: 8px; + } + + > .body { + display: flex; + width: calc(100% - 54px); + position: relative; + + > .name { + width: 45%; + + @media (max-width: 500px) { + width: 100%; + } + + > .name, + > .acct { + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0; + } + + > .name { + font-size: 16px; + line-height: 24px; + } + + > .acct { + font-size: 15px; + line-height: 16px; + opacity: 0.7; + } + } + + > .description { + width: 55%; + line-height: 42px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.7; + font-size: 14px; + padding-right: 40px; + padding-left: 8px; + box-sizing: border-box; + + @media (max-width: 500px) { + display: none; + } + } + + > .koudoku-button { + position: absolute; + top: 0; + bottom: 0; + right: 0; + margin: auto 0; + } + } + } +} +</style> diff --git a/src/client/app/common/views/components/activity.vue b/src/client/pages/user/index.activity.vue similarity index 97% rename from src/client/app/common/views/components/activity.vue rename to src/client/pages/user/index.activity.vue index a958616943f220c956e28aa37a2b813a3a804e05..29dcca066448cf099473bb6939bef73d4ede3d5a 100644 --- a/src/client/app/common/views/components/activity.vue +++ b/src/client/pages/user/index.activity.vue @@ -17,7 +17,7 @@ export default Vue.extend({ limit: { type: Number, required: false, - default: 21 + default: 40 } }, data() { @@ -69,7 +69,7 @@ export default Vue.extend({ }, plotOptions: { bar: { - columnWidth: '80%' + columnWidth: '40%' } }, dataLabels: { diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/pages/user/index.photos.vue similarity index 59% rename from src/client/app/mobile/views/pages/user/home.photos.vue rename to src/client/pages/user/index.photos.vue index b5547c916f834a4d8bb0c15d4155353cb2c1a2b1..cd29254f483ca5a793fd365e624bb4151799c4db 100644 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ b/src/client/pages/user/index.photos.vue @@ -1,6 +1,6 @@ <template> -<div class="root photos"> - <p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> +<div class="ujigsodd"> + <mk-loading v-if="fetching"/> <div class="stream" v-if="!fetching && images.length > 0"> <a v-for="(image, i) in images" :key="i" class="img" @@ -14,11 +14,11 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; +import i18n from '../../i18n'; +import { getStaticImageUrl } from '../../scripts/get-static-image-url'; export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.photos.vue'), + i18n, props: ['user'], data() { return { @@ -63,37 +63,36 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.root.photos +<style lang="scss" scoped> +.ujigsodd { - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px + > .stream { + display: flex; + justify-content: center; + flex-wrap: wrap; + padding: 8px; - > .img - flex 1 1 33% - width 33% - height 90px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - border-radius 4px + > .img { + flex: 1 1 33%; + width: 33%; + height: 90px; + box-sizing: border-box; + 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 var(--text) - - > i - margin-right 4px + > .empty { + margin: 0; + padding: 16px; + text-align: center; + > i { + margin-right: 4px; + } + } +} </style> - diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..1878a9b1f38c1d2a04e5d480bf5d6e7a345633e2 --- /dev/null +++ b/src/client/pages/user/index.timeline.vue @@ -0,0 +1,79 @@ +<template> +<div class="kjeftjfm"> + <div class="with"> + <button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button> + <button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button> + <button class="_button" @click="with_ = 'files'" :class="{ active: with_ === 'files' }">{{ $t('withFiles') }}</button> + </div> + <x-notes ref="timeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNotes from '../../components/notes.vue'; + +export default Vue.extend({ + components: { + XNotes + }, + + props: { + user: { + type: Object, + required: true, + }, + }, + + watch: { + user() { + this.$refs.timeline.reload(); + }, + + with_() { + this.$refs.timeline.reload(); + }, + }, + + data() { + return { + date: null, + with_: null, + pagination: { + endpoint: 'users/notes', + limit: 10, + params: init => ({ + userId: this.user.id, + includeReplies: this.with_ === 'replies', + withFiles: this.with_ === 'files', + untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + }) + } + }; + }, +}); +</script> + +<style lang="scss" scoped> +.kjeftjfm { + > .with { + display: flex; + margin-bottom: var(--margin); + + @media (max-width: 500px) { + font-size: 80%; + } + + > button { + flex: 1; + padding: 11px 8px 8px 8px; + border-bottom: solid 3px transparent; + + &.active { + color: var(--accent); + border-bottom-color: var(--accent); + } + } + } +} +</style> diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..7bf45621b38022f2805342d5b5785caa73657e8f --- /dev/null +++ b/src/client/pages/user/index.vue @@ -0,0 +1,476 @@ +<template> +<div class="mk-user-page" v-if="user"> + <portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal> + <portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> + + <div class="remote-caution _panel" v-if="user.host != null"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div> + <transition name="zoom" mode="out-in" appear> + <div class="profile _panel" :key="user.id"> + <div class="banner-container" :style="style"> + <div class="banner" ref="banner" :style="style"></div> + <div class="fade"></div> + <div class="title"> + <mk-user-name class="name" :user="user" :nowrap="true"/> + <div class="bottom"> + <span class="username"><mk-acct :user="user" :detail="true" /></span> + <span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span> + <span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span> + <span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span> + </div> + </div> + <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span> + <div class="actions" v-if="$store.getters.isSignedIn"> + <button @click="menu" class="menu _button" ref="menu"><fa :icon="faEllipsisH"/></button> + <x-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" class="koudoku"/> + </div> + </div> + <mk-avatar class="avatar" :user="user" :disable-preview="true"/> + <div class="title"> + <mk-user-name :user="user" :nowrap="false" class="name"/> + <div class="bottom"> + <span class="username"><mk-acct :user="user" :detail="true" /></span> + <span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span> + <span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span> + <span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span> + </div> + </div> + <div class="description"> + <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> + <p v-else class="empty">{{ $t('noAccountDescription') }}</p> + </div> + <div class="fields system"> + <dl class="field" v-if="user.location"> + <dt class="name"><fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt> + <dd class="value">{{ user.location }}</dd> + </dl> + <dl class="field" v-if="user.birthday"> + <dt class="name"><fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt> + <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> + </dl> + <dl class="field"> + <dt class="name"><fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt> + <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<mk-time :time="user.createdAt"/>)</dd> + </dl> + </div> + <div class="fields" v-if="user.fields.length > 0"> + <dl class="field" v-for="(field, i) in user.fields" :key="i"> + <dt class="name"> + <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> + </dt> + <dd class="value"> + <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/> + </dd> + </dl> + </div> + <div class="status" v-if="user.host === null"> + <router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }"> + <b>{{ user.notesCount | number }}</b> + <span>{{ $t('notes') }}</span> + </router-link> + <router-link :to="user | userPage('following')" :class="{ active: $route.name === 'userFollowing' }"> + <b>{{ user.followingCount | number }}</b> + <span>{{ $t('following') }}</span> + </router-link> + <router-link :to="user | userPage('followers')" :class="{ active: $route.name === 'userFollowers' }"> + <b>{{ user.followersCount | number }}</b> + <span>{{ $t('followers') }}</span> + </router-link> + </div> + </div> + </transition> + <router-view :user="user"></router-view> + <template v-if="$route.name == 'user'"> + <sequential-entrance class="pins"> + <x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :data-index="i" :detail="true" :pinned="true"/> + </sequential-entrance> + <mk-container :body-togglable="true" class="content"> + <template #header><fa :icon="faImage"/>{{ $t('images') }}</template> + <div> + <x-photos :user="user" :key="user.id"/> + </div> + </mk-container> + <mk-container :body-togglable="true" class="content"> + <template #header><fa :icon="faChartBar"/>{{ $t('activity') }}</template> + <div style="padding:8px;"> + <x-activity :user="user" :key="user.id"/> + </div> + </mk-container> + <x-user-timeline :user="user"/> + </template> +</div> +<div v-else-if="error"> + <mk-error @retry="fetch()"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons'; +import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'; +import * as age from 's-age'; +import XUserTimeline from './index.timeline.vue'; +import XUserMenu from '../../components/user-menu.vue'; +import XNote from '../../components/note.vue'; +import XFollowButton from '../../components/follow-button.vue'; +import MkContainer from '../../components/ui/container.vue'; +import Progress from '../../scripts/loading'; +import parseAcct from '../../../misc/acct/parse'; + +export default Vue.extend({ + components: { + XUserTimeline, + XNote, + XFollowButton, + MkContainer, + XPhotos: () => import('./index.photos.vue').then(m => m.default), + XActivity: () => import('./index.activity.vue').then(m => m.default), + }, + + metaInfo() { + return { + title: (this.user ? '@' + Vue.filter('acct')(this.user).replace('@', ' | ') : null) as string + }; + }, + + data() { + return { + user: null, + error: null, + parallaxAnimationId: null, + faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt + }; + }, + + computed: { + style(): any { + if (this.user.bannerUrl == null) return {}; + return { + backgroundImage: `url(${ this.user.bannerUrl })` + }; + }, + + age(): number { + return age(this.user.birthday); + } + }, + + watch: { + $route: 'fetch' + }, + + created() { + this.fetch(); + }, + + mounted() { + window.requestAnimationFrame(this.parallaxLoop); + window.addEventListener('scroll', this.parallax, { passive: true }); + document.addEventListener('touchmove', this.parallax, { passive: true }); + this.$once('hook:beforeDestroy', () => { + window.cancelAnimationFrame(this.parallaxAnimationId); + window.removeEventListener('scroll', this.parallax); + document.removeEventListener('touchmove', this.parallax); + }); + }, + + methods: { + fetch() { + Progress.start(); + this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + }).catch(e => { + this.error = e; + }).finally(() => { + Progress.done(); + }); + }, + + menu() { + this.$root.new(XUserMenu, { + source: this.$refs.menu, + user: this.user + }); + }, + + parallaxLoop() { + this.parallaxAnimationId = window.requestAnimationFrame(this.parallaxLoop); + this.parallax(); + }, + + parallax() { + const banner = this.$refs.banner as any; + if (banner == null) return; + + const top = window.scrollY; + + if (top < 0) return; + + const z = 1.75; // 奥行ã(å°ã•ã„ã»ã©å¥¥) + const pos = -(top / z); + banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; + }, + } +}); +</script> + +<style lang="scss" scoped> +.mk-user-page { + > .remote-caution { + font-size: 0.8em; + padding: 16px; + margin-bottom: var(--margin); + + > a { + margin-left: 4px; + color: var(--accent); + } + } + + > .profile { + position: relative; + margin-bottom: var(--margin); + overflow: hidden; + + > .banner-container { + position: relative; + height: 250px; + overflow: hidden; + background-size: cover; + background-position: center; + + @media (max-width: 500px) { + height: 140px; + } + + > .banner { + height: 100%; + background-color: #4c5e6d; + background-size: cover; + background-position: center; + box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; + } + + > .fade { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 78px; + background: linear-gradient(transparent, rgba(#000, 0.7)); + + @media (max-width: 500px) { + display: none; + } + } + + > .followed { + position: absolute; + top: 12px; + left: 12px; + padding: 4px 6px; + color: #fff; + background: rgba(0, 0, 0, 0.7); + font-size: 12px; + } + + > .actions { + position: absolute; + top: 12px; + right: 12px; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + background: rgba(0, 0, 0, 0.2); + padding: 8px; + border-radius: 24px; + + > .menu { + vertical-align: bottom; + height: 31px; + width: 31px; + color: #fff; + text-shadow: 0 0 8px #000; + font-size: 16px; + } + + > .koudoku { + margin-left: 4px; + vertical-align: bottom; + } + } + + > .title { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 0 0 8px 154px; + box-sizing: border-box; + color: #fff; + + @media (max-width: 500px) { + display: none; + } + + > .name { + display: block; + margin: 0; + line-height: 32px; + font-weight: bold; + font-size: 1.8em; + text-shadow: 0 0 8px #000; + } + + > .bottom { + > * { + display: inline-block; + margin-right: 16px; + line-height: 20px; + opacity: 0.8; + + &.username { + font-weight: bold; + } + } + } + } + } + + > .title { + display: none; + text-align: center; + padding: 50px 8px 16px 8px; + font-weight: bold; + border-bottom: solid 1px var(--divider); + + @media (max-width: 500px) { + display: block; + } + + > .bottom { + > * { + display: inline-block; + margin-right: 8px; + opacity: 0.8; + } + } + } + + > .avatar { + display: block; + position: absolute; + top: 170px; + left: 16px; + z-index: 2; + width: 120px; + height: 120px; + box-shadow: 1px 1px 3px rgba(#000, 0.2); + + @media (max-width: 500px) { + top: 90px; + left: 0; + right: 0; + width: 92px; + height: 92px; + margin: auto; + } + } + + > .description { + padding: 24px 24px 24px 154px; + font-size: 15px; + + @media (max-width: 500px) { + padding: 16px; + text-align: center; + } + + > .empty { + margin: 0; + opacity: 0.5; + } + } + + > .fields { + padding: 24px; + font-size: 14px; + border-top: solid 1px var(--divider); + + @media (max-width: 500px) { + padding: 16px; + } + + > .field { + display: flex; + padding: 0; + margin: 0; + align-items: center; + + &:not(:last-child) { + margin-bottom: 8px; + } + + > .name { + width: 30%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: bold; + text-align: center; + } + + > .value { + width: 70%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + &.system > .field > .name { + } + } + + > .status { + display: flex; + padding: 24px; + border-top: solid 1px var(--divider); + + @media (max-width: 500px) { + padding: 16px; + } + + > a { + flex: 1; + text-align: center; + + &.active { + color: var(--accent); + } + + &:hover { + text-decoration: none; + } + + > b { + display: block; + line-height: 16px; + } + + > span { + font-size: 70%; + } + } + } + } + + > .pins { + > .note { + margin-bottom: var(--margin); + } + } + + > .content { + margin-bottom: var(--margin); + } +} +</style> diff --git a/src/client/router.ts b/src/client/router.ts new file mode 100644 index 0000000000000000000000000000000000000000..7eb12c8e44f11103684fe8241c9e756bb163b960 --- /dev/null +++ b/src/client/router.ts @@ -0,0 +1,53 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import MkIndex from './pages/index.vue'; + +Vue.use(VueRouter); + +export const router = new VueRouter({ + mode: 'history', + routes: [ + { path: '/', name: 'index', component: MkIndex }, + { path: '/@:user', name: 'user', component: () => import('./pages/user/index.vue').then(m => m.default), children: [ + { path: 'following', name: 'userFollowing', component: () => import('./pages/user/follow-list.vue').then(m => m.default), props: { type: 'following' } }, + { path: 'followers', name: 'userFollowers', component: () => import('./pages/user/follow-list.vue').then(m => m.default), props: { type: 'followers' } }, + ]}, + { path: '/@:user/pages/:page', component: () => import('./pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) }, + { path: '/@:user/pages/:pageName/view-source', component: () => import('./pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, + { path: '/announcements', component: () => import('./pages/announcements.vue').then(m => m.default) }, + { path: '/about', component: () => import('./pages/about.vue').then(m => m.default) }, + { path: '/featured', component: () => import('./pages/featured.vue').then(m => m.default) }, + { path: '/explore', component: () => import('./pages/explore.vue').then(m => m.default) }, + { path: '/explore/tags/:tag', props: true, component: () => import('./pages/explore.vue').then(m => m.default) }, + { path: '/search', component: () => import('./pages/search.vue').then(m => m.default) }, + { path: '/my/favorites', component: () => import('./pages/favorites.vue').then(m => m.default) }, + { path: '/my/messages', component: () => import('./pages/messages.vue').then(m => m.default) }, + { path: '/my/mentions', component: () => import('./pages/mentions.vue').then(m => m.default) }, + { path: '/my/messaging', name: 'messaging', component: () => import('./pages/messaging.vue').then(m => m.default) }, + { path: '/my/messaging/:user', component: () => import('./pages/messaging-room.vue').then(m => m.default) }, + { path: '/my/drive', name: 'drive', component: () => import('./pages/drive.vue').then(m => m.default) }, + { path: '/my/drive/folder/:folder', component: () => import('./pages/drive.vue').then(m => m.default) }, + { path: '/my/pages', name: 'pages', component: () => import('./pages/pages.vue').then(m => m.default) }, + { path: '/my/pages/new', component: () => import('./pages/page-editor/page-editor.vue').then(m => m.default) }, + { path: '/my/pages/edit/:pageId', component: () => import('./pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, + { path: '/my/settings', component: () => import('./pages/settings/index.vue').then(m => m.default) }, + { path: '/my/follow-requests', component: () => import('./pages/follow-requests.vue').then(m => m.default) }, + { path: '/my/lists', component: () => import('./pages/my-lists/index.vue').then(m => m.default) }, + { path: '/my/lists/:list', component: () => import('./pages/my-lists/list.vue').then(m => m.default) }, + { path: '/my/antennas', component: () => import('./pages/my-antennas/index.vue').then(m => m.default) }, + { path: '/instance', component: () => import('./pages/instance/index.vue').then(m => m.default) }, + { path: '/instance/emojis', component: () => import('./pages/instance/emojis.vue').then(m => m.default) }, + { path: '/instance/users', component: () => import('./pages/instance/users.vue').then(m => m.default) }, + { path: '/instance/files', component: () => import('./pages/instance/files.vue').then(m => m.default) }, + { path: '/instance/monitor', component: () => import('./pages/instance/monitor.vue').then(m => m.default) }, + { path: '/instance/queue', component: () => import('./pages/instance/queue.vue').then(m => m.default) }, + { path: '/instance/stats', component: () => import('./pages/instance/stats.vue').then(m => m.default) }, + { path: '/instance/federation', component: () => import('./pages/instance/federation.vue').then(m => m.default) }, + { path: '/instance/announcements', component: () => import('./pages/instance/announcements.vue').then(m => m.default) }, + { path: '/notes/:note', name: 'note', component: () => import('./pages/note.vue').then(m => m.default) }, + { path: '/tags/:tag', component: () => import('./pages/tag.vue').then(m => m.default) }, + { path: '/auth/:token', component: () => import('./pages/auth.vue').then(m => m.default) }, + { path: '/authorize-follow', component: () => import('./pages/follow.vue').then(m => m.default) }, + /*{ path: '*', component: MkNotFound }*/ + ] +}); diff --git a/src/client/app/common/scripts/2fa.ts b/src/client/scripts/2fa.ts similarity index 64% rename from src/client/app/common/scripts/2fa.ts rename to src/client/scripts/2fa.ts index f638cce156e2bcf7f5b01e13b09abfc8936588f5..e431361aac5005b3b44f0f43180f6dff0c6cc490 100644 --- a/src/client/app/common/scripts/2fa.ts +++ b/src/client/scripts/2fa.ts @@ -1,5 +1,5 @@ export function hexifyAB(buffer) { return Array.from(new Uint8Array(buffer)) - .map(item => item.toString(16).padStart(2, 0)) + .map(item => item.toString(16).padStart(2, '0')) .join(''); } diff --git a/src/misc/aiscript/evaluator.ts b/src/client/scripts/aiscript/evaluator.ts similarity index 99% rename from src/misc/aiscript/evaluator.ts rename to src/client/scripts/aiscript/evaluator.ts index c53cef75b607935b7dc137f46b71cf5040601fd5..cc1adf4499fc0dd0e49ac0e8c37c3da1588dd86f 100644 --- a/src/misc/aiscript/evaluator.ts +++ b/src/client/scripts/aiscript/evaluator.ts @@ -1,6 +1,7 @@ import autobind from 'autobind-decorator'; import * as seedrandom from 'seedrandom'; import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.'; +import { version } from '../../config'; type Fn = { slots: string[]; @@ -16,7 +17,7 @@ export class ASEvaluator { private envVars: Record<keyof typeof envVarsDef, any>; private opts: { - randomSeed: string; user?: any; visitor?: any; page?: any; url?: string; version: string; + randomSeed: string; user?: any; visitor?: any; page?: any; url?: string; }; constructor(variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) { @@ -28,7 +29,7 @@ export class ASEvaluator { this.envVars = { AI: 'kawaii', - VERSION: opts.version, + VERSION: version, URL: opts.page ? `${opts.url}/@${opts.page.user.username}/pages/${opts.page.name}` : '', LOGIN: opts.visitor != null, NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '', diff --git a/src/misc/aiscript/index.ts b/src/client/scripts/aiscript/index.ts similarity index 100% rename from src/misc/aiscript/index.ts rename to src/client/scripts/aiscript/index.ts diff --git a/src/misc/aiscript/type-checker.ts b/src/client/scripts/aiscript/type-checker.ts similarity index 100% rename from src/misc/aiscript/type-checker.ts rename to src/client/scripts/aiscript/type-checker.ts diff --git a/src/client/app/common/scripts/collect-page-vars.ts b/src/client/scripts/collect-page-vars.ts similarity index 100% rename from src/client/app/common/scripts/collect-page-vars.ts rename to src/client/scripts/collect-page-vars.ts diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/scripts/compose-notification.ts similarity index 60% rename from src/client/app/common/scripts/compose-notification.ts rename to src/client/scripts/compose-notification.ts index ec854f2f4d7463ae49b8cf98b8473cc5d44cebcf..bf3255250676b94415f14ccf4b14dd45dbc63a29 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/scripts/compose-notification.ts @@ -1,6 +1,5 @@ -import getNoteSummary from '../../../../misc/get-note-summary'; -import getReactionEmoji from '../../../../misc/get-reaction-emoji'; -import getUserName from '../../../../misc/get-user-name'; +import getNoteSummary from '../../misc/get-note-summary'; +import getUserName from '../../misc/get-user-name'; type Notification = { title: string; @@ -20,20 +19,6 @@ export default function(type, data): Notification { icon: data.url }; - case 'unreadMessagingMessage': - return { - title: `New message from ${getUserName(data.user)}`, - body: data.text, // TODO: getMessagingMessageSummary(data), - icon: data.user.avatarUrl - }; - - case 'reversiInvited': - return { - title: 'Play reversi with me', - body: `You got reversi invitation from ${getUserName(data.parent)}`, - icon: data.parent.avatarUrl - }; - case 'notification': switch (data.type) { case 'mention': @@ -59,7 +44,7 @@ export default function(type, data): Notification { case 'reaction': return { - title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, + title: `${getUserName(data.user)}: ${data.reaction}:`, body: getNoteSummary(data.note), icon: data.user.avatarUrl }; diff --git a/src/client/app/common/scripts/contains.ts b/src/client/scripts/contains.ts similarity index 55% rename from src/client/app/common/scripts/contains.ts rename to src/client/scripts/contains.ts index a5071b3f250a093e6d6493a20a1ba3b1a5d066ef..770bda63bb051ffb0c843d8873bf5818a6fb67f1 100644 --- a/src/client/app/common/scripts/contains.ts +++ b/src/client/scripts/contains.ts @@ -1,4 +1,5 @@ -export default (parent, child) => { +export default (parent, child, checkSame = true) => { + if (checkSame && parent === child) return true; let node = child.parentNode; while (node) { if (node == parent) return true; diff --git a/src/client/app/common/scripts/copy-to-clipboard.ts b/src/client/scripts/copy-to-clipboard.ts similarity index 100% rename from src/client/app/common/scripts/copy-to-clipboard.ts rename to src/client/scripts/copy-to-clipboard.ts diff --git a/src/client/app/common/scripts/gen-search-query.ts b/src/client/scripts/gen-search-query.ts similarity index 86% rename from src/client/app/common/scripts/gen-search-query.ts rename to src/client/scripts/gen-search-query.ts index fc26cb7f787f22bd194515b9bae7f6b0662cf101..2520da75df7fc501955eb24b2e6ea74f92b54dc0 100644 --- a/src/client/app/common/scripts/gen-search-query.ts +++ b/src/client/scripts/gen-search-query.ts @@ -1,5 +1,5 @@ -import parseAcct from '../../../../misc/acct/parse'; -import { host as localHost } from '../../config'; +import parseAcct from '../../misc/acct/parse'; +import { host as localHost } from '../config'; export async function genSearchQuery(v: any, q: string) { let host: string; diff --git a/src/client/scripts/get-instance-name.ts b/src/client/scripts/get-instance-name.ts new file mode 100644 index 0000000000000000000000000000000000000000..b12a3a4c67efee7de59736d1ede940785f81fbfd --- /dev/null +++ b/src/client/scripts/get-instance-name.ts @@ -0,0 +1,8 @@ +export function getInstanceName() { + const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement; + if (siteName && siteName.content) { + return siteName.content; + } + + return 'Misskey'; +} diff --git a/src/client/app/common/scripts/get-md5.ts b/src/client/scripts/get-md5.ts similarity index 100% rename from src/client/app/common/scripts/get-md5.ts rename to src/client/scripts/get-md5.ts diff --git a/src/client/app/common/scripts/get-static-image-url.ts b/src/client/scripts/get-static-image-url.ts similarity index 75% rename from src/client/app/common/scripts/get-static-image-url.ts rename to src/client/scripts/get-static-image-url.ts index 7460ca38f2f8066b1e7a469f18a4c63b5257c0dd..eff76af25637a6e1f6bf049d731dd1b5d3b02201 100644 --- a/src/client/app/common/scripts/get-static-image-url.ts +++ b/src/client/scripts/get-static-image-url.ts @@ -1,5 +1,5 @@ -import { url as instanceUrl } from '../../config'; -import * as url from '../../../../prelude/url'; +import { url as instanceUrl } from '../config'; +import * as url from '../../prelude/url'; export function getStaticImageUrl(baseUrl: string): string { const u = new URL(baseUrl); diff --git a/src/client/app/common/hotkey.ts b/src/client/scripts/hotkey.ts similarity index 98% rename from src/client/app/common/hotkey.ts rename to src/client/scripts/hotkey.ts index a53d3f479e32ff0ae63e125279fd47bd5a175517..ec627ab15b68299d91e0500be035f68835a8d542 100644 --- a/src/client/app/common/hotkey.ts +++ b/src/client/scripts/hotkey.ts @@ -1,5 +1,5 @@ import keyCode from './keycode'; -import { concat } from '../../../prelude/array'; +import { concat } from '../../prelude/array'; type pattern = { which: string[]; diff --git a/src/client/app/common/keycode.ts b/src/client/scripts/keycode.ts similarity index 100% rename from src/client/app/common/keycode.ts rename to src/client/scripts/keycode.ts diff --git a/src/client/app/common/scripts/loading.ts b/src/client/scripts/loading.ts similarity index 100% rename from src/client/app/common/scripts/loading.ts rename to src/client/scripts/loading.ts diff --git a/src/client/app/common/scripts/paging.ts b/src/client/scripts/paging.ts similarity index 52% rename from src/client/app/common/scripts/paging.ts rename to src/client/scripts/paging.ts index b4f2ec1ae1d86ca9f4241ca38c8f81c177b6bf78..b24d705f1589cb51d351d09f4a6eba5bd4150520 100644 --- a/src/client/app/common/scripts/paging.ts +++ b/src/client/scripts/paging.ts @@ -4,7 +4,6 @@ export default (opts) => ({ data() { return { items: [], - queue: [], offset: 0, fetching: true, moreFetching: false, @@ -20,14 +19,10 @@ export default (opts) => ({ error(): boolean { return !this.fetching && !this.inited; - } + }, }, watch: { - queue(x) { - if (opts.onQueueChanged) opts.onQueueChanged(this, x); - }, - pagination() { this.init(); } @@ -38,51 +33,31 @@ export default (opts) => ({ this.init(); }, - mounted() { - if (opts.captureWindowScroll) { - this.isScrollTop = () => { - return window.scrollY <= 8; - }; - - window.addEventListener('scroll', this.onScroll, { passive: true }); - } else if (opts.isContainer) { - this.isScrollTop = () => { - return this.$el.scrollTop <= 8; - }; - - this.$el.addEventListener('scroll', this.onScroll, { passive: true }); - } - }, - - beforeDestroy() { - if (opts.captureWindowScroll) { - window.removeEventListener('scroll', this.onScroll); - } else if (opts.isContainer) { - this.$el.removeEventListener('scroll', this.onScroll); - } - }, - methods: { + isScrollTop() { + return window.scrollY <= 8; + }, + updateItem(i, item) { Vue.set((this as any).items, i, item); }, reload() { - this.queue = []; this.items = []; this.init(); }, async init() { this.fetching = true; - if (opts.beforeInit) opts.beforeInit(this); + if (opts.before) opts.before(this); let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; if (params && params.then) params = await params; - await this.$root.api(this.pagination.endpoint, { - limit: (this.pagination.limit || 10) + 1, + const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; + await this.$root.api(endpoint, { + limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, ...params }).then(x => { - if (x.length == (this.pagination.limit || 10) + 1) { + if (!this.pagination.noPaging && (x.length === (this.pagination.limit || 10) + 1)) { x.pop(); this.items = x; this.more = true; @@ -93,10 +68,10 @@ export default (opts) => ({ this.offset = x.length; this.inited = true; this.fetching = false; - if (opts.onInited) opts.onInited(this); + if (opts.after) opts.after(this, null); }, e => { this.fetching = false; - if (opts.onInited) opts.onInited(this); + if (opts.after) opts.after(this, e); }); }, @@ -105,16 +80,17 @@ export default (opts) => ({ this.moreFetching = true; let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; if (params && params.then) params = await params; - await this.$root.api(this.pagination.endpoint, { + const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; + await this.$root.api(endpoint, { limit: (this.pagination.limit || 10) + 1, - ...(this.pagination.endpoint === 'notes/search' ? { + ...(this.pagination.offsetMode ? { offset: this.offset, } : { untilId: this.items[this.items.length - 1].id, }), ...params }).then(x => { - if (x.length == (this.pagination.limit || 10) + 1) { + if (x.length === (this.pagination.limit || 10) + 1) { x.pop(); this.items = this.items.concat(x); this.more = true; @@ -135,17 +111,15 @@ export default (opts) => ({ if (cancel) return; } - if (this.isScrollTop == null || this.isScrollTop()) { - // Prepend the item - this.items.unshift(item); + // Prepend the item + this.items.unshift(item); + if (this.isScrollTop()) { // オーãƒãƒ¼ãƒ•ãƒãƒ¼ã—ãŸã‚‰å¤ã„投稿ã¯æ¨ã¦ã‚‹ if (this.items.length >= opts.displayLimit) { this.items = this.items.slice(0, opts.displayLimit); this.more = true; } - } else { - this.queue.push(item); } }, @@ -153,37 +127,8 @@ export default (opts) => ({ this.items.push(item); }, - releaseQueue() { - for (const n of this.queue) { - this.prepend(n, true); - } - this.queue = []; - }, - - onScroll() { - if (this.isScrollTop()) { - this.onTop(); - } - - if (this.$store.state.settings.fetchOnScroll) { - // 親è¦ç´ ㌠display none ã ã£ãŸã‚‰å¼¾ã - // https://github.com/syuilo/misskey/issues/1569 - // http://d.hatena.ne.jp/favril/20091105/1257403319 - if (this.$el.offsetHeight == 0) return; - - const bottomPosition = opts.isContainer ? this.$el.scrollHeight : document.body.offsetHeight; - - const currentBottomPosition = opts.isContainer ? this.$el.scrollTop + this.$el.clientHeight : window.scrollY + window.innerHeight; - if (currentBottomPosition > (bottomPosition - 8)) this.onBottom(); - } - }, - - onTop() { - this.releaseQueue(); + remove(find) { + this.items = this.items.filter(x => !find(x)); }, - - onBottom() { - this.fetchMore(); - } } }); diff --git a/src/client/app/common/scripts/please-login.ts b/src/client/scripts/please-login.ts similarity index 100% rename from src/client/app/common/scripts/please-login.ts rename to src/client/scripts/please-login.ts diff --git a/src/client/app/common/scripts/search.ts b/src/client/scripts/search.ts similarity index 95% rename from src/client/app/common/scripts/search.ts rename to src/client/scripts/search.ts index 2897ed63181ccdd66010a29a3a51dd6b64acaa41..02dd39b035cd1ec696ee50ed594ad7add53e677a 100644 --- a/src/client/app/common/scripts/search.ts +++ b/src/client/scripts/search.ts @@ -28,7 +28,7 @@ export async function search(v: any, q: string) { v.$root.$emit('warp', date); v.$root.dialog({ icon: faHistory, - splash: true, + iconOnly: true, autoClose: true }); return; } @@ -36,7 +36,7 @@ export async function search(v: any, q: string) { if (q.startsWith('https://')) { const dialog = v.$root.dialog({ type: 'waiting', - text: v.$t('@.fetching-as-ap-object'), + text: v.$t('fetchingAsApObject') + '...', showOkButton: false, showCancelButton: false, cancelableByBgClick: false diff --git a/src/client/scripts/select-drive-file.ts b/src/client/scripts/select-drive-file.ts new file mode 100644 index 0000000000000000000000000000000000000000..004ddf2fd953fd0826217c3bc8707516755d3ed5 --- /dev/null +++ b/src/client/scripts/select-drive-file.ts @@ -0,0 +1,12 @@ +import DriveWindow from '../components/drive-window.vue'; + +export function selectDriveFile($root: any, multiple) { + return new Promise((res, rej) => { + const w = $root.new(DriveWindow, { + multiple + }); + w.$once('selected', files => { + res(multiple ? files : files[0]); + }); + }); +} diff --git a/src/client/app/common/scripts/stream.ts b/src/client/scripts/stream.ts similarity index 98% rename from src/client/app/common/scripts/stream.ts rename to src/client/scripts/stream.ts index a1b4223b55fbaf69e966040841571a4484cbdf37..7f0e1280b6b7eebca240de6be8d457b9d7985053 100644 --- a/src/client/app/common/scripts/stream.ts +++ b/src/client/scripts/stream.ts @@ -1,8 +1,8 @@ import autobind from 'autobind-decorator'; import { EventEmitter } from 'eventemitter3'; import ReconnectingWebsocket from 'reconnecting-websocket'; -import { wsUrl } from '../../config'; -import MiOS from '../../mios'; +import { wsUrl } from '../config'; +import MiOS from '../mios'; /** * Misskey stream connection diff --git a/src/client/store.ts b/src/client/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9f61b811275dd31ad075977433af705a3d51482 --- /dev/null +++ b/src/client/store.ts @@ -0,0 +1,193 @@ +import Vuex from 'vuex'; +import createPersistedState from 'vuex-persistedstate'; +import * as nestedProperty from 'nested-property'; + +import MiOS from './mios'; + +const defaultSettings = { + keepCw: false, + showFullAcct: false, + rememberNoteVisibility: false, + defaultNoteVisibility: 'public', + uploadFolder: null, + pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]', + wallpaper: null, + memo: null, + reactions: ['ðŸ‘', 'â¤ï¸', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', 'ðŸ®'], + widgets: [] +}; + +const defaultDeviceSettings = { + lang: null, + loadRawImages: false, + alwaysShowNsfw: false, + useOsDefaultEmojis: false, + accounts: [], + recentEmojis: [], + themes: [], + theme: 'light', +}; + +export default (os: MiOS) => new Vuex.Store({ + plugins: [createPersistedState({ + paths: ['i', 'device', 'settings'] + })], + + state: { + i: null, + }, + + getters: { + isSignedIn: state => state.i != null, + }, + + mutations: { + updateI(state, x) { + state.i = x; + }, + + updateIKeyValue(state, x) { + state.i[x.key] = x.value; + }, + }, + + actions: { + login(ctx, i) { + ctx.commit('updateI', i); + ctx.dispatch('settings/merge', i.clientData); + ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') }); + }, + + addAcount(ctx, info) { + if (!ctx.state.device.accounts.some(x => x.id === info.id)) { + ctx.commit('device/set', { + key: 'accounts', + value: ctx.state.device.accounts.concat([{ id: info.id, token: info.i }]) + }); + } + }, + + logout(ctx) { + ctx.commit('updateI', null); + localStorage.removeItem('i'); + }, + + switchAccount(ctx, i) { + ctx.commit('updateI', i); + ctx.commit('settings/init', i.clientData); + localStorage.setItem('i', i.token); + }, + + mergeMe(ctx, me) { + for (const [key, value] of Object.entries(me)) { + ctx.commit('updateIKeyValue', { key, value }); + } + + if (me.clientData) { + ctx.dispatch('settings/merge', me.clientData); + } + }, + }, + + modules: { + device: { + namespaced: true, + + state: defaultDeviceSettings, + + mutations: { + set(state, x: { key: string; value: any }) { + state[x.key] = x.value; + }, + + setTl(state, x) { + state.tl = { + src: x.src, + arg: x.arg + }; + }, + + setVisibility(state, visibility) { + state.visibility = visibility; + }, + } + }, + + settings: { + namespaced: true, + + state: defaultSettings, + + mutations: { + set(state, x: { key: string; value: any }) { + nestedProperty.set(state, x.key, x.value); + }, + + init(state, x) { + for (const [key, value] of Object.entries(defaultSettings)) { + if (x[key]) { + state[key] = x[key]; + } else { + state[key] = value; + } + } + }, + }, + + actions: { + merge(ctx, settings) { + if (settings == null) return; + for (const [key, value] of Object.entries(settings)) { + ctx.commit('set', { key, value }); + } + }, + + set(ctx, x) { + ctx.commit('set', x); + + if (ctx.rootGetters.isSignedIn) { + os.api('i/update-client-setting', { + name: x.key, + value: x.value + }); + } + }, + + setWidgets(ctx, widgets) { + ctx.state.widgets = widgets; + ctx.dispatch('updateWidgets'); + }, + + addWidget(ctx, widget) { + ctx.state.widgets.unshift(widget); + ctx.dispatch('updateWidgets'); + }, + + removeWidget(ctx, widget) { + ctx.state.widgets = ctx.state.widgets.filter(w => w.id != widget.id); + ctx.dispatch('updateWidgets'); + }, + + updateWidget(ctx, x) { + const w = ctx.state.widgets.find(w => w.id == x.id); + if (w) { + w.data = x.data; + ctx.dispatch('updateWidgets'); + } + }, + + updateWidgets(ctx) { + const widgets = ctx.state.widgets; + ctx.commit('set', { + key: 'widgets', + value: widgets + }); + os.api('i/update-client-setting', { + name: 'widgets', + value: widgets + }); + }, + } + } + } +}); diff --git a/src/client/style.scss b/src/client/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..5f90a7aa144df253a2944c4817ea424a3a206248 --- /dev/null +++ b/src/client/style.scss @@ -0,0 +1,341 @@ +@charset "utf-8"; + +:root { + --radius: 8px; + --marginFull: 16px; + --marginHalf: 8px; + + --margin: var(--marginFull); + + @media (max-width: 500px) { + --margin: var(--marginHalf); + } +} + +* { + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; +} + +html { + background-color: var(--bg); + background-attachment: fixed; + background-size: cover; + background-position: center; + color: var(--fg); + overflow: auto; + overflow-y: scroll; + + &, * { + scrollbar-color: var(--scrollbarHandle) var(--panel); + + &:hover { + scrollbar-color: var(--scrollbarHandleHover) var(--panel); + } + + &:active { + scrollbar-color: var(--accent) var(--panel); + } + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: var(--panel); + } + + &::-webkit-scrollbar-thumb { + background: var(--scrollbarHandle); + + &:hover { + background: var(--scrollbarHandleHover); + } + + &:active { + background: var(--accent); + } + } + } +} + +html.changing-theme { + &, * { + transition: background 1s ease !important; + } +} + +body { + overflow-wrap: break-word; +} + +#ini { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: wait; + + > svg { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + width: 64px; + height: 64px; + animation: ini 0.6s infinite linear; + } +} + +html, body { + margin: 0; + padding: 0; + scroll-behavior: smooth; + text-size-adjust: 100%; + font-family: Roboto, HelveticaNeue, Arial, sans-serif; +} + +a { + text-decoration: none; + cursor: pointer; + color: inherit; + + &:hover { + text-decoration: underline; + } + + * { + cursor: pointer; + } +} + +#nprogress { + pointer-events: none; + position: absolute; + z-index: 10000; + + .bar { + background: var(--accent); + position: fixed; + z-index: 10001; + top: 0; + left: 0; + width: 100%; + height: 2px; + } + + .peg { + display: block; + position: absolute; + right: 0; + width: 100px; + height: 100%; + box-shadow: 0 0 10px var(--accent), 0 0 5px var(--accent); + opacity: 1; + transform: rotate(3deg) translate(0px, -4px); + } +} + +#wait { + display: block; + position: fixed; + z-index: 10000; + top: 15px; + right: 15px; + + &:before { + content: ""; + display: block; + width: 18px; + height: 18px; + box-sizing: border-box; + border: solid 2px transparent; + border-top-color: var(--accent); + border-left-color: var(--accent); + border-radius: 50%; + animation: progress-spinner 400ms linear infinite; + } +} + +._button { + appearance: none; + padding: 0; + background: none; + border: none; + cursor: pointer; + color: var(--fg); + touch-action: manipulation; + font-size: 1em; + + &, * { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + } + + * { + pointer-events: none; + } + + &:focus { + outline: none; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } +} + +._buttonPrimary { + @extend ._button; + color: #fff; + background: var(--accent); + + &:not(:disabled):hover { + background: var(--jkhztclx); + } + + &:not(:disabled):active { + background: var(--zbqjwygh); + } +} + +._textButton { + @extend ._button; + color: var(--accent); + + &:not(:disabled):hover { + text-decoration: underline; + } +} + +._shadow { + box-shadow: 0 8px 32px var(--shadow); + + @media (max-width: 700px) { + box-shadow: 0 4px 16px var(--shadow); + } + + @media (max-width: 500px) { + box-shadow: 0 2px 8px var(--shadow); + } +} + +._panel { + @extend ._shadow; + position: relative; + background: var(--panel); + border-radius: var(--radius); +} + +._section { + @extend ._panel; + + & + ._section { + margin-top: var(--margin); + } + + > ._title { + margin: 0; + padding: 22px 32px; + font-size: 1.1em; + border-bottom: solid 1px var(--divider); + font-weight: bold; + + @media (max-width: 500px) { + padding: 16px; + font-size: 1em; + } + } + + > ._content { + padding: 32px; + + @media (max-width: 500px) { + padding: 16px; + } + + & + ._content { + border-top: solid 1px var(--divider); + } + + &._list { + padding: 16px; + + @media (max-width: 500px) { + padding: 8px; + } + + ._listItem { + padding: 8px 16px; + border-radius: var(--radius); + + @media (max-width: 500px) { + padding: 8px; + } + + &:hover { + background: var(--listItemHoverBg); + } + + > * { + pointer-events: none; + } + } + } + } + + > ._footer { + border-top: solid 1px var(--divider); + padding: 24px 32px; + + @media (max-width: 500px) { + padding: 16px; + } + } +} + +.zoom-enter-active, .zoom-leave-active { + transition: opacity 0.5s, transform 0.5s !important; +} +.zoom-enter, .zoom-leave-to { + opacity: 0; + transform: scale(0.9); +} + +.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); +} + +@keyframes progress-spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes ini { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/client/style.styl b/src/client/style.styl deleted file mode 100644 index 9638ea53051636d927c7c87b868899008b529bac..0000000000000000000000000000000000000000 --- a/src/client/style.styl +++ /dev/null @@ -1,43 +0,0 @@ -@charset "utf-8" - -/* - ::selection - background var(--primary) - 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 Roboto, HelveticaNeue, Arial, sans-serif - -html.changing-theme - &, * - transition background 1s ease !important - -a - text-decoration none - color var(--link) - cursor pointer - - &:hover - text-decoration underline - - * - cursor pointer - -@css { - a { - tap-highlight-color: var(--linkTapHighlight) !important; - -webkit-tap-highlight-color: var(--linkTapHighlight) !important; - } -} diff --git a/src/client/app/sw.js b/src/client/sw.js similarity index 87% rename from src/client/app/sw.js rename to src/client/sw.js index d080130e3d385c5a576a0f3c385c8a2919c7f239..a84647b6567dd18fb29cd572940bd1a17b5c8ff0 100644 --- a/src/client/app/sw.js +++ b/src/client/sw.js @@ -2,7 +2,7 @@ * Service Worker */ -import composeNotification from './common/scripts/compose-notification'; +import composeNotification from './scripts/compose-notification'; // eslint-disable-next-line no-undef const version = _VERSION_; @@ -18,10 +18,7 @@ self.addEventListener('install', ev => { caches.open(cacheName) .then(cache => { return cache.addAll([ - "/", - `/assets/desktop.${version}.js`, - `/assets/mobile.${version}.js`, - "/assets/error.jpg" + '/assets/error.jpg' ]); }) .then(() => self.skipWaiting()) @@ -48,7 +45,7 @@ self.addEventListener('fetch', ev => { return response || fetch(ev.request); }) .catch(() => { - return caches.match("/"); + return caches.match('/'); }) ); }); diff --git a/src/client/theme.ts b/src/client/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..644ab2c989e464b232fb4de8119a935a1d690ee5 --- /dev/null +++ b/src/client/theme.ts @@ -0,0 +1,100 @@ +import * as tinycolor from 'tinycolor2'; + +export type Theme = { + id: string; + name: string; + author: string; + desc?: string; + base?: 'dark' | 'light'; + props: { [key: string]: string }; +}; + +export const lightTheme: Theme = require('./themes/light.json5'); +export const darkTheme: Theme = require('./themes/dark.json5'); + +export const builtinThemes = [ + lightTheme, + darkTheme, + require('./themes/lavender.json5'), + require('./themes/halloween.json5'), + require('./themes/garden.json5'), + require('./themes/mauve.json5'), + require('./themes/elegant.json5'), + require('./themes/rainy.json5'), + require('./themes/urban.json5'), + require('./themes/cafe.json5'), +]; + +let timeout = null; + +export function applyTheme(theme: Theme, persist = true) { + if (timeout) clearTimeout(timeout); + + document.documentElement.classList.add('changing-theme'); + + timeout = setTimeout(() => { + document.documentElement.classList.remove('changing-theme'); + }, 1000); + + // Deep copy + const _theme = JSON.parse(JSON.stringify(theme)); + + if (_theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id == _theme.base); + _theme.props = Object.assign({}, base.props, _theme.props); + } + + const props = compile(_theme); + + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', props['accent']); + break; + } + } + + for (const [k, v] of Object.entries(props)) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + } + + if (persist) { + localStorage.setItem('theme', JSON.stringify(props)); + } +} + +function compile(theme: Theme): { [key: string]: string } { + function getColor(code: string): tinycolor.Instance { + // ref + if (code[0] == '@') { + return getColor(theme.props[code.substr(1)]); + } + + // func + if (code[0] == ':') { + const parts = code.split('<'); + const func = parts.shift().substr(1); + const arg = parseFloat(parts.shift()); + const color = getColor(parts.join('<')); + + switch (func) { + case 'darken': return color.darken(arg); + case 'lighten': return color.lighten(arg); + case 'alpha': return color.setAlpha(arg); + } + } + + return tinycolor(code); + } + + const props = {}; + + for (const [k, v] of Object.entries(theme.props)) { + props[k] = genValue(getColor(v)); + } + + return props; +} + +function genValue(c: tinycolor.Instance): string { + return c.toRgbString(); +} diff --git a/src/client/themes/cafe.json5 b/src/client/themes/cafe.json5 index 084f69299ce617e511d73d2da7d7fb5962c3ce96..b86ea3f6ec454f4aedc7b67209a66b2895c88d85 100644 --- a/src/client/themes/cafe.json5 +++ b/src/client/themes/cafe.json5 @@ -6,16 +6,15 @@ base: 'light', - vars: { - primary: 'rgb(234, 154, 82)', - secondary: 'rgb(238, 236, 232)', - text: 'rgb(149, 143, 139)', - }, - props: { - renoteGradient: '#ffe1c7', - renoteText: '$primary', - quoteBorder: '$primary', - mfmMention: '#56907b', + accent: 'rgb(234, 154, 82)', + bg: '#DDD9D1', + fg: 'rgb(149, 143, 139)', + panel: '#EEECE8', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + inputBorder: 'rgba(0, 0, 0, 0.1)', }, } diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5 index 0665d59901367d4c682daa99e9f86f12aa288f02..3bb56c8ae364fdf748ec3cddba6cf8202ff509c3 100644 --- a/src/client/themes/dark.json5 +++ b/src/client/themes/dark.json5 @@ -6,237 +6,59 @@ desc: 'Default dark theme', kind: 'dark', - vars: { - primary: '#fb4e4e', - secondary: '#282C37', - text: '#d6dae0', - }, - props: { - primary: '$primary', - primaryForeground: '#fff', - secondary: '$secondary', - bg: ':darken<8<$secondary', - text: '$text', - textHighlighted: ':lighten<7<$text', - - scrollbarTrack: ':darken<5<$secondary', - scrollbarHandle: ':lighten<5<$secondary', - scrollbarHandleHover: ':lighten<10<$secondary', - - link: '$primary', - linkTapHighlight: ':alpha<0.7<@link', - - notificationIndicator: '$primary', - - switchActive: '$primary', - switchActiveTrack: ':alpha<0.4<@switchActive', - radioActive: '$primary', - - face: '$secondary', - faceText: '#fff', - faceHeader: ':lighten<5<$secondary', - faceHeaderText: '$text', - faceDivider: 'rgba(0, 0, 0, 0.3)', - faceTextButton: '$text', - faceTextButtonHover: ':lighten<10<$text', - faceTextButtonActive: ':darken<10<$text', - faceClearButtonHover: 'rgba(0, 0, 0, 0.1)', - faceClearButtonActive: 'rgba(0, 0, 0, 0.2)', - popupBg: ':lighten<5<$secondary', - popupFg: '$text', - - subNoteBg: 'rgba(0, 0, 0, 0.18)', - subNoteText: ':alpha<0.7<$text', - renoteGradient: '#314027', - renoteText: '#9dbb00', - quoteBorder: '#4e945e', - noteText: '#fff', - noteHeaderName: '#fff', - noteHeaderBadgeFg: '#758188', - noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.25)', - noteHeaderAdminFg: '#f15f71', - noteHeaderAdminBg: '#5d282e', - noteHeaderAcct: ':alpha<0.65<$text', - noteHeaderInfo: ':alpha<0.5<$text', - - noteActions: ':alpha<0.45<$text', - noteActionsHover: ':alpha<0.6<$text', - noteActionsReplyHover: '#0af', - noteActionsRenoteHover: '#8d0', - noteActionsReactionHover: '#fa0', - noteActionsHighlighted: ':alpha<0.7<$text', - - noteAttachedFile: 'rgba(255, 255, 255, 0.1)', - - modalBackdrop: 'rgba(0, 0, 0, 0.5)', - - dateDividerBg: ':darken<2<$secondary', - dateDividerFg: ':alpha<0.7<$text', - - switchTrack: 'rgba(255, 255, 255, 0.15)', - radioBorder: 'rgba(255, 255, 255, 0.6)', - inputBorder: 'rgba(255, 255, 255, 0.7)', - inputLabel: 'rgba(255, 255, 255, 0.7)', - inputText: '#fff', - - buttonBg: 'rgba(255, 255, 255, 0.05)', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonActiveBg: 'rgba(255, 255, 255, 0.15)', - - autocompleteItemHoverBg: 'rgba(255, 255, 255, 0.1)', - autocompleteItemText: 'rgba(255, 255, 255, 0.8)', - autocompleteItemTextSub: 'rgba(255, 255, 255, 0.3)', - - cwButtonBg: '#687390', - cwButtonFg: '#393f4f', - cwButtonHoverBg: '#707b97', - - reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)', - - reactionViewerButtonBg: 'rgba(255, 255, 255, 0.1)', - reactionViewerButtonHoverBg: 'rgba(255, 255, 255, 0.2)', - - pollEditorInputBg: 'rgba(0, 0, 0, 0.25)', - - pollChoiceText: '#fff', - pollChoiceBorder: 'rgba(255, 255, 255, 0.1)', - - urlPreviewBorder: 'rgba(0, 0, 0, 0.4)', - urlPreviewBorderHover: 'rgba(255, 255, 255, 0.2)', - urlPreviewTitle: '$text', - urlPreviewText: ':alpha<0.7<$text', - urlPreviewInfo: ':alpha<0.8<$text', - - calendarWeek: '#43d5dc', - calendarSaturdayOrSunday: '#ff6679', - calendarDay: '$text', - - materBg: 'rgba(0, 0, 0, 0.3)', - - chartCaption: ':alpha<0.6<$text', - - announcementsBg: '#253a50', - announcementsTitle: '#539eff', - announcementsText: '#fff', - - googleSearchBg: 'rgba(0, 0, 0, 0.2)', - googleSearchFg: '#dee4e8', - googleSearchBorder: 'rgba(255, 255, 255, 0.2)', - googleSearchHoverBorder: 'rgba(255, 255, 255, 0.3)', - googleSearchHoverButton: 'rgba(255, 255, 255, 0.1)', - - mfmTitleBg: 'rgba(0, 0, 0, 0.2)', - mfmQuote: ':alpha<0.7<$text', - mfmQuoteLine: ':alpha<0.6<$text', - mfmUrl: '$primary', - mfmLink: '@mfmUrl', - mfmMention: '$primary', - mfmMentionForeground: '@primaryForeground', - mfmHashtag: '$primary', - - suspendedInfoBg: '#611d1d', - suspendedInfoFg: '#ffb4b4', - remoteInfoBg: '#42321c', - remoteInfoFg: '#ffbd3e', - + accent: '#86b300', + accentDarken: ':darken<10<@accent', + accentLighten: ':lighten<10<@accent', + focus: ':alpha<0.3<@accent', + bg: '#000', + fg: '#c7d1d8', + panel: '#111213', + shadow: 'rgba(0, 0, 0, 0.1)', + header: 'rgba(20, 20, 20, 0.75)', + navBg: '@panel', + navFg: '@fg', + navActive: '@accent', + navIndicator: '@accent', + link: '#44a4c1', + hashtag: '#ff9156', + mention: '@accent', + renote: '#229e82', + modalBg: 'rgba(0, 0, 0, 0.5)', + divider: 'rgba(255, 255, 255, 0.1)', + scrollbarHandle: 'rgba(255, 255, 255, 0.2)', + scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + dateLabelBg: 'rgba(255, 255, 255, 0.08)', + dateLabelFg: '#fff', infoBg: '#253142', infoFg: '#fff', infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', - - messagingRoomBg: '@bg', - messagingRoomInfo: '#fff', - messagingRoomDateDividerLine: 'rgba(255, 255, 255, 0.1)', - messagingRoomDateDividerText: 'rgba(255, 255, 255, 0.3)', - messagingRoomMessageInfo: 'rgba(255, 255, 255, 0.4)', - messagingRoomMessageBg: '$secondary', - messagingRoomMessageFg: '#fff', - - driveFileIcon: '$text', - - formButtonBorder: 'rgba(255, 255, 255, 0.1)', - formButtonHoverBg: ':alpha<0.2<$primary', - formButtonHoverBorder: ':alpha<0.5<$primary', - formButtonActiveBg: ':alpha<0.12<$primary', - - desktopHeaderBg: ':lighten<5<$secondary', - desktopHeaderFg: '$text', - desktopHeaderHoverFg: '#fff', - desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.1)', - desktopHeaderSearchHoverBg: 'rgba(255, 255, 255, 0.04)', - desktopHeaderSearchFg: '#fff', - desktopNotificationBg: ':alpha<0.9<$secondary', - desktopNotificationFg: ':alpha<0.7<$text', - desktopNotificationShadow: 'rgba(0, 0, 0, 0.4)', - desktopPostFormBg: '@face', - desktopPostFormTextareaBg: 'rgba(0, 0, 0, 0.25)', - desktopPostFormTextareaFg: '#fff', - desktopPostFormTransparentButtonFg: '$primary', - desktopPostFormTransparentButtonActiveGradientStart: ':darken<8<$secondary', - desktopPostFormTransparentButtonActiveGradientEnd: ':darken<3<$secondary', - desktopRenoteFormFooter: ':lighten<5<$secondary', - desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.15)', - desktopTimelineSrc: '@faceTextButton', - desktopTimelineSrcHover: '@faceTextButtonHover', - desktopWindowTitle: '@faceHeaderText', - desktopWindowShadow: 'rgba(0, 0, 0, 0.5)', - desktopDriveBg: '@bg', - desktopDriveFolderBg: ':alpha<0.2<$primary', - desktopDriveFolderHoverBg: ':alpha<0.3<$primary', - desktopDriveFolderActiveBg: ':alpha<0.3<:darken<10<$primary', - desktopDriveFolderFg: '#fff', - desktopSettingsNavItem: ':alpha<0.8<$text', - desktopSettingsNavItemHover: ':lighten<10<$text', - - deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)', - deckColumnBg: ':darken<3<@face', - - mobileHeaderBg: ':lighten<5<$secondary', - mobileHeaderFg: '$text', - mobileNavBackdrop: 'rgba(0, 0, 0, 0.7)', - mobilePostFormDivider: 'rgba(0, 0, 0, 0.2)', - mobilePostFormTextareaBg: 'rgba(0, 0, 0, 0.3)', - mobilePostFormButton: '$text', - mobileDriveNavBg: ':alpha<0.75<$secondary', - mobileHomeTlItemHover: 'rgba(255, 255, 255, 0.1)', - mobileUserPageName: '#fff', - mobileUserPageAcct: '$text', - mobileUserPageDescription: '$text', - mobileUserPageFollowedBg: 'rgba(0, 0, 0, 0.3)', - mobileUserPageFollowedFg: '$text', - mobileUserPageStatusHighlight: '#fff', - mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.3)', - mobileAnnouncement: 'rgba(30, 129, 216, 0.2)', - mobileAnnouncementFg: '#fff', - mobileSignedInAsBg: '#273c34', - mobileSignedInAsFg: '#49ab63', - mobileSignoutBg: '#652222', - mobileSignoutFg: '#ff5f56', - - reversiBannerGradientStart: '#45730e', - reversiBannerGradientEnd: '#464300', - reversiDescBg: 'rgba(255, 255, 255, 0.1)', - reversiListItemShadow: 'rgba(0, 0, 0, 0.7)', - reversiMapSelectBorder: 'rgba(255, 255, 255, 0.1)', - reversiMapSelectHoverBorder: 'rgba(255, 255, 255, 0.2)', - reversiRoomFormShadow: 'rgba(0, 0, 0, 0.7)', - reversiRoomFooterBg: ':alpha<0.9<$secondary', - reversiGameHeaderLine: ':alpha<0.5<$secondary', - reversiGameEmptyCell: ':lighten<2<$secondary', - reversiGameEmptyCellMyTurn: ':lighten<5<$secondary', - reversiGameEmptyCellCanPut: ':lighten<4<$secondary', - - adminDashboardHeaderFg: ':alpha<0.9<$text', - adminDashboardHeaderBorder: 'rgba(0, 0, 0, 0.3)', - adminDashboardCardBg: '$secondary', - adminDashboardCardFg: '$text', - adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)', - - pageBlockBorder: 'rgba(255, 255, 255, 0.1)', - pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)', - - groupUserListOwnerFg: '#f15f71', - groupUserListOwnerBg: '#5d282e' + cwBg: '#687390', + cwFg: '#393f4f', + cwHoverBg: '#707b97', + toastBg: 'rgba(0, 0, 0, 0.5)', + toastFg: '#c7d1d8', + buttonBg: 'rgba(255, 255, 255, 0.05)', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + inputBorder: '#959da2', + listItemHoverBg: 'rgba(255, 255, 255, 0.03)', + driveFolderBg: ':alpha<0.3<@accent', + bonzsgfz: ':alpha<0<@bg', + pcncwizz: ':darken<2<@panel', + vocsgcxy: 'rgba(0, 0, 0, 0.5)', + yrnqrguo: 'rgba(255, 255, 255, 0.05)', + mkykhqkw: ':lighten<3<@fg', + nwjktjjq: 'rgba(255, 255, 255, 0.1)', + geavgsxy: 'rgba(255, 255, 255, 0.05)', + nhzhphzx: 'rgba(255, 255, 255, 0.15)', + tyvedwbe: 'rgba(0, 0, 0, 0.5)', + bwqtlupy: 'rgba(255, 255, 255, 0.05)', + jkhztclx: ':lighten<5<@accent', + zbqjwygh: ':darken<5<@accent', + xxubwiul: ':alpha<0.4<@accent', + aupeazdm: 'rgba(0, 0, 0, 0.3)', + jvhmlskx: 'rgba(255, 255, 255, 0.1)', + yakfpmhl: 'rgba(255, 255, 255, 0.15)', }, } diff --git a/src/client/themes/elegant.json5 b/src/client/themes/elegant.json5 new file mode 100644 index 0000000000000000000000000000000000000000..52ae4cbfa0ab5f68c373b8e2e6b289033612b8c2 --- /dev/null +++ b/src/client/themes/elegant.json5 @@ -0,0 +1,18 @@ +{ + id: 'de465368-9dd1-4bba-b1b9-69f312fcf6e7', + + name: 'Elegant', + author: 'syuilo', + desc: 'Inspired by meimei\'s theme', + + base: 'dark', + + props: { + accent: 'rgb(187, 146, 45)', + panel: 'rgb(76, 33, 33)', + bg: 'rgb(53, 21, 21)', + fg: 'rgb(216, 210, 199)', + header: 'rgba(76, 45, 33, 0.75)', + renote: '@accent', + }, +} diff --git a/src/client/themes/future.json5 b/src/client/themes/future.json5 deleted file mode 100644 index c89b90fae71ae16b0dddbafa9ddfcef8cf5637f3..0000000000000000000000000000000000000000 --- a/src/client/themes/future.json5 +++ /dev/null @@ -1,39 +0,0 @@ -{ - id: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2', - - name: 'Future', - author: 'syuilo', - desc: 'Sci-fi flavored', - - base: 'dark', - - vars: { - c0: '#0e0e0e', - c1: 'rgb(255, 105, 78)', - c2: 'rgb(99, 197, 210)', - c4: 'rgb(253, 254, 214)', - c3: 'rgb(204, 254, 253)', - primary: '$c1', - secondary: '#191919', - text: '$c3', - }, - - props: { - bg: '$c0', - noteText: '$c4', - noteHeaderAcct: ':alpha<0.65<$c4', - noteHeaderInfo: ':alpha<0.5<$c4', - subNoteText: ':alpha<0.7<$c4', - renoteGradient: '$secondary', - renoteText: '$c2', - quoteBorder: '$c2', - mfmHashtag: '$c1', - mfmUrl: '$c2', - mfmLink: '$c2', - mfmMention: '$c1', - mfmMentionForeground: '#fff', - notificationIndicator: '$c2', - link: '$c2', - desktopHeaderBg: '$secondary', - }, -} diff --git a/src/client/themes/garden.json5 b/src/client/themes/garden.json5 new file mode 100644 index 0000000000000000000000000000000000000000..ae12fb3e78f6e39adf04c1b8397ab68bb85c8973 --- /dev/null +++ b/src/client/themes/garden.json5 @@ -0,0 +1,17 @@ +{ + id: '45b13782-9143-4dd8-ac0c-4a872321fc63', + + name: 'Garden', + author: 'syuilo', + + base: 'light', + + props: { + accent: 'rgb(147, 206, 188)', + bg: 'rgb(253, 245, 242)', + fg: 'rgb(161, 147, 139)', + renote: '@accent', + hashtag: 'rgb(226, 152, 48)', + link: '#aac12c', + }, +} diff --git a/src/client/themes/gray.json5 b/src/client/themes/gray.json5 deleted file mode 100644 index 59494f278af10592ac227658d921c4c01a8e591a..0000000000000000000000000000000000000000 --- a/src/client/themes/gray.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: '56ff14eb-1e6d-4c0c-9e84-71eb156234e5', - - name: 'Gray', - author: 'syuilo', - - base: 'light', - - vars: { - primary: '#C03233', - secondary: 'rgb(213, 213, 213)', - text: 'rgb(102, 102, 102)', - }, - - props: { - renoteGradient: '#bdbdbd', - renoteText: '$primary', - quoteBorder: '$primary', - desktopPostFormBg: '#ececec', - }, -} diff --git a/src/client/themes/gruvbox-dark.json5 b/src/client/themes/gruvbox-dark.json5 deleted file mode 100644 index 2d03153190a634495436e0bb2183f37dc189fd0e..0000000000000000000000000000000000000000 --- a/src/client/themes/gruvbox-dark.json5 +++ /dev/null @@ -1,29 +0,0 @@ -{ - id: '0c6e70e2-a1ec-4053-9b1a-b6082fe016cb', - - name: 'Gruvbox Dark', - author: 'syuilo', - - base: 'dark', - - vars: { - primary: 'rgb(215, 153, 33)', - secondary: 'rgb(40, 40, 40)', - text: 'rgb(235, 219, 178)', - }, - - props: { - renoteGradient: '#58581e', - renoteText: 'rgb(169, 174, 36)', - quoteBorder: 'rgb(169, 174, 36)', - mfmMention: 'rgb(177, 98, 134)', - mfmMentionForeground: '#fff', - mfmUrl: 'rgb(69, 133, 136)', - mfmLink: 'rgb(104, 157, 106)', - mfmHashtag: 'rgb(251, 73, 52)', - notificationIndicator: 'rgb(184, 187, 38)', - switchActive: 'rgb(254, 128, 25)', - radioActive: 'rgb(131, 165, 152)', - link: 'rgb(104, 157, 106)', - }, -} diff --git a/src/client/themes/halloween.json5 b/src/client/themes/halloween.json5 index 608105903a69a03c68070d1d7d8b402cc8f74f9f..1394c793edcb56398ecc3a50ad91a0dbbe3b53a9 100644 --- a/src/client/themes/halloween.json5 +++ b/src/client/themes/halloween.json5 @@ -7,15 +7,11 @@ base: 'dark', - vars: { - primary: '#d67036', - secondary: '#1f1d30', - text: '#b1bee3', - }, - props: { - renoteGradient: '#5d2d1a', - renoteText: '#ff6c00', - quoteBorder: '#c3631c', + accent: '#d67036', + panel: '#1f1d30', + bg: '#0f0e17', + fg: '#b1bee3', + renote: '@accent', }, } diff --git a/src/client/themes/japanese-sushi-set.json5 b/src/client/themes/japanese-sushi-set.json5 deleted file mode 100644 index 94edecca52eb15e9d74c90b3742c1d9271ac7c2d..0000000000000000000000000000000000000000 --- a/src/client/themes/japanese-sushi-set.json5 +++ /dev/null @@ -1,20 +0,0 @@ -{ - id: '2b0a0654-cdb4-4c9a-8244-736b647d3c2a', - - name: 'Japanese Sushi Set', - author: 'Noizenecio', - - base: 'dark', - - vars: { - primary: 'rgb(234, 136, 50)', - secondary: 'rgb(34, 36, 42)', - text: 'rgb(221, 209, 203)', - }, - - props: { - renoteGradient: '#6d3d14', - renoteText: '$primary', - quoteBorder: '$primary', - }, -} diff --git a/src/client/themes/lavender.json5 b/src/client/themes/lavender.json5 index e3078ad516af1fe23bdbd65ff6c95579152509f0..4eb4a54749c494bc854d1240a9f23af432a46a0d 100644 --- a/src/client/themes/lavender.json5 +++ b/src/client/themes/lavender.json5 @@ -2,19 +2,18 @@ id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36', name: 'Lavender', - author: 'sokuyuku', + author: 'syuilo', base: 'light', - vars: { - primary: 'rgb(206, 147, 191)', - secondary: 'rgb(253, 242, 243)', - text: 'rgb(161, 139, 146)', - }, - props: { - renoteGradient: '#f7e4ec', - renoteText: '$primary', - quoteBorder: '$primary', + accent: 'rgb(206, 147, 191)', + bg: 'rgb(253, 242, 243)', + fg: 'rgb(161, 139, 146)', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + dateLabelBg: 'rgb(204, 186, 188)', }, } diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5 index cbe456ca5d667430d2f25f859cecdc35f945da7e..075933f3508e39ec130ef97ee040c34b8149a1ea 100644 --- a/src/client/themes/light.json5 +++ b/src/client/themes/light.json5 @@ -6,237 +6,59 @@ desc: 'Default light theme', kind: 'light', - vars: { - primary: '#f18570', - secondary: '#fff', - text: '#666', - }, - props: { - primary: '$primary', - primaryForeground: '#fff', - secondary: '$secondary', - bg: ':darken<8<$secondary', - text: '$text', - textHighlighted: ':darken<7<$text', - - scrollbarTrack: '#fff', - scrollbarHandle: '#00000033', - scrollbarHandleHover: '#00000066', - - link: '$primary', - linkTapHighlight: ':alpha<0.7<@link', - - notificationIndicator: '$primary', - - switchActive: '$primary', - switchActiveTrack: ':alpha<0.4<@switchActive', - radioActive: '$primary', - - face: '$secondary', - faceText: '$text', - faceHeader: ':lighten<5<$secondary', - faceHeaderText: '$text', - faceDivider: 'rgba(0, 0, 0, 0.082)', - faceTextButton: ':alpha<0.7<$text', - faceTextButtonHover: ':alpha<0.7<:darken<7<$text', - faceTextButtonActive: ':alpha<0.7<:darken<10<$text', - faceClearButtonHover: 'rgba(0, 0, 0, 0.025)', - faceClearButtonActive: 'rgba(0, 0, 0, 0.05)', - popupBg: ':lighten<5<$secondary', - popupFg: '$text', - - subNoteBg: 'rgba(0, 0, 0, 0.01)', - subNoteText: ':alpha<0.7<$text', - renoteGradient: '#edfde2', - renoteText: '#9dbb00', - quoteBorder: '#c0dac6', - noteText: '$text', - noteHeaderName: ':darken<2<$text', - noteHeaderBadgeFg: '#aaa', - noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.05)', - noteHeaderAdminFg: '#f15f71', - noteHeaderAdminBg: '#ffdfdf', - noteHeaderAcct: ':alpha<0.7<@noteHeaderName', - noteHeaderInfo: ':alpha<0.7<@noteHeaderName', - - noteActions: ':alpha<0.3<$text', - noteActionsHover: ':alpha<0.9<$text', - noteActionsReplyHover: '#0af', - noteActionsRenoteHover: '#8d0', - noteActionsReactionHover: '#fa0', - noteActionsHighlighted: '#888', - - noteAttachedFile: 'rgba(0, 0, 0, 0.05)', - - modalBackdrop: 'rgba(0, 0, 0, 0.1)', - - dateDividerBg: ':darken<2<$secondary', - dateDividerFg: ':alpha<0.7<$text', - - switchTrack: 'rgba(0, 0, 0, 0.25)', - radioBorder: 'rgba(0, 0, 0, 0.4)', - inputBorder: 'rgba(0, 0, 0, 0.42)', - inputLabel: 'rgba(0, 0, 0, 0.54)', - inputText: '#000', - - buttonBg: 'rgba(0, 0, 0, 0.05)', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonActiveBg: 'rgba(0, 0, 0, 0.15)', - - autocompleteItemHoverBg: 'rgba(0, 0, 0, 0.1)', - autocompleteItemText: 'rgba(0, 0, 0, 0.8)', - autocompleteItemTextSub: 'rgba(0, 0, 0, 0.3)', - - cwButtonBg: '#b1b9c1', - cwButtonFg: '#fff', - cwButtonHoverBg: '#bbc4ce', - - reactionPickerButtonHoverBg: '#eee', - - reactionViewerButtonBg: 'rgba(0, 0, 0, 0.05)', - reactionViewerButtonHoverBg: 'rgba(0, 0, 0, 0.1)', - - pollEditorInputBg: '#fff', - - pollChoiceText: '#000', - pollChoiceBorder: 'rgba(0, 0, 0, 0.1)', - - urlPreviewBorder: 'rgba(0, 0, 0, 0.1)', - urlPreviewBorderHover: 'rgba(0, 0, 0, 0.2)', - urlPreviewTitle: '$text', - urlPreviewText: ':alpha<0.7<$text', - urlPreviewInfo: ':alpha<0.8<$text', - - calendarWeek: '#19a2a9', - calendarSaturdayOrSunday: '#ef95a0', - calendarDay: '$text', - - materBg: 'rgba(0, 0, 0, 0.1)', - - chartCaption: ':alpha<0.6<$text', - - announcementsBg: '#f3f9ff', - announcementsTitle: '#4078c0', - announcementsText: '#57616f', - - googleSearchBg: '#fff', - googleSearchFg: '#55595c', - googleSearchBorder: 'rgba(0, 0, 0, 0.2)', - googleSearchHoverBorder: 'rgba(0, 0, 0, 0.3)', - googleSearchHoverButton: 'rgba(0, 0, 0, 0.05)', - - mfmTitleBg: 'rgba(0, 0, 0, 0.07)', - mfmQuote: ':alpha<0.6<$text', - mfmQuoteLine: ':alpha<0.5<$text', - mfmUrl: '$primary', - mfmLink: '@mfmUrl', - mfmMention: '$primary', - mfmMentionForeground: '@primaryForeground', - mfmHashtag: '$primary', - - suspendedInfoBg: '#ffdbdb', - suspendedInfoFg: '#570808', - remoteInfoBg: '#fff0db', - remoteInfoFg: '#573c08', - + accent: '#86b300', + accentDarken: ':darken<10<@accent', + accentLighten: ':lighten<10<@accent', + focus: ':alpha<0.3<@accent', + bg: '#fafafa', + fg: '#5c6a73', + panel: '#fff', + shadow: 'rgba(0, 0, 0, 0.1)', + header: 'rgba(255, 255, 255, 0.75)', + navBg: '@panel', + navFg: '@fg', + navActive: '@accent', + navIndicator: '@accent', + link: '#44a4c1', + hashtag: '#ff9156', + mention: '@accent', + renote: '#229e82', + modalBg: 'rgba(0, 0, 0, 0.3)', + divider: 'rgba(0, 0, 0, 0.1)', + scrollbarHandle: 'rgba(0, 0, 0, 0.2)', + scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', + dateLabelBg: 'rgba(0, 0, 0, 0.5)', + dateLabelFg: '#fff', infoBg: '#e5f5ff', infoFg: '#72818a', infoWarnBg: '#fff0db', infoWarnFg: '#573c08', - - messagingRoomBg: '#fff', - messagingRoomInfo: '#000', - messagingRoomDateDividerLine: 'rgba(0, 0, 0, 0.1)', - messagingRoomDateDividerText: 'rgba(0, 0, 0, 0.3)', - messagingRoomMessageInfo: 'rgba(0, 0, 0, 0.4)', - messagingRoomMessageBg: '#eee', - messagingRoomMessageFg: '#333', - - driveFileIcon: '$text', - - formButtonBorder: 'rgba(0, 0, 0, 0.1)', - formButtonHoverBg: ':alpha<0.12<$primary', - formButtonHoverBorder: ':alpha<0.3<$primary', - formButtonActiveBg: ':alpha<0.12<$primary', - - desktopHeaderBg: ':lighten<5<$secondary', - desktopHeaderFg: '$text', - desktopHeaderHoverFg: ':darken<7<$text', - desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.05)', - desktopHeaderSearchHoverBg: 'rgba(0, 0, 0, 0.08)', - desktopHeaderSearchFg: '#000', - desktopNotificationBg: ':alpha<0.9<$secondary', - desktopNotificationFg: ':alpha<0.7<$text', - desktopNotificationShadow: 'rgba(0, 0, 0, 0.2)', - desktopPostFormBg: ':lighten<33<$primary', - desktopPostFormTextareaBg: '#fff', - desktopPostFormTextareaFg: '#333', - desktopPostFormTransparentButtonFg: ':alpha<0.5<$primary', - desktopPostFormTransparentButtonActiveGradientStart: ':lighten<30<$primary', - desktopPostFormTransparentButtonActiveGradientEnd: ':lighten<33<$primary', - desktopRenoteFormFooter: ':lighten<33<$primary', - desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.08)', - desktopTimelineSrc: '$text', - desktopTimelineSrcHover: ':darken<7<$text', - desktopWindowTitle: '$text', - desktopWindowShadow: 'rgba(0, 0, 0, 0.2)', - desktopDriveBg: '#fff', - desktopDriveFolderBg: ':lighten<31<$primary', - desktopDriveFolderHoverBg: ':lighten<27<$primary', - desktopDriveFolderActiveBg: ':lighten<25<$primary', - desktopDriveFolderFg: ':darken<10<$primary', - desktopSettingsNavItem: ':alpha<0.8<$text', - desktopSettingsNavItemHover: ':darken<10<$text', - - deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)', - deckColumnBg: ':darken<4<@face', - - mobileHeaderBg: ':lighten<5<$secondary', - mobileHeaderFg: '$text', - mobileNavBackdrop: 'rgba(0, 0, 0, 0.2)', - mobilePostFormDivider: 'rgba(0, 0, 0, 0.1)', - mobilePostFormTextareaBg: '#fff', - mobilePostFormButton: '$text', - mobileDriveNavBg: ':alpha<0.75<$secondary', - mobileHomeTlItemHover: 'rgba(0, 0, 0, 0.05)', - mobileUserPageName: '#757c82', - mobileUserPageAcct: '#969ea5', - mobileUserPageDescription: '#757c82', - mobileUserPageFollowedBg: '#a7bec7', - mobileUserPageFollowedFg: '#fff', - mobileUserPageStatusHighlight: '#787e86', - mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.07)', - mobileAnnouncement: 'rgba(155, 196, 232, 0.2)', - mobileAnnouncementFg: '#3f4967', - mobileSignedInAsBg: '#fcfff5', - mobileSignedInAsFg: '#2c662d', - mobileSignoutBg: '#fff6f5', - mobileSignoutFg: '#cc2727', - - reversiBannerGradientStart: '#8bca3e', - reversiBannerGradientEnd: '#d6cf31', - reversiDescBg: 'rgba(0, 0, 0, 0.1)', - reversiListItemShadow: 'rgba(0, 0, 0, 0.15)', - reversiMapSelectBorder: 'rgba(0, 0, 0, 0.1)', - reversiMapSelectHoverBorder: 'rgba(0, 0, 0, 0.2)', - reversiRoomFormShadow: 'rgba(0, 0, 0, 0.1)', - reversiRoomFooterBg: ':alpha<0.9<$secondary', - reversiGameHeaderLine: '#c4cdd4', - reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)', - reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)', - reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.09)', - - adminDashboardHeaderFg: ':alpha<0.9<$text', - adminDashboardHeaderBorder: 'rgba(0, 0, 0, 0.1)', - adminDashboardCardBg: '$secondary', - adminDashboardCardFg: '$text', - adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)', - - pageBlockBorder: 'rgba(0, 0, 0, 0.1)', - pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)', - - groupUserListOwnerFg: '#f15f71', - groupUserListOwnerBg: '#ffdfdf' + cwBg: '#b1b9c1', + cwFg: '#fff', + cwHoverBg: '#bbc4ce', + toastBg: 'rgba(255, 255, 255, 0.5)', + toastFg: '#0c0c0c', + buttonBg: 'rgba(0, 0, 0, 0.05)', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', + inputBorder: '#dae0e4', + listItemHoverBg: 'rgba(0, 0, 0, 0.03)', + driveFolderBg: ':alpha<0.3<@accent', + bonzsgfz: ':alpha<0<@bg', + pcncwizz: ':darken<2<@panel', + vocsgcxy: 'rgba(255, 255, 255, 0.5)', + yrnqrguo: 'rgba(0, 0, 0, 0.05)', + mkykhqkw: ':darken<3<@fg', + nwjktjjq: 'rgba(0, 0, 0, 0.1)', + geavgsxy: 'rgba(0, 0, 0, 0.05)', + nhzhphzx: 'rgba(0, 0, 0, 0.25)', + tyvedwbe: 'rgba(0, 0, 0, 0.1)', + bwqtlupy: 'rgba(0, 0, 0, 0.05)', + jkhztclx: ':lighten<5<@accent', + zbqjwygh: ':darken<5<@accent', + xxubwiul: ':alpha<0.4<@accent', + aupeazdm: 'rgba(0, 0, 0, 0.1)', + jvhmlskx: 'rgba(0, 0, 0, 0.1)', + yakfpmhl: 'rgba(0, 0, 0, 0.15)', }, } diff --git a/src/client/themes/mauve.json5 b/src/client/themes/mauve.json5 index b2ec28b44558d66430fdee6ff54806979ec819ad..47304c922f26598c50691deee65559437c801593 100644 --- a/src/client/themes/mauve.json5 +++ b/src/client/themes/mauve.json5 @@ -1,20 +1,18 @@ { - id: '252b2caf-86c2-4c3f-a73f-e1fc1cfa5298', + id: '6846bcbe-afbe-487c-bece-77e8cfc4ab8a', name: 'Mauve', - author: 'ã¨ã‚ã“', + author: 'syuilo', base: 'dark', - vars: { - primary: 'rgb(133, 88, 150)', - secondary: 'rgb(54, 43, 59)', - text: 'rgb(229, 223, 231)', - }, - props: { - renoteGradient: '#54415d', - renoteText: '$primary', - quoteBorder: '$primary', + accent: 'rgb(133, 88, 150)', + panel: 'rgb(54, 43, 59)', + bg: '#201A23', + fg: 'rgb(229, 223, 231)', + shadow: 'rgba(0, 0, 0, 0.2)', + header: 'rgba(87, 70, 97, 0.5)', + renote: '@accent', }, } diff --git a/src/client/themes/monokai.json5 b/src/client/themes/monokai.json5 deleted file mode 100644 index 1ecd68730e6198c9987b192b5788fff66e541026..0000000000000000000000000000000000000000 --- a/src/client/themes/monokai.json5 +++ /dev/null @@ -1,29 +0,0 @@ -{ - id: 'fef11dc4-6b17-436e-b374-73282c44ddc0', - - name: 'Monokai', - author: 'syuilo', - - base: 'dark', - - vars: { - primary: '#f92672', - secondary: '#272822', - text: '#f8f8f2', - }, - - props: { - renoteGradient: '#3f500f', - renoteText: '#a6e22e', - quoteBorder: '#a6e22e', - mfmMention: '#ae81ff', - mfmMentionForeground: '#fff', - mfmUrl: '#66d9ef', - mfmLink: '#e6db74', - mfmHashtag: '#fd971f', - notificationIndicator: '#66d9ef', - switchActive: 'rgb(166, 226, 46)', - radioActive: '#fd971f', - link: '#e6db74', - }, -} diff --git a/src/client/themes/rainy.json5 b/src/client/themes/rainy.json5 index 26ff3a6c86eaf69a99927526c68ca482306b83c8..0ad6338295dd8ddb7da4d53b84f0e591a35028f7 100644 --- a/src/client/themes/rainy.json5 +++ b/src/client/themes/rainy.json5 @@ -1,21 +1,15 @@ { - id: '2058b33e-5127-4e63-ae67-a900f3a11723', + id: '2d7d1479-acb8-4e2e-85bb-565a2d8e6966', name: 'Rainy', author: 'syuilo', - desc: 'It\'s a rainy day.', base: 'light', - vars: { - primary: 'rgb(100, 184, 193)', - secondary: 'rgb(228, 234, 234)', - text: 'rgb(85, 94, 92)', - }, - props: { - renoteGradient: '#bcd0d0', - renoteText: '$primary', - quoteBorder: '$primary', + accent: 'rgb(147, 199, 206)', + bg: 'rgb(220, 229, 232)', + fg: 'rgb(139, 153, 161)', + renote: '@accent', }, } diff --git a/src/client/themes/tweet-deck.json5 b/src/client/themes/tweet-deck.json5 deleted file mode 100644 index aac9e3d009f21dbff054238198d4a95edaebd7f9..0000000000000000000000000000000000000000 --- a/src/client/themes/tweet-deck.json5 +++ /dev/null @@ -1,44 +0,0 @@ -{ - name: 'Tweet Deck', - id: '06f82fb4-0dad-4d70-8a3f-56cae91e1163', - author: 'simirall', - desc: 'Tweet like a pro.', - base: 'dark', - vars: { - primary: '#1da1f2', - secondary: '#15202b', - text: '#fdfdfd', - }, - props: { - bg: '#10171e', - faceHeader: '$secondary', - faceTextButton: '$primary', - renoteGradient: '$secondary', - renoteText: '#17bf63', - quoteBorder: '#38444d', - noteHeaderAdminFg: '$primary', - noteHeaderAdminBg: '$secondary', - noteActionsReplyHover: '$primary', - noteActionsRenoteHover: '#17bf63', - noteActionsReactionHover: '#e0245e', - calendarWeek: '$primary', - calendarSaturdayOrSunday: '#e0245e', - announcementsBg: '$secondary', - announcementsTitle: '$primary', - suspendedInfoBg: '$secondary', - suspendedInfoFg: '$primary', - remoteInfoBg: '$secondary', - remoteInfoFg: '$primary', - desktopHeaderBg: '#1c2938', - desktopHeaderFg: '#a9adae', - desktopHeaderHoverFg: '#fff', - desktopPostFormTransparentButtonFg: '#a9adae', - desktopTimelineSrc: '$primary', - desktopTimelineSrcHover: '#fff', - deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.0)', - reversiBannerGradientStart: '$primary', - reversiBannerGradientEnd: '$primary', - reversiGameEmptyCellMyTurn: ':lighten<5<$primary', - reversiGameEmptyCellCanPut: ':lighten<4<$primary', - }, -} diff --git a/src/client/themes/urban.json5 b/src/client/themes/urban.json5 new file mode 100644 index 0000000000000000000000000000000000000000..342d3b9cabd1a81a68a6eed9370c3944e774117a --- /dev/null +++ b/src/client/themes/urban.json5 @@ -0,0 +1,18 @@ +{ + id: 'b9392635-8c3d-4397-aaf7-796e49781899', + + name: 'Urban', + author: 'syuilo', + + base: 'dark', + + props: { + accent: 'rgb(212, 104, 48)', + panel: 'rgb(38, 44, 53)', + bg: 'rgb(26, 29, 33)', + fg: 'rgb(199, 209, 216)', + shadow: 'rgba(0, 0, 0, 0.2)', + header: 'rgba(51, 64, 72, 0.75)', + renote: '@accent', + }, +} diff --git a/src/client/themes/vivid.json5 b/src/client/themes/vivid.json5 deleted file mode 100644 index 27bf742f3f30a4c15879b9cf37bd65f40b9642d0..0000000000000000000000000000000000000000 --- a/src/client/themes/vivid.json5 +++ /dev/null @@ -1,23 +0,0 @@ -{ - id: '2d066d6e-bd39-4f23-bd48-686d5c1c6ae8', - - name: 'Vivid', - author: 'syuilo', - - base: 'light', - - vars: { - primary: 'rgb(255, 153, 64)', - secondary: 'rgb(255, 255, 255)', - text: 'rgb(108, 118, 128)', - }, - - props: { - bg: 'rgb(250, 250, 250)', - mfmMention: '#f07171', - mfmMentionForeground: '#fff', - mfmUrl: '#86b300', - mfmLink: '#399ee6', - mfmHashtag: '#fa8d3e' - }, -} diff --git a/src/client/app/tsconfig.json b/src/client/tsconfig.json similarity index 100% rename from src/client/app/tsconfig.json rename to src/client/tsconfig.json diff --git a/src/client/app/v.d.ts b/src/client/v.d.ts similarity index 100% rename from src/client/app/v.d.ts rename to src/client/v.d.ts diff --git a/src/client/widgets/calendar.vue b/src/client/widgets/calendar.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae9dbfecefee6100f55efc80ea9134dadd13701f --- /dev/null +++ b/src/client/widgets/calendar.vue @@ -0,0 +1,206 @@ +<template> +<div class="mkw-calendar" :class="{ _panel: props.design === 0 }"> + <div class="calendar" :data-is-holiday="isHoliday"> + <p class="month-and-year"> + <span class="year">{{ $t('yearX', { year }) }}</span> + <span class="month">{{ $t('monthX', { month }) }}</span> + </p> + <p class="day">{{ $t('dayX', { day }) }}</p> + <p class="week-day">{{ weekDay }}</p> + </div> + <div class="info"> + <div> + <p>{{ $t('today') }}: <b>{{ dayP.toFixed(1) }}%</b></p> + <div class="meter"> + <div class="val" :style="{ width: `${dayP}%` }"></div> + </div> + </div> + <div> + <p>{{ $t('thisMonth') }}: <b>{{ monthP.toFixed(1) }}%</b></p> + <div class="meter"> + <div class="val" :style="{ width: `${monthP}%` }"></div> + </div> + </div> + <div> + <p>{{ $t('thisYear') }}: <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 './define'; +import i18n from '../i18n'; + +export default define({ + name: 'calendar', + props: () => ({ + design: 0 + }) +}).extend({ + i18n, + data() { + return { + now: new Date(), + year: null, + month: null, + day: null, + weekDay: null, + yearP: null, + dayP: null, + monthP: null, + isHoliday: null, + clock: null + }; + }, + created() { + this.tick(); + this.clock = setInterval(this.tick, 1000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + func() { + if (this.props.design == 2) { + this.props.design = 0; + } else { + this.props.design++; + } + this.save(); + }, + 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 = [ + this.$t('_weekday.sunday'), + this.$t('_weekday.monday'), + this.$t('_weekday.tuesday'), + this.$t('_weekday.wednesday'), + this.$t('_weekday.thursday'), + this.$t('_weekday.friday'), + this.$t('_weekday.saturday') + ][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; + } + } +}); +</script> + +<style lang="scss" scoped> +.mkw-calendar { + padding: 16px 0; + + &: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; + box-sizing: border-box; + + > div { + margin-bottom: 8px; + + &:last-child { + margin-bottom: 4px; + } + + > p { + margin: 0 0 2px 0; + font-size: 12px; + line-height: 18px; + opacity: 0.8; + + > b { + margin-left: 2px; + } + } + + > .meter { + width: 100%; + overflow: hidden; + background: var(--aupeazdm); + border-radius: 8px; + + > .val { + height: 4px; + transition: width .3s cubic-bezier(0.23, 1, 0.32, 1); + } + } + + &: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/client/app/common/define-widget.ts b/src/client/widgets/define.ts similarity index 75% rename from src/client/app/common/define-widget.ts rename to src/client/widgets/define.ts index ba4deafe3ae0fea26c1abe6b36d2c84ba5d2a158..768446c128196e0ae4164f39280cec27e6a333c4 100644 --- a/src/client/app/common/define-widget.ts +++ b/src/client/widgets/define.ts @@ -9,14 +9,6 @@ export default function <T extends object>(data: { widget: { type: Object }, - column: { - type: Object, - default: null - }, - platform: { - type: String, - required: true - }, isCustomizeMode: { type: Boolean, default: false @@ -59,11 +51,7 @@ export default function <T extends object>(data: { }, save() { - if (this.platform == 'deck') { - this.$store.commit('updateDeckColumn', this.column); - } else { - this.$store.commit('updateWidget', this.widget); - } + this.$store.dispatch('settings/updateWidget', this.widget); } } }); diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4743be0763b2b5d9f2fd8b66afb68be3684469fa --- /dev/null +++ b/src/client/widgets/index.ts @@ -0,0 +1,8 @@ +import Vue from 'vue'; + +Vue.component('mkw-memo', () => import('./memo.vue').then(m => m.default)); +Vue.component('mkw-notifications', () => import('./notifications.vue').then(m => m.default)); +Vue.component('mkw-timeline', () => import('./timeline.vue').then(m => m.default)); +Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default)); +Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default)); +Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default)); diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue new file mode 100644 index 0000000000000000000000000000000000000000..974c13eb0d38a992669ed39a9838e8c3824338e3 --- /dev/null +++ b/src/client/widgets/memo.vue @@ -0,0 +1,120 @@ +<template> +<div> + <mk-container :show-header="!props.compact"> + <template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template> + + <div class="otgbylcu"> + <textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> + <button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import { faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import MkContainer from '../components/ui/container.vue'; +import define from './define'; +import i18n from '../i18n'; + +export default define({ + name: 'memo', + props: () => ({ + compact: false + }) +}).extend({ + i18n, + + components: { + MkContainer + }, + + data() { + return { + text: null, + changed: false, + timeoutId: null, + faStickyNote + }; + }, + + created() { + this.text = this.$store.state.settings.memo; + + this.$watch('$store.state.settings.memo', text => { + this.text = text; + }); + }, + + methods: { + func() { + this.props.compact = !this.props.compact; + this.save(); + }, + + onChange() { + this.changed = true; + clearTimeout(this.timeoutId); + this.timeoutId = setTimeout(this.saveMemo, 1000); + }, + + saveMemo() { + this.$store.dispatch('settings/set', { + key: 'memo', + value: this.text + }); + this.changed = false; + } + } +}); +</script> + +<style lang="scss" scoped> +.otgbylcu { + padding-bottom: 28px + 16px; + + > textarea { + display: block; + width: 100%; + max-width: 100%; + min-width: 100%; + padding: 16px; + color: var(--inputText); + background: var(--face); + border: none; + border-bottom: solid var(--lineWidth) var(--faceDivider); + border-radius: 0; + } + + > button { + display: block; + position: absolute; + bottom: 8px; + right: 8px; + margin: 0; + padding: 0 10px; + height: 28px; + color: #fff; + background: var(--accent) !important; + outline: none; + border: none; + border-radius: 4px; + transition: background 0.1s ease; + cursor: pointer; + + &:hover { + background: var(--accentLighten10) !important; + } + + &:active { + background: var(--accentDarken) !important; + transition: background 0s ease; + } + + &:disabled { + opacity: 0.7; + cursor: default; + } + } +} +</style> diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue new file mode 100644 index 0000000000000000000000000000000000000000..bc9b3a65a0c02820f378655475715417ef61dca2 --- /dev/null +++ b/src/client/widgets/notifications.vue @@ -0,0 +1,46 @@ +<template> +<div class="mkw-notifications"> + <mk-container :show-header="!props.compact"> + <template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> + + <div style="height: 300px; overflow: auto; background: var(--bg);"> + <x-notifications/> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import { faBell } from '@fortawesome/free-solid-svg-icons'; +import MkContainer from '../components/ui/container.vue'; +import XNotifications from '../components/notifications.vue'; +import define from './define'; +import i18n from '../i18n'; + +export default define({ + name: 'notifications', + props: () => ({ + compact: false + }) +}).extend({ + i18n, + + components: { + MkContainer, + XNotifications, + }, + + data() { + return { + faBell + }; + }, + + methods: { + func() { + this.props.compact = !this.props.compact; + this.save(); + }, + } +}); +</script> diff --git a/src/client/widgets/rss.vue b/src/client/widgets/rss.vue new file mode 100644 index 0000000000000000000000000000000000000000..61c1e23b6e1b32b9506685144470e3b47220c7c6 --- /dev/null +++ b/src/client/widgets/rss.vue @@ -0,0 +1,101 @@ +<template> +<div> + <mk-container :show-header="!props.compact"> + <template #header><fa :icon="faRssSquare"/>RSS</template> + <template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template> + + <div class="ekmkgxbj"> + <mk-loading v-if="fetching"/> + <div class="feed" v-else> + <a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> + </div> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import { faRssSquare, faCog } from '@fortawesome/free-solid-svg-icons'; +import MkContainer from '../components/ui/container.vue'; +import define from './define'; +import i18n from '../i18n'; + +export default define({ + name: 'rss', + props: () => ({ + compact: false, + url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews' + }) +}).extend({ + i18n, + components: { + MkContainer + }, + data() { + return { + items: [], + fetching: true, + clock: null, + faRssSquare, faCog + }; + }, + mounted() { + this.fetch(); + this.clock = setInterval(this.fetch, 60000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + func() { + this.props.compact = !this.props.compact; + this.save(); + }, + fetch() { + fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { + }).then(res => { + res.json().then(feed => { + this.items = feed.items; + this.fetching = false; + }); + }); + }, + setting() { + this.$root.dialog({ + title: 'URL', + input: { + type: 'url', + default: this.props.url + } + }).then(({ canceled, result: url }) => { + if (canceled) return; + this.props.url = url; + this.save(); + this.fetch(); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.ekmkgxbj { + > .feed { + padding: 0; + font-size: 0.9em; + + > a { + display: block; + padding: 8px 16px; + color: var(--text); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + &:nth-child(even) { + background: rgba(#000, 0.05); + } + } + } +} +</style> diff --git a/src/client/widgets/timeline.vue b/src/client/widgets/timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..5a22a0c1a574184d2767edaf9bfd75551e96ad25 --- /dev/null +++ b/src/client/widgets/timeline.vue @@ -0,0 +1,113 @@ +<template> +<div class="mkw-timeline"> + <mk-container :show-header="!props.compact"> + <template #header> + <button @click="choose" class="_button"> + <fa v-if="props.src === 'home'" :icon="faHome"/> + <fa v-if="props.src === 'local'" :icon="faComments"/> + <fa v-if="props.src === 'social'" :icon="faShareAlt"/> + <fa v-if="props.src === 'global'" :icon="faGlobe"/> + <fa v-if="props.src === 'list'" :icon="faListUl"/> + <fa v-if="props.src === 'antenna'" :icon="faSatellite"/> + <span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span> + <fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> + </button> + </template> + + <div style="height: 300px; padding: 8px; overflow: auto; background: var(--bg);"> + <x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite } from '@fortawesome/free-solid-svg-icons'; +import { faComments } from '@fortawesome/free-regular-svg-icons'; +import MkContainer from '../components/ui/container.vue'; +import XTimeline from '../components/timeline.vue'; +import define from './define'; +import i18n from '../i18n'; + +export default define({ + name: 'timeline', + props: () => ({ + src: 'home', + list: null, + compact: false + }) +}).extend({ + i18n, + + components: { + MkContainer, + XTimeline, + }, + + data() { + return { + menuOpened: false, + faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite + }; + }, + + methods: { + func() { + this.props.compact = !this.props.compact; + this.save(); + }, + + async choose(ev) { + this.menuOpened = true; + const [antennas, lists] = await Promise.all([ + this.$root.api('antennas/list'), + this.$root.api('users/lists/list') + ]); + const antennaItems = antennas.map(antenna => ({ + text: antenna.name, + icon: faSatellite, + action: () => { + this.props.antenna = antenna; + this.setSrc('antenna'); + } + })); + const listItems = lists.map(list => ({ + text: list.name, + icon: faListUl, + action: () => { + this.props.list = list; + this.setSrc('list'); + } + })); + this.$root.menu({ + items: [{ + text: this.$t('_timelines.home'), + icon: faHome, + action: () => { this.setSrc('home') } + }, { + text: this.$t('_timelines.local'), + icon: faComments, + action: () => { this.setSrc('local') } + }, { + text: this.$t('_timelines.social'), + icon: faShareAlt, + action: () => { this.setSrc('social') } + }, { + text: this.$t('_timelines.global'), + icon: faGlobe, + action: () => { this.setSrc('global') } + }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], + noCenter: true, + source: ev.currentTarget || ev.target + }).then(() => { + this.menuOpened = false; + }); + }, + + setSrc(src) { + this.props.src = src; + this.save(); + }, + } +}); +</script> diff --git a/src/client/app/common/views/components/trends.chart.vue b/src/client/widgets/trends.chart.vue similarity index 100% rename from src/client/app/common/views/components/trends.chart.vue rename to src/client/widgets/trends.chart.vue diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue new file mode 100644 index 0000000000000000000000000000000000000000..7e887f0f224a06d91281d1e92c855f112e854237 --- /dev/null +++ b/src/client/widgets/trends.vue @@ -0,0 +1,124 @@ +<template> +<div> + <mk-container :show-header="!props.compact"> + <template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template> + + <div class="wbrkwala"> + <transition-group tag="div" name="chart"> + <div v-for="stat in stats" :key="stat.tag"> + <div class="tag"> + <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> + <p>{{ $t('count').replace('{}', stat.usersCount) }}</p> + </div> + <x-chart class="chart" :src="stat.chart"/> + </div> + </transition-group> + </div> + </mk-container> +</div> +</template> + +<script lang="ts"> +import { faHashtag } from '@fortawesome/free-solid-svg-icons'; +import MkContainer from '../components/ui/container.vue'; +import define from './define'; +import i18n from '../i18n'; +import XChart from './trends.chart.vue'; + +export default define({ + name: 'hashtags', + props: () => ({ + compact: false + }) +}).extend({ + i18n, + components: { + MkContainer, XChart + }, + data() { + return { + stats: [], + fetching: true, + faHashtag + }; + }, + mounted() { + this.fetch(); + this.clock = setInterval(this.fetch, 1000 * 60); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + func() { + this.props.compact = !this.props.compact; + this.save(); + }, + fetch() { + this.$root.api('hashtags/trend').then(stats => { + this.stats = stats; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.wbrkwala { + > .fetching, + > .empty { + margin: 0; + padding: 16px; + text-align: center; + color: var(--text); + opacity: 0.7; + + > [data-icon] { + margin-right: 4px; + } + } + + > div { + .chart-move { + transition: transform 1s ease; + } + + > div { + display: flex; + align-items: center; + padding: 14px 16px; + + &:not(:last-child) { + border-bottom: solid 1px var(--divider); + } + + > .tag { + flex: 1; + overflow: hidden; + font-size: 14px; + color: var(--fg); + + > a { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: inherit; + } + + > p { + margin: 0; + font-size: 75%; + opacity: 0.7; + } + } + + > .chart { + height: 30px; + } + } + } +} +</style> diff --git a/src/config/load.ts b/src/config/load.ts index eea9ed8561575afc488899a9b4bdd076c40fa989..3c07494e92682478adc600dcf1262dc20e14c18b 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -41,8 +41,6 @@ export default function load() { mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - if (config.autoAdmin == null) config.autoAdmin = false; - if (!config.redis.prefix) config.redis.prefix = mixin.host; return Object.assign(config, mixin); diff --git a/src/config/types.ts b/src/config/types.ts index aeb2c1233334bdbdab16f660c7b2e7d459d912c0..78ae0251333c6180e277bd050c30e518fd49dcbd 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -32,8 +32,6 @@ export type Source = { ssl?: boolean; }; - autoAdmin?: boolean; - proxy?: string; proxySmtp?: string; diff --git a/src/daemons/notes-stats-child.ts b/src/daemons/notes-stats-child.ts deleted file mode 100644 index b60f5badfd12323d8e4d4eac9257620523612e27..0000000000000000000000000000000000000000 --- a/src/daemons/notes-stats-child.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MoreThanOrEqual, getRepository } from 'typeorm'; -import { Note } from '../models/entities/note'; -import { initDb } from '../db/postgre'; - -const interval = 5000; - -initDb().then(() => { - const Notes = getRepository(Note); - - async function tick() { - const [all, local] = await Promise.all([Notes.count({ - createdAt: MoreThanOrEqual(new Date(Date.now() - interval)) - }), Notes.count({ - createdAt: MoreThanOrEqual(new Date(Date.now() - interval)), - userHost: null - })]); - - const stats = { - all, local - }; - - process.send!(stats); - } - - tick(); - - setInterval(tick, interval); -}); diff --git a/src/daemons/notes-stats.ts b/src/daemons/notes-stats.ts deleted file mode 100644 index bddb54cfa56aeb93c4ca54a8c37d3359993079ce..0000000000000000000000000000000000000000 --- a/src/daemons/notes-stats.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as childProcess from 'child_process'; -import * as Deque from 'double-ended-queue'; -import Xev from 'xev'; - -const ev = new Xev(); - -export default function() { - const log = new Deque<any>(); - - const p = childProcess.fork(__dirname + '/notes-stats-child.js'); - - p.on('message', stats => { - ev.emit('notesStats', stats); - log.push(stats); - if (log.length > 100) log.shift(); - }); - - ev.on('requestNotesStatsLog', id => { - ev.emit(`notesStatsLog:${id}`, log.toArray()); - }); - - process.on('exit', code => { - process.kill(p.pid); - }); - -} diff --git a/src/daemons/queue-stats.ts b/src/daemons/queue-stats.ts index e560354c74ccc8774e5e5d0898d7edefc6705530..288e855ae973aa8f2771aedb71025bbb07403417 100644 --- a/src/daemons/queue-stats.ts +++ b/src/daemons/queue-stats.ts @@ -1,25 +1,22 @@ -import * as Deque from 'double-ended-queue'; import Xev from 'xev'; -import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '../queue'; +import { deliverQueue, inboxQueue } from '../queue'; const ev = new Xev(); -const interval = 3000; +const interval = 10000; /** * Report queue stats regularly */ export default function() { - const log = new Deque<any>(); + const log = [] as any[]; ev.on('requestQueueStatsLog', x => { - ev.emit(`queueStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50)); + ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50)); }); let activeDeliverJobs = 0; let activeInboxJobs = 0; - let activeDbJobs = 0; - let activeObjectStorageJobs = 0; deliverQueue.on('global:active', () => { activeDeliverJobs++; @@ -29,19 +26,9 @@ export default function() { activeInboxJobs++; }); - dbQueue.on('global:active', () => { - activeDbJobs++; - }); - - objectStorageQueue.on('global:active', () => { - activeObjectStorageJobs++; - }); - async function tick() { const deliverJobCounts = await deliverQueue.getJobCounts(); const inboxJobCounts = await inboxQueue.getJobCounts(); - const dbJobCounts = await dbQueue.getJobCounts(); - const objectStorageJobCounts = await objectStorageQueue.getJobCounts(); const stats = { deliver: { @@ -56,18 +43,6 @@ export default function() { waiting: inboxJobCounts.waiting, delayed: inboxJobCounts.delayed }, - db: { - activeSincePrevTick: activeDbJobs, - active: dbJobCounts.active, - waiting: dbJobCounts.waiting, - delayed: dbJobCounts.delayed - }, - objectStorage: { - activeSincePrevTick: activeObjectStorageJobs, - active: objectStorageJobCounts.active, - waiting: objectStorageJobCounts.waiting, - delayed: objectStorageJobCounts.delayed - }, }; ev.emit('queueStats', stats); @@ -77,8 +52,6 @@ export default function() { activeDeliverJobs = 0; activeInboxJobs = 0; - activeDbJobs = 0; - activeObjectStorageJobs = 0; } tick(); diff --git a/src/daemons/server-stats.ts b/src/daemons/server-stats.ts index ee62c32d7ebd3859d1281113db1e5573c8763d90..88df421ba007033cf0f9157896873c67f3748176 100644 --- a/src/daemons/server-stats.ts +++ b/src/daemons/server-stats.ts @@ -1,39 +1,41 @@ -import * as os from 'os'; -import * as sysUtils from 'systeminformation'; -import * as diskusage from 'diskusage'; -import * as Deque from 'double-ended-queue'; +import * as si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; const ev = new Xev(); -const interval = 1000; +const interval = 2000; /** * Report server stats regularly */ export default function() { - const log = new Deque<any>(); + const log = [] as any[]; ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50)); + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); }); async function tick() { const cpu = await cpuUsage(); - const usedmem = await usedMem(); - const totalmem = await totalMem(); - const disk = await diskusage.check(os.platform() == 'win32' ? 'c:' : '/'); + const memStats = await mem(); + const netStats = await net(); + const fsStats = await fs(); const stats = { - cpu_usage: cpu, + cpu: cpu, mem: { - total: totalmem, - used: usedmem + used: memStats.used, + active: memStats.active, }, - disk, - os_uptime: os.uptime(), - process_uptime: process.uptime() + net: { + rx: Math.max(0, netStats.rx_sec), + tx: Math.max(0, netStats.tx_sec), + }, + fs: { + r: Math.max(0, fsStats.rIO_sec), + w: Math.max(0, fsStats.wIO_sec), + } }; ev.emit('serverStats', stats); log.unshift(stats); @@ -54,14 +56,21 @@ function cpuUsage() { }); } -// MEMORY(excl buffer + cache) STAT -async function usedMem() { - const data = await sysUtils.mem(); - return data.active; +// MEMORY STAT +async function mem() { + const data = await si.mem(); + return data; +} + +// NETWORK STAT +async function net() { + const iface = await si.networkInterfaceDefault(); + const data = await si.networkStats(iface); + return data[0]; } -// TOTAL MEMORY STAT -async function totalMem() { - const data = await sysUtils.mem(); - return data.total; +// FS STAT +async function fs() { + const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); + return data; } diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 0947a5983b6d01cb9dbba0a5d133b3bb28120f47..3e12db3a072ae3acfe025a31a12bba683eeadada 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -49,6 +49,12 @@ import { Page } from '../models/entities/page'; import { PageLike } from '../models/entities/page-like'; import { ModerationLog } from '../models/entities/moderation-log'; import { UsedUsername } from '../models/entities/used-username'; +import { Announcement } from '../models/entities/announcement'; +import { AnnouncementRead } from '../models/entities/announcement-read'; +import { Clip } from '../models/entities/clip'; +import { ClipNote } from '../models/entities/clip-note'; +import { Antenna } from '../models/entities/antenna'; +import { AntennaNote } from '../models/entities/antenna-note'; const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); @@ -85,6 +91,8 @@ class MyCustomLogger implements Logger { } export const entities = [ + Announcement, + AnnouncementRead, Meta, Instance, App, @@ -128,6 +136,10 @@ export const entities = [ MessagingMessage, Signin, ModerationLog, + Clip, + ClipNote, + Antenna, + AntennaNote, ReversiGame, ReversiMatching, ...charts as any diff --git a/src/docs/style.styl b/src/docs/style.styl index 96d14c2b981337eb9d1f85b0eb9b9011acae575d..6b63cab7d7a56e401caee09c2311af55244247fe 100644 --- a/src/docs/style.styl +++ b/src/docs/style.styl @@ -2,7 +2,7 @@ @import "./ui" html - --primary #fb4e4e + --accent #fb4e4e --link #fb4e4e --linkTapHighlight #fb4e4eb3 diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts new file mode 100644 index 0000000000000000000000000000000000000000..b527d34354f83fba6bc0499fb348be523f8bf73c --- /dev/null +++ b/src/misc/check-hit-antenna.ts @@ -0,0 +1,53 @@ +import { Antenna } from '../models/entities/antenna'; +import { Note } from '../models/entities/note'; +import { User } from '../models/entities/user'; +import { UserListJoinings } from '../models'; +import parseAcct from './acct/parse'; +import { getFullApAccount } from './convert-host'; + +export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: User, followers: User['id'][]): Promise<boolean> { + if (note.visibility === 'specified') return false; + + if (note.visibility === 'followers') { + if (!followers.includes(antenna.userId)) return false; + } + + if (!antenna.withReplies && note.replyId != null) return false; + + if (antenna.src === 'home') { + if (!followers.includes(antenna.userId)) return false; + } else if (antenna.src === 'list') { + const listUsers = (await UserListJoinings.find({ + userListId: antenna.userListId! + })).map(x => x.userId); + + if (!listUsers.includes(note.userId)) return false; + } else if (antenna.src === 'users') { + const accts = antenna.users.map(x => { + const { username, host } = parseAcct(x); + return getFullApAccount(username, host).toLowerCase(); + }); + if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; + } + + if (antenna.keywords.length > 0) { + if (note.text == null) return false; + + const matched = antenna.keywords.some(keywords => + keywords.every(keyword => + antenna.caseSensitive + ? note.text!.includes(keyword) + : note.text!.toLowerCase().includes(keyword.toLowerCase()) + )); + + if (!matched) return false; + } + + if (antenna.withFile) { + if (note.fileIds.length === 0) return false; + } + + // TODO: eval expression + + return true; +} diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts index ced90ce78f5f90db389efbf5880876690fda8c55..eba57ea30388f2fa04addf2fe5587875e779e487 100644 --- a/src/misc/reaction-lib.ts +++ b/src/misc/reaction-lib.ts @@ -2,31 +2,29 @@ import { emojiRegex } from './emoji-regex'; import { fetchMeta } from './fetch-meta'; import { Emojis } from '../models'; -const basic10: Record<string, string> = { - 'ðŸ‘': 'like', - 'â¤': 'love', // ã“ã“ã«è¨˜è¿°ã™ã‚‹å ´åˆã¯ç•°ä½“å—セレクタを入れãªã„ - '😆': 'laugh', - '🤔': 'hmm', - '😮': 'surprise', - '🎉': 'congrats', - '💢': 'angry', - '😥': 'confused', - '😇': 'rip', - 'ðŸ®': 'pudding', +const legacy10: Record<string, string> = { + 'like': 'ðŸ‘', + 'love': 'â¤', // ã“ã“ã«è¨˜è¿°ã™ã‚‹å ´åˆã¯ç•°ä½“å—セレクタを入れãªã„ + 'laugh': '😆', + 'hmm': '🤔', + 'surprise': '😮', + 'congrats': '🎉', + 'angry': '💢', + 'confused': '😥', + 'rip': '😇', + 'pudding': 'ðŸ®', }; export async function getFallbackReaction(): Promise<string> { const meta = await fetchMeta(); - return meta.useStarForReactionFallback ? 'star' : 'like'; + return meta.useStarForReactionFallback ? 'â' : 'ðŸ‘'; } -export async function toDbReaction(reaction?: string | null, enableEmoji = true): Promise<string> { +export async function toDbReaction(reaction?: string | null): Promise<string> { if (reaction == null) return await getFallbackReaction(); - // æ—¢å˜ã®æ–‡å—列リアクションã¯ãã®ã¾ã¾ - if (Object.values(basic10).includes(reaction)) return reaction; - - if (!enableEmoji) return await getFallbackReaction(); + // æ–‡å—列タイプã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’絵文å—ã«å¤‰æ› + if (Object.keys(legacy10).includes(reaction)) return legacy10[reaction]; // Unicodeçµµæ–‡å— const match = emojiRegex.exec(reaction); @@ -34,17 +32,8 @@ export async function toDbReaction(reaction?: string | null, enableEmoji = true) // åˆå—ã‚’å«ã‚€1ã¤ã®çµµæ–‡å— const unicode = match[0]; - // 異体å—セレクタ除去後ã®çµµæ–‡å— - const normalized = unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); - - // Unicodeプリンã¯å¯¿å¸åŒ–ä¸èƒ½ã¨ã™ã‚‹ãŸã‚æ–‡å—列化ã—ãªã„ - if (normalized === 'ðŸ®') return normalized; - - // プリン以外ã®æ—¢å˜ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯æ–‡å—列化ã™ã‚‹ - if (basic10[normalized]) return basic10[normalized]; - - // ãれ以外ã¯Unicodeã®ã¾ã¾ - return normalized; + // 異体å—セレクタ除去 + return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); } const custom = reaction.match(/^:([\w+-]+):$/); @@ -59,3 +48,8 @@ export async function toDbReaction(reaction?: string | null, enableEmoji = true) return await getFallbackReaction(); } + +export function convertLegacyReaction(reaction: string): string { + if (Object.keys(legacy10).includes(reaction)) return legacy10[reaction]; + return reaction; +} diff --git a/src/models/entities/announcement-read.ts b/src/models/entities/announcement-read.ts new file mode 100644 index 0000000000000000000000000000000000000000..892beb826f8a39eaaee3ca5b117f6584f6cb335a --- /dev/null +++ b/src/models/entities/announcement-read.ts @@ -0,0 +1,36 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Announcement } from './announcement'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'announcementId'], { unique: true }) +export class AnnouncementRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AnnouncementRead.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public announcementId: Announcement['id']; + + @ManyToOne(type => Announcement, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public announcement: Announcement | null; +} diff --git a/src/models/entities/announcement.ts b/src/models/entities/announcement.ts new file mode 100644 index 0000000000000000000000000000000000000000..06d379c2295f5786b9202735ae5bf31467e71c38 --- /dev/null +++ b/src/models/entities/announcement.ts @@ -0,0 +1,43 @@ +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Announcement { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Announcement.' + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the Announcement.', + nullable: true + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 8192, nullable: false + }) + public text: string; + + @Column('varchar', { + length: 256, nullable: false + }) + public title: string; + + @Column('varchar', { + length: 1024, nullable: true + }) + public imageUrl: string | null; + + constructor(data: Partial<Announcement>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/entities/antenna-note.ts b/src/models/entities/antenna-note.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b911524efc6cbf67c5c8e607101384761094c6d --- /dev/null +++ b/src/models/entities/antenna-note.ts @@ -0,0 +1,43 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { Note } from './note'; +import { Antenna } from './antenna'; +import { id } from '../id'; + +@Entity() +@Index(['noteId', 'antennaId'], { unique: true }) +export class AntennaNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.' + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The antenna ID.' + }) + public antennaId: Antenna['id']; + + @ManyToOne(type => Antenna, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public antenna: Antenna | null; + + @Index() + @Column('boolean', { + default: false + }) + public read: boolean; +} diff --git a/src/models/entities/antenna.ts b/src/models/entities/antenna.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9971c6c07e1dbf29b523e16c74aae6e5282210f --- /dev/null +++ b/src/models/entities/antenna.ts @@ -0,0 +1,81 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; +import { UserList } from './user-list'; + +@Entity() +export class Antenna { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Antenna.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the Antenna.' + }) + public name: string; + + @Column('enum', { enum: ['home', 'all', 'users', 'list'] }) + public src: 'home' | 'all' | 'users' | 'list'; + + @Column({ + ...id(), + nullable: true + }) + public userListId: UserList['id'] | null; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userList: UserList | null; + + @Column('varchar', { + length: 1024, array: true, + default: '{}' + }) + public users: string[]; + + @Column('jsonb', { + default: [] + }) + public keywords: string[][]; + + @Column('boolean', { + default: false + }) + public caseSensitive: boolean; + + @Column('boolean', { + default: false + }) + public withReplies: boolean; + + @Column('boolean') + public withFile: boolean; + + @Column('varchar', { + length: 2048, nullable: true, + }) + public expression: string | null; + + @Column('boolean') + public notify: boolean; +} diff --git a/src/models/entities/clip-note.ts b/src/models/entities/clip-note.ts new file mode 100644 index 0000000000000000000000000000000000000000..19e4750fc6f42908bd52b0a35ae96424fcaa0f91 --- /dev/null +++ b/src/models/entities/clip-note.ts @@ -0,0 +1,37 @@ +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { Note } from './note'; +import { Clip } from './clip'; +import { id } from '../id'; + +@Entity() +@Index(['noteId', 'clipId'], { unique: true }) +export class ClipNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.' + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Index() + @Column({ + ...id(), + comment: 'The clip ID.' + }) + public clipId: Clip['id']; + + @ManyToOne(type => Clip, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public clip: Clip | null; +} diff --git a/src/models/entities/clip.ts b/src/models/entities/clip.ts new file mode 100644 index 0000000000000000000000000000000000000000..37d21f73b11ed4e00c4bb78e9f9f26d12fe11f93 --- /dev/null +++ b/src/models/entities/clip.ts @@ -0,0 +1,38 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class Clip { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Clip.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the Clip.' + }) + public name: string; + + @Column('boolean', { + default: false + }) + public isPublic: boolean; +} diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index e5b189ef836f0bb289242a6d5fba10182838b2d1..4063c811391022aabbfd26bd85f4a38ea1bf4404 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -34,11 +34,6 @@ export class Meta { }) public maintainerEmail: string | null; - @Column('jsonb', { - default: [], - }) - public announcements: Record<string, any>[]; - @Column('boolean', { default: false, }) @@ -54,11 +49,6 @@ export class Meta { }) public disableGlobalTimeline: boolean; - @Column('boolean', { - default: true, - }) - public enableEmojiReaction: boolean; - @Column('boolean', { default: false, }) diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts index d67faae829cff901e2127f5ebd7ede43598e501b..c77c7bd8d6484fd7069034aa04e71519d8695721 100644 --- a/src/models/entities/note.ts +++ b/src/models/entities/note.ts @@ -58,18 +58,6 @@ export class Note { }) public cw: string | null; - @Column({ - ...id(), - nullable: true - }) - public appId: App['id'] | null; - - @ManyToOne(type => App, { - onDelete: 'SET NULL' - }) - @JoinColumn() - public app: App | null; - @Index() @Column({ ...id(), @@ -177,11 +165,6 @@ export class Note { }) public hasPoll: boolean; - @Column('jsonb', { - nullable: true, default: null - }) - public geo: any | null; - //#region Denormalized fields @Index() @Column('varchar', { diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts index 627a57bececeedd972c6de0c9405ca435ce386e6..e359640e828b14203d1f17a4297030d82c91d58d 100644 --- a/src/models/entities/notification.ts +++ b/src/models/entities/notification.ts @@ -2,6 +2,7 @@ import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typ import { User } from './user'; import { id } from '../id'; import { Note } from './note'; +import { FollowRequest } from './follow-request'; @Entity() export class Notification { @@ -54,12 +55,14 @@ export class Notification { * quote - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ãŒå¼•ç”¨Renoteã•ã‚ŒãŸ * reaction - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã•ã‚ŒãŸ * pollVote - (自分ã¾ãŸã¯è‡ªåˆ†ãŒWatchã—ã¦ã„ã‚‹)投稿ã®æŠ•ç¥¨ã«æŠ•ç¥¨ã•ã‚ŒãŸ + * receiveFollowRequest - フォãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚ŒãŸ + * followRequestAccepted - 自分ã®é€ã£ãŸãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæ‰¿èªã•ã‚ŒãŸ */ - @Column('varchar', { - length: 32, + @Column('enum', { + enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'], comment: 'The type of the Notification.' }) - public type: string; + public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted'; /** * 通知ãŒèªã¾ã‚ŒãŸã‹ã©ã†ã‹ @@ -82,6 +85,18 @@ export class Notification { @JoinColumn() public note: Note | null; + @Column({ + ...id(), + nullable: true + }) + public followRequestId: FollowRequest['id'] | null; + + @ManyToOne(type => FollowRequest, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public followRequest: FollowRequest | null; + @Column('varchar', { length: 128, nullable: true }) diff --git a/src/models/index.ts b/src/models/index.ts index fc40ebfb230895c89a56b5d88ff52e27dabdb3a9..15a5c5470dca4ab3d674683abe47c58b63c691e2 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,6 @@ import { getRepository, getCustomRepository } from 'typeorm'; +import { Announcement } from './entities/announcement'; +import { AnnouncementRead } from './entities/announcement-read'; import { Instance } from './entities/instance'; import { Emoji } from './entities/emoji'; import { Poll } from './entities/poll'; @@ -44,7 +46,13 @@ import { PageRepository } from './repositories/page'; import { PageLikeRepository } from './repositories/page-like'; import { ModerationLogRepository } from './repositories/moderation-logs'; import { UsedUsername } from './entities/used-username'; +import { ClipRepository } from './repositories/clip'; +import { ClipNote } from './entities/clip-note'; +import { AntennaRepository } from './repositories/antenna'; +import { AntennaNote } from './entities/antenna-note'; +export const Announcements = getRepository(Announcement); +export const AnnouncementReads = getRepository(AnnouncementRead); export const Apps = getCustomRepository(AppRepository); export const Notes = getCustomRepository(NoteRepository); export const NoteFavorites = getCustomRepository(NoteFavoriteRepository); @@ -90,3 +98,7 @@ export const Logs = getRepository(Log); export const Pages = getCustomRepository(PageRepository); export const PageLikes = getCustomRepository(PageLikeRepository); export const ModerationLogs = getCustomRepository(ModerationLogRepository); +export const Clips = getCustomRepository(ClipRepository); +export const ClipNotes = getRepository(ClipNote); +export const Antennas = getCustomRepository(AntennaRepository); +export const AntennaNotes = getRepository(AntennaNote); diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts new file mode 100644 index 0000000000000000000000000000000000000000..c47a7ea35c13298770a1ecba0b323fcfecbc1af7 --- /dev/null +++ b/src/models/repositories/antenna.ts @@ -0,0 +1,58 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Antenna } from '../entities/antenna'; +import { ensure } from '../../prelude/ensure'; +import { SchemaType } from '../../misc/schema'; +import { AntennaNotes } from '..'; + +export type PackedAntenna = SchemaType<typeof packedAntennaSchema>; + +@EntityRepository(Antenna) +export class AntennaRepository extends Repository<Antenna> { + public async pack( + src: Antenna['id'] | Antenna, + ): Promise<PackedAntenna> { + const antenna = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null; + + return { + id: antenna.id, + createdAt: antenna.createdAt.toISOString(), + name: antenna.name, + keywords: antenna.keywords, + src: antenna.src, + userListId: antenna.userListId, + users: antenna.users, + caseSensitive: antenna.caseSensitive, + notify: antenna.notify, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + hasUnreadNote + }; + } +} + +export const packedAntennaSchema = { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + description: 'The unique identifier for this Antenna.', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'date-time', + description: 'The date that the Antenna was created.' + }, + name: { + type: 'string' as const, + optional: false as const, nullable: false as const, + description: 'The name of the Antenna.' + }, + }, +}; diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts new file mode 100644 index 0000000000000000000000000000000000000000..9644ceec7e7267c1c0f98da92691b73392dabe5d --- /dev/null +++ b/src/models/repositories/clip.ts @@ -0,0 +1,46 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Clip } from '../entities/clip'; +import { ensure } from '../../prelude/ensure'; +import { SchemaType } from '../../misc/schema'; + +export type PackedClip = SchemaType<typeof packedClipSchema>; + +@EntityRepository(Clip) +export class ClipRepository extends Repository<Clip> { + public async pack( + src: Clip['id'] | Clip, + ): Promise<PackedClip> { + const clip = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: clip.id, + createdAt: clip.createdAt.toISOString(), + name: clip.name, + }; + } +} + +export const packedClipSchema = { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + description: 'The unique identifier for this Clip.', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'date-time', + description: 'The date that the Clip was created.' + }, + name: { + type: 'string' as const, + optional: false as const, nullable: false as const, + description: 'The name of the Clip.' + }, + }, +}; diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts index 79b4989a20851d20dd3fbdd183a0e44cfc9e7265..3439f3c8cb8334857c7bac9d06a2580476a86d1e 100644 --- a/src/models/repositories/note-reaction.ts +++ b/src/models/repositories/note-reaction.ts @@ -3,6 +3,7 @@ import { NoteReaction } from '../entities/note-reaction'; import { Users } from '..'; import { ensure } from '../../prelude/ensure'; import { SchemaType } from '../../misc/schema'; +import { convertLegacyReaction } from '../../misc/reaction-lib'; export type PackedNoteReaction = SchemaType<typeof packedNoteReactionSchema>; @@ -18,7 +19,7 @@ export class NoteReactionRepository extends Repository<NoteReaction> { id: reaction.id, createdAt: reaction.createdAt.toISOString(), user: await Users.pack(reaction.userId, me), - type: reaction.reaction, + type: convertLegacyReaction(reaction.reaction), }; } } diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 9c01bf84d5161c988913cbb0e745e97a2ea75425..73eb2f05383f171161f0319ea7ea6cb1a495438a 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -7,6 +7,7 @@ import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, import { ensure } from '../../prelude/ensure'; import { SchemaType } from '../../misc/schema'; import { awaitAll } from '../../prelude/await-all'; +import { convertLegacyReaction } from '../../misc/reaction-lib'; export type PackedNote = SchemaType<typeof packedNoteSchema>; @@ -71,7 +72,6 @@ export class NoteRepository extends Repository<Note> { packedNote.text = null; packedNote.poll = undefined; packedNote.cw = null; - packedNote.geo = undefined; packedNote.isHidden = true; } } @@ -163,7 +163,7 @@ export class NoteRepository extends Repository<Note> { }); if (reaction) { - return reaction.reaction; + return convertLegacyReaction(reaction.reaction); } return undefined; @@ -178,7 +178,6 @@ export class NoteRepository extends Repository<Note> { const packed = await awaitAll({ id: note.id, createdAt: note.createdAt.toISOString(), - app: note.appId ? Apps.pack(note.appId) : undefined, userId: note.userId, user: Users.pack(note.user || note.userId, meId), text: text, @@ -189,7 +188,7 @@ export class NoteRepository extends Repository<Note> { viaMobile: note.viaMobile || undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactions: note.reactions, + reactions: note.reactions, // v12 TODO: convert legacy reaction tags: note.tags.length > 0 ? note.tags : undefined, emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)), fileIds: note.fileIds, @@ -356,9 +355,6 @@ export const packedNoteSchema = { type: 'object' as const, optional: true as const, nullable: true as const, }, - geo: { - type: 'object' as const, - optional: true as const, nullable: true as const, - }, + }, }; diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts index 4a1036816096d11c355cf147bd3191193e062c71..6407c19d4c559336ad42ee5e6e49c1fda7acf786 100644 --- a/src/models/repositories/notification.ts +++ b/src/models/repositories/notification.ts @@ -70,7 +70,7 @@ export const packedNotificationSchema = { type: { type: 'string' as const, optional: false as const, nullable: false as const, - enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'], + enum: ['follow', 'followRequestAccepted', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'], description: 'The type of the notification.' }, userId: { diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 5c8cbb3d17e78e500f55d9fff1227f2a3609bdd7..c86fd6e1fc17adea9f08dd69024bc4002d182991 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { EntityRepository, Repository, In, Not } from 'typeorm'; import { User, ILocalUser, IRemoteUser } from '../entities/user'; -import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages } from '..'; +import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes } from '..'; import { ensure } from '../../prelude/ensure'; import config from '../../config'; import { SchemaType } from '../../misc/schema'; @@ -84,6 +84,47 @@ export class UserRepository extends Repository<User> { return withUser || withGroups.some(x => x); } + public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> { + const reads = await AnnouncementReads.find({ + userId: userId + }); + + const count = await Announcements.count(reads.length > 0 ? { + id: Not(In(reads.map(read => read.announcementId))) + } : {}); + + return count > 0; + } + + public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> { + const antennas = await Antennas.find({ userId }); + + const unread = antennas.length > 0 ? await AntennaNotes.findOne({ + antennaId: In(antennas.map(x => x.id)), + read: false + }) : null; + + return unread != null; + } + + public async getHasUnreadNotification(userId: User['id']): Promise<boolean> { + const mute = await Mutings.find({ + muterId: userId + }); + const mutedUserIds = mute.map(m => m.muteeId); + + const count = await Notifications.count({ + where: { + notifieeId: userId, + ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), + isRead: false + }, + take: 1 + }); + + return count > 0; + } + public async pack( src: User['id'] | User, me?: User['id'] | User | null | undefined, @@ -193,14 +234,10 @@ export class UserRepository extends Repository<User> { alwaysMarkNsfw: profile!.alwaysMarkNsfw, carefulBot: profile!.carefulBot, autoAcceptFollowed: profile!.autoAcceptFollowed, + hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), + hasUnreadAntenna: this.getHasUnreadAntenna(user.id), hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), - hasUnreadNotification: Notifications.count({ - where: { - notifieeId: user.id, - isRead: false - }, - take: 1 - }).then(count => count > 0), + hasUnreadNotification: this.getHasUnreadNotification(user.id), pendingReceivedFollowRequestsCount: FollowRequests.count({ followeeId: user.id }), diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts index 94a4302e052728faafdb0995247b5c0f9b92f52b..0fd8c02c4ad6731d546b877742de2bc80a796b39 100644 --- a/src/queue/processors/db/export-notes.ts +++ b/src/queue/processors/db/export-notes.ts @@ -128,8 +128,6 @@ function serialize(note: Note, poll: Poll | null = null): any { viaMobile: note.viaMobile, visibility: note.visibility, visibleUserIds: note.visibleUserIds, - appId: note.appId, - geo: note.geo, localOnly: note.localOnly }; } diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts index 1a583ec865eead8ad880a80c80b95427edd54745..74108f354b7bdf6c4b542b26068cb275c2c3088c 100644 --- a/src/queue/processors/inbox.ts +++ b/src/queue/processors/inbox.ts @@ -3,7 +3,6 @@ import * as httpSignature from 'http-signature'; import { IRemoteUser } from '../../models/entities/user'; import perform from '../../remote/activitypub/perform'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; -import { publishApLogStream } from '../../services/stream'; import Logger from '../../services/logger'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; import { Instances, Users, UserPublickeys } from '../../models'; @@ -89,15 +88,6 @@ export default async (job: Bull.Job): Promise<void> => { return; } - //#region Log - publishApLogStream({ - direction: 'in', - activity: activity.type, - host: user.host, - actor: user.username - }); - //#endregion - // Update stats registerOrFetchInstanceDoc(user.host).then(i => { Instances.update(i.id, { diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 10cd68f6adf7d476088b92e8298228abd9fda358..d5c83208b6c404da60f4adb18fc7552683705fde 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -267,7 +267,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s text, viaMobile: false, localOnly: false, - geo: undefined, visibility, visibleUsers, apMentions, diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 869fabd032388a6eb2346d57ca9be095d5c37755..dc777a3c5d5fd554b6661c7c652b08cb04616f41 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -5,7 +5,6 @@ import * as cache from 'lookup-dns-cache'; import config from '../../config'; import { ILocalUser } from '../../models/entities/user'; -import { publishApLogStream } from '../../services/stream'; import { UserKeypairs } from '../../models'; import { ensure } from '../../prelude/ensure'; import * as httpsProxyAgent from 'https-proxy-agent'; @@ -69,13 +68,4 @@ export default async (user: ILocalUser, url: string, object: any) => { req.end(data); }); - - //#region Log - publishApLogStream({ - direction: 'out', - activity: object.type, - host: null, - actor: user.username - }); - //#endregion }; diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index c8d43ba286266cfe3ee7e2c1d34a60f7989b89f7..f686446c5c3a99ba67b431fb801b0f14c4ce5130 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -1,8 +1,8 @@ import { publishMainStream } from '../../../services/stream'; import { User } from '../../../models/entities/user'; import { Notification } from '../../../models/entities/notification'; -import { Mutings, Notifications } from '../../../models'; -import { In, Not } from 'typeorm'; +import { Notifications, Users } from '../../../models'; +import { In } from 'typeorm'; /** * Mark notifications as read @@ -11,11 +11,6 @@ export async function readNotification( userId: User['id'], notificationIds: Notification['id'][] ) { - const mute = await Mutings.find({ - muterId: userId - }); - const mutedUserIds = mute.map(m => m.muteeId); - // Update documents await Notifications.update({ id: In(notificationIds), @@ -24,14 +19,7 @@ export async function readNotification( isRead: true }); - // Calc count of my unread notifications - const count = await Notifications.count({ - notifieeId: userId, - ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), - isRead: false - }); - - if (count === 0) { + if (!await Users.getHasUnreadNotification(userId)) { // å…¨ã¦ã®(ã„ã¾ã¾ã§æœªèªã ã£ãŸ)通知を(ã“ã‚Œã§)èªã¿ã¾ã—ãŸã‚ˆã¨ã„ã†ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 publishMainStream(userId, 'readAllNotifications'); } diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index 66f89182d290417dba72f8151e0c496c984ad1fd..aa2786f8fcf312ee0eb0d18ee46010055515b144 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -24,7 +24,10 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { ctx.redirect(config.url); } else { - ctx.body = { i: user.token }; + ctx.body = { + id: user.id, + i: user.token + }; ctx.status = 200; } diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0eb27e5e4546dac6f98f87ab18ed0fbd4d408a9 --- /dev/null +++ b/src/server/api/common/signup.ts @@ -0,0 +1,104 @@ +import * as bcrypt from 'bcryptjs'; +import { generateKeyPair } from 'crypto'; +import generateUserToken from './generate-native-user-token'; +import { User } from '../../../models/entities/user'; +import { Users, UsedUsernames } from '../../../models'; +import { UserProfile } from '../../../models/entities/user-profile'; +import { getConnection } from 'typeorm'; +import { genId } from '../../../misc/gen-id'; +import { toPunyNullable } from '../../../misc/convert-host'; +import { UserKeypair } from '../../../models/entities/user-keypair'; +import { usersChart } from '../../../services/chart'; +import { UsedUsername } from '../../../models/entities/used-username'; + +export async function signup(username: User['username'], password: UserProfile['password'], host: string | null = null) { + // Validate username + if (!Users.validateLocalUsername.ok(username)) { + throw new Error('INVALID_USERNAME'); + } + + // Validate password + if (!Users.validatePassword.ok(password)) { + throw new Error('INVALID_PASSWORD'); + } + + const usersCount = await Users.count({}); + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + // Generate secret + const secret = generateUserToken(); + + // Check username duplication + if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) { + throw new Error('DUPLICATED_USERNAME'); + } + + // Check deleted username duplication + if (await UsedUsernames.findOne({ username: username.toLowerCase() })) { + throw new Error('USED_USERNAME'); + } + + const keyPair = await new Promise<string[]>((res, rej) => + generateKeyPair('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: undefined, + passphrase: undefined + } + } as any, (err, publicKey, privateKey) => + err ? rej(err) : res([publicKey, privateKey]) + )); + + let account!: User; + + // Start transaction + await getConnection().transaction(async transactionalEntityManager => { + const exist = await transactionalEntityManager.findOne(User, { + usernameLower: username.toLowerCase(), + host: null + }); + + if (exist) throw new Error(' the username is already used'); + + account = await transactionalEntityManager.save(new User({ + id: genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: toPunyNullable(host), + token: secret, + isAdmin: usersCount === 0, + })); + + await transactionalEntityManager.save(new UserKeypair({ + publicKey: keyPair[0], + privateKey: keyPair[1], + userId: account.id + })); + + await transactionalEntityManager.save(new UserProfile({ + userId: account.id, + autoAcceptFollowed: true, + autoWatch: false, + password: hash, + })); + + await transactionalEntityManager.save(new UsedUsername({ + createdAt: new Date(), + username: username.toLowerCase(), + })); + }); + + usersChart.update(account, true); + + return { account, secret }; +} diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac80b579b76127258817725c59aaa9bb55771dd1 --- /dev/null +++ b/src/server/api/endpoints/admin/accounts/create.ts @@ -0,0 +1,33 @@ +import define from '../../../define'; +import { Users } from '../../../../../models'; +import { signup } from '../../../common/signup'; + +export const meta = { + tags: ['admin'], + + params: { + username: { + validator: Users.validateLocalUsername, + }, + + password: { + validator: Users.validatePassword, + } + } +}; + +export default define(meta, async (ps, me) => { + const noUsers = (await Users.count({})) === 0; + if (!noUsers && me == null) throw new Error('access denied'); + + const { account, secret } = await signup(ps.username, ps.password); + + const res = await Users.pack(account, account, { + detail: true, + includeSecrets: true + }); + + (res as any).token = secret; + + return res; +}); diff --git a/src/server/api/endpoints/admin/announcements/create.ts b/src/server/api/endpoints/admin/announcements/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1d48a7d3857c7c7f00981b23f14b06af52a9da9 --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/create.ts @@ -0,0 +1,36 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { Announcements } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + title: { + validator: $.str.min(1) + }, + text: { + validator: $.str.min(1) + }, + imageUrl: { + validator: $.nullable.str.min(1) + } + } +}; + +export default define(meta, async (ps) => { + const announcement = await Announcements.save({ + id: genId(), + createdAt: new Date(), + updatedAt: null, + title: ps.title, + text: ps.text, + imageUrl: ps.imageUrl, + }); + + return announcement; +}); diff --git a/src/server/api/endpoints/admin/announcements/delete.ts b/src/server/api/endpoints/admin/announcements/delete.ts new file mode 100644 index 0000000000000000000000000000000000000000..284b4bf54925a1ff6d3e08df57b6c05c9898bb6a --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/delete.ts @@ -0,0 +1,34 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '../../../../../misc/cafy-id'; +import { Announcements } from '../../../../../models'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + id: { + validator: $.type(ID) + } + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: 'ecad8040-a276-4e85-bda9-015a708d291e' + } + } +}; + +export default define(meta, async (ps, me) => { + const announcement = await Announcements.findOne(ps.id); + + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + + await Announcements.delete(announcement.id); +}); diff --git a/src/server/api/endpoints/admin/announcements/list.ts b/src/server/api/endpoints/admin/announcements/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4e622144e5fa5366c21e8d98d5a3145910fb824 --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/list.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { Announcements, AnnouncementReads } from '../../../../../models'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps) => { + const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + + const announcements = await query.take(ps.limit!).getMany(); + + for (const announcement of announcements) { + (announcement as any).reads = await AnnouncementReads.count({ + announcementId: announcement.id + }); + } + + return announcements; +}); diff --git a/src/server/api/endpoints/admin/announcements/update.ts b/src/server/api/endpoints/admin/announcements/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..b65c3a4f93041ef2cd9de697a35eac0a6aeafd89 --- /dev/null +++ b/src/server/api/endpoints/admin/announcements/update.ts @@ -0,0 +1,48 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '../../../../../misc/cafy-id'; +import { Announcements } from '../../../../../models'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + id: { + validator: $.type(ID) + }, + title: { + validator: $.str.min(1) + }, + text: { + validator: $.str.min(1) + }, + imageUrl: { + validator: $.nullable.str.min(1) + } + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc' + } + } +}; + +export default define(meta, async (ps, me) => { + const announcement = await Announcements.findOne(ps.id); + + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + + await Announcements.update(announcement.id, { + updatedAt: new Date(), + title: ps.title, + text: ps.text, + imageUrl: ps.imageUrl, + }); +}); diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a3e74c33322f009e4d10c228ab1e825042a6801 --- /dev/null +++ b/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { Emojis } from '../../../../../models'; +import { toPuny } from '../../../../../misc/convert-host'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { ID } from '../../../../../misc/cafy-id'; + +export const meta = { + desc: { + 'ja-JP': 'カスタム絵文å—ã‚’å–å¾—ã—ã¾ã™ã€‚' + }, + + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + host: { + validator: $.optional.nullable.str, + default: null as any + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + } + } +}; + +export default define(meta, async (ps) => { + const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); + + if (ps.host == null) { + q.andWhere(`emoji.host IS NOT NULL`); + } else { + q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) }); + } + + const emojis = await q + .orderBy('emoji.category', 'ASC') + .orderBy('emoji.name', 'ASC') + .take(ps.limit!) + .getMany(); + + return emojis.map(e => ({ + id: e.id, + name: e.name, + category: e.category, + aliases: e.aliases, + host: e.host, + url: e.url + })); +}); diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts index d2a5e7df0d61fa1b487177dea01e417cf1703d7b..d525a659c01775375dfeaabdcab3f189681e8359 100644 --- a/src/server/api/endpoints/admin/emoji/list.ts +++ b/src/server/api/endpoints/admin/emoji/list.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; import { Emojis } from '../../../../../models'; -import { toPunyNullable } from '../../../../../misc/convert-host'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { ID } from '../../../../../misc/cafy-id'; export const meta = { desc: { @@ -14,23 +15,28 @@ export const meta = { requireModerator: true, params: { - host: { - validator: $.optional.nullable.str, - default: null as any + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), } } }; export default define(meta, async (ps) => { - const emojis = await Emojis.find({ - where: { - host: toPunyNullable(ps.host) - }, - order: { - category: 'ASC', - name: 'ASC' - } - }); + const emojis = await makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) + .andWhere(`emoji.host IS NULL`) + .orderBy('emoji.category', 'ASC') + .orderBy('emoji.name', 'ASC') + .take(ps.limit!) + .getMany(); return emojis.map(e => ({ id: e.id, diff --git a/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/src/server/api/endpoints/admin/queue/deliver-delayed.ts new file mode 100644 index 0000000000000000000000000000000000000000..d33837c09925add93d5f5c88c2dec1bf014a8d96 --- /dev/null +++ b/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -0,0 +1,31 @@ +import define from '../../../define'; +import { deliverQueue } from '../../../../../queue'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + } +}; + +export default define(meta, async (ps) => { + const jobs = await deliverQueue.getJobs(['delayed']); + + const res = [] as [string, number][]; + + for (const job of jobs) { + const host = new URL(job.data.to).host; + if (res.find(x => x[0] === host)) { + res.find(x => x[0] === host)![1]++; + } else { + res.push([host, 1]); + } + } + + res.sort((a, b) => b[1] - a[1]); + + return res; +}); diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts new file mode 100644 index 0000000000000000000000000000000000000000..643e22f10da014017d36a57a112c1ffbf19ca715 --- /dev/null +++ b/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -0,0 +1,31 @@ +import define from '../../../define'; +import { inboxQueue } from '../../../../../queue'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + } +}; + +export default define(meta, async (ps) => { + const jobs = await inboxQueue.getJobs(['delayed']); + + const res = [] as [string, number][]; + + for (const job of jobs) { + const host = new URL(job.data.signature.keyId).host; + if (res.find(x => x[0] === host)) { + res.find(x => x[0] === host)![1]++; + } else { + res.push([host, 1]); + } + } + + res.sort((a, b) => b[1] - a[1]); + + return res; +}); diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts new file mode 100644 index 0000000000000000000000000000000000000000..f51040a2c88f4c408aedcc1895ca22754b07813d --- /dev/null +++ b/src/server/api/endpoints/admin/server-info.ts @@ -0,0 +1,45 @@ +import * as os from 'os'; +import * as si from 'systeminformation'; +import { getConnection } from 'typeorm'; +import define from '../../define'; +import redis from '../../../../db/redis'; + +export const meta = { + requireCredential: false, + + desc: { + }, + + tags: ['meta'], + + params: { + }, +}; + +export default define(meta, async () => { + const memStats = await si.mem(); + const fsStats = await si.fsSize(); + const netInterface = await si.networkInterfaceDefault(); + + return { + machine: os.hostname(), + os: os.platform(), + node: process.version, + psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version), + redis: redis.server_info.redis_version, + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length + }, + mem: { + total: memStats.total + }, + fs: { + total: fsStats[0].size, + used: fsStats[0].used, + }, + net: { + interface: netInterface + } + }; +}); diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index bc37228d0aa0bb33b497e25c51fd66d2d3a5d5c1..65650f12952fb49c329fb92f38172f3a666d595f 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -13,16 +13,9 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, params: { - announcements: { - validator: $.optional.nullable.arr($.obj()), - desc: { - 'ja-JP': 'ãŠçŸ¥ã‚‰ã›' - } - }, - disableRegistration: { validator: $.optional.nullable.bool, desc: { @@ -44,13 +37,6 @@ export const meta = { } }, - enableEmojiReaction: { - validator: $.optional.nullable.bool, - desc: { - 'ja-JP': '絵文å—リアクションを有効ã«ã™ã‚‹ã‹å¦ã‹' - } - }, - useStarForReactionFallback: { validator: $.optional.nullable.bool, desc: { @@ -347,7 +333,7 @@ export const meta = { } }, - ToSUrl: { + tosUrl: { validator: $.optional.nullable.str, desc: { 'ja-JP': '利用è¦ç´„ã®URL' @@ -413,10 +399,6 @@ export const meta = { export default define(meta, async (ps, me) => { const set = {} as Partial<Meta>; - if (ps.announcements) { - set.announcements = ps.announcements; - } - if (typeof ps.disableRegistration === 'boolean') { set.disableRegistration = ps.disableRegistration; } @@ -429,10 +411,6 @@ export default define(meta, async (ps, me) => { set.disableGlobalTimeline = ps.disableGlobalTimeline; } - if (typeof ps.enableEmojiReaction === 'boolean') { - set.enableEmojiReaction = ps.enableEmojiReaction; - } - if (typeof ps.useStarForReactionFallback === 'boolean') { set.useStarForReactionFallback = ps.useStarForReactionFallback; } @@ -601,8 +579,8 @@ export default define(meta, async (ps, me) => { set.swPrivateKey = ps.swPrivateKey; } - if (ps.ToSUrl !== undefined) { - set.ToSUrl = ps.ToSUrl; + if (ps.tosUrl !== undefined) { + set.ToSUrl = ps.tosUrl; } if (ps.repositoryUrl !== undefined) { diff --git a/src/server/api/endpoints/announcements.ts b/src/server/api/endpoints/announcements.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6050d609201dc7231db5924118559ea5c3ba2e8 --- /dev/null +++ b/src/server/api/endpoints/announcements.ts @@ -0,0 +1,42 @@ +import $ from 'cafy'; +import { ID } from '../../../misc/cafy-id'; +import define from '../define'; +import { Announcements, AnnouncementReads } from '../../../models'; +import { makePaginationQuery } from '../common/make-pagination-query'; + +export const meta = { + requireCredential: false, + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + + const announcements = await query.take(ps.limit!).getMany(); + + if (user) { + const reads = (await AnnouncementReads.find({ + userId: user.id + })).map(x => x.announcementId); + + for (const announcement of announcements) { + (announcement as any).isRead = reads.includes(announcement.id); + } + } + + return announcements; +}); diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e00eda1a455d12c683a99d7263337f83f195c3c --- /dev/null +++ b/src/server/api/endpoints/antennas/create.ts @@ -0,0 +1,92 @@ +import $ from 'cafy'; +import define from '../../define'; +import { genId } from '../../../../misc/gen-id'; +import { Antennas, UserLists } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; +import { ApiError } from '../../error'; + +export const meta = { + tags: ['antennas'], + + requireCredential: true, + + kind: 'write:account', + + params: { + name: { + validator: $.str.range(1, 100) + }, + + src: { + validator: $.str.or(['home', 'all', 'users', 'list']) + }, + + userListId: { + validator: $.nullable.optional.type(ID), + }, + + keywords: { + validator: $.arr($.arr($.str)) + }, + + users: { + validator: $.arr($.str) + }, + + caseSensitive: { + validator: $.bool + }, + + withReplies: { + validator: $.bool + }, + + withFile: { + validator: $.bool + }, + + notify: { + validator: $.bool + } + }, + + errors: { + noSuchUserList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f' + } + } +}; + +export default define(meta, async (ps, user) => { + let userList; + + if (ps.src === 'list') { + userList = await UserLists.findOne({ + id: ps.userListId, + userId: user.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchUserList); + } + } + + const antenna = await Antennas.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + src: ps.src, + userListId: userList ? userList.id : null, + keywords: ps.keywords, + users: ps.users, + caseSensitive: ps.caseSensitive, + withReplies: ps.withReplies, + withFile: ps.withFile, + notify: ps.notify, + }); + + return await Antennas.pack(antenna); +}); diff --git a/src/server/api/endpoints/antennas/delete.ts b/src/server/api/endpoints/antennas/delete.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bf9165aedfe5803c1904911523f8c1818c87f2c --- /dev/null +++ b/src/server/api/endpoints/antennas/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Antennas } from '../../../../models'; + +export const meta = { + tags: ['antennas'], + + requireCredential: true, + + kind: 'write:account', + + params: { + antennaId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df' + } + } +}; + +export default define(meta, async (ps, user) => { + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: user.id + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + await Antennas.delete(antenna.id); +}); diff --git a/src/server/api/endpoints/antennas/list.ts b/src/server/api/endpoints/antennas/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f9deff32f9dbeb2b71f4e84c5f7372e65e04846 --- /dev/null +++ b/src/server/api/endpoints/antennas/list.ts @@ -0,0 +1,18 @@ +import define from '../../define'; +import { Antennas } from '../../../../models'; + +export const meta = { + tags: ['antennas', 'account'], + + requireCredential: true, + + kind: 'read:account', +}; + +export default define(meta, async (ps, me) => { + const antennas = await Antennas.find({ + userId: me.id, + }); + + return await Promise.all(antennas.map(x => Antennas.pack(x))); +}); diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4c8e7e698b4a8939287b532eb61700b16e6955b --- /dev/null +++ b/src/server/api/endpoints/antennas/notes.ts @@ -0,0 +1,72 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Antennas, Notes, AntennaNotes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { ApiError } from '../../error'; + +export const meta = { + tags: ['account', 'notes', 'antennas'], + + requireCredential: true, + + kind: 'read:account', + + params: { + antennaId: { + validator: $.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe' + } + } +}; + +export default define(meta, async (ps, user) => { + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: user.id + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + const antennaQuery = AntennaNotes.createQueryBuilder('joining') + .select('joining.noteId') + .where('joining.antennaId = :antennaId', { antennaId: antenna.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.id IN (${ antennaQuery.getQuery() })`) + .leftJoinAndSelect('note.user', 'user') + .setParameters(antennaQuery.getParameters()); + + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + + const notes = await query + .take(ps.limit!) + .getMany(); + + return await Notes.packMany(notes, user); +}); diff --git a/src/server/api/endpoints/antennas/show.ts b/src/server/api/endpoints/antennas/show.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd87de1dce2b5fc9a1bb35c5ebe557c4941e01ce --- /dev/null +++ b/src/server/api/endpoints/antennas/show.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Antennas } from '../../../../models'; + +export const meta = { + tags: ['antennas', 'account'], + + requireCredential: true, + + kind: 'read:account', + + params: { + antennaId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the antenna + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: me.id, + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + return await Antennas.pack(antenna); +}); diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..28875d0f08492a31799f6f24ea707a2f968d062a --- /dev/null +++ b/src/server/api/endpoints/antennas/update.ts @@ -0,0 +1,108 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Antennas, UserLists } from '../../../../models'; + +export const meta = { + tags: ['antennas'], + + requireCredential: true, + + kind: 'write:account', + + params: { + antennaId: { + validator: $.type(ID), + }, + + name: { + validator: $.str.range(1, 100) + }, + + src: { + validator: $.str.or(['home', 'all', 'users', 'list']) + }, + + userListId: { + validator: $.nullable.optional.type(ID), + }, + + keywords: { + validator: $.arr($.arr($.str)) + }, + + users: { + validator: $.arr($.str) + }, + + caseSensitive: { + validator: $.bool + }, + + withReplies: { + validator: $.bool + }, + + withFile: { + validator: $.bool + }, + + notify: { + validator: $.bool + } + }, + + errors: { + noSuchAntenna: { + message: 'No such antenna.', + code: 'NO_SUCH_ANTENNA', + id: '10c673ac-8852-48eb-aa1f-f5b67f069290' + }, + + noSuchUserList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '1c6b35c9-943e-48c2-81e4-2844989407f7' + } + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the antenna + const antenna = await Antennas.findOne({ + id: ps.antennaId, + userId: user.id + }); + + if (antenna == null) { + throw new ApiError(meta.errors.noSuchAntenna); + } + + let userList; + + if (ps.src === 'list') { + userList = await UserLists.findOne({ + id: ps.userListId, + userId: user.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchUserList); + } + } + + await Antennas.update(antenna.id, { + name: ps.name, + src: ps.src, + userListId: userList ? userList.id : null, + keywords: ps.keywords, + users: ps.users, + caseSensitive: ps.caseSensitive, + withReplies: ps.withReplies, + withFile: ps.withFile, + notify: ps.notify, + }); + + return await Antennas.pack(antenna.id); +}); diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6761c5533e38c9736f0516178d6694858e61964 --- /dev/null +++ b/src/server/api/endpoints/clips/create.ts @@ -0,0 +1,29 @@ +import $ from 'cafy'; +import define from '../../define'; +import { genId } from '../../../../misc/gen-id'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips'], + + requireCredential: true, + + kind: 'write:account', + + params: { + name: { + validator: $.str.range(1, 100) + } + }, +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + }); + + return await Clips.pack(clip); +}); diff --git a/src/server/api/endpoints/clips/delete.ts b/src/server/api/endpoints/clips/delete.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e185e4652da40822c4b06fa67174b75d0f6f4ac --- /dev/null +++ b/src/server/api/endpoints/clips/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips'], + + requireCredential: true, + + kind: 'write:account', + + params: { + clipId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754' + } + } +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + await Clips.delete(clip.id); +}); diff --git a/src/server/api/endpoints/clips/list.ts b/src/server/api/endpoints/clips/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa16a18d42cc630eaf44521e96819bfbc66dc659 --- /dev/null +++ b/src/server/api/endpoints/clips/list.ts @@ -0,0 +1,18 @@ +import define from '../../define'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips', 'account'], + + requireCredential: true, + + kind: 'read:account', +}; + +export default define(meta, async (ps, me) => { + const clips = await Clips.find({ + userId: me.id, + }); + + return await Promise.all(clips.map(x => Clips.pack(x))); +}); diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e76a4d1f364c0d4b560ba3d5521bf602e60a69a --- /dev/null +++ b/src/server/api/endpoints/clips/notes.ts @@ -0,0 +1,67 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Clips, Notes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; + +export const meta = { + tags: ['account', 'notes', 'clips'], + + requireCredential: true, + + kind: 'read:account', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + }, + + errors: { + noSuchClip: { + message: 'No such list.', + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00' + } + } +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + const clipQuery = ClipNotes.createQueryBuilder('joining') + .select('joining.noteId') + .where('joining.clipId = :clipId', { clipId: clip.id }); + + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.id IN (${ clipQuery.getQuery() })`) + .leftJoinAndSelect('note.user', 'user') + .setParameters(clipQuery.getParameters()); + + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + + const notes = await query + .take(ps.limit!) + .getMany(); + + return await Notes.packMany(notes, user); +}); diff --git a/src/server/api/endpoints/clips/show.ts b/src/server/api/endpoints/clips/show.ts new file mode 100644 index 0000000000000000000000000000000000000000..0766b3e929fabd37bc4747f835536816739f3e66 --- /dev/null +++ b/src/server/api/endpoints/clips/show.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips', 'account'], + + requireCredential: true, + + kind: 'read:account', + + params: { + clipId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the clip + const clip = await Clips.findOne({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + return await Clips.pack(clip); +}); diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1c31eb8e69172bf270675178d73d5c5b3d3efb7 --- /dev/null +++ b/src/server/api/endpoints/clips/update.ts @@ -0,0 +1,49 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Clips } from '../../../../models'; + +export const meta = { + tags: ['clips'], + + requireCredential: true, + + kind: 'write:account', + + params: { + clipId: { + validator: $.type(ID), + }, + + name: { + validator: $.str.range(1, 100), + } + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Fetch the clip + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + await Clips.update(clip.id, { + name: ps.name + }); + + return await Clips.pack(clip.id); +}); diff --git a/src/server/api/endpoints/federation/followers.ts b/src/server/api/endpoints/federation/followers.ts new file mode 100644 index 0000000000000000000000000000000000000000..d885daf70e006e9c2b8d858794bae5ba94390a81 --- /dev/null +++ b/src/server/api/endpoints/federation/followers.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + params: { + host: { + validator: $.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Following', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followeeHost = :host`, { host: ps.host }); + + const followings = await query + .take(ps.limit!) + .getMany(); + + return await Followings.packMany(followings, me, { populateFollowee: true }); +}); diff --git a/src/server/api/endpoints/federation/following.ts b/src/server/api/endpoints/federation/following.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f7981731826e0380cbe3a94dd805f5bcf92e0eb --- /dev/null +++ b/src/server/api/endpoints/federation/following.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + params: { + host: { + validator: $.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Following', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followerHost = :host`, { host: ps.host }); + + const followings = await query + .take(ps.limit!) + .getMany(); + + return await Followings.packMany(followings, me, { populateFollowee: true }); +}); diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts index bc0eb9a1d700a5c5e00a906759965e53d0805046..002cfd4335e78d2309d74e15d583eedd7f7e9394 100644 --- a/src/server/api/endpoints/federation/instances.ts +++ b/src/server/api/endpoints/federation/instances.ts @@ -9,6 +9,10 @@ export const meta = { requireCredential: false, params: { + host: { + validator: $.optional.nullable.str, + }, + blocked: { validator: $.optional.nullable.bool, }, @@ -17,7 +21,19 @@ export const meta = { validator: $.optional.nullable.bool, }, - markedAsClosed: { + suspended: { + validator: $.optional.nullable.bool, + }, + + federating: { + validator: $.optional.nullable.bool, + }, + + subscribing: { + validator: $.optional.nullable.bool, + }, + + publishing: { validator: $.optional.nullable.bool, }, @@ -41,6 +57,8 @@ export default define(meta, async (ps, me) => { const query = Instances.createQueryBuilder('instance'); switch (ps.sort) { + case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break; + case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break; case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; case '+users': query.orderBy('instance.usersCount', 'DESC'); break; @@ -78,14 +96,42 @@ export default define(meta, async (ps, me) => { } } - if (typeof ps.markedAsClosed === 'boolean') { - if (ps.markedAsClosed) { - query.andWhere('instance.isMarkedAsClosed = TRUE'); + if (typeof ps.suspended === 'boolean') { + if (ps.suspended) { + query.andWhere('instance.isSuspended = TRUE'); } else { - query.andWhere('instance.isMarkedAsClosed = FALSE'); + query.andWhere('instance.isSuspended = FALSE'); } } + if (typeof ps.federating === 'boolean') { + if (ps.federating) { + query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); + } else { + query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))'); + } + } + + if (typeof ps.subscribing === 'boolean') { + if (ps.subscribing) { + query.andWhere('instance.followersCount > 0'); + } else { + query.andWhere('instance.followersCount = 0'); + } + } + + if (typeof ps.publishing === 'boolean') { + if (ps.publishing) { + query.andWhere('instance.followingCount > 0'); + } else { + query.andWhere('instance.followingCount = 0'); + } + } + + if (ps.host) { + query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }) + } + const instances = await query.take(ps.limit!).skip(ps.offset).getMany(); return instances; diff --git a/src/server/api/endpoints/federation/users.ts b/src/server/api/endpoints/federation/users.ts new file mode 100644 index 0000000000000000000000000000000000000000..f69bbf949c9a572ac495533fb33e1cf461bb8f55 --- /dev/null +++ b/src/server/api/endpoints/federation/users.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Users } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + params: { + host: { + validator: $.str + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId) + .andWhere(`user.host = :host`, { host: ps.host }); + + const users = await query + .take(ps.limit!) + .getMany(); + + return await Users.packMany(users, me, { detail: true }); +}); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index cd00501a2ed6c8adabad8916924d50bf4a1605d2..f624550d493fb89aab2ae48ac20e776ff1ee9738 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -42,12 +42,12 @@ export const meta = { }, includeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])), default: [] as string[] }, excludeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])), default: [] as string[] } }, diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5fbe7d576da76dddc73a35d1afa8e6ced942e4d --- /dev/null +++ b/src/server/api/endpoints/i/read-announcement.ts @@ -0,0 +1,60 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { genId } from '../../../../misc/gen-id'; +import { AnnouncementReads, Announcements, Users } from '../../../../models'; +import { publishMainStream } from '../../../../services/stream'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'write:account', + + params: { + announcementId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: '184663db-df88-4bc2-8b52-fb85f0681939' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Check if announcement exists + const announcement = await Announcements.findOne(ps.announcementId); + + if (announcement == null) { + throw new ApiError(meta.errors.noSuchAnnouncement); + } + + // Check if already read + const read = await AnnouncementReads.findOne({ + announcementId: ps.announcementId, + userId: user.id + }); + + if (read != null) { + return; + } + + // Create read + await AnnouncementReads.save({ + id: genId(), + createdAt: new Date(), + announcementId: ps.announcementId, + userId: user.id, + }); + + if (!await Users.getHasUnreadAnnouncement(user.id)) { + publishMainStream(user.id, 'readAllAnnouncements'); + } +}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index b71c35946e51f923d24f993da8d73d9f5834c1ab..2c605a6f0ba85d739b4be746f9533b5d85ee316a 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -1,11 +1,8 @@ import $ from 'cafy'; -import * as os from 'os'; import config from '../../../config'; import define from '../define'; import { fetchMeta } from '../../../misc/fetch-meta'; -import { Emojis } from '../../../models'; -import { getConnection } from 'typeorm'; -import redis from '../../../db/redis'; +import { Emojis, Users } from '../../../models'; import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits'; export const meta = { @@ -83,11 +80,6 @@ export const meta = { optional: false as const, nullable: false as const, description: 'Whether disabled GTL.', }, - enableEmojiReaction: { - type: 'boolean' as const, - optional: false as const, nullable: false as const, - description: 'Whether enabled emoji reaction.', - }, } } }; @@ -119,27 +111,15 @@ export default define(meta, async (ps, me) => { uri: config.url, description: instance.description, langs: instance.langs, - ToSUrl: instance.ToSUrl, + tosUrl: instance.ToSUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, secure: config.https != null, - machine: os.hostname(), - os: os.platform(), - node: process.version, - psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version), - redis: redis.server_info.redis_version, - - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length - }, - announcements: instance.announcements || [], disableRegistration: instance.disableRegistration, disableLocalTimeline: instance.disableLocalTimeline, disableGlobalTimeline: instance.disableGlobalTimeline, - enableEmojiReaction: instance.enableEmojiReaction, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, cacheRemoteFiles: instance.cacheRemoteFiles, @@ -159,6 +139,7 @@ export default define(meta, async (ps, me) => { category: e.category, url: e.url, })), + requireSetup: (await Users.count({})) === 0, enableEmail: instance.enableEmail, enableTwitterIntegration: instance.enableTwitterIntegration, @@ -183,7 +164,7 @@ export default define(meta, async (ps, me) => { }; } - if (me && (me.isAdmin || me.isModerator)) { + if (me && me.isAdmin) { response.useStarForReactionFallback = instance.useStarForReactionFallback; response.pinnedUsers = instance.pinnedUsers; response.hiddenTags = instance.hiddenTags; diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 810ad51b6718f3489b5d408a00352f1b879f8abe..73db73ed97d40d9fa76d3f98a618760f40b589e9 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -113,23 +113,6 @@ export const meta = { } }, - geo: { - validator: $.optional.nullable.obj({ - coordinates: $.arr().length(2) - .item(0, $.num.range(-180, 180)) - .item(1, $.num.range(-90, 90)), - altitude: $.nullable.num, - accuracy: $.nullable.num, - altitudeAccuracy: $.nullable.num, - heading: $.nullable.num.range(0, 360), - speed: $.nullable.num - }).strict(), - desc: { - 'ja-JP': 'ä½ç½®æƒ…å ±' - }, - ref: 'geo' - }, - fileIds: { validator: $.optional.arr($.type(ID)).unique().range(1, 4), desc: { @@ -308,7 +291,6 @@ export default define(meta, async (ps, user, app) => { apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, - geo: ps.geo }); return { diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts index 0a1d8668b0c4d98edb5c9a2660a72811b42fd8d3..a499afabf0e3e5875eb28efb97970b3e28352ce4 100644 --- a/src/server/api/endpoints/notes/featured.ts +++ b/src/server/api/endpoints/notes/featured.ts @@ -15,12 +15,17 @@ export const meta = { params: { limit: { - validator: $.optional.num.range(1, 30), + validator: $.optional.num.range(1, 100), default: 10, desc: { 'ja-JP': '最大数' } - } + }, + + offset: { + validator: $.optional.num.min(0), + default: 0 + }, }, res: { @@ -35,6 +40,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const max = 30; const day = 1000 * 60 * 60 * 24 * 3; // 3æ—¥å‰ã¾ã§ const query = Notes.createQueryBuilder('note') @@ -46,7 +52,14 @@ export default define(meta, async (ps, user) => { if (user) generateMuteQuery(query, user); - const notes = await query.orderBy('note.score', 'DESC').take(ps.limit!).getMany(); + let notes = await query + .orderBy('note.score', 'DESC') + .take(max) + .getMany(); + + notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + notes = notes.slice(ps.offset, ps.offset + ps.limit); return await Notes.packMany(notes, user); }); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index 5557b469e408dffaccfef9f09ac79344d6bed3b1..efc08d0d4a403b9071509ec4caea84ce37e0c0f5 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -1,11 +1,13 @@ import $ from 'cafy'; import es from '../../../../db/elasticsearch'; import define from '../../define'; -import { ApiError } from '../../error'; import { Notes } from '../../../../models'; import { In } from 'typeorm'; import { ID } from '../../../../misc/cafy-id'; import config from '../../../../config'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; export const meta = { desc: { @@ -22,16 +24,19 @@ export const meta = { validator: $.str }, + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 }, - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - host: { validator: $.optional.nullable.str, default: undefined @@ -54,74 +59,80 @@ export const meta = { }, errors: { - searchingNotAvailable: { - message: 'Searching not available.', - code: 'SEARCHING_NOT_AVAILABLE', - id: '7ee9c119-16a1-479f-a6fd-6fab00ed946f' - } } }; export default define(meta, async (ps, me) => { - if (es == null) throw new ApiError(meta.errors.searchingNotAvailable); + if (es == null) { + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) + .leftJoinAndSelect('note.user', 'user'); - const userQuery = ps.userId != null ? [{ - term: { - userId: ps.userId - } - }] : []; - - const hostQuery = ps.userId == null ? - ps.host === null ? [{ - bool: { - must_not: { - exists: { - field: 'userHost' - } - } - } - }] : ps.host !== undefined ? [{ + generateVisibilityQuery(query, me); + if (me) generateMuteQuery(query, me); + + const notes = await query.take(ps.limit!).getMany(); + + return await Notes.packMany(notes, me); + } else { + const userQuery = ps.userId != null ? [{ term: { - userHost: ps.host + userId: ps.userId } - }] : [] - : []; - - const result = await es.search({ - index: config.elasticsearch.index || 'misskey_note', - body: { - size: ps.limit!, - from: ps.offset, - query: { + }] : []; + + const hostQuery = ps.userId == null ? + ps.host === null ? [{ bool: { - must: [{ - simple_query_string: { - fields: ['text'], - query: ps.query.toLowerCase(), - default_operator: 'and' - }, - }, ...hostQuery, ...userQuery] + must_not: { + exists: { + field: 'userHost' + } + } } - }, - sort: [{ - _doc: 'desc' - }] - } - }); + }] : ps.host !== undefined ? [{ + term: { + userHost: ps.host + } + }] : [] + : []; + + const result = await es.search({ + index: config.elasticsearch.index || 'misskey_note', + body: { + size: ps.limit!, + from: ps.offset, + query: { + bool: { + must: [{ + simple_query_string: { + fields: ['text'], + query: ps.query.toLowerCase(), + default_operator: 'and' + }, + }, ...hostQuery, ...userQuery] + } + }, + sort: [{ + _doc: 'desc' + }] + } + }); - const hits = result.body.hits.hits.map((hit: any) => hit._id); + const hits = result.body.hits.hits.map((hit: any) => hit._id); - if (hits.length === 0) return []; + if (hits.length === 0) return []; - // Fetch found notes - const notes = await Notes.find({ - where: { - id: In(hits) - }, - order: { - id: -1 - } - }); + // Fetch found notes + const notes = await Notes.find({ + where: { + id: In(hits) + }, + order: { + id: -1 + } + }); - return await Notes.packMany(notes, me); + return await Notes.packMany(notes, me); + } }); diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts new file mode 100644 index 0000000000000000000000000000000000000000..8544731dfd2777d24931d8a7367ca816e6988d49 --- /dev/null +++ b/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -0,0 +1,101 @@ +import $ from 'cafy'; +import define from '../../define'; +import { Users } from '../../../../models'; +import { User } from '../../../../models/entities/user'; + +export const meta = { + desc: { + 'ja-JP': 'ユーザーを検索ã—ã¾ã™ã€‚' + }, + + tags: ['users'], + + requireCredential: false, + + params: { + username: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': 'クエリ' + } + }, + + host: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': 'クエリ' + } + }, + + offset: { + validator: $.optional.num.min(0), + default: 0, + desc: { + 'ja-JP': 'オフセット' + } + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + desc: { + 'ja-JP': 'å–å¾—ã™ã‚‹æ•°' + } + }, + + detail: { + validator: $.optional.bool, + default: true, + desc: { + 'ja-JP': '詳細ãªãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±ã‚’å«ã‚ã‚‹ã‹å¦ã‹' + } + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User', + } + }, +}; + +export default define(meta, async (ps, me) => { + if (ps.host) { + const q = Users.createQueryBuilder('user') + .where('user.isSuspended = FALSE') + .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); + + if (ps.username) { + q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + } + + const users = await q.take(ps.limit!).skip(ps.offset).getMany(); + + return await Users.packMany(users, me, { detail: ps.detail }); + } else { + let users = await Users.createQueryBuilder('user') + .where('user.host IS NULL') + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + .take(ps.limit!) + .skip(ps.offset) + .getMany(); + + if (users.length < ps.limit!) { + const otherUsers = await Users.createQueryBuilder('user') + .where('user.host IS NOT NULL') + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + .take(ps.limit! - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + + return await Users.packMany(users, me, { detail: ps.detail }); + } +}); diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index af1aefda84c68b9ae07914254dc8bde31a8fcdb4..79ee74389c312ef63316f7f56198342245929321 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,19 +1,8 @@ import * as Koa from 'koa'; -import * as bcrypt from 'bcryptjs'; -import { generateKeyPair } from 'crypto'; -import generateUserToken from '../common/generate-native-user-token'; -import config from '../../../config'; import { fetchMeta } from '../../../misc/fetch-meta'; import * as recaptcha from 'recaptcha-promise'; -import { Users, Signins, RegistrationTickets, UsedUsernames } from '../../../models'; -import { genId } from '../../../misc/gen-id'; -import { usersChart } from '../../../services/chart'; -import { User } from '../../../models/entities/user'; -import { UserKeypair } from '../../../models/entities/user-keypair'; -import { toPunyNullable } from '../../../misc/convert-host'; -import { UserProfile } from '../../../models/entities/user-profile'; -import { getConnection } from 'typeorm'; -import { UsedUsername } from '../../../models/entities/used-username'; +import { Users, RegistrationTickets } from '../../../models'; +import { signup } from '../common/signup'; export default async (ctx: Koa.Context) => { const body = ctx.request.body; @@ -31,7 +20,6 @@ export default async (ctx: Koa.Context) => { if (!success) { ctx.throw(400, 'recaptcha-failed'); - return; } } @@ -58,114 +46,18 @@ export default async (ctx: Koa.Context) => { RegistrationTickets.delete(ticket.id); } - // Validate username - if (!Users.validateLocalUsername.ok(username)) { - ctx.status = 400; - return; - } - - // Validate password - if (!Users.validatePassword.ok(password)) { - ctx.status = 400; - return; - } - - const usersCount = await Users.count({}); - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(password, salt); - - // Generate secret - const secret = generateUserToken(); - - // Check username duplication - if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) { - ctx.status = 400; - return; - } - - // Check deleted username duplication - if (await UsedUsernames.findOne({ username: username.toLowerCase() })) { - ctx.status = 400; - return; - } - - const keyPair = await new Promise<string[]>((res, rej) => - generateKeyPair('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: undefined, - passphrase: undefined - } - } as any, (err, publicKey, privateKey) => - err ? rej(err) : res([publicKey, privateKey]) - )); - - let account!: User; + try { + const { account, secret } = await signup(username, password, host); - // Start transaction - await getConnection().transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOne(User, { - usernameLower: username.toLowerCase(), - host: null + const res = await Users.pack(account, account, { + detail: true, + includeSecrets: true }); - if (exist) throw new Error(' the username is already used'); - - account = await transactionalEntityManager.save(new User({ - id: genId(), - createdAt: new Date(), - username: username, - usernameLower: username.toLowerCase(), - host: toPunyNullable(host), - token: secret, - isAdmin: config.autoAdmin && usersCount === 0, - })); - - await transactionalEntityManager.save(new UserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], - userId: account.id - })); - - await transactionalEntityManager.save(new UserProfile({ - userId: account.id, - autoAcceptFollowed: true, - autoWatch: false, - password: hash, - })); - - await transactionalEntityManager.save(new UsedUsername({ - createdAt: new Date(), - username: username.toLowerCase(), - })); - }); + (res as any).token = secret; - usersChart.update(account, true); - - // Append signin history - await Signins.save({ - id: genId(), - createdAt: new Date(), - userId: account.id, - ip: ctx.ip, - headers: ctx.headers, - success: true - }); - - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true - }); - - (res as any).token = secret; - - ctx.body = res; + ctx.body = res; + } catch (e) { + ctx.throw(400, e); + } }; diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts new file mode 100644 index 0000000000000000000000000000000000000000..714edb502d7cceca04250a7109c9bfdc36d66338 --- /dev/null +++ b/src/server/api/stream/channels/antenna.ts @@ -0,0 +1,41 @@ +import autobind from 'autobind-decorator'; +import Channel from '../channel'; +import { Notes } from '../../../../models'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; + +export default class extends Channel { + public readonly chName = 'antenna'; + public static shouldShare = false; + public static requireCredential = false; + private antennaId: string; + + @autobind + public async init(params: any) { + this.antennaId = params.antennaId as string; + + // Subscribe stream + this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); + } + + @autobind + private async onEvent(data: any) { + const { type, body } = data; + + if (type === 'note') { + const note = await Notes.pack(body.id, this.user, { detail: true }); + + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ + if (shouldMuteThisNote(note, this.muting)) return; + + this.send('note', note); + } else { + this.send(type, body); + } + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent); + } +} diff --git a/src/server/api/stream/channels/ap-log.ts b/src/server/api/stream/channels/ap-log.ts deleted file mode 100644 index 867fd3670b9e27398a47346b80fa84043868dc92..0000000000000000000000000000000000000000 --- a/src/server/api/stream/channels/ap-log.ts +++ /dev/null @@ -1,25 +0,0 @@ -import autobind from 'autobind-decorator'; -import Channel from '../channel'; - -export default class extends Channel { - public readonly chName = 'apLog'; - public static shouldShare = true; - public static requireCredential = false; - - @autobind - public async init(params: any) { - // Subscribe events - this.subscriber.on('apLog', this.onLog); - } - - @autobind - private async onLog(log: any) { - this.send('log', log); - } - - @autobind - public dispose() { - // Unsubscribe events - this.subscriber.off('apLog', this.onLog); - } -} diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts index b9feb702580de3f7a2909514c3f1b8ba3dbb9863..e32f4111c29668e9f1e32b9e44993408edf85140 100644 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ b/src/server/api/stream/channels/hybrid-timeline.ts @@ -50,7 +50,7 @@ export default class extends Channel { detail: true }); } - } + } // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (shouldMuteThisNote(note, this.muting)) return; diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts index 4527fb1e46dd0ecfb456b892a193e4a9096aad83..6efad078c6712c2e1177618fd40f148558d36c02 100644 --- a/src/server/api/stream/channels/index.ts +++ b/src/server/api/stream/channels/index.ts @@ -3,15 +3,14 @@ import homeTimeline from './home-timeline'; import localTimeline from './local-timeline'; import hybridTimeline from './hybrid-timeline'; import globalTimeline from './global-timeline'; -import notesStats from './notes-stats'; import serverStats from './server-stats'; import queueStats from './queue-stats'; import userList from './user-list'; +import antenna from './antenna'; import messaging from './messaging'; import messagingIndex from './messaging-index'; import drive from './drive'; import hashtag from './hashtag'; -import apLog from './ap-log'; import admin from './admin'; import gamesReversi from './games/reversi'; import gamesReversiGame from './games/reversi-game'; @@ -22,15 +21,14 @@ export default { localTimeline, hybridTimeline, globalTimeline, - notesStats, serverStats, queueStats, userList, + antenna, messaging, messagingIndex, drive, hashtag, - apLog, admin, gamesReversi, gamesReversiGame diff --git a/src/server/api/stream/channels/notes-stats.ts b/src/server/api/stream/channels/notes-stats.ts deleted file mode 100644 index 0c6b84d6cf7c4d51c6b166a3f698e3c56dbabc53..0000000000000000000000000000000000000000 --- a/src/server/api/stream/channels/notes-stats.ts +++ /dev/null @@ -1,38 +0,0 @@ -import autobind from 'autobind-decorator'; -import Xev from 'xev'; -import Channel from '../channel'; - -const ev = new Xev(); - -export default class extends Channel { - public readonly chName = 'notesStats'; - public static shouldShare = true; - public static requireCredential = false; - - @autobind - public async init(params: any) { - ev.addListener('notesStats', this.onStats); - } - - @autobind - private onStats(stats: any) { - this.send('stats', stats); - } - - @autobind - public onMessage(type: string, body: any) { - switch (type) { - case 'requestLog': - ev.once(`notesStatsLog:${body.id}`, statsLog => { - this.send('statsLog', statsLog); - }); - ev.emit('requestNotesStatsLog', body.id); - break; - } - } - - @autobind - public dispose() { - ev.removeListener('notesStats', this.onStats); - } -} diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index f73f3229d5081ab8d292c76d431659de027ac7d3..6ec644a024cd9aaec0db879b59964fc8b5f57236 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -9,6 +9,7 @@ import { EventEmitter } from 'events'; import { User } from '../../../models/entities/user'; import { App } from '../../../models/entities/app'; import { Users, Followings, Mutings } from '../../../models'; +import { ApiError } from '../error'; /** * Main stream connection @@ -83,8 +84,16 @@ export default class Connection { // 呼ã³å‡ºã— call(endpoint, user, this.app, payload.data).then(res => { this.sendMessageToWs(`api:${payload.id}`, { res }); - }).catch(e => { - this.sendMessageToWs(`api:${payload.id}`, { e }); + }).catch((e: ApiError) => { + this.sendMessageToWs(`api:${payload.id}`, { + error: { + message: e.message, + code: e.code, + id: e.id, + kind: e.kind, + ...(e.info ? { info: e.info } : {}) + } + }); }); } @@ -111,7 +120,7 @@ export default class Connection { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } - if (payload.read && this.user) { + if (this.user) { readNote(this.user.id, payload.id); } } diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts index 211fa2a73ec21c085c98dc28fed35c5317b0a70e..2ff924e68d0d018374f1ff3226302da23423ab3e 100644 --- a/src/server/nodeinfo.ts +++ b/src/server/nodeinfo.ts @@ -59,10 +59,9 @@ const nodeinfo2 = async () => { email: meta.maintainerEmail }, langs: meta.langs, - ToSUrl: meta.ToSUrl, + tosUrl: meta.ToSUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, - announcements: meta.announcements, disableRegistration: meta.disableRegistration, disableLocalTimeline: meta.disableLocalTimeline, disableGlobalTimeline: meta.disableGlobalTimeline, diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index 558e8114661ff424a19d549858caadb1a7ee5281..c6c5fd1e2fcf8f36921f9c1ccc3e70efd1f0ec0e 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -12,7 +12,6 @@ import * as send from 'koa-send'; import * as glob from 'glob'; import config from '../../config'; import { licenseHtml } from '../../misc/license'; -import { copyright } from '../../const.json'; import * as locales from '../../../locales'; import * as nestedProperty from 'nested-property'; @@ -48,7 +47,7 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> { vars['config'] = config; - vars['copyright'] = copyright; + vars['copyright'] = '(c) Misskey'; vars['license'] = licenseHtml; diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 06c7274f5a5f0f422817390ebfb034072c00aaf7..57bcb855a1bbe94f8177d12b0a4ce5fbe244dc75 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -31,6 +31,7 @@ const app = new Koa(); app.use(views(__dirname + '/views', { extension: 'pug', options: { + version: config.version, config } })); diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug index 97c7a87e1bd347b4cfb3c8f321e4fc547af72e4f..43b82a5f05a6a2aac6bab1cdcb7c816e0a125550 100644 --- a/src/server/web/views/base.pug +++ b/src/server/web/views/base.pug @@ -10,7 +10,7 @@ html meta(charset='utf-8') meta(name='application-name' content='Misskey') meta(name='referrer' content='origin') - meta(name='theme-color' content='#105779') + meta(name='theme-color' content='#86b300') meta(property='og:site_name' content= instanceName || 'Misskey') meta(name='viewport' content='width=device-width, initial-scale=1') link(rel='icon' href= icon || '/favicon.ico') @@ -30,12 +30,23 @@ html meta(property='og:image' content=img) style - include ./../../../../built/client/assets/init.css - script - include ./../../../../built/client/assets/boot.js - - script - include ./../../../../built/client/assets/safe.js + include ./../../../../built/client/assets/style.css + script(src=`/assets/app.${version}.js` async defer) + script. + const theme = localStorage.getItem('theme'); + if (theme) { + for (const [k, v] of Object.entries(JSON.parse(theme))) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + if (k === 'accent') { + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', v); + break; + } + } + } + } + } body noscript: p diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts new file mode 100644 index 0000000000000000000000000000000000000000..0055639c0b07d9852df4927d7a64c867bc5642f6 --- /dev/null +++ b/src/services/add-note-to-antenna.ts @@ -0,0 +1,54 @@ +import { Antenna } from '../models/entities/antenna'; +import { Note } from '../models/entities/note'; +import { AntennaNotes, Mutings, Notes } from '../models'; +import { genId } from '../misc/gen-id'; +import shouldMuteThisNote from '../misc/should-mute-this-note'; +import { ensure } from '../prelude/ensure'; +import { publishAntennaStream, publishMainStream } from './stream'; +import { User } from '../models/entities/user'; + +export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: User) { + // 通知ã—ãªã„è¨å®šã«ãªã£ã¦ã„ã‚‹ã‹ã€è‡ªåˆ†è‡ªèº«ã®æŠ•ç¨¿ãªã‚‰æ—¢èªã«ã™ã‚‹ + const read = !antenna.notify || (antenna.userId === noteUser.id); + + AntennaNotes.save({ + id: genId(), + antennaId: antenna.id, + noteId: note.id, + read: read, + }); + + publishAntennaStream(antenna.id, 'note', note); + + if (!read) { + const mutings = await Mutings.find({ + where: { + muterId: antenna.userId + }, + select: ['muteeId'] + }); + + const _note: Note = { + ...note + }; + + if (note.replyId != null) { + _note.reply = await Notes.findOne(note.replyId).then(ensure); + } + if (note.renoteId != null) { + _note.renote = await Notes.findOne(note.renoteId).then(ensure); + } + + if (shouldMuteThisNote(_note, mutings.map(x => x.muteeId))) { + return; + } + + // 2秒経ã£ã¦ã‚‚æ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰é€šçŸ¥ + setTimeout(async () => { + const unread = await AntennaNotes.findOne({ antennaId: antenna.id, read: false }); + if (unread) { + publishMainStream(antenna.userId, 'unreadAntenna', antenna); + } + }, 2000); + } +} diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts index 5bff8adfd4dbdc499fb4e439b845ed13bdd8f583..f9cf04dc69ee967bea45131808345ca3992558c6 100644 --- a/src/services/create-notification.ts +++ b/src/services/create-notification.ts @@ -5,6 +5,7 @@ import { genId } from '../misc/gen-id'; import { User } from '../models/entities/user'; import { Note } from '../models/entities/note'; import { Notification } from '../models/entities/notification'; +import { FollowRequest } from '../models/entities/follow-request'; export async function createNotification( notifieeId: User['id'], @@ -14,6 +15,7 @@ export async function createNotification( noteId?: Note['id']; reaction?: string; choice?: number; + followRequestId?: FollowRequest['id']; } ) { if (notifieeId === notifierId) { @@ -33,6 +35,7 @@ export async function createNotification( if (content.noteId) data.noteId = content.noteId; if (content.reaction) data.reaction = content.reaction; if (content.choice) data.choice = content.choice; + if (content.followRequestId) data.followRequestId = content.followRequestId; } // Create notification diff --git a/src/services/following/create.ts b/src/services/following/create.ts index b69dfe42b93fcd0a4dae0bb2a01c5c0eb0b02b1c..ce352ffbc1d51f51bc74a4be7225858fde1caf71 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -45,11 +45,21 @@ export async function insertFollowingDoc(followee: User, follower: User) { } }); - await FollowRequests.delete({ + const req = await FollowRequests.findOne({ followeeId: followee.id, followerId: follower.id }); + if (req) { + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id + }); + + // é€šçŸ¥ã‚’ä½œæˆ + createNotification(follower.id, followee.id, 'followRequestAccepted'); + } + if (alreadyFollowed) return; //#region Increment counts diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts index 32e79d136d72819d9f9e7bdd3ed40a314d28908d..1c6bac0fbe90b2d33043145be39595a14c0cddc5 100644 --- a/src/services/following/requests/create.ts +++ b/src/services/following/requests/create.ts @@ -25,7 +25,7 @@ export default async function(follower: User, followee: User, requestId?: string if (blocking != null) throw new Error('blocking'); if (blocked != null) throw new Error('blocked'); - await FollowRequests.save({ + const followRequest = await FollowRequests.save({ id: genId(), createdAt: new Date(), followerId: follower.id, @@ -50,7 +50,9 @@ export default async function(follower: User, followee: User, requestId?: string }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); // é€šçŸ¥ã‚’ä½œæˆ - createNotification(followee.id, follower.id, 'receiveFollowRequest'); + createNotification(followee.id, follower.id, 'receiveFollowRequest', { + followRequestId: followRequest.id + }); } if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 289a3393b090389a07ef699c79eb2151e1414dfd..e6433ac04d9d0965ed73b55039e62790d1da914e 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -17,7 +17,7 @@ import extractMentions from '../../misc/extract-mentions'; import extractEmojis from '../../misc/extract-emojis'; import extractHashtags from '../../misc/extract-hashtags'; import { Note, IMentionedRemoteUsers } from '../../models/entities/note'; -import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles } from '../../models'; +import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings } from '../../models'; import { DriveFile } from '../../models/entities/drive-file'; import { App } from '../../models/entities/app'; import { Not, getConnection, In } from 'typeorm'; @@ -28,6 +28,8 @@ import { Poll, IPoll } from '../../models/entities/poll'; import { createNotification } from '../create-notification'; import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; import { ensure } from '../../prelude/ensure'; +import { checkHitAntenna } from '../../misc/check-hit-antenna'; +import { addNoteToAntenna } from '../add-note-to-antenna'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -90,7 +92,6 @@ type Option = { reply?: Note | null; renote?: Note | null; files?: DriveFile[] | null; - geo?: any | null; poll?: IPoll | null; viaMobile?: boolean | null; localOnly?: boolean | null; @@ -207,6 +208,23 @@ export default async (user: User, data: Option, silent = false) => new Promise<N // Increment notes count (user) incNotesCountOfUser(user); + // Antenna + Antennas.find().then(async antennas => { + const followings = await Followings.createQueryBuilder('following') + .andWhere(`following.followeeId = :userId`, { userId: note.userId }) + .getMany(); + + const followers = followings.map(f => f.followerId); + + for (const antenna of antennas) { + checkHitAntenna(antenna, note, user, followers).then(hit => { + if (hit) { + addNoteToAntenna(antenna, note, user); + } + }); + } + }); + if (data.reply) { saveReply(data.reply, note); } @@ -361,8 +379,6 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri userId: user.id, viaMobile: data.viaMobile!, localOnly: data.localOnly!, - geo: data.geo || null, - appId: data.app ? data.app.id : null, visibility: data.visibility as any, visibleUserIds: data.visibility == 'specified' ? data.visibleUsers diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 436347774b4bcf3bfa187925bb991ddea76e8641..a09fbd9c2fc3a8c99c921a65eb895475e71b9510 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -5,7 +5,6 @@ import { deliver } from '../../../queue'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { IdentifiableError } from '../../../misc/identifiable-error'; import { toDbReaction } from '../../../misc/reaction-lib'; -import { fetchMeta } from '../../../misc/fetch-meta'; import { User } from '../../../models/entities/user'; import { Note } from '../../../models/entities/note'; import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles } from '../../../models'; @@ -22,8 +21,7 @@ export default async (user: User, note: Note, reaction?: string) => { throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note'); } - const meta = await fetchMeta(); - reaction = await toDbReaction(reaction, meta.enableEmojiReaction); + reaction = await toDbReaction(reaction); // Create reaction await NoteReactions.save({ diff --git a/src/services/note/read.ts b/src/services/note/read.ts index c05a58534abbfc9d884330f6e842f9b584f77e03..1cbe0e311b340806712c3a981a3a34f3158ca7f1 100644 --- a/src/services/note/read.ts +++ b/src/services/note/read.ts @@ -1,7 +1,7 @@ import { publishMainStream } from '../stream'; import { Note } from '../../models/entities/note'; import { User } from '../../models/entities/user'; -import { NoteUnreads } from '../../models'; +import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models'; /** * Mark a note as read @@ -17,27 +17,54 @@ export default ( }); // v11 TODO: https://github.com/typeorm/typeorm/issues/2415 - //if (res.affected == 0) { + //if (res.affected === 0) { // return; //} - const count1 = await NoteUnreads.count({ - userId: userId, - isSpecified: false - }); - - const count2 = await NoteUnreads.count({ - userId: userId, - isSpecified: true - }); + const [count1, count2] = await Promise.all([ + NoteUnreads.count({ + userId: userId, + isSpecified: false + }), + NoteUnreads.count({ + userId: userId, + isSpecified: true + }) + ]); - if (count1 == 0) { + if (count1 === 0) { // å…¨ã¦æ—¢èªã«ãªã£ãŸã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 publishMainStream(userId, 'readAllUnreadMentions'); } - if (count2 == 0) { + if (count2 === 0) { // å…¨ã¦æ—¢èªã«ãªã£ãŸã‚¤ãƒ™ãƒ³ãƒˆã‚’発行 publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); } + + const antennas = await Antennas.find({ userId }); + + await Promise.all(antennas.map(async antenna => { + await AntennaNotes.update({ + antennaId: antenna.id, + noteId: noteId + }, { + read: true + }); + + const count = await AntennaNotes.count({ + antennaId: antenna.id, + read: false + }); + + if (count === 0) { + publishMainStream(userId, 'readAntenna', antenna); + } + })); + + Users.getHasUnreadAntenna(userId).then(unread => { + if (!unread) { + publishMainStream(userId, 'readAllAntennas'); + } + }) }); diff --git a/src/services/stream.ts b/src/services/stream.ts index 32b990ced76a948c119e84c74b98ab6c856845ac..269aed56b968586bef32cda9b89a2df81eff50d2 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -5,6 +5,7 @@ import { UserList } from '../models/entities/user-list'; import { ReversiGame } from '../models/entities/games/reversi/game'; import { UserGroup } from '../models/entities/user-group'; import config from '../config'; +import { Antenna } from '../models/entities/antenna'; class Publisher { private publish = (channel: string, type: string | null, value?: any): void => { @@ -37,6 +38,10 @@ class Publisher { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } + public publishAntennaStream = (antennaId: Antenna['id'], type: string, value?: any): void => { + this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); + } + public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } @@ -61,10 +66,6 @@ class Publisher { this.publish('notesStream', null, note); } - public publishApLogStream = (log: any): void => { - this.publish('apLog', null, log); - } - public publishAdminStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } @@ -79,10 +80,10 @@ export const publishDriveStream = publisher.publishDriveStream; export const publishNoteStream = publisher.publishNoteStream; export const publishNotesStream = publisher.publishNotesStream; export const publishUserListStream = publisher.publishUserListStream; +export const publishAntennaStream = publisher.publishAntennaStream; export const publishMessagingStream = publisher.publishMessagingStream; export const publishGroupMessagingStream = publisher.publishGroupMessagingStream; export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; export const publishReversiStream = publisher.publishReversiStream; export const publishReversiGameStream = publisher.publishReversiGameStream; -export const publishApLogStream = publisher.publishApLogStream; export const publishAdminStream = publisher.publishAdminStream; diff --git a/webpack.config.ts b/webpack.config.ts index 195773f33bcc6cae48d19988834f0ce6f9719a06..bec2093d57158d381ceb22fd9b672bb08d1052c1 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -6,7 +6,6 @@ import * as fs from 'fs'; import * as webpack from 'webpack'; import * as chalk from 'chalk'; const { VueLoaderPlugin } = require('vue-loader'); -const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); @@ -20,13 +19,9 @@ class WebpackOnBuildPlugin { } const isProduction = process.env.NODE_ENV == 'production'; -const useHardSource = process.env.MISSKEY_USE_HARD_SOURCE; - -const constants = require('./src/const.json'); const locales = require('./locales'); const meta = require('./package.json'); -const codename = meta.codename; const postcss = { loader: 'postcss-loader', @@ -41,12 +36,8 @@ const postcss = { module.exports = { entry: { - desktop: './src/client/app/desktop/script.ts', - mobile: './src/client/app/mobile/script.ts', - dev: './src/client/app/dev/script.ts', - auth: './src/client/app/auth/script.ts', - admin: './src/client/app/admin/script.ts', - sw: './src/client/app/sw.js' + app: './src/client/init.ts', + sw: './src/client/sw.js' }, module: { rules: [{ @@ -64,7 +55,7 @@ module.exports = { loader: 'vue-svg-inline-loader' }] }, { - test: /\.styl(us)?$/, + test: /\.scss?$/, exclude: /node_modules/, oneOf: [{ resourceQuery: /module/, @@ -76,7 +67,13 @@ module.exports = { modules: true } }, postcss, { - loader: 'stylus-loader' + loader: 'sass-loader', + options: { + implementation: require('sass'), + sassOptions: { + fiber: require('fibers') + } + } }] }, { use: [{ @@ -84,7 +81,13 @@ module.exports = { }, { loader: 'css-loader' }, postcss, { - loader: 'stylus-loader' + loader: 'sass-loader', + options: { + implementation: require('sass'), + sassOptions: { + fiber: require('fibers') + } + } }] }] }, { @@ -107,42 +110,32 @@ module.exports = { loader: 'ts-loader', options: { happyPackMode: true, - configFile: __dirname + '/src/client/app/tsconfig.json', + transpileOnly: true, + configFile: __dirname + '/src/client/tsconfig.json', appendTsSuffixTo: [/\.vue$/] } }] }] }, plugins: [ - ...(useHardSource ? [new HardSourceWebpackPlugin()] : []), new ProgressBarPlugin({ - format: chalk` {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`, + format: chalk` {cyan.bold Yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray :elapseds}`, clear: false }), new webpack.DefinePlugin({ - _COPYRIGHT_: JSON.stringify(constants.copyright), _VERSION_: JSON.stringify(meta.version), - _CODENAME_: JSON.stringify(codename), _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]: [string, any]) => [k, v && v.meta && v.meta.lang])), _ENV_: JSON.stringify(process.env.NODE_ENV) }), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development') - }), + new VueLoaderPlugin(), new WebpackOnBuildPlugin((stats: any) => { fs.writeFileSync('./built/meta.json', JSON.stringify({ version: meta.version }), 'utf-8'); - - fs.mkdirSync('./built/client/assets/locales', { recursive: true }); - - for (const [lang, locale] of Object.entries(locales)) - fs.writeFileSync(`./built/client/assets/locales/${lang}.json`, JSON.stringify(locale), 'utf-8'); }), - new VueLoaderPlugin(), - new webpack.optimize.ModuleConcatenationPlugin() ], output: { path: __dirname + '/built/client/assets', filename: `[name].${meta.version}.js`, + chunkFilename: '[hash:5].[id].js', publicPath: `/assets/` }, resolve: { @@ -156,6 +149,9 @@ module.exports = { resolveLoader: { modules: ['node_modules'] }, + externals: { + moment: 'moment' + }, optimization: { minimizer: [new TerserPlugin()] }, diff --git a/yarn.lock b/yarn.lock index 764d48cbeca017da8e69aa6ddca128f4058676cf..cc96c1b9179e4fb854782e8f575aa030637d5aa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,33 +3,33 @@ "@babel/code-frame@^7.0.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" - integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== dependencies: - "@babel/highlight" "^7.0.0" + "@babel/highlight" "^7.8.3" -"@babel/highlight@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" - integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== dependencies: chalk "^2.0.0" esutils "^2.0.2" js-tokens "^4.0.0" "@babel/polyfill@^7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.7.0.tgz#e1066e251e17606ec7908b05617f9b7f8180d8f3" - integrity sha512-/TS23MVvo34dFmf8mwCisCbWGrfhbiWZSwBo6HkADTBhUa2Q/jWltyY/tpofz/b6/RIhqaqQcquptCirqIhOaQ== + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.8.3.tgz#2333fc2144a542a7c07da39502ceeeb3abe4debd" + integrity sha512-0QEgn2zkCzqGIkSWWAEmvxD7e00Nm9asTtQvi7HdlYvMhjy/J38V/1Y9ode0zEJeIuxAI0uftiAzqc7nVeWUGg== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.2" "@babel/runtime@^7.7.4": - version "7.7.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf" - integrity sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA== + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1" + integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w== dependencies: regenerator-runtime "^0.13.2" @@ -142,10 +142,10 @@ dependencies: type-detect "4.0.8" -"@tokenizer/token@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.0.tgz#9ee92540c56ca02e997d65dd14645cf9438c3dcf" - integrity sha512-fXk7a5R+aE8bfDRbfT+xRG2evSatjbljGGSUflfQmqw555My8II/EWly2GmcHaqXF5HCMitBEfSNhCRZCrLGGg== +"@tokenizer/token@^0.1.0", "@tokenizer/token@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3" + integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w== "@types/accepts@*": version "1.3.5" @@ -204,9 +204,9 @@ "@types/node" "*" "@types/cheerio@^0.22.10": - version "0.22.13" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6" - integrity sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA== + version "0.22.15" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.15.tgz#69040ffa92c309beeeeb7e92db66ac3f80700c0b" + integrity sha512-UGiiVtJK5niCqMKYmLEFz1Wl/3L5zF/u78lu8CwoUywWXRr9LDimeYuOzXVLXBMO758fcTdFtgjvqlztMH90MA== dependencies: "@types/node" "*" @@ -216,9 +216,9 @@ integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== "@types/connect@*": - version "3.4.32" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" - integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== dependencies: "@types/node" "*" @@ -252,18 +252,23 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/expect@^1.20.4": + version "1.20.4" + resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" + integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== + "@types/express-serve-static-core@*": - version "4.16.9" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz#69e00643b0819b024bdede95ced3ff239bb54558" - integrity sha512-GqpaVWR0DM8FnRUJYKlWgyARoBUAVfRIeVDZQKOttLFp5SmhhF9YFIYeTPwMd/AXfxlP7xVO2dj1fGu0Q+krKQ== + version "4.17.1" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.1.tgz#82be64a77211b205641e0209096fd3afb62481d3" + integrity sha512-9e7jj549ZI+RxY21Cl0t8uBnWyb22HzILupyHZjYEVK//5TT/1bZodU+yUbLnPdoYViBBnNWbxp4zYjGV0zUGw== dependencies: "@types/node" "*" "@types/range-parser" "*" "@types/express@*": - version "4.17.1" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.1.tgz#4cf7849ae3b47125a567dfee18bfca4254b88c5c" - integrity sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w== + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c" + integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "*" @@ -335,9 +340,9 @@ integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ== "@types/ioredis@*": - version "4.0.18" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.0.18.tgz#6c48f43cb03992960d724ab9e549d581d12e3a76" - integrity sha512-iDIRGPGP4LwoeiKNxQcI38ZA5T8SC+MbGCiiNFJ+LNy9tdegj6f9PAZ7se4tiWJhUHbf25kEJt7k3YfmYjWKZg== + version "4.14.4" + resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.14.4.tgz#4e46ab8f995f860535518bc9fa206c36f325aa69" + integrity sha512-6eM+TBd6YE8E+4DruPYKBYvMKSx+eRdBcJLlaxqZsViR5UQWu+VEkkltet5Z2ZFhRqHMnDf38xDvI1GESCD2Ig== dependencies: "@types/node" "*" @@ -361,9 +366,9 @@ parse5 "^4.0.0" "@types/json-schema@^7.0.3": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" - integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + version "7.0.4" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" + integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== "@types/katex@0.11.0": version "0.11.0" @@ -371,9 +376,9 @@ integrity sha512-27BfE8zASRLYfSBNMk5/+KIjr2CBBrH0i5lhsO04fca4TGirIIMay73v3zNkzqmsaeIa/Mi5kejWDcxPLAmkvA== "@types/keygrip@*": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.1.tgz#ff540462d2fb4d0a88441ceaf27d287b01c3d878" - integrity sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg= + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" + integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== "@types/koa-bodyparser@4.3.0": version "4.3.0" @@ -383,9 +388,9 @@ "@types/koa" "*" "@types/koa-compose@*": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.4.tgz#76a461634a59c3e13449831708bb9b355fb1548e" - integrity sha512-ioou0rxkuWL+yBQYsHUQAzRTfVxAg8Y2VfMftU+Y3RA03/MzuFL0x/M2sXXj3PkfnENbHsjeHR1aMdezLYpTeA== + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" + integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ== dependencies: "@types/koa" "*" @@ -439,19 +444,7 @@ dependencies: "@types/koa" "*" -"@types/koa@*": - version "2.0.50" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.50.tgz#c295cbbd59f4898e7a8794c0f2e42e60da83aab2" - integrity sha512-TcgOD2lh0EISSadAk1DOBYw7kNoY9XdeB3vEMOKiDDaTMYm+V54nyPsU7Ulb/htb5OBIR79RgTeCWntCcophLw== - dependencies: - "@types/accepts" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - -"@types/koa@2.11.0": +"@types/koa@*", "@types/koa@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.0.tgz#394a3e9ec94f796003a6c8374b4dbc2778746f20" integrity sha512-Hgx/1/rVlJvqYBrdeCsS7PDiR2qbxlMt1RnmNWD4Uxi5FF9nwkYqIldo7urjc+dfNpk+2NRGcnAYd4L5xEhCcQ== @@ -505,9 +498,9 @@ integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== "@types/node@*": - version "12.7.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc" - integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ== + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b" + integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A== "@types/node@13.1.4": version "13.1.4" @@ -610,17 +603,7 @@ dependencies: "@types/node" "*" -"@types/request@*": - version "2.48.3" - resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.3.tgz#970b8ed2317568c390361d29c555a95e74bd6135" - integrity sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w== - dependencies: - "@types/caseless" "*" - "@types/node" "*" - "@types/tough-cookie" "*" - form-data "^2.5.0" - -"@types/request@2.48.4": +"@types/request@*", "@types/request@2.48.4": version "2.48.4" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.4.tgz#df3d43d7b9ed3550feaa1286c6eabf0738e6cf7e" integrity sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw== @@ -683,9 +666,9 @@ systeminformation "*" "@types/tapable@*": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" - integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" + integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== "@types/tinycolor2@1.4.2": version "1.4.2" @@ -698,9 +681,9 @@ integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== "@types/tough-cookie@*": - version "2.3.5" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d" - integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg== + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" + integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ== "@types/uglify-js@*": version "3.0.4" @@ -738,10 +721,11 @@ "@types/vinyl" "*" "@types/vinyl@*": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.3.tgz#80a6ce362ab5b32a0c98e860748a31bce9bff0de" - integrity sha512-hrT6xg16CWSmndZqOTJ6BGIn2abKyTw0B58bI+7ioUoj3Sma6u8ftZ1DTI2yCaJamOVGLOnQWiPH3a74+EaqTA== + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a" + integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ== dependencies: + "@types/expect" "^1.20.4" "@types/node" "*" "@types/web-push@3.3.0": @@ -752,9 +736,9 @@ "@types/node" "*" "@types/webpack-sources@*": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" - integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.6.tgz#3d21dfc2ec0ad0c77758e79362426a9ba7d7cbcb" + integrity sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ== dependencies: "@types/node" "*" "@types/source-list-map" "*" @@ -769,9 +753,9 @@ "@types/webpack" "*" "@types/webpack@*": - version "4.39.3" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.39.3.tgz#1d55f8fce117a325368bf7612950552ee4ed4467" - integrity sha512-afGNNuTfKk1YfHrQ+IwF0QhDkSSMIMMt8BRRErTKaGVvWTMABDjT22/4kJ4bRoSzir9LVgxuuceyZ4Z5I82Cyg== + version "4.41.2" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.2.tgz#c6faf0111de27afdffe1158dac559e447c273516" + integrity sha512-DNMQOfEvwzWRRyp6Wy9QVCgJ3gkelZsuBE2KUD318dg95s9DKGiT5CszmmV58hq8jk89I9NClre48AEy1MWAJA== dependencies: "@types/anymatch" "*" "@types/node" "*" @@ -839,16 +823,16 @@ tsutils "^3.17.1" "@vue/component-compiler-utils@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.0.tgz#64cd394925f5af1f9c3228c66e954536f5311857" - integrity sha512-OJ7swvl8LtKtX5aYP8jHhO6fQBIRIGkU6rvWzK+CGJiNOnvg16nzcBkd9qMZzW8trI2AsqAKx263nv7kb5rhZw== + version "3.1.1" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.1.tgz#d4ef8f80292674044ad6211e336a302e4d2a6575" + integrity sha512-+lN3nsfJJDGMNz7fCpcoYIORrXo0K3OTsdr8jCM7FuqdI4+70TY6gxY6viJ2Xi1clqyPg7LpeOWwjF31vSMmUw== dependencies: consolidate "^0.15.1" hash-sum "^1.0.2" lru-cache "^4.1.2" merge-source-map "^1.1.0" postcss "^7.0.14" - postcss-selector-parser "^5.0.0" + postcss-selector-parser "^6.0.2" prettier "^1.18.2" source-map "~0.6.1" vue-template-es2015-compiler "^1.9.0" @@ -1010,9 +994,9 @@ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" - integrity sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" + integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== abbrev@1: version "1.1.1" @@ -1027,26 +1011,6 @@ accepts@^1.3.5: mime-types "~2.1.24" negotiator "0.6.2" -accord@^0.26.3: - version "0.26.4" - resolved "https://registry.yarnpkg.com/accord/-/accord-0.26.4.tgz#fc4c8d3ebab406a07cb28819b859651c44a92e80" - integrity sha1-/EyNPrq0BqB8sogZuFllHESpLoA= - dependencies: - convert-source-map "^1.2.0" - glob "^7.0.5" - indx "^0.2.3" - lodash.clone "^4.3.2" - lodash.defaults "^4.0.1" - lodash.flatten "^4.2.0" - lodash.merge "^4.4.0" - lodash.partialright "^4.1.4" - lodash.pick "^4.2.1" - lodash.uniq "^4.3.0" - resolve "^1.1.7" - semver "^5.3.0" - uglify-js "^2.7.0" - when "^3.7.7" - acorn-globals@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" @@ -1088,9 +1052,9 @@ acorn@^4.0.4, acorn@~4.0.2: integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= acorn@^6.0.1, acorn@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" - integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== + version "6.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784" + integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== acorn@^7.1.0: version "7.1.0" @@ -1137,11 +1101,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== dependencies: - fast-deep-equal "^2.0.1" + fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" @@ -1182,13 +1146,6 @@ ansi-colors@^3.0.5: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-cyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" - integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= - dependencies: - ansi-wrap "0.1.0" - ansi-escapes@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" @@ -1203,13 +1160,6 @@ ansi-gray@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-red@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" - integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= - dependencies: - ansi-wrap "0.1.0" - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -1243,9 +1193,9 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.0.tgz#5681f0dcf7ae5880a7841d8831c4724ed9cc0172" - integrity sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg== + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== dependencies: "@types/color-name" "^1.1.1" color-convert "^2.0.1" @@ -1324,9 +1274,9 @@ are-we-there-yet@~1.1.2: readable-stream "^2.0.6" arg@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" - integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw== + version "4.1.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064" + integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg== argparse@^1.0.7: version "1.0.10" @@ -1335,14 +1285,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -arr-diff@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a" - integrity sha1-aHwydYFjWI/vfeezb6vklesaOZo= - dependencies: - arr-flatten "^1.0.1" - array-slice "^0.2.3" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1367,11 +1309,6 @@ arr-map@^2.0.0, arr-map@^2.0.2: dependencies: make-iterator "^1.0.0" -arr-union@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" - integrity sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= - arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -1402,11 +1339,6 @@ array-last@^1.1.1: dependencies: is-number "^4.0.0" -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= - array-slice@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" @@ -1441,13 +1373,14 @@ asn1.js@^4.0.0: minimalistic-assert "^1.0.0" asn1.js@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.2.0.tgz#292c0357f26a47802ac9727e8772c09c7fc9bd85" - integrity sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A== + version "5.3.0" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689" + integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA== dependencies: bn.js "^4.0.0" inherits "^2.0.1" minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" asn1@~0.2.3: version "0.2.4" @@ -1499,11 +1432,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-limiter@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async-settle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" @@ -1535,7 +1463,7 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atob@^2.1.1: +atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== @@ -1578,9 +1506,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== babel-runtime@^6.26.0: version "6.26.0" @@ -1681,9 +1609,16 @@ binary-extensions@^2.0.0: integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== binaryextensions@2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.2.tgz#c83c3d74233ba7674e4f313cb2a2b70f54e94b7c" - integrity sha512-xVNN69YGDghOqCCtA6FI7avYrr02mTJjOgB0/f1VPD3pJC8QEvjTKWc4epDx8AqxxA75NI0QpVM2gPJXUbE4Tg== + version "2.2.0" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.2.0.tgz#e7c6ba82d4f5f5758c26078fe8eea28881233311" + integrity sha512-bHhs98rj/7i/RZpCSJ3uk55pLXOItjIrh2sRQZSM6OoktScX+LxJzvlU+FELp9j3TdcddTmmYArLSGptCTwjuw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" bl@^3.0.0: version "3.0.0" @@ -1693,9 +1628,9 @@ bl@^3.0.0: readable-stream "^3.0.1" bluebird@^3.1.1, bluebird@^3.4.1, bluebird@^3.5.5: - version "3.7.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.0.tgz#56a6a886e03f6ae577cffedeb524f8f2450293cf" - integrity sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg== + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1718,16 +1653,11 @@ bootstrap-vue@2.1.0: portal-vue "^2.1.6" vue-functional-data-merge "^3.1.0" -bootstrap@4.4.1: +bootstrap@4.4.1, "bootstrap@>=4.3.1 <5.0.0": version "4.4.1" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.4.1.tgz#8582960eea0c5cd2bede84d8b0baf3789c3e8b01" integrity sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA== -"bootstrap@>=4.3.1 <5.0.0": - version "4.3.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" - integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1834,13 +1764,13 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.0.0: - version "4.8.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.2.tgz#b45720ad5fbc8713b7253c20766f701c9a694289" - integrity sha512-+M4oeaTplPm/f1pXDw84YohEv7B1i/2Aisei8s4s6k3QsoSHa7i5sz8u/cGQkkatCPxMASKxPualR4wwYgVboA== + version "4.8.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.3.tgz#65802fcd77177c878e015f0e3189f2c4f627ba44" + integrity sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg== dependencies: - caniuse-lite "^1.0.30001015" + caniuse-lite "^1.0.30001017" electron-to-chromium "^1.3.322" - node-releases "^1.1.42" + node-releases "^1.1.44" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1885,7 +1815,7 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@4.9.1, buffer@^4.3.0: +buffer@4.9.1: version "4.9.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= @@ -1894,6 +1824,15 @@ buffer@4.9.1, buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^5.1.0, buffer@^5.4.3: version "5.4.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" @@ -2071,10 +2010,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001015: - version "1.0.30001015" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001015.tgz#15a7ddf66aba786a71d99626bc8f2b91c6f0f5f0" - integrity sha512-/xL2AbW/XWHNu1gnIrO8UitBGoFthcsDgU9VLK1/dpsoxbaD5LscHozKze05R6WLsBvLhqv78dAPozMFQBYLbQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001017: + version "1.0.30001021" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001021.tgz#e75ed1ef6dbadd580ac7e7720bb16f07b083f254" + integrity sha512-wuMhT7/hwkgd8gldgp2jcrUjOU9RXJ4XxGumQeOsUr91l3WwmM68Cpa/ymCnWEDqakwFXhuDQbaKNHXBPgeE9g== caseless@~0.12.0: version "0.12.0" @@ -2149,6 +2088,29 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chart.js@2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7" + integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw== + dependencies: + chartjs-color "^2.1.0" + moment "^2.10.2" + +chartjs-color-string@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71" + integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A== + dependencies: + color-name "^1.0.0" + +chartjs-color@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0" + integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w== + dependencies: + chartjs-color-string "^0.6.0" + color-convert "^1.9.3" + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -2215,6 +2177,21 @@ chokidar@3.3.0: optionalDependencies: fsevents "~2.1.1" +"chokidar@>=2.0.0 <4.0.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" + integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.3.0" + optionalDependencies: + fsevents "~2.1.2" + chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.1.2: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2288,7 +2265,7 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-highlight@2.1.4: +cli-highlight@2.1.4, cli-highlight@^2.0.0: version "2.1.4" resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.4.tgz#098cb642cf17f42adc1c1145e07f960ec4d7522b" integrity sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ== @@ -2300,17 +2277,6 @@ cli-highlight@2.1.4: parse5-htmlparser2-tree-adapter "^5.1.1" yargs "^15.0.0" -cli-highlight@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.1.tgz#2180223d51618b112f4509cf96e4a6c750b07e97" - integrity sha512-0y0VlNmdD99GXZHYnvrQcmHxP8Bi6T00qucGgBgGv4kJ0RyDthNnnFPupHV7PYv/OXSVk+azFbOeaW6+vGmx9A== - dependencies: - chalk "^2.3.0" - highlight.js "^9.6.0" - mz "^2.4.0" - parse5 "^4.0.0" - yargs "^13.0.0" - cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -2366,6 +2332,15 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" @@ -2446,7 +2421,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -2503,17 +2478,12 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== - commander@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== -commander@^2.12.1, commander@^2.19.0, commander@^2.20.0: +commander@^2.12.1, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2529,11 +2499,11 @@ component-emitter@^1.2.1: integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== compressible@^2.0.0: - version "2.0.17" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" - integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: - mime-db ">= 1.40.0 < 2" + mime-db ">= 1.43.0 < 2" concat-map@0.0.1: version "0.0.1" @@ -2568,16 +2538,14 @@ config-chain@^1.1.12: proto-list "~1.2.1" consola@^2.10.1: - version "2.10.1" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.10.1.tgz#4693edba714677c878d520e4c7e4f69306b4b927" - integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w== + version "2.11.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.11.3.tgz#f7315836224c143ac5094b47fd4c816c2cd1560e" + integrity sha512-aoW0YIIAmeftGR8GSpw6CGQluNdkWMWh3yEFjH/hmynTYnMtibXszii3lxCXmk8YxJtI3FAK5aTiquA5VH68Gw== console-browserify@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" - integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= - dependencies: - date-now "^0.1.4" + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" @@ -2618,10 +2586,10 @@ content-type@^1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@1.X, convert-source-map@^1.2.0, convert-source-map@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== +convert-source-map@1.X, convert-source-map@^1.5.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" @@ -2672,9 +2640,9 @@ copy-to@^2.0.1: integrity sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU= core-js@^2.4.0, core-js@^2.6.10, core-js@^2.6.5: - version "2.6.10" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" - integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -2818,25 +2786,18 @@ css-loader@3.4.1: postcss-value-parser "^4.0.2" schema-utils "^2.6.0" -css-parse@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" - integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= - dependencies: - css "^2.0.0" - css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== css-select@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede" - integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== dependencies: boolbase "^1.0.0" - css-what "^2.1.2" + css-what "^3.2.1" domutils "^1.7.0" nth-check "^1.0.2" @@ -2863,12 +2824,17 @@ css-unit-converter@^1.1.1: resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= -css-what@2.1, css-what@^2.1.2: +css-what@2.1: version "2.1.3" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== -css@2.X, css@^2.0.0, css@^2.2.1: +css-what@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" + integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== + +css@2.X, css@^2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== @@ -3014,11 +2980,6 @@ data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-now@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" - integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= - dateformat@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -3052,7 +3013,7 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@3.2.6, debug@3.X, debug@^3.1.0, debug@^3.2.6: +debug@3.2.6, debug@3.X, debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3127,7 +3088,7 @@ default-resolution@^2.0.0: resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= -define-properties@^1.1.1, define-properties@^1.1.2, define-properties@^1.1.3: +define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -3187,9 +3148,9 @@ depd@~2.0.0: integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== des.js@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" - integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw= + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== dependencies: inherits "^2.0.1" minimalistic-assert "^1.0.0" @@ -3209,7 +3170,7 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-libc@^1.0.2, detect-libc@^1.0.3: +detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= @@ -3233,9 +3194,9 @@ diff@3.5.0: integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== diff@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" - integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== diffie-hellman@^5.0.0: version "5.0.3" @@ -3272,9 +3233,9 @@ doctypes@^1.1.0: integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= dom-serializer@0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb" - integrity sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q== + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== dependencies: domelementtype "^2.0.1" entities "^2.0.0" @@ -3403,14 +3364,14 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.322: - version "1.3.322" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz#a6f7e1c79025c2b05838e8e344f6e89eb83213a8" - integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA== + version "1.3.337" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.337.tgz#b2c093cdb66121a946d333b454adcdc5666ceaed" + integrity sha512-uJ+wLjslYQ/2rAusDg+6FlK8DLhHWTLCe7gkofBehTifW7KCkPVTn5rhKSCncWYNq34Iy/o4OfswuEkAO2RBaw== elliptic@^6.0.0: - version "6.5.1" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b" - integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg== + version "6.5.2" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" + integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -3494,39 +3455,40 @@ error-inject@^1.0.0: resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= -es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.1: - version "1.15.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.15.0.tgz#8884928ec7e40a79e3c9bc812d37d10c8b24cc57" - integrity sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ== +es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: + version "1.17.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.2.tgz#965b10af56597b631da15872c17a405e86c1fd46" + integrity sha512-YoKuru3Lyoy7yVTBSH2j7UxTqe/je3dWAruC0sHvZX1GNd5zX8SSLvQqEgO9b3Ex8IW+goFI9arEEsFIbulhOw== dependencies: - es-to-primitive "^1.2.0" + es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" - has-symbols "^1.0.0" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-inspect "^1.6.0" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" object-keys "^1.1.1" - string.prototype.trimleft "^2.1.0" - string.prototype.trimright "^2.1.0" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== dependencies: is-callable "^1.1.4" is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.51" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.51.tgz#ed2d7d9d48a12df86e0299287e93a09ff478842f" - integrity sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ== +es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.53" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== dependencies: es6-iterator "~2.0.3" - es6-symbol "~3.1.1" - next-tick "^1.0.0" + es6-symbol "~3.1.3" + next-tick "~1.0.0" es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3: version "2.0.3" @@ -3549,13 +3511,13 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -es6-symbol@^3.1.1, es6-symbol@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.2.tgz#859fdd34f32e905ff06d752e7171ddd4444a7ed1" - integrity sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ== +es6-symbol@^3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== dependencies: d "^1.0.1" - es5-ext "^0.10.51" + ext "^1.1.2" es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: version "2.0.3" @@ -3583,11 +3545,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.11.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" - integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== + version "1.13.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.13.0.tgz#c7adf9bd3f3cc675bb752f202f79a720189cab29" + integrity sha512-eYk2dCkxR07DsHA/X2hRBj0CFAZeri/LyDMc0C8JT1Hqi6JnVpMhJ7XFITbb0+yZS3lVkaPL2oCkZ3AVmeVbMw== dependencies: - esprima "^3.1.3" + esprima "^4.0.1" estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" @@ -3682,12 +3644,7 @@ espree@^6.1.2: acorn-jsx "^5.1.0" eslint-visitor-keys "^1.1.0" -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esprima@^4.0.0: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -3735,9 +3692,9 @@ events@1.1.1: integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= events@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" - integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" + integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" @@ -3805,12 +3762,12 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -extend-shallow@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" - integrity sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= +ext@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" + integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== dependencies: - kind-of "^1.1.0" + type "^2.0.0" extend-shallow@^2.0.1: version "2.0.1" @@ -3875,17 +3832,17 @@ fancy-log@1.3.3, fancy-log@^1.3.2: parse-node-version "^1.0.0" time-stamp "^1.0.0" -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: +fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -3897,6 +3854,13 @@ feed@4.1.0: dependencies: xml-js "^1.6.11" +fibers@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fibers/-/fibers-4.0.2.tgz#d04f9ccd0aba179588202202faeb4fed65d497f5" + integrity sha512-FhICi1K4WZh9D6NC18fh2ODF3EWy1z0gzIdV9P7+s2pRjfRBnCkMDJ6x3bV1DkVymKH8HGrQa/FNOBjYvnJ/tQ== + dependencies: + detect-libc "^1.0.3" + figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -3931,6 +3895,11 @@ file-type@13.0.1: token-types "^2.0.0" typedarray-to-buffer "^3.1.5" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -4127,13 +4096,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-minipass@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== - dependencies: - minipass "^2.6.0" - fs-minipass@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.0.0.tgz#a6415edab02fae4b9e9230bc87ee2e4472003cd1" @@ -4165,14 +4127,14 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.9" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" - integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== dependencies: + bindings "^1.5.0" nan "^2.12.1" - node-pre-gyp "^0.12.0" -fsevents@~2.1.1: +fsevents@~2.1.1, fsevents@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== @@ -4224,11 +4186,9 @@ get-paths@0.0.7: pify "^4.0.1" get-port@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6" - integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ== - dependencies: - type-fest "^0.3.0" + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== get-stream@^4.0.0: version "4.1.0" @@ -4316,19 +4276,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.6: +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -4398,9 +4346,9 @@ good-listener@^1.2.2: delegate "^3.1.2" graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" - integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== growl@1.10.5: version "1.10.5" @@ -4441,6 +4389,20 @@ gulp-cli@^2.2.0: v8flags "^3.0.1" yargs "^7.1.0" +gulp-dart-sass@0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/gulp-dart-sass/-/gulp-dart-sass-0.9.1.tgz#6243d12744759e17568bf06183b4aa1a16a74432" + integrity sha512-6gmKmzDwSzjrN1nIC7K0URpX3fG5jMXxbpBLF42GFhnuM5+9Is7D9W55K+9A0MnFeqKyZGIzof4a9GDpB8wJgQ== + dependencies: + chalk "^2.3.0" + lodash.clonedeep "^4.3.2" + plugin-error "^1.0.1" + replace-ext "^1.0.0" + sass "^1.10.3" + strip-ansi "^4.0.0" + through2 "^2.0.0" + vinyl-sourcemaps-apply "^0.2.0" + gulp-mocha@7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.2.tgz#c7e13d133b3fde96d777e877f90b46225255e408" @@ -4484,19 +4446,6 @@ gulp-sourcemaps@2.6.5: strip-bom-string "1.X" through2 "2.X" -gulp-stylus@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/gulp-stylus/-/gulp-stylus-2.7.0.tgz#f3e932626004927b75ea27ff5c1d3b0ba0b7cbb1" - integrity sha512-LlneLeHcaRBaEqxwo5YCirpsfkR7uleQ4pHXW8IE2ZeA6M3jpgI90+zQ6SptMTSWr1RSQW3WYFZVA3P0coUojw== - dependencies: - accord "^0.26.3" - lodash.assign "^3.2.0" - plugin-error "^0.1.2" - replace-ext "0.0.1" - stylus "^0.54.0" - through2 "^2.0.0" - vinyl-sourcemaps-apply "^0.2.0" - gulp-terser@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gulp-terser/-/gulp-terser-1.2.0.tgz#41df2a1d0257d011ba8b05efb2568432ecd0495b" @@ -4548,6 +4497,17 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" +handlebars@^4.5.3: + version "4.7.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.2.tgz#01127b3840156a0927058779482031afe0e730d7" + integrity sha512-4PwqDL2laXtTWZghzzCtunQUTLbo31pcCJrd/B/9JP8XbhVzpS5ZXuKqlOzsd1rtcaLo4KqAn8nl8mkknS4MHw== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -4597,10 +4557,10 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== has-unicode@^2.0.0: version "2.0.1" @@ -4638,7 +4598,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.0, has@^1.0.1, has@^1.0.3: +has@^1.0.0, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -4677,14 +4637,16 @@ hex-color-regex@^1.1.0: integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== highlight.js@^9.6.0: - version "9.15.10" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" - integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== + version "9.17.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.17.1.tgz#14a4eded23fd314b05886758bb906e39dd627f9a" + integrity sha512-TA2/doAur5Ol8+iM3Ov7qy3jYcr/QiJ2eDTdRF4dfbjG7AaaB99J5G+zSl11ljbl6cIcahgPY6SKb3sC3EJ0fw== + dependencies: + handlebars "^4.5.3" highlightjs@^9.8.0: - version "9.12.0" - resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.12.0.tgz#9b84eb42a7aa8488eb69ac79fec44cf495bf72a1" - integrity sha512-eAhWMtDZaOZIQdxIP4UEB1vNp/CVXQPdMSihTSuaExhFIRC0BVpXbtP3mTP1hDoGOyh7nbB3cuC3sOPhG5wGDA== + version "9.16.2" + resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.16.2.tgz#07ea6cc7c93340fc440734fb7abf28558f1f0fe1" + integrity sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg== hmac-drbg@^1.0.0: version "1.0.1" @@ -4834,9 +4796,9 @@ https-proxy-agent@4.0.0: debug "4" https-proxy-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" - integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" + integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== dependencies: agent-base "^4.3.0" debug "^3.1.0" @@ -4853,7 +4815,7 @@ humanize-number@0.0.2: resolved "https://registry.yarnpkg.com/humanize-number/-/humanize-number-0.0.2.tgz#11c0af6a471643633588588048f1799541489c18" integrity sha1-EcCvakcWQ2M1iFiASPF5lUFInBg= -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -4877,13 +4839,6 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= -ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== - dependencies: - minimatch "^3.0.4" - ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -4910,9 +4865,9 @@ import-fresh@^2.0.0: resolve-from "^3.0.0" import-fresh@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118" - integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -4947,11 +4902,6 @@ indexes-of@^1.0.1: resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= -indx@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/indx/-/indx-0.2.3.tgz#15dcf56ee9cf65c0234c513c27fbd580e70fbc50" - integrity sha1-Fdz1bunPZcAjTFE8J/vVgOcPvFA= - infer-owner@^1.0.3, infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -4991,9 +4941,9 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== inquirer@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" - integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ== + version "7.0.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.3.tgz#f9b4cd2dff58b9f73e8d43759436ace15bed4567" + integrity sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw== dependencies: ansi-escapes "^4.2.1" chalk "^2.4.2" @@ -5004,7 +4954,7 @@ inquirer@^7.0.0: lodash "^4.17.15" mute-stream "0.0.8" run-async "^2.2.0" - rxjs "^6.4.0" + rxjs "^6.5.3" string-width "^4.1.0" strip-ansi "^5.1.0" through "^2.3.6" @@ -5118,10 +5068,10 @@ is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== +is-callable@^1.1.4, is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== is-color-stop@^1.0.0: version "1.1.0" @@ -5150,9 +5100,9 @@ is-data-descriptor@^1.0.0: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== is-descriptor@^0.1.0: version "0.1.6" @@ -5239,11 +5189,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: is-extglob "^2.1.1" is-nan@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" - integrity sha1-n69ltvttskt/XAYoR16nH5iEAeI= + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" + integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ== dependencies: - define-properties "^1.1.1" + define-properties "^1.1.3" is-negated-glob@^1.0.0: version "1.0.0" @@ -5296,12 +5246,12 @@ is-promise@^2.0.0, is-promise@^2.1, is-promise@^2.1.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= -is-regex@^1.0.3, is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= +is-regex@^1.0.3, is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== dependencies: - has "^1.0.1" + has "^1.0.3" is-relative@^1.0.0: version "1.0.0" @@ -5345,11 +5295,11 @@ is-svg@^3.0.0: html-comment-regex "^1.1.0" is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== dependencies: - has-symbols "^1.0.0" + has-symbols "^1.0.1" is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" @@ -5453,9 +5403,9 @@ jpeg-js@^0.3.3: integrity sha512-MUj2XlMB8kpe+8DJUGH/3UJm4XpI8XEgZQ+CiHDeyrGoKPdW/8FJv6ku+3UiYm5Fz3CWaL+iXmD8Q4Ap6aC1Jw== js-beautify@^1.6.12: - version "1.10.2" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.2.tgz#88c9099cd6559402b124cfab18754936f8a7b178" - integrity sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ== + version "1.10.3" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.3.tgz#c73fa10cf69d3dfa52d8ed624f23c64c0a6a94c1" + integrity sha512-wfk/IAWobz1TfApSdivH5PJ0miIHgDoYb1ugSqHcODPmaYu46rYe5FVuIEkhjg8IQiv6rDNPyhsqbsohI/C2vQ== dependencies: config-chain "^1.1.12" editorconfig "^0.15.3" @@ -5487,9 +5437,9 @@ jsbn@~0.1.0: integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= jschardet@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.0.tgz#0491e1e00ec091490d4ca3dc8e30926b38de596b" - integrity sha512-Fuk25QlebgHnvCQvAm258+y2D8yVs1VFIFwiqXbvxG4n9vo5YZsBPZuMxnc7Gb9ElAeN8QfRDvgkkIhW4DPoPA== + version "2.1.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184" + integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q== jsdom@15.2.1: version "15.2.1" @@ -5635,11 +5585,6 @@ keygrip@~1.1.0: dependencies: tsscmp "1.0.6" -kind-of@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" - integrity sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= - kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -5660,9 +5605,9 @@ kind-of@^5.0.0, kind-of@^5.0.2: integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== koa-bodyparser@4.2.1: version "4.2.1" @@ -5937,52 +5882,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4= - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._createassigner@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" - integrity sha1-g4pbri/aymOsIt7o4Z+k5taXCxE= - dependencies: - lodash._bindcallback "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.restparam "^3.0.0" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw= - -lodash.assign@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" - integrity sha1-POnwI0tLIiPilrj6CsH+6OvKZPo= - dependencies: - lodash._baseassign "^3.0.0" - lodash._createassigner "^3.0.0" - lodash.keys "^3.0.0" - lodash.assignin@^4.0.9: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" @@ -5993,12 +5892,7 @@ lodash.bind@^4.1.4: resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= -lodash.clone@^4.3.2: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" - integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= - -lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.3.2: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= @@ -6023,16 +5917,6 @@ lodash.foreach@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= - lodash.isfinite@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3" @@ -6043,15 +5927,6 @@ lodash.isregexp@3.0.5: resolved "https://registry.yarnpkg.com/lodash.isregexp/-/lodash.isregexp-3.0.5.tgz#e0f596242f2fa228a840086b6c8ad82e4b71fd2d" integrity sha1-4PWWJC8voiioQAhrbIrYLktx/S0= -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo= - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - lodash.map@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -6067,11 +5942,6 @@ lodash.merge@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.partialright@^4.1.4: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.partialright/-/lodash.partialright-4.2.1.tgz#0130d80e83363264d40074f329b8a3e7a8a1cc4b" - integrity sha1-ATDYDoM2MmTUAHTzKbij56ihzEs= - lodash.pick@^4.2.1: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -6087,11 +5957,6 @@ lodash.reject@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU= -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.some@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -6112,7 +5977,7 @@ lodash.unescape@4.0.1: resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= -lodash.uniq@^4.3.0, lodash.uniq@^4.5.0: +lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= @@ -6365,22 +6230,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -"mime-db@>= 1.40.0 < 2": - version "1.42.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" - integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== +mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== dependencies: - mime-db "1.40.0" + mime-db "1.43.0" mime@^2.4.4: version "2.4.4" @@ -6424,6 +6284,11 @@ minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -6445,28 +6310,13 @@ minipass-pipeline@^1.2.2: dependencies: minipass "^3.0.0" -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minipass@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.0.1.tgz#b4fec73bd61e8a40f0b374ddd04260ade2c8ec20" - integrity sha512-2y5okJ4uBsjoD2vAbLKL9EUQPPkC0YMIp+2mZOXG3nBba++pdfJWRxx2Ewirc0pwAJYu4XtWg2EkVo1nRXuO/w== +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5" + integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w== dependencies: yallist "^4.0.0" -minizlib@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== - dependencies: - minipass "^2.9.0" - minizlib@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3" @@ -6499,7 +6349,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1, mkdirp@~0.5.x: +mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -6537,9 +6387,9 @@ mocha@7.0.0: yargs-unparser "1.6.0" mocha@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c" - integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A== + version "6.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20" + integrity sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A== dependencies: ansi-colors "3.2.3" browser-stdout "1.3.1" @@ -6573,13 +6423,13 @@ moji@0.5.1: object-assign "^3.0.0" moment-timezone@^0.5.25: - version "0.5.26" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.26.tgz#c0267ca09ae84631aa3dc33f65bedbe6e8e0d772" - integrity sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g== + version "0.5.27" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.27.tgz#73adec8139b6fe30452e78f210f27b1f346b8877" + integrity sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw== dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.22.2: +"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== @@ -6676,21 +6526,12 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0, neo-async@^2.6.1: +neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== @@ -6705,7 +6546,7 @@ next-line@^1.1.0: resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603" integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM= -next-tick@1, next-tick@^1.0.0: +next-tick@1, next-tick@^1.0.0, next-tick@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= @@ -6723,9 +6564,9 @@ no-case@^2.2.0: lower-case "^1.1.1" node-abi@^2.7.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.11.0.tgz#b7dce18815057544a049be5ae75cd1fdc2e9ea59" - integrity sha512-kuy/aEg75u40v378WRllQ4ZexaXJiCvB68D2scDXclp/I4cRq6togpbOoKhmN07tns9Zldu51NNERo0wehfX9g== + version "2.13.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63" + integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA== dependencies: semver "^5.4.1" @@ -6784,26 +6625,10 @@ node-object-hash@^1.2.0: resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94" integrity sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ== -node-pre-gyp@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" - integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -node-releases@^1.1.42: - version "1.1.42" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.42.tgz#a999f6a62f8746981f6da90627a8d2fc090bbad7" - integrity sha512-OQ/ESmUqGawI2PRX+XIRao44qWYBBfN54ImQYdWVTQqUckuejOg76ysSqDBK8NG3zwySRVnX36JwDQ6x+9GxzA== +node-releases@^1.1.44: + version "1.1.46" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.46.tgz#6b262afef1bdc9a950a96df2e77e0d2290f484bf" + integrity sha512-YOjdx+Uoh9FbRO7yVYbnbt1puRWPQMemR3SutLeyv2XfxKs1ihpe0OLAUwBPEP2ImNH/PZC7SEiC6j32dwRZ7g== dependencies: semver "^6.3.0" @@ -6822,7 +6647,7 @@ noop-logger@^0.1.1: resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= -nopt@^4.0.1, nopt@~4.0.1: +nopt@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= @@ -6864,19 +6689,6 @@ now-and-later@^2.0.0: dependencies: once "^1.3.2" -npm-bundled@^1.0.1: - version "1.0.6" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" - integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== - -npm-packlist@^1.1.6: - version "1.4.6" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4" - integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -6891,7 +6703,7 @@ npm-run-path@^3.0.0: dependencies: path-key "^3.0.0" -npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2: +npmlog@^4.0.1, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -6957,10 +6769,10 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" - integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -6974,7 +6786,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@4.1.0, object.assign@^4.0.1, object.assign@^4.0.4: +object.assign@4.1.0, object.assign@^4.0.1, object.assign@^4.0.4, object.assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -6994,13 +6806,13 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= +object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" object.map@^1.0.0: version "1.0.1" @@ -7026,12 +6838,12 @@ object.reduce@^1.0.0: make-iterator "^1.0.0" object.values@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" - integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== dependencies: define-properties "^1.1.3" - es-abstract "^1.12.0" + es-abstract "^1.17.0-next.1" function-bind "^1.1.1" has "^1.0.3" @@ -7066,19 +6878,15 @@ opentype.js@^0.4.3: resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.4.11.tgz#281a2390639cc15931c955d8d63c14a7c7772b41" integrity sha1-KBojkGOcwVkxyVXY1jwUp8d3K0E= -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" + minimist "~0.0.1" + wordwrap "~0.0.2" -optionator@^0.8.3: +optionator@^0.8.1, optionator@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== @@ -7167,9 +6975,9 @@ p-is-promise@^3.0.0: integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== p-limit@^2.0.0, p-limit@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" - integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== dependencies: p-try "^2.0.0" @@ -7370,9 +7178,9 @@ path-key@^2.0.0, path-key@^2.0.1: integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-key@^3.0.0, path-key@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.0.tgz#99a10d870a803bdd5ee6f0470e58dfcd2f9a54d3" - integrity sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg== + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: version "1.0.6" @@ -7485,16 +7293,11 @@ pgpass@1.x: dependencies: split "^1.0.0" -picomatch@^2.0.4: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: version "2.2.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== -picomatch@^2.0.5: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== - pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7546,17 +7349,6 @@ plugin-error@1.0.1, plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -plugin-error@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" - integrity sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= - dependencies: - ansi-cyan "^0.1.1" - ansi-red "^0.1.1" - arr-diff "^1.0.1" - arr-union "^2.0.1" - extend-shallow "^1.1.2" - pn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" @@ -7572,10 +7364,10 @@ popper.js@^1.16.0: resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== -portal-vue@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.6.tgz#a7d4790b14a79af7fd159a60ec88c30cddc6c639" - integrity sha512-lvCF85D4e8whd0nN32D8FqKwwkk7nYUI3Ku8UAEx4Z1reomu75dv5evRUTZNaj1EalxxWNXiNl0EHRq36fG8WA== +portal-vue@2.1.7, portal-vue@^2.1.6: + version "2.1.7" + resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4" + integrity sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g== portscanner@2.2.0: version "2.2.0" @@ -7879,7 +7671,7 @@ postcss-selector-parser@^3.0.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.4: +postcss-selector-parser@^5.0.0-rc.4: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== @@ -7926,28 +7718,10 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== -postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233" - integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.1: - version "7.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.23.tgz#9f9759fad661b15964f3cfc3140f66f1e05eadc1" - integrity sha512-hOlMf3ouRIFXD+j2VJecwssTwbvsPGJVMzupptg+85WA+i7MwyrydmQAgY3R+m0Bc0exunhbJmijy8u8+vufuQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.23: - version "7.0.25" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.25.tgz#dd2a2a753d50b13bed7a2009b4a18ac14d9db21e" - integrity sha512-NXXVvWq9icrm/TgQC0O6YVFi4StfJz46M1iNd/h6B26Nvh/HKI+q4YZtFN/EjcInZliEscO/WL10BXnc1E5nwg== +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.23, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587" + integrity sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -8096,12 +7870,12 @@ promise-sequential@1.1.1: integrity sha1-956JUO+G56eoW/MgRSZDWS9tL7I= promise.prototype.finally@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.1.tgz#cb279d3a5020ca6403b3d92357f8e22d50ed92aa" - integrity sha512-gnt8tThx0heJoI3Ms8a/JdkYBVhYP/wv+T7yQimR+kdOEJL21xTFbiJhMRqnSPcr54UVvMbsscDk2w+ivyaLPw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz#b8af89160c9c673cefe3b4c4435b53cfd0287067" + integrity sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA== dependencies: define-properties "^1.1.3" - es-abstract "^1.13.0" + es-abstract "^1.17.0-next.0" function-bind "^1.1.1" promise@^7.0.1: @@ -8127,9 +7901,9 @@ pseudomap@^1.0.2: integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= psl@^1.1.24, psl@^1.1.28: - version "1.4.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" - integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== public-encrypt@^4.0.0: version "4.0.3" @@ -8321,9 +8095,9 @@ qrcode@1.4.4: yargs "^13.2.4" qs@^6.4.0, qs@^6.5.2: - version "6.9.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.0.tgz#d1297e2a049c53119cb49cca366adbbacc80b409" - integrity sha512-27RP4UotQORTpmNQDX8BHPukOnBP3p1uUJY5UnDhaJB+rMt9iMsok724XL+UHU23bEFOHRMQ2ZhI99qOWUMGFA== + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== qs@~6.5.2: version "6.5.2" @@ -8415,9 +8189,9 @@ read-pkg@^1.0.0: path-type "^1.0.0" "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -8438,9 +8212,9 @@ readable-stream@1.1.x: string_decoder "~0.10.x" "readable-stream@2 || 3", readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + version "3.5.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" + integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -8467,6 +8241,13 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" +readdirp@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" + integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== + dependencies: + picomatch "^2.0.7" + recaptcha-promise@0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/recaptcha-promise/-/recaptcha-promise-0.1.3.tgz#7d3d66d045a53674054ebdfa1684e0609ef5d912" @@ -8595,11 +8376,6 @@ repeat-string@^1.5.2, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= - replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" @@ -8637,7 +8413,7 @@ request-promise-core@1.1.3: dependencies: lodash "^4.17.15" -request-promise-native@1.0.7, request-promise-native@^1.0.7: +request-promise-native@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== @@ -8646,7 +8422,7 @@ request-promise-native@1.0.7, request-promise-native@^1.0.7: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request-promise-native@1.0.8: +request-promise-native@1.0.8, request-promise-native@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== @@ -8760,9 +8536,9 @@ resolve-url@^0.2.1: integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" - integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== dependencies: path-parse "^1.0.6" @@ -8810,7 +8586,7 @@ rimraf@3.0.0: dependencies: glob "^7.1.3" -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: +rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -8857,10 +8633,10 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.4.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" - integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== +rxjs@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" + integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" @@ -8886,11 +8662,29 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sass-loader@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.1.tgz#46cc2bc90de9ed0a121d023800a32049215fb57c" + integrity sha512-ANR2JHuoxzCI+OPDA0hJBv1Y16A2021hucu0S3DOGgpukKzq9W+4vX9jhIqs4qibT5E7RIRsHMMrN0kdF5nUig== + dependencies: + clone-deep "^4.0.1" + loader-utils "^1.2.3" + neo-async "^2.6.1" + schema-utils "^2.6.1" + semver "^7.1.1" + +sass@1.25.0, sass@^1.10.3: + version "1.25.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.25.0.tgz#f8bd7dfbb39d6b0305e27704a8ebe637820693f3" + integrity sha512-uQMjye0Y70SEDGO56n0j91tauqS9E1BmpKHtiYNQScXDHeaE9uHwNEqQNFf4Bes/3DHMNinB6u79JsG10XWNyw== + dependencies: + chokidar ">=2.0.0 <4.0.0" + sax@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" @@ -8917,26 +8711,10 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.4.1.tgz#e89ade5d056dc8bcaca377574bb4a9c4e1b8be56" - integrity sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w== - dependencies: - ajv "^6.10.2" - ajv-keywords "^3.4.1" - -schema-utils@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f" - integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ== - dependencies: - ajv "^6.10.2" - ajv-keywords "^3.4.1" - -schema-utils@^2.6.0, schema-utils@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.1.tgz#eb78f0b945c7bcfa2082b3565e8db3548011dc4f" - integrity sha512-0WXHDs1VDJyo+Zqs9TKLKyD/h7yDpHUhEFsM2CzkICFdoX1av+GBq/J2xRTFfsQO5kBfhZzANf2VcIm84jqDbg== +schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.1: + version "2.6.4" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53" + integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ== dependencies: ajv "^6.10.2" ajv-keywords "^3.4.1" @@ -8983,6 +8761,11 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" + integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A== + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" @@ -9026,6 +8809,13 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + sharp@0.23.4: version "0.23.4" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.23.4.tgz#ca36067cb6ff7067fa6c77b01651cb9a890f8eb3" @@ -9162,9 +8952,9 @@ sort-keys@^2.0.0: is-plain-obj "^1.0.0" sortablejs@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.1.tgz#3d52b00f871be00f00f84d99a60d120bf3dfe52c" - integrity sha512-N6r7GrVmO8RW1rn0cTdvK3JR0BcqecAJ0PmYMCL3ZuqTH3pY+9QyqkmJSkkLyyDvd+AJnwaxTP22Ybr/83V9hQ== + version "1.10.2" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290" + integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A== source-list-map@^2.0.0: version "2.0.1" @@ -9172,20 +8962,20 @@ source-list-map@^2.0.0: integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" - integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== dependencies: - atob "^2.1.1" + atob "^2.1.2" decode-uri-component "^0.2.0" resolve-url "^0.2.1" source-map-url "^0.4.0" urix "^0.1.0" source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -9295,12 +9085,12 @@ ssri@^6.0.1: figgy-pudding "^3.5.1" ssri@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.0.1.tgz#b0cab7bbb11ac9ea07f003453e2011f8cbed9f34" - integrity sha512-FfndBvkXL9AHyGLNzU3r9AvYIBBZ7gm+m+kd0p8cT3/v4OliMAyipZAhLVEv1Zi/k4QFq9CstRGVd9pW/zcHFQ== + version "7.1.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.1.0.tgz#92c241bf6de82365b5c7fb4bd76e975522e1294d" + integrity sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g== dependencies: figgy-pudding "^3.5.1" - minipass "^3.0.0" + minipass "^3.1.1" stable@^0.1.8: version "0.1.8" @@ -9375,9 +9165,9 @@ stream-parser@~0.3.1: debug "2" stream-shift@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" - integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== streamsearch@0.1.2: version "0.1.2" @@ -9419,18 +9209,18 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimleft@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" - integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== dependencies: define-properties "^1.1.3" function-bind "^1.1.1" -string.prototype.trimright@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" - integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== dependencies: define-properties "^1.1.3" function-bind "^1.1.1" @@ -9522,11 +9312,11 @@ strip-json-comments@^3.0.1: integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== strtok3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.1.tgz#f89e09fa024dff82e39c160faf8d8706c24e7d21" - integrity sha512-AWliiIjyb87onqO8pM+1Hozm+PPcR4YYIWbFUT5OKQ+tOMwgdT8HwJd/IS8v3/gKdAtE5aE2p3FhcWqryuZPLQ== + version "5.0.2" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.2.tgz#bb81f1f56742e16f1a30ccce5dc3d9498aa5475a" + integrity sha512-EFeVpFC5qDsqPEJSrIYyS/ueFBknGhgSK9cW+YAJF/cgJG/KSjoK7X6rK5xnpcLe7y1LVkVFCXWbAb+ClNKzKQ== dependencies: - "@tokenizer/token" "^0.1.0" + "@tokenizer/token" "^0.1.1" debug "^4.1.1" peek-readable "^3.1.0" @@ -9547,29 +9337,6 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -stylus-loader@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-3.0.2.tgz#27a706420b05a38e038e7cacb153578d450513c6" - integrity sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA== - dependencies: - loader-utils "^1.0.2" - lodash.clonedeep "^4.5.0" - when "~3.6.x" - -stylus@0.54.7, stylus@^0.54.0: - version "0.54.7" - resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.7.tgz#c6ce4793965ee538bcebe50f31537bfc04d88cd2" - integrity sha512-Yw3WMTzVwevT6ZTrLCYNHAFmanMxdylelL3hkWNgPMeTCpMwpV3nXjpOHuBXtFv7aiO2xRuQS6OoAdgkNcSNug== - dependencies: - css-parse "~2.0.0" - debug "~3.1.0" - glob "^7.1.3" - mkdirp "~0.5.x" - safer-buffer "^2.1.2" - sax "~1.2.4" - semver "^6.0.0" - source-map "^0.7.3" - summaly@2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.3.1.tgz#028ef4d206a5da06ae5c723fc60719d38368d6f2" @@ -9706,9 +9473,9 @@ syslog-pro@1.0.0: moment "^2.22.2" systeminformation@*: - version "4.15.3" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.15.3.tgz#639cdc224b5c3811f1a0bfba33f869bbc6fb930f" - integrity sha512-Fx2ARGHtLl2/xLeNoTR8/doXSxUXuAzIN+dyCK9O43j/UETLBt77yTEbTxmYsVD47PYjX1iQTdcY41CZckY+zg== + version "4.19.1" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.19.1.tgz#2f43204152feaf55d8692b61b855f16287817115" + integrity sha512-6Jb+0aepgRb2B4vFgxKKcrr8QexhERkPX1z0EzIyCKDMQjILt+ZbNtFxrGjFwa67tn9Wbqs1L5t/CjCUwB1FcA== systeminformation@4.17.3: version "4.17.3" @@ -9756,19 +9523,6 @@ tar-stream@^2.0.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^4: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" - tar@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/tar/-/tar-5.0.5.tgz#03fcdb7105bc8ea3ce6c86642b9c942495b04f93" @@ -9810,28 +9564,10 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@^4.0.0: - version "4.4.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.2.tgz#448fffad0245f4c8a277ce89788b458bfd7706e8" - integrity sha512-Uufrsvhj9O1ikwgITGsZ5EZS6qPokUOkCegS7fYOdGTv+OA90vndUbU6PEjr5ePqHfNUbGyMO7xyIZv2MhsALQ== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^4.1.2: - version "4.3.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.8.tgz#707f05f3f4c1c70c840e626addfdb1c158a17136" - integrity sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^4.4.3: - version "4.4.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.3.tgz#401abc52b88869cf904412503b1eb7da093ae2f0" - integrity sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA== +terser@^4.0.0, terser@^4.1.2, terser@^4.4.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.3.tgz#e33aa42461ced5238d352d2df2a67f21921f8d87" + integrity sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -9848,9 +9584,9 @@ textarea-caret@3.1.0: integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q== textextensions@2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.5.0.tgz#e21d3831dafa37513dd80666dff541414e314293" - integrity sha512-1IkVr355eHcomgK7fgj1Xsokturx6L5S2JRT5WcRdA6v5shk9sxWuO/w/VbpQexwkXJMQIa/j1dBi3oo7+HhcA== + version "2.6.0" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" + integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ== thenify-all@^1.0.0: version "1.6.0" @@ -10161,11 +9897,6 @@ type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" - integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== - type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -10189,6 +9920,11 @@ type@^1.0.1: resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== +type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3" + integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -10226,7 +9962,7 @@ typescript@3.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== -uglify-js@^2.6.1, uglify-js@^2.7.0: +uglify-js@^2.6.1: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= @@ -10236,12 +9972,12 @@ uglify-js@^2.6.1, uglify-js@^2.7.0: optionalDependencies: uglify-to-browserify "~1.0.0" -uglify-js@^3.5.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.1.tgz#ae7688c50e1bdcf2f70a0e162410003cf9798311" - integrity sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ== +uglify-js@^3.1.4, uglify-js@^3.5.1: + version "3.7.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.5.tgz#278c7c24927ac5a32d3336fc68fd4ae1177a486a" + integrity sha512-GFZ3EXRptKGvb/C1Sq6nO1iI7AGcjyqmIyOw0DrD0675e+NNbGO72xmMM2iEBdFbxaTLo70NbjM/Wy54uZIlsg== dependencies: - commander "2.20.0" + commander "~2.20.3" source-map "~0.6.1" uglify-to-browserify@~1.0.0: @@ -10412,12 +10148,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@^1.0.0, util.promisify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" util@0.10.3: version "0.10.3" @@ -10438,11 +10176,16 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@3.3.3, uuid@^3.3.2, uuid@^3.3.3: +uuid@3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== +uuid@^3.3.2, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + v-animate-css@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/v-animate-css/-/v-animate-css-0.0.3.tgz#fa3f41e5995404f0e5a741cdb2c576ce01f9df16" @@ -10565,9 +10308,9 @@ vinyl@^2.0.0, vinyl@^2.1.0: replace-ext "^1.0.0" vm-browserify@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" - integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== void-elements@^2.0.1: version "2.0.1" @@ -10625,11 +10368,6 @@ vue-i18n@8.15.3: resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.15.3.tgz#9f947802d9b734fcb92e2ce724da654f2f9fc0f4" integrity sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew== -vue-js-modal@1.3.31: - version "1.3.31" - resolved "https://registry.yarnpkg.com/vue-js-modal/-/vue-js-modal-1.3.31.tgz#fdece823d4f2816c8b1075c1fd8f667df11f5a42" - integrity sha512-gwt2904sWbMUuUcHwKQ510IEs4G7S3bqVWLYeTOc2eEyWMmmnT9UmojDsXIexFnPVM7cZTua37z3Jm/h0i0y8Q== - vue-json-pretty@1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/vue-json-pretty/-/vue-json-pretty-1.6.3.tgz#c7f378f3c9f68977047de28197735bc2cf81b15b" @@ -10653,6 +10391,13 @@ vue-marquee-text-component@1.1.1: dependencies: vue "^2.5.17" +vue-meta@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/vue-meta/-/vue-meta-2.3.1.tgz#32a1c2634f49433f30e7e7a028aa5e5743f84f6a" + integrity sha512-hnZvDNvLh+PefJLfYkZhG6cSBNKikgQyiEK8lI/P2qscM1DC/qHHOfdACPQ/VDnlaWU9VlcobCTNyVtssTR4XQ== + dependencies: + deepmerge "^4.0.0" + vue-prism-component@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vue-prism-component/-/vue-prism-component-1.1.1.tgz#df0e375f7f9b367b069b2d54e6ed86facde96030" @@ -10700,16 +10445,11 @@ vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== -vue@2.6.11: +vue@2.6.11, vue@^2.5.13, vue@^2.5.17: version "2.6.11" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== -vue@^2.5.13, vue@^2.5.16, vue@^2.5.17: - version "2.6.10" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637" - integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ== - vuedraggable@2.23.2: version "2.23.2" resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.23.2.tgz#0d95d7fdf4f02f56755a26b3c9dca5c7ca9cfa72" @@ -10717,13 +10457,6 @@ vuedraggable@2.23.2: dependencies: sortablejs "^1.10.1" -vuewordcloud@18.7.11: - version "18.7.11" - resolved "https://registry.yarnpkg.com/vuewordcloud/-/vuewordcloud-18.7.11.tgz#285ae2b6d93ac6f1da6e60141227f68a64b30f24" - integrity sha1-KFrittk6xvHabmAUEif2imSzDyQ= - dependencies: - vue "^2.5.16" - vuex-persistedstate@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-2.7.0.tgz#f60aae4e1163bf293696a625526dbffaa42e429e" @@ -10857,24 +10590,14 @@ whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" - integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" webidl-conversions "^4.0.2" -when@^3.7.7: - version "3.7.8" - resolved "https://registry.yarnpkg.com/when/-/when-3.7.8.tgz#c7130b6a7ea04693e842cdc9e7a1f2aa39a39f82" - integrity sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I= - -when@~3.6.x: - version "3.6.4" - resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e" - integrity sha1-RztRfsFZ4rhQBUl6E5g/CVQS404= - which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" @@ -10898,9 +10621,9 @@ which@1.3.1, which@^1.1.1, which@^1.2.14, which@^1.2.9, which@^1.3.1: isexe "^2.0.0" which@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.1.tgz#f1cf94d07a8e571b6ff006aeb91d0300c47ef0a4" - integrity sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w== + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" @@ -10934,10 +10657,10 @@ wordwrap@0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= worker-farm@^1.7.0: version "1.7.0" @@ -11005,18 +10728,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" -ws@7.2.1: +ws@7.2.1, ws@^7.0.0: version "7.2.1" resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A== -ws@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73" - integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg== - dependencies: - async-limiter "^1.0.0" - xev@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xev/-/xev-2.0.1.tgz#24484173a22115bc8a990ef5d4d5129695b827a7" @@ -11043,12 +10759,11 @@ xml2js@0.4.19: xmlbuilder "~9.0.1" xml2js@^0.4.17: - version "0.4.22" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02" - integrity sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw== + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== dependencies: sax ">=0.6.0" - util.promisify "~1.0.0" xmlbuilder "~11.0.0" xmlbuilder@~11.0.0: @@ -11091,7 +10806,7 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: +yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== @@ -11167,7 +10882,7 @@ yargs@13.2.4: y18n "^4.0.0" yargs-parser "^13.1.0" -yargs@13.3.0, yargs@^13.0.0, yargs@^13.2.1, yargs@^13.2.4, yargs@^13.3.0: +yargs@13.3.0, yargs@^13.2.1, yargs@^13.2.4, yargs@^13.3.0: version "13.3.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== @@ -11201,9 +10916,9 @@ yargs@^14.2: yargs-parser "^15.0.0" yargs@^15.0.0: - version "15.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3" - integrity sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q== + version "15.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219" + integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg== dependencies: cliui "^6.0.0" decamelize "^1.2.0"