Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
This commit is contained in:
syuilo 2020-01-30 04:37:25 +09:00 committed by GitHub
parent a5955c1123
commit f6154dc0af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
943 changed files with 33515 additions and 79325 deletions

View File

@ -111,10 +111,6 @@ id: 'aid'
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Other configuration └───────────────────────────────────── #───┘ Other configuration └─────────────────────────────────────
# If enabled:
# The first account created is automatically marked as Admin.
autoAdmin: true
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #disableHsts: true

View File

@ -1,6 +1,56 @@
ChangeLog 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) 11.37.1 (2020/01/07)
-------------------- --------------------
### 🐛Fixes ### 🐛Fixes

View File

@ -2,38 +2,25 @@
* Gulp tasks * Gulp tasks
*/ */
import * as fs from 'fs';
import * as gulp from 'gulp'; import * as gulp from 'gulp';
import * as ts from 'gulp-typescript'; 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 rimraf from 'rimraf';
import * as chalk from 'chalk';
import * as rename from 'gulp-rename'; 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 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 locales = require('./locales');
const meta = require('./package.json');
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.'));
}
gulp.task('build:ts', () => { gulp.task('build:ts', () => {
const tsProject = ts.createProject('./tsconfig.json'); const tsProject = ts.createProject('./tsconfig.json');
return tsProject return tsProject
.src() .src()
.pipe(sourcemaps.init())
.pipe(tsProject()) .pipe(tsProject())
.on('error', () => {}) .on('error', () => {})
.pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' }))
.pipe(gulp.dest('./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.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
); );
gulp.task('build:copy:fonts', () => gulp.task('build:copy:locales', cb => {
gulp.src('./node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/client/assets/fonts/')) 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([ gulp.src([
'./src/const.json',
'./src/emojilist.json', './src/emojilist.json',
'./src/server/web/views/**/*', './src/server/web/views/**/*',
'./src/**/assets/**/*', './src/**/assets/**/*',
'!./src/client/app/**/assets/**/*' '!./src/client/assets/**/*'
]).pipe(gulp.dest('./built/')) ]).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 => gulp.task('clean', cb =>
rimraf('./built', cb) rimraf('./built', cb)
); );
@ -90,20 +55,9 @@ gulp.task('cleanall', gulp.parallel('clean', cb =>
rimraf('./node_modules', 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.task('build:client:styles', () =>
gulp.src('./src/client/app/init.css') gulp.src('./src/client/style.scss')
.pipe(sass({ fiber }))
.pipe(cleanCSS()) .pipe(cleanCSS())
.pipe(gulp.dest('./built/client/assets/')) .pipe(gulp.dest('./built/client/assets/'))
); );
@ -112,7 +66,6 @@ gulp.task('copy:client', () =>
gulp.src([ gulp.src([
'./assets/**/*', './assets/**/*',
'./src/client/assets/**/*', './src/client/assets/**/*',
'./src/client/app/*/assets/**/*'
]) ])
.pipe(rename(path => { .pipe(rename(path => {
path.dirname = path.dirname!.replace('assets', '.'); path.dirname = path.dirname!.replace('assets', '.');
@ -120,15 +73,7 @@ gulp.task('copy:client', () =>
.pipe(gulp.dest('./built/client/assets/')) .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( gulp.task('build:client', gulp.parallel(
'build:client:script',
'build:client:styles', 'build:client:styles',
'copy:client' 'copy:client'
)); ));
@ -137,7 +82,6 @@ gulp.task('build', gulp.parallel(
'build:ts', 'build:ts',
'build:copy', 'build:copy',
'build:client', 'build:client',
'doc'
)); ));
gulp.task('default', gulp.task('build')); gulp.task('default', gulp.task('build'));

View File

@ -1 +0,0 @@
---

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,19 +14,19 @@ const merge = (...args) => args.reduce((a, c) => ({
}), {}); }), {});
const languages = [ const languages = [
'cs-CZ', /*'cs-CZ',
'da-DK', 'da-DK',
'de-DE', 'de-DE',
'en-US', 'en-US',
'es-ES', 'es-ES',
'fr-FR', 'fr-FR',*/
'ja-JP', 'ja-JP',
'ja-KS', /*'ja-KS',
'ko-KR', 'ko-KR',
'nl-NL', 'nl-NL',
'pl-PL', 'pl-PL',
'zh-CN', 'zh-CN',
'zh-TW', 'zh-TW',*/
]; ];
const primaries = { const primaries = {

View File

@ -1,6 +0,0 @@
---
meta:
lang: "Italiano"
common:
misskey: "A ⭐ of the fediverse"
about-title: "A ⭐ of the fediverse."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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"

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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: "Мусорное ведро"

File diff suppressed because it is too large Load Diff

View File

@ -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: "月"

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "11.37.1", "version": "12.0.0-alpha.10",
"codename": "daybreak", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/syuilo/misskey.git" "url": "https://github.com/syuilo/misskey.git"
@ -112,6 +112,7 @@
"cbor": "5.0.1", "cbor": "5.0.1",
"chai": "4.2.0", "chai": "4.2.0",
"chalk": "3.0.0", "chalk": "3.0.0",
"chart.js": "2.9.3",
"cli-highlight": "2.1.4", "cli-highlight": "2.1.4",
"commander": "4.1.0", "commander": "4.1.0",
"content-disposition": "0.5.3", "content-disposition": "0.5.3",
@ -125,15 +126,16 @@
"eslint-plugin-vue": "6.1.2", "eslint-plugin-vue": "6.1.2",
"eventemitter3": "4.0.0", "eventemitter3": "4.0.0",
"feed": "4.1.0", "feed": "4.1.0",
"fibers": "4.0.2",
"file-type": "13.0.1", "file-type": "13.0.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-clean-css": "4.2.0", "gulp-clean-css": "4.2.0",
"gulp-dart-sass": "0.9.1",
"gulp-mocha": "7.0.2", "gulp-mocha": "7.0.2",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.0.0", "gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.5", "gulp-sourcemaps": "2.6.5",
"gulp-stylus": "2.7.0",
"gulp-terser": "1.2.0", "gulp-terser": "1.2.0",
"gulp-tslint": "8.1.4", "gulp-tslint": "8.1.4",
"gulp-typescript": "5.0.1", "gulp-typescript": "5.0.1",
@ -177,6 +179,7 @@
"parse5": "5.1.1", "parse5": "5.1.1",
"parsimmon": "1.13.0", "parsimmon": "1.13.0",
"pg": "7.17.0", "pg": "7.17.0",
"portal-vue": "2.1.7",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"prismjs": "1.18.0", "prismjs": "1.18.0",
@ -204,6 +207,8 @@
"rimraf": "3.0.0", "rimraf": "3.0.0",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sass": "1.25.0",
"sass-loader": "8.0.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"sharp": "0.23.4", "sharp": "0.23.4",
"showdown": "1.9.1", "showdown": "1.9.1",
@ -211,8 +216,6 @@
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"stringz": "2.0.0", "stringz": "2.0.0",
"style-loader": "1.1.2", "style-loader": "1.1.2",
"stylus": "0.54.7",
"stylus-loader": "3.0.2",
"summaly": "2.3.1", "summaly": "2.3.1",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "4.17.3", "systeminformation": "4.17.3",
@ -238,10 +241,10 @@
"vue-content-loading": "1.6.0", "vue-content-loading": "1.6.0",
"vue-cropperjs": "4.0.1", "vue-cropperjs": "4.0.1",
"vue-i18n": "8.15.3", "vue-i18n": "8.15.3",
"vue-js-modal": "1.3.31",
"vue-json-pretty": "1.6.3", "vue-json-pretty": "1.6.3",
"vue-loader": "15.8.3", "vue-loader": "15.8.3",
"vue-marquee-text-component": "1.1.1", "vue-marquee-text-component": "1.1.1",
"vue-meta": "2.3.1",
"vue-prism-component": "1.1.1", "vue-prism-component": "1.1.1",
"vue-router": "3.1.3", "vue-router": "3.1.3",
"vue-sequential-entrance": "1.1.3", "vue-sequential-entrance": "1.1.3",
@ -249,7 +252,6 @@
"vue-svg-inline-loader": "1.4.4", "vue-svg-inline-loader": "1.4.4",
"vue-template-compiler": "2.6.11", "vue-template-compiler": "2.6.11",
"vuedraggable": "2.23.2", "vuedraggable": "2.23.2",
"vuewordcloud": "18.7.11",
"vuex": "3.1.2", "vuex": "3.1.2",
"vuex-persistedstate": "2.7.0", "vuex-persistedstate": "2.7.0",
"web-push": "3.4.3", "web-push": "3.4.3",

View File

@ -1,3 +0,0 @@
declare module '*/const.json' {
const copyright: string;
}

View File

@ -77,7 +77,6 @@ export async function masterMain() {
if (!program.noDaemons) { if (!program.noDaemons) {
require('../daemons/server-stats').default(); require('../daemons/server-stats').default();
require('../daemons/notes-stats').default();
require('../daemons/queue-stats').default(); require('../daemons/queue-stats').default();
require('../daemons/janitor').default(); require('../daemons/janitor').default();
} }

1105
src/client/app.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,150 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 135.46667 135.46667"
version="1.1"
id="svg8"
inkscape:version="0.92.1 r15371"
sodipodi:docname="header-icon.dark.svg"
inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\32.png"
inkscape:export-xdpi="6"
inkscape:export-ydpi="6">
<defs
id="defs2">
<inkscape:path-effect
effect="simplify"
id="path-effect5115"
is_visible="true"
steps="1"
threshold="0.000408163"
smooth_angles="360"
helper_size="0"
simplify_individual_paths="false"
simplify_just_coalesce="false"
simplifyindividualpaths="false"
simplifyJustCoalesce="false" />
<inkscape:path-effect
effect="simplify"
id="path-effect5111"
is_visible="true"
steps="1"
threshold="0.000408163"
smooth_angles="360"
helper_size="0"
simplify_individual_paths="false"
simplify_just_coalesce="false"
simplifyindividualpaths="false"
simplifyJustCoalesce="false" />
<inkscape:path-effect
effect="simplify"
id="path-effect5104"
is_visible="true"
steps="1"
threshold="0.000408163"
smooth_angles="360"
helper_size="0"
simplify_individual_paths="false"
simplify_just_coalesce="false"
simplifyindividualpaths="false"
simplifyJustCoalesce="false" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4142136"
inkscape:cx="114.309"
inkscape:cy="251.50613"
inkscape:document-units="px"
inkscape:current-layer="g4502"
showgrid="true"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="false"
inkscape:snap-smooth-nodes="true"
inkscape:snap-center="true"
inkscape:snap-page="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="1072"
inkscape:window-maximized="1"
inkscape:snap-object-midpoints="true"
inkscape:snap-midpoints="true"
inkscape:object-paths="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
objecttolerance="1"
guidetolerance="1"
inkscape:snap-nodes="false"
inkscape:snap-others="false">
<inkscape:grid
type="xygrid"
id="grid4504"
spacingx="4.2333334"
spacingy="4.2333334"
empcolor="#ff3fff"
empopacity="0.25098039"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="レイヤー 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-30.809093,-111.78601)">
<g
id="g4502"
transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)">
<g
style="fill-opacity:1"
transform="translate(-1.3333333e-6,-1.3439941e-6)"
id="g5125">
<g
transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)"
id="text4489"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
aria-label="Mi">
<path
sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz"
inkscape:connector-curvature="0"
id="path5210"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px"
d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" />
<path
inkscape:connector-curvature="0"
id="path5212"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px"
d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -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);
});

View File

@ -1,6 +0,0 @@
@import "../app"
@import "../reset"
html
height 100%
background var(--bg)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,527 +0,0 @@
<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>
</header>
<div ref="chart"></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import * as tinycolor from 'tinycolor2';
import ApexCharts from 'apexcharts';
const limit = 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/charts.vue'),
data() {
return {
chart: null,
src: 'notes',
span: 'hour',
chartInstance: null
};
},
computed: {
data(): any {
if (this.chart == null) return null;
switch (this.src) {
case 'federation-instances': return this.federationInstancesChart(false);
case 'federation-instances-total': return this.federationInstancesChart(true);
case 'users': return this.usersChart(false);
case 'users-total': return this.usersChart(true);
case 'active-users': return this.activeUsersChart();
case 'notes': return this.notesChart('combined');
case 'local-notes': return this.notesChart('local');
case 'remote-notes': return this.notesChart('remote');
case 'notes-total': return this.notesTotalChart();
case 'drive': return this.driveChart();
case 'drive-total': return this.driveTotalChart();
case 'drive-files': return this.driveFilesChart();
case 'drive-files-total': return this.driveFilesTotalChart();
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 :
null;
return stats;
}
},
watch: {
src() {
this.render();
},
span() {
this.render();
}
},
async mounted() {
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' })
]), 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' })
])]);
const chart = {
perHour: {
federation: perHour[0],
users: perHour[1],
activeUsers: perHour[2],
notes: perHour[3],
drive: perHour[4],
network: perHour[5]
},
perDay: {
federation: perDay[0],
users: perDay[1],
activeUsers: perDay[2],
notes: perDay[3],
drive: perDay[4],
network: perDay[5]
}
};
this.chart = chart;
this.render();
},
beforeDestroy() {
this.chartInstance.destroy();
},
methods: {
setSrc(src) {
this.src = src;
},
render() {
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)',
xaxis: {
lines: {
show: true,
}
},
},
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()
}
},
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.span == 'day' ? new Date(y, m, d - i) :
this.span == 'hour' ? new Date(y, m, d, h - i) :
null
);
},
format(arr) {
return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v }));
},
federationInstancesChart(total: boolean): any {
return {
series: [{
name: 'Instances',
data: this.format(total
? this.stats.federation.instance.total
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
)
}]
};
},
notesChart(type: string): any {
return {
series: [{
name: 'All',
type: 'line',
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))
)
}, {
name: 'Renotes',
type: 'area',
data: this.format(type == 'combined'
? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
: this.stats.notes[type].diffs.renote
)
}, {
name: 'Replies',
type: 'area',
data: this.format(type == 'combined'
? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
: this.stats.notes[type].diffs.reply
)
}, {
name: 'Normal',
type: 'area',
data: this.format(type == 'combined'
? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
: this.stats.notes[type].diffs.normal
)
}]
};
},
notesTotalChart(): any {
return {
series: [{
name: 'Combined',
type: 'line',
data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
}, {
name: 'Local',
type: 'area',
data: this.format(this.stats.notes.local.total)
}, {
name: 'Remote',
type: 'area',
data: this.format(this.stats.notes.remote.total)
}]
};
},
usersChart(total: boolean): any {
return {
series: [{
name: 'Combined',
type: 'line',
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))
)
}, {
name: 'Local',
type: 'area',
data: this.format(total
? this.stats.users.local.total
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
)
}, {
name: 'Remote',
type: 'area',
data: this.format(total
? this.stats.users.remote.total
: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
)
}]
};
},
activeUsersChart(): any {
return {
series: [{
name: 'Combined',
type: 'line',
data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
}, {
name: 'Local',
type: 'area',
data: this.format(this.stats.activeUsers.local.count)
}, {
name: 'Remote',
type: 'area',
data: this.format(this.stats.activeUsers.remote.count)
}]
};
},
driveChart(): any {
return {
bytes: true,
series: [{
name: 'All',
type: 'line',
data: this.format(
sum(
this.stats.drive.local.incSize,
negate(this.stats.drive.local.decSize),
this.stats.drive.remote.incSize,
negate(this.stats.drive.remote.decSize)
)
)
}, {
name: 'Local +',
type: 'area',
data: this.format(this.stats.drive.local.incSize)
}, {
name: 'Local -',
type: 'area',
data: this.format(negate(this.stats.drive.local.decSize))
}, {
name: 'Remote +',
type: 'area',
data: this.format(this.stats.drive.remote.incSize)
}, {
name: 'Remote -',
type: 'area',
data: this.format(negate(this.stats.drive.remote.decSize))
}]
};
},
driveTotalChart(): any {
return {
bytes: true,
series: [{
name: 'Combined',
type: 'line',
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
}, {
name: 'Local',
type: 'area',
data: this.format(this.stats.drive.local.totalSize)
}, {
name: 'Remote',
type: 'area',
data: this.format(this.stats.drive.remote.totalSize)
}]
};
},
driveFilesChart(): any {
return {
series: [{
name: 'All',
type: 'line',
data: this.format(
sum(
this.stats.drive.local.incCount,
negate(this.stats.drive.local.decCount),
this.stats.drive.remote.incCount,
negate(this.stats.drive.remote.decCount)
)
)
}, {
name: 'Local +',
type: 'area',
data: this.format(this.stats.drive.local.incCount)
}, {
name: 'Local -',
type: 'area',
data: this.format(negate(this.stats.drive.local.decCount))
}, {
name: 'Remote +',
type: 'area',
data: this.format(this.stats.drive.remote.incCount)
}, {
name: 'Remote -',
type: 'area',
data: this.format(negate(this.stats.drive.remote.decCount))
}]
};
},
driveFilesTotalChart(): any {
return {
series: [{
name: 'Combined',
type: 'line',
data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
}, {
name: 'Local',
type: 'area',
data: this.format(this.stats.drive.local.totalCount)
}, {
name: 'Remote',
type: 'area',
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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; }
}

View File

@ -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

View File

@ -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>

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 512 512" width="512" height="512"><defs><clipPath id="_clipPath_P6eAE2OaBltOJ3gHGVajfqsOnfv4xIns"><rect width="512" height="512"/></clipPath></defs><g clip-path="url(#_clipPath_P6eAE2OaBltOJ3gHGVajfqsOnfv4xIns)"><clipPath id="_clipPath_P6q7MZAUp3XpQhVgs2GuAbegX9v4gkom"><rect x="0" y="0" width="512" height="512" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_P6q7MZAUp3XpQhVgs2GuAbegX9v4gkom)"><g id="Group"><g id="g4502"><g id="g5125"><g id="text4489"><path d=" M 190.093 359.243 C 167.923 359.32 148.881 345.963 139.9 330.409 C 135.104 323.615 125.617 321.198 125.482 330.409 L 125.482 372.939 C 125.482 390.026 119.253 404.799 106.794 417.258 C 94.69 429.362 79.917 435.413 62.474 435.413 C 45.387 435.413 30.614 429.362 18.155 417.258 C 6.052 404.799 0 390.026 0 372.939 L 0 139.061 C 0 125.89 3.738 113.965 11.213 103.285 C 19.045 92.25 29.012 84.596 41.116 80.325 C 47.879 77.833 54.999 76.587 62.474 76.587 C 81.697 76.587 97.716 84.062 110.531 99.013 C 117.295 106.489 121.211 110.405 122.279 110.761 C 122.279 110.761 173.043 172.145 174.467 173.213 C 175.891 174.281 180.073 182.446 190.093 182.446 C 200.112 182.446 204.829 174.281 206.253 173.213 C 207.676 172.145 258.44 110.761 258.44 110.761 C 258.796 111.117 262.534 107.201 269.654 99.013 C 282.825 84.062 299.022 76.587 318.245 76.587 C 325.364 76.587 332.484 77.833 339.603 80.325 C 351.707 84.596 361.496 92.25 368.972 103.285 C 376.803 113.965 380.719 125.89 380.719 139.061 L 380.719 372.939 C 380.719 390.026 374.489 404.799 362.03 417.258 C 349.927 429.362 335.154 435.413 317.711 435.413 C 300.624 435.413 285.851 429.362 273.391 417.258 C 261.288 404.799 255.237 390.026 255.237 372.939 L 255.237 330.409 C 254.184 318.802 243.925 326.116 240.285 330.409 C 230.674 348.208 212.262 359.167 190.093 359.243 Z M 457.535 184.448 Q 435.109 184.448 419.09 168.963 Q 403.605 152.944 403.605 130.518 Q 403.605 108.091 419.09 92.606 Q 435.109 76.587 457.535 76.587 Q 479.962 76.587 495.981 92.606 Q 512 108.091 512 130.518 Q 512 152.944 495.981 168.963 Q 479.962 184.448 457.535 184.448 Z M 458.069 195.128 Q 480.496 195.128 495.981 211.147 Q 512 227.166 512 249.592 L 512 381.482 Q 512 403.909 495.981 419.928 Q 480.496 435.413 458.069 435.413 Q 435.643 435.413 419.624 419.928 Q 403.605 403.909 403.605 381.482 L 403.605 249.592 Q 403.605 227.166 419.624 211.147 Q 435.643 195.128 458.069 195.128 Z " fill-rule="evenodd" fill="rgb(157,157,157)"/></g></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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);
});

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
})();

View File

@ -1,70 +0,0 @@
import Vue from 'vue';
export default function <T extends object>(data: {
name: string;
props?: () => T;
}) {
return Vue.extend({
props: {
widget: {
type: Object
},
column: {
type: Object,
default: null
},
platform: {
type: String,
required: true
},
isCustomizeMode: {
type: Boolean,
default: false
}
},
computed: {
id(): string {
return this.widget.id;
},
props(): T {
return this.widget.data;
}
},
data() {
return {
bakedOldProps: null
};
},
created() {
this.mergeProps();
this.$watch('props', () => {
this.mergeProps();
});
},
methods: {
mergeProps() {
if (data.props) {
const defaultProps = data.props();
for (const prop of Object.keys(defaultProps)) {
if (this.props.hasOwnProperty(prop)) continue;
Vue.set(this.props, prop, defaultProps[prop]);
}
}
},
save() {
if (this.platform == 'deck') {
this.$store.commit('updateDeckColumn', this.column);
} else {
this.$store.commit('updateWidget', this.widget);
}
}
}
});
}

View File

@ -1,106 +0,0 @@
import keyCode from './keycode';
import { concat } from '../../../prelude/array';
type pattern = {
which: string[];
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
};
type action = {
patterns: pattern[];
callback: Function;
};
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
const result = {
patterns: [],
callback: callback
} as action;
result.patterns = patterns.split('|').map(part => {
const pattern = {
which: [],
ctrl: false,
alt: false,
shift: false
} as pattern;
const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
for (const key of keys) {
switch (key) {
case 'ctrl': pattern.ctrl = true; break;
case 'alt': pattern.alt = true; break;
case 'shift': pattern.shift = true; break;
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
}
}
return pattern;
});
return result;
});
const ignoreElemens = ['input', 'textarea'];
function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
const key = e.code.toLowerCase();
return patterns.some(pattern => pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey &&
!e.metaKey
);
}
export default {
install(Vue) {
Vue.directive('hotkey', {
bind(el, binding) {
el._hotkey_global = binding.modifiers.global === true;
const actions = getKeyMap(binding.value);
// flatten
const reservedKeys = concat(actions.map(a => a.patterns));
el._misskey_reservedKeys = reservedKeys;
el._keyHandler = (e: KeyboardEvent) => {
const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : [];
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
for (const action of actions) {
const matched = match(e, action.patterns);
if (matched) {
if (el._hotkey_global && match(e, targetReservedKeys)) return;
e.preventDefault();
e.stopPropagation();
action.callback(e);
break;
}
}
};
if (el._hotkey_global) {
document.addEventListener('keydown', el._keyHandler);
} else {
el.addEventListener('keydown', el._keyHandler);
}
},
unbind(el) {
if (el._hotkey_global) {
document.removeEventListener('keydown', el._keyHandler);
} else {
el.removeEventListener('keydown', el._keyHandler);
}
}
});
}
};

View File

@ -1,5 +0,0 @@
export function hexifyAB(buffer) {
return Array.from(new Uint8Array(buffer))
.map(item => item.toString(16).padStart(2, 0))
.join('');
}

View File

@ -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;
}
}

View File

@ -1,74 +0,0 @@
import getNoteSummary from '../../../../misc/get-note-summary';
import getReactionEmoji from '../../../../misc/get-reaction-emoji';
import getUserName from '../../../../misc/get-user-name';
type Notification = {
title: string;
body: string;
icon: string;
onclick?: any;
};
// TODO: i18n
export default function(type, data): Notification {
switch (type) {
case 'driveFileCreated':
return {
title: 'File uploaded',
body: data.name,
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':
return {
title: `${getUserName(data.user)}:`,
body: getNoteSummary(data),
icon: data.user.avatarUrl
};
case 'reply':
return {
title: `You got reply from ${getUserName(data.user)}:`,
body: getNoteSummary(data),
icon: data.user.avatarUrl
};
case 'quote':
return {
title: `${getUserName(data.user)}:`,
body: getNoteSummary(data),
icon: data.user.avatarUrl
};
case 'reaction':
return {
title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
body: getNoteSummary(data.note),
icon: data.user.avatarUrl
};
default:
return null;
}
default:
return null;
}
}

View File

@ -1,8 +0,0 @@
export default (parent, child) => {
let node = child.parentNode;
while (node) {
if (node == parent) return true;
node = node.parentNode;
}
return false;
};

View File

@ -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;
}

View File

@ -1,31 +0,0 @@
import parseAcct from '../../../../misc/acct/parse';
import { host as localHost } from '../../config';
export async function genSearchQuery(v: any, q: string) {
let host: string;
let userId: string;
if (q.split(' ').some(x => x.startsWith('@'))) {
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
if (at.includes('.')) {
if (at === localHost || at === '.') {
host = null;
} else {
host = at;
}
} else {
const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null);
if (user) {
userId = user.id;
} else {
// todo: show error
}
}
}
}
return {
query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
host: host,
userId: userId
};
}

View File

@ -1,11 +0,0 @@
const faces = [
'(=^・・^=)',
'v(\'ω\')v',
'🐡( \'-\' 🐡 )フグパンチ!!!!',
'✌️(´・_・`)✌️',
'(。><。)',
'(Δ・x・Δ)',
'(コ`・ヘ・´ケ)'
];
export default () => faces[Math.floor(Math.random() * faces.length)];

View File

@ -1,11 +0,0 @@
import { url as instanceUrl } from '../../config';
import * as url from '../../../../prelude/url';
export function getStaticImageUrl(baseUrl: string): string {
const u = new URL(baseUrl);
const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので
return `${instanceUrl}/proxy/${dummy}?${url.query({
url: u.href,
static: '1'
})}`;
}

View File

@ -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);
}
}
});

View File

@ -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;
}
}
},
}
});

View File

@ -1,189 +0,0 @@
import Vue from 'vue';
export default (opts) => ({
data() {
return {
items: [],
queue: [],
offset: 0,
fetching: true,
moreFetching: false,
inited: false,
more: false
};
},
computed: {
empty(): boolean {
return this.items.length == 0 && !this.fetching && this.inited;
},
error(): boolean {
return !this.fetching && !this.inited;
}
},
watch: {
queue(x) {
if (opts.onQueueChanged) opts.onQueueChanged(this, x);
},
pagination() {
this.init();
}
},
created() {
opts.displayLimit = opts.displayLimit || 30;
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: {
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);
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,
...params
}).then(x => {
if (x.length == (this.pagination.limit || 10) + 1) {
x.pop();
this.items = x;
this.more = true;
} else {
this.items = x;
this.more = false;
}
this.offset = x.length;
this.inited = true;
this.fetching = false;
if (opts.onInited) opts.onInited(this);
}, e => {
this.fetching = false;
if (opts.onInited) opts.onInited(this);
});
},
async fetchMore() {
if (!this.more || this.moreFetching || this.items.length === 0) return;
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, {
limit: (this.pagination.limit || 10) + 1,
...(this.pagination.endpoint === 'notes/search' ? {
offset: this.offset,
} : {
untilId: this.items[this.items.length - 1].id,
}),
...params
}).then(x => {
if (x.length == (this.pagination.limit || 10) + 1) {
x.pop();
this.items = this.items.concat(x);
this.more = true;
} else {
this.items = this.items.concat(x);
this.more = false;
}
this.offset += x.length;
this.moreFetching = false;
}, e => {
this.moreFetching = false;
});
},
prepend(item, silent = false) {
if (opts.onPrepend) {
const cancel = opts.onPrepend(this, item, silent);
if (cancel) return;
}
if (this.isScrollTop == null || this.isScrollTop()) {
// Prepend the item
this.items.unshift(item);
// オーバーフローしたら古い投稿は捨てる
if (this.items.length >= opts.displayLimit) {
this.items = this.items.slice(0, opts.displayLimit);
this.more = true;
}
} else {
this.queue.push(item);
}
},
append(item) {
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();
},
onBottom() {
this.fetchMore();
}
}
});

View File

@ -1,537 +0,0 @@
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(),
components: {
XPostFormAttaches: () => import('../views/components/post-form-attaches.vue').then(m => m.default),
XPollEditor: () => import('../views/components/poll-editor.vue').then(m => m.default)
},
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
}
},
data() {
return {
posting: false,
text: '',
files: [],
uploadings: [],
poll: false,
pollChoices: [],
pollMultiple: false,
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
};
},
computed: {
draftId(): string {
return this.renote
? `renote:${this.renote.id}`
: this.reply
? `reply:${this.reply.id}`
: 'note';
},
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')
];
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.reply
? this.$t('@.post-form.reply-placeholder')
: x;
},
submitText(): string {
return this.renote
? this.$t('@.post-form.renote')
: this.reply
? this.$t('@.post-form.reply')
: this.$t('@.post-form.submit');
},
canPost(): boolean {
return !this.posting &&
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
(length(this.text.trim()) <= this.maxNoteTextLength) &&
(!this.poll || this.pollChoices.length >= 2);
}
},
created() {
this.$root.getMeta().then(meta => {
this.maxNoteTextLength = meta.maxNoteTextLength;
});
},
mounted() {
if (this.initialText) {
this.text = this.initialText;
}
if (this.mention) {
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
this.text += ' ';
}
if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
}
if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text);
for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
// 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) continue;
if (this.$store.state.i.username == x.username && x.host == host) continue;
// 重複は除外
if (this.text.indexOf(`${mention} `) != -1) continue;
this.text += `${mention} `;
}
}
// デフォルト公開範囲
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;
if (this.reply.visibility === 'specified') {
this.$root.api('users/show', {
userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId)
}).then(users => {
this.visibleUsers.push(...users);
});
if (this.reply.userId !== this.$store.state.i.id) {
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
this.visibleUsers.push(user);
});
}
}
}
// keep cw when reply
if (this.$store.state.settings.keepCw && this.reply && this.reply.cw) {
this.useCw = true;
this.cw = this.reply.cw;
}
this.focus();
this.$nextTick(() => {
this.focus();
});
this.$nextTick(() => {
// 書きかけの投稿を復元
if (!this.instant && !this.mention) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
if (draft) {
this.text = draft.data.text;
this.files = (draft.data.files || []).filter(e => e);
if (draft.data.poll) {
this.poll = true;
this.$nextTick(() => {
(this.$refs.poll as any).set(draft.data.poll);
});
}
this.$emit('change-attached-files', this.files);
}
}
// 削除して編集
if (this.initialNote) {
const init = this.initialNote;
this.text = init.text ? init.text : '';
this.files = init.files;
this.cw = init.cw;
this.useCw = init.cw != null;
if (init.poll) {
this.poll = true;
this.$nextTick(() => {
(this.$refs.poll as any).set({
choices: init.poll.choices.map(c => c.text),
multiple: init.poll.multiple
});
});
}
// hack 位置情報投稿が動くようになったら適用する
this.geo = null;
this.visibility = init.visibility;
this.localOnly = init.localOnly;
this.quoteId = init.renote ? init.renote.id : null;
}
this.$nextTick(() => this.watch());
});
},
methods: {
watch() {
this.$watch('text', () => this.saveDraft());
this.$watch('poll', () => this.saveDraft());
this.$watch('files', () => this.saveDraft());
},
trimmedLength(text: string) {
return length(text.trim());
},
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
focus() {
(this.$refs.text as any).focus();
},
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);
},
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);
},
onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads);
},
onPollUpdate() {
const got = this.$refs.poll.get();
this.pollChoices = got.choices;
this.pollMultiple = got.multiple;
this.pollExpiration = [got.expiration, got.expiresAt || got.expiredAfter];
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
});
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;
}
},
addVisibleUser() {
this.$root.dialog({
title: this.$t('@.post-form.enter-username'),
user: true
}).then(({ canceled, result: user }) => {
if (canceled) return;
this.visibleUsers.push(user);
});
},
removeVisibleUser(user) {
this.visibleUsers = erase(user, this.visibleUsers);
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
this.quoteId = null;
this.$emit('change-attached-files', this.files);
},
onKeydown(e) {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) 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);
}
}
const paste = e.clipboardData.getData('text');
if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
e.preventDefault();
this.$root.dialog({
type: 'info',
text: this.$t('@.post-form.quote-question'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) {
insertTextAtCursor(this.$refs.text, paste);
return;
}
this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
});
}
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
if (isFile || isDriveFile) {
e.preventDefault();
this.draghover = true;
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
}
},
onDragenter(e) {
this.draghover = true;
},
onDragleave(e) {
this.draghover = false;
},
onDrop(e): void {
this.draghover = false;
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
e.preventDefault();
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);
this.$emit('change-attached-files', this.files);
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);
});
this.$once('hook:beforeDestroy', () => {
vm.close();
});
},
saveDraft() {
if (this.instant) return;
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftId] = {
updatedAt: new Date(),
data: {
text: this.text,
files: this.files,
poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined
}
};
localStorage.setItem('drafts', JSON.stringify(data));
},
deleteDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
delete data[this.draftId];
localStorage.setItem('drafts', JSON.stringify(data));
},
kao() {
this.text += 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,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
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
}).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;
});
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
}
});

View File

@ -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>;
};

View File

@ -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",
}
]

View File

@ -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();
}
}

View File

@ -1,64 +0,0 @@
import { faHistory } from '@fortawesome/free-solid-svg-icons';
export async function search(v: any, q: string) {
q = q.trim();
if (q.startsWith('@') && !q.includes(' ')) {
v.$router.push(`/${q}`);
return;
}
if (q.startsWith('#')) {
v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
return;
}
// like 2018/03/12
if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) {
const date = new Date(q.replace(/-/g, '/'));
// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
date.setHours(23, 59, 59, 999);
}
v.$root.$emit('warp', date);
v.$root.dialog({
icon: faHistory,
splash: true,
});
return;
}
if (q.startsWith('https://')) {
const dialog = v.$root.dialog({
type: 'waiting',
text: v.$t('@.fetching-as-ap-object'),
showOkButton: false,
showCancelButton: false,
cancelableByBgClick: false
});
try {
const res = await v.$root.api('ap/show', {
uri: q
});
dialog.close();
if (res.type == 'User') {
v.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
v.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {
dialog.close();
// TODO: Show error
}
return;
}
v.$router.push(`/search?q=${encodeURIComponent(q)}`);
}

View File

@ -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))
);
}

View File

@ -1,301 +0,0 @@
import autobind from 'autobind-decorator';
import { EventEmitter } from 'eventemitter3';
import ReconnectingWebsocket from 'reconnecting-websocket';
import { wsUrl } from '../../config';
import MiOS from '../../mios';
/**
* Misskey stream connection
*/
export default class Stream extends EventEmitter {
private stream: ReconnectingWebsocket;
public state: string;
private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = [];
private nonSharedConnections: NonSharedConnection[] = [];
constructor(os: MiOS) {
super();
this.state = 'initializing';
const user = os.store.state.i;
this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : ''), '', { minReconnectionDelay: 1 }); // https://github.com/pladaria/reconnecting-websocket/issues/91
this.stream.addEventListener('open', this.onOpen);
this.stream.addEventListener('close', this.onClose);
this.stream.addEventListener('message', this.onMessage);
}
@autobind
public useSharedConnection(channel: string): SharedConnection {
let pool = this.sharedConnectionPools.find(p => p.channel === channel);
if (pool == null) {
pool = new Pool(this, channel);
this.sharedConnectionPools.push(pool);
}
const connection = new SharedConnection(this, channel, pool);
this.sharedConnections.push(connection);
return connection;
}
@autobind
public removeSharedConnection(connection: SharedConnection) {
this.sharedConnections = this.sharedConnections.filter(c => c !== connection);
}
@autobind
public removeSharedConnectionPool(pool: Pool) {
this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool);
}
@autobind
public connectToChannel(channel: string, params?: any): NonSharedConnection {
const connection = new NonSharedConnection(this, channel, params);
this.nonSharedConnections.push(connection);
return connection;
}
@autobind
public disconnectToChannel(connection: NonSharedConnection) {
this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection);
}
/**
* Callback of when open connection
*/
@autobind
private onOpen() {
const isReconnect = this.state == 'reconnecting';
this.state = 'connected';
this.emit('_connected_');
// チャンネル再接続
if (isReconnect) {
for (const p of this.sharedConnectionPools)
p.connect();
for (const c of this.nonSharedConnections)
c.connect();
}
}
/**
* Callback of when close connection
*/
@autobind
private onClose() {
if (this.state == 'connected') {
this.state = 'reconnecting';
this.emit('_disconnected_');
}
}
/**
* Callback of when received a message from connection
*/
@autobind
private onMessage(message) {
const { type, body } = JSON.parse(message.data);
if (type == 'channel') {
const id = body.id;
let connections: Connection[];
connections = this.sharedConnections.filter(c => c.id === id);
if (connections.length === 0) {
connections = [this.nonSharedConnections.find(c => c.id === id)];
}
for (const c of connections.filter(c => c != null)) {
c.emit(body.type, body.body);
}
} else {
this.emit(type, body);
}
}
/**
* Send a message to connection
*/
@autobind
public send(typeOrPayload, payload?) {
const data = payload === undefined ? typeOrPayload : {
type: typeOrPayload,
body: payload
};
this.stream.send(JSON.stringify(data));
}
/**
* Close this connection
*/
@autobind
public close() {
this.stream.removeEventListener('open', this.onOpen);
this.stream.removeEventListener('message', this.onMessage);
}
}
class Pool {
public channel: string;
public id: string;
protected stream: Stream;
public users = 0;
private disposeTimerId: any;
private isConnected = false;
constructor(stream: Stream, channel: string) {
this.channel = channel;
this.stream = stream;
this.id = Math.random().toString().substr(2, 8);
this.stream.on('_disconnected_', this.onStreamDisconnected);
}
@autobind
private onStreamDisconnected() {
this.isConnected = false;
}
@autobind
public inc() {
if (this.users === 0 && !this.isConnected) {
this.connect();
}
this.users++;
// タイマー解除
if (this.disposeTimerId) {
clearTimeout(this.disposeTimerId);
this.disposeTimerId = null;
}
}
@autobind
public dec() {
this.users--;
// そのコネクションの利用者が誰もいなくなったら
if (this.users === 0) {
// また直ぐに再利用される可能性があるので、一定時間待ち、
// 新たな利用者が現れなければコネクションを切断する
this.disposeTimerId = setTimeout(() => {
this.disconnect();
}, 3000);
}
}
@autobind
public connect() {
if (this.isConnected) return;
this.isConnected = true;
this.stream.send('connect', {
channel: this.channel,
id: this.id
});
}
@autobind
private disconnect() {
this.stream.off('_disconnected_', this.onStreamDisconnected);
this.stream.send('disconnect', { id: this.id });
this.stream.removeSharedConnectionPool(this);
}
}
abstract class Connection extends EventEmitter {
public channel: string;
protected stream: Stream;
public abstract id: string;
constructor(stream: Stream, channel: string) {
super();
this.stream = stream;
this.channel = channel;
}
@autobind
public send(id: string, typeOrPayload, payload?) {
const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
const body = payload === undefined ? typeOrPayload.body : payload;
this.stream.send('ch', {
id: id,
type: type,
body: body
});
}
public abstract dispose(): void;
}
class SharedConnection extends Connection {
private pool: Pool;
public get id(): string {
return this.pool.id;
}
constructor(stream: Stream, channel: string, pool: Pool) {
super(stream, channel);
this.pool = pool;
this.pool.inc();
}
@autobind
public send(typeOrPayload, payload?) {
super.send(this.pool.id, typeOrPayload, payload);
}
@autobind
public dispose() {
this.pool.dec();
this.removeAllListeners();
this.stream.removeSharedConnection(this);
}
}
class NonSharedConnection extends Connection {
public id: string;
protected params: any;
constructor(stream: Stream, channel: string, params?: any) {
super(stream, channel);
this.params = params;
this.id = Math.random().toString().substr(2, 8);
this.connect();
}
@autobind
public connect() {
this.stream.send('connect', {
channel: this.channel,
id: this.id,
params: this.params
});
}
@autobind
public send(typeOrPayload, payload?) {
super.send(this.id, typeOrPayload, payload);
}
@autobind
public dispose() {
this.removeAllListeners();
this.stream.send('disconnect', { id: this.id });
this.stream.disconnectToChannel(this);
}
}

View File

@ -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);
}
}
}
});
}
};

View File

@ -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>

View File

@ -1,114 +0,0 @@
<template>
<div>
<div ref="chart"></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import ApexCharts from 'apexcharts';
export default Vue.extend({
props: {
user: {
type: Object,
required: true
},
limit: {
type: Number,
required: false,
default: 21
}
},
data() {
return {
fetching: true,
data: [],
peak: null
};
},
mounted() {
this.$root.api('charts/user/notes', {
userId: this.user.id,
span: 'day',
limit: this.limit
}).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 < this.limit; 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]
]);
}
const 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: 0,
right: 8,
bottom: 0,
left: 8
}
},
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
}
}
});
chart.render();
});
}
});
</script>

View File

@ -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>

View File

@ -1,458 +0,0 @@
<template>
<div class="mk-autocomplete" @contextmenu.prevent="() => {}">
<ol class="users" ref="suggests" v-if="users.length > 0">
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
<img class="avatar" :src="user.avatarUrl" alt=""/>
<span class="name">
<mk-user-name :user="user" :key="user.id"/>
</span>
<span class="username">@{{ user | acct }}</span>
</li>
</ol>
<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
<span class="name">{{ hashtag }}</span>
</li>
</ol>
<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else-if="!useOsDefaultEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else>{{ emoji.emoji }}</span>
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
</li>
</ol>
</div>
</template>
<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';
type EmojiDef = {
emoji: string;
name: string;
aliasOf?: string;
url?: string;
isCustomEmoji?: boolean;
};
const lib = emojilist.filter(x => x.category !== 'flags');
const char2file = (char: string) => {
let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
codes = codes.filter(x => x && x.length);
return codes.join('-');
};
const emjdb: EmojiDef[] = lib.map(x => ({
emoji: x.char,
name: x.name,
aliasOf: null,
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
}));
for (const x of lib) {
if (x.keywords) {
for (const k of x.keywords) {
emjdb.push({
emoji: x.char,
name: k,
aliasOf: x.name,
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
});
}
}
}
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,
},
},
data() {
return {
getStaticImageUrl,
fetching: true,
users: [],
hashtags: [],
emojis: [],
select: -1,
emojilist,
emojiDb: [] as EmojiDef[]
}
},
computed: {
items(): HTMLCollection {
return (this.$refs.suggests as Element).children;
},
useOsDefaultEmojis(): boolean {
return this.$store.state.device.useOsDefaultEmojis;
}
},
updated() {
//#region 調
if (this.x + this.$el.offsetWidth > window.innerWidth) {
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
} else {
this.$el.style.left = this.x + 'px';
}
if (this.y + this.$el.offsetHeight > window.innerHeight) {
this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
this.$el.style.marginTop = '0';
} else {
this.$el.style.top = this.y + 'px';
this.$el.style.marginTop = 'calc(1em + 8px)';
}
//#endregion
},
mounted() {
//#region Construct Emoji DB
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) {
emojiDefinitions.push({
name: x.name,
emoji: `:${x.name}:`,
url: x.url,
isCustomEmoji: true
});
if (x.aliases) {
for (const alias of x.aliases) {
emojiDefinitions.push({
name: alias,
aliasOf: x.name,
emoji: `:${x.name}:`,
url: x.url,
isCustomEmoji: true
});
}
}
}
emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
this.emojiDb = emojiDefinitions.concat(emjdb);
//#endregion
this.textarea.addEventListener('keydown', this.onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', this.onMousedown);
}
this.$nextTick(() => {
this.exec();
this.$watch('q', () => {
this.$nextTick(() => {
this.exec();
});
});
});
},
beforeDestroy() {
this.textarea.removeEventListener('keydown', this.onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', this.onMousedown);
}
},
methods: {
exec() {
this.select = -1;
if (this.$refs.suggests) {
for (const el of Array.from(this.items)) {
el.removeAttribute('data-selected');
}
}
if (this.type == 'user') {
const cacheKey = `autocomplete:user:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const users = JSON.parse(cache);
this.users = users;
this.fetching = false;
} else {
this.$root.api('users/search', {
query: this.q,
limit: 10,
detail: false
}).then(users => {
this.users = users;
this.fetching = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
} else if (this.type == 'hashtag') {
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
} else {
const cacheKey = `autocomplete:hashtag:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
this.hashtags = hashtags;
this.fetching = false;
} else {
this.$root.api('hashtags/search', {
query: this.q,
limit: 30
}).then(hashtags => {
this.hashtags = hashtags;
this.fetching = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
});
}
}
} else if (this.type == 'emoji') {
if (this.q == null || this.q == '') {
this.emojis = this.emojiDb.filter(x => x.isCustomEmoji && !x.aliasOf).sort((a, b) => {
var textA = a.name.toUpperCase();
var textB = b.name.toUpperCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
});
return;
}
const matched = [];
const max = 30;
this.emojiDb.some(x => {
if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
if (matched.length < max) {
this.emojiDb.some(x => {
if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
}
if (matched.length < max) {
this.emojiDb.some(x => {
if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
}
this.emojis = matched;
}
},
onMousedown(e) {
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
},
onKeydown(e) {
const cancel = () => {
e.preventDefault();
e.stopPropagation();
};
switch (e.which) {
case 10: // [ENTER]
case 13: // [ENTER]
if (this.select !== -1) {
cancel();
(this.items[this.select] as any).click();
} else {
this.close();
}
break;
case 27: // [ESC]
cancel();
this.close();
break;
case 38: // []
if (this.select !== -1) {
cancel();
this.selectPrev();
} else {
this.close();
}
break;
case 9: // [TAB]
case 40: // []
cancel();
this.selectNext();
break;
default:
e.stopPropagation();
this.textarea.focus();
}
},
selectNext() {
if (++this.select >= this.items.length) this.select = 0;
this.applySelect();
},
selectPrev() {
if (--this.select < 0) this.select = this.items.length - 1;
this.applySelect();
},
applySelect() {
for (const el of Array.from(this.items)) {
el.removeAttribute('data-selected');
}
this.items[this.select].setAttribute('data-selected', 'true');
(this.items[this.select] as any).focus();
}
}
});
</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>

View File

@ -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>

View File

@ -1,36 +0,0 @@
<template>
<x-prism :inline="inline" :language="prismLang">{{ code }}</x-prism>
</template>
<script lang="ts">
import Vue from 'vue';
import 'prismjs';
import 'prismjs/themes/prism-okaidia.css';
import XPrism from 'vue-prism-component';
export default Vue.extend({
components: {
XPrism
},
props: {
code: {
type: String,
required: true
},
lang: {
type: String,
required: false
},
inline: {
type: Boolean,
required: false
}
},
computed: {
prismLang() {
return Prism.languages[this.lang] ? this.lang : 'js';
}
}
});
</script>

View File

@ -1,28 +0,0 @@
<template>
<x-code :code="code" :lang="lang" :inline="inline"/>
</template>
<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,
required: true
},
lang: {
type: String,
required: false
},
inline: {
type: Boolean,
required: false
}
}
});
</script>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,184 +0,0 @@
<template>
<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`">
<img
:src="file.url"
:alt="file.name"
:title="file.name"
@load="onThumbnailLoaded"
v-if="detail && is === 'image'"/>
<video
:src="file.url"
ref="volumectrl"
preload="metadata"
controls
v-else-if="detail && is === 'video'"/>
<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
<audio
:src="file.url"
ref="volumectrl"
preload="metadata"
controls
v-else-if="detail && is === 'audio'"/>
<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
<fa :icon="faFile" class="icon" v-else/>
<fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import anime from 'animejs';
import {
faFile,
faFileAlt,
faFileImage,
faMusic,
faFileVideo,
faFileCsv,
faFilePdf,
faFileArchive,
faFilm
} from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
props: {
file: {
type: Object,
required: true
},
fit: {
type: String,
required: false,
default: 'cover'
},
detail: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isContextmenuShowing: false,
isDragging: false,
faFile,
faFileAlt,
faFileImage,
faMusic,
faFileVideo,
faFileCsv,
faFilePdf,
faFileArchive,
faFilm
};
},
computed: {
is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
if (this.file.type.startsWith('image/')) return 'image';
if (this.file.type.startsWith('video/')) return 'video';
if (this.file.type === 'audio/midi') return 'midi';
if (this.file.type.startsWith('audio/')) return 'audio';
if (this.file.type.endsWith('/csv')) return 'csv';
if (this.file.type.endsWith('/pdf')) return 'pdf';
if (this.file.type.startsWith('text/')) return 'textfile';
if ([
"application/zip",
"application/x-cpio",
"application/x-bzip",
"application/x-bzip2",
"application/java-archive",
"application/x-rar-compressed",
"application/x-tar",
"application/gzip",
"application/x-7z-compressed"
].some(e => e === this.file.type)) return 'archive';
return 'unknown';
},
isThumbnailAvailable(): boolean {
return this.file.thumbnailUrl
? (this.is === 'image' || this.is === 'video')
: false;
},
background(): string {
return this.file.properties.avgColor || 'transparent';
}
},
mounted() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
},
methods: {
onThumbnailLoaded() {
if (this.file.properties.avgColor) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: 'transparent', // TODO fade
duration: 100,
easing: 'linear'
});
}
},
volumechange() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
}
}
});
</script>
<style lang="stylus" scoped>
.zdjebgpv
display flex
> img,
> .icon
pointer-events none
> .icon-sub
position absolute
width 30%
height auto
margin 0
right 4%
bottom 4%
> *
margin auto
&:not(.detail)
> img
height 100%
width 100%
object-fit cover
> .icon
height 65%
width 65%
> video,
> audio
width 100%
&.detail
> .icon
height 100px
width 100px
margin 16px
> *:not(.icon)
max-height 300px
max-width 100%
height 100%
object-fit contain
</style>

Some files were not shown because too many files have changed in this diff Show More